Something went wrong. Try again.
automerge-repo drop in replacement backed by Habitat "permissioned spaces" om your PDS
Something went wrong. Try again.
3.2 kB · 91 lines
TypeScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import { change as amChange, getHeads, load, merge, save, type Doc } from '@automerge/automerge'import type { DocumentId } from '../../document-id.js'import type { Source } from '../source.js'
/** * The application-facing adapter for a document — deliberately shaped like a * whole-doc handle so a client (an editor) gets a familiar `doc()`/`change()`/ * `on()` experience. */export interface Handle<T = unknown> { readonly url: DocumentId doc(): Doc<T> change(fn: (doc: T) => void, options?: { message?: string }): void on(event: 'change', listener: () => void): void off(event: 'change', listener: () => void): void}
function sameHeads(a: readonly string[], b: readonly string[]): boolean { return a.length === b.length && a.every((head, i) => head === b[i])}
/** * A live {@link Source} that is *also* the client-facing {@link Handle}. Source * and Handle have disjoint method names, so one object plays both roles: * * - The session treats it as a Source: `push` delivers the merged canonical in * (updating the client's view); `subscribe` lets the session receive the * client's local edits. * - The client treats it as a Handle: `change` applies a local edit — updating * this source's own doc and delivering it to the session — and `doc()`/`on()` * read and observe. * * Because `change` (client) and `push` (session) are distinct methods, there is * no loop and no source-tag filtering: a session push updates the view but is * never delivered back, and a client change is delivered but the session never * pushes it back (it skips the delivering source). */export function createEditorSource<T = unknown>(documentId: DocumentId, initialDoc: Doc<T>): Source & Handle<T> { // Own an independent copy — never share a doc object with the session's canonical. let doc = load<T>(save(initialDoc)) const listeners = new Set<() => void>() let deliver: ((documentId: DocumentId, doc: Doc<unknown>) => void) | undefined
const notify = (): void => { for (const listener of listeners) listener() }
return { // --- Source (session-facing) --- id: 'editor',
async push(_documentId: DocumentId, incoming: Doc<unknown>): Promise<void> { const own = load<T>(save(incoming)) const before = getHeads(doc) doc = merge(doc, own) if (!sameHeads(before, getHeads(doc))) notify() },
async pull(): Promise<Doc<unknown> | null> { return doc as Doc<unknown> },
subscribe(onInbound: (documentId: DocumentId, doc: Doc<unknown>) => void): () => void { deliver = onInbound return () => { if (deliver === onInbound) deliver = undefined } },
// --- Handle (client-facing) --- url: documentId,
doc(): Doc<T> { return doc },
change(fn: (doc: T) => void, options?: { message?: string }): void { doc = options?.message ? amChange(doc, { message: options.message }, fn) : amChange(doc, fn) notify() deliver?.(documentId, doc as Doc<unknown>) },
on(_event: 'change', listener: () => void): void { listeners.add(listener) },
off(_event: 'change', listener: () => void): void { listeners.delete(listener) }, }}