@mindees/data 0.1.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/LICENSE +31 -0
- package/README.md +70 -0
- package/dist/collection.d.ts +76 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +171 -0
- package/dist/collection.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/hlc.d.ts +79 -0
- package/dist/hlc.d.ts.map +1 -0
- package/dist/hlc.js +95 -0
- package/dist/hlc.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lww.d.ts +38 -0
- package/dist/lww.d.ts.map +1 -0
- package/dist/lww.js +105 -0
- package/dist/lww.js.map +1 -0
- package/dist/or-set.d.ts +33 -0
- package/dist/or-set.d.ts.map +1 -0
- package/dist/or-set.js +71 -0
- package/dist/or-set.js.map +1 -0
- package/dist/persist.d.ts +25 -0
- package/dist/persist.d.ts.map +1 -0
- package/dist/persist.js +16 -0
- package/dist/persist.js.map +1 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +38 -0
- package/dist/server.js.map +1 -0
- package/dist/sync.d.ts +108 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +164 -0
- package/dist/sync.js.map +1 -0
- package/dist/version-vector.d.ts +28 -0
- package/dist/version-vector.d.ts.map +1 -0
- package/dist/version-vector.js +40 -0
- package/dist/version-vector.js.map +1 -0
- package/package.json +39 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Collection, CollectionOptions, Id, OptimisticChange, createCollection } from "./collection.js";
|
|
2
|
+
import { DataError, DataErrorCode } from "./errors.js";
|
|
3
|
+
import { Clock, ClockOptions, Hlc, compareHlc, createClock, decodeHlc, encodeHlc } from "./hlc.js";
|
|
4
|
+
import { LwwMap, LwwRegister, lwwDelete, lwwGet, lwwHas, lwwKeys, lwwSet, mergeLwwMap, mergeRegister } from "./lww.js";
|
|
5
|
+
import { OrSet, emptyOrSet, mergeOrSet, orAdd, orHas, orRemove, orValues } from "./or-set.js";
|
|
6
|
+
import { Persistence, createMemoryPersistence } from "./persist.js";
|
|
7
|
+
import { Cursor, MutationLog, Op, SyncEngine, SyncEngineOptions, SyncSnapshot, SyncTransport, createMemoryHub, createMutationLog, createSyncEngine } from "./sync.js";
|
|
8
|
+
import { VersionVector, vvDominates, vvEquals, vvGet, vvMerge, vvObserve } from "./version-vector.js";
|
|
9
|
+
import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@mindees/core";
|
|
10
|
+
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
/** The npm package name. */
|
|
13
|
+
declare const name = "@mindees/data";
|
|
14
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
15
|
+
declare const VERSION = "0.1.0";
|
|
16
|
+
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
17
|
+
declare const maturity: Maturity;
|
|
18
|
+
/**
|
|
19
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
20
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
21
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
22
|
+
*/
|
|
23
|
+
declare const info: PackageInfo;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { type Clock, type ClockOptions, type Collection, type CollectionOptions, type Cursor, DataError, type DataErrorCode, type Hlc, type Id, type LwwMap, type LwwRegister, type Maturity, type MutationLog, NotImplementedError, type Op, type OptimisticChange, type OrSet, type PackageInfo, type Persistence, type SyncEngine, type SyncEngineOptions, type SyncSnapshot, type SyncTransport, VERSION, type VersionVector, compareHlc, createClock, createCollection, createMemoryHub, createMemoryPersistence, createMutationLog, createSyncEngine, decodeHlc, emptyOrSet, encodeHlc, info, lwwDelete, lwwGet, lwwHas, lwwKeys, lwwSet, maturity, mergeLwwMap, mergeOrSet, mergeRegister, name, notImplemented, orAdd, orHas, orRemove, orValues, vvDominates, vvEquals, vvGet, vvMerge, vvObserve };
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;cAoBa,IAAA;;cAGA,OAAA;AAGb;AAAA,cAAa,QAAA,EAAU,QAAyB;;;AAAA;AAOhD;;cAAa,IAAA,EAAM,WAAiE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DataError } from "./errors.js";
|
|
2
|
+
import { createCollection } from "./collection.js";
|
|
3
|
+
import { compareHlc, createClock, decodeHlc, encodeHlc } from "./hlc.js";
|
|
4
|
+
import { lwwDelete, lwwGet, lwwHas, lwwKeys, lwwSet, mergeLwwMap, mergeRegister } from "./lww.js";
|
|
5
|
+
import { emptyOrSet, mergeOrSet, orAdd, orHas, orRemove, orValues } from "./or-set.js";
|
|
6
|
+
import { createMemoryPersistence } from "./persist.js";
|
|
7
|
+
import { createMemoryHub, createMutationLog, createSyncEngine } from "./sync.js";
|
|
8
|
+
import { vvDominates, vvEquals, vvGet, vvMerge, vvObserve } from "./version-vector.js";
|
|
9
|
+
import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
10
|
+
//#region src/index.ts
|
|
11
|
+
/** The npm package name. */
|
|
12
|
+
const name = "@mindees/data";
|
|
13
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
+
const VERSION = "0.1.0";
|
|
15
|
+
/** Current maturity of this package. See the repository `STATUS.md`. */
|
|
16
|
+
const maturity = "experimental";
|
|
17
|
+
/**
|
|
18
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
19
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
20
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
21
|
+
*/
|
|
22
|
+
const info = Object.freeze({
|
|
23
|
+
name,
|
|
24
|
+
version: VERSION,
|
|
25
|
+
maturity
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
export { DataError, NotImplementedError, VERSION, compareHlc, createClock, createCollection, createMemoryHub, createMemoryPersistence, createMutationLog, createSyncEngine, decodeHlc, emptyOrSet, encodeHlc, info, lwwDelete, lwwGet, lwwHas, lwwKeys, lwwSet, maturity, mergeLwwMap, mergeOrSet, mergeRegister, name, notImplemented, orAdd, orHas, orRemove, orValues, vvDominates, vvEquals, vvGet, vvMerge, vvObserve };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
package/dist/lww.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Hlc } from "./hlc.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lww.d.ts
|
|
4
|
+
/** A last-write-wins register: a value (or a delete tombstone) tagged with an HLC stamp. */
|
|
5
|
+
type LwwRegister<V> = {
|
|
6
|
+
readonly stamp: Hlc;
|
|
7
|
+
readonly op: 'set';
|
|
8
|
+
readonly value: V;
|
|
9
|
+
} | {
|
|
10
|
+
readonly stamp: Hlc;
|
|
11
|
+
readonly op: 'del';
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Merge two registers: keep the one with the greater stamp. When two **different**
|
|
15
|
+
* registers carry the **same** stamp (reachable via a reused `nodeId` across clock
|
|
16
|
+
* instances, or a hostile sync peer that replays a stamp), the content is broken
|
|
17
|
+
* deterministically — **delete wins** over set, and equal ops break by a stable,
|
|
18
|
+
* **total** key over the value — so the result is independent of argument order
|
|
19
|
+
* (required for convergence; a stamp-only compare would diverge by delivery order).
|
|
20
|
+
*/
|
|
21
|
+
declare function mergeRegister<V>(a: LwwRegister<V>, b: LwwRegister<V>): LwwRegister<V>;
|
|
22
|
+
/** A per-field last-write-wins map: each field is an independent {@link LwwRegister}. */
|
|
23
|
+
type LwwMap<V> = Readonly<Record<string, LwwRegister<V>>>;
|
|
24
|
+
/** Set `field` to `value` at `stamp` (never regresses a field carrying a greater stamp). */
|
|
25
|
+
declare function lwwSet<V>(map: LwwMap<V>, field: string, value: V, stamp: Hlc): LwwMap<V>;
|
|
26
|
+
/** Delete `field` at `stamp` (a tombstone; never regresses a greater stamp). */
|
|
27
|
+
declare function lwwDelete<V>(map: LwwMap<V>, field: string, stamp: Hlc): LwwMap<V>;
|
|
28
|
+
/** The live value of `field`, or `undefined` if unset or deleted. */
|
|
29
|
+
declare function lwwGet<V>(map: LwwMap<V>, field: string): V | undefined;
|
|
30
|
+
/** Whether `field` is live (present and not a tombstone). */
|
|
31
|
+
declare function lwwHas<V>(map: LwwMap<V>, field: string): boolean;
|
|
32
|
+
/** The live (non-deleted) field names. */
|
|
33
|
+
declare function lwwKeys<V>(map: LwwMap<V>): string[];
|
|
34
|
+
/** Merge two maps field-by-field (union of fields; {@link mergeRegister} per field). */
|
|
35
|
+
declare function mergeLwwMap<V>(a: LwwMap<V>, b: LwwMap<V>): LwwMap<V>;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { LwwMap, LwwRegister, lwwDelete, lwwGet, lwwHas, lwwKeys, lwwSet, mergeLwwMap, mergeRegister };
|
|
38
|
+
//# sourceMappingURL=lww.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lww.d.ts","names":[],"sources":["../src/lww.ts"],"mappings":";;;;KAaY,WAAA;EAAA,SACG,KAAA,EAAO,GAAA;EAAA,SAAc,EAAA;EAAA,SAAoB,KAAA,EAAO,CAAA;AAAA;EAAA,SAChD,KAAA,EAAO,GAAA;EAAA,SAAc,EAAA;AAAA;;;AAAE;AAUtC;;;;;iBAAgB,aAAA,IAAiB,CAAA,EAAG,WAAA,CAAY,CAAA,GAAI,CAAA,EAAG,WAAA,CAAY,CAAA,IAAK,WAAA,CAAY,CAAA;;KA+CxE,MAAA,MAAY,QAAA,CAAS,MAAA,SAAe,WAAA,CAAY,CAAA;;iBAG5C,MAAA,IAAU,GAAA,EAAK,MAAA,CAAO,CAAA,GAAI,KAAA,UAAe,KAAA,EAAO,CAAA,EAAG,KAAA,EAAO,GAAA,GAAM,MAAA,CAAO,CAAA;;iBAKvE,SAAA,IAAa,GAAA,EAAK,MAAA,CAAO,CAAA,GAAI,KAAA,UAAe,KAAA,EAAO,GAAA,GAAM,MAAA,CAAO,CAAA;;iBAchE,MAAA,IAAU,GAAA,EAAK,MAAA,CAAO,CAAA,GAAI,KAAA,WAAgB,CAAA;;iBAO1C,MAAA,IAAU,GAAA,EAAK,MAAM,CAAC,CAAA,GAAI,KAAA;;iBAM1B,OAAA,IAAW,GAAA,EAAK,MAAM,CAAC,CAAA;;iBAKvB,WAAA,IAAe,CAAA,EAAG,MAAA,CAAO,CAAA,GAAI,CAAA,EAAG,MAAA,CAAO,CAAA,IAAK,MAAA,CAAO,CAAA"}
|
package/dist/lww.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { compareHlc } from "./hlc.js";
|
|
2
|
+
//#region src/lww.ts
|
|
3
|
+
/**
|
|
4
|
+
* Last-Write-Wins CRDTs for Continuum — an HLC-stamped register and a **per-field**
|
|
5
|
+
* map. State-based (CvRDT): `merge` is commutative, associative, and idempotent, so
|
|
6
|
+
* replicas converge regardless of message order/duplication. The merge key is the 10B
|
|
7
|
+
* {@link Hlc} (a total order), so merge and sync share one ordering. See
|
|
8
|
+
* `docs/adr/0014-continuum-crdt.md`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Merge two registers: keep the one with the greater stamp. When two **different**
|
|
14
|
+
* registers carry the **same** stamp (reachable via a reused `nodeId` across clock
|
|
15
|
+
* instances, or a hostile sync peer that replays a stamp), the content is broken
|
|
16
|
+
* deterministically — **delete wins** over set, and equal ops break by a stable,
|
|
17
|
+
* **total** key over the value — so the result is independent of argument order
|
|
18
|
+
* (required for convergence; a stamp-only compare would diverge by delivery order).
|
|
19
|
+
*/
|
|
20
|
+
function mergeRegister(a, b) {
|
|
21
|
+
const c = compareHlc(a.stamp, b.stamp);
|
|
22
|
+
if (c !== 0) return c > 0 ? a : b;
|
|
23
|
+
if (a.op !== b.op) return a.op === "del" ? a : b;
|
|
24
|
+
if (a.op === "set" && b.op === "set" && !Object.is(a.value, b.value)) return tieKey(a.value) >= tieKey(b.value) ? a : b;
|
|
25
|
+
return a;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A **total**, type-tagged key for the same-stamp tie-break. Unlike raw
|
|
29
|
+
* `JSON.stringify`, it (a) never returns `undefined` (which would make the `>=`
|
|
30
|
+
* comparison `false` in BOTH orders → opposite winners → divergence), and (b)
|
|
31
|
+
* distinguishes values that stringify identically but are observably different
|
|
32
|
+
* (e.g. `NaN` and `null` both stringify to `"null"`). For JSON-representable values
|
|
33
|
+
* an equal key implies an equal value (so returning either side is convergent);
|
|
34
|
+
* functions/symbols can never arrive over sync, so their best-effort key is harmless.
|
|
35
|
+
*/
|
|
36
|
+
function tieKey(value) {
|
|
37
|
+
switch (typeof value) {
|
|
38
|
+
case "undefined": return "u";
|
|
39
|
+
case "boolean": return `b:${value}`;
|
|
40
|
+
case "number": return Number.isNaN(value) ? "n:NaN" : Object.is(value, -0) ? "n:-0" : `n:${value}`;
|
|
41
|
+
case "bigint": return `i:${value}`;
|
|
42
|
+
case "string": return `s:${value}`;
|
|
43
|
+
case "object":
|
|
44
|
+
if (value === null) return "z";
|
|
45
|
+
try {
|
|
46
|
+
return `o:${JSON.stringify(value)}`;
|
|
47
|
+
} catch {
|
|
48
|
+
return "o:circular";
|
|
49
|
+
}
|
|
50
|
+
default: return `f:${String(value)}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Set `field` to `value` at `stamp` (never regresses a field carrying a greater stamp). */
|
|
54
|
+
function lwwSet(map, field, value, stamp) {
|
|
55
|
+
return withField(map, field, {
|
|
56
|
+
stamp,
|
|
57
|
+
op: "set",
|
|
58
|
+
value
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Delete `field` at `stamp` (a tombstone; never regresses a greater stamp). */
|
|
62
|
+
function lwwDelete(map, field, stamp) {
|
|
63
|
+
return withField(map, field, {
|
|
64
|
+
stamp,
|
|
65
|
+
op: "del"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function withField(map, field, next) {
|
|
69
|
+
const existing = Object.hasOwn(map, field) ? map[field] : void 0;
|
|
70
|
+
const merged = existing ? mergeRegister(existing, next) : next;
|
|
71
|
+
const out = Object.create(null);
|
|
72
|
+
for (const k of Object.keys(map)) out[k] = map[k];
|
|
73
|
+
out[field] = merged;
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** The live value of `field`, or `undefined` if unset or deleted. */
|
|
77
|
+
function lwwGet(map, field) {
|
|
78
|
+
if (!Object.hasOwn(map, field)) return void 0;
|
|
79
|
+
const reg = map[field];
|
|
80
|
+
return reg && reg.op === "set" ? reg.value : void 0;
|
|
81
|
+
}
|
|
82
|
+
/** Whether `field` is live (present and not a tombstone). */
|
|
83
|
+
function lwwHas(map, field) {
|
|
84
|
+
if (!Object.hasOwn(map, field)) return false;
|
|
85
|
+
return map[field]?.op === "set";
|
|
86
|
+
}
|
|
87
|
+
/** The live (non-deleted) field names. */
|
|
88
|
+
function lwwKeys(map) {
|
|
89
|
+
return Object.keys(map).filter((k) => map[k]?.op === "set");
|
|
90
|
+
}
|
|
91
|
+
/** Merge two maps field-by-field (union of fields; {@link mergeRegister} per field). */
|
|
92
|
+
function mergeLwwMap(a, b) {
|
|
93
|
+
const out = Object.create(null);
|
|
94
|
+
for (const k of Object.keys(a)) out[k] = a[k];
|
|
95
|
+
for (const k of Object.keys(b)) {
|
|
96
|
+
const bv = b[k];
|
|
97
|
+
const av = Object.hasOwn(out, k) ? out[k] : void 0;
|
|
98
|
+
out[k] = av ? mergeRegister(av, bv) : bv;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { lwwDelete, lwwGet, lwwHas, lwwKeys, lwwSet, mergeLwwMap, mergeRegister };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=lww.js.map
|
package/dist/lww.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lww.js","names":[],"sources":["../src/lww.ts"],"sourcesContent":["/**\n * Last-Write-Wins CRDTs for Continuum — an HLC-stamped register and a **per-field**\n * map. State-based (CvRDT): `merge` is commutative, associative, and idempotent, so\n * replicas converge regardless of message order/duplication. The merge key is the 10B\n * {@link Hlc} (a total order), so merge and sync share one ordering. See\n * `docs/adr/0014-continuum-crdt.md`.\n *\n * @module\n */\n\nimport { compareHlc, type Hlc } from './hlc'\n\n/** A last-write-wins register: a value (or a delete tombstone) tagged with an HLC stamp. */\nexport type LwwRegister<V> =\n | { readonly stamp: Hlc; readonly op: 'set'; readonly value: V }\n | { readonly stamp: Hlc; readonly op: 'del' }\n\n/**\n * Merge two registers: keep the one with the greater stamp. When two **different**\n * registers carry the **same** stamp (reachable via a reused `nodeId` across clock\n * instances, or a hostile sync peer that replays a stamp), the content is broken\n * deterministically — **delete wins** over set, and equal ops break by a stable,\n * **total** key over the value — so the result is independent of argument order\n * (required for convergence; a stamp-only compare would diverge by delivery order).\n */\nexport function mergeRegister<V>(a: LwwRegister<V>, b: LwwRegister<V>): LwwRegister<V> {\n const c = compareHlc(a.stamp, b.stamp)\n if (c !== 0) return c > 0 ? a : b\n if (a.op !== b.op) return a.op === 'del' ? a : b // delete wins a same-stamp tie\n if (a.op === 'set' && b.op === 'set' && !Object.is(a.value, b.value)) {\n return tieKey(a.value) >= tieKey(b.value) ? a : b\n }\n return a\n}\n\n/**\n * A **total**, type-tagged key for the same-stamp tie-break. Unlike raw\n * `JSON.stringify`, it (a) never returns `undefined` (which would make the `>=`\n * comparison `false` in BOTH orders → opposite winners → divergence), and (b)\n * distinguishes values that stringify identically but are observably different\n * (e.g. `NaN` and `null` both stringify to `\"null\"`). For JSON-representable values\n * an equal key implies an equal value (so returning either side is convergent);\n * functions/symbols can never arrive over sync, so their best-effort key is harmless.\n */\nfunction tieKey(value: unknown): string {\n switch (typeof value) {\n case 'undefined':\n return 'u'\n case 'boolean':\n return `b:${value}`\n case 'number':\n // `-0` and `+0` are distinct under Object.is but both stringify to \"0\"; tag `-0`\n // so a same-stamp -0-vs-0 tie still picks one winner deterministically.\n return Number.isNaN(value) ? 'n:NaN' : Object.is(value, -0) ? 'n:-0' : `n:${value}`\n case 'bigint':\n return `i:${value}`\n case 'string':\n return `s:${value}`\n case 'object': {\n if (value === null) return 'z'\n try {\n return `o:${JSON.stringify(value)}`\n } catch {\n return 'o:circular' // unserializable object — cannot sync as data anyway\n }\n }\n default:\n return `f:${String(value)}` // function | symbol — never crosses the sync boundary\n }\n}\n\n/** A per-field last-write-wins map: each field is an independent {@link LwwRegister}. */\nexport type LwwMap<V> = Readonly<Record<string, LwwRegister<V>>>\n\n/** Set `field` to `value` at `stamp` (never regresses a field carrying a greater stamp). */\nexport function lwwSet<V>(map: LwwMap<V>, field: string, value: V, stamp: Hlc): LwwMap<V> {\n return withField(map, field, { stamp, op: 'set', value })\n}\n\n/** Delete `field` at `stamp` (a tombstone; never regresses a greater stamp). */\nexport function lwwDelete<V>(map: LwwMap<V>, field: string, stamp: Hlc): LwwMap<V> {\n return withField(map, field, { stamp, op: 'del' })\n}\n\nfunction withField<V>(map: LwwMap<V>, field: string, next: LwwRegister<V>): LwwMap<V> {\n const existing = Object.hasOwn(map, field) ? map[field] : undefined\n const merged = existing ? mergeRegister(existing, next) : next\n const out: Record<string, LwwRegister<V>> = Object.create(null)\n for (const k of Object.keys(map)) out[k] = map[k] as LwwRegister<V>\n out[field] = merged\n return out\n}\n\n/** The live value of `field`, or `undefined` if unset or deleted. */\nexport function lwwGet<V>(map: LwwMap<V>, field: string): V | undefined {\n if (!Object.hasOwn(map, field)) return undefined\n const reg = map[field]\n return reg && reg.op === 'set' ? reg.value : undefined\n}\n\n/** Whether `field` is live (present and not a tombstone). */\nexport function lwwHas<V>(map: LwwMap<V>, field: string): boolean {\n if (!Object.hasOwn(map, field)) return false\n return map[field]?.op === 'set'\n}\n\n/** The live (non-deleted) field names. */\nexport function lwwKeys<V>(map: LwwMap<V>): string[] {\n return Object.keys(map).filter((k) => map[k]?.op === 'set')\n}\n\n/** Merge two maps field-by-field (union of fields; {@link mergeRegister} per field). */\nexport function mergeLwwMap<V>(a: LwwMap<V>, b: LwwMap<V>): LwwMap<V> {\n const out: Record<string, LwwRegister<V>> = Object.create(null)\n for (const k of Object.keys(a)) out[k] = a[k] as LwwRegister<V>\n for (const k of Object.keys(b)) {\n const bv = b[k] as LwwRegister<V>\n const av = Object.hasOwn(out, k) ? out[k] : undefined\n out[k] = av ? mergeRegister(av, bv) : bv\n }\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,cAAiB,GAAmB,GAAmC;CACrF,MAAM,IAAI,WAAW,EAAE,OAAO,EAAE,KAAK;CACrC,IAAI,MAAM,GAAG,OAAO,IAAI,IAAI,IAAI;CAChC,IAAI,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,OAAO,QAAQ,IAAI;CAC/C,IAAI,EAAE,OAAO,SAAS,EAAE,OAAO,SAAS,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,KAAK,GACjE,OAAO,OAAO,EAAE,KAAK,KAAK,OAAO,EAAE,KAAK,IAAI,IAAI;CAElD,OAAO;AACT;;;;;;;;;;AAWA,SAAS,OAAO,OAAwB;CACtC,QAAQ,OAAO,OAAf;EACE,KAAK,aACH,OAAO;EACT,KAAK,WACH,OAAO,KAAK;EACd,KAAK,UAGH,OAAO,OAAO,MAAM,KAAK,IAAI,UAAU,OAAO,GAAG,OAAO,EAAE,IAAI,SAAS,KAAK;EAC9E,KAAK,UACH,OAAO,KAAK;EACd,KAAK,UACH,OAAO,KAAK;EACd,KAAK;GACH,IAAI,UAAU,MAAM,OAAO;GAC3B,IAAI;IACF,OAAO,KAAK,KAAK,UAAU,KAAK;GAClC,QAAQ;IACN,OAAO;GACT;EAEF,SACE,OAAO,KAAK,OAAO,KAAK;CAC5B;AACF;;AAMA,SAAgB,OAAU,KAAgB,OAAe,OAAU,OAAuB;CACxF,OAAO,UAAU,KAAK,OAAO;EAAE;EAAO,IAAI;EAAO;CAAM,CAAC;AAC1D;;AAGA,SAAgB,UAAa,KAAgB,OAAe,OAAuB;CACjF,OAAO,UAAU,KAAK,OAAO;EAAE;EAAO,IAAI;CAAM,CAAC;AACnD;AAEA,SAAS,UAAa,KAAgB,OAAe,MAAiC;CACpF,MAAM,WAAW,OAAO,OAAO,KAAK,KAAK,IAAI,IAAI,SAAS,KAAA;CAC1D,MAAM,SAAS,WAAW,cAAc,UAAU,IAAI,IAAI;CAC1D,MAAM,MAAsC,OAAO,OAAO,IAAI;CAC9D,KAAK,MAAM,KAAK,OAAO,KAAK,GAAG,GAAG,IAAI,KAAK,IAAI;CAC/C,IAAI,SAAS;CACb,OAAO;AACT;;AAGA,SAAgB,OAAU,KAAgB,OAA8B;CACtE,IAAI,CAAC,OAAO,OAAO,KAAK,KAAK,GAAG,OAAO,KAAA;CACvC,MAAM,MAAM,IAAI;CAChB,OAAO,OAAO,IAAI,OAAO,QAAQ,IAAI,QAAQ,KAAA;AAC/C;;AAGA,SAAgB,OAAU,KAAgB,OAAwB;CAChE,IAAI,CAAC,OAAO,OAAO,KAAK,KAAK,GAAG,OAAO;CACvC,OAAO,IAAI,QAAQ,OAAO;AAC5B;;AAGA,SAAgB,QAAW,KAA0B;CACnD,OAAO,OAAO,KAAK,GAAG,EAAE,QAAQ,MAAM,IAAI,IAAI,OAAO,KAAK;AAC5D;;AAGA,SAAgB,YAAe,GAAc,GAAyB;CACpE,MAAM,MAAsC,OAAO,OAAO,IAAI;CAC9D,KAAK,MAAM,KAAK,OAAO,KAAK,CAAC,GAAG,IAAI,KAAK,EAAE;CAC3C,KAAK,MAAM,KAAK,OAAO,KAAK,CAAC,GAAG;EAC9B,MAAM,KAAK,EAAE;EACb,MAAM,KAAK,OAAO,OAAO,KAAK,CAAC,IAAI,IAAI,KAAK,KAAA;EAC5C,IAAI,KAAK,KAAK,cAAc,IAAI,EAAE,IAAI;CACxC;CACA,OAAO;AACT"}
|
package/dist/or-set.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/or-set.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Add-wins Observed-Remove Set (OR-Set) for Continuum — a state-based CRDT set of
|
|
4
|
+
* string elements where a concurrent **add wins** over a remove. `merge` is
|
|
5
|
+
* commutative/associative/idempotent, so replicas converge. Each add carries a
|
|
6
|
+
* globally-unique **tag** (the caller supplies one, e.g. `encodeHlc(clock.tick())`);
|
|
7
|
+
* remove tombstones only the tags it has observed, so an add the remover never saw
|
|
8
|
+
* survives. See `docs/adr/0014-continuum-crdt.md`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** An add-wins OR-Set of string elements. Plain JSON; safe to sync as data. */
|
|
13
|
+
interface OrSet {
|
|
14
|
+
/** element → the add-tags currently recorded for it. */
|
|
15
|
+
readonly adds: Readonly<Record<string, readonly string[]>>;
|
|
16
|
+
/** tombstone: add-tags that have been removed. */
|
|
17
|
+
readonly removed: Readonly<Record<string, true>>;
|
|
18
|
+
}
|
|
19
|
+
/** An empty OR-Set. */
|
|
20
|
+
declare function emptyOrSet(): OrSet;
|
|
21
|
+
/** Add `element` with a globally-unique `tag` (e.g. an encoded HLC). */
|
|
22
|
+
declare function orAdd(set: OrSet, element: string, tag: string): OrSet;
|
|
23
|
+
/** Remove `element` by tombstoning every add-tag currently observed for it (add-wins). */
|
|
24
|
+
declare function orRemove(set: OrSet, element: string): OrSet;
|
|
25
|
+
/** Whether `element` is present (has an add-tag that has not been removed). */
|
|
26
|
+
declare function orHas(set: OrSet, element: string): boolean;
|
|
27
|
+
/** The present elements (sorted for a deterministic snapshot). */
|
|
28
|
+
declare function orValues(set: OrSet): string[];
|
|
29
|
+
/** Merge two OR-Sets: union `adds` (per element, de-duped) and union `removed`. */
|
|
30
|
+
declare function mergeOrSet(a: OrSet, b: OrSet): OrSet;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { OrSet, emptyOrSet, mergeOrSet, orAdd, orHas, orRemove, orValues };
|
|
33
|
+
//# sourceMappingURL=or-set.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"or-set.d.ts","names":[],"sources":["../src/or-set.ts"],"mappings":";;AAYA;;;;;;;;;;UAAiB,KAAA;EAES;EAAA,SAAf,IAAA,EAAM,QAAA,CAAS,MAAA;EAEN;EAAA,SAAT,OAAA,EAAS,QAAA,CAAS,MAAA;AAAA;AAAM;AAAA,iBAInB,UAAA,IAAc,KAAK;;iBAKnB,KAAA,CAAM,GAAA,EAAK,KAAA,EAAO,OAAA,UAAiB,GAAA,WAAc,KAAK;;iBAStD,QAAA,CAAS,GAAA,EAAK,KAAA,EAAO,OAAA,WAAkB,KAAK;AAT5D;AAAA,iBAmBgB,KAAA,CAAM,GAAA,EAAK,KAAK,EAAE,OAAA;;iBAOlB,QAAA,CAAS,GAAU,EAAL,KAAK;;iBAOnB,UAAA,CAAW,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,KAAA,GAAQ,KAAA"}
|
package/dist/or-set.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/or-set.ts
|
|
2
|
+
/** An empty OR-Set. */
|
|
3
|
+
function emptyOrSet() {
|
|
4
|
+
return {
|
|
5
|
+
adds: Object.create(null),
|
|
6
|
+
removed: Object.create(null)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** Add `element` with a globally-unique `tag` (e.g. an encoded HLC). */
|
|
10
|
+
function orAdd(set, element, tag) {
|
|
11
|
+
const adds = cloneAdds(set.adds);
|
|
12
|
+
const existing = Object.hasOwn(adds, element) ? adds[element] : [];
|
|
13
|
+
if (!existing.includes(tag)) adds[element] = [...existing, tag];
|
|
14
|
+
else adds[element] = existing;
|
|
15
|
+
return {
|
|
16
|
+
adds,
|
|
17
|
+
removed: set.removed
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Remove `element` by tombstoning every add-tag currently observed for it (add-wins). */
|
|
21
|
+
function orRemove(set, element) {
|
|
22
|
+
if (!Object.hasOwn(set.adds, element)) return set;
|
|
23
|
+
const observed = set.adds[element] ?? [];
|
|
24
|
+
if (observed.length === 0) return set;
|
|
25
|
+
const removed = cloneRemoved(set.removed);
|
|
26
|
+
for (const tag of observed) removed[tag] = true;
|
|
27
|
+
return {
|
|
28
|
+
adds: set.adds,
|
|
29
|
+
removed
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Whether `element` is present (has an add-tag that has not been removed). */
|
|
33
|
+
function orHas(set, element) {
|
|
34
|
+
if (!Object.hasOwn(set.adds, element)) return false;
|
|
35
|
+
return (set.adds[element] ?? []).some((tag) => !Object.hasOwn(set.removed, tag));
|
|
36
|
+
}
|
|
37
|
+
/** The present elements (sorted for a deterministic snapshot). */
|
|
38
|
+
function orValues(set) {
|
|
39
|
+
return Object.keys(set.adds).filter((element) => orHas(set, element)).sort();
|
|
40
|
+
}
|
|
41
|
+
/** Merge two OR-Sets: union `adds` (per element, de-duped) and union `removed`. */
|
|
42
|
+
function mergeOrSet(a, b) {
|
|
43
|
+
const adds = cloneAdds(a.adds);
|
|
44
|
+
for (const element of Object.keys(b.adds)) {
|
|
45
|
+
const bt = b.adds[element] ?? [];
|
|
46
|
+
const at = Object.hasOwn(adds, element) ? adds[element] : [];
|
|
47
|
+
const union = new Set(at);
|
|
48
|
+
for (const tag of bt) union.add(tag);
|
|
49
|
+
adds[element] = [...union];
|
|
50
|
+
}
|
|
51
|
+
const removed = cloneRemoved(a.removed);
|
|
52
|
+
for (const tag of Object.keys(b.removed)) removed[tag] = true;
|
|
53
|
+
return {
|
|
54
|
+
adds,
|
|
55
|
+
removed
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function cloneAdds(adds) {
|
|
59
|
+
const out = Object.create(null);
|
|
60
|
+
for (const element of Object.keys(adds)) out[element] = [...adds[element] ?? []];
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function cloneRemoved(removed) {
|
|
64
|
+
const out = Object.create(null);
|
|
65
|
+
for (const tag of Object.keys(removed)) out[tag] = true;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { emptyOrSet, mergeOrSet, orAdd, orHas, orRemove, orValues };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=or-set.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"or-set.js","names":[],"sources":["../src/or-set.ts"],"sourcesContent":["/**\n * Add-wins Observed-Remove Set (OR-Set) for Continuum — a state-based CRDT set of\n * string elements where a concurrent **add wins** over a remove. `merge` is\n * commutative/associative/idempotent, so replicas converge. Each add carries a\n * globally-unique **tag** (the caller supplies one, e.g. `encodeHlc(clock.tick())`);\n * remove tombstones only the tags it has observed, so an add the remover never saw\n * survives. See `docs/adr/0014-continuum-crdt.md`.\n *\n * @module\n */\n\n/** An add-wins OR-Set of string elements. Plain JSON; safe to sync as data. */\nexport interface OrSet {\n /** element → the add-tags currently recorded for it. */\n readonly adds: Readonly<Record<string, readonly string[]>>\n /** tombstone: add-tags that have been removed. */\n readonly removed: Readonly<Record<string, true>>\n}\n\n/** An empty OR-Set. */\nexport function emptyOrSet(): OrSet {\n return { adds: Object.create(null), removed: Object.create(null) }\n}\n\n/** Add `element` with a globally-unique `tag` (e.g. an encoded HLC). */\nexport function orAdd(set: OrSet, element: string, tag: string): OrSet {\n const adds: Record<string, string[]> = cloneAdds(set.adds)\n const existing = Object.hasOwn(adds, element) ? (adds[element] as string[]) : []\n if (!existing.includes(tag)) adds[element] = [...existing, tag]\n else adds[element] = existing\n return { adds, removed: set.removed }\n}\n\n/** Remove `element` by tombstoning every add-tag currently observed for it (add-wins). */\nexport function orRemove(set: OrSet, element: string): OrSet {\n if (!Object.hasOwn(set.adds, element)) return set\n const observed = (set.adds[element] as string[] | undefined) ?? []\n if (observed.length === 0) return set\n const removed: Record<string, true> = cloneRemoved(set.removed)\n for (const tag of observed) removed[tag] = true\n return { adds: set.adds, removed }\n}\n\n/** Whether `element` is present (has an add-tag that has not been removed). */\nexport function orHas(set: OrSet, element: string): boolean {\n if (!Object.hasOwn(set.adds, element)) return false\n const tags = (set.adds[element] as string[] | undefined) ?? []\n return tags.some((tag) => !Object.hasOwn(set.removed, tag))\n}\n\n/** The present elements (sorted for a deterministic snapshot). */\nexport function orValues(set: OrSet): string[] {\n return Object.keys(set.adds)\n .filter((element) => orHas(set, element))\n .sort()\n}\n\n/** Merge two OR-Sets: union `adds` (per element, de-duped) and union `removed`. */\nexport function mergeOrSet(a: OrSet, b: OrSet): OrSet {\n const adds: Record<string, string[]> = cloneAdds(a.adds)\n for (const element of Object.keys(b.adds)) {\n const bt = (b.adds[element] as string[] | undefined) ?? []\n const at = Object.hasOwn(adds, element) ? (adds[element] as string[]) : []\n const union = new Set<string>(at)\n for (const tag of bt) union.add(tag)\n adds[element] = [...union]\n }\n const removed: Record<string, true> = cloneRemoved(a.removed)\n for (const tag of Object.keys(b.removed)) removed[tag] = true\n return { adds, removed }\n}\n\nfunction cloneAdds(adds: Readonly<Record<string, readonly string[]>>): Record<string, string[]> {\n const out: Record<string, string[]> = Object.create(null)\n for (const element of Object.keys(adds))\n out[element] = [...((adds[element] as string[] | undefined) ?? [])]\n return out\n}\n\nfunction cloneRemoved(removed: Readonly<Record<string, true>>): Record<string, true> {\n const out: Record<string, true> = Object.create(null)\n for (const tag of Object.keys(removed)) out[tag] = true\n return out\n}\n"],"mappings":";;AAoBA,SAAgB,aAAoB;CAClC,OAAO;EAAE,MAAM,OAAO,OAAO,IAAI;EAAG,SAAS,OAAO,OAAO,IAAI;CAAE;AACnE;;AAGA,SAAgB,MAAM,KAAY,SAAiB,KAAoB;CACrE,MAAM,OAAiC,UAAU,IAAI,IAAI;CACzD,MAAM,WAAW,OAAO,OAAO,MAAM,OAAO,IAAK,KAAK,WAAwB,CAAC;CAC/E,IAAI,CAAC,SAAS,SAAS,GAAG,GAAG,KAAK,WAAW,CAAC,GAAG,UAAU,GAAG;MACzD,KAAK,WAAW;CACrB,OAAO;EAAE;EAAM,SAAS,IAAI;CAAQ;AACtC;;AAGA,SAAgB,SAAS,KAAY,SAAwB;CAC3D,IAAI,CAAC,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,OAAO;CAC9C,MAAM,WAAY,IAAI,KAAK,YAAqC,CAAC;CACjE,IAAI,SAAS,WAAW,GAAG,OAAO;CAClC,MAAM,UAAgC,aAAa,IAAI,OAAO;CAC9D,KAAK,MAAM,OAAO,UAAU,QAAQ,OAAO;CAC3C,OAAO;EAAE,MAAM,IAAI;EAAM;CAAQ;AACnC;;AAGA,SAAgB,MAAM,KAAY,SAA0B;CAC1D,IAAI,CAAC,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,OAAO;CAE9C,QADc,IAAI,KAAK,YAAqC,CAAC,GACjD,MAAM,QAAQ,CAAC,OAAO,OAAO,IAAI,SAAS,GAAG,CAAC;AAC5D;;AAGA,SAAgB,SAAS,KAAsB;CAC7C,OAAO,OAAO,KAAK,IAAI,IAAI,EACxB,QAAQ,YAAY,MAAM,KAAK,OAAO,CAAC,EACvC,KAAK;AACV;;AAGA,SAAgB,WAAW,GAAU,GAAiB;CACpD,MAAM,OAAiC,UAAU,EAAE,IAAI;CACvD,KAAK,MAAM,WAAW,OAAO,KAAK,EAAE,IAAI,GAAG;EACzC,MAAM,KAAM,EAAE,KAAK,YAAqC,CAAC;EACzD,MAAM,KAAK,OAAO,OAAO,MAAM,OAAO,IAAK,KAAK,WAAwB,CAAC;EACzE,MAAM,QAAQ,IAAI,IAAY,EAAE;EAChC,KAAK,MAAM,OAAO,IAAI,MAAM,IAAI,GAAG;EACnC,KAAK,WAAW,CAAC,GAAG,KAAK;CAC3B;CACA,MAAM,UAAgC,aAAa,EAAE,OAAO;CAC5D,KAAK,MAAM,OAAO,OAAO,KAAK,EAAE,OAAO,GAAG,QAAQ,OAAO;CACzD,OAAO;EAAE;EAAM;CAAQ;AACzB;AAEA,SAAS,UAAU,MAA6E;CAC9F,MAAM,MAAgC,OAAO,OAAO,IAAI;CACxD,KAAK,MAAM,WAAW,OAAO,KAAK,IAAI,GACpC,IAAI,WAAW,CAAC,GAAK,KAAK,YAAqC,CAAC,CAAE;CACpE,OAAO;AACT;AAEA,SAAS,aAAa,SAA+D;CACnF,MAAM,MAA4B,OAAO,OAAO,IAAI;CACpD,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GAAG,IAAI,OAAO;CACnD,OAAO;AACT"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/persist.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Persistence (10F) — a minimal async key/value capability so a Continuum replica
|
|
4
|
+
* survives restart (mirrors the CLI's `FileSystem` / Pulse's `UpdateStorage`). Persist a
|
|
5
|
+
* sync engine's {@link "./sync".SyncEngine.export snapshot} through any `Persistence`
|
|
6
|
+
* and restore it on next launch, so `seq` survives and op ids never collide.
|
|
7
|
+
*
|
|
8
|
+
* `createMemoryPersistence` is the reference; `localStorage`/IndexedDB (web) and native
|
|
9
|
+
* SQLite are research-track adapters (see STATUS). See
|
|
10
|
+
* `docs/adr/0016-continuum-server-persistence.md`.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** A minimal async key/value store for persisting Continuum state. */
|
|
15
|
+
interface Persistence {
|
|
16
|
+
/** Read a value, or `null` if absent. */
|
|
17
|
+
load(key: string): Promise<string | null>;
|
|
18
|
+
/** Write a value. */
|
|
19
|
+
save(key: string, value: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** An in-memory reference {@link Persistence} for tests and as a contract example. */
|
|
22
|
+
declare function createMemoryPersistence(): Persistence;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Persistence, createMemoryPersistence };
|
|
25
|
+
//# sourceMappingURL=persist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.d.ts","names":[],"sources":["../src/persist.ts"],"mappings":";;AAcA;;;;;;;;;;;;UAAiB,WAAA;EAQD;EANd,IAAA,CAAK,GAAA,WAAc,OAAA;;EAEnB,IAAA,CAAK,GAAA,UAAa,KAAA,WAAgB,OAAO;AAAA;;iBAI3B,uBAAA,IAA2B,WAAW"}
|
package/dist/persist.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/persist.ts
|
|
2
|
+
/** An in-memory reference {@link Persistence} for tests and as a contract example. */
|
|
3
|
+
function createMemoryPersistence() {
|
|
4
|
+
const store = /* @__PURE__ */ new Map();
|
|
5
|
+
return {
|
|
6
|
+
load: (key) => Promise.resolve(store.get(key) ?? null),
|
|
7
|
+
save: (key, value) => {
|
|
8
|
+
store.set(key, value);
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { createMemoryPersistence };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=persist.js.map
|
|
@@ -0,0 +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; `localStorage`/IndexedDB (web) and native\n * SQLite are research-track adapters (see STATUS). See\n * `docs/adr/0016-continuum-server-persistence.md`.\n *\n * @module\n */\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"],"mappings":";;AAsBA,SAAgB,0BAAuC;CACrD,MAAM,wBAAQ,IAAI,IAAoB;CACtC,OAAO;EACL,OAAO,QAAQ,QAAQ,QAAQ,MAAM,IAAI,GAAG,KAAK,IAAI;EACrD,OAAO,KAAK,UAAU;GACpB,MAAM,IAAI,KAAK,KAAK;GACpB,OAAO,QAAQ,QAAQ;EACzB;CACF;AACF"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Cursor, Op, SyncTransport } from "./sync.js";
|
|
2
|
+
|
|
3
|
+
//#region src/server.d.ts
|
|
4
|
+
/** An injected, append-only op log the server reads/writes (de-dupes by op id). */
|
|
5
|
+
interface OpLogStore<T> {
|
|
6
|
+
/** Append ops not already present; resolves with the ids accepted (incl. already-known). */
|
|
7
|
+
append(ops: readonly Op<T>[]): Promise<{
|
|
8
|
+
readonly acked: readonly string[];
|
|
9
|
+
}>;
|
|
10
|
+
/** Ops after `cursor` (null = from the beginning), plus the new cursor. */
|
|
11
|
+
since(cursor: Cursor | null): Promise<{
|
|
12
|
+
readonly ops: readonly Op<T>[];
|
|
13
|
+
readonly cursor: Cursor;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
/** An in-memory reference {@link OpLogStore}. */
|
|
17
|
+
declare function createMemoryOpLog<T>(): OpLogStore<T>;
|
|
18
|
+
/** A sync server — the server side of the {@link SyncTransport} contract. */
|
|
19
|
+
type SyncServer<T> = SyncTransport<T>;
|
|
20
|
+
/** Create a {@link SyncServer} backed by an injected {@link OpLogStore}. */
|
|
21
|
+
declare function createSyncServer<T>(options: {
|
|
22
|
+
readonly log: OpLogStore<T>;
|
|
23
|
+
}): SyncServer<T>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { OpLogStore, SyncServer, createMemoryOpLog, createSyncServer };
|
|
26
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;UAeiB,UAAA;EAIsB;EAFrC,MAAA,CAAO,GAAA,WAAc,EAAA,CAAG,CAAA,MAAO,OAAA;IAAA,SAAmB,KAAA;EAAA;EAA7B;EAErB,KAAA,CAAM,MAAA,EAAQ,MAAA,UAAgB,OAAA;IAAA,SAAmB,GAAA,WAAc,EAAA,CAAG,CAAA;IAAA,SAAe,MAAA,EAAQ,MAAA;EAAA;AAAA;;iBAI3E,iBAAA,OAAwB,UAAU,CAAC,CAAA;;KAuBvC,UAAA,MAAgB,aAAa,CAAC,CAAA;;iBAG1B,gBAAA,IAAoB,OAAA;EAAA,SAAoB,GAAA,EAAK,UAAA,CAAW,CAAA;AAAA,IAAO,UAAA,CAAW,CAAA"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/server.ts
|
|
2
|
+
/** An in-memory reference {@link OpLogStore}. */
|
|
3
|
+
function createMemoryOpLog() {
|
|
4
|
+
const log = [];
|
|
5
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6
|
+
return {
|
|
7
|
+
append(ops) {
|
|
8
|
+
const acked = [];
|
|
9
|
+
for (const op of ops) {
|
|
10
|
+
if (!seen.has(op.id)) {
|
|
11
|
+
seen.add(op.id);
|
|
12
|
+
log.push(op);
|
|
13
|
+
}
|
|
14
|
+
acked.push(op.id);
|
|
15
|
+
}
|
|
16
|
+
return Promise.resolve({ acked });
|
|
17
|
+
},
|
|
18
|
+
since(cursor) {
|
|
19
|
+
const from = cursor ?? 0;
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
ops: log.slice(from),
|
|
22
|
+
cursor: log.length
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Create a {@link SyncServer} backed by an injected {@link OpLogStore}. */
|
|
28
|
+
function createSyncServer(options) {
|
|
29
|
+
const { log } = options;
|
|
30
|
+
return {
|
|
31
|
+
push: (ops) => log.append(ops),
|
|
32
|
+
pull: (cursor) => log.since(cursor)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { createMemoryOpLog, createSyncServer };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * The Continuum reference **sync server** (10E) — the durable counterpart of 10D's\n * in-memory hub. `createSyncServer` serves the {@link SyncTransport} contract over an\n * **injected, append-only op log** (`OpLogStore`), so a DB/object-store backend slots\n * in later without touching the server. It never signs and never trusts blindly: ops\n * are stored verbatim and each client validates on apply (the engine HLC-validates\n * pulled ops). Pure-TS; a `node:http` adapter lives in `examples/data-sync-server/`.\n * See `docs/adr/0016-continuum-server-persistence.md`.\n *\n * @module\n */\n\nimport type { Cursor, Op, SyncTransport } from './sync'\n\n/** An injected, append-only op log the server reads/writes (de-dupes by op id). */\nexport interface OpLogStore<T> {\n /** Append ops not already present; resolves with the ids accepted (incl. already-known). */\n append(ops: readonly Op<T>[]): Promise<{ readonly acked: readonly string[] }>\n /** Ops after `cursor` (null = from the beginning), plus the new cursor. */\n since(cursor: Cursor | null): Promise<{ readonly ops: readonly Op<T>[]; readonly cursor: Cursor }>\n}\n\n/** An in-memory reference {@link OpLogStore}. */\nexport function createMemoryOpLog<T>(): OpLogStore<T> {\n const log: Op<T>[] = []\n const seen = new Set<string>()\n return {\n append(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)\n }\n return Promise.resolve({ acked })\n },\n since(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 sync server — the server side of the {@link SyncTransport} contract. */\nexport type SyncServer<T> = SyncTransport<T>\n\n/** Create a {@link SyncServer} backed by an injected {@link OpLogStore}. */\nexport function createSyncServer<T>(options: { readonly log: OpLogStore<T> }): SyncServer<T> {\n const { log } = options\n return {\n push: (ops) => log.append(ops),\n pull: (cursor) => log.since(cursor),\n }\n}\n"],"mappings":";;AAuBA,SAAgB,oBAAsC;CACpD,MAAM,MAAe,CAAC;CACtB,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO;EACL,OAAO,KAAmC;GACxC,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,MAAM,QAAmD;GACvD,MAAM,OAAO,UAAU;GACvB,OAAO,QAAQ,QAAQ;IAAE,KAAK,IAAI,MAAM,IAAI;IAAG,QAAQ,IAAI;GAAO,CAAC;EACrE;CACF;AACF;;AAMA,SAAgB,iBAAoB,SAAyD;CAC3F,MAAM,EAAE,QAAQ;CAChB,OAAO;EACL,OAAO,QAAQ,IAAI,OAAO,GAAG;EAC7B,OAAO,WAAW,IAAI,MAAM,MAAM;CACpC;AACF"}
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Id } from "./collection.js";
|
|
2
|
+
import { Hlc } from "./hlc.js";
|
|
3
|
+
import { LwwRegister } from "./lww.js";
|
|
4
|
+
|
|
5
|
+
//#region src/sync.d.ts
|
|
6
|
+
/** An HLC-stamped, idempotently-applied record mutation. */
|
|
7
|
+
type Op<T> = {
|
|
8
|
+
/** Unique, stable op id (`${actor}:${seq}`). Re-applying the same id is a no-op. */readonly id: string; /** The replica that produced the op. */
|
|
9
|
+
readonly actor: string; /** Monotonic per-actor sequence number. */
|
|
10
|
+
readonly seq: number; /** The collection the record belongs to. */
|
|
11
|
+
readonly collection: string; /** The record's id. */
|
|
12
|
+
readonly recordId: Id; /** The merge key (10B). */
|
|
13
|
+
readonly hlc: Hlc;
|
|
14
|
+
} & ({
|
|
15
|
+
readonly kind: 'set';
|
|
16
|
+
readonly value: T;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'del';
|
|
19
|
+
});
|
|
20
|
+
/** A convergent materialized view: per-record LWW state built by applying ops. */
|
|
21
|
+
interface MutationLog<T> {
|
|
22
|
+
/** Merge an op into the state (commutative + idempotent). Returns whether state changed. */
|
|
23
|
+
apply(op: Op<T>): boolean;
|
|
24
|
+
/** The live value of a record (or `undefined` if absent/deleted). */
|
|
25
|
+
get(recordId: Id): T | undefined;
|
|
26
|
+
/** All live records as `[recordId, value]` pairs. */
|
|
27
|
+
records(): Array<readonly [Id, T]>;
|
|
28
|
+
/** The full per-record register state (incl. tombstones) — for persistence. */
|
|
29
|
+
dump(): Array<readonly [Id, LwwRegister<T>]>;
|
|
30
|
+
}
|
|
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>;
|
|
33
|
+
/** An opaque, monotonic sync cursor (the hub's log position). */
|
|
34
|
+
type Cursor = number;
|
|
35
|
+
/** Minimal cancellation signal — a real `AbortSignal` is structurally compatible. */
|
|
36
|
+
interface AbortLike {
|
|
37
|
+
readonly aborted: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** The push/pull transport a {@link createSyncEngine sync engine} talks to. */
|
|
40
|
+
interface SyncTransport<T> {
|
|
41
|
+
/** Send local ops upstream; resolves with the ids the server accepted. */
|
|
42
|
+
push(ops: readonly Op<T>[]): Promise<{
|
|
43
|
+
readonly acked: readonly string[];
|
|
44
|
+
}>;
|
|
45
|
+
/** Fetch ops after `cursor` (null = from the beginning). */
|
|
46
|
+
pull(cursor: Cursor | null): Promise<{
|
|
47
|
+
readonly ops: readonly Op<T>[];
|
|
48
|
+
readonly cursor: Cursor;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
/** An in-memory reference transport: an append-only, op-id-deduped log. */
|
|
52
|
+
declare function createMemoryHub<T>(): SyncTransport<T>;
|
|
53
|
+
/** A serializable snapshot of a {@link SyncEngine}'s durable state (10F). */
|
|
54
|
+
interface SyncSnapshot<T> {
|
|
55
|
+
/** The next local op sequence number (persisted so ids don't collide across restarts). */
|
|
56
|
+
readonly seq: number;
|
|
57
|
+
/** The last pull cursor. */
|
|
58
|
+
readonly cursor: Cursor | null;
|
|
59
|
+
/** The materialized per-record register state. */
|
|
60
|
+
readonly registers: ReadonlyArray<readonly [Id, LwwRegister<T>]>;
|
|
61
|
+
/** Local ops applied but not yet acked (retried on next sync). */
|
|
62
|
+
readonly outbox: readonly Op<T>[];
|
|
63
|
+
/**
|
|
64
|
+
* The local HLC high-water mark at export time. Restoring it seeds the clock so the
|
|
65
|
+
* first post-restart edit is strictly newer than the replica's pre-restart writes —
|
|
66
|
+
* without it the clock regresses to 0 and a same-record edit can lose the LWW merge.
|
|
67
|
+
*/
|
|
68
|
+
readonly clock: Hlc;
|
|
69
|
+
}
|
|
70
|
+
/** Options for {@link createSyncEngine}. */
|
|
71
|
+
interface SyncEngineOptions<T> {
|
|
72
|
+
/**
|
|
73
|
+
* This replica's id (the op `actor`). **Must be globally unique**, and a durable
|
|
74
|
+
* replica must **persist its op sequence** across restarts — op ids are `${nodeId}:${seq}`
|
|
75
|
+
* and the transport de-dupes by id, so a reused `(nodeId, seq)` pair would silently
|
|
76
|
+
* drop the second op. (This in-memory engine resets `seq` on construction; persistence
|
|
77
|
+
* + content-addressed ids land with 10F.)
|
|
78
|
+
*/
|
|
79
|
+
readonly nodeId: string;
|
|
80
|
+
/** The transport to sync through. */
|
|
81
|
+
readonly transport: SyncTransport<T>;
|
|
82
|
+
/** Injected physical clock. Default `() => Date.now()`. */
|
|
83
|
+
readonly now?: () => number;
|
|
84
|
+
/** Restore from a previously {@link SyncEngine.export}ed snapshot (durable replica). */
|
|
85
|
+
readonly snapshot?: SyncSnapshot<T>;
|
|
86
|
+
}
|
|
87
|
+
/** A local-first sync engine over a {@link SyncTransport}. */
|
|
88
|
+
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>;
|
|
91
|
+
/** Optimistically delete a record locally and queue the op. */
|
|
92
|
+
delete(collection: string, recordId: Id): Op<T>;
|
|
93
|
+
/** Read a record's live value (local + already-synced state). */
|
|
94
|
+
get(recordId: Id): T | undefined;
|
|
95
|
+
/** All live records. */
|
|
96
|
+
records(): Array<readonly [Id, T]>;
|
|
97
|
+
/** Ops applied locally but not yet acked by the transport. */
|
|
98
|
+
pending(): readonly Op<T>[];
|
|
99
|
+
/** Push pending ops, then pull + apply remote ops. */
|
|
100
|
+
sync(signal?: AbortLike): Promise<void>;
|
|
101
|
+
/** A serializable snapshot to persist (restore via the `snapshot` option). */
|
|
102
|
+
export(): SyncSnapshot<T>;
|
|
103
|
+
}
|
|
104
|
+
/** Create a {@link SyncEngine}. */
|
|
105
|
+
declare function createSyncEngine<T>(options: SyncEngineOptions<T>): SyncEngine<T>;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { Cursor, MutationLog, Op, SyncEngine, SyncEngineOptions, SyncSnapshot, SyncTransport, createMemoryHub, createMutationLog, createSyncEngine };
|
|
108
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +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"}
|