import {type EncodableObject, encode, type Encoded, type ID} from './encoder' import {SetSketch} from './reconciler' import type {SynchronizationRequest, SynchronizationResult} from './sync' /** * A set descriptor. This follows the very specific form with a property called * `keys` containing other descriptor IDs. * * @public */ export type EncodedSet = Encoded /** * SetSynchronization contains information about a set so that it can be * synchronized. * * @public */ export interface SetSynchronization { /** @internal */ set: EncodedSet /** @internal */ digest: Uint8Array /** @internal */ objectValues: Record> /** @internal */ setValues: Record> /** @internal */ sketch: SetSketch } export type SetBuilderOptions = { /** * See {@link encode} */ rewriteMap?: Map } /** * SetBuilder is a class which helps you construct a set for efficient synchronization. * * @public */ export class SetBuilder { private objectValues: Record> = {} private setValues: Record> = {} private keys: string[] = [] private sketch: SetSketch = new SetSketch(32, 8) private rewriteMap: Map | undefined constructor({rewriteMap}: SetBuilderOptions = {}) { this.rewriteMap = rewriteMap } /** * Add an object to the set. */ addObject(type: Type, obj: EncodableObject): void { const value = encode(type, obj, { withDigest: (digest) => { this.sketch.toggle(digest) }, rewriteMap: this.rewriteMap, }) this.objectValues[value.id] = value this.keys.push(value.id) } /** * Add another set to the set. */ addSet(sync: SetSynchronization): void { this.setValues[sync.set.id] = sync this.sketch.toggle(sync.digest) this.keys.push(sync.set.id) } build(type: Type): SetSynchronization { this.keys.sort() let digest: Uint8Array const set = encode( type, {keys: this.keys}, { withDigest: (d) => { digest = d }, }, ) return { set, digest: digest!, objectValues: this.objectValues, setValues: this.setValues, sketch: this.sketch, } } } /** * The main logic for synchronizing a set to a server. * * Initially this function should be invoked with `prevResult` set to `null`. * This returns a SynchronizationRequest which should then be sent to a server. * Once the server returns a result this function should be invoked with this * as a parameter. This proccess should be continued until this function return * `null`. * * @param sync The set to synchronize. * @param prevResult The result from the previous synchronization. * @returns `null` when the synchronization is complete, or a request which should be sent. * @public */ export function processSetSynchronization( sync: SetSynchronization, prevResult: SynchronizationResult | null, ): SynchronizationRequest | null { const id = sync.set.id if (!prevResult) return {id} if (prevResult.type === 'complete') return null const descriptors: Array> = [] for (const missingId of prevResult.missingIds) { const descriptor = findDescriptor(sync, missingId) if (!descriptor) throw new Error(`Synchronization server is requested an unknonwn descriptor`) descriptors.push(descriptor) } return {id, descriptors} } function findDescriptor( sync: SetSynchronization, id: ID, ): Encoded | null { if (sync.set.id === id) return sync.set const desc = sync.objectValues[id] if (desc) return desc for (const child of Object.values(sync.setValues)) { const childDesc = findDescriptor(child, id) if (childDesc) return childDesc } return null }