import * as A from '@automerge/automerge' import bs58check from 'bs58check' import type { AutomergeUrl, DocHandle, DocumentId, Repository } from './document-runtime-contract' import type { DirectoryEntry, PushworkDirectoryDocument } from '../types/pushwork' /** Versioned tuples, not concatenated names or mutable account handles. */ export async function reservedDigest(...parts: string[]): Promise { return new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(parts)))) } function hex(bytes: Uint8Array): string { return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('') } const openings = new WeakMap>>>() function canonicalInitial(initial: T): T { return JSON.parse(JSON.stringify(initial, (_key, value: unknown) => { if (value && typeof value === 'object' && !Array.isArray(value)) { return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) } return value })) as T } function assertSharedGenesis(document: A.Doc, genesis: A.Doc): void { const hash = A.getHeads(genesis)[0]! const roots = A.getAllChanges(document).map(bytes => A.decodeChange(bytes)).filter(change => change.deps.length === 0) if (roots.length !== 1 || roots[0]!.hash !== hash) { throw new Error('Reserved document genesis mismatch') } } /** The library import primitive loads with a fresh actor and merges shared * history. Genesis is independent of callers' defaults, clocks and devices. */ export async function reservedDocumentGenesis(scopeId: string, kind: string, initial: T): Promise<{ digest: Uint8Array; document: A.Doc }> { const digest = await reservedDigest('reams/internal/v1', scopeId, kind) const document = A.change(A.init({ actor: hex(digest) }), { time: 0 }, draft => Object.assign(draft as object, canonicalInitial(initial))) return { digest, document } } export async function openReservedDocument(repo: Repository, scopeId: string, kind: string, initial: T): Promise> { const { digest, document: genesis } = await reservedDocumentGenesis(scopeId, kind, initial) const url = repo.reservedDocumentUrl?.(digest.slice(0, 16)) ?? `automerge:${bs58check.encode(digest.slice(0, 16))}` as AutomergeUrl const id = url.slice('automerge:'.length) as DocumentId let pending = openings.get(repo) if (!pending) { pending = new Map(); openings.set(repo, pending) } const previous = pending.get(url) if (previous) { const opened = await previous assertSharedGenesis(opened.doc(), genesis) return opened as DocHandle } const opening = (async () => { if (repo.openReserved) return await repo.openReserved(url, A.save(genesis)) if (!repo.import) throw new Error('Repository does not support reserved document import') const cached = repo.handles?.[id] if (cached?.isDeleted()) throw new Error('Reserved document is deleted') if (cached?.isReady()) { assertSharedGenesis(cached.doc(), genesis) return cached as DocHandle } // Classic import treats a new handle as fresh and writes it immediately; // recover persisted detached history before that write can replace it. const stored = await repo.storageSubsystem?.loadDoc(id) if (stored) assertSharedGenesis(stored, genesis) const document = stored ? A.merge(A.load(A.save(stored)), genesis) : genesis repo.import(A.save(document), { docId: id }) return await repo.find(url) })() pending.set(url, opening) try { return await opening } finally { if (pending.get(url) === opening) pending.delete(url) } } /** Append exactly the same Automerge operation on independently initialized * clients. Both use the founding list's history and a per-link reserved actor; * ordinary device edits continue using the handle's independent actor. * This is an idempotent insertion, not projection-time deduplication. */ export async function linkReservedDocument(parent: DocHandle, entry: DirectoryEntry, validateChild: () => void = () => {}): Promise { entry = canonicalInitial(entry) const doc = parent.doc() assertFolderListUncontested(doc) const matches = doc.docs.filter(item => item.name === entry.name) if (matches.length) { if (matches.length === 1 && matches[0]!.url === entry.url && matches[0]!.type === entry.type) { validateChild(); return } throw new Error(`Ambiguous reserved link ${entry.name}`) } const listId = A.getObjectId(doc.docs) const founding = A.getAllChanges(doc).map(bytes => A.decodeChange(bytes)).find(change => change.ops.some((op, index) => op.action === 'makeList' && `${change.startOp + index}@${change.actor}` === listId), ) if (!founding) throw new Error('Reserved link requires a founding folder list') const actor = hex(await reservedDigest('reams/internal-link/v1', parent.url, founding.hash, JSON.stringify(entry))) const base = A.clone(A.view(doc, [founding.hash]), { actor }) const linked = A.change(base, { time: 0 }, draft => { draft.docs.push(entry) }) const change = A.getLastLocalChange(linked)! parent.update(current => { validateChild() assertFolderListUncontested(current) const occupied = current.docs.filter(item => item.name === entry.name) if (occupied.length) { if (occupied.length === 1 && occupied[0]!.url === entry.url && occupied[0]!.type === entry.type) return current throw new Error(`Ambiguous reserved link ${entry.name}`) } if (A.getObjectId(current.docs) !== listId) throw new Error('Folder list changed during reserved linking') const linked = A.applyChanges(A.clone(current, { actor: A.getActorId(current) }), [change])[0] const published = linked.docs.filter(item => item.name === entry.name) if (published.length !== 1 || published[0]!.url !== entry.url) { throw new Error('Reserved link was previously removed; explicit recovery is required') } return linked }) } export function assertFolderListUncontested(doc: A.Doc): void { if (!Array.isArray(doc.docs) || Object.keys(A.getConflicts(doc, 'docs') ?? {}).length > 1) { throw new Error('Conflicting or malformed folder list roots') } }