@korajs/store 1.0.0-beta.13 → 1.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  2. package/dist/adapters/better-sqlite3.js +1 -1
  3. package/dist/adapters/indexeddb.cjs.map +1 -1
  4. package/dist/adapters/indexeddb.js +4 -4
  5. package/dist/adapters/sqlite-wasm-worker.cjs +67 -15
  6. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  7. package/dist/adapters/sqlite-wasm-worker.js +65 -17
  8. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  10. package/dist/adapters/sqlite-wasm.js +5 -5
  11. package/dist/{backup-4NUPDEMB.js → backup-IHKVMUAM.js} +5 -5
  12. package/dist/{chunk-Y5LRH7KI.js → chunk-5AQSGMJV.js} +151 -21
  13. package/dist/chunk-5AQSGMJV.js.map +1 -0
  14. package/dist/{chunk-W5F2LWG6.js → chunk-5JMGMPUE.js} +2 -2
  15. package/dist/{chunk-HZTDBLDB.js → chunk-BILSA6XZ.js} +4 -3
  16. package/dist/{chunk-HZTDBLDB.js.map → chunk-BILSA6XZ.js.map} +1 -1
  17. package/dist/{chunk-QDUSJMFV.js → chunk-BQSIL6TT.js} +5 -5
  18. package/dist/{chunk-MCQKEHOD.js → chunk-KK3GBONL.js} +2 -2
  19. package/dist/{chunk-32EDKWMI.js → chunk-KTVNANUH.js} +2 -2
  20. package/dist/{chunk-SJP2HTYZ.js → chunk-QW4UU7NG.js} +2 -2
  21. package/dist/{chunk-DUKTWVZW.js → chunk-SRHDJF5B.js} +13 -2
  22. package/dist/{chunk-DUKTWVZW.js.map → chunk-SRHDJF5B.js.map} +1 -1
  23. package/dist/{chunk-P4V375FY.js → chunk-UC2LGFPV.js} +4 -4
  24. package/dist/index.cjs +203 -47
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +96 -6
  27. package/dist/index.d.ts +96 -6
  28. package/dist/index.js +55 -39
  29. package/dist/index.js.map +1 -1
  30. package/dist/internal.cjs +1 -0
  31. package/dist/internal.cjs.map +1 -1
  32. package/dist/internal.d.cts +2 -2
  33. package/dist/internal.d.ts +2 -2
  34. package/dist/internal.js +3 -3
  35. package/dist/{restore-FR7XIKT2.js → restore-MNDPR557.js} +4 -4
  36. package/dist/{sql-builder-C4u55jKW.d.cts → sql-builder-D0tC3sv9.d.cts} +8 -1
  37. package/dist/{sql-builder-CGckdBDL.d.ts → sql-builder-D5UMzHEo.d.ts} +8 -1
  38. package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
  39. package/dist/store-HNGX4KTT.js +16 -0
  40. package/package.json +2 -2
  41. package/dist/chunk-Y5LRH7KI.js.map +0 -1
  42. package/dist/sqlite-wasm-channel-MLHLKEED.js +0 -12
  43. package/dist/store-IMWKZLWB.js +0 -16
  44. /package/dist/{backup-4NUPDEMB.js.map → backup-IHKVMUAM.js.map} +0 -0
  45. /package/dist/{chunk-W5F2LWG6.js.map → chunk-5JMGMPUE.js.map} +0 -0
  46. /package/dist/{chunk-QDUSJMFV.js.map → chunk-BQSIL6TT.js.map} +0 -0
  47. /package/dist/{chunk-MCQKEHOD.js.map → chunk-KK3GBONL.js.map} +0 -0
  48. /package/dist/{chunk-32EDKWMI.js.map → chunk-KTVNANUH.js.map} +0 -0
  49. /package/dist/{chunk-SJP2HTYZ.js.map → chunk-QW4UU7NG.js.map} +0 -0
  50. /package/dist/{chunk-P4V375FY.js.map → chunk-UC2LGFPV.js.map} +0 -0
  51. /package/dist/{restore-FR7XIKT2.js.map → restore-MNDPR557.js.map} +0 -0
  52. /package/dist/{sqlite-wasm-channel-MLHLKEED.js.map → sqlite-wasm-channel-QCCNBYCU.js.map} +0 -0
  53. /package/dist/{store-IMWKZLWB.js.map → store-HNGX4KTT.js.map} +0 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/persistence/storage-persistence.ts","../src/reactivity/query-store.ts","../src/reactivity/assert-query-ready.ts","../src/reactivity/query-store-cache.ts","../src/richtext/create-richtext-controller.ts","../src/richtext/as-rich-text-sync-engine.ts","../src/blob/opfs-blob-store.ts","../src/blob/blob-chunking.ts","../src/blob/blob-transfer.ts","../src/blob/blob-manifest-transfer.ts","../src/blob/server-blob-callbacks.ts","../src/blob/blob-gc.ts","../src/blob/blob-chunk-transport.ts"],"sourcesContent":["import type { KoraEventEmitter } from '@korajs/core'\n\n/**\n * Durable-storage state of this origin, as far as Kora knows (NEW-STORE-4).\n * - `unknown`: not checked yet (the boot check runs in the background).\n * - `persisted`: the browser granted persistent storage; it will not evict this\n * origin's data under storage pressure.\n * - `best-effort`: storage may be evicted under pressure (the browser default).\n * - `unsupported`: the runtime has no StorageManager persistence API (Node,\n * older browsers, some WebViews).\n * - `error`: the check or request threw; `lastError` says why.\n */\nexport type StoragePersistenceState =\n\t| 'unknown'\n\t| 'persisted'\n\t| 'best-effort'\n\t| 'unsupported'\n\t| 'error'\n\n/** Snapshot returned by `app.storage.persistence.status()`. */\nexport interface StoragePersistenceStatus {\n\tstate: StoragePersistenceState\n\t/** True only when the browser reported persistent storage. */\n\tpersisted: boolean\n\t/** True once `persist()` was asked for (explicitly or by an automatic trigger). */\n\trequested: boolean\n\t/** Message of the last failure, when `state` is `error`. */\n\tlastError?: string\n}\n\n/** Why an automatic (background) persistence request was made. */\nexport type PersistenceRequestReason = 'explicit' | 'sign-in' | 'first-write' | 'installed-app'\n\n/** The subset of `navigator.storage` used here. */\nexport interface PersistenceStorageManager {\n\tpersist?: () => Promise<boolean>\n\tpersisted?: () => Promise<boolean>\n}\n\n/** Options for {@link StoragePersistence}. */\nexport interface StoragePersistenceOptions {\n\t/** Receives `storage:persistence` events. */\n\temitter?: KoraEventEmitter | null\n\t/**\n\t * The StorageManager to use. Defaults to `globalThis.navigator.storage`,\n\t * resolved on each call so tests and late polyfills are honoured.\n\t */\n\tstorage?: PersistenceStorageManager | null\n}\n\nfunction defaultStorageManager(): PersistenceStorageManager | null {\n\tconst nav = (globalThis as { navigator?: { storage?: PersistenceStorageManager } }).navigator\n\treturn nav?.storage ?? null\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error)\n}\n\n/**\n * Tracks and requests durable storage (`navigator.storage.persist()`) without ever\n * putting it on the startup path (NEW-STORE-4).\n *\n * In Firefox `persist()` opens a permission prompt and its promise stays pending\n * until the user answers, so awaiting it inside startup could hang `app.ready`\n * forever. Kora therefore:\n * - checks `persisted()` at boot, which never prompts, in the background;\n * - requests persistence only on an explicit `request()` or, without awaiting,\n * after a meaningful moment (sign-in, the first local write, running as an\n * installed app);\n * - surfaces every result through `status()` and the `storage:persistence` event,\n * never by blocking.\n */\nexport class StoragePersistence {\n\tprivate current: StoragePersistenceStatus = {\n\t\tstate: 'unknown',\n\t\tpersisted: false,\n\t\trequested: false,\n\t}\n\tprivate inFlightRequest: Promise<StoragePersistenceStatus> | null = null\n\tprivate readonly emitter: KoraEventEmitter | null\n\tprivate readonly storageOverride: PersistenceStorageManager | null | undefined\n\n\tconstructor(options: StoragePersistenceOptions = {}) {\n\t\tthis.emitter = options.emitter ?? null\n\t\tthis.storageOverride = options.storage\n\t}\n\n\t/** Synchronous snapshot of the last known state. Never prompts. */\n\tstatus(): StoragePersistenceStatus {\n\t\treturn { ...this.current }\n\t}\n\n\t/**\n\t * Read whether storage is already persistent (`navigator.storage.persisted()`).\n\t * Never prompts and never throws: a failure becomes `state: 'error'`.\n\t */\n\tasync check(): Promise<StoragePersistenceStatus> {\n\t\tconst storage = this.storage()\n\t\tif (!storage || typeof storage.persisted !== 'function') {\n\t\t\tthis.update({ state: 'unsupported', persisted: false })\n\t\t\tif (storage) this.emit('unsupported', false)\n\t\t\treturn this.status()\n\t\t}\n\t\ttry {\n\t\t\tconst persisted = (await storage.persisted()) === true\n\t\t\t// A grant observed by a concurrent request() is never downgraded here.\n\t\t\tif (!this.current.persisted) {\n\t\t\t\tthis.update({ state: persisted ? 'persisted' : 'best-effort', persisted })\n\t\t\t}\n\t\t\tthis.emit('checked', this.current.persisted)\n\t\t} catch (error) {\n\t\t\tthis.update({ state: 'error', persisted: false, lastError: errorMessage(error) })\n\t\t\tthis.emit('error', false, errorMessage(error))\n\t\t}\n\t\treturn this.status()\n\t}\n\n\t/**\n\t * Ask the browser for persistent storage (`navigator.storage.persist()`).\n\t * May show a permission prompt (Firefox), so call it from a user gesture or a\n\t * meaningful moment, and do not block rendering on it. Concurrent calls share\n\t * one request. Never throws: a failure becomes `state: 'error'`.\n\t *\n\t * @returns The state after the browser answered\n\t */\n\trequest(): Promise<StoragePersistenceStatus> {\n\t\tif (this.inFlightRequest) return this.inFlightRequest\n\t\tconst storage = this.storage()\n\t\tif (!storage || typeof storage.persist !== 'function') {\n\t\t\tthis.update({ state: 'unsupported', persisted: false })\n\t\t\tif (storage) this.emit('unsupported', false)\n\t\t\treturn Promise.resolve(this.status())\n\t\t}\n\t\tif (this.current.persisted) return Promise.resolve(this.status())\n\t\tconst persist = storage.persist.bind(storage)\n\t\tthis.current = { ...this.current, requested: true }\n\t\tconst pending = (async (): Promise<StoragePersistenceStatus> => {\n\t\t\ttry {\n\t\t\t\tconst persisted = (await persist()) === true\n\t\t\t\tthis.update({ state: persisted ? 'persisted' : 'best-effort', persisted })\n\t\t\t\tthis.emit('requested', persisted)\n\t\t\t} catch (error) {\n\t\t\t\tthis.update({ state: 'error', persisted: false, lastError: errorMessage(error) })\n\t\t\t\tthis.emit('error', false, errorMessage(error))\n\t\t\t} finally {\n\t\t\t\tthis.inFlightRequest = null\n\t\t\t}\n\t\t\treturn this.status()\n\t\t})()\n\t\tthis.inFlightRequest = pending\n\t\treturn pending\n\t}\n\n\t/**\n\t * Fire-and-forget `request()` for an automatic trigger. Never awaited by Kora,\n\t * so a pending prompt cannot block anything; the outcome arrives as a\n\t * `storage:persistence` event. Skipped when storage is already persistent or a\n\t * request was already made this session.\n\t *\n\t * @param _reason - Which trigger fired (kept for tracing at call sites)\n\t */\n\trequestInBackground(_reason: PersistenceRequestReason): void {\n\t\tif (this.current.persisted || this.current.requested) return\n\t\tvoid this.request()\n\t}\n\n\tprivate storage(): PersistenceStorageManager | null {\n\t\treturn this.storageOverride === undefined ? defaultStorageManager() : this.storageOverride\n\t}\n\n\tprivate update(next: Omit<StoragePersistenceStatus, 'requested'>): void {\n\t\tthis.current = { ...next, requested: this.current.requested }\n\t}\n\n\tprivate emit(\n\t\tstate: 'checked' | 'requested' | 'unsupported' | 'error',\n\t\tpersisted: boolean,\n\t\tmessage?: string,\n\t): void {\n\t\tthis.emitter?.emit({\n\t\t\ttype: 'storage:persistence',\n\t\t\tstate,\n\t\t\tpersisted,\n\t\t\t...(message !== undefined ? { message } : {}),\n\t\t})\n\t}\n}\n","import type { QueryBuilder } from '../query/query-builder'\nimport type { CollectionRecord } from '../types'\n\n/**\n * Frozen empty array returned as the initial snapshot before any data loads.\n * Same reference every time prevents infinite update loops in UI bindings.\n */\nconst EMPTY_ARRAY: readonly unknown[] = Object.freeze([])\n\n/**\n * Bridges the async QueryBuilder.subscribe() API with synchronous snapshot reads\n * required by React useSyncExternalStore, Vue composables, and Svelte stores.\n *\n * Uses lazy subscription: the underlying query subscription starts when the first\n * listener attaches and stops when the last listener detaches.\n */\nexport class QueryStore<T = CollectionRecord> {\n\tprivate snapshot: readonly T[] = EMPTY_ARRAY as readonly T[]\n\tprivate listeners = new Set<() => void>()\n\tprivate unsubscribeQuery: (() => void) | null = null\n\tprivate active = false\n\tprivate hasEmittedSnapshot = false\n\tprivate error: Error | null = null\n\tprivate readonly queryBuilder: QueryBuilder<T>\n\n\tconstructor(queryBuilder: QueryBuilder<T>) {\n\t\tthis.queryBuilder = queryBuilder\n\t}\n\n\t/**\n\t * Subscribe to snapshot changes.\n\t *\n\t * @returns Unsubscribe function\n\t */\n\tsubscribe = (onStoreChange: () => void): (() => void) => {\n\t\tthis.listeners.add(onStoreChange)\n\n\t\tif (!this.active) {\n\t\t\tthis.startSubscription()\n\t\t}\n\n\t\treturn () => {\n\t\t\tthis.listeners.delete(onStoreChange)\n\t\t\tif (this.listeners.size === 0) {\n\t\t\t\tthis.stopSubscription()\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Synchronous read of the latest query results. */\n\tgetSnapshot = (): readonly T[] => {\n\t\treturn this.snapshot\n\t}\n\n\t/**\n\t * Whether the underlying query subscription has delivered at least one\n\t * authoritative result for the current subscription lifecycle.\n\t */\n\thasSnapshot = (): boolean => {\n\t\treturn this.hasEmittedSnapshot\n\t}\n\n\t/**\n\t * The latest query failure, or null. Set when the query (or a re-run after a\n\t * write) fails; cleared by the next successful delivery. Listeners are notified\n\t * on both transitions, so bindings read it next to `getSnapshot()`.\n\t */\n\tgetError = (): Error | null => {\n\t\treturn this.error\n\t}\n\n\t/** Tear down listeners and the underlying query subscription. */\n\tdestroy(): void {\n\t\tthis.stopSubscription()\n\t\tthis.listeners.clear()\n\t\tthis.snapshot = EMPTY_ARRAY as readonly T[]\n\t\tthis.hasEmittedSnapshot = false\n\t\tthis.error = null\n\t}\n\n\tprivate startSubscription(): void {\n\t\tthis.active = true\n\t\tthis.unsubscribeQuery = this.queryBuilder.subscribe(\n\t\t\t(results: readonly T[]) => {\n\t\t\t\tif (!this.active) return\n\t\t\t\tthis.hasEmittedSnapshot = true\n\t\t\t\tthis.error = null\n\t\t\t\tthis.snapshot = Object.freeze([...results])\n\t\t\t\tthis.notifyListeners()\n\t\t\t},\n\t\t\t{\n\t\t\t\t// STORE-12: the failure reaches bindings as error state; the last\n\t\t\t\t// snapshot stays readable and the next success clears the error.\n\t\t\t\tonError: (failure) => {\n\t\t\t\t\tif (!this.active) return\n\t\t\t\t\tthis.error = failure.error\n\t\t\t\t\tthis.notifyListeners()\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t}\n\n\tprivate stopSubscription(): void {\n\t\tthis.active = false\n\t\tif (this.unsubscribeQuery) {\n\t\t\tthis.unsubscribeQuery()\n\t\t\tthis.unsubscribeQuery = null\n\t\t}\n\t}\n\n\tprivate notifyListeners(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener()\n\t\t}\n\t}\n}\n","import { AppNotReadyError } from '@korajs/core'\nimport type { QueryBuilder } from '../query/query-builder'\n\nconst LEGACY_PENDING_COLLECTION = '__pending__'\n\n/**\n * Ensures a query builder is backed by an open store (not a legacy pending placeholder).\n */\nexport function assertQueryReady(query: QueryBuilder<unknown>): void {\n\tconst descriptor = query.getDescriptor()\n\tif (descriptor.collection === LEGACY_PENDING_COLLECTION) {\n\t\tthrow new AppNotReadyError(\n\t\t\t'Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>.',\n\t\t)\n\t}\n}\n","import type { QueryBuilder } from '../query/query-builder'\nimport { queryKey } from '../query/query-key'\nimport type { CollectionRecord } from '../types'\nimport { QueryStore } from './query-store'\n\ninterface CacheEntry {\n\tqueryStore: QueryStore<CollectionRecord>\n\trefCount: number\n}\n\n/**\n * Reference-counted cache of {@link QueryStore} instances keyed by query descriptor.\n * Identical queries from different components share one underlying subscription.\n */\nexport class QueryStoreCache {\n\tprivate entries = new Map<string, CacheEntry>()\n\n\tconstructor(private readonly scopeKey = 'default') {}\n\n\tgetOrCreate<T>(queryBuilder: QueryBuilder<T>): QueryStore<T> {\n\t\tconst key = this.getKey(queryBuilder)\n\t\tconst existing = this.entries.get(key)\n\n\t\tif (existing) {\n\t\t\texisting.refCount++\n\t\t\treturn existing.queryStore as QueryStore<T>\n\t\t}\n\n\t\tconst queryStore = new QueryStore<T>(queryBuilder)\n\t\tthis.entries.set(key, {\n\t\t\tqueryStore: queryStore as QueryStore<CollectionRecord>,\n\t\t\trefCount: 1,\n\t\t})\n\t\treturn queryStore\n\t}\n\n\trelease(queryBuilder: QueryBuilder<unknown>): void {\n\t\tconst key = this.getKey(queryBuilder)\n\t\tconst entry = this.entries.get(key)\n\t\tif (!entry) {\n\t\t\treturn\n\t\t}\n\n\t\tentry.refCount--\n\t\tif (entry.refCount <= 0) {\n\t\t\tentry.queryStore.destroy()\n\t\t\tthis.entries.delete(key)\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tentry.queryStore.destroy()\n\t\t}\n\t\tthis.entries.clear()\n\t}\n\n\tget size(): number {\n\t\treturn this.entries.size\n\t}\n\n\tprivate getKey(queryBuilder: QueryBuilder<unknown>): string {\n\t\treturn `${this.scopeKey}:${queryKey(queryBuilder.getDescriptor())}`\n\t}\n}\n\nlet sharedCache: QueryStoreCache | null = null\n\n/**\n * Process-wide shared query store cache for framework bindings.\n *\n * @deprecated Prefer {@link KoraApp.getQueryStoreCache} via `KoraProvider` context\n * so each app instance owns an isolated cache.\n */\nexport function getSharedQueryStoreCache(): QueryStoreCache {\n\tif (!sharedCache) {\n\t\tsharedCache = new QueryStoreCache()\n\t}\n\treturn sharedCache\n}\n","import * as Y from 'yjs'\nimport { encodeRichtext } from '../serialization/richtext-serializer'\nimport type {\n\tCreateRichTextControllerOptions,\n\tRichTextAwarenessState,\n\tRichTextAwarenessUser,\n\tRichTextController,\n\tRichTextControllerSnapshot,\n\tRichTextCursorInfo,\n} from './types'\n\nconst LOAD_ORIGIN = 'kora-load'\nconst REMOTE_ORIGIN = 'kora-remote'\nconst DOC_CHANNEL_ORIGIN = 'kora-doc-channel'\nconst TEXT_KEY = 'content'\nconst COMPACT_AFTER_DELTAS = 20\nconst PERSIST_DEBOUNCE_MS = 400\n\n/**\n * Framework-agnostic richtext binding: Yjs document lifecycle, persistence,\n * incremental doc channel, and awareness cursors.\n */\nexport function createRichTextController(\n\toptions: CreateRichTextControllerOptions,\n): RichTextController {\n\tconst {\n\t\tcollection,\n\t\tcollectionName,\n\t\trecordId,\n\t\tfieldName,\n\t\tstore,\n\t\tsyncEngine = null,\n\t\tuseDocChannel,\n\t} = options\n\n\tconst doc = new Y.Doc()\n\tconst text = doc.getText(TEXT_KEY)\n\tconst undoManager = new Y.UndoManager(text)\n\n\tlet user = options.user\n\tlet disposed = false\n\tlet ready = false\n\tlet error: Error | null = null\n\tlet canUndo = false\n\tlet canRedo = false\n\tlet cursors: RichTextCursorInfo[] = []\n\n\tconst listeners = new Set<() => void>()\n\tlet snapshot: RichTextControllerSnapshot = {\n\t\tready: false,\n\t\terror: null,\n\t\tcanUndo: false,\n\t\tcanRedo: false,\n\t\tcursors: [],\n\t}\n\tconst baseUpdateRef = { current: null as Uint8Array | null }\n\tconst pendingDeltasRef = { current: [] as Uint8Array[] }\n\tconst docChannelActiveRef = { current: false }\n\tlet persistTimer: ReturnType<typeof setTimeout> | null = null\n\n\tlet recordUnsubscribe: (() => void) | null = null\n\tlet docChannelUnsubscribe: (() => void) | null = null\n\tlet awarenessUnsubscribe: (() => void) | null = null\n\n\tconst refreshSnapshot = (): void => {\n\t\tsnapshot = {\n\t\t\tready,\n\t\t\terror,\n\t\t\tcanUndo,\n\t\t\tcanRedo,\n\t\t\tcursors: [...cursors],\n\t\t}\n\t}\n\n\tconst notify = (): void => {\n\t\trefreshSnapshot()\n\t\tfor (const listener of listeners) {\n\t\t\tlistener()\n\t\t}\n\t}\n\n\tconst syncHistoryState = (): void => {\n\t\tcanUndo = undoManager.undoStack.length > 0\n\t\tcanRedo = undoManager.redoStack.length > 0\n\t\tnotify()\n\t}\n\n\tconst getSnapshot = (): RichTextControllerSnapshot => snapshot\n\n\tconst resolveAwarenessUser = (): RichTextAwarenessUser => {\n\t\tif (user) {\n\t\t\treturn user\n\t\t}\n\t\tif (syncEngine) {\n\t\t\tconst existing = syncEngine.getAwarenessManager().getLocalState()?.user\n\t\t\tif (existing) {\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tname: 'Anonymous',\n\t\t\tcolor: '#6366f1',\n\t\t}\n\t}\n\n\tconst setCursor = (anchor: number, head: number): void => {\n\t\tif (!syncEngine) {\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tconst state: RichTextAwarenessState = {\n\t\t\tuser: resolveAwarenessUser(),\n\t\t\tcursor: {\n\t\t\t\tcollection: collectionName,\n\t\t\t\trecordId,\n\t\t\t\tfield: fieldName,\n\t\t\t\tanchor,\n\t\t\t\thead,\n\t\t\t},\n\t\t}\n\t\tawareness.setLocalState(state)\n\t}\n\n\tconst clearCursor = (): void => {\n\t\tif (!syncEngine) {\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tconst current = awareness.getLocalState()\n\t\tif (!current?.cursor) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\tcurrent.cursor.collection !== collectionName ||\n\t\t\tcurrent.cursor.recordId !== recordId ||\n\t\t\tcurrent.cursor.field !== fieldName\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tawareness.setLocalState({ user: current.user })\n\t}\n\n\tconst applyRemoteSnapshot = (value: unknown): void => {\n\t\tconst encoded = encodeRichtextInput(value)\n\t\tif (!encoded) {\n\t\t\treturn\n\t\t}\n\n\t\tconst currentSnapshot = Y.encodeStateAsUpdate(doc)\n\t\tif (updatesEqual(currentSnapshot, encoded)) {\n\t\t\treturn\n\t\t}\n\n\t\tY.applyUpdate(doc, encoded, REMOTE_ORIGIN)\n\t\tbaseUpdateRef.current = encoded\n\t\tpendingDeltasRef.current = []\n\t\tsyncHistoryState()\n\t}\n\n\tconst flushPersist = async (): Promise<void> => {\n\t\tconst snapshot = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current)\n\t\tif (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {\n\t\t\tbaseUpdateRef.current = snapshot\n\t\t\tpendingDeltasRef.current = []\n\t\t}\n\n\t\ttry {\n\t\t\tawait collection.update(recordId, {\n\t\t\t\t[fieldName]: snapshot,\n\t\t\t})\n\t\t} catch (cause) {\n\t\t\tif (!disposed) {\n\t\t\t\terror = cause instanceof Error ? cause : new Error(String(cause))\n\t\t\t\tnotify()\n\t\t\t}\n\t\t}\n\t}\n\n\tconst schedulePersist = (): void => {\n\t\tif (persistTimer) {\n\t\t\tclearTimeout(persistTimer)\n\t\t}\n\t\tpersistTimer = setTimeout(() => {\n\t\t\tpersistTimer = null\n\t\t\tvoid flushPersist()\n\t\t}, PERSIST_DEBOUNCE_MS)\n\t}\n\n\tconst onDocUpdate = (update: Uint8Array, origin: unknown): void => {\n\t\tsyncHistoryState()\n\t\tif (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {\n\t\t\treturn\n\t\t}\n\n\t\tpendingDeltasRef.current.push(update)\n\n\t\tif (docChannelActiveRef.current && syncEngine) {\n\t\t\tsyncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update)\n\t\t\tschedulePersist()\n\t\t\treturn\n\t\t}\n\n\t\tvoid flushPersist()\n\t}\n\n\tconst subscribeRecordChanges = (): void => {\n\t\trecordUnsubscribe?.()\n\t\trecordUnsubscribe = store\n\t\t\t.collection(collectionName)\n\t\t\t.where({ id: recordId })\n\t\t\t.subscribe((results) => {\n\t\t\t\tconst record = results[0]\n\t\t\t\tif (!record) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tapplyRemoteSnapshot(record[fieldName])\n\t\t\t})\n\t}\n\n\tconst subscribeDocChannel = (): void => {\n\t\tdocChannelUnsubscribe?.()\n\t\tif (!ready || !syncEngine || !docChannelActiveRef.current) {\n\t\t\treturn\n\t\t}\n\n\t\tconst channel = syncEngine.getRichtextDocChannel?.()\n\t\tif (!channel) {\n\t\t\treturn\n\t\t}\n\n\t\tdocChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {\n\t\t\tY.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN)\n\t\t\tsyncHistoryState()\n\t\t})\n\t}\n\n\tconst updateCursors = (): void => {\n\t\tif (!syncEngine) {\n\t\t\tcursors = []\n\t\t\tnotify()\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tconst localClientId = awareness.clientId\n\t\tconst states = awareness.getStates()\n\t\tconst fieldCursors: RichTextCursorInfo[] = []\n\n\t\tfor (const [clientId, state] of states) {\n\t\t\tif (clientId === localClientId) continue\n\t\t\tif (!state.cursor) continue\n\t\t\tif (\n\t\t\t\tstate.cursor.collection !== collectionName ||\n\t\t\t\tstate.cursor.recordId !== recordId ||\n\t\t\t\tstate.cursor.field !== fieldName\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfieldCursors.push({\n\t\t\t\tclientId,\n\t\t\t\tuserName: state.user.name,\n\t\t\t\tcolor: state.user.color,\n\t\t\t\tanchor: state.cursor.anchor,\n\t\t\t\thead: state.cursor.head,\n\t\t\t})\n\t\t}\n\n\t\tcursors = fieldCursors\n\t\tnotify()\n\t}\n\n\tconst subscribeAwareness = (): void => {\n\t\tawarenessUnsubscribe?.()\n\t\tif (!syncEngine) {\n\t\t\tcursors = []\n\t\t\tnotify()\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tawarenessUnsubscribe = awareness.on('change', () => {\n\t\t\tupdateCursors()\n\t\t})\n\t\tupdateCursors()\n\t}\n\n\tconst initialize = async (): Promise<void> => {\n\t\tready = false\n\t\terror = null\n\t\tnotify()\n\n\t\ttry {\n\t\t\tconst record = await collection.findById(recordId)\n\t\t\tif (disposed) return\n\n\t\t\tdoc.transact(() => {\n\t\t\t\tconst target = doc.getText(TEXT_KEY)\n\t\t\t\ttarget.delete(0, target.length)\n\t\t\t}, LOAD_ORIGIN)\n\n\t\t\tconst encoded = encodeRichtextInput(record?.[fieldName])\n\t\t\tbaseUpdateRef.current = encoded\n\t\t\tpendingDeltasRef.current = []\n\t\t\tif (encoded) {\n\t\t\t\tY.applyUpdate(doc, encoded, LOAD_ORIGIN)\n\t\t\t}\n\n\t\t\tconst channel = syncEngine?.getRichtextDocChannel?.()\n\t\t\tdocChannelActiveRef.current =\n\t\t\t\tchannel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false\n\n\t\t\tready = true\n\t\t\tsyncHistoryState()\n\t\t\tsubscribeRecordChanges()\n\t\t\tsubscribeDocChannel()\n\t\t\tsubscribeAwareness()\n\t\t\tnotify()\n\t\t} catch (cause) {\n\t\t\tif (disposed) return\n\t\t\terror = cause instanceof Error ? cause : new Error(String(cause))\n\t\t\tnotify()\n\t\t}\n\t}\n\n\tdoc.on('update', onDocUpdate)\n\tvoid initialize()\n\n\treturn {\n\t\tdoc,\n\t\ttext,\n\t\tgetSnapshot,\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener)\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener)\n\t\t\t}\n\t\t},\n\t\tundo() {\n\t\t\tundoManager.undo()\n\t\t\tsyncHistoryState()\n\t\t},\n\t\tredo() {\n\t\t\tundoManager.redo()\n\t\t\tsyncHistoryState()\n\t\t},\n\t\tsetCursor,\n\t\tclearCursor,\n\t\tsetUser(nextUser: RichTextAwarenessUser | undefined) {\n\t\t\tuser = nextUser\n\t\t},\n\t\tdestroy() {\n\t\t\tif (disposed) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdisposed = true\n\t\t\tif (persistTimer) {\n\t\t\t\tclearTimeout(persistTimer)\n\t\t\t\tpersistTimer = null\n\t\t\t}\n\t\t\tdoc.off('update', onDocUpdate)\n\t\t\trecordUnsubscribe?.()\n\t\t\tdocChannelUnsubscribe?.()\n\t\t\tawarenessUnsubscribe?.()\n\t\t\tclearCursor()\n\t\t\tundoManager.destroy()\n\t\t\tbaseUpdateRef.current = null\n\t\t\tpendingDeltasRef.current = []\n\t\t\tdocChannelActiveRef.current = false\n\t\t\tlisteners.clear()\n\t\t},\n\t}\n}\n\nfunction encodeRichtextInput(value: unknown): Uint8Array | null {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\ttry {\n\t\treturn encodeRichtext(value as Parameters<typeof encodeRichtext>[0])\n\t} catch {\n\t\tif (typeof value === 'string') {\n\t\t\tconst fallbackDoc = new Y.Doc()\n\t\t\tfallbackDoc.getText(TEXT_KEY).insert(0, value)\n\t\t\treturn Y.encodeStateAsUpdate(fallbackDoc)\n\t\t}\n\t\tthrow new Error('Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.')\n\t}\n}\n\nfunction composeRichtextSnapshot(base: Uint8Array | null, deltas: Uint8Array[]): Uint8Array {\n\tconst mergedDoc = new Y.Doc()\n\tif (base) {\n\t\tY.applyUpdate(mergedDoc, base)\n\t}\n\n\tfor (const delta of deltas) {\n\t\tY.applyUpdate(mergedDoc, delta)\n\t}\n\n\treturn Y.encodeStateAsUpdate(mergedDoc)\n}\n\nfunction updatesEqual(left: Uint8Array, right: Uint8Array): boolean {\n\tif (left.length !== right.length) {\n\t\treturn false\n\t}\n\tfor (let index = 0; index < left.length; index++) {\n\t\tif (left[index] !== right[index]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n","import type { RichTextSyncEngine } from './types'\n\n/**\n * Narrows a sync engine reference to the richtext controller surface.\n * SyncEngine satisfies this interface at runtime; types live in different packages.\n */\nexport function asRichTextSyncEngine(engine: unknown): RichTextSyncEngine | null {\n\tif (engine == null) {\n\t\treturn null\n\t}\n\treturn engine as RichTextSyncEngine\n}\n","import { type BlobRef, type BlobRefMetadata, createBlobRef, hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * The minimal directory abstraction an {@link OpfsBlobStore} needs. Blobs are\n * laid out as `<shard>/<name>` (shard = first two hex chars of the hash) so a\n * single directory never holds millions of entries.\n *\n * The real backend ({@link createOpfsBlobDirectory}) is the browser Origin\n * Private File System; tests supply an in-memory implementation. Keeping this\n * port small means the content-addressed logic (hashing, integrity, dedup,\n * sharding) is fully testable without a browser.\n */\nexport interface OpfsBlobDirectory {\n\t/** Read the bytes at `<shard>/<name>`, or null if absent. */\n\tread(shard: string, name: string): Promise<Uint8Array | null>\n\t/**\n\t * Write bytes at `<shard>/<name>`, creating the shard directory as needed.\n\t * Must be atomic: a crash mid-write may not leave a partially-written file\n\t * that a later read would trust.\n\t */\n\twrite(shard: string, name: string, bytes: Uint8Array): Promise<void>\n\t/** Remove `<shard>/<name>`. Returns whether anything was removed. */\n\tremove(shard: string, name: string): Promise<boolean>\n\t/** Whether `<shard>/<name>` exists. */\n\thas(shard: string, name: string): Promise<boolean>\n\t/** Count of stored blobs across all shards (temp files excluded). */\n\tcount(): Promise<number>\n\t/** Every stored blob name (hash) across all shards (temp files excluded). */\n\tkeys(): Promise<string[]>\n}\n\n/**\n * A persistent {@link ContentAddressedBlobStore} for the browser, backed by the\n * Origin Private File System (OPFS) — the same durable storage the SQLite WASM\n * adapter uses. Blobs survive reloads, are sharded by hash prefix, deduplicated,\n * and integrity-verified on read (the returned bytes are checked to hash to the\n * requested key). Writes commit atomically so a torn write cannot surface as\n * trusted data.\n *\n * The store itself is environment-neutral: it talks to an {@link OpfsBlobDirectory}.\n * Use {@link createOpfsBlobStore} in a browser to get one backed by real OPFS.\n */\nexport class OpfsBlobStore implements ContentAddressedBlobStore {\n\tconstructor(private readonly dir: OpfsBlobDirectory) {}\n\n\tprivate shardFor(hash: string): string {\n\t\t// Two hex chars → 256 shards. Hashes are hex, so this is always well-formed.\n\t\treturn hash.slice(0, 2)\n\t}\n\n\tasync put(bytes: Uint8Array, metadata: BlobRefMetadata = {}): Promise<BlobRef> {\n\t\tconst ref = await createBlobRef(bytes, metadata)\n\t\tconst shard = this.shardFor(ref.hash)\n\n\t\t// Dedup: identical content already stored is a no-op.\n\t\tif (await this.dir.has(shard, ref.hash)) {\n\t\t\treturn ref\n\t\t}\n\n\t\t// Copy into an exactly-sized buffer so a subarray view writes its own bytes.\n\t\tconst out = new Uint8Array(bytes.byteLength)\n\t\tout.set(bytes)\n\t\tawait this.dir.write(shard, ref.hash, out)\n\t\treturn ref\n\t}\n\n\tasync get(hash: string): Promise<Uint8Array | null> {\n\t\tconst bytes = await this.dir.read(this.shardFor(hash), hash)\n\t\tif (bytes === null) {\n\t\t\treturn null\n\t\t}\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\treturn bytes\n\t}\n\n\tasync has(hash: string): Promise<boolean> {\n\t\treturn this.dir.has(this.shardFor(hash), hash)\n\t}\n\n\tasync delete(hash: string): Promise<boolean> {\n\t\treturn this.dir.remove(this.shardFor(hash), hash)\n\t}\n\n\tasync size(): Promise<number> {\n\t\treturn this.dir.count()\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\treturn this.dir.keys()\n\t}\n}\n\n// --- Real OPFS binding (browser only) ---\n\n/** Minimal structural views of the File System Access handles we use. */\ninterface OpfsWritableStream {\n\twrite(data: Uint8Array): Promise<void>\n\tclose(): Promise<void>\n}\ninterface OpfsFileHandle {\n\tcreateWritable(options?: { keepExistingData?: boolean }): Promise<OpfsWritableStream>\n\tgetFile(): Promise<{ arrayBuffer(): Promise<ArrayBuffer> }>\n}\ninterface OpfsDirectoryHandle {\n\tgetDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectoryHandle>\n\tgetFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileHandle>\n\tremoveEntry(name: string, options?: { recursive?: boolean }): Promise<void>\n\tkeys(): AsyncIterableIterator<string>\n}\n\ninterface StorageManagerLike {\n\tgetDirectory(): Promise<OpfsDirectoryHandle>\n}\n\nfunction getStorageManager(): StorageManagerLike {\n\tconst nav = (globalThis as unknown as { navigator?: { storage?: StorageManagerLike } }).navigator\n\tif (!nav?.storage || typeof nav.storage.getDirectory !== 'function') {\n\t\tthrow new Error(\n\t\t\t'OPFS is unavailable in this environment. The OPFS blob store requires a browser with Origin Private File System support; use MemoryBlobStore or a server-side store elsewhere.',\n\t\t)\n\t}\n\treturn nav.storage\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' &&\n\t\terror !== null &&\n\t\t(error as { name?: string }).name === 'NotFoundError'\n\t)\n}\n\n/**\n * Create an {@link OpfsBlobDirectory} backed by the browser's Origin Private\n * File System, rooted at a named subdirectory. It does not request persistent\n * storage: that is `app.storage.persistence`, kept off the startup path.\n *\n * @param rootDirName - The OPFS subdirectory to store blobs under (default `kora-blobs`)\n * @throws {Error} If OPFS is unavailable (e.g. called outside a browser)\n */\nexport async function createOpfsBlobDirectory(\n\trootDirName = 'kora-blobs',\n): Promise<OpfsBlobDirectory> {\n\tconst storage = getStorageManager()\n\t// NEW-STORE-4: no persist() here. In Firefox it opens a permission prompt that\n\t// stays pending until the user answers, and this runs inside app startup.\n\t// Durable storage is checked and requested by StoragePersistence\n\t// (app.storage.persistence), never on the startup path.\n\tconst opfsRoot = await storage.getDirectory()\n\tconst root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true })\n\n\tasync function shardHandle(shard: string, create: boolean): Promise<OpfsDirectoryHandle | null> {\n\t\ttry {\n\t\t\treturn await root.getDirectoryHandle(shard, { create })\n\t\t} catch (error) {\n\t\t\tif (isNotFoundError(error)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\treturn {\n\t\tasync read(shard, name) {\n\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\tif (!dir) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet handle: OpfsFileHandle\n\t\t\ttry {\n\t\t\t\thandle = await dir.getFileHandle(name, { create: false })\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tconst file = await handle.getFile()\n\t\t\treturn new Uint8Array(await file.arrayBuffer())\n\t\t},\n\t\tasync write(shard, name, bytes) {\n\t\t\tconst dir = await root.getDirectoryHandle(shard, { create: true })\n\t\t\tconst handle = await dir.getFileHandle(name, { create: true })\n\t\t\t// createWritable buffers to a swap file and only commits on close(), so\n\t\t\t// the target file is updated atomically. Combined with integrity-on-read,\n\t\t\t// a torn write is never trusted.\n\t\t\tconst writable = await handle.createWritable({ keepExistingData: false })\n\t\t\ttry {\n\t\t\t\tawait writable.write(bytes)\n\t\t\t} finally {\n\t\t\t\tawait writable.close()\n\t\t\t}\n\t\t},\n\t\tasync remove(shard, name) {\n\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\tif (!dir) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait dir.removeEntry(name)\n\t\t\t\treturn true\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t},\n\t\tasync has(shard, name) {\n\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\tif (!dir) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait dir.getFileHandle(name, { create: false })\n\t\t\t\treturn true\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t},\n\t\tasync count() {\n\t\t\tlet total = 0\n\t\t\tfor await (const shard of root.keys()) {\n\t\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\t\tif (!dir) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor await (const name of dir.keys()) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\ttotal++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn total\n\t\t},\n\t\tasync keys() {\n\t\t\tconst names: string[] = []\n\t\t\tfor await (const shard of root.keys()) {\n\t\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\t\tif (!dir) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor await (const name of dir.keys()) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\tnames.push(name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn names\n\t\t},\n\t}\n}\n\n/**\n * Create a persistent {@link OpfsBlobStore} backed by real browser OPFS.\n *\n * @param rootDirName - The OPFS subdirectory to store blobs under (default `kora-blobs`)\n * @throws {Error} If OPFS is unavailable (e.g. called outside a browser)\n */\nexport async function createOpfsBlobStore(rootDirName = 'kora-blobs'): Promise<OpfsBlobStore> {\n\treturn new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName))\n}\n","import { hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/** Default chunk size for out-of-band blob transfer (256 KiB). */\nexport const DEFAULT_CHUNK_SIZE = 256 * 1024\n\n/**\n * A manifest describing how a blob is split into content-addressed chunks.\n *\n * The manifest is small and travels with the operation stream (or alongside it);\n * the chunk bytes travel out of band. Because every chunk is content-addressed,\n * identical chunks across blobs are stored and transferred once, and a peer only\n * ever requests chunk hashes it does not already hold.\n */\nexport interface BlobManifest {\n\t/** Hex SHA-256 hash of the full reassembled blob (its content address). */\n\tblobHash: string\n\t/** Total size of the full blob in bytes. */\n\tsize: number\n\t/** The chunk size used to split the blob. */\n\tchunkSize: number\n\t/** Ordered hashes of each chunk. Repeats when identical chunks recur. */\n\tchunkHashes: string[]\n\tmimeType?: string\n\tfilename?: string\n}\n\n/** Optional metadata carried on a manifest. */\nexport interface BlobManifestMetadata {\n\tmimeType?: string\n\tfilename?: string\n}\n\n/**\n * Split a blob into content-addressed chunks and produce its manifest.\n *\n * @param bytes - The full blob content\n * @param chunkSize - Bytes per chunk (defaults to {@link DEFAULT_CHUNK_SIZE})\n * @param metadata - Optional mime type / filename to record on the manifest\n * @returns The manifest and a map of chunk hash to chunk bytes (deduplicated)\n */\nexport async function chunkBlob(\n\tbytes: Uint8Array,\n\tchunkSize: number = DEFAULT_CHUNK_SIZE,\n\tmetadata: BlobManifestMetadata = {},\n): Promise<{ manifest: BlobManifest; chunks: Map<string, Uint8Array> }> {\n\tif (chunkSize <= 0) {\n\t\tthrow new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`)\n\t}\n\n\tconst chunkHashes: string[] = []\n\tconst chunks = new Map<string, Uint8Array>()\n\n\tfor (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {\n\t\tconst end = Math.min(offset + chunkSize, bytes.byteLength)\n\t\tconst chunk = new Uint8Array(end - offset)\n\t\tchunk.set(bytes.subarray(offset, end))\n\t\tconst hash = await hashBlob(chunk)\n\t\tchunkHashes.push(hash)\n\t\t// Identical chunks collapse to one stored entry; the ordered hash list\n\t\t// still records every position so reassembly reproduces the exact bytes.\n\t\tif (!chunks.has(hash)) {\n\t\t\tchunks.set(hash, chunk)\n\t\t}\n\t}\n\n\tconst blobHash = await hashBlob(bytes)\n\tconst manifest: BlobManifest = {\n\t\tblobHash,\n\t\tsize: bytes.byteLength,\n\t\tchunkSize,\n\t\tchunkHashes,\n\t}\n\tif (metadata.mimeType !== undefined) {\n\t\tmanifest.mimeType = metadata.mimeType\n\t}\n\tif (metadata.filename !== undefined) {\n\t\tmanifest.filename = metadata.filename\n\t}\n\n\treturn { manifest, chunks }\n}\n\n/**\n * Reassemble a blob from chunks held in a content-addressed store, verifying\n * integrity at both levels: each chunk is verified by the store on read, and the\n * reassembled whole is verified against the manifest's blob hash.\n *\n * @param manifest - The blob manifest\n * @param chunkStore - A store holding the chunk bytes keyed by chunk hash\n * @returns The full reassembled blob bytes\n * @throws {BlobIntegrityError} If the reassembled bytes do not match blobHash\n * @throws {Error} If a chunk named in the manifest is missing from the store\n */\nexport async function reassembleBlob(\n\tmanifest: BlobManifest,\n\tchunkStore: ContentAddressedBlobStore,\n): Promise<Uint8Array> {\n\tconst parts: Uint8Array[] = []\n\tlet total = 0\n\n\tfor (const hash of manifest.chunkHashes) {\n\t\tconst chunk = await chunkStore.get(hash)\n\t\tif (chunk === null) {\n\t\t\tthrow new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`)\n\t\t}\n\t\tparts.push(chunk)\n\t\ttotal += chunk.byteLength\n\t}\n\n\tconst out = new Uint8Array(total)\n\tlet offset = 0\n\tfor (const part of parts) {\n\t\tout.set(part, offset)\n\t\toffset += part.byteLength\n\t}\n\n\tconst actual = await hashBlob(out)\n\tif (actual !== manifest.blobHash) {\n\t\tthrow new BlobIntegrityError(manifest.blobHash, actual)\n\t}\n\treturn out\n}\n","import { type BlobRef, hashBlob } from '@korajs/core'\nimport { type BlobManifest, chunkBlob, reassembleBlob } from './blob-chunking'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * A source of chunk bytes on the sending side of a transfer. In production this\n * is backed by the sync connection (the receiver requests a chunk hash, the peer\n * that holds the blob answers with the bytes); in tests it is backed by a map.\n */\nexport interface ChunkProvider {\n\t/** Fetch the bytes for a chunk hash, or null if the provider does not have it. */\n\tgetChunk(hash: string): Promise<Uint8Array | null>\n}\n\n/** Result of receiving a blob out of band. */\nexport interface ReceiveBlobResult {\n\t/** The content-addressed reference to the reassembled blob. */\n\tref: BlobRef\n\t/** Number of chunks actually fetched from the provider. */\n\tchunksFetched: number\n\t/** Number of chunks skipped because they were already held locally. */\n\tchunksSkipped: number\n}\n\n/** Stores used while receiving a blob. */\nexport interface ReceiveBlobStores {\n\t/**\n\t * Where fetched chunks are staged, keyed by chunk hash. A persistent chunk\n\t * store makes a transfer resumable: an interrupted transfer leaves its chunks\n\t * here, and a resumed transfer skips them.\n\t */\n\tchunkStore: ContentAddressedBlobStore\n\t/** Where the fully reassembled, verified blob is written. */\n\tblobStore: ContentAddressedBlobStore\n}\n\n/**\n * Receive a blob out of band, chunk by chunk, resumably and with integrity\n * verification at every step.\n *\n * For each chunk named in the manifest: if the chunk store already holds it (a\n * prior, possibly interrupted transfer), it is skipped; otherwise it is fetched\n * from the provider and verified to hash to the expected chunk hash before being\n * staged. Once every chunk is present, the blob is reassembled, verified against\n * the manifest's blob hash, and written to the blob store (which deduplicates).\n *\n * The transfer is idempotent: running it again after completion fetches nothing\n * and returns the same reference.\n *\n * @param manifest - The manifest of the blob to receive\n * @param provider - The source of chunk bytes\n * @param stores - The chunk staging store and the destination blob store\n * @returns The reference plus how many chunks were fetched versus skipped\n * @throws {BlobIntegrityError} If a fetched chunk or the whole blob fails verification\n * @throws {Error} If the provider cannot supply a required chunk\n */\nexport async function receiveBlob(\n\tmanifest: BlobManifest,\n\tprovider: ChunkProvider,\n\tstores: ReceiveBlobStores,\n): Promise<ReceiveBlobResult> {\n\tlet chunksFetched = 0\n\tlet chunksSkipped = 0\n\n\t// Distinct chunk hashes only: a manifest may repeat a hash, but we need each\n\t// unique chunk exactly once. Skipping duplicates also means a blob made of\n\t// identical chunks transfers that chunk a single time.\n\tconst seen = new Set<string>()\n\tfor (const hash of manifest.chunkHashes) {\n\t\tif (seen.has(hash)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(hash)\n\n\t\tif (await stores.chunkStore.has(hash)) {\n\t\t\tchunksSkipped++\n\t\t\tcontinue\n\t\t}\n\n\t\tconst bytes = await provider.getChunk(hash)\n\t\tif (bytes === null) {\n\t\t\tthrow new Error(`Blob transfer failed: provider could not supply chunk ${hash}`)\n\t\t}\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\tawait stores.chunkStore.put(bytes)\n\t\tchunksFetched++\n\t}\n\n\tconst full = await reassembleBlob(manifest, stores.chunkStore)\n\tconst ref = await stores.blobStore.put(full, {\n\t\tmimeType: manifest.mimeType,\n\t\tfilename: manifest.filename,\n\t})\n\n\treturn { ref, chunksFetched, chunksSkipped }\n}\n\n/**\n * Prepare a blob for out-of-band sending: split it into chunks, stage those\n * chunks in a content-addressed store, and return the manifest plus a\n * {@link ChunkProvider} that serves the chunks from that store.\n *\n * @param bytes - The full blob content\n * @param chunkStore - Where to stage the chunks (served to receivers)\n * @param options - Optional chunk size and metadata\n * @returns The manifest and a provider that serves the staged chunks\n */\nexport async function prepareBlobForSend(\n\tbytes: Uint8Array,\n\tchunkStore: ContentAddressedBlobStore,\n\toptions: { chunkSize?: number; mimeType?: string; filename?: string } = {},\n): Promise<{ manifest: BlobManifest; provider: ChunkProvider }> {\n\tconst { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {\n\t\tmimeType: options.mimeType,\n\t\tfilename: options.filename,\n\t})\n\tfor (const chunk of chunks.values()) {\n\t\tawait chunkStore.put(chunk)\n\t}\n\tconst provider: ChunkProvider = {\n\t\tgetChunk: (hash) => chunkStore.get(hash),\n\t}\n\treturn { manifest, provider }\n}\n","import { type BlobRef, type BlobRefMetadata, hashBlob } from '@korajs/core'\nimport { type BlobManifest, chunkBlob } from './blob-chunking'\nimport type { ChunkProvider } from './blob-transfer'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * Serialize a {@link BlobManifest} to canonical bytes for content addressing.\n *\n * Fields are emitted in a fixed order so the same manifest always produces the\n * same bytes, which is what lets the manifest be stored and fetched under its own\n * SHA-256 hash. Optional fields are omitted when absent.\n */\nexport function serializeBlobManifest(manifest: BlobManifest): Uint8Array {\n\tconst canonical: Record<string, unknown> = {\n\t\tblobHash: manifest.blobHash,\n\t\tsize: manifest.size,\n\t\tchunkSize: manifest.chunkSize,\n\t\tchunkHashes: manifest.chunkHashes,\n\t}\n\tif (manifest.mimeType !== undefined) {\n\t\tcanonical.mimeType = manifest.mimeType\n\t}\n\tif (manifest.filename !== undefined) {\n\t\tcanonical.filename = manifest.filename\n\t}\n\treturn new TextEncoder().encode(JSON.stringify(canonical))\n}\n\n/**\n * Parse canonical manifest bytes back into a {@link BlobManifest}, validating the\n * shape. Throws on malformed input rather than returning a partial manifest.\n */\nexport function parseBlobManifest(bytes: Uint8Array): BlobManifest {\n\tconst parsed: unknown = JSON.parse(new TextDecoder().decode(bytes))\n\tif (typeof parsed !== 'object' || parsed === null) {\n\t\tthrow new Error('Invalid blob manifest: not an object')\n\t}\n\tconst obj = parsed as Record<string, unknown>\n\tif (typeof obj.blobHash !== 'string') {\n\t\tthrow new Error('Invalid blob manifest: missing blobHash')\n\t}\n\tif (typeof obj.size !== 'number' || typeof obj.chunkSize !== 'number') {\n\t\tthrow new Error('Invalid blob manifest: missing size or chunkSize')\n\t}\n\tif (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== 'string')) {\n\t\tthrow new Error('Invalid blob manifest: chunkHashes must be an array of strings')\n\t}\n\tconst manifest: BlobManifest = {\n\t\tblobHash: obj.blobHash,\n\t\tsize: obj.size,\n\t\tchunkSize: obj.chunkSize,\n\t\tchunkHashes: obj.chunkHashes as string[],\n\t}\n\tif (typeof obj.mimeType === 'string') {\n\t\tmanifest.mimeType = obj.mimeType\n\t}\n\tif (typeof obj.filename === 'string') {\n\t\tmanifest.filename = obj.filename\n\t}\n\treturn manifest\n}\n\n/**\n * Store a blob for out-of-band transfer, addressed entirely by content.\n *\n * Stages the blob's chunks (so this device can serve them), stores the full blob\n * (so local reads by hash are O(1)), and stores the blob's manifest as its own\n * content-addressed object. The returned {@link BlobRef} carries `manifestHash`,\n * so a peer that receives the reference in a synced record can pull the bytes\n * knowing only the reference — no separate manifest hand-off.\n *\n * @param store - The content-addressed store to stage chunks, blob, and manifest in\n * @param bytes - The full blob content\n * @param options - Optional chunk size and metadata\n * @returns The reference (including `manifestHash`) and the manifest\n */\nexport async function putBlobForTransfer(\n\tstore: ContentAddressedBlobStore,\n\tbytes: Uint8Array,\n\toptions: { chunkSize?: number } & BlobRefMetadata = {},\n): Promise<{ ref: BlobRef; manifest: BlobManifest }> {\n\tconst { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {\n\t\t...(options.mimeType !== undefined ? { mimeType: options.mimeType } : {}),\n\t\t...(options.filename !== undefined ? { filename: options.filename } : {}),\n\t})\n\tfor (const chunk of chunks.values()) {\n\t\tawait store.put(chunk)\n\t}\n\t// Full blob for local O(1) reads by content hash.\n\tconst baseRef = await store.put(bytes, {\n\t\t...(options.mimeType !== undefined ? { mimeType: options.mimeType } : {}),\n\t\t...(options.filename !== undefined ? { filename: options.filename } : {}),\n\t})\n\t// Manifest as its own content-addressed object, served over the same channel.\n\tconst manifestRef = await store.put(serializeBlobManifest(manifest))\n\treturn { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest }\n}\n\n/**\n * Fetch and integrity-verify a blob's manifest from a {@link ChunkProvider}.\n *\n * The manifest is a content-addressed object like a chunk, so it is requested by\n * its hash, verified to hash to that value, and parsed. Used to resolve a\n * {@link BlobRef}'s manifest before pulling the blob's chunks.\n *\n * @param provider - The source to fetch the manifest bytes from\n * @param manifestHash - The manifest's content hash (from `BlobRef.manifestHash`)\n * @throws {BlobIntegrityError} If the fetched bytes do not hash to `manifestHash`\n * @throws {Error} If the provider cannot supply the manifest\n */\nexport async function fetchBlobManifest(\n\tprovider: ChunkProvider,\n\tmanifestHash: string,\n): Promise<BlobManifest> {\n\tconst bytes = await provider.getChunk(manifestHash)\n\tif (bytes === null) {\n\t\tthrow new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`)\n\t}\n\tconst actual = await hashBlob(bytes)\n\tif (actual !== manifestHash) {\n\t\tthrow new BlobIntegrityError(manifestHash, actual)\n\t}\n\treturn parseBlobManifest(bytes)\n}\n\n/**\n * Resolve the manifest for a {@link BlobRef} that was stored via\n * {@link putBlobForTransfer}. Fetches and verifies the manifest by the ref's\n * `manifestHash`.\n *\n * @throws {Error} If the ref has no `manifestHash` (it was not stored for transfer)\n */\nexport async function resolveBlobManifest(\n\tprovider: ChunkProvider,\n\tref: BlobRef,\n): Promise<BlobManifest> {\n\tif (ref.manifestHash === undefined) {\n\t\tthrow new Error(\n\t\t\t`Blob ${ref.hash} has no manifest to resolve. Store it with putBlobForTransfer (or app.blobs.put) so its reference carries a manifestHash, or pull with an explicit manifest.`,\n\t\t)\n\t}\n\treturn fetchBlobManifest(provider, ref.manifestHash)\n}\n","import { hashBlob } from '@korajs/core'\nimport { type ContentAddressedBlobStore, MemoryBlobStore } from './content-addressed-blob-store'\n\n/**\n * The server-side blob callbacks a `KoraSyncServer` accepts: read a chunk by\n * hash (to serve it) and persist an uploaded chunk by hash (to store it).\n */\nexport interface ServerBlobCallbacks {\n\tresolveBlobChunk: (hash: string) => Promise<Uint8Array | null>\n\tpersistBlobChunk: (hash: string, bytes: Uint8Array) => Promise<void>\n}\n\n/**\n * Adapt a {@link ContentAddressedBlobStore} into the read/persist callbacks a\n * `KoraSyncServer` needs for central blob storage. The server package depends\n * only on these plain function signatures, so a self-hosted server can back its\n * blob storage with any store (for example a `FilesystemBlobStore`) without the\n * server package taking a dependency on `@korajs/store`.\n *\n * @example\n * ```typescript\n * const server = new KoraSyncServer({\n * store,\n * ...toServerBlobCallbacks(new FilesystemBlobStore('/var/kora/blobs')),\n * })\n * ```\n */\nexport function toServerBlobCallbacks(store: ContentAddressedBlobStore): ServerBlobCallbacks {\n\treturn {\n\t\tresolveBlobChunk: (hash) => store.get(hash),\n\t\tasync persistBlobChunk(hash, bytes) {\n\t\t\t// The store keys by the hash it computes from the bytes; verify it matches\n\t\t\t// the declared hash so a mismatched upload is rejected, not silently stored\n\t\t\t// under the wrong key.\n\t\t\tconst actual = await hashBlob(bytes)\n\t\t\tif (actual !== hash) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tawait store.put(bytes)\n\t\t},\n\t}\n}\n\n/**\n * Convenience: an in-memory central blob store plus its server callbacks. For\n * tests and single-process deployments. Persistent deployments should back\n * {@link toServerBlobCallbacks} with a durable store instead.\n */\nexport function createMemoryServerBlobStore(): {\n\tstore: MemoryBlobStore\n\tcallbacks: ServerBlobCallbacks\n} {\n\tconst store = new MemoryBlobStore()\n\treturn { store, callbacks: toServerBlobCallbacks(store) }\n}\n","import { type BlobRef, isBlobRef } from '@korajs/core'\nimport { parseBlobManifest } from './blob-manifest-transfer'\nimport type { ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/** Outcome of a blob garbage-collection sweep. */\nexport interface BlobGcResult {\n\t/** Total distinct blobs held before the sweep. */\n\tscanned: number\n\t/** Distinct hashes kept because a live reference points to them (blob, manifest, or chunk). */\n\tlive: number\n\t/** Number of blobs deleted (or, with `dryRun`, that would be deleted). */\n\tcollected: number\n\t/** The hashes deleted (or, with `dryRun`, that would be deleted). */\n\tcollectedHashes: string[]\n}\n\n/** Options for {@link collectBlobGarbage}. */\nexport interface BlobGcOptions {\n\t/** Compute what would be collected without deleting anything. */\n\tdryRun?: boolean\n}\n\n/**\n * Extract the {@link BlobRef}s referenced by a materialized record. Scans the\n * record's top-level field values, so it works without the schema; a `blob`\n * field simply holds a `BlobRef`.\n */\nexport function extractBlobRefs(record: Record<string, unknown>): BlobRef[] {\n\tconst refs: BlobRef[] = []\n\tfor (const value of Object.values(record)) {\n\t\tif (isBlobRef(value)) {\n\t\t\trefs.push(value)\n\t\t}\n\t}\n\treturn refs\n}\n\n/**\n * Garbage-collect a content-addressed blob store by mark-and-sweep: keep every\n * blob reachable from a live reference, delete the rest.\n *\n * The live set is closed over the reference graph — for each live {@link BlobRef}\n * it retains the blob hash, the manifest hash, and every chunk hash named in that\n * manifest. Because chunks (and blobs) are shared by content address, a chunk\n * referenced by any surviving blob is retained even if another blob that used it\n * was collected. This makes GC safe under deduplication.\n *\n * Mark-and-sweep (rather than reference counting) is deliberate: counts are\n * fragile under concurrent edits and CRDT merges, whereas a sweep against the\n * current live set is always correct for the state it observes. Run it\n * periodically or on demand.\n *\n * A manifest that cannot be read or parsed (corruption) contributes no chunk\n * hashes; its own hash still counts as live. The blob it describes is already\n * unusable, so its chunks becoming collectable is acceptable, and GC never\n * throws on a single bad manifest.\n *\n * @param store - The blob store to sweep\n * @param liveRefs - Every reference still reachable from live records\n * @param options - `dryRun` to report without deleting\n */\nexport async function collectBlobGarbage(\n\tstore: ContentAddressedBlobStore,\n\tliveRefs: Iterable<BlobRef>,\n\toptions: BlobGcOptions = {},\n): Promise<BlobGcResult> {\n\tconst live = new Set<string>()\n\tfor (const ref of liveRefs) {\n\t\tlive.add(ref.hash)\n\t\tif (ref.manifestHash === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tlive.add(ref.manifestHash)\n\t\ttry {\n\t\t\tconst manifestBytes = await store.get(ref.manifestHash)\n\t\t\tif (manifestBytes !== null) {\n\t\t\t\tconst manifest = parseBlobManifest(manifestBytes)\n\t\t\t\tfor (const chunkHash of manifest.chunkHashes) {\n\t\t\t\t\tlive.add(chunkHash)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// A corrupt/unreadable manifest contributes no chunk hashes; the blob is\n\t\t\t// already broken. Never abort the whole sweep for one bad manifest.\n\t\t}\n\t}\n\n\tconst all = await store.list()\n\tconst collectedHashes: string[] = []\n\tfor (const hash of all) {\n\t\tif (live.has(hash)) {\n\t\t\tcontinue\n\t\t}\n\t\tcollectedHashes.push(hash)\n\t\tif (!options.dryRun) {\n\t\t\tawait store.delete(hash)\n\t\t}\n\t}\n\n\treturn {\n\t\tscanned: all.length,\n\t\tlive: live.size,\n\t\tcollected: collectedHashes.length,\n\t\tcollectedHashes,\n\t}\n}\n","import { generateUUIDv7 } from '@korajs/core'\nimport type { ChunkProvider } from './blob-transfer'\nimport type { ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * Blob chunk exchange messages. These ride any message transport (the sync\n * WebSocket in production, an in-memory pair in tests). A receiver requests a\n * chunk by hash and correlates the answer by `requestId`.\n */\nexport interface ChunkRequestMessage {\n\ttype: 'blob-chunk-request'\n\trequestId: string\n\thash: string\n}\nexport interface ChunkResponseMessage {\n\ttype: 'blob-chunk-response'\n\trequestId: string\n\t/** The chunk bytes, or null when the responder does not hold that hash. */\n\tbytes: Uint8Array | null\n\t/**\n\t * The server refused the request for rate, not because the chunk is missing\n\t * (RT-24). The requester retries after `retryAfterMs` instead of failing.\n\t */\n\tthrottled?: boolean\n\t/** With `throttled`: how long to wait before retrying, in ms. */\n\tretryAfterMs?: number\n}\nexport type ChunkMessage = ChunkRequestMessage | ChunkResponseMessage\n\n/**\n * A bidirectional message port carrying {@link ChunkMessage}s. Implemented over\n * the sync connection in production; `createChunkPortPair` provides an in-memory\n * duplex pair for tests.\n */\nexport interface ChunkMessagePort {\n\tsend(message: ChunkMessage): void\n\t/** Register a handler for incoming messages. Multiple handlers are allowed. */\n\tonMessage(handler: (message: ChunkMessage) => void): void\n}\n\n/** A {@link ChunkProvider} that fetches chunks over a message port. */\nexport interface RemoteChunkProvider extends ChunkProvider {\n\t/** Number of requests still awaiting a response (for diagnostics/tests). */\n\tpendingCount(): number\n}\n\n/** Options for {@link createRemoteChunkProvider}. */\nexport interface RemoteChunkProviderOptions {\n\t/** Per-attempt timeout in ms (default 30s). */\n\ttimeoutMs?: number\n\t/**\n\t * Longest total time one chunk may spend waiting on \"throttled\" answers before\n\t * the request fails (default 10 minutes). The transfer is resumable, so a failed\n\t * request can simply be retried later.\n\t */\n\tmaxThrottleWaitMs?: number\n\t/** Smallest back-off after a throttled answer, in ms (default 250). */\n\tminThrottleDelayMs?: number\n\t/** Largest single back-off after a throttled answer, in ms (default 30s). */\n\tmaxThrottleDelayMs?: number\n}\n\n/**\n * Create a {@link ChunkProvider} that requests chunks over a message port,\n * correlating each answer to its request and timing out a stalled request so a\n * dropped response cannot hang a transfer forever (the transfer is resumable, so\n * a timed-out request can simply be retried).\n *\n * A \"throttled\" answer (the server's blob request budget is spent, RT-24) is not a\n * failure: the request is re-sent after the server's `retryAfterMs` (bounded, with\n * exponential back-off when absent), until `maxThrottleWaitMs` has elapsed.\n *\n * @param port - The message port to the peer that holds the blob\n * @param options - Timeouts and throttle back-off bounds\n */\nexport function createRemoteChunkProvider(\n\tport: ChunkMessagePort,\n\toptions: RemoteChunkProviderOptions = {},\n): RemoteChunkProvider {\n\tconst timeoutMs = options.timeoutMs ?? 30_000\n\tconst maxThrottleWaitMs = options.maxThrottleWaitMs ?? 10 * 60_000\n\tconst minDelayMs = options.minThrottleDelayMs ?? 250\n\tconst maxDelayMs = options.maxThrottleDelayMs ?? 30_000\n\tconst pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\thash: string\n\t\t\tresolve: (bytes: Uint8Array | null) => void\n\t\t\treject: (error: Error) => void\n\t\t\ttimer: ReturnType<typeof setTimeout>\n\t\t\tstartedAtMs: number\n\t\t\tthrottledCount: number\n\t\t}\n\t>()\n\n\tconst armTimeout = (requestId: string, hash: string): ReturnType<typeof setTimeout> =>\n\t\tsetTimeout(() => {\n\t\t\tconst entry = pending.get(requestId)\n\t\t\tif (!entry) return\n\t\t\tpending.delete(requestId)\n\t\t\tentry.reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`))\n\t\t}, timeoutMs)\n\n\tport.onMessage((message) => {\n\t\tif (message.type !== 'blob-chunk-response') {\n\t\t\treturn\n\t\t}\n\t\tconst entry = pending.get(message.requestId)\n\t\tif (!entry) {\n\t\t\treturn\n\t\t}\n\t\tclearTimeout(entry.timer)\n\t\tif (message.throttled === true) {\n\t\t\t// Over the server's blob budget: back off and ask again, same request id.\n\t\t\tentry.throttledCount += 1\n\t\t\tconst backoff = Math.min(maxDelayMs, minDelayMs * 2 ** (entry.throttledCount - 1))\n\t\t\tconst delay = Math.min(maxDelayMs, Math.max(minDelayMs, message.retryAfterMs ?? backoff))\n\t\t\tif (Date.now() + delay - entry.startedAtMs > maxThrottleWaitMs) {\n\t\t\t\tpending.delete(message.requestId)\n\t\t\t\tentry.reject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`Blob chunk request for ${entry.hash} was throttled for longer than ${maxThrottleWaitMs}ms`,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tentry.timer = setTimeout(() => {\n\t\t\t\tif (pending.get(message.requestId) !== entry) return\n\t\t\t\tentry.timer = armTimeout(message.requestId, entry.hash)\n\t\t\t\tport.send({ type: 'blob-chunk-request', requestId: message.requestId, hash: entry.hash })\n\t\t\t}, delay)\n\t\t\treturn\n\t\t}\n\t\tpending.delete(message.requestId)\n\t\tentry.resolve(message.bytes)\n\t})\n\n\treturn {\n\t\tpendingCount: () => pending.size,\n\t\tgetChunk(hash: string): Promise<Uint8Array | null> {\n\t\t\tconst requestId = generateUUIDv7()\n\t\t\treturn new Promise<Uint8Array | null>((resolve, reject) => {\n\t\t\t\tpending.set(requestId, {\n\t\t\t\t\thash,\n\t\t\t\t\tresolve,\n\t\t\t\t\treject,\n\t\t\t\t\ttimer: armTimeout(requestId, hash),\n\t\t\t\t\tstartedAtMs: Date.now(),\n\t\t\t\t\tthrottledCount: 0,\n\t\t\t\t})\n\t\t\t\tport.send({ type: 'blob-chunk-request', requestId, hash })\n\t\t\t})\n\t\t},\n\t}\n}\n\n/**\n * Serve blob chunk requests arriving on a message port from a blob store. Answers\n * each `blob-chunk-request` with the bytes for that hash, or null when the store\n * does not hold it. Returns nothing; attach it once per connection.\n *\n * @param port - The message port from the peer requesting chunks\n * @param blobStore - The store to serve chunks from (keyed by chunk hash)\n */\nexport function serveBlobChunks(\n\tport: ChunkMessagePort,\n\tblobStore: ContentAddressedBlobStore,\n): void {\n\tport.onMessage((message) => {\n\t\tif (message.type !== 'blob-chunk-request') {\n\t\t\treturn\n\t\t}\n\t\tvoid blobStore.get(message.hash).then(\n\t\t\t(bytes) => {\n\t\t\t\tport.send({ type: 'blob-chunk-response', requestId: message.requestId, bytes })\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Integrity failure or read error: report as not-held rather than\n\t\t\t\t// crashing the connection. The requester treats null as unavailable.\n\t\t\t\tport.send({ type: 'blob-chunk-response', requestId: message.requestId, bytes: null })\n\t\t\t},\n\t\t)\n\t})\n}\n\n/**\n * Create a connected in-memory pair of {@link ChunkMessagePort}s. Messages sent\n * on one arrive on the other, asynchronously (next microtask) so the pair models\n * a real transport rather than re-entrant synchronous delivery. For tests.\n */\nexport function createChunkPortPair(): { a: ChunkMessagePort; b: ChunkMessagePort } {\n\tconst handlersA: Array<(m: ChunkMessage) => void> = []\n\tconst handlersB: Array<(m: ChunkMessage) => void> = []\n\n\tconst a: ChunkMessagePort = {\n\t\tsend(message) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tfor (const handler of handlersB) {\n\t\t\t\t\thandler(message)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\tonMessage(handler) {\n\t\t\thandlersA.push(handler)\n\t\t},\n\t}\n\tconst b: ChunkMessagePort = {\n\t\tsend(message) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tfor (const handler of handlersA) {\n\t\t\t\t\thandler(message)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\tonMessage(handler) {\n\t\t\thandlersB.push(handler)\n\t\t},\n\t}\n\treturn { a, b }\n}\n"],"mappings":"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","names":["snapshot","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob"]}
1
+ {"version":3,"sources":["../src/persistence/storage-persistence.ts","../src/reactivity/query-store.ts","../src/reactivity/assert-query-ready.ts","../src/reactivity/query-store-cache.ts","../src/richtext/create-richtext-controller.ts","../src/richtext/as-rich-text-sync-engine.ts","../src/blob/opfs-blob-store.ts","../src/blob/blob-chunking.ts","../src/blob/blob-transfer.ts","../src/blob/blob-manifest-transfer.ts","../src/blob/server-blob-callbacks.ts","../src/blob/blob-gc.ts","../src/blob/blob-chunk-transport.ts"],"sourcesContent":["import type { KoraEventEmitter } from '@korajs/core'\n\n/**\n * Durable-storage state of this origin, as far as Kora knows (NEW-STORE-4).\n * - `unknown`: not checked yet (the boot check runs in the background).\n * - `persisted`: the browser granted persistent storage; it will not evict this\n * origin's data under storage pressure.\n * - `best-effort`: storage may be evicted under pressure (the browser default).\n * - `unsupported`: the runtime has no StorageManager persistence API (Node,\n * older browsers, some WebViews).\n * - `error`: the check or request threw; `lastError` says why.\n */\nexport type StoragePersistenceState =\n\t| 'unknown'\n\t| 'persisted'\n\t| 'best-effort'\n\t| 'unsupported'\n\t| 'error'\n\n/** Snapshot returned by `app.storage.persistence.status()`. */\nexport interface StoragePersistenceStatus {\n\tstate: StoragePersistenceState\n\t/** True only when the browser reported persistent storage. */\n\tpersisted: boolean\n\t/** True once `persist()` was asked for (explicitly or by an automatic trigger). */\n\trequested: boolean\n\t/** Message of the last failure, when `state` is `error`. */\n\tlastError?: string\n}\n\n/** Why an automatic (background) persistence request was made. */\nexport type PersistenceRequestReason = 'explicit' | 'sign-in' | 'first-write' | 'installed-app'\n\n/** The subset of `navigator.storage` used here. */\nexport interface PersistenceStorageManager {\n\tpersist?: () => Promise<boolean>\n\tpersisted?: () => Promise<boolean>\n}\n\n/** Options for {@link StoragePersistence}. */\nexport interface StoragePersistenceOptions {\n\t/** Receives `storage:persistence` events. */\n\temitter?: KoraEventEmitter | null\n\t/**\n\t * The StorageManager to use. Defaults to `globalThis.navigator.storage`,\n\t * resolved on each call so tests and late polyfills are honoured.\n\t */\n\tstorage?: PersistenceStorageManager | null\n}\n\nfunction defaultStorageManager(): PersistenceStorageManager | null {\n\tconst nav = (globalThis as { navigator?: { storage?: PersistenceStorageManager } }).navigator\n\treturn nav?.storage ?? null\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error)\n}\n\n/**\n * Tracks and requests durable storage (`navigator.storage.persist()`) without ever\n * putting it on the startup path (NEW-STORE-4).\n *\n * In Firefox `persist()` opens a permission prompt and its promise stays pending\n * until the user answers, so awaiting it inside startup could hang `app.ready`\n * forever. Kora therefore:\n * - checks `persisted()` at boot, which never prompts, in the background;\n * - requests persistence only on an explicit `request()` or, without awaiting,\n * after a meaningful moment (sign-in, the first local write, running as an\n * installed app);\n * - surfaces every result through `status()` and the `storage:persistence` event,\n * never by blocking.\n */\nexport class StoragePersistence {\n\tprivate current: StoragePersistenceStatus = {\n\t\tstate: 'unknown',\n\t\tpersisted: false,\n\t\trequested: false,\n\t}\n\tprivate inFlightRequest: Promise<StoragePersistenceStatus> | null = null\n\tprivate readonly emitter: KoraEventEmitter | null\n\tprivate readonly storageOverride: PersistenceStorageManager | null | undefined\n\n\tconstructor(options: StoragePersistenceOptions = {}) {\n\t\tthis.emitter = options.emitter ?? null\n\t\tthis.storageOverride = options.storage\n\t}\n\n\t/** Synchronous snapshot of the last known state. Never prompts. */\n\tstatus(): StoragePersistenceStatus {\n\t\treturn { ...this.current }\n\t}\n\n\t/**\n\t * Read whether storage is already persistent (`navigator.storage.persisted()`).\n\t * Never prompts and never throws: a failure becomes `state: 'error'`.\n\t */\n\tasync check(): Promise<StoragePersistenceStatus> {\n\t\tconst storage = this.storage()\n\t\tif (!storage || typeof storage.persisted !== 'function') {\n\t\t\tthis.update({ state: 'unsupported', persisted: false })\n\t\t\tif (storage) this.emit('unsupported', false)\n\t\t\treturn this.status()\n\t\t}\n\t\ttry {\n\t\t\tconst persisted = (await storage.persisted()) === true\n\t\t\t// A grant observed by a concurrent request() is never downgraded here.\n\t\t\tif (!this.current.persisted) {\n\t\t\t\tthis.update({ state: persisted ? 'persisted' : 'best-effort', persisted })\n\t\t\t}\n\t\t\tthis.emit('checked', this.current.persisted)\n\t\t} catch (error) {\n\t\t\tthis.update({ state: 'error', persisted: false, lastError: errorMessage(error) })\n\t\t\tthis.emit('error', false, errorMessage(error))\n\t\t}\n\t\treturn this.status()\n\t}\n\n\t/**\n\t * Ask the browser for persistent storage (`navigator.storage.persist()`).\n\t * May show a permission prompt (Firefox), so call it from a user gesture or a\n\t * meaningful moment, and do not block rendering on it. Concurrent calls share\n\t * one request. Never throws: a failure becomes `state: 'error'`.\n\t *\n\t * @returns The state after the browser answered\n\t */\n\trequest(): Promise<StoragePersistenceStatus> {\n\t\tif (this.inFlightRequest) return this.inFlightRequest\n\t\tconst storage = this.storage()\n\t\tif (!storage || typeof storage.persist !== 'function') {\n\t\t\tthis.update({ state: 'unsupported', persisted: false })\n\t\t\tif (storage) this.emit('unsupported', false)\n\t\t\treturn Promise.resolve(this.status())\n\t\t}\n\t\tif (this.current.persisted) return Promise.resolve(this.status())\n\t\tconst persist = storage.persist.bind(storage)\n\t\tthis.current = { ...this.current, requested: true }\n\t\tconst pending = (async (): Promise<StoragePersistenceStatus> => {\n\t\t\ttry {\n\t\t\t\tconst persisted = (await persist()) === true\n\t\t\t\tthis.update({ state: persisted ? 'persisted' : 'best-effort', persisted })\n\t\t\t\tthis.emit('requested', persisted)\n\t\t\t} catch (error) {\n\t\t\t\tthis.update({ state: 'error', persisted: false, lastError: errorMessage(error) })\n\t\t\t\tthis.emit('error', false, errorMessage(error))\n\t\t\t} finally {\n\t\t\t\tthis.inFlightRequest = null\n\t\t\t}\n\t\t\treturn this.status()\n\t\t})()\n\t\tthis.inFlightRequest = pending\n\t\treturn pending\n\t}\n\n\t/**\n\t * Fire-and-forget `request()` for an automatic trigger. Never awaited by Kora,\n\t * so a pending prompt cannot block anything; the outcome arrives as a\n\t * `storage:persistence` event. Skipped when storage is already persistent or a\n\t * request was already made this session.\n\t *\n\t * @param _reason - Which trigger fired (kept for tracing at call sites)\n\t */\n\trequestInBackground(_reason: PersistenceRequestReason): void {\n\t\tif (this.current.persisted || this.current.requested) return\n\t\tvoid this.request()\n\t}\n\n\tprivate storage(): PersistenceStorageManager | null {\n\t\treturn this.storageOverride === undefined ? defaultStorageManager() : this.storageOverride\n\t}\n\n\tprivate update(next: Omit<StoragePersistenceStatus, 'requested'>): void {\n\t\tthis.current = { ...next, requested: this.current.requested }\n\t}\n\n\tprivate emit(\n\t\tstate: 'checked' | 'requested' | 'unsupported' | 'error',\n\t\tpersisted: boolean,\n\t\tmessage?: string,\n\t): void {\n\t\tthis.emitter?.emit({\n\t\t\ttype: 'storage:persistence',\n\t\t\tstate,\n\t\t\tpersisted,\n\t\t\t...(message !== undefined ? { message } : {}),\n\t\t})\n\t}\n}\n","import type { QueryBuilder } from '../query/query-builder'\nimport type { CollectionRecord } from '../types'\n\n/**\n * Frozen empty array returned as the initial snapshot before any data loads.\n * Same reference every time prevents infinite update loops in UI bindings.\n */\nconst EMPTY_ARRAY: readonly unknown[] = Object.freeze([])\n\n/**\n * Bridges the async QueryBuilder.subscribe() API with synchronous snapshot reads\n * required by React useSyncExternalStore, Vue composables, and Svelte stores.\n *\n * Uses lazy subscription: the underlying query subscription starts when the first\n * listener attaches and stops when the last listener detaches.\n */\nexport class QueryStore<T = CollectionRecord> {\n\tprivate snapshot: readonly T[] = EMPTY_ARRAY as readonly T[]\n\tprivate listeners = new Set<() => void>()\n\tprivate unsubscribeQuery: (() => void) | null = null\n\tprivate active = false\n\tprivate hasEmittedSnapshot = false\n\tprivate error: Error | null = null\n\tprivate readonly queryBuilder: QueryBuilder<T>\n\n\tconstructor(queryBuilder: QueryBuilder<T>) {\n\t\tthis.queryBuilder = queryBuilder\n\t}\n\n\t/**\n\t * Subscribe to snapshot changes.\n\t *\n\t * @returns Unsubscribe function\n\t */\n\tsubscribe = (onStoreChange: () => void): (() => void) => {\n\t\tthis.listeners.add(onStoreChange)\n\n\t\tif (!this.active) {\n\t\t\tthis.startSubscription()\n\t\t}\n\n\t\treturn () => {\n\t\t\tthis.listeners.delete(onStoreChange)\n\t\t\tif (this.listeners.size === 0) {\n\t\t\t\tthis.stopSubscription()\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Synchronous read of the latest query results. */\n\tgetSnapshot = (): readonly T[] => {\n\t\treturn this.snapshot\n\t}\n\n\t/**\n\t * Whether the underlying query subscription has delivered at least one\n\t * authoritative result for the current subscription lifecycle.\n\t */\n\thasSnapshot = (): boolean => {\n\t\treturn this.hasEmittedSnapshot\n\t}\n\n\t/**\n\t * The latest query failure, or null. Set when the query (or a re-run after a\n\t * write) fails; cleared by the next successful delivery. Listeners are notified\n\t * on both transitions, so bindings read it next to `getSnapshot()`.\n\t */\n\tgetError = (): Error | null => {\n\t\treturn this.error\n\t}\n\n\t/** Tear down listeners and the underlying query subscription. */\n\tdestroy(): void {\n\t\tthis.stopSubscription()\n\t\tthis.listeners.clear()\n\t\tthis.snapshot = EMPTY_ARRAY as readonly T[]\n\t\tthis.hasEmittedSnapshot = false\n\t\tthis.error = null\n\t}\n\n\tprivate startSubscription(): void {\n\t\tthis.active = true\n\t\tthis.unsubscribeQuery = this.queryBuilder.subscribe(\n\t\t\t(results: readonly T[]) => {\n\t\t\t\tif (!this.active) return\n\t\t\t\tthis.hasEmittedSnapshot = true\n\t\t\t\tthis.error = null\n\t\t\t\tthis.snapshot = Object.freeze([...results])\n\t\t\t\tthis.notifyListeners()\n\t\t\t},\n\t\t\t{\n\t\t\t\t// STORE-12: the failure reaches bindings as error state; the last\n\t\t\t\t// snapshot stays readable and the next success clears the error.\n\t\t\t\tonError: (failure) => {\n\t\t\t\t\tif (!this.active) return\n\t\t\t\t\tthis.error = failure.error\n\t\t\t\t\tthis.notifyListeners()\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t}\n\n\tprivate stopSubscription(): void {\n\t\tthis.active = false\n\t\tif (this.unsubscribeQuery) {\n\t\t\tthis.unsubscribeQuery()\n\t\t\tthis.unsubscribeQuery = null\n\t\t}\n\t}\n\n\tprivate notifyListeners(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener()\n\t\t}\n\t}\n}\n","import { AppNotReadyError } from '@korajs/core'\nimport type { QueryBuilder } from '../query/query-builder'\n\nconst LEGACY_PENDING_COLLECTION = '__pending__'\n\n/**\n * Ensures a query builder is backed by an open store (not a legacy pending placeholder).\n */\nexport function assertQueryReady(query: QueryBuilder<unknown>): void {\n\tconst descriptor = query.getDescriptor()\n\tif (descriptor.collection === LEGACY_PENDING_COLLECTION) {\n\t\tthrow new AppNotReadyError(\n\t\t\t'Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>.',\n\t\t)\n\t}\n}\n","import type { QueryBuilder } from '../query/query-builder'\nimport { queryKey } from '../query/query-key'\nimport type { CollectionRecord } from '../types'\nimport { QueryStore } from './query-store'\n\ninterface CacheEntry {\n\tqueryStore: QueryStore<CollectionRecord>\n\trefCount: number\n}\n\n/**\n * Reference-counted cache of {@link QueryStore} instances keyed by query descriptor.\n * Identical queries from different components share one underlying subscription.\n */\nexport class QueryStoreCache {\n\tprivate entries = new Map<string, CacheEntry>()\n\n\tconstructor(private readonly scopeKey = 'default') {}\n\n\tgetOrCreate<T>(queryBuilder: QueryBuilder<T>): QueryStore<T> {\n\t\tconst key = this.getKey(queryBuilder)\n\t\tconst existing = this.entries.get(key)\n\n\t\tif (existing) {\n\t\t\texisting.refCount++\n\t\t\treturn existing.queryStore as QueryStore<T>\n\t\t}\n\n\t\tconst queryStore = new QueryStore<T>(queryBuilder)\n\t\tthis.entries.set(key, {\n\t\t\tqueryStore: queryStore as QueryStore<CollectionRecord>,\n\t\t\trefCount: 1,\n\t\t})\n\t\treturn queryStore\n\t}\n\n\trelease(queryBuilder: QueryBuilder<unknown>): void {\n\t\tconst key = this.getKey(queryBuilder)\n\t\tconst entry = this.entries.get(key)\n\t\tif (!entry) {\n\t\t\treturn\n\t\t}\n\n\t\tentry.refCount--\n\t\tif (entry.refCount <= 0) {\n\t\t\tentry.queryStore.destroy()\n\t\t\tthis.entries.delete(key)\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tentry.queryStore.destroy()\n\t\t}\n\t\tthis.entries.clear()\n\t}\n\n\tget size(): number {\n\t\treturn this.entries.size\n\t}\n\n\tprivate getKey(queryBuilder: QueryBuilder<unknown>): string {\n\t\treturn `${this.scopeKey}:${queryKey(queryBuilder.getDescriptor())}`\n\t}\n}\n\nlet sharedCache: QueryStoreCache | null = null\n\n/**\n * Process-wide shared query store cache for framework bindings.\n *\n * @deprecated Prefer {@link KoraApp.getQueryStoreCache} via `KoraProvider` context\n * so each app instance owns an isolated cache.\n */\nexport function getSharedQueryStoreCache(): QueryStoreCache {\n\tif (!sharedCache) {\n\t\tsharedCache = new QueryStoreCache()\n\t}\n\treturn sharedCache\n}\n","import * as Y from 'yjs'\nimport { encodeRichtext } from '../serialization/richtext-serializer'\nimport type {\n\tCreateRichTextControllerOptions,\n\tRichTextAwarenessState,\n\tRichTextAwarenessUser,\n\tRichTextController,\n\tRichTextControllerSnapshot,\n\tRichTextCursorInfo,\n} from './types'\n\nconst LOAD_ORIGIN = 'kora-load'\nconst REMOTE_ORIGIN = 'kora-remote'\nconst DOC_CHANNEL_ORIGIN = 'kora-doc-channel'\nconst TEXT_KEY = 'content'\nconst PERSIST_DEBOUNCE_MS = 400\n\n/**\n * Framework-agnostic richtext binding: Yjs document lifecycle, persistence,\n * incremental doc channel, and awareness cursors.\n *\n * Every save writes the full state of the live Y.Doc, which holds every local edit\n * and every merged remote change, so a stored change arriving while a save waits\n * cannot drop local typing. A refused save keeps the edits unsaved\n * (`hasUnsavedChanges`, `getUnsavedState()`) and the next edit or `retrySave()`\n * saves them again; the first successful save clears `error`.\n */\nexport function createRichTextController(\n\toptions: CreateRichTextControllerOptions,\n): RichTextController {\n\tconst {\n\t\tcollection,\n\t\tcollectionName,\n\t\trecordId,\n\t\tfieldName,\n\t\tstore,\n\t\tsyncEngine = null,\n\t\tuseDocChannel,\n\t} = options\n\n\tconst doc = new Y.Doc()\n\tconst text = doc.getText(TEXT_KEY)\n\tconst undoManager = new Y.UndoManager(text)\n\n\tlet user = options.user\n\tlet disposed = false\n\tlet ready = false\n\tlet error: Error | null = null\n\tlet canUndo = false\n\tlet canRedo = false\n\tlet cursors: RichTextCursorInfo[] = []\n\t// Local edits are counted; a save that succeeds covers every edit made before it\n\t// read the document. Edits after the last covered one are unsaved.\n\tlet editGeneration = 0\n\tlet savedGeneration = 0\n\tlet saving: Promise<void> | null = null\n\tlet saveAgain = false\n\n\tconst listeners = new Set<() => void>()\n\tlet snapshot: RichTextControllerSnapshot = {\n\t\tready: false,\n\t\terror: null,\n\t\tcanUndo: false,\n\t\tcanRedo: false,\n\t\tcursors: [],\n\t\thasUnsavedChanges: false,\n\t}\n\tconst docChannelActiveRef = { current: false }\n\tlet persistTimer: ReturnType<typeof setTimeout> | null = null\n\n\tlet recordUnsubscribe: (() => void) | null = null\n\tlet docChannelUnsubscribe: (() => void) | null = null\n\tlet awarenessUnsubscribe: (() => void) | null = null\n\n\tconst refreshSnapshot = (): void => {\n\t\tsnapshot = {\n\t\t\tready,\n\t\t\terror,\n\t\t\tcanUndo,\n\t\t\tcanRedo,\n\t\t\tcursors: [...cursors],\n\t\t\thasUnsavedChanges: editGeneration > savedGeneration,\n\t\t}\n\t}\n\n\tconst notify = (): void => {\n\t\trefreshSnapshot()\n\t\tfor (const listener of listeners) {\n\t\t\tlistener()\n\t\t}\n\t}\n\n\tconst syncHistoryState = (): void => {\n\t\tcanUndo = undoManager.undoStack.length > 0\n\t\tcanRedo = undoManager.redoStack.length > 0\n\t\tnotify()\n\t}\n\n\tconst getSnapshot = (): RichTextControllerSnapshot => snapshot\n\n\tconst resolveAwarenessUser = (): RichTextAwarenessUser => {\n\t\tif (user) {\n\t\t\treturn user\n\t\t}\n\t\tif (syncEngine) {\n\t\t\tconst existing = syncEngine.getAwarenessManager().getLocalState()?.user\n\t\t\tif (existing) {\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tname: 'Anonymous',\n\t\t\tcolor: '#6366f1',\n\t\t}\n\t}\n\n\tconst setCursor = (anchor: number, head: number): void => {\n\t\tif (!syncEngine) {\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tconst state: RichTextAwarenessState = {\n\t\t\tuser: resolveAwarenessUser(),\n\t\t\tcursor: {\n\t\t\t\tcollection: collectionName,\n\t\t\t\trecordId,\n\t\t\t\tfield: fieldName,\n\t\t\t\tanchor,\n\t\t\t\thead,\n\t\t\t},\n\t\t}\n\t\tawareness.setLocalState(state)\n\t}\n\n\tconst clearCursor = (): void => {\n\t\tif (!syncEngine) {\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tconst current = awareness.getLocalState()\n\t\tif (!current?.cursor) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\tcurrent.cursor.collection !== collectionName ||\n\t\t\tcurrent.cursor.recordId !== recordId ||\n\t\t\tcurrent.cursor.field !== fieldName\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tawareness.setLocalState({ user: current.user })\n\t}\n\n\tconst applyRemoteSnapshot = (value: unknown): void => {\n\t\tconst encoded = encodeRichtextInput(value)\n\t\tif (!encoded) {\n\t\t\treturn\n\t\t}\n\n\t\tconst currentSnapshot = Y.encodeStateAsUpdate(doc)\n\t\tif (updatesEqual(currentSnapshot, encoded)) {\n\t\t\treturn\n\t\t}\n\n\t\t// A Yjs merge: the stored state joins the live document and local edits that\n\t\t// are not saved yet stay in it, so the next save (of the live document) keeps them.\n\t\tY.applyUpdate(doc, encoded, REMOTE_ORIGIN)\n\t\tsyncHistoryState()\n\t}\n\n\t/** Save the live document once; the outcome updates `error` and the unsaved state. */\n\tconst persistOnce = async (): Promise<void> => {\n\t\tconst generation = editGeneration\n\t\tconst state = Y.encodeStateAsUpdate(doc)\n\t\ttry {\n\t\t\tawait collection.update(recordId, {\n\t\t\t\t[fieldName]: state,\n\t\t\t})\n\t\t\tif (generation > savedGeneration) savedGeneration = generation\n\t\t\terror = null\n\t\t} catch (cause) {\n\t\t\terror = cause instanceof Error ? cause : new Error(String(cause))\n\t\t}\n\t\tif (!disposed) notify()\n\t}\n\n\t/**\n\t * Save the live document. Saves run one at a time; a request during a save runs\n\t * one more save after it, which reads the document then and so holds every edit.\n\t */\n\tconst flushPersist = (): Promise<void> => {\n\t\tif (saving) {\n\t\t\tsaveAgain = true\n\t\t\treturn saving\n\t\t}\n\t\tconst run = (async () => {\n\t\t\tdo {\n\t\t\t\tsaveAgain = false\n\t\t\t\tawait persistOnce()\n\t\t\t} while (saveAgain)\n\t\t})()\n\t\tsaving = run\n\t\tvoid run.finally(() => {\n\t\t\tif (saving === run) saving = null\n\t\t})\n\t\treturn run\n\t}\n\n\tconst schedulePersist = (): void => {\n\t\tif (persistTimer) {\n\t\t\tclearTimeout(persistTimer)\n\t\t}\n\t\tpersistTimer = setTimeout(() => {\n\t\t\tpersistTimer = null\n\t\t\tvoid flushPersist()\n\t\t}, PERSIST_DEBOUNCE_MS)\n\t}\n\n\tconst onDocUpdate = (update: Uint8Array, origin: unknown): void => {\n\t\tsyncHistoryState()\n\t\tif (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {\n\t\t\treturn\n\t\t}\n\n\t\teditGeneration += 1\n\n\t\tif (docChannelActiveRef.current && syncEngine) {\n\t\t\tsyncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update)\n\t\t\tschedulePersist()\n\t\t\treturn\n\t\t}\n\n\t\tvoid flushPersist()\n\t}\n\n\tconst subscribeRecordChanges = (): void => {\n\t\trecordUnsubscribe?.()\n\t\trecordUnsubscribe = store\n\t\t\t.collection(collectionName)\n\t\t\t.where({ id: recordId })\n\t\t\t.subscribe((results) => {\n\t\t\t\tconst record = results[0]\n\t\t\t\tif (!record) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tapplyRemoteSnapshot(record[fieldName])\n\t\t\t})\n\t}\n\n\tconst subscribeDocChannel = (): void => {\n\t\tdocChannelUnsubscribe?.()\n\t\tif (!ready || !syncEngine || !docChannelActiveRef.current) {\n\t\t\treturn\n\t\t}\n\n\t\tconst channel = syncEngine.getRichtextDocChannel?.()\n\t\tif (!channel) {\n\t\t\treturn\n\t\t}\n\n\t\tdocChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {\n\t\t\tY.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN)\n\t\t\tsyncHistoryState()\n\t\t})\n\t}\n\n\tconst updateCursors = (): void => {\n\t\tif (!syncEngine) {\n\t\t\tcursors = []\n\t\t\tnotify()\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tconst localClientId = awareness.clientId\n\t\tconst states = awareness.getStates()\n\t\tconst fieldCursors: RichTextCursorInfo[] = []\n\n\t\tfor (const [clientId, state] of states) {\n\t\t\tif (clientId === localClientId) continue\n\t\t\tif (!state.cursor) continue\n\t\t\tif (\n\t\t\t\tstate.cursor.collection !== collectionName ||\n\t\t\t\tstate.cursor.recordId !== recordId ||\n\t\t\t\tstate.cursor.field !== fieldName\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfieldCursors.push({\n\t\t\t\tclientId,\n\t\t\t\tuserName: state.user.name,\n\t\t\t\tcolor: state.user.color,\n\t\t\t\tanchor: state.cursor.anchor,\n\t\t\t\thead: state.cursor.head,\n\t\t\t})\n\t\t}\n\n\t\tcursors = fieldCursors\n\t\tnotify()\n\t}\n\n\tconst subscribeAwareness = (): void => {\n\t\tawarenessUnsubscribe?.()\n\t\tif (!syncEngine) {\n\t\t\tcursors = []\n\t\t\tnotify()\n\t\t\treturn\n\t\t}\n\n\t\tconst awareness = syncEngine.getAwarenessManager()\n\t\tawarenessUnsubscribe = awareness.on('change', () => {\n\t\t\tupdateCursors()\n\t\t})\n\t\tupdateCursors()\n\t}\n\n\tconst initialize = async (): Promise<void> => {\n\t\tready = false\n\t\terror = null\n\t\tnotify()\n\n\t\ttry {\n\t\t\tconst record = await collection.findById(recordId)\n\t\t\tif (disposed) return\n\n\t\t\tdoc.transact(() => {\n\t\t\t\tconst target = doc.getText(TEXT_KEY)\n\t\t\t\ttarget.delete(0, target.length)\n\t\t\t}, LOAD_ORIGIN)\n\n\t\t\tconst encoded = encodeRichtextInput(record?.[fieldName])\n\t\t\tif (encoded) {\n\t\t\t\tY.applyUpdate(doc, encoded, LOAD_ORIGIN)\n\t\t\t}\n\n\t\t\tconst channel = syncEngine?.getRichtextDocChannel?.()\n\t\t\tdocChannelActiveRef.current =\n\t\t\t\tchannel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false\n\n\t\t\tready = true\n\t\t\tsyncHistoryState()\n\t\t\tsubscribeRecordChanges()\n\t\t\tsubscribeDocChannel()\n\t\t\tsubscribeAwareness()\n\t\t\tnotify()\n\t\t} catch (cause) {\n\t\t\tif (disposed) return\n\t\t\terror = cause instanceof Error ? cause : new Error(String(cause))\n\t\t\tnotify()\n\t\t}\n\t}\n\n\tdoc.on('update', onDocUpdate)\n\tvoid initialize()\n\n\treturn {\n\t\tdoc,\n\t\ttext,\n\t\tgetSnapshot,\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener)\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener)\n\t\t\t}\n\t\t},\n\t\tundo() {\n\t\t\tundoManager.undo()\n\t\t\tsyncHistoryState()\n\t\t},\n\t\tredo() {\n\t\t\tundoManager.redo()\n\t\t\tsyncHistoryState()\n\t\t},\n\t\tsetCursor,\n\t\tclearCursor,\n\t\tsetUser(nextUser: RichTextAwarenessUser | undefined) {\n\t\t\tuser = nextUser\n\t\t},\n\t\tretrySave() {\n\t\t\tif (persistTimer) {\n\t\t\t\tclearTimeout(persistTimer)\n\t\t\t\tpersistTimer = null\n\t\t\t}\n\t\t\treturn flushPersist()\n\t\t},\n\t\tgetUnsavedState() {\n\t\t\treturn editGeneration > savedGeneration ? Y.encodeStateAsUpdate(doc) : null\n\t\t},\n\t\tdestroy() {\n\t\t\tif (disposed) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdisposed = true\n\t\t\t// Edits waiting for the debounced save are saved now, not dropped.\n\t\t\tif (persistTimer) {\n\t\t\t\tclearTimeout(persistTimer)\n\t\t\t\tpersistTimer = null\n\t\t\t}\n\t\t\tif (editGeneration > savedGeneration) {\n\t\t\t\tvoid flushPersist()\n\t\t\t}\n\t\t\tdoc.off('update', onDocUpdate)\n\t\t\trecordUnsubscribe?.()\n\t\t\tdocChannelUnsubscribe?.()\n\t\t\tawarenessUnsubscribe?.()\n\t\t\tclearCursor()\n\t\t\tundoManager.destroy()\n\t\t\tdocChannelActiveRef.current = false\n\t\t\tlisteners.clear()\n\t\t},\n\t}\n}\n\nfunction encodeRichtextInput(value: unknown): Uint8Array | null {\n\tif (value === null || value === undefined) {\n\t\treturn null\n\t}\n\n\ttry {\n\t\treturn encodeRichtext(value as Parameters<typeof encodeRichtext>[0])\n\t} catch {\n\t\tif (typeof value === 'string') {\n\t\t\tconst fallbackDoc = new Y.Doc()\n\t\t\tfallbackDoc.getText(TEXT_KEY).insert(0, value)\n\t\t\treturn Y.encodeStateAsUpdate(fallbackDoc)\n\t\t}\n\t\tthrow new Error('Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.')\n\t}\n}\n\nfunction updatesEqual(left: Uint8Array, right: Uint8Array): boolean {\n\tif (left.length !== right.length) {\n\t\treturn false\n\t}\n\tfor (let index = 0; index < left.length; index++) {\n\t\tif (left[index] !== right[index]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n","import type { RichTextSyncEngine } from './types'\n\n/**\n * Narrows a sync engine reference to the richtext controller surface.\n * SyncEngine satisfies this interface at runtime; types live in different packages.\n */\nexport function asRichTextSyncEngine(engine: unknown): RichTextSyncEngine | null {\n\tif (engine == null) {\n\t\treturn null\n\t}\n\treturn engine as RichTextSyncEngine\n}\n","import { type BlobRef, type BlobRefMetadata, createBlobRef, hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * The minimal directory abstraction an {@link OpfsBlobStore} needs. Blobs are\n * laid out as `<shard>/<name>` (shard = first two hex chars of the hash) so a\n * single directory never holds millions of entries.\n *\n * The real backend ({@link createOpfsBlobDirectory}) is the browser Origin\n * Private File System; tests supply an in-memory implementation. Keeping this\n * port small means the content-addressed logic (hashing, integrity, dedup,\n * sharding) is fully testable without a browser.\n */\nexport interface OpfsBlobDirectory {\n\t/** Read the bytes at `<shard>/<name>`, or null if absent. */\n\tread(shard: string, name: string): Promise<Uint8Array | null>\n\t/**\n\t * Write bytes at `<shard>/<name>`, creating the shard directory as needed.\n\t * Must be atomic: a crash mid-write may not leave a partially-written file\n\t * that a later read would trust.\n\t */\n\twrite(shard: string, name: string, bytes: Uint8Array): Promise<void>\n\t/** Remove `<shard>/<name>`. Returns whether anything was removed. */\n\tremove(shard: string, name: string): Promise<boolean>\n\t/** Whether `<shard>/<name>` exists. */\n\thas(shard: string, name: string): Promise<boolean>\n\t/** Count of stored blobs across all shards (temp files excluded). */\n\tcount(): Promise<number>\n\t/** Every stored blob name (hash) across all shards (temp files excluded). */\n\tkeys(): Promise<string[]>\n}\n\n/**\n * A persistent {@link ContentAddressedBlobStore} for the browser, backed by the\n * Origin Private File System (OPFS) — the same durable storage the SQLite WASM\n * adapter uses. Blobs survive reloads, are sharded by hash prefix, deduplicated,\n * and integrity-verified on read (the returned bytes are checked to hash to the\n * requested key). Writes commit atomically so a torn write cannot surface as\n * trusted data.\n *\n * The store itself is environment-neutral: it talks to an {@link OpfsBlobDirectory}.\n * Use {@link createOpfsBlobStore} in a browser to get one backed by real OPFS.\n */\nexport class OpfsBlobStore implements ContentAddressedBlobStore {\n\tconstructor(private readonly dir: OpfsBlobDirectory) {}\n\n\tprivate shardFor(hash: string): string {\n\t\t// Two hex chars → 256 shards. Hashes are hex, so this is always well-formed.\n\t\treturn hash.slice(0, 2)\n\t}\n\n\tasync put(bytes: Uint8Array, metadata: BlobRefMetadata = {}): Promise<BlobRef> {\n\t\tconst ref = await createBlobRef(bytes, metadata)\n\t\tconst shard = this.shardFor(ref.hash)\n\n\t\t// Dedup: identical content already stored is a no-op.\n\t\tif (await this.dir.has(shard, ref.hash)) {\n\t\t\treturn ref\n\t\t}\n\n\t\t// Copy into an exactly-sized buffer so a subarray view writes its own bytes.\n\t\tconst out = new Uint8Array(bytes.byteLength)\n\t\tout.set(bytes)\n\t\tawait this.dir.write(shard, ref.hash, out)\n\t\treturn ref\n\t}\n\n\tasync get(hash: string): Promise<Uint8Array | null> {\n\t\tconst bytes = await this.dir.read(this.shardFor(hash), hash)\n\t\tif (bytes === null) {\n\t\t\treturn null\n\t\t}\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\treturn bytes\n\t}\n\n\tasync has(hash: string): Promise<boolean> {\n\t\treturn this.dir.has(this.shardFor(hash), hash)\n\t}\n\n\tasync delete(hash: string): Promise<boolean> {\n\t\treturn this.dir.remove(this.shardFor(hash), hash)\n\t}\n\n\tasync size(): Promise<number> {\n\t\treturn this.dir.count()\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\treturn this.dir.keys()\n\t}\n}\n\n// --- Real OPFS binding (browser only) ---\n\n/** Minimal structural views of the File System Access handles we use. */\ninterface OpfsWritableStream {\n\twrite(data: Uint8Array): Promise<void>\n\tclose(): Promise<void>\n}\ninterface OpfsFileHandle {\n\tcreateWritable(options?: { keepExistingData?: boolean }): Promise<OpfsWritableStream>\n\tgetFile(): Promise<{ arrayBuffer(): Promise<ArrayBuffer> }>\n}\ninterface OpfsDirectoryHandle {\n\tgetDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectoryHandle>\n\tgetFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileHandle>\n\tremoveEntry(name: string, options?: { recursive?: boolean }): Promise<void>\n\tkeys(): AsyncIterableIterator<string>\n}\n\ninterface StorageManagerLike {\n\tgetDirectory(): Promise<OpfsDirectoryHandle>\n}\n\nfunction getStorageManager(): StorageManagerLike {\n\tconst nav = (globalThis as unknown as { navigator?: { storage?: StorageManagerLike } }).navigator\n\tif (!nav?.storage || typeof nav.storage.getDirectory !== 'function') {\n\t\tthrow new Error(\n\t\t\t'OPFS is unavailable in this environment. The OPFS blob store requires a browser with Origin Private File System support; use MemoryBlobStore or a server-side store elsewhere.',\n\t\t)\n\t}\n\treturn nav.storage\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' &&\n\t\terror !== null &&\n\t\t(error as { name?: string }).name === 'NotFoundError'\n\t)\n}\n\n/**\n * Create an {@link OpfsBlobDirectory} backed by the browser's Origin Private\n * File System, rooted at a named subdirectory. It does not request persistent\n * storage: that is `app.storage.persistence`, kept off the startup path.\n *\n * @param rootDirName - The OPFS subdirectory to store blobs under (default `kora-blobs`)\n * @throws {Error} If OPFS is unavailable (e.g. called outside a browser)\n */\nexport async function createOpfsBlobDirectory(\n\trootDirName = 'kora-blobs',\n): Promise<OpfsBlobDirectory> {\n\tconst storage = getStorageManager()\n\t// NEW-STORE-4: no persist() here. In Firefox it opens a permission prompt that\n\t// stays pending until the user answers, and this runs inside app startup.\n\t// Durable storage is checked and requested by StoragePersistence\n\t// (app.storage.persistence), never on the startup path.\n\tconst opfsRoot = await storage.getDirectory()\n\tconst root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true })\n\n\tasync function shardHandle(shard: string, create: boolean): Promise<OpfsDirectoryHandle | null> {\n\t\ttry {\n\t\t\treturn await root.getDirectoryHandle(shard, { create })\n\t\t} catch (error) {\n\t\t\tif (isNotFoundError(error)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\treturn {\n\t\tasync read(shard, name) {\n\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\tif (!dir) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet handle: OpfsFileHandle\n\t\t\ttry {\n\t\t\t\thandle = await dir.getFileHandle(name, { create: false })\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tconst file = await handle.getFile()\n\t\t\treturn new Uint8Array(await file.arrayBuffer())\n\t\t},\n\t\tasync write(shard, name, bytes) {\n\t\t\tconst dir = await root.getDirectoryHandle(shard, { create: true })\n\t\t\tconst handle = await dir.getFileHandle(name, { create: true })\n\t\t\t// createWritable buffers to a swap file and only commits on close(), so\n\t\t\t// the target file is updated atomically. Combined with integrity-on-read,\n\t\t\t// a torn write is never trusted.\n\t\t\tconst writable = await handle.createWritable({ keepExistingData: false })\n\t\t\ttry {\n\t\t\t\tawait writable.write(bytes)\n\t\t\t} finally {\n\t\t\t\tawait writable.close()\n\t\t\t}\n\t\t},\n\t\tasync remove(shard, name) {\n\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\tif (!dir) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait dir.removeEntry(name)\n\t\t\t\treturn true\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t},\n\t\tasync has(shard, name) {\n\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\tif (!dir) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait dir.getFileHandle(name, { create: false })\n\t\t\t\treturn true\n\t\t\t} catch (error) {\n\t\t\t\tif (isNotFoundError(error)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t},\n\t\tasync count() {\n\t\t\tlet total = 0\n\t\t\tfor await (const shard of root.keys()) {\n\t\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\t\tif (!dir) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor await (const name of dir.keys()) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\ttotal++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn total\n\t\t},\n\t\tasync keys() {\n\t\t\tconst names: string[] = []\n\t\t\tfor await (const shard of root.keys()) {\n\t\t\t\tconst dir = await shardHandle(shard, false)\n\t\t\t\tif (!dir) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor await (const name of dir.keys()) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\tnames.push(name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn names\n\t\t},\n\t}\n}\n\n/**\n * Create a persistent {@link OpfsBlobStore} backed by real browser OPFS.\n *\n * @param rootDirName - The OPFS subdirectory to store blobs under (default `kora-blobs`)\n * @throws {Error} If OPFS is unavailable (e.g. called outside a browser)\n */\nexport async function createOpfsBlobStore(rootDirName = 'kora-blobs'): Promise<OpfsBlobStore> {\n\treturn new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName))\n}\n","import { hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/** Default chunk size for out-of-band blob transfer (256 KiB). */\nexport const DEFAULT_CHUNK_SIZE = 256 * 1024\n\n/**\n * A manifest describing how a blob is split into content-addressed chunks.\n *\n * The manifest is small and travels with the operation stream (or alongside it);\n * the chunk bytes travel out of band. Because every chunk is content-addressed,\n * identical chunks across blobs are stored and transferred once, and a peer only\n * ever requests chunk hashes it does not already hold.\n */\nexport interface BlobManifest {\n\t/** Hex SHA-256 hash of the full reassembled blob (its content address). */\n\tblobHash: string\n\t/** Total size of the full blob in bytes. */\n\tsize: number\n\t/** The chunk size used to split the blob. */\n\tchunkSize: number\n\t/** Ordered hashes of each chunk. Repeats when identical chunks recur. */\n\tchunkHashes: string[]\n\tmimeType?: string\n\tfilename?: string\n}\n\n/** Optional metadata carried on a manifest. */\nexport interface BlobManifestMetadata {\n\tmimeType?: string\n\tfilename?: string\n}\n\n/**\n * Split a blob into content-addressed chunks and produce its manifest.\n *\n * @param bytes - The full blob content\n * @param chunkSize - Bytes per chunk (defaults to {@link DEFAULT_CHUNK_SIZE})\n * @param metadata - Optional mime type / filename to record on the manifest\n * @returns The manifest and a map of chunk hash to chunk bytes (deduplicated)\n */\nexport async function chunkBlob(\n\tbytes: Uint8Array,\n\tchunkSize: number = DEFAULT_CHUNK_SIZE,\n\tmetadata: BlobManifestMetadata = {},\n): Promise<{ manifest: BlobManifest; chunks: Map<string, Uint8Array> }> {\n\tif (chunkSize <= 0) {\n\t\tthrow new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`)\n\t}\n\n\tconst chunkHashes: string[] = []\n\tconst chunks = new Map<string, Uint8Array>()\n\n\tfor (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {\n\t\tconst end = Math.min(offset + chunkSize, bytes.byteLength)\n\t\tconst chunk = new Uint8Array(end - offset)\n\t\tchunk.set(bytes.subarray(offset, end))\n\t\tconst hash = await hashBlob(chunk)\n\t\tchunkHashes.push(hash)\n\t\t// Identical chunks collapse to one stored entry; the ordered hash list\n\t\t// still records every position so reassembly reproduces the exact bytes.\n\t\tif (!chunks.has(hash)) {\n\t\t\tchunks.set(hash, chunk)\n\t\t}\n\t}\n\n\tconst blobHash = await hashBlob(bytes)\n\tconst manifest: BlobManifest = {\n\t\tblobHash,\n\t\tsize: bytes.byteLength,\n\t\tchunkSize,\n\t\tchunkHashes,\n\t}\n\tif (metadata.mimeType !== undefined) {\n\t\tmanifest.mimeType = metadata.mimeType\n\t}\n\tif (metadata.filename !== undefined) {\n\t\tmanifest.filename = metadata.filename\n\t}\n\n\treturn { manifest, chunks }\n}\n\n/**\n * Reassemble a blob from chunks held in a content-addressed store, verifying\n * integrity at both levels: each chunk is verified by the store on read, and the\n * reassembled whole is verified against the manifest's blob hash.\n *\n * @param manifest - The blob manifest\n * @param chunkStore - A store holding the chunk bytes keyed by chunk hash\n * @returns The full reassembled blob bytes\n * @throws {BlobIntegrityError} If the reassembled bytes do not match blobHash\n * @throws {Error} If a chunk named in the manifest is missing from the store\n */\nexport async function reassembleBlob(\n\tmanifest: BlobManifest,\n\tchunkStore: ContentAddressedBlobStore,\n): Promise<Uint8Array> {\n\tconst parts: Uint8Array[] = []\n\tlet total = 0\n\n\tfor (const hash of manifest.chunkHashes) {\n\t\tconst chunk = await chunkStore.get(hash)\n\t\tif (chunk === null) {\n\t\t\tthrow new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`)\n\t\t}\n\t\tparts.push(chunk)\n\t\ttotal += chunk.byteLength\n\t}\n\n\tconst out = new Uint8Array(total)\n\tlet offset = 0\n\tfor (const part of parts) {\n\t\tout.set(part, offset)\n\t\toffset += part.byteLength\n\t}\n\n\tconst actual = await hashBlob(out)\n\tif (actual !== manifest.blobHash) {\n\t\tthrow new BlobIntegrityError(manifest.blobHash, actual)\n\t}\n\treturn out\n}\n","import { type BlobRef, hashBlob } from '@korajs/core'\nimport { type BlobManifest, chunkBlob, reassembleBlob } from './blob-chunking'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * A source of chunk bytes on the sending side of a transfer. In production this\n * is backed by the sync connection (the receiver requests a chunk hash, the peer\n * that holds the blob answers with the bytes); in tests it is backed by a map.\n */\nexport interface ChunkProvider {\n\t/** Fetch the bytes for a chunk hash, or null if the provider does not have it. */\n\tgetChunk(hash: string): Promise<Uint8Array | null>\n}\n\n/** Result of receiving a blob out of band. */\nexport interface ReceiveBlobResult {\n\t/** The content-addressed reference to the reassembled blob. */\n\tref: BlobRef\n\t/** Number of chunks actually fetched from the provider. */\n\tchunksFetched: number\n\t/** Number of chunks skipped because they were already held locally. */\n\tchunksSkipped: number\n}\n\n/** Stores used while receiving a blob. */\nexport interface ReceiveBlobStores {\n\t/**\n\t * Where fetched chunks are staged, keyed by chunk hash. A persistent chunk\n\t * store makes a transfer resumable: an interrupted transfer leaves its chunks\n\t * here, and a resumed transfer skips them.\n\t */\n\tchunkStore: ContentAddressedBlobStore\n\t/** Where the fully reassembled, verified blob is written. */\n\tblobStore: ContentAddressedBlobStore\n}\n\n/**\n * Receive a blob out of band, chunk by chunk, resumably and with integrity\n * verification at every step.\n *\n * For each chunk named in the manifest: if the chunk store already holds it (a\n * prior, possibly interrupted transfer), it is skipped; otherwise it is fetched\n * from the provider and verified to hash to the expected chunk hash before being\n * staged. Once every chunk is present, the blob is reassembled, verified against\n * the manifest's blob hash, and written to the blob store (which deduplicates).\n *\n * The transfer is idempotent: running it again after completion fetches nothing\n * and returns the same reference.\n *\n * @param manifest - The manifest of the blob to receive\n * @param provider - The source of chunk bytes\n * @param stores - The chunk staging store and the destination blob store\n * @returns The reference plus how many chunks were fetched versus skipped\n * @throws {BlobIntegrityError} If a fetched chunk or the whole blob fails verification\n * @throws {Error} If the provider cannot supply a required chunk\n */\nexport async function receiveBlob(\n\tmanifest: BlobManifest,\n\tprovider: ChunkProvider,\n\tstores: ReceiveBlobStores,\n): Promise<ReceiveBlobResult> {\n\tlet chunksFetched = 0\n\tlet chunksSkipped = 0\n\n\t// Distinct chunk hashes only: a manifest may repeat a hash, but we need each\n\t// unique chunk exactly once. Skipping duplicates also means a blob made of\n\t// identical chunks transfers that chunk a single time.\n\tconst seen = new Set<string>()\n\tfor (const hash of manifest.chunkHashes) {\n\t\tif (seen.has(hash)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(hash)\n\n\t\tif (await stores.chunkStore.has(hash)) {\n\t\t\tchunksSkipped++\n\t\t\tcontinue\n\t\t}\n\n\t\tconst bytes = await provider.getChunk(hash)\n\t\tif (bytes === null) {\n\t\t\tthrow new Error(`Blob transfer failed: provider could not supply chunk ${hash}`)\n\t\t}\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\tawait stores.chunkStore.put(bytes)\n\t\tchunksFetched++\n\t}\n\n\tconst full = await reassembleBlob(manifest, stores.chunkStore)\n\tconst ref = await stores.blobStore.put(full, {\n\t\tmimeType: manifest.mimeType,\n\t\tfilename: manifest.filename,\n\t})\n\n\treturn { ref, chunksFetched, chunksSkipped }\n}\n\n/**\n * Prepare a blob for out-of-band sending: split it into chunks, stage those\n * chunks in a content-addressed store, and return the manifest plus a\n * {@link ChunkProvider} that serves the chunks from that store.\n *\n * @param bytes - The full blob content\n * @param chunkStore - Where to stage the chunks (served to receivers)\n * @param options - Optional chunk size and metadata\n * @returns The manifest and a provider that serves the staged chunks\n */\nexport async function prepareBlobForSend(\n\tbytes: Uint8Array,\n\tchunkStore: ContentAddressedBlobStore,\n\toptions: { chunkSize?: number; mimeType?: string; filename?: string } = {},\n): Promise<{ manifest: BlobManifest; provider: ChunkProvider }> {\n\tconst { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {\n\t\tmimeType: options.mimeType,\n\t\tfilename: options.filename,\n\t})\n\tfor (const chunk of chunks.values()) {\n\t\tawait chunkStore.put(chunk)\n\t}\n\tconst provider: ChunkProvider = {\n\t\tgetChunk: (hash) => chunkStore.get(hash),\n\t}\n\treturn { manifest, provider }\n}\n","import { type BlobRef, type BlobRefMetadata, hashBlob } from '@korajs/core'\nimport { type BlobManifest, chunkBlob } from './blob-chunking'\nimport type { ChunkProvider } from './blob-transfer'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * Serialize a {@link BlobManifest} to canonical bytes for content addressing.\n *\n * Fields are emitted in a fixed order so the same manifest always produces the\n * same bytes, which is what lets the manifest be stored and fetched under its own\n * SHA-256 hash. Optional fields are omitted when absent.\n */\nexport function serializeBlobManifest(manifest: BlobManifest): Uint8Array {\n\tconst canonical: Record<string, unknown> = {\n\t\tblobHash: manifest.blobHash,\n\t\tsize: manifest.size,\n\t\tchunkSize: manifest.chunkSize,\n\t\tchunkHashes: manifest.chunkHashes,\n\t}\n\tif (manifest.mimeType !== undefined) {\n\t\tcanonical.mimeType = manifest.mimeType\n\t}\n\tif (manifest.filename !== undefined) {\n\t\tcanonical.filename = manifest.filename\n\t}\n\treturn new TextEncoder().encode(JSON.stringify(canonical))\n}\n\n/**\n * Parse canonical manifest bytes back into a {@link BlobManifest}, validating the\n * shape. Throws on malformed input rather than returning a partial manifest.\n */\nexport function parseBlobManifest(bytes: Uint8Array): BlobManifest {\n\tconst parsed: unknown = JSON.parse(new TextDecoder().decode(bytes))\n\tif (typeof parsed !== 'object' || parsed === null) {\n\t\tthrow new Error('Invalid blob manifest: not an object')\n\t}\n\tconst obj = parsed as Record<string, unknown>\n\tif (typeof obj.blobHash !== 'string') {\n\t\tthrow new Error('Invalid blob manifest: missing blobHash')\n\t}\n\tif (typeof obj.size !== 'number' || typeof obj.chunkSize !== 'number') {\n\t\tthrow new Error('Invalid blob manifest: missing size or chunkSize')\n\t}\n\tif (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== 'string')) {\n\t\tthrow new Error('Invalid blob manifest: chunkHashes must be an array of strings')\n\t}\n\tconst manifest: BlobManifest = {\n\t\tblobHash: obj.blobHash,\n\t\tsize: obj.size,\n\t\tchunkSize: obj.chunkSize,\n\t\tchunkHashes: obj.chunkHashes as string[],\n\t}\n\tif (typeof obj.mimeType === 'string') {\n\t\tmanifest.mimeType = obj.mimeType\n\t}\n\tif (typeof obj.filename === 'string') {\n\t\tmanifest.filename = obj.filename\n\t}\n\treturn manifest\n}\n\n/**\n * Store a blob for out-of-band transfer, addressed entirely by content.\n *\n * Stages the blob's chunks (so this device can serve them), stores the full blob\n * (so local reads by hash are O(1)), and stores the blob's manifest as its own\n * content-addressed object. The returned {@link BlobRef} carries `manifestHash`,\n * so a peer that receives the reference in a synced record can pull the bytes\n * knowing only the reference — no separate manifest hand-off.\n *\n * @param store - The content-addressed store to stage chunks, blob, and manifest in\n * @param bytes - The full blob content\n * @param options - Optional chunk size and metadata\n * @returns The reference (including `manifestHash`) and the manifest\n */\nexport async function putBlobForTransfer(\n\tstore: ContentAddressedBlobStore,\n\tbytes: Uint8Array,\n\toptions: { chunkSize?: number } & BlobRefMetadata = {},\n): Promise<{ ref: BlobRef; manifest: BlobManifest }> {\n\tconst { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {\n\t\t...(options.mimeType !== undefined ? { mimeType: options.mimeType } : {}),\n\t\t...(options.filename !== undefined ? { filename: options.filename } : {}),\n\t})\n\tfor (const chunk of chunks.values()) {\n\t\tawait store.put(chunk)\n\t}\n\t// Full blob for local O(1) reads by content hash.\n\tconst baseRef = await store.put(bytes, {\n\t\t...(options.mimeType !== undefined ? { mimeType: options.mimeType } : {}),\n\t\t...(options.filename !== undefined ? { filename: options.filename } : {}),\n\t})\n\t// Manifest as its own content-addressed object, served over the same channel.\n\tconst manifestRef = await store.put(serializeBlobManifest(manifest))\n\treturn { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest }\n}\n\n/**\n * Fetch and integrity-verify a blob's manifest from a {@link ChunkProvider}.\n *\n * The manifest is a content-addressed object like a chunk, so it is requested by\n * its hash, verified to hash to that value, and parsed. Used to resolve a\n * {@link BlobRef}'s manifest before pulling the blob's chunks.\n *\n * @param provider - The source to fetch the manifest bytes from\n * @param manifestHash - The manifest's content hash (from `BlobRef.manifestHash`)\n * @throws {BlobIntegrityError} If the fetched bytes do not hash to `manifestHash`\n * @throws {Error} If the provider cannot supply the manifest\n */\nexport async function fetchBlobManifest(\n\tprovider: ChunkProvider,\n\tmanifestHash: string,\n): Promise<BlobManifest> {\n\tconst bytes = await provider.getChunk(manifestHash)\n\tif (bytes === null) {\n\t\tthrow new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`)\n\t}\n\tconst actual = await hashBlob(bytes)\n\tif (actual !== manifestHash) {\n\t\tthrow new BlobIntegrityError(manifestHash, actual)\n\t}\n\treturn parseBlobManifest(bytes)\n}\n\n/**\n * Resolve the manifest for a {@link BlobRef} that was stored via\n * {@link putBlobForTransfer}. Fetches and verifies the manifest by the ref's\n * `manifestHash`.\n *\n * @throws {Error} If the ref has no `manifestHash` (it was not stored for transfer)\n */\nexport async function resolveBlobManifest(\n\tprovider: ChunkProvider,\n\tref: BlobRef,\n): Promise<BlobManifest> {\n\tif (ref.manifestHash === undefined) {\n\t\tthrow new Error(\n\t\t\t`Blob ${ref.hash} has no manifest to resolve. Store it with putBlobForTransfer (or app.blobs.put) so its reference carries a manifestHash, or pull with an explicit manifest.`,\n\t\t)\n\t}\n\treturn fetchBlobManifest(provider, ref.manifestHash)\n}\n","import { hashBlob } from '@korajs/core'\nimport { type ContentAddressedBlobStore, MemoryBlobStore } from './content-addressed-blob-store'\n\n/**\n * The server-side blob callbacks a `KoraSyncServer` accepts: read a chunk by\n * hash (to serve it) and persist an uploaded chunk by hash (to store it).\n */\nexport interface ServerBlobCallbacks {\n\tresolveBlobChunk: (hash: string) => Promise<Uint8Array | null>\n\tpersistBlobChunk: (hash: string, bytes: Uint8Array) => Promise<void>\n}\n\n/**\n * Adapt a {@link ContentAddressedBlobStore} into the read/persist callbacks a\n * `KoraSyncServer` needs for central blob storage. The server package depends\n * only on these plain function signatures, so a self-hosted server can back its\n * blob storage with any store (for example a `FilesystemBlobStore`) without the\n * server package taking a dependency on `@korajs/store`.\n *\n * @example\n * ```typescript\n * const server = new KoraSyncServer({\n * store,\n * ...toServerBlobCallbacks(new FilesystemBlobStore('/var/kora/blobs')),\n * })\n * ```\n */\nexport function toServerBlobCallbacks(store: ContentAddressedBlobStore): ServerBlobCallbacks {\n\treturn {\n\t\tresolveBlobChunk: (hash) => store.get(hash),\n\t\tasync persistBlobChunk(hash, bytes) {\n\t\t\t// The store keys by the hash it computes from the bytes; verify it matches\n\t\t\t// the declared hash so a mismatched upload is rejected, not silently stored\n\t\t\t// under the wrong key.\n\t\t\tconst actual = await hashBlob(bytes)\n\t\t\tif (actual !== hash) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tawait store.put(bytes)\n\t\t},\n\t}\n}\n\n/**\n * Convenience: an in-memory central blob store plus its server callbacks. For\n * tests and single-process deployments. Persistent deployments should back\n * {@link toServerBlobCallbacks} with a durable store instead.\n */\nexport function createMemoryServerBlobStore(): {\n\tstore: MemoryBlobStore\n\tcallbacks: ServerBlobCallbacks\n} {\n\tconst store = new MemoryBlobStore()\n\treturn { store, callbacks: toServerBlobCallbacks(store) }\n}\n","import { type BlobRef, isBlobRef } from '@korajs/core'\nimport { parseBlobManifest } from './blob-manifest-transfer'\nimport type { ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/** Outcome of a blob garbage-collection sweep. */\nexport interface BlobGcResult {\n\t/** Total distinct blobs held before the sweep. */\n\tscanned: number\n\t/** Distinct hashes kept because a live reference points to them (blob, manifest, or chunk). */\n\tlive: number\n\t/** Number of blobs deleted (or, with `dryRun`, that would be deleted). */\n\tcollected: number\n\t/** The hashes deleted (or, with `dryRun`, that would be deleted). */\n\tcollectedHashes: string[]\n}\n\n/** Options for {@link collectBlobGarbage}. */\nexport interface BlobGcOptions {\n\t/** Compute what would be collected without deleting anything. */\n\tdryRun?: boolean\n}\n\n/**\n * Extract the {@link BlobRef}s referenced by a materialized record. Scans the\n * record's top-level field values, so it works without the schema; a `blob`\n * field simply holds a `BlobRef`.\n */\nexport function extractBlobRefs(record: Record<string, unknown>): BlobRef[] {\n\tconst refs: BlobRef[] = []\n\tfor (const value of Object.values(record)) {\n\t\tif (isBlobRef(value)) {\n\t\t\trefs.push(value)\n\t\t}\n\t}\n\treturn refs\n}\n\n/**\n * Garbage-collect a content-addressed blob store by mark-and-sweep: keep every\n * blob reachable from a live reference, delete the rest.\n *\n * The live set is closed over the reference graph — for each live {@link BlobRef}\n * it retains the blob hash, the manifest hash, and every chunk hash named in that\n * manifest. Because chunks (and blobs) are shared by content address, a chunk\n * referenced by any surviving blob is retained even if another blob that used it\n * was collected. This makes GC safe under deduplication.\n *\n * Mark-and-sweep (rather than reference counting) is deliberate: counts are\n * fragile under concurrent edits and CRDT merges, whereas a sweep against the\n * current live set is always correct for the state it observes. Run it\n * periodically or on demand.\n *\n * A manifest that cannot be read or parsed (corruption) contributes no chunk\n * hashes; its own hash still counts as live. The blob it describes is already\n * unusable, so its chunks becoming collectable is acceptable, and GC never\n * throws on a single bad manifest.\n *\n * @param store - The blob store to sweep\n * @param liveRefs - Every reference still reachable from live records\n * @param options - `dryRun` to report without deleting\n */\nexport async function collectBlobGarbage(\n\tstore: ContentAddressedBlobStore,\n\tliveRefs: Iterable<BlobRef>,\n\toptions: BlobGcOptions = {},\n): Promise<BlobGcResult> {\n\tconst live = new Set<string>()\n\tfor (const ref of liveRefs) {\n\t\tlive.add(ref.hash)\n\t\tif (ref.manifestHash === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tlive.add(ref.manifestHash)\n\t\ttry {\n\t\t\tconst manifestBytes = await store.get(ref.manifestHash)\n\t\t\tif (manifestBytes !== null) {\n\t\t\t\tconst manifest = parseBlobManifest(manifestBytes)\n\t\t\t\tfor (const chunkHash of manifest.chunkHashes) {\n\t\t\t\t\tlive.add(chunkHash)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// A corrupt/unreadable manifest contributes no chunk hashes; the blob is\n\t\t\t// already broken. Never abort the whole sweep for one bad manifest.\n\t\t}\n\t}\n\n\tconst all = await store.list()\n\tconst collectedHashes: string[] = []\n\tfor (const hash of all) {\n\t\tif (live.has(hash)) {\n\t\t\tcontinue\n\t\t}\n\t\tcollectedHashes.push(hash)\n\t\tif (!options.dryRun) {\n\t\t\tawait store.delete(hash)\n\t\t}\n\t}\n\n\treturn {\n\t\tscanned: all.length,\n\t\tlive: live.size,\n\t\tcollected: collectedHashes.length,\n\t\tcollectedHashes,\n\t}\n}\n","import { generateUUIDv7 } from '@korajs/core'\nimport type { ChunkProvider } from './blob-transfer'\nimport type { ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * Blob chunk exchange messages. These ride any message transport (the sync\n * WebSocket in production, an in-memory pair in tests). A receiver requests a\n * chunk by hash and correlates the answer by `requestId`.\n */\nexport interface ChunkRequestMessage {\n\ttype: 'blob-chunk-request'\n\trequestId: string\n\thash: string\n}\nexport interface ChunkResponseMessage {\n\ttype: 'blob-chunk-response'\n\trequestId: string\n\t/** The chunk bytes, or null when the responder does not hold that hash. */\n\tbytes: Uint8Array | null\n\t/**\n\t * The server refused the request for rate, not because the chunk is missing\n\t * (RT-24). The requester retries after `retryAfterMs` instead of failing.\n\t */\n\tthrottled?: boolean\n\t/** With `throttled`: how long to wait before retrying, in ms. */\n\tretryAfterMs?: number\n}\nexport type ChunkMessage = ChunkRequestMessage | ChunkResponseMessage\n\n/**\n * A bidirectional message port carrying {@link ChunkMessage}s. Implemented over\n * the sync connection in production; `createChunkPortPair` provides an in-memory\n * duplex pair for tests.\n */\nexport interface ChunkMessagePort {\n\tsend(message: ChunkMessage): void\n\t/** Register a handler for incoming messages. Multiple handlers are allowed. */\n\tonMessage(handler: (message: ChunkMessage) => void): void\n}\n\n/** A {@link ChunkProvider} that fetches chunks over a message port. */\nexport interface RemoteChunkProvider extends ChunkProvider {\n\t/** Number of requests still awaiting a response (for diagnostics/tests). */\n\tpendingCount(): number\n}\n\n/** Options for {@link createRemoteChunkProvider}. */\nexport interface RemoteChunkProviderOptions {\n\t/** Per-attempt timeout in ms (default 30s). */\n\ttimeoutMs?: number\n\t/**\n\t * Longest total time one chunk may spend waiting on \"throttled\" answers before\n\t * the request fails (default 10 minutes). The transfer is resumable, so a failed\n\t * request can simply be retried later.\n\t */\n\tmaxThrottleWaitMs?: number\n\t/** Smallest back-off after a throttled answer, in ms (default 250). */\n\tminThrottleDelayMs?: number\n\t/** Largest single back-off after a throttled answer, in ms (default 30s). */\n\tmaxThrottleDelayMs?: number\n}\n\n/**\n * Create a {@link ChunkProvider} that requests chunks over a message port,\n * correlating each answer to its request and timing out a stalled request so a\n * dropped response cannot hang a transfer forever (the transfer is resumable, so\n * a timed-out request can simply be retried).\n *\n * A \"throttled\" answer (the server's blob request budget is spent, RT-24) is not a\n * failure: the request is re-sent after the server's `retryAfterMs` (bounded, with\n * exponential back-off when absent), until `maxThrottleWaitMs` has elapsed.\n *\n * @param port - The message port to the peer that holds the blob\n * @param options - Timeouts and throttle back-off bounds\n */\nexport function createRemoteChunkProvider(\n\tport: ChunkMessagePort,\n\toptions: RemoteChunkProviderOptions = {},\n): RemoteChunkProvider {\n\tconst timeoutMs = options.timeoutMs ?? 30_000\n\tconst maxThrottleWaitMs = options.maxThrottleWaitMs ?? 10 * 60_000\n\tconst minDelayMs = options.minThrottleDelayMs ?? 250\n\tconst maxDelayMs = options.maxThrottleDelayMs ?? 30_000\n\tconst pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\thash: string\n\t\t\tresolve: (bytes: Uint8Array | null) => void\n\t\t\treject: (error: Error) => void\n\t\t\ttimer: ReturnType<typeof setTimeout>\n\t\t\tstartedAtMs: number\n\t\t\tthrottledCount: number\n\t\t}\n\t>()\n\n\tconst armTimeout = (requestId: string, hash: string): ReturnType<typeof setTimeout> =>\n\t\tsetTimeout(() => {\n\t\t\tconst entry = pending.get(requestId)\n\t\t\tif (!entry) return\n\t\t\tpending.delete(requestId)\n\t\t\tentry.reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`))\n\t\t}, timeoutMs)\n\n\tport.onMessage((message) => {\n\t\tif (message.type !== 'blob-chunk-response') {\n\t\t\treturn\n\t\t}\n\t\tconst entry = pending.get(message.requestId)\n\t\tif (!entry) {\n\t\t\treturn\n\t\t}\n\t\tclearTimeout(entry.timer)\n\t\tif (message.throttled === true) {\n\t\t\t// Over the server's blob budget: back off and ask again, same request id.\n\t\t\tentry.throttledCount += 1\n\t\t\tconst backoff = Math.min(maxDelayMs, minDelayMs * 2 ** (entry.throttledCount - 1))\n\t\t\tconst delay = Math.min(maxDelayMs, Math.max(minDelayMs, message.retryAfterMs ?? backoff))\n\t\t\tif (Date.now() + delay - entry.startedAtMs > maxThrottleWaitMs) {\n\t\t\t\tpending.delete(message.requestId)\n\t\t\t\tentry.reject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`Blob chunk request for ${entry.hash} was throttled for longer than ${maxThrottleWaitMs}ms`,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tentry.timer = setTimeout(() => {\n\t\t\t\tif (pending.get(message.requestId) !== entry) return\n\t\t\t\tentry.timer = armTimeout(message.requestId, entry.hash)\n\t\t\t\tport.send({ type: 'blob-chunk-request', requestId: message.requestId, hash: entry.hash })\n\t\t\t}, delay)\n\t\t\treturn\n\t\t}\n\t\tpending.delete(message.requestId)\n\t\tentry.resolve(message.bytes)\n\t})\n\n\treturn {\n\t\tpendingCount: () => pending.size,\n\t\tgetChunk(hash: string): Promise<Uint8Array | null> {\n\t\t\tconst requestId = generateUUIDv7()\n\t\t\treturn new Promise<Uint8Array | null>((resolve, reject) => {\n\t\t\t\tpending.set(requestId, {\n\t\t\t\t\thash,\n\t\t\t\t\tresolve,\n\t\t\t\t\treject,\n\t\t\t\t\ttimer: armTimeout(requestId, hash),\n\t\t\t\t\tstartedAtMs: Date.now(),\n\t\t\t\t\tthrottledCount: 0,\n\t\t\t\t})\n\t\t\t\tport.send({ type: 'blob-chunk-request', requestId, hash })\n\t\t\t})\n\t\t},\n\t}\n}\n\n/**\n * Serve blob chunk requests arriving on a message port from a blob store. Answers\n * each `blob-chunk-request` with the bytes for that hash, or null when the store\n * does not hold it. Returns nothing; attach it once per connection.\n *\n * @param port - The message port from the peer requesting chunks\n * @param blobStore - The store to serve chunks from (keyed by chunk hash)\n */\nexport function serveBlobChunks(\n\tport: ChunkMessagePort,\n\tblobStore: ContentAddressedBlobStore,\n): void {\n\tport.onMessage((message) => {\n\t\tif (message.type !== 'blob-chunk-request') {\n\t\t\treturn\n\t\t}\n\t\tvoid blobStore.get(message.hash).then(\n\t\t\t(bytes) => {\n\t\t\t\tport.send({ type: 'blob-chunk-response', requestId: message.requestId, bytes })\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t// Integrity failure or read error: report as not-held rather than\n\t\t\t\t// crashing the connection. The requester treats null as unavailable.\n\t\t\t\tport.send({ type: 'blob-chunk-response', requestId: message.requestId, bytes: null })\n\t\t\t},\n\t\t)\n\t})\n}\n\n/**\n * Create a connected in-memory pair of {@link ChunkMessagePort}s. Messages sent\n * on one arrive on the other, asynchronously (next microtask) so the pair models\n * a real transport rather than re-entrant synchronous delivery. For tests.\n */\nexport function createChunkPortPair(): { a: ChunkMessagePort; b: ChunkMessagePort } {\n\tconst handlersA: Array<(m: ChunkMessage) => void> = []\n\tconst handlersB: Array<(m: ChunkMessage) => void> = []\n\n\tconst a: ChunkMessagePort = {\n\t\tsend(message) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tfor (const handler of handlersB) {\n\t\t\t\t\thandler(message)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\tonMessage(handler) {\n\t\t\thandlersA.push(handler)\n\t\t},\n\t}\n\tconst b: ChunkMessagePort = {\n\t\tsend(message) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tfor (const handler of handlersA) {\n\t\t\t\t\thandler(message)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\tonMessage(handler) {\n\t\t\thandlersB.push(handler)\n\t\t},\n\t}\n\treturn { a, b }\n}\n"],"mappings":"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","names":["hashBlob","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob","hashBlob"]}
package/dist/internal.cjs CHANGED
@@ -1011,6 +1011,7 @@ async function writeDeleteInTx(env, scope, collection, id, options = {}) {
1011
1011
  };
1012
1012
  }
1013
1013
  async function buildLocalOperation(env, scope, input) {
1014
+ env.assertLocalWriteAllowed?.();
1014
1015
  const sequenceNumber = await scope.sequence.take();
1015
1016
  const causalDeps = scope.causal.depsFor(input.collection, input.extraCausalDeps);
1016
1017
  const operation = await (0, import_core10.createOperation)(