@mindees/data 0.1.0 → 0.3.0

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
@@ -12,7 +12,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
12
12
  /** The npm package name. */
13
13
  declare const name = "@mindees/data";
14
14
  /** The package version. All `@mindees/*` packages share one locked version line. */
15
- declare const VERSION = "0.1.0";
15
+ declare const VERSION = "0.3.0";
16
16
  /** Current maturity of this package. See the repository `STATUS.md`. */
17
17
  declare const maturity: Maturity;
18
18
  /**
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
11
11
  /** The npm package name. */
12
12
  const name = "@mindees/data";
13
13
  /** The package version. All `@mindees/*` packages share one locked version line. */
14
- const VERSION = "0.1.0";
14
+ const VERSION = "0.3.0";
15
15
  /** Current maturity of this package. See the repository `STATUS.md`. */
16
16
  const maturity = "experimental";
17
17
  /**
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.1.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 { 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 { createMemoryPersistence, type Persistence } 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.3.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 { 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 { createMemoryPersistence, type Persistence } 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/sync.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Id } from "./collection.js";
2
2
  import { Hlc } from "./hlc.js";
3
- import { LwwRegister } from "./lww.js";
3
+ import { LwwMap, LwwRegister } from "./lww.js";
4
4
 
5
5
  //#region src/sync.d.ts
6
6
  /** An HLC-stamped, idempotently-applied record mutation. */
@@ -13,11 +13,30 @@ type Op<T> = {
13
13
  readonly hlc: Hlc;
14
14
  } & ({
15
15
  readonly kind: 'set';
16
- readonly value: T;
16
+ /**
17
+ * The **changed fields only** (a partial record). Each field is stamped at this op's
18
+ * HLC and merged independently, so two replicas editing *different* fields of the same
19
+ * record both keep their edit. Omitted fields are untouched — `set` MERGES, it does not
20
+ * replace. (Records must be plain objects; the field is the merge granularity.)
21
+ */
22
+ readonly value: Partial<T>;
17
23
  } | {
18
24
  readonly kind: 'del';
19
25
  });
20
- /** A convergent materialized view: per-record LWW state built by applying ops. */
26
+ /**
27
+ * A record's convergent state: a per-field LWW map plus an optional whole-record delete
28
+ * tombstone. A field is live iff it's a `set` whose stamp is strictly newer than the tombstone
29
+ * (so a delete shadows older fields, but a later field-write *resurrects* that field).
30
+ */
31
+ interface RecordState {
32
+ /** Per-field LWW registers (the merge granularity). */
33
+ readonly fields: LwwMap<unknown>;
34
+ /** Whole-record delete stamp, or `null` if never deleted. */
35
+ readonly tomb: Hlc | null;
36
+ }
37
+ /** The persisted dump entry — new format, or a legacy whole-record register (auto-migrated). */
38
+ type DumpEntry<T> = readonly [Id, RecordState | LwwRegister<T>];
39
+ /** A convergent materialized view: per-record, per-field LWW state built by applying ops. */
21
40
  interface MutationLog<T> {
22
41
  /** Merge an op into the state (commutative + idempotent). Returns whether state changed. */
23
42
  apply(op: Op<T>): boolean;
@@ -25,11 +44,11 @@ interface MutationLog<T> {
25
44
  get(recordId: Id): T | undefined;
26
45
  /** All live records as `[recordId, value]` pairs. */
27
46
  records(): Array<readonly [Id, T]>;
28
- /** The full per-record register state (incl. tombstones) — for persistence. */
29
- dump(): Array<readonly [Id, LwwRegister<T>]>;
47
+ /** The full per-record state (incl. tombstones) — for persistence. */
48
+ dump(): Array<readonly [Id, RecordState]>;
30
49
  }
31
- /** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump}. */
32
- declare function createMutationLog<T>(initial?: Iterable<readonly [Id, LwwRegister<T>]>): MutationLog<T>;
50
+ /** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump} (legacy-migrated). */
51
+ declare function createMutationLog<T>(initial?: Iterable<DumpEntry<T>>): MutationLog<T>;
33
52
  /** An opaque, monotonic sync cursor (the hub's log position). */
34
53
  type Cursor = number;
35
54
  /** Minimal cancellation signal — a real `AbortSignal` is structurally compatible. */
@@ -56,8 +75,11 @@ interface SyncSnapshot<T> {
56
75
  readonly seq: number;
57
76
  /** The last pull cursor. */
58
77
  readonly cursor: Cursor | null;
59
- /** The materialized per-record register state. */
60
- readonly registers: ReadonlyArray<readonly [Id, LwwRegister<T>]>;
78
+ /**
79
+ * The materialized per-record state (per-field LWW + tombstone). Legacy snapshots that stored
80
+ * a whole-record register per id are auto-migrated on restore, so old persisted data still loads.
81
+ */
82
+ readonly registers: ReadonlyArray<readonly [Id, RecordState]>;
61
83
  /** Local ops applied but not yet acked (retried on next sync). */
62
84
  readonly outbox: readonly Op<T>[];
63
85
  /**
@@ -86,8 +108,13 @@ interface SyncEngineOptions<T> {
86
108
  }
87
109
  /** A local-first sync engine over a {@link SyncTransport}. */
88
110
  interface SyncEngine<T> {
89
- /** Optimistically set a record locally and queue the op for the next `sync()`. */
90
- set(collection: string, recordId: Id, value: T): Op<T>;
111
+ /**
112
+ * Optimistically **merge** record fields locally and queue the op. Pass only the fields you
113
+ * changed: `set('users', 'u1', { name: 'Ada' })` leaves other fields untouched and lets a
114
+ * concurrent edit to a *different* field on another replica survive. (Per-field LWW — not a
115
+ * whole-record replace. To remove a record use {@link SyncEngine.delete}.)
116
+ */
117
+ set(collection: string, recordId: Id, value: Partial<T>): Op<T>;
91
118
  /** Optimistically delete a record locally and queue the op. */
92
119
  delete(collection: string, recordId: Id): Op<T>;
93
120
  /** Read a record's live value (local + already-synced state). */
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;;;KAkBY,EAAA;EAQD,6FANA,EAAA,UAQU;EAAA,SANV,KAAA,UAQK;EAAA,SANL,GAAA,UAO2B;EAAA,SAL3B,UAAA,UAKmD;EAAA,SAHnD,QAAA,EAAU,EAAA,EAG6C;EAAA,SADvD,GAAA,EAAK,GAAA;AAAA;EAAA,SACA,IAAA;EAAA,SAAsB,KAAA,EAAO,CAAA;AAAA;EAAA,SAAiB,IAAA;AAAA;;UAG7C,WAAA;EAMJ;EAJX,KAAA,CAAM,EAAA,EAAI,EAAA,CAAG,CAAA;EAM2B;EAJxC,GAAA,CAAI,QAAA,EAAU,EAAA,GAAK,CAAA;EAIX;EAFR,OAAA,IAAW,KAAA,WAAgB,EAAA,EAAI,CAAA;EAElB;EAAb,IAAA,IAAQ,KAAA,WAAgB,EAAA,EAAI,WAAA,CAAY,CAAA;AAAA;;iBAI1B,iBAAA,IACd,OAAA,GAAU,QAAA,WAAmB,EAAA,EAAI,WAAA,CAAY,CAAA,MAC5C,WAAA,CAAY,CAAA;;KAsCH,MAAA;;UAGK,SAAA;EAAA,SACN,OAAO;AAAA;;UAID,aAAA;EAtDgB;EAwD/B,IAAA,CAAK,GAAA,WAAc,EAAA,CAAG,CAAA,MAAO,OAAA;IAAA,SAAmB,KAAA;EAAA;EAtDpB;EAwD5B,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;EA9EL;EAAA,SAgFD,GAAA;EA/ER;EAAA,SAiFQ,MAAA,EAAQ,MAAA;EAjFL;EAAA,SAmFH,SAAA,EAAW,aAAA,WAAwB,EAAA,EAAI,WAAA,CAAY,CAAA;EApFlD;EAAA,SAsFD,MAAA,WAAiB,EAAA,CAAG,CAAA;EAtFI;;;;;EAAA,SA4FxB,KAAA,EAAO,GAAA;AAAA;AArDlB;AAAA,UAyDiB,iBAAA;;;AAzDC;AAGlB;;;;WA8DW,MAAA;EAzDM;EAAA,SA2DN,SAAA,EAAW,aAAA,CAAc,CAAA;EA3DN;EAAA,SA6DnB,GAAA;EA3DU;EAAA,SA6DV,QAAA,GAAW,YAAA,CAAa,CAAA;AAAA;;UAIlB,UAAA;EA/DyE;EAiExF,GAAA,CAAI,UAAA,UAAoB,QAAA,EAAU,EAAA,EAAI,KAAA,EAAO,CAAA,GAAI,EAAA,CAAG,CAAA;EAjEhB;EAmEpC,MAAA,CAAO,UAAA,UAAoB,QAAA,EAAU,EAAA,GAAK,EAAA,CAAG,CAAA;EAvEhB;EAyE7B,GAAA,CAAI,QAAA,EAAU,EAAA,GAAK,CAAA;EAvEA;EAyEnB,OAAA,IAAW,KAAA,WAAgB,EAAA,EAAI,CAAA;EAzE1B;EA2EL,OAAA,aAAoB,EAAA,CAAG,CAAA;EA3EyB;EA6EhD,IAAA,CAAK,MAAA,GAAS,SAAA,GAAY,OAAA;EA3Eb;EA6Eb,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;;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"}
package/dist/sync.js CHANGED
@@ -1,43 +1,122 @@
1
- import { createClock } from "./hlc.js";
1
+ import { compareHlc, createClock } from "./hlc.js";
2
2
  import { mergeRegister } from "./lww.js";
3
3
  //#region src/sync.ts
4
- /** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump}. */
4
+ const EMPTY_FIELDS = Object.freeze(Object.create(null));
5
+ const WHOLE = "\0mindees.whole";
6
+ /** Migrate a dump entry: pass through new {@link RecordState}, lift a legacy whole-record register. */
7
+ function migrateState(value) {
8
+ if ("fields" in value) return value;
9
+ const reg = value;
10
+ if (reg.op === "del") return {
11
+ fields: EMPTY_FIELDS,
12
+ tomb: reg.stamp
13
+ };
14
+ const fields = Object.create(null);
15
+ const record = reg.value;
16
+ if (record !== null && typeof record === "object" && !Array.isArray(record)) for (const k of Object.keys(record)) fields[k] = {
17
+ stamp: reg.stamp,
18
+ op: "set",
19
+ value: record[k]
20
+ };
21
+ else fields[WHOLE] = {
22
+ stamp: reg.stamp,
23
+ op: "set",
24
+ value: record
25
+ };
26
+ return {
27
+ fields,
28
+ tomb: null
29
+ };
30
+ }
31
+ /** Reconstruct a record's live value from its per-field state (tombstone-shadowed). */
32
+ function reconstruct(rs) {
33
+ const tomb = rs.tomb;
34
+ const live = (reg) => !!reg && reg.op === "set" && (tomb === null || compareHlc(reg.stamp, tomb) > 0);
35
+ const whole = rs.fields[WHOLE];
36
+ if (live(whole)) return whole.value;
37
+ const out = Object.create(null);
38
+ let any = false;
39
+ for (const k of Object.keys(rs.fields)) {
40
+ if (k === WHOLE) continue;
41
+ const reg = rs.fields[k];
42
+ if (!live(reg)) continue;
43
+ out[k] = reg.value;
44
+ any = true;
45
+ }
46
+ return any ? out : void 0;
47
+ }
48
+ /** Canonicalize a record's state for deterministic serialization (sorted field keys). */
49
+ function canonicalState(rs) {
50
+ const fields = Object.create(null);
51
+ for (const k of Object.keys(rs.fields).sort()) fields[k] = rs.fields[k];
52
+ return {
53
+ fields,
54
+ tomb: rs.tomb
55
+ };
56
+ }
57
+ /** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump} (legacy-migrated). */
5
58
  function createMutationLog(initial) {
6
- const state = new Map(initial);
59
+ const state = /* @__PURE__ */ new Map();
60
+ if (initial) for (const [id, value] of initial) state.set(id, migrateState(value));
7
61
  return {
8
62
  apply(op) {
9
- const incoming = op.kind === "set" ? {
10
- stamp: op.hlc,
11
- op: "set",
12
- value: op.value
13
- } : {
14
- stamp: op.hlc,
15
- op: "del"
63
+ const prev = state.get(op.recordId) ?? {
64
+ fields: EMPTY_FIELDS,
65
+ tomb: null
16
66
  };
17
- const existing = state.get(op.recordId);
18
- const merged = existing ? mergeRegister(existing, incoming) : incoming;
19
- if (existing === merged) return false;
20
- state.set(op.recordId, merged);
67
+ if (op.kind === "set") {
68
+ const value = op.value;
69
+ let changed = false;
70
+ let fields = prev.fields;
71
+ for (const k of Object.keys(value)) {
72
+ const incoming = {
73
+ stamp: op.hlc,
74
+ op: "set",
75
+ value: value[k]
76
+ };
77
+ const existing = Object.hasOwn(fields, k) ? fields[k] : void 0;
78
+ const merged = existing ? mergeRegister(existing, incoming) : incoming;
79
+ if (merged === existing) continue;
80
+ if (!changed) {
81
+ const copy = Object.create(null);
82
+ for (const j of Object.keys(fields)) copy[j] = fields[j];
83
+ fields = copy;
84
+ changed = true;
85
+ }
86
+ fields[k] = merged;
87
+ }
88
+ if (!changed) return false;
89
+ state.set(op.recordId, {
90
+ fields,
91
+ tomb: prev.tomb
92
+ });
93
+ return true;
94
+ }
95
+ const tomb = prev.tomb === null || compareHlc(op.hlc, prev.tomb) > 0 ? op.hlc : prev.tomb;
96
+ if (tomb === prev.tomb) return false;
97
+ state.set(op.recordId, {
98
+ fields: prev.fields,
99
+ tomb
100
+ });
21
101
  return true;
22
102
  },
23
103
  get(recordId) {
24
- const reg = state.get(recordId);
25
- return reg ? lwwReg(reg) : void 0;
104
+ const rs = state.get(recordId);
105
+ return rs ? reconstruct(rs) : void 0;
26
106
  },
27
107
  records() {
28
108
  const out = [];
29
- for (const [recordId, reg] of state) if (reg.op === "set") out.push([recordId, reg.value]);
109
+ for (const [recordId, rs] of state) {
110
+ const value = reconstruct(rs);
111
+ if (value !== void 0) out.push([recordId, value]);
112
+ }
30
113
  return out;
31
114
  },
32
115
  dump() {
33
- return [...state];
116
+ return [...state].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(([id, rs]) => [id, canonicalState(rs)]);
34
117
  }
35
118
  };
36
119
  }
37
- /** Read the live value of a single register (mirrors `lwwGet` for a bare register). */
38
- function lwwReg(reg) {
39
- return reg.op === "set" ? reg.value : void 0;
40
- }
41
120
  /** An in-memory reference transport: an append-only, op-id-deduped log. */
42
121
  function createMemoryHub() {
43
122
  const log = [];
@@ -78,7 +157,7 @@ function createSyncEngine(options) {
78
157
  const outbox = snapshot ? [...snapshot.outbox] : [];
79
158
  let seq = snapshot?.seq ?? 0;
80
159
  let cursor = snapshot?.cursor ?? null;
81
- let syncing = false;
160
+ let syncChain = Promise.resolve();
82
161
  const emit = (op) => {
83
162
  log.apply(op);
84
163
  outbox.push(op);
@@ -119,14 +198,10 @@ function createSyncEngine(options) {
119
198
  pending() {
120
199
  return [...outbox];
121
200
  },
122
- async sync(signal) {
123
- if (syncing) return;
124
- syncing = true;
125
- try {
126
- await runSync(signal);
127
- } finally {
128
- syncing = false;
129
- }
201
+ sync(signal) {
202
+ const run = syncChain.then(() => runSync(signal));
203
+ syncChain = run.then(() => void 0, () => void 0);
204
+ return run;
130
205
  },
131
206
  export() {
132
207
  return {
@@ -142,12 +217,12 @@ function createSyncEngine(options) {
142
217
  if (outbox.length > 0) {
143
218
  const sending = [...outbox];
144
219
  const { acked } = await transport.push(sending);
145
- if (signal?.aborted) return;
146
220
  const ackedSet = new Set(acked);
147
221
  for (let i = outbox.length - 1; i >= 0; i--) {
148
222
  const op = outbox[i];
149
223
  if (op && ackedSet.has(op.id)) outbox.splice(i, 1);
150
224
  }
225
+ if (signal?.aborted) return;
151
226
  }
152
227
  const { ops, cursor: next } = await transport.pull(cursor);
153
228
  if (signal?.aborted) return;
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, createClock, type Hlc } from './hlc'\nimport { 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} & ({ readonly kind: 'set'; readonly value: T } | { readonly kind: 'del' })\n\n/** A convergent materialized view: per-record 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 register state (incl. tombstones) — for persistence. */\n dump(): Array<readonly [Id, LwwRegister<T>]>\n}\n\n/** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump}. */\nexport function createMutationLog<T>(\n initial?: Iterable<readonly [Id, LwwRegister<T>]>,\n): MutationLog<T> {\n const state = new Map<Id, LwwRegister<T>>(initial)\n\n return {\n apply(op): boolean {\n const incoming: LwwRegister<T> =\n op.kind === 'set'\n ? { stamp: op.hlc, op: 'set', value: op.value }\n : { stamp: op.hlc, op: 'del' }\n const existing = state.get(op.recordId)\n const merged = existing ? mergeRegister(existing, incoming) : incoming\n if (existing === merged) return false // mergeRegister returns an arg by reference\n state.set(op.recordId, merged)\n return true\n },\n get(recordId): T | undefined {\n const reg = state.get(recordId)\n return reg ? lwwReg(reg) : undefined\n },\n records(): Array<readonly [Id, T]> {\n const out: Array<readonly [Id, T]> = []\n for (const [recordId, reg] of state) {\n if (reg.op === 'set') out.push([recordId, reg.value])\n }\n return out\n },\n dump(): Array<readonly [Id, LwwRegister<T>]> {\n return [...state]\n },\n }\n}\n\n/** Read the live value of a single register (mirrors `lwwGet` for a bare register). */\nfunction lwwReg<T>(reg: LwwRegister<T>): T | undefined {\n return reg.op === 'set' ? reg.value : undefined\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 /** The materialized per-record register state. */\n readonly registers: ReadonlyArray<readonly [Id, LwwRegister<T>]>\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 /** Optimistically set a record locally and queue the op for the next `sync()`. */\n set(collection: string, recordId: Id, value: 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 let syncing = false\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 async sync(signal): Promise<void> {\n // Not re-entrant: overlapping calls would double-pull and regress the cursor.\n // Concurrent calls coalesce into the in-flight one; call sync() again afterward.\n if (syncing) return\n syncing = true\n try {\n await runSync(signal)\n } finally {\n syncing = false\n }\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 if (signal?.aborted) return\n const ackedSet = new Set(acked)\n // Drop acked ops from the outbox (keep any the server didn't accept).\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 }\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":";;;;AA8CA,SAAgB,kBACd,SACgB;CAChB,MAAM,QAAQ,IAAI,IAAwB,OAAO;CAEjD,OAAO;EACL,MAAM,IAAa;GACjB,MAAM,WACJ,GAAG,SAAS,QACR;IAAE,OAAO,GAAG;IAAK,IAAI;IAAO,OAAO,GAAG;GAAM,IAC5C;IAAE,OAAO,GAAG;IAAK,IAAI;GAAM;GACjC,MAAM,WAAW,MAAM,IAAI,GAAG,QAAQ;GACtC,MAAM,SAAS,WAAW,cAAc,UAAU,QAAQ,IAAI;GAC9D,IAAI,aAAa,QAAQ,OAAO;GAChC,MAAM,IAAI,GAAG,UAAU,MAAM;GAC7B,OAAO;EACT;EACA,IAAI,UAAyB;GAC3B,MAAM,MAAM,MAAM,IAAI,QAAQ;GAC9B,OAAO,MAAM,OAAO,GAAG,IAAI,KAAA;EAC7B;EACA,UAAmC;GACjC,MAAM,MAA+B,CAAC;GACtC,KAAK,MAAM,CAAC,UAAU,QAAQ,OAC5B,IAAI,IAAI,OAAO,OAAO,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC;GAEtD,OAAO;EACT;EACA,OAA6C;GAC3C,OAAO,CAAC,GAAG,KAAK;EAClB;CACF;AACF;;AAGA,SAAS,OAAU,KAAoC;CACrD,OAAO,IAAI,OAAO,QAAQ,IAAI,QAAQ,KAAA;AACxC;;AAmBA,SAAgB,kBAAuC;CACrD,MAAM,MAAoB,CAAC;CAC3B,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO;EACL,KAAK,KAAmC;GACtC,MAAM,QAAkB,CAAC;GACzB,KAAK,MAAM,MAAM,KAAK;IACpB,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,GAAG;KACpB,KAAK,IAAI,GAAG,EAAE;KACd,IAAI,KAAK,EAAE;IACb;IACA,MAAM,KAAK,GAAG,EAAE;GAClB;GACA,OAAO,QAAQ,QAAQ,EAAE,MAAM,CAAC;EAClC;EACA,KAAK,QAAmD;GACtD,MAAM,OAAO,UAAU;GACvB,OAAO,QAAQ,QAAQ;IAAE,KAAK,IAAI,MAAM,IAAI;IAAG,QAAQ,IAAI;GAAO,CAAC;EACrE;CACF;AACF;;AAyDA,SAAgB,iBAAoB,SAA8C;CAChF,MAAM,EAAE,QAAQ,WAAW,aAAa;CACxC,MAAM,QAAe,YAAY;EAC/B;EACA,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;EAG1C,GAAI,UAAU,QACV,EAAE,MAAM;GAAE,QAAQ,SAAS,MAAM;GAAQ,SAAS,SAAS,MAAM;EAAQ,EAAE,IAC3E,CAAC;CACP,CAAC;CACD,MAAM,MAAM,kBAAqB,UAAU,SAAS;CACpD,MAAM,SAAkB,WAAW,CAAC,GAAG,SAAS,MAAM,IAAI,CAAC;CAC3D,IAAI,MAAM,UAAU,OAAO;CAC3B,IAAI,SAAwB,UAAU,UAAU;CAChD,IAAI,UAAU;CAEd,MAAM,QAAQ,OAAqB;EACjC,IAAI,MAAM,EAAE;EACZ,OAAO,KAAK,EAAE;EACd,OAAO;CACT;CAEA,OAAO;EACL,IAAI,YAAY,UAAU,OAAc;GACtC,OAAO;GACP,OAAO,KAAK;IACV,IAAI,GAAG,OAAO,GAAG;IACjB,OAAO;IACP;IACA;IACA;IACA,KAAK,MAAM,KAAK;IAChB,MAAM;IACN;GACF,CAAC;EACH;EAEA,OAAO,YAAY,UAAiB;GAClC,OAAO;GACP,OAAO,KAAK;IACV,IAAI,GAAG,OAAO,GAAG;IACjB,OAAO;IACP;IACA;IACA;IACA,KAAK,MAAM,KAAK;IAChB,MAAM;GACR,CAAC;EACH;EAEA,IAAI,UAAyB;GAC3B,OAAO,IAAI,IAAI,QAAQ;EACzB;EACA,UAAmC;GACjC,OAAO,IAAI,QAAQ;EACrB;EACA,UAA4B;GAC1B,OAAO,CAAC,GAAG,MAAM;EACnB;EAEA,MAAM,KAAK,QAAuB;GAGhC,IAAI,SAAS;GACb,UAAU;GACV,IAAI;IACF,MAAM,QAAQ,MAAM;GACtB,UAAU;IACR,UAAU;GACZ;EACF;EAEA,SAA0B;GACxB,OAAO;IAAE;IAAK;IAAQ,WAAW,IAAI,KAAK;IAAG,QAAQ,CAAC,GAAG,MAAM;IAAG,OAAO,MAAM,KAAK;GAAE;EACxF;CACF;CAEA,eAAe,QAAQ,QAAmC;EACxD,IAAI,OAAO,SAAS,GAAG;GACrB,MAAM,UAAU,CAAC,GAAG,MAAM;GAC1B,MAAM,EAAE,UAAU,MAAM,UAAU,KAAK,OAAO;GAC9C,IAAI,QAAQ,SAAS;GACrB,MAAM,WAAW,IAAI,IAAI,KAAK;GAE9B,KAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;IAC3C,MAAM,KAAK,OAAO;IAClB,IAAI,MAAM,SAAS,IAAI,GAAG,EAAE,GAAG,OAAO,OAAO,GAAG,CAAC;GACnD;EACF;EACA,MAAM,EAAE,KAAK,QAAQ,SAAS,MAAM,UAAU,KAAK,MAAM;EACzD,IAAI,QAAQ,SAAS;EACrB,KAAK,MAAM,MAAM,KACf,IAAI;GAMF,MAAM,OAAO,GAAG,GAAG;GACnB,IAAI,MAAM,EAAE;EACd,QAAQ,CAGR;EAEF,SAAS;CACX;AACF"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/data",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
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.1.0"
30
+ "@mindees/core": "0.3.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "fast-check": "4.8.0"