import { Slot } from '@revesuite/global/utils'; import { merge } from 'merge'; import type { Awareness as YAwareness } from 'y-protocols/awareness.js'; import type { Space } from './space.js'; import type { Store } from './store.js'; import { uuidv4 } from './utils/id-generator.js'; export interface UserRange { startOffset: number; endOffset: number; blockIds: string[]; } export interface UserInfo { id: number; name: string; color: string; } type Request< Flags extends Record = BlockSuiteFlags, Key extends keyof Flags = keyof Flags > = { id: string; clientId: number; field: Key; value: Flags[Key]; }; type Response = { id: string; }; // Raw JSON state in awareness CRDT export type RawAwarenessState< Flags extends Record = BlockSuiteFlags > = { rangeMap?: Record; user?: UserInfo; flags: Flags; request?: Request[]; response?: Response[]; }; interface AwarenessEvent< Flags extends Record = BlockSuiteFlags > { id: number; type: 'add' | 'update' | 'remove'; state?: RawAwarenessState; } export class AwarenessStore< Flags extends Record = BlockSuiteFlags > { readonly awareness: YAwareness>; readonly store: Store; readonly slots = { update: new Slot>(), }; constructor( store: Store, awareness: YAwareness>, defaultFlags: Flags ) { this.store = store; this.awareness = awareness; this.awareness.on('change', this._onAwarenessChange); this.slots.update.on(this._onAwarenessMessage); const upstreamFlags = awareness.getLocalState()?.flags; if (upstreamFlags) { this.awareness.setLocalStateField( 'flags', merge(true, defaultFlags, upstreamFlags) ); } else { this.awareness.setLocalStateField('flags', { ...defaultFlags }); } } setFlag(field: Key, value: Flags[Key]) { const oldFlags = this.awareness.getLocalState()?.flags ?? {}; this.awareness.setLocalStateField('flags', { ...oldFlags, [field]: value }); } getFlag(field: Key): Flags[Key] | undefined { const flags = this.awareness.getLocalState()?.flags ?? {}; return flags[field]; } setReadonly(space: Space, value: boolean): void { const flags = this.getFlag('readonly') ?? {}; this.setFlag('readonly', { ...flags, [space.prefixedId]: value, } as Flags['readonly']); } isReadonly(space: Space): boolean { const rd = this.getFlag('readonly'); if (rd && typeof rd === 'object') { return Boolean((rd as Record)[space.prefixedId]); } else { return false; } } setRemoteFlag( clientId: number, field: Key, value: Flags[Key] ) { if (!this.getFlag('enable_set_remote_flag')) { console.error('set remote flag feature disabled'); return; } const oldRequest = this.awareness.getLocalState()?.request ?? []; this.awareness.setLocalStateField('request', [ ...oldRequest, { id: uuidv4(), clientId, field, value, }, ] satisfies Request[]); } setLocalRange(space: Space, range: UserRange | null) { const rangeMap = this.awareness.getLocalState()?.rangeMap ?? {}; if (range === null) { delete rangeMap[space.prefixedId]; this.awareness.setLocalStateField('rangeMap', rangeMap); } else { this.awareness.setLocalStateField('rangeMap', { ...rangeMap, [space.prefixedId]: range, }); } } getLocalRange(space: Space): UserRange | undefined { return this.awareness.getLocalState()?.['rangeMap']?.[space.prefixedId]; } getStates(): Map> { return this.awareness.getStates(); } private _onAwarenessChange = (diff: { added: number[]; removed: number[]; updated: number[]; }) => { const { added, removed, updated } = diff; const states = this.awareness.getStates(); added.forEach(id => { this.slots.update.emit({ id, type: 'add', state: states.get(id) as RawAwarenessState, }); }); updated.forEach(id => { this.slots.update.emit({ id, type: 'update', state: states.get(id) as RawAwarenessState, }); }); removed.forEach(id => { this.slots.update.emit({ id, type: 'remove', }); }); }; private _onAwarenessMessage = (awMsg: AwarenessEvent) => { if (this.getFlag('enable_set_remote_flag') === true) { this._handleRemoteFlags(); } }; private _handleRemoteFlags() { const nextTick: (() => void)[] = []; const localState = this.awareness.getLocalState() as RawAwarenessState; const request = (localState?.request ?? []) as Request[]; const selfResponse = [] as Response[]; const fakeDirtyResponse = [] as Response[]; if (localState && Array.isArray(localState.response)) { selfResponse.push(...localState.response); fakeDirtyResponse.push(...localState.response); } const response = [] as Response[]; for (const [clientId, state] of this.awareness.getStates()) { if (clientId === this.awareness.clientID) { continue; } if (Array.isArray(state.response)) { response.push(...state.response); } if (Array.isArray(state.request)) { const remoteRequest = state.request as Request[]; selfResponse.forEach((response, idx) => { if (response === null) { return; } const index = remoteRequest.findIndex( request => request.id === response.id ); if (index === -1) { fakeDirtyResponse[idx].id = 'remove'; } }); remoteRequest.forEach(request => { if (request.clientId === this.awareness.clientID) { // handle request nextTick.push(() => { this.setFlag(request.field, request.value); }); selfResponse.push({ id: request.id, }); } }); } } response.forEach(response => { const idx = request.findIndex(request => request.id === response.id); if (idx !== -1) { request.splice(idx, 1); } }); nextTick.push(() => { this.awareness.setLocalStateField('request', request); this.awareness.setLocalStateField( 'response', selfResponse.filter((response, idx) => fakeDirtyResponse[idx] ? fakeDirtyResponse[idx].id !== 'remove' : true ) ); }); setTimeout(() => { nextTick.forEach(fn => fn()); }, 100); } destroy() { if (this.awareness) { this.awareness.off('change', this._onAwarenessChange); this.slots.update.dispose(); } } }