import type { ChangeFn, Doc } from '@automerge/automerge' import bs58check from 'bs58check' export type AutomergeUrl = string & { __documentUrl: true } export type DocumentId = string & { __documentId: true } export type Heads = string[] & { __automergeUrlHeads: unknown } export type StorageKey = string[] export interface DocumentLoadOptions { readonly priority?: 'interactive' | 'background' readonly signal?: AbortSignal } export interface StorageAdapterInterface { load(key: StorageKey): Promise save(key: StorageKey, data: Uint8Array): Promise saveBatch(entries: Array<[StorageKey, Uint8Array]>): Promise remove(key: StorageKey): Promise loadRange(prefix: StorageKey): Promise> removeRange(prefix: StorageKey): Promise } export interface ChangePayload { readonly handle: DocHandle readonly doc: Doc readonly message?: string readonly patches?: readonly unknown[] readonly patchInfo: { readonly before: Doc readonly after: Doc } } export interface DocHandle { readonly url: AutomergeUrl readonly documentId: string isReady(): boolean isDeleted(): boolean isUnloaded(): boolean doc(): Doc history(): readonly unknown[] metadata(change?: string): { message?: string | null; time?: number; actor?: string } | undefined change(callback: ChangeFn, options?: { message?: string }): void changeAt(heads: Heads, callback: ChangeFn, options?: { message?: string }): Heads | undefined update(callback: (doc: Doc) => Doc): void merge(other: DocHandle): void on(event: 'change', listener: (payload: ChangePayload) => void): void off(event: 'change', listener: (payload: ChangePayload) => void): void } export interface Repository { create(initial?: T): DocHandle /** Install/merge shared history at a reserved ID; never replace existing edits. */ import?(binary: Uint8Array, args?: { docId?: DocumentId }): DocHandle openReserved?(url: AutomergeUrl, genesis: Uint8Array): Promise> reservedDocumentUrl?(identity: Uint8Array): AutomergeUrl /** Classic's supported local load seam. Read before importing a reserved ID * so a detached document left by an interrupted initialization is not reset. */ readonly storageSubsystem?: { loadDoc(id: DocumentId): Promise | null | undefined> } readonly handles?: Record> find(url: AutomergeUrl): Promise> /** Optional demand/priority capability. Classic keeps its normal find seam. */ load?(url: AutomergeUrl, options?: DocumentLoadOptions): Promise> /** Background consumers may wait for initial workspace readiness. */ readonly backgroundReady?: Promise startBackgroundSync?(): void delete(url: AutomergeUrl): void | Promise flush(documentIds?: DocumentId[]): Promise /** Acknowledge local durability while retaining unpublished intent. May * schedule background publication, but never waits for or acknowledges it. * Edition runtimes may persist their whole partition rather than selected IDs. */ flushLocal?(documentIds?: DocumentId[]): Promise } export function encodeHeads(heads: readonly string[]): Heads { return heads.map(head => { if (!/^[0-9a-f]{64}$/.test(head)) throw new Error(`Invalid Automerge head: ${head}`) const bytes = new Uint8Array(head.length / 2) for (let index = 0; index < bytes.length; index += 1) { bytes[index] = Number.parseInt(head.slice(index * 2, index * 2 + 2), 16) } return bs58check.encode(bytes) }) as Heads } export function decodeHeads(heads: Heads): string[] { return heads.map(head => { let bytes: Uint8Array try { bytes = bs58check.decode(head) } catch { throw new Error(`Invalid encoded Automerge head: ${head}`) } if (bytes.length !== 32 || bs58check.encode(bytes) !== head) throw new Error(`Invalid encoded Automerge head: ${head}`) return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('') }) }