@mindees/data 0.22.0 → 0.22.2

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.
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
13
13
  /** The npm package name. */
14
14
  declare const name = "@mindees/data";
15
15
  /** The package version. All `@mindees/*` packages share one locked version line. */
16
- declare const VERSION = "0.22.0";
16
+ declare const VERSION = "0.22.2";
17
17
  /** Current maturity of this package. See the repository `STATUS.md`. */
18
18
  declare const maturity: Maturity;
19
19
  /**
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
12
12
  /** The npm package name. */
13
13
  const name = "@mindees/data";
14
14
  /** The package version. All `@mindees/*` packages share one locked version line. */
15
- const VERSION = "0.22.0";
15
+ const VERSION = "0.22.2";
16
16
  /** Current maturity of this package. See the repository `STATUS.md`. */
17
17
  const maturity = "experimental";
18
18
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/data` (Continuum) — local-first reactive store + sync.\n *\n * Phase 10 ships the **reactive document store**: {@link createCollection}, a\n * signals-native, in-memory collection with fine-grained reactive reads\n * (`get`/`has`/`all`/`where`/`size`), atomic mutations (`insert`/`upsert`/`update`/\n * `delete`/`clear`/`tx`), and {@link Collection.optimistic optimistic} changes that can\n * be rolled back. Built on `@mindees/core` signals only. Hybrid-logical-clock causality,\n * CRDT conflict resolution, the local-first sync engine, a reference sync server on the\n * `@mindees/data/server` subpath, and a persistence contract/export/restore path build\n * on this. Native durable adapters, production sync hardening, and CRDT-library/rich-text\n * interop remain research tracks.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/data'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.0'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport {\n type Collection,\n type CollectionOptions,\n createCollection,\n type Id,\n type OptimisticChange,\n} from './collection'\nexport {\n type Counter,\n counterDec,\n counterInc,\n counterValue,\n emptyCounter,\n mergeCounter,\n} from './counter'\nexport { DataError, type DataErrorCode } from './errors'\nexport {\n type Clock,\n type ClockOptions,\n compareHlc,\n createClock,\n decodeHlc,\n encodeHlc,\n type Hlc,\n} from './hlc'\nexport {\n type LwwMap,\n type LwwRegister,\n lwwDelete,\n lwwGet,\n lwwHas,\n lwwKeys,\n lwwSet,\n mergeLwwMap,\n mergeRegister,\n} from './lww'\nexport {\n emptyOrSet,\n mergeOrSet,\n type OrSet,\n orAdd,\n orHas,\n orRemove,\n orValues,\n} from './or-set'\nexport {\n createIndexedDbPersistence,\n createMemoryPersistence,\n createPersistentEngine,\n createWebStoragePersistence,\n type IndexedDbFactoryLike,\n type IndexedDbPersistenceOptions,\n loadSnapshot,\n type Persistence,\n type PersistentEngineOptions,\n persistEngine,\n type WebStorageLike,\n} from './persist'\nexport {\n type Cursor,\n createMemoryHub,\n createMutationLog,\n createSyncEngine,\n type MutationLog,\n type Op,\n type SyncEngine,\n type SyncEngineOptions,\n type SyncSnapshot,\n type SyncTransport,\n} from './sync'\nexport {\n type VersionVector,\n vvDominates,\n vvEquals,\n vvGet,\n vvMerge,\n vvObserve,\n} from './version-vector'\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;AAoBA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/data` (Continuum) — local-first reactive store + sync.\n *\n * Phase 10 ships the **reactive document store**: {@link createCollection}, a\n * signals-native, in-memory collection with fine-grained reactive reads\n * (`get`/`has`/`all`/`where`/`size`), atomic mutations (`insert`/`upsert`/`update`/\n * `delete`/`clear`/`tx`), and {@link Collection.optimistic optimistic} changes that can\n * be rolled back. Built on `@mindees/core` signals only. Hybrid-logical-clock causality,\n * CRDT conflict resolution, the local-first sync engine, a reference sync server on the\n * `@mindees/data/server` subpath, and a persistence contract/export/restore path build\n * on this. Native durable adapters, production sync hardening, and CRDT-library/rich-text\n * interop remain research tracks.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/data'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.2'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport {\n type Collection,\n type CollectionOptions,\n createCollection,\n type Id,\n type OptimisticChange,\n} from './collection'\nexport {\n type Counter,\n counterDec,\n counterInc,\n counterValue,\n emptyCounter,\n mergeCounter,\n} from './counter'\nexport { DataError, type DataErrorCode } from './errors'\nexport {\n type Clock,\n type ClockOptions,\n compareHlc,\n createClock,\n decodeHlc,\n encodeHlc,\n type Hlc,\n} from './hlc'\nexport {\n type LwwMap,\n type LwwRegister,\n lwwDelete,\n lwwGet,\n lwwHas,\n lwwKeys,\n lwwSet,\n mergeLwwMap,\n mergeRegister,\n} from './lww'\nexport {\n emptyOrSet,\n mergeOrSet,\n type OrSet,\n orAdd,\n orHas,\n orRemove,\n orValues,\n} from './or-set'\nexport {\n createIndexedDbPersistence,\n createMemoryPersistence,\n createPersistentEngine,\n createWebStoragePersistence,\n type IndexedDbFactoryLike,\n type IndexedDbPersistenceOptions,\n loadSnapshot,\n type Persistence,\n type PersistentEngineOptions,\n persistEngine,\n type WebStorageLike,\n} from './persist'\nexport {\n type Cursor,\n createMemoryHub,\n createMutationLog,\n createSyncEngine,\n type MutationLog,\n type Op,\n type SyncEngine,\n type SyncEngineOptions,\n type SyncSnapshot,\n type SyncTransport,\n} from './sync'\nexport {\n type VersionVector,\n vvDominates,\n vvEquals,\n vvGet,\n vvMerge,\n vvObserve,\n} from './version-vector'\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;AAoBA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
package/dist/persist.js CHANGED
@@ -104,15 +104,21 @@ function createIndexedDbPersistence(options = {}) {
104
104
  if (!factory) throw new Error("createIndexedDbPersistence: IndexedDB is unavailable; pass `factory`.");
105
105
  let dbPromise;
106
106
  const openDb = () => {
107
- if (!dbPromise) dbPromise = new Promise((resolve, reject) => {
108
- const request = factory.open(dbName, 1);
109
- request.onupgradeneeded = () => {
110
- const db = request.result;
111
- if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
112
- };
113
- request.onsuccess = () => resolve(request.result);
114
- request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB open failed"));
115
- });
107
+ if (!dbPromise) {
108
+ const pending = new Promise((resolve, reject) => {
109
+ const request = factory.open(dbName, 1);
110
+ request.onupgradeneeded = () => {
111
+ const db = request.result;
112
+ if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
113
+ };
114
+ request.onsuccess = () => resolve(request.result);
115
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB open failed"));
116
+ });
117
+ pending.catch(() => {
118
+ if (dbPromise === pending) dbPromise = void 0;
119
+ });
120
+ dbPromise = pending;
121
+ }
116
122
  return dbPromise;
117
123
  };
118
124
  const run = (mode, op) => openDb().then((db) => new Promise((resolve, reject) => {
@@ -1 +1 @@
1
- {"version":3,"file":"persist.js","names":[],"sources":["../src/persist.ts"],"sourcesContent":["/**\n * Persistence (10F) — a minimal async key/value capability so a Continuum replica\n * survives restart (mirrors the CLI's `FileSystem` / Pulse's `UpdateStorage`). Persist a\n * sync engine's {@link \"./sync\".SyncEngine.export snapshot} through any `Persistence`\n * and restore it on next launch, so `seq` survives and op ids never collide.\n *\n * `createMemoryPersistence` is the reference; `createWebStoragePersistence` adapts a Web Storage\n * (`localStorage`/`sessionStorage`); {@link persistEngine} wires auto-save + restore so a replica is\n * durable with one call. See `docs/adr/0016-continuum-server-persistence.md`.\n *\n * @module\n */\n\nimport {\n createSyncEngine,\n type SyncEngine,\n type SyncEngineOptions,\n type SyncSnapshot,\n} from './sync'\n\n/** A minimal async key/value store for persisting Continuum state. */\nexport interface Persistence {\n /** Read a value, or `null` if absent. */\n load(key: string): Promise<string | null>\n /** Write a value. */\n save(key: string, value: string): Promise<void>\n}\n\n/** An in-memory reference {@link Persistence} for tests and as a contract example. */\nexport function createMemoryPersistence(): Persistence {\n const store = new Map<string, string>()\n return {\n load: (key) => Promise.resolve(store.get(key) ?? null),\n save: (key, value) => {\n store.set(key, value)\n return Promise.resolve()\n },\n }\n}\n\n/** The synchronous Web Storage shape (`localStorage`/`sessionStorage`), injected so this stays DOM-free. */\nexport interface WebStorageLike {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n}\n\n/**\n * Adapt a Web Storage (`localStorage`/`sessionStorage`) to {@link Persistence}. Inject the storage\n * (`createWebStoragePersistence(localStorage)`) rather than reaching for a global, so it runs in any\n * environment and tests. Storage is synchronous; the async contract is satisfied trivially.\n */\nexport function createWebStoragePersistence(storage: WebStorageLike): Persistence {\n return {\n load: (key) => Promise.resolve(storage.getItem(key)),\n save: (key, value) => {\n storage.setItem(key, value)\n return Promise.resolve()\n },\n }\n}\n\n/** Load + parse a persisted {@link SyncSnapshot}, or `undefined` if absent / unparseable. */\nexport async function loadSnapshot<T>(\n persistence: Persistence,\n key: string,\n): Promise<SyncSnapshot<T> | undefined> {\n const raw = await persistence.load(key)\n if (raw === null) return undefined\n try {\n return JSON.parse(raw) as SyncSnapshot<T>\n } catch {\n // A corrupt/partial blob must not wedge startup — start fresh rather than throw.\n return undefined\n }\n}\n\n/**\n * Wrap a {@link SyncEngine} so every mutation (`set`/`delete`) and `sync()` auto-saves its snapshot\n * to `persistence` under `key`. Saves are SERIALIZED (chained) so a burst of edits can't write an\n * older snapshot last, and failures are swallowed (best-effort durability never breaks a mutation).\n * To RESTORE on next launch, pass `snapshot: await loadSnapshot(...)` into the engine first —\n * {@link createPersistentEngine} does both.\n */\nexport function persistEngine<T>(\n engine: SyncEngine<T>,\n persistence: Persistence,\n key: string,\n): SyncEngine<T> {\n let chain: Promise<void> = Promise.resolve()\n const save = (): void => {\n const snapshot = JSON.stringify(engine.export())\n chain = chain\n .then(() => persistence.save(key, snapshot))\n .then(\n () => undefined,\n () => undefined,\n )\n }\n return {\n ...engine,\n set(collection, recordId, value) {\n const op = engine.set(collection, recordId, value)\n save()\n return op\n },\n delete(collection, recordId) {\n const op = engine.delete(collection, recordId)\n save()\n return op\n },\n async sync(signal) {\n await engine.sync(signal)\n save()\n },\n }\n}\n\n/** Options for {@link createPersistentEngine}: a {@link SyncEngineOptions} minus `snapshot` (loaded here). */\nexport interface PersistentEngineOptions<T> extends Omit<SyncEngineOptions<T>, 'snapshot'> {\n /** Where to persist. */\n readonly persistence: Persistence\n /** The storage key for this replica's snapshot. */\n readonly key: string\n}\n\n/**\n * Create a durable {@link SyncEngine}: restore the persisted snapshot (so `seq`/HLC survive and op\n * ids never collide across restarts), then auto-save on every change. One call for a replica that\n * survives restart.\n */\nexport async function createPersistentEngine<T>(\n options: PersistentEngineOptions<T>,\n): Promise<SyncEngine<T>> {\n const { persistence, key, ...syncOptions } = options\n const snapshot = await loadSnapshot<T>(persistence, key)\n const engine = createSyncEngine<T>(snapshot ? { ...syncOptions, snapshot } : syncOptions)\n return persistEngine(engine, persistence, key)\n}\n\n// --- IndexedDB persistence (durable browser storage; large + async, beyond localStorage's sync ~5MB) ---\n//\n// A minimal structural subset of the DOM IndexedDB types, declared here so @mindees/data stays\n// DOM-lib-free (mirrors WebStorageLike). The real `globalThis.indexedDB` and `fake-indexeddb` are\n// structurally assignable to IndexedDbFactoryLike.\n\ninterface IdbRequestLike<T> {\n result: T\n error: unknown\n onsuccess: (() => void) | null\n onerror: (() => void) | null\n}\ninterface IdbOpenRequestLike extends IdbRequestLike<IdbDatabaseLike> {\n onupgradeneeded: (() => void) | null\n}\ninterface IdbObjectStoreLike {\n get(key: string): IdbRequestLike<unknown>\n put(value: string, key: string): IdbRequestLike<unknown>\n}\ninterface IdbTransactionLike {\n objectStore(name: string): IdbObjectStoreLike\n}\ninterface IdbDatabaseLike {\n objectStoreNames: { contains(name: string): boolean }\n createObjectStore(name: string): unknown\n transaction(storeNames: string, mode: 'readonly' | 'readwrite'): IdbTransactionLike\n}\n/** The minimal IndexedDB factory surface (a structural subset of the DOM `IDBFactory`). */\nexport interface IndexedDbFactoryLike {\n open(name: string, version?: number): IdbOpenRequestLike\n}\n\n/** Options for {@link createIndexedDbPersistence}. */\nexport interface IndexedDbPersistenceOptions {\n /** Database name (default `'mindees'`). */\n readonly databaseName?: string\n /** Object-store name (default `'continuum'`). */\n readonly storeName?: string\n /** The IndexedDB factory; defaults to `globalThis.indexedDB`. Inject (e.g. `fake-indexeddb`) elsewhere. */\n readonly factory?: IndexedDbFactoryLike\n}\n\n/**\n * A durable {@link Persistence} backed by IndexedDB — large, asynchronous browser storage (beyond\n * `localStorage`'s synchronous ~5MB cap), a better home for a growing Continuum op log/snapshot. The\n * database + object store open lazily on first use and are reused. Inject `factory` to run outside a\n * browser (tests, or a custom environment); throws if none is available.\n */\nexport function createIndexedDbPersistence(options: IndexedDbPersistenceOptions = {}): Persistence {\n const dbName = options.databaseName ?? 'mindees'\n const storeName = options.storeName ?? 'continuum'\n const factory = options.factory ?? (globalThis as { indexedDB?: IndexedDbFactoryLike }).indexedDB\n if (!factory) {\n throw new Error('createIndexedDbPersistence: IndexedDB is unavailable; pass `factory`.')\n }\n\n let dbPromise: Promise<IdbDatabaseLike> | undefined\n const openDb = (): Promise<IdbDatabaseLike> => {\n if (!dbPromise) {\n dbPromise = new Promise<IdbDatabaseLike>((resolve, reject) => {\n const request = factory.open(dbName, 1)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName)\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB open failed'))\n })\n }\n return dbPromise\n }\n\n const run = <T>(\n mode: 'readonly' | 'readwrite',\n op: (store: IdbObjectStoreLike) => IdbRequestLike<T>,\n ): Promise<T> =>\n openDb().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const request = op(db.transaction(storeName, mode).objectStore(storeName))\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'))\n }),\n )\n\n return {\n load: (key) =>\n run('readonly', (store) => store.get(key)).then((value) =>\n value === undefined || value === null ? null : String(value),\n ),\n save: (key, value) => run('readwrite', (store) => store.put(value, key)).then(() => undefined),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"persist.js","names":[],"sources":["../src/persist.ts"],"sourcesContent":["/**\n * Persistence (10F) — a minimal async key/value capability so a Continuum replica\n * survives restart (mirrors the CLI's `FileSystem` / Pulse's `UpdateStorage`). Persist a\n * sync engine's {@link \"./sync\".SyncEngine.export snapshot} through any `Persistence`\n * and restore it on next launch, so `seq` survives and op ids never collide.\n *\n * `createMemoryPersistence` is the reference; `createWebStoragePersistence` adapts a Web Storage\n * (`localStorage`/`sessionStorage`); {@link persistEngine} wires auto-save + restore so a replica is\n * durable with one call. See `docs/adr/0016-continuum-server-persistence.md`.\n *\n * @module\n */\n\nimport {\n createSyncEngine,\n type SyncEngine,\n type SyncEngineOptions,\n type SyncSnapshot,\n} from './sync'\n\n/** A minimal async key/value store for persisting Continuum state. */\nexport interface Persistence {\n /** Read a value, or `null` if absent. */\n load(key: string): Promise<string | null>\n /** Write a value. */\n save(key: string, value: string): Promise<void>\n}\n\n/** An in-memory reference {@link Persistence} for tests and as a contract example. */\nexport function createMemoryPersistence(): Persistence {\n const store = new Map<string, string>()\n return {\n load: (key) => Promise.resolve(store.get(key) ?? null),\n save: (key, value) => {\n store.set(key, value)\n return Promise.resolve()\n },\n }\n}\n\n/** The synchronous Web Storage shape (`localStorage`/`sessionStorage`), injected so this stays DOM-free. */\nexport interface WebStorageLike {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n}\n\n/**\n * Adapt a Web Storage (`localStorage`/`sessionStorage`) to {@link Persistence}. Inject the storage\n * (`createWebStoragePersistence(localStorage)`) rather than reaching for a global, so it runs in any\n * environment and tests. Storage is synchronous; the async contract is satisfied trivially.\n */\nexport function createWebStoragePersistence(storage: WebStorageLike): Persistence {\n return {\n load: (key) => Promise.resolve(storage.getItem(key)),\n save: (key, value) => {\n storage.setItem(key, value)\n return Promise.resolve()\n },\n }\n}\n\n/** Load + parse a persisted {@link SyncSnapshot}, or `undefined` if absent / unparseable. */\nexport async function loadSnapshot<T>(\n persistence: Persistence,\n key: string,\n): Promise<SyncSnapshot<T> | undefined> {\n const raw = await persistence.load(key)\n if (raw === null) return undefined\n try {\n return JSON.parse(raw) as SyncSnapshot<T>\n } catch {\n // A corrupt/partial blob must not wedge startup — start fresh rather than throw.\n return undefined\n }\n}\n\n/**\n * Wrap a {@link SyncEngine} so every mutation (`set`/`delete`) and `sync()` auto-saves its snapshot\n * to `persistence` under `key`. Saves are SERIALIZED (chained) so a burst of edits can't write an\n * older snapshot last, and failures are swallowed (best-effort durability never breaks a mutation).\n * To RESTORE on next launch, pass `snapshot: await loadSnapshot(...)` into the engine first —\n * {@link createPersistentEngine} does both.\n */\nexport function persistEngine<T>(\n engine: SyncEngine<T>,\n persistence: Persistence,\n key: string,\n): SyncEngine<T> {\n let chain: Promise<void> = Promise.resolve()\n const save = (): void => {\n const snapshot = JSON.stringify(engine.export())\n chain = chain\n .then(() => persistence.save(key, snapshot))\n .then(\n () => undefined,\n () => undefined,\n )\n }\n return {\n ...engine,\n set(collection, recordId, value) {\n const op = engine.set(collection, recordId, value)\n save()\n return op\n },\n delete(collection, recordId) {\n const op = engine.delete(collection, recordId)\n save()\n return op\n },\n async sync(signal) {\n await engine.sync(signal)\n save()\n },\n }\n}\n\n/** Options for {@link createPersistentEngine}: a {@link SyncEngineOptions} minus `snapshot` (loaded here). */\nexport interface PersistentEngineOptions<T> extends Omit<SyncEngineOptions<T>, 'snapshot'> {\n /** Where to persist. */\n readonly persistence: Persistence\n /** The storage key for this replica's snapshot. */\n readonly key: string\n}\n\n/**\n * Create a durable {@link SyncEngine}: restore the persisted snapshot (so `seq`/HLC survive and op\n * ids never collide across restarts), then auto-save on every change. One call for a replica that\n * survives restart.\n */\nexport async function createPersistentEngine<T>(\n options: PersistentEngineOptions<T>,\n): Promise<SyncEngine<T>> {\n const { persistence, key, ...syncOptions } = options\n const snapshot = await loadSnapshot<T>(persistence, key)\n const engine = createSyncEngine<T>(snapshot ? { ...syncOptions, snapshot } : syncOptions)\n return persistEngine(engine, persistence, key)\n}\n\n// --- IndexedDB persistence (durable browser storage; large + async, beyond localStorage's sync ~5MB) ---\n//\n// A minimal structural subset of the DOM IndexedDB types, declared here so @mindees/data stays\n// DOM-lib-free (mirrors WebStorageLike). The real `globalThis.indexedDB` and `fake-indexeddb` are\n// structurally assignable to IndexedDbFactoryLike.\n\ninterface IdbRequestLike<T> {\n result: T\n error: unknown\n onsuccess: (() => void) | null\n onerror: (() => void) | null\n}\ninterface IdbOpenRequestLike extends IdbRequestLike<IdbDatabaseLike> {\n onupgradeneeded: (() => void) | null\n}\ninterface IdbObjectStoreLike {\n get(key: string): IdbRequestLike<unknown>\n put(value: string, key: string): IdbRequestLike<unknown>\n}\ninterface IdbTransactionLike {\n objectStore(name: string): IdbObjectStoreLike\n}\ninterface IdbDatabaseLike {\n objectStoreNames: { contains(name: string): boolean }\n createObjectStore(name: string): unknown\n transaction(storeNames: string, mode: 'readonly' | 'readwrite'): IdbTransactionLike\n}\n/** The minimal IndexedDB factory surface (a structural subset of the DOM `IDBFactory`). */\nexport interface IndexedDbFactoryLike {\n open(name: string, version?: number): IdbOpenRequestLike\n}\n\n/** Options for {@link createIndexedDbPersistence}. */\nexport interface IndexedDbPersistenceOptions {\n /** Database name (default `'mindees'`). */\n readonly databaseName?: string\n /** Object-store name (default `'continuum'`). */\n readonly storeName?: string\n /** The IndexedDB factory; defaults to `globalThis.indexedDB`. Inject (e.g. `fake-indexeddb`) elsewhere. */\n readonly factory?: IndexedDbFactoryLike\n}\n\n/**\n * A durable {@link Persistence} backed by IndexedDB — large, asynchronous browser storage (beyond\n * `localStorage`'s synchronous ~5MB cap), a better home for a growing Continuum op log/snapshot. The\n * database + object store open lazily on first use and are reused. Inject `factory` to run outside a\n * browser (tests, or a custom environment); throws if none is available.\n */\nexport function createIndexedDbPersistence(options: IndexedDbPersistenceOptions = {}): Persistence {\n const dbName = options.databaseName ?? 'mindees'\n const storeName = options.storeName ?? 'continuum'\n const factory = options.factory ?? (globalThis as { indexedDB?: IndexedDbFactoryLike }).indexedDB\n if (!factory) {\n throw new Error('createIndexedDbPersistence: IndexedDB is unavailable; pass `factory`.')\n }\n\n let dbPromise: Promise<IdbDatabaseLike> | undefined\n const openDb = (): Promise<IdbDatabaseLike> => {\n if (!dbPromise) {\n const pending = new Promise<IdbDatabaseLike>((resolve, reject) => {\n const request = factory.open(dbName, 1)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName)\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB open failed'))\n })\n // Never memoize a REJECTED open — a transient failure (blocked upgrade, a concurrent-tab version\n // change, a one-off InvalidStateError) must not brick the handle for the whole session. Clear the\n // cache so the next load/save retries `factory.open`; guard against clobbering a newer attempt.\n pending.catch(() => {\n if (dbPromise === pending) dbPromise = undefined\n })\n dbPromise = pending\n }\n return dbPromise\n }\n\n const run = <T>(\n mode: 'readonly' | 'readwrite',\n op: (store: IdbObjectStoreLike) => IdbRequestLike<T>,\n ): Promise<T> =>\n openDb().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const request = op(db.transaction(storeName, mode).objectStore(storeName))\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'))\n }),\n )\n\n return {\n load: (key) =>\n run('readonly', (store) => store.get(key)).then((value) =>\n value === undefined || value === null ? null : String(value),\n ),\n save: (key, value) => run('readwrite', (store) => store.put(value, key)).then(() => undefined),\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;;;KAkBY,EAAA;EAMD,6FAJA,EAAA,UAQA;EAAA,SANA,KAAA,UAQA;EAAA,SANA,GAAA,UASI;EAAA,SAPJ,UAAA,UAcW;EAAA,SAZX,QAAA,EAAU,EAAA,EAcN;EAAA,SAZJ,GAAA,EAAK,GAAA;AAAA;EAAA,SAGD,IAAA;EAiBa;;;;;;EAAA,SAVb,KAAA,EAAO,OAAA,CAAQ,CAAA;AAAA;EAAA,SAEf,IAAA;AAAA;;;;;;UAQE,WAAA;EAQ0C;EAAA,SANhD,MAAA,EAAQ,MAAA;EAMJ;EAAA,SAJJ,IAAA,EAAM,GAAG;AAAA;;KAIf,SAAA,gBAAyB,EAAA,EAAI,WAAA,GAAc,WAAA,CAAY,CAAA;;UAG3C,WAAA;EAAA;EAEf,KAAA,CAAM,EAAA,EAAI,EAAA,CAAG,CAAA;EAFa;EAI1B,GAAA,CAAI,QAAA,EAAU,EAAA,GAAK,CAAA;EAFT;EAIV,OAAA,IAAW,KAAA,WAAgB,EAAA,EAAI,CAAA;EAFZ;EAInB,IAAA,IAAQ,KAAA,WAAgB,EAAA,EAAI,WAAA;AAAA;;iBA+Dd,iBAAA,IAAqB,OAAA,GAAU,QAAA,CAAS,SAAA,CAAU,CAAA,KAAM,WAAA,CAAY,CAAA;;KA2DxE,MAAA;;UAGK,SAAA;EAAA,SACN,OAAO;AAAA;;UAID,aAAA;EAxIT;EA0IN,IAAA,CAAK,GAAA,WAAc,EAAA,CAAG,CAAA,MAAO,OAAA;IAAA,SAAmB,KAAA;EAAA;EAxI7B;EA0InB,IAAA,CAAK,MAAA,EAAQ,MAAA,UAAgB,OAAA;IAAA,SAAmB,GAAA,WAAc,EAAA,CAAG,CAAA;IAAA,SAAe,MAAA,EAAQ,MAAA;EAAA;AAAA;;iBAI1E,eAAA,OAAsB,aAAa,CAAC,CAAA;;UAuBnC,YAAA;EAjKwB;EAAA,SAmK9B,GAAA;EApGsB;EAAA,SAsGtB,MAAA,EAAQ,MAAA;EAtG+C;;;;EAAA,SA2GvD,SAAA,EAAW,aAAA,WAAwB,EAAA,EAAI,WAAA;EA3GiC;EAAA,SA6GxE,MAAA,WAAiB,EAAA,CAAG,CAAA;EA7GG;;;;;EAAA,SAmHvB,KAAA,EAAO,GAAA;AAAA;;UAID,iBAAA;EA5DL;;;;AAAM;AAGlB;;EAHY,SAoED,MAAA;EAhEO;EAAA,SAkEP,SAAA,EAAW,aAAA,CAAc,CAAA;EA9DnB;EAAA,SAgEN,GAAA;EAhEmB;EAAA,SAkEnB,QAAA,GAAW,YAAA,CAAa,CAAA;AAAA;;UAIlB,UAAA;EAlEkD;;;;;;EAyEjE,GAAA,CAAI,UAAA,UAAoB,QAAA,EAAU,EAAA,EAAI,KAAA,EAAO,OAAA,CAAQ,CAAA,IAAK,EAAA,CAAG,CAAA;EA3E7D;EA6EA,MAAA,CAAO,UAAA,UAAoB,QAAA,EAAU,EAAA,GAAK,EAAA,CAAG,CAAA;EA7EvB;EA+EtB,GAAA,CAAI,QAAA,EAAU,EAAA,GAAK,CAAA;EA/EU;EAiF7B,OAAA,IAAW,KAAA,WAAgB,EAAA,EAAI,CAAA;EA/E/B;EAiFA,OAAA,aAAoB,EAAA,CAAG,CAAA;EAjFlB;EAmFL,IAAA,CAAK,MAAA,GAAS,SAAA,GAAY,OAAA;EAnFsB;EAqFhD,MAAA,IAAU,YAAA,CAAa,CAAA;AAAA;;iBAIT,gBAAA,IAAoB,OAAA,EAAS,iBAAA,CAAkB,CAAA,IAAK,UAAA,CAAW,CAAA"}
1
+ {"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;;;KAkBY,EAAA;EAMD,6FAJA,EAAA,UAQA;EAAA,SANA,KAAA,UAQA;EAAA,SANA,GAAA,UASI;EAAA,SAPJ,UAAA,UAcW;EAAA,SAZX,QAAA,EAAU,EAAA,EAcN;EAAA,SAZJ,GAAA,EAAK,GAAA;AAAA;EAAA,SAGD,IAAA;EAiBa;;;;;;EAAA,SAVb,KAAA,EAAO,OAAA,CAAQ,CAAA;AAAA;EAAA,SAEf,IAAA;AAAA;;;;;;UAQE,WAAA;EAQ0C;EAAA,SANhD,MAAA,EAAQ,MAAA;EAMJ;EAAA,SAJJ,IAAA,EAAM,GAAG;AAAA;;KAIf,SAAA,gBAAyB,EAAA,EAAI,WAAA,GAAc,WAAA,CAAY,CAAA;;UAG3C,WAAA;EAAA;EAEf,KAAA,CAAM,EAAA,EAAI,EAAA,CAAG,CAAA;EAFa;EAI1B,GAAA,CAAI,QAAA,EAAU,EAAA,GAAK,CAAA;EAFT;EAIV,OAAA,IAAW,KAAA,WAAgB,EAAA,EAAI,CAAA;EAFZ;EAInB,IAAA,IAAQ,KAAA,WAAgB,EAAA,EAAI,WAAA;AAAA;;iBAsEd,iBAAA,IAAqB,OAAA,GAAU,QAAA,CAAS,SAAA,CAAU,CAAA,KAAM,WAAA,CAAY,CAAA;;KA2DxE,MAAA;;UAGK,SAAA;EAAA,SACN,OAAO;AAAA;;UAID,aAAA;EA/IT;EAiJN,IAAA,CAAK,GAAA,WAAc,EAAA,CAAG,CAAA,MAAO,OAAA;IAAA,SAAmB,KAAA;EAAA;EA/I7B;EAiJnB,IAAA,CAAK,MAAA,EAAQ,MAAA,UAAgB,OAAA;IAAA,SAAmB,GAAA,WAAc,EAAA,CAAG,CAAA;IAAA,SAAe,MAAA,EAAQ,MAAA;EAAA;AAAA;;iBAI1E,eAAA,OAAsB,aAAa,CAAC,CAAA;;UAuBnC,YAAA;EAxKwB;EAAA,SA0K9B,GAAA;EApGsB;EAAA,SAsGtB,MAAA,EAAQ,MAAA;EAtG+C;;;;EAAA,SA2GvD,SAAA,EAAW,aAAA,WAAwB,EAAA,EAAI,WAAA;EA3GiC;EAAA,SA6GxE,MAAA,WAAiB,EAAA,CAAG,CAAA;EA7GG;;;;;EAAA,SAmHvB,KAAA,EAAO,GAAA;AAAA;;UAID,iBAAA;EA5DL;;;;AAAM;AAGlB;;EAHY,SAoED,MAAA;EAhEO;EAAA,SAkEP,SAAA,EAAW,aAAA,CAAc,CAAA;EA9DnB;EAAA,SAgEN,GAAA;EAhEmB;EAAA,SAkEnB,QAAA,GAAW,YAAA,CAAa,CAAA;AAAA;;UAIlB,UAAA;EAlEkD;;;;;;EAyEjE,GAAA,CAAI,UAAA,UAAoB,QAAA,EAAU,EAAA,EAAI,KAAA,EAAO,OAAA,CAAQ,CAAA,IAAK,EAAA,CAAG,CAAA;EA3E7D;EA6EA,MAAA,CAAO,UAAA,UAAoB,QAAA,EAAU,EAAA,GAAK,EAAA,CAAG,CAAA;EA7EvB;EA+EtB,GAAA,CAAI,QAAA,EAAU,EAAA,GAAK,CAAA;EA/EU;EAiF7B,OAAA,IAAW,KAAA,WAAgB,EAAA,EAAI,CAAA;EA/E/B;EAiFA,OAAA,aAAoB,EAAA,CAAG,CAAA;EAjFlB;EAmFL,IAAA,CAAK,MAAA,GAAS,SAAA,GAAY,OAAA;EAnFsB;EAqFhD,MAAA,IAAU,YAAA,CAAa,CAAA;AAAA;;iBAIT,gBAAA,IAAoB,OAAA,EAAS,iBAAA,CAAkB,CAAA,IAAK,UAAA,CAAW,CAAA"}
package/dist/sync.js CHANGED
@@ -33,17 +33,19 @@ function reconstruct(rs) {
33
33
  const tomb = rs.tomb;
34
34
  const live = (reg) => !!reg && reg.op === "set" && (tomb === null || compareHlc(reg.stamp, tomb) > 0);
35
35
  const whole = rs.fields[WHOLE];
36
- if (live(whole)) return whole.value;
36
+ const wholeStamp = live(whole) ? whole.stamp : null;
37
37
  const out = Object.create(null);
38
38
  let any = false;
39
39
  for (const k of Object.keys(rs.fields)) {
40
40
  if (k === WHOLE) continue;
41
41
  const reg = rs.fields[k];
42
42
  if (!live(reg)) continue;
43
+ if (wholeStamp !== null && compareHlc(reg.stamp, wholeStamp) <= 0) continue;
43
44
  out[k] = reg.value;
44
45
  any = true;
45
46
  }
46
- return any ? out : void 0;
47
+ if (any) return out;
48
+ if (live(whole)) return whole.value;
47
49
  }
48
50
  /** Canonicalize a record's state for deterministic serialization (sorted field keys). */
49
51
  function canonicalState(rs) {
package/dist/sync.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["/**\n * The Continuum local-first **sync engine** — the headline of Phase 10. Two replicas\n * that edited offline converge through a {@link SyncTransport}, tolerating concurrent\n * edits, duplicate delivery, and out-of-order pulls — with no network and no native\n * code (the in-memory {@link createMemoryHub hub} is the real, tested fallback).\n *\n * Built on the 10B HLC + 10C `mergeRegister` (so apply is a CvRDT merge: commutative +\n * idempotent ⇒ convergent). Ops are plain JSON; zero third-party deps. See\n * `docs/adr/0015-continuum-sync-engine.md`.\n *\n * @module\n */\n\nimport type { Id } from './collection'\nimport { type Clock, compareHlc, createClock, type Hlc } from './hlc'\nimport { type LwwMap, type LwwRegister, mergeRegister } from './lww'\n\n/** An HLC-stamped, idempotently-applied record mutation. */\nexport type Op<T> = {\n /** Unique, stable op id (`${actor}:${seq}`). Re-applying the same id is a no-op. */\n readonly id: string\n /** The replica that produced the op. */\n readonly actor: string\n /** Monotonic per-actor sequence number. */\n readonly seq: number\n /** The collection the record belongs to. */\n readonly collection: string\n /** The record's id. */\n readonly recordId: Id\n /** The merge key (10B). */\n readonly hlc: Hlc\n} & (\n | {\n readonly kind: 'set'\n /**\n * The **changed fields only** (a partial record). Each field is stamped at this op's\n * HLC and merged independently, so two replicas editing *different* fields of the same\n * record both keep their edit. Omitted fields are untouched — `set` MERGES, it does not\n * replace. (Records must be plain objects; the field is the merge granularity.)\n */\n readonly value: Partial<T>\n }\n | { readonly kind: 'del' }\n)\n\n/**\n * A record's convergent state: a per-field LWW map plus an optional whole-record delete\n * tombstone. A field is live iff it's a `set` whose stamp is strictly newer than the tombstone\n * (so a delete shadows older fields, but a later field-write *resurrects* that field).\n */\nexport interface RecordState {\n /** Per-field LWW registers (the merge granularity). */\n readonly fields: LwwMap<unknown>\n /** Whole-record delete stamp, or `null` if never deleted. */\n readonly tomb: Hlc | null\n}\n\n/** The persisted dump entry — new format, or a legacy whole-record register (auto-migrated). */\ntype DumpEntry<T> = readonly [Id, RecordState | LwwRegister<T>]\n\n/** A convergent materialized view: per-record, per-field LWW state built by applying ops. */\nexport interface MutationLog<T> {\n /** Merge an op into the state (commutative + idempotent). Returns whether state changed. */\n apply(op: Op<T>): boolean\n /** The live value of a record (or `undefined` if absent/deleted). */\n get(recordId: Id): T | undefined\n /** All live records as `[recordId, value]` pairs. */\n records(): Array<readonly [Id, T]>\n /** The full per-record state (incl. tombstones) — for persistence. */\n dump(): Array<readonly [Id, RecordState]>\n}\n\nconst EMPTY_FIELDS: LwwMap<unknown> = Object.freeze(Object.create(null))\n\n// Reserved sentinel field for migrating a legacy NON-object record (array/primitive): the whole\n// value is preserved under this key so a restored snapshot never silently corrupts/drops it.\nconst WHOLE = '\u0000mindees.whole'\n\n/** Migrate a dump entry: pass through new {@link RecordState}, lift a legacy whole-record register. */\nfunction migrateState<T>(value: RecordState | LwwRegister<T>): RecordState {\n if ('fields' in value) return value // already the new per-field format\n const reg = value as LwwRegister<T>\n if (reg.op === 'del') return { fields: EMPTY_FIELDS, tomb: reg.stamp }\n const fields: Record<string, LwwRegister<unknown>> = Object.create(null)\n const record = reg.value\n if (record !== null && typeof record === 'object' && !Array.isArray(record)) {\n // Plain object: lift each field to its own register at the record's stamp.\n for (const k of Object.keys(record)) {\n fields[k] = { stamp: reg.stamp, op: 'set', value: (record as Record<string, unknown>)[k] }\n }\n } else {\n // Array / primitive / null: per-field doesn't apply — preserve the whole value losslessly.\n fields[WHOLE] = { stamp: reg.stamp, op: 'set', value: record }\n }\n return { fields, tomb: null }\n}\n\n/** Reconstruct a record's live value from its per-field state (tombstone-shadowed). */\nfunction reconstruct<T>(rs: RecordState): T | undefined {\n const tomb = rs.tomb\n const live = (\n reg: LwwRegister<unknown> | undefined,\n ): reg is LwwRegister<unknown> & { op: 'set' } =>\n !!reg && reg.op === 'set' && (tomb === null || compareHlc(reg.stamp, tomb) > 0)\n\n // A migrated non-object (whole-value) record round-trips as its original value.\n const whole = rs.fields[WHOLE]\n if (live(whole)) return whole.value as T\n\n // Object record: a field survives a whole-record delete only if written strictly after it.\n // `out` is null-prototype so a field literally named `__proto__` is a real own property, not\n // a prototype mutation.\n const out: Record<string, unknown> = Object.create(null)\n let any = false\n for (const k of Object.keys(rs.fields)) {\n if (k === WHOLE) continue\n const reg = rs.fields[k]\n if (!live(reg)) continue\n out[k] = reg.value\n any = true\n }\n return any ? (out as T) : undefined\n}\n\n/** Canonicalize a record's state for deterministic serialization (sorted field keys). */\nfunction canonicalState(rs: RecordState): RecordState {\n const fields: Record<string, LwwRegister<unknown>> = Object.create(null)\n for (const k of Object.keys(rs.fields).sort()) fields[k] = rs.fields[k] as LwwRegister<unknown>\n return { fields, tomb: rs.tomb }\n}\n\n/** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump} (legacy-migrated). */\nexport function createMutationLog<T>(initial?: Iterable<DumpEntry<T>>): MutationLog<T> {\n const state = new Map<Id, RecordState>()\n if (initial) for (const [id, value] of initial) state.set(id, migrateState(value))\n\n return {\n apply(op): boolean {\n const prev = state.get(op.recordId) ?? { fields: EMPTY_FIELDS, tomb: null }\n if (op.kind === 'set') {\n // Merge each changed field independently (per-field LWW): concurrent edits to\n // DIFFERENT fields both survive; same-field conflicts resolve by HLC.\n const value = op.value as Record<string, unknown>\n let changed = false\n let fields = prev.fields\n for (const k of Object.keys(value)) {\n const incoming: LwwRegister<unknown> = { stamp: op.hlc, op: 'set', value: value[k] }\n const existing = Object.hasOwn(fields, k) ? fields[k] : undefined\n const merged = existing ? mergeRegister(existing, incoming) : incoming\n if (merged === existing) continue // idempotent / older — no change to this field\n if (!changed) {\n const copy: Record<string, LwwRegister<unknown>> = Object.create(null)\n for (const j of Object.keys(fields)) copy[j] = fields[j] as LwwRegister<unknown>\n fields = copy\n changed = true\n }\n ;(fields as Record<string, LwwRegister<unknown>>)[k] = merged\n }\n if (!changed) return false\n state.set(op.recordId, { fields, tomb: prev.tomb })\n return true\n }\n // del: advance the whole-record tombstone (max HLC).\n const tomb = prev.tomb === null || compareHlc(op.hlc, prev.tomb) > 0 ? op.hlc : prev.tomb\n if (tomb === prev.tomb) return false\n state.set(op.recordId, { fields: prev.fields, tomb })\n return true\n },\n get(recordId): T | undefined {\n const rs = state.get(recordId)\n return rs ? reconstruct<T>(rs) : undefined\n },\n records(): Array<readonly [Id, T]> {\n const out: Array<readonly [Id, T]> = []\n for (const [recordId, rs] of state) {\n const value = reconstruct<T>(rs)\n if (value !== undefined) out.push([recordId, value])\n }\n return out\n },\n dump(): Array<readonly [Id, RecordState]> {\n // Canonical (records sorted by id, fields sorted by key) so export() is byte-identical\n // across op orderings — required for content-addressed snapshots / anti-entropy hashing.\n return [...state]\n .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))\n .map(([id, rs]) => [id, canonicalState(rs)] as const)\n },\n }\n}\n\n/** An opaque, monotonic sync cursor (the hub's log position). */\nexport type Cursor = number\n\n/** Minimal cancellation signal — a real `AbortSignal` is structurally compatible. */\nexport interface AbortLike {\n readonly aborted: boolean\n}\n\n/** The push/pull transport a {@link createSyncEngine sync engine} talks to. */\nexport interface SyncTransport<T> {\n /** Send local ops upstream; resolves with the ids the server accepted. */\n push(ops: readonly Op<T>[]): Promise<{ readonly acked: readonly string[] }>\n /** Fetch ops after `cursor` (null = from the beginning). */\n pull(cursor: Cursor | null): Promise<{ readonly ops: readonly Op<T>[]; readonly cursor: Cursor }>\n}\n\n/** An in-memory reference transport: an append-only, op-id-deduped log. */\nexport function createMemoryHub<T>(): SyncTransport<T> {\n const log: Array<Op<T>> = []\n const seen = new Set<string>()\n return {\n push(ops): Promise<{ acked: string[] }> {\n const acked: string[] = []\n for (const op of ops) {\n if (!seen.has(op.id)) {\n seen.add(op.id)\n log.push(op)\n }\n acked.push(op.id) // idempotent: already-known ops still ack\n }\n return Promise.resolve({ acked })\n },\n pull(cursor): Promise<{ ops: Op<T>[]; cursor: Cursor }> {\n const from = cursor ?? 0\n return Promise.resolve({ ops: log.slice(from), cursor: log.length })\n },\n }\n}\n\n/** A serializable snapshot of a {@link SyncEngine}'s durable state (10F). */\nexport interface SyncSnapshot<T> {\n /** The next local op sequence number (persisted so ids don't collide across restarts). */\n readonly seq: number\n /** The last pull cursor. */\n readonly cursor: Cursor | null\n /**\n * The materialized per-record state (per-field LWW + tombstone). Legacy snapshots that stored\n * a whole-record register per id are auto-migrated on restore, so old persisted data still loads.\n */\n readonly registers: ReadonlyArray<readonly [Id, RecordState]>\n /** Local ops applied but not yet acked (retried on next sync). */\n readonly outbox: readonly Op<T>[]\n /**\n * The local HLC high-water mark at export time. Restoring it seeds the clock so the\n * first post-restart edit is strictly newer than the replica's pre-restart writes —\n * without it the clock regresses to 0 and a same-record edit can lose the LWW merge.\n */\n readonly clock: Hlc\n}\n\n/** Options for {@link createSyncEngine}. */\nexport interface SyncEngineOptions<T> {\n /**\n * This replica's id (the op `actor`). **Must be globally unique**, and a durable\n * replica must **persist its op sequence** across restarts — op ids are `${nodeId}:${seq}`\n * and the transport de-dupes by id, so a reused `(nodeId, seq)` pair would silently\n * drop the second op. (This in-memory engine resets `seq` on construction; persistence\n * + content-addressed ids land with 10F.)\n */\n readonly nodeId: string\n /** The transport to sync through. */\n readonly transport: SyncTransport<T>\n /** Injected physical clock. Default `() => Date.now()`. */\n readonly now?: () => number\n /** Restore from a previously {@link SyncEngine.export}ed snapshot (durable replica). */\n readonly snapshot?: SyncSnapshot<T>\n}\n\n/** A local-first sync engine over a {@link SyncTransport}. */\nexport interface SyncEngine<T> {\n /**\n * Optimistically **merge** record fields locally and queue the op. Pass only the fields you\n * changed: `set('users', 'u1', { name: 'Ada' })` leaves other fields untouched and lets a\n * concurrent edit to a *different* field on another replica survive. (Per-field LWW — not a\n * whole-record replace. To remove a record use {@link SyncEngine.delete}.)\n */\n set(collection: string, recordId: Id, value: Partial<T>): Op<T>\n /** Optimistically delete a record locally and queue the op. */\n delete(collection: string, recordId: Id): Op<T>\n /** Read a record's live value (local + already-synced state). */\n get(recordId: Id): T | undefined\n /** All live records. */\n records(): Array<readonly [Id, T]>\n /** Ops applied locally but not yet acked by the transport. */\n pending(): readonly Op<T>[]\n /** Push pending ops, then pull + apply remote ops. */\n sync(signal?: AbortLike): Promise<void>\n /** A serializable snapshot to persist (restore via the `snapshot` option). */\n export(): SyncSnapshot<T>\n}\n\n/** Create a {@link SyncEngine}. */\nexport function createSyncEngine<T>(options: SyncEngineOptions<T>): SyncEngine<T> {\n const { nodeId, transport, snapshot } = options\n const clock: Clock = createClock({\n nodeId,\n ...(options.now ? { now: options.now } : {}),\n // Seed from the persisted high-water mark so a restored replica's clock never\n // regresses (a post-restart edit must out-stamp its own pre-restart writes).\n ...(snapshot?.clock\n ? { seed: { wallMs: snapshot.clock.wallMs, counter: snapshot.clock.counter } }\n : {}),\n })\n const log = createMutationLog<T>(snapshot?.registers)\n const outbox: Op<T>[] = snapshot ? [...snapshot.outbox] : []\n let seq = snapshot?.seq ?? 0\n let cursor: Cursor | null = snapshot?.cursor ?? null\n // Serializes sync() runs (see sync() below) so overlapping callers neither double-pull nor\n // lose their queued ops to an early-return coalesce.\n let syncChain: Promise<void> = Promise.resolve()\n\n const emit = (op: Op<T>): Op<T> => {\n log.apply(op) // optimistic local apply\n outbox.push(op)\n return op\n }\n\n return {\n set(collection, recordId, value): Op<T> {\n seq += 1\n return emit({\n id: `${nodeId}:${seq}`,\n actor: nodeId,\n seq,\n collection,\n recordId,\n hlc: clock.tick(),\n kind: 'set',\n value,\n })\n },\n\n delete(collection, recordId): Op<T> {\n seq += 1\n return emit({\n id: `${nodeId}:${seq}`,\n actor: nodeId,\n seq,\n collection,\n recordId,\n hlc: clock.tick(),\n kind: 'del',\n })\n },\n\n get(recordId): T | undefined {\n return log.get(recordId)\n },\n records(): Array<readonly [Id, T]> {\n return log.records()\n },\n pending(): readonly Op<T>[] {\n return [...outbox]\n },\n\n sync(signal): Promise<void> {\n // Serialize, don't coalesce: each call waits for the prior run, then does its OWN full\n // push+pull. This keeps runs non-overlapping (no double-pull / cursor regression) while\n // guaranteeing a caller's just-queued ops are actually pushed — the previous \"return early\n // while another sync is in flight\" silently dropped that work.\n const run = syncChain.then(() => runSync(signal))\n syncChain = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n },\n\n export(): SyncSnapshot<T> {\n return { seq, cursor, registers: log.dump(), outbox: [...outbox], clock: clock.peek() }\n },\n }\n\n async function runSync(signal?: AbortLike): Promise<void> {\n if (outbox.length > 0) {\n const sending = [...outbox]\n const { acked } = await transport.push(sending)\n // Drop acked ops from the outbox BEFORE honoring an abort: the server already accepted\n // them, so leaving them queued would re-push (harmless via op-id dedup, but over-reports\n // pending()). Re-pushing unacked ops next sync is correct.\n const ackedSet = new Set(acked)\n for (let i = outbox.length - 1; i >= 0; i--) {\n const op = outbox[i]\n if (op && ackedSet.has(op.id)) outbox.splice(i, 1)\n }\n if (signal?.aborted) return\n }\n const { ops, cursor: next } = await transport.pull(cursor)\n if (signal?.aborted) return\n for (const op of ops) {\n try {\n // Advance our local clock toward the op (clamped, so a clock-skewed peer can't\n // poison it), then merge the op. clock.update throws ONLY for a structurally\n // invalid (non-encodable) HLC — never for a merely far-future one — so a\n // legitimately clock-skewed peer's op is still applied (CRDT convergence), and\n // we only ever skip ops that are permanently unusable.\n clock.update(op.hlc)\n log.apply(op) // merges by HLC; our own returning ops re-apply idempotently\n } catch {\n // A structurally-invalid op can never be ordered/stored, so skipping it (and\n // letting the cursor advance past it) loses nothing — unlike a recoverable op.\n }\n }\n cursor = next\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["/**\n * The Continuum local-first **sync engine** — the headline of Phase 10. Two replicas\n * that edited offline converge through a {@link SyncTransport}, tolerating concurrent\n * edits, duplicate delivery, and out-of-order pulls — with no network and no native\n * code (the in-memory {@link createMemoryHub hub} is the real, tested fallback).\n *\n * Built on the 10B HLC + 10C `mergeRegister` (so apply is a CvRDT merge: commutative +\n * idempotent ⇒ convergent). Ops are plain JSON; zero third-party deps. See\n * `docs/adr/0015-continuum-sync-engine.md`.\n *\n * @module\n */\n\nimport type { Id } from './collection'\nimport { type Clock, compareHlc, createClock, type Hlc } from './hlc'\nimport { type LwwMap, type LwwRegister, mergeRegister } from './lww'\n\n/** An HLC-stamped, idempotently-applied record mutation. */\nexport type Op<T> = {\n /** Unique, stable op id (`${actor}:${seq}`). Re-applying the same id is a no-op. */\n readonly id: string\n /** The replica that produced the op. */\n readonly actor: string\n /** Monotonic per-actor sequence number. */\n readonly seq: number\n /** The collection the record belongs to. */\n readonly collection: string\n /** The record's id. */\n readonly recordId: Id\n /** The merge key (10B). */\n readonly hlc: Hlc\n} & (\n | {\n readonly kind: 'set'\n /**\n * The **changed fields only** (a partial record). Each field is stamped at this op's\n * HLC and merged independently, so two replicas editing *different* fields of the same\n * record both keep their edit. Omitted fields are untouched — `set` MERGES, it does not\n * replace. (Records must be plain objects; the field is the merge granularity.)\n */\n readonly value: Partial<T>\n }\n | { readonly kind: 'del' }\n)\n\n/**\n * A record's convergent state: a per-field LWW map plus an optional whole-record delete\n * tombstone. A field is live iff it's a `set` whose stamp is strictly newer than the tombstone\n * (so a delete shadows older fields, but a later field-write *resurrects* that field).\n */\nexport interface RecordState {\n /** Per-field LWW registers (the merge granularity). */\n readonly fields: LwwMap<unknown>\n /** Whole-record delete stamp, or `null` if never deleted. */\n readonly tomb: Hlc | null\n}\n\n/** The persisted dump entry — new format, or a legacy whole-record register (auto-migrated). */\ntype DumpEntry<T> = readonly [Id, RecordState | LwwRegister<T>]\n\n/** A convergent materialized view: per-record, per-field LWW state built by applying ops. */\nexport interface MutationLog<T> {\n /** Merge an op into the state (commutative + idempotent). Returns whether state changed. */\n apply(op: Op<T>): boolean\n /** The live value of a record (or `undefined` if absent/deleted). */\n get(recordId: Id): T | undefined\n /** All live records as `[recordId, value]` pairs. */\n records(): Array<readonly [Id, T]>\n /** The full per-record state (incl. tombstones) — for persistence. */\n dump(): Array<readonly [Id, RecordState]>\n}\n\nconst EMPTY_FIELDS: LwwMap<unknown> = Object.freeze(Object.create(null))\n\n// Reserved sentinel field for migrating a legacy NON-object record (array/primitive): the whole\n// value is preserved under this key so a restored snapshot never silently corrupts/drops it.\nconst WHOLE = '\u0000mindees.whole'\n\n/** Migrate a dump entry: pass through new {@link RecordState}, lift a legacy whole-record register. */\nfunction migrateState<T>(value: RecordState | LwwRegister<T>): RecordState {\n if ('fields' in value) return value // already the new per-field format\n const reg = value as LwwRegister<T>\n if (reg.op === 'del') return { fields: EMPTY_FIELDS, tomb: reg.stamp }\n const fields: Record<string, LwwRegister<unknown>> = Object.create(null)\n const record = reg.value\n if (record !== null && typeof record === 'object' && !Array.isArray(record)) {\n // Plain object: lift each field to its own register at the record's stamp.\n for (const k of Object.keys(record)) {\n fields[k] = { stamp: reg.stamp, op: 'set', value: (record as Record<string, unknown>)[k] }\n }\n } else {\n // Array / primitive / null: per-field doesn't apply — preserve the whole value losslessly.\n fields[WHOLE] = { stamp: reg.stamp, op: 'set', value: record }\n }\n return { fields, tomb: null }\n}\n\n/** Reconstruct a record's live value from its per-field state (tombstone-shadowed). */\nfunction reconstruct<T>(rs: RecordState): T | undefined {\n const tomb = rs.tomb\n const live = (\n reg: LwwRegister<unknown> | undefined,\n ): reg is LwwRegister<unknown> & { op: 'set' } =>\n !!reg && reg.op === 'set' && (tomb === null || compareHlc(reg.stamp, tomb) > 0)\n\n // A migrated whole-value (primitive/array) register behaves like a set of the ENTIRE record at its\n // stamp: it shadows per-field registers written at/before it, but is itself SUPERSEDED by any\n // per-field set written strictly after it (the record then transitions to the per-field/object\n // representation). Resolving it by HLC — instead of unconditionally short-circuiting — keeps a\n // strictly-newer write from being masked and makes a migrated replica converge with a fresh one.\n const whole = rs.fields[WHOLE]\n const wholeStamp = live(whole) ? whole.stamp : null\n\n // Object record: a field survives the whole-record set/delete only if written strictly after it.\n // `out` is null-prototype so a field literally named `__proto__` is a real own property, not\n // a prototype mutation.\n const out: Record<string, unknown> = Object.create(null)\n let any = false\n for (const k of Object.keys(rs.fields)) {\n if (k === WHOLE) continue\n const reg = rs.fields[k]\n if (!live(reg)) continue\n if (wholeStamp !== null && compareHlc(reg.stamp, wholeStamp) <= 0) continue // shadowed by the whole-set\n out[k] = reg.value\n any = true\n }\n if (any) return out as T // a per-field set newer than the whole-value won → object representation\n if (live(whole)) return whole.value as T // no newer field → the migrated whole value stands\n return undefined\n}\n\n/** Canonicalize a record's state for deterministic serialization (sorted field keys). */\nfunction canonicalState(rs: RecordState): RecordState {\n const fields: Record<string, LwwRegister<unknown>> = Object.create(null)\n for (const k of Object.keys(rs.fields).sort()) fields[k] = rs.fields[k] as LwwRegister<unknown>\n return { fields, tomb: rs.tomb }\n}\n\n/** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump} (legacy-migrated). */\nexport function createMutationLog<T>(initial?: Iterable<DumpEntry<T>>): MutationLog<T> {\n const state = new Map<Id, RecordState>()\n if (initial) for (const [id, value] of initial) state.set(id, migrateState(value))\n\n return {\n apply(op): boolean {\n const prev = state.get(op.recordId) ?? { fields: EMPTY_FIELDS, tomb: null }\n if (op.kind === 'set') {\n // Merge each changed field independently (per-field LWW): concurrent edits to\n // DIFFERENT fields both survive; same-field conflicts resolve by HLC.\n const value = op.value as Record<string, unknown>\n let changed = false\n let fields = prev.fields\n for (const k of Object.keys(value)) {\n const incoming: LwwRegister<unknown> = { stamp: op.hlc, op: 'set', value: value[k] }\n const existing = Object.hasOwn(fields, k) ? fields[k] : undefined\n const merged = existing ? mergeRegister(existing, incoming) : incoming\n if (merged === existing) continue // idempotent / older — no change to this field\n if (!changed) {\n const copy: Record<string, LwwRegister<unknown>> = Object.create(null)\n for (const j of Object.keys(fields)) copy[j] = fields[j] as LwwRegister<unknown>\n fields = copy\n changed = true\n }\n ;(fields as Record<string, LwwRegister<unknown>>)[k] = merged\n }\n if (!changed) return false\n state.set(op.recordId, { fields, tomb: prev.tomb })\n return true\n }\n // del: advance the whole-record tombstone (max HLC).\n const tomb = prev.tomb === null || compareHlc(op.hlc, prev.tomb) > 0 ? op.hlc : prev.tomb\n if (tomb === prev.tomb) return false\n state.set(op.recordId, { fields: prev.fields, tomb })\n return true\n },\n get(recordId): T | undefined {\n const rs = state.get(recordId)\n return rs ? reconstruct<T>(rs) : undefined\n },\n records(): Array<readonly [Id, T]> {\n const out: Array<readonly [Id, T]> = []\n for (const [recordId, rs] of state) {\n const value = reconstruct<T>(rs)\n if (value !== undefined) out.push([recordId, value])\n }\n return out\n },\n dump(): Array<readonly [Id, RecordState]> {\n // Canonical (records sorted by id, fields sorted by key) so export() is byte-identical\n // across op orderings — required for content-addressed snapshots / anti-entropy hashing.\n return [...state]\n .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))\n .map(([id, rs]) => [id, canonicalState(rs)] as const)\n },\n }\n}\n\n/** An opaque, monotonic sync cursor (the hub's log position). */\nexport type Cursor = number\n\n/** Minimal cancellation signal — a real `AbortSignal` is structurally compatible. */\nexport interface AbortLike {\n readonly aborted: boolean\n}\n\n/** The push/pull transport a {@link createSyncEngine sync engine} talks to. */\nexport interface SyncTransport<T> {\n /** Send local ops upstream; resolves with the ids the server accepted. */\n push(ops: readonly Op<T>[]): Promise<{ readonly acked: readonly string[] }>\n /** Fetch ops after `cursor` (null = from the beginning). */\n pull(cursor: Cursor | null): Promise<{ readonly ops: readonly Op<T>[]; readonly cursor: Cursor }>\n}\n\n/** An in-memory reference transport: an append-only, op-id-deduped log. */\nexport function createMemoryHub<T>(): SyncTransport<T> {\n const log: Array<Op<T>> = []\n const seen = new Set<string>()\n return {\n push(ops): Promise<{ acked: string[] }> {\n const acked: string[] = []\n for (const op of ops) {\n if (!seen.has(op.id)) {\n seen.add(op.id)\n log.push(op)\n }\n acked.push(op.id) // idempotent: already-known ops still ack\n }\n return Promise.resolve({ acked })\n },\n pull(cursor): Promise<{ ops: Op<T>[]; cursor: Cursor }> {\n const from = cursor ?? 0\n return Promise.resolve({ ops: log.slice(from), cursor: log.length })\n },\n }\n}\n\n/** A serializable snapshot of a {@link SyncEngine}'s durable state (10F). */\nexport interface SyncSnapshot<T> {\n /** The next local op sequence number (persisted so ids don't collide across restarts). */\n readonly seq: number\n /** The last pull cursor. */\n readonly cursor: Cursor | null\n /**\n * The materialized per-record state (per-field LWW + tombstone). Legacy snapshots that stored\n * a whole-record register per id are auto-migrated on restore, so old persisted data still loads.\n */\n readonly registers: ReadonlyArray<readonly [Id, RecordState]>\n /** Local ops applied but not yet acked (retried on next sync). */\n readonly outbox: readonly Op<T>[]\n /**\n * The local HLC high-water mark at export time. Restoring it seeds the clock so the\n * first post-restart edit is strictly newer than the replica's pre-restart writes —\n * without it the clock regresses to 0 and a same-record edit can lose the LWW merge.\n */\n readonly clock: Hlc\n}\n\n/** Options for {@link createSyncEngine}. */\nexport interface SyncEngineOptions<T> {\n /**\n * This replica's id (the op `actor`). **Must be globally unique**, and a durable\n * replica must **persist its op sequence** across restarts — op ids are `${nodeId}:${seq}`\n * and the transport de-dupes by id, so a reused `(nodeId, seq)` pair would silently\n * drop the second op. (This in-memory engine resets `seq` on construction; persistence\n * + content-addressed ids land with 10F.)\n */\n readonly nodeId: string\n /** The transport to sync through. */\n readonly transport: SyncTransport<T>\n /** Injected physical clock. Default `() => Date.now()`. */\n readonly now?: () => number\n /** Restore from a previously {@link SyncEngine.export}ed snapshot (durable replica). */\n readonly snapshot?: SyncSnapshot<T>\n}\n\n/** A local-first sync engine over a {@link SyncTransport}. */\nexport interface SyncEngine<T> {\n /**\n * Optimistically **merge** record fields locally and queue the op. Pass only the fields you\n * changed: `set('users', 'u1', { name: 'Ada' })` leaves other fields untouched and lets a\n * concurrent edit to a *different* field on another replica survive. (Per-field LWW — not a\n * whole-record replace. To remove a record use {@link SyncEngine.delete}.)\n */\n set(collection: string, recordId: Id, value: Partial<T>): Op<T>\n /** Optimistically delete a record locally and queue the op. */\n delete(collection: string, recordId: Id): Op<T>\n /** Read a record's live value (local + already-synced state). */\n get(recordId: Id): T | undefined\n /** All live records. */\n records(): Array<readonly [Id, T]>\n /** Ops applied locally but not yet acked by the transport. */\n pending(): readonly Op<T>[]\n /** Push pending ops, then pull + apply remote ops. */\n sync(signal?: AbortLike): Promise<void>\n /** A serializable snapshot to persist (restore via the `snapshot` option). */\n export(): SyncSnapshot<T>\n}\n\n/** Create a {@link SyncEngine}. */\nexport function createSyncEngine<T>(options: SyncEngineOptions<T>): SyncEngine<T> {\n const { nodeId, transport, snapshot } = options\n const clock: Clock = createClock({\n nodeId,\n ...(options.now ? { now: options.now } : {}),\n // Seed from the persisted high-water mark so a restored replica's clock never\n // regresses (a post-restart edit must out-stamp its own pre-restart writes).\n ...(snapshot?.clock\n ? { seed: { wallMs: snapshot.clock.wallMs, counter: snapshot.clock.counter } }\n : {}),\n })\n const log = createMutationLog<T>(snapshot?.registers)\n const outbox: Op<T>[] = snapshot ? [...snapshot.outbox] : []\n let seq = snapshot?.seq ?? 0\n let cursor: Cursor | null = snapshot?.cursor ?? null\n // Serializes sync() runs (see sync() below) so overlapping callers neither double-pull nor\n // lose their queued ops to an early-return coalesce.\n let syncChain: Promise<void> = Promise.resolve()\n\n const emit = (op: Op<T>): Op<T> => {\n log.apply(op) // optimistic local apply\n outbox.push(op)\n return op\n }\n\n return {\n set(collection, recordId, value): Op<T> {\n seq += 1\n return emit({\n id: `${nodeId}:${seq}`,\n actor: nodeId,\n seq,\n collection,\n recordId,\n hlc: clock.tick(),\n kind: 'set',\n value,\n })\n },\n\n delete(collection, recordId): Op<T> {\n seq += 1\n return emit({\n id: `${nodeId}:${seq}`,\n actor: nodeId,\n seq,\n collection,\n recordId,\n hlc: clock.tick(),\n kind: 'del',\n })\n },\n\n get(recordId): T | undefined {\n return log.get(recordId)\n },\n records(): Array<readonly [Id, T]> {\n return log.records()\n },\n pending(): readonly Op<T>[] {\n return [...outbox]\n },\n\n sync(signal): Promise<void> {\n // Serialize, don't coalesce: each call waits for the prior run, then does its OWN full\n // push+pull. This keeps runs non-overlapping (no double-pull / cursor regression) while\n // guaranteeing a caller's just-queued ops are actually pushed — the previous \"return early\n // while another sync is in flight\" silently dropped that work.\n const run = syncChain.then(() => runSync(signal))\n syncChain = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n },\n\n export(): SyncSnapshot<T> {\n return { seq, cursor, registers: log.dump(), outbox: [...outbox], clock: clock.peek() }\n },\n }\n\n async function runSync(signal?: AbortLike): Promise<void> {\n if (outbox.length > 0) {\n const sending = [...outbox]\n const { acked } = await transport.push(sending)\n // Drop acked ops from the outbox BEFORE honoring an abort: the server already accepted\n // them, so leaving them queued would re-push (harmless via op-id dedup, but over-reports\n // pending()). Re-pushing unacked ops next sync is correct.\n const ackedSet = new Set(acked)\n for (let i = outbox.length - 1; i >= 0; i--) {\n const op = outbox[i]\n if (op && ackedSet.has(op.id)) outbox.splice(i, 1)\n }\n if (signal?.aborted) return\n }\n const { ops, cursor: next } = await transport.pull(cursor)\n if (signal?.aborted) return\n for (const op of ops) {\n try {\n // Advance our local clock toward the op (clamped, so a clock-skewed peer can't\n // poison it), then merge the op. clock.update throws ONLY for a structurally\n // invalid (non-encodable) HLC — never for a merely far-future one — so a\n // legitimately clock-skewed peer's op is still applied (CRDT convergence), and\n // we only ever skip ops that are permanently unusable.\n clock.update(op.hlc)\n log.apply(op) // merges by HLC; our own returning ops re-apply idempotently\n } catch {\n // A structurally-invalid op can never be ordered/stored, so skipping it (and\n // letting the cursor advance past it) loses nothing — unlike a recoverable op.\n }\n }\n cursor = next\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/data",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
4
4
  "description": "MindeesNative Continuum - local-first reactive store and sync: a signals-native document store with fine-grained reactive queries, optimistic updates, CRDT conflict resolution, and delta sync.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "directory": "packages/data"
28
28
  },
29
29
  "dependencies": {
30
- "@mindees/core": "0.22.0"
30
+ "@mindees/core": "0.22.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "fake-indexeddb": "6.0.0",