@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/dist/sync.js ADDED
@@ -0,0 +1,164 @@
1
+ import { createClock } from "./hlc.js";
2
+ import { mergeRegister } from "./lww.js";
3
+ //#region src/sync.ts
4
+ /** Create a {@link MutationLog}, optionally seeded from a {@link MutationLog.dump}. */
5
+ function createMutationLog(initial) {
6
+ const state = new Map(initial);
7
+ return {
8
+ 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"
16
+ };
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);
21
+ return true;
22
+ },
23
+ get(recordId) {
24
+ const reg = state.get(recordId);
25
+ return reg ? lwwReg(reg) : void 0;
26
+ },
27
+ records() {
28
+ const out = [];
29
+ for (const [recordId, reg] of state) if (reg.op === "set") out.push([recordId, reg.value]);
30
+ return out;
31
+ },
32
+ dump() {
33
+ return [...state];
34
+ }
35
+ };
36
+ }
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
+ /** An in-memory reference transport: an append-only, op-id-deduped log. */
42
+ function createMemoryHub() {
43
+ const log = [];
44
+ const seen = /* @__PURE__ */ new Set();
45
+ return {
46
+ push(ops) {
47
+ const acked = [];
48
+ for (const op of ops) {
49
+ if (!seen.has(op.id)) {
50
+ seen.add(op.id);
51
+ log.push(op);
52
+ }
53
+ acked.push(op.id);
54
+ }
55
+ return Promise.resolve({ acked });
56
+ },
57
+ pull(cursor) {
58
+ const from = cursor ?? 0;
59
+ return Promise.resolve({
60
+ ops: log.slice(from),
61
+ cursor: log.length
62
+ });
63
+ }
64
+ };
65
+ }
66
+ /** Create a {@link SyncEngine}. */
67
+ function createSyncEngine(options) {
68
+ const { nodeId, transport, snapshot } = options;
69
+ const clock = createClock({
70
+ nodeId,
71
+ ...options.now ? { now: options.now } : {},
72
+ ...snapshot?.clock ? { seed: {
73
+ wallMs: snapshot.clock.wallMs,
74
+ counter: snapshot.clock.counter
75
+ } } : {}
76
+ });
77
+ const log = createMutationLog(snapshot?.registers);
78
+ const outbox = snapshot ? [...snapshot.outbox] : [];
79
+ let seq = snapshot?.seq ?? 0;
80
+ let cursor = snapshot?.cursor ?? null;
81
+ let syncing = false;
82
+ const emit = (op) => {
83
+ log.apply(op);
84
+ outbox.push(op);
85
+ return op;
86
+ };
87
+ return {
88
+ set(collection, recordId, value) {
89
+ seq += 1;
90
+ return emit({
91
+ id: `${nodeId}:${seq}`,
92
+ actor: nodeId,
93
+ seq,
94
+ collection,
95
+ recordId,
96
+ hlc: clock.tick(),
97
+ kind: "set",
98
+ value
99
+ });
100
+ },
101
+ delete(collection, recordId) {
102
+ seq += 1;
103
+ return emit({
104
+ id: `${nodeId}:${seq}`,
105
+ actor: nodeId,
106
+ seq,
107
+ collection,
108
+ recordId,
109
+ hlc: clock.tick(),
110
+ kind: "del"
111
+ });
112
+ },
113
+ get(recordId) {
114
+ return log.get(recordId);
115
+ },
116
+ records() {
117
+ return log.records();
118
+ },
119
+ pending() {
120
+ return [...outbox];
121
+ },
122
+ async sync(signal) {
123
+ if (syncing) return;
124
+ syncing = true;
125
+ try {
126
+ await runSync(signal);
127
+ } finally {
128
+ syncing = false;
129
+ }
130
+ },
131
+ export() {
132
+ return {
133
+ seq,
134
+ cursor,
135
+ registers: log.dump(),
136
+ outbox: [...outbox],
137
+ clock: clock.peek()
138
+ };
139
+ }
140
+ };
141
+ async function runSync(signal) {
142
+ if (outbox.length > 0) {
143
+ const sending = [...outbox];
144
+ const { acked } = await transport.push(sending);
145
+ if (signal?.aborted) return;
146
+ const ackedSet = new Set(acked);
147
+ for (let i = outbox.length - 1; i >= 0; i--) {
148
+ const op = outbox[i];
149
+ if (op && ackedSet.has(op.id)) outbox.splice(i, 1);
150
+ }
151
+ }
152
+ const { ops, cursor: next } = await transport.pull(cursor);
153
+ if (signal?.aborted) return;
154
+ for (const op of ops) try {
155
+ clock.update(op.hlc);
156
+ log.apply(op);
157
+ } catch {}
158
+ cursor = next;
159
+ }
160
+ }
161
+ //#endregion
162
+ export { createMemoryHub, createMutationLog, createSyncEngine };
163
+
164
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +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":"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"}
@@ -0,0 +1,28 @@
1
+ //#region src/version-vector.d.ts
2
+ /**
3
+ * Version vectors — a compact "what has this replica seen" summary, mapping each
4
+ * replica's `nodeId` to the highest op sequence number observed from it. The sync layer
5
+ * (10D) diffs two vectors to compute exactly the ops a peer is missing. Pure +
6
+ * immutable. See `docs/adr/0013-continuum-hlc-causality.md`.
7
+ *
8
+ * @module
9
+ */
10
+ /** Maps each replica `nodeId` to the highest op sequence number seen from it. */
11
+ type VersionVector = Readonly<Record<string, number>>;
12
+ /**
13
+ * The highest sequence seen from `nodeId` (0 if never). Uses `Object.hasOwn` so an
14
+ * untrusted `nodeId` of `__proto__`/`constructor` reads as 0 rather than an inherited
15
+ * member.
16
+ */
17
+ declare function vvGet(vv: VersionVector, nodeId: string): number;
18
+ /** A new vector with `nodeId` raised to `max(current, seq)`. */
19
+ declare function vvObserve(vv: VersionVector, nodeId: string, seq: number): VersionVector;
20
+ /** A new vector that is the per-replica maximum of `a` and `b`. */
21
+ declare function vvMerge(a: VersionVector, b: VersionVector): VersionVector;
22
+ /** Whether `a` covers everything `b` has seen (`a[k] >= b[k]` for every `k` in `b`). */
23
+ declare function vvDominates(a: VersionVector, b: VersionVector): boolean;
24
+ /** Whether two vectors are equal (same non-zero entries). */
25
+ declare function vvEquals(a: VersionVector, b: VersionVector): boolean;
26
+ //#endregion
27
+ export { VersionVector, vvDominates, vvEquals, vvGet, vvMerge, vvObserve };
28
+ //# sourceMappingURL=version-vector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-vector.d.ts","names":[],"sources":["../src/version-vector.ts"],"mappings":";;AAUA;;;;AAA2C;AAO3C;;;KAPY,aAAA,GAAgB,QAAQ,CAAC,MAAA;;;;;AAOkB;iBAAvC,KAAA,CAAM,EAAA,EAAI,aAAa,EAAE,MAAA;;iBAKzB,SAAA,CAAU,EAAA,EAAI,aAAA,EAAe,MAAA,UAAgB,GAAA,WAAc,aAAa;;iBAQxE,OAAA,CAAQ,CAAA,EAAG,aAAA,EAAe,CAAA,EAAG,aAAA,GAAgB,aAAA;;iBAc7C,WAAA,CAAY,CAAA,EAAG,aAAA,EAAe,CAAA,EAAG,aAAa;;iBAQ9C,QAAA,CAAS,CAAA,EAAG,aAAA,EAAe,CAAA,EAAG,aAAa"}
@@ -0,0 +1,40 @@
1
+ //#region src/version-vector.ts
2
+ /**
3
+ * The highest sequence seen from `nodeId` (0 if never). Uses `Object.hasOwn` so an
4
+ * untrusted `nodeId` of `__proto__`/`constructor` reads as 0 rather than an inherited
5
+ * member.
6
+ */
7
+ function vvGet(vv, nodeId) {
8
+ return Object.hasOwn(vv, nodeId) ? vv[nodeId] ?? 0 : 0;
9
+ }
10
+ /** A new vector with `nodeId` raised to `max(current, seq)`. */
11
+ function vvObserve(vv, nodeId, seq) {
12
+ if (seq <= vvGet(vv, nodeId)) return vv;
13
+ return {
14
+ ...vv,
15
+ [nodeId]: seq
16
+ };
17
+ }
18
+ /** A new vector that is the per-replica maximum of `a` and `b`. */
19
+ function vvMerge(a, b) {
20
+ const out = Object.create(null);
21
+ for (const nodeId of Object.keys(a)) out[nodeId] = vvGet(a, nodeId);
22
+ for (const nodeId of Object.keys(b)) {
23
+ const bv = vvGet(b, nodeId);
24
+ if (bv > (out[nodeId] ?? 0)) out[nodeId] = bv;
25
+ }
26
+ return out;
27
+ }
28
+ /** Whether `a` covers everything `b` has seen (`a[k] >= b[k]` for every `k` in `b`). */
29
+ function vvDominates(a, b) {
30
+ for (const nodeId of Object.keys(b)) if (vvGet(a, nodeId) < (b[nodeId] ?? 0)) return false;
31
+ return true;
32
+ }
33
+ /** Whether two vectors are equal (same non-zero entries). */
34
+ function vvEquals(a, b) {
35
+ return vvDominates(a, b) && vvDominates(b, a);
36
+ }
37
+ //#endregion
38
+ export { vvDominates, vvEquals, vvGet, vvMerge, vvObserve };
39
+
40
+ //# sourceMappingURL=version-vector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-vector.js","names":[],"sources":["../src/version-vector.ts"],"sourcesContent":["/**\n * Version vectors — a compact \"what has this replica seen\" summary, mapping each\n * replica's `nodeId` to the highest op sequence number observed from it. The sync layer\n * (10D) diffs two vectors to compute exactly the ops a peer is missing. Pure +\n * immutable. See `docs/adr/0013-continuum-hlc-causality.md`.\n *\n * @module\n */\n\n/** Maps each replica `nodeId` to the highest op sequence number seen from it. */\nexport type VersionVector = Readonly<Record<string, number>>\n\n/**\n * The highest sequence seen from `nodeId` (0 if never). Uses `Object.hasOwn` so an\n * untrusted `nodeId` of `__proto__`/`constructor` reads as 0 rather than an inherited\n * member.\n */\nexport function vvGet(vv: VersionVector, nodeId: string): number {\n return Object.hasOwn(vv, nodeId) ? (vv[nodeId] ?? 0) : 0\n}\n\n/** A new vector with `nodeId` raised to `max(current, seq)`. */\nexport function vvObserve(vv: VersionVector, nodeId: string, seq: number): VersionVector {\n if (seq <= vvGet(vv, nodeId)) return vv\n // Object literal computed key uses CreateDataProperty (not [[Set]]), so even a\n // `__proto__` nodeId becomes an own data property rather than mutating a prototype.\n return { ...vv, [nodeId]: seq }\n}\n\n/** A new vector that is the per-replica maximum of `a` and `b`. */\nexport function vvMerge(a: VersionVector, b: VersionVector): VersionVector {\n // null-prototype accumulator: `out[nodeId] = …` then creates an own property even for\n // a `__proto__` nodeId, instead of hitting Object.prototype's accessor (which would\n // silently drop the entry).\n const out: Record<string, number> = Object.create(null)\n for (const nodeId of Object.keys(a)) out[nodeId] = vvGet(a, nodeId)\n for (const nodeId of Object.keys(b)) {\n const bv = vvGet(b, nodeId)\n if (bv > (out[nodeId] ?? 0)) out[nodeId] = bv\n }\n return out\n}\n\n/** Whether `a` covers everything `b` has seen (`a[k] >= b[k]` for every `k` in `b`). */\nexport function vvDominates(a: VersionVector, b: VersionVector): boolean {\n for (const nodeId of Object.keys(b)) {\n if (vvGet(a, nodeId) < (b[nodeId] ?? 0)) return false\n }\n return true\n}\n\n/** Whether two vectors are equal (same non-zero entries). */\nexport function vvEquals(a: VersionVector, b: VersionVector): boolean {\n return vvDominates(a, b) && vvDominates(b, a)\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,MAAM,IAAmB,QAAwB;CAC/D,OAAO,OAAO,OAAO,IAAI,MAAM,IAAK,GAAG,WAAW,IAAK;AACzD;;AAGA,SAAgB,UAAU,IAAmB,QAAgB,KAA4B;CACvF,IAAI,OAAO,MAAM,IAAI,MAAM,GAAG,OAAO;CAGrC,OAAO;EAAE,GAAG;GAAK,SAAS;CAAI;AAChC;;AAGA,SAAgB,QAAQ,GAAkB,GAAiC;CAIzE,MAAM,MAA8B,OAAO,OAAO,IAAI;CACtD,KAAK,MAAM,UAAU,OAAO,KAAK,CAAC,GAAG,IAAI,UAAU,MAAM,GAAG,MAAM;CAClE,KAAK,MAAM,UAAU,OAAO,KAAK,CAAC,GAAG;EACnC,MAAM,KAAK,MAAM,GAAG,MAAM;EAC1B,IAAI,MAAM,IAAI,WAAW,IAAI,IAAI,UAAU;CAC7C;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,GAAkB,GAA2B;CACvE,KAAK,MAAM,UAAU,OAAO,KAAK,CAAC,GAChC,IAAI,MAAM,GAAG,MAAM,KAAK,EAAE,WAAW,IAAI,OAAO;CAElD,OAAO;AACT;;AAGA,SAAgB,SAAS,GAAkB,GAA2B;CACpE,OAAO,YAAY,GAAG,CAAC,KAAK,YAAY,GAAG,CAAC;AAC9C"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@mindees/data",
3
+ "version": "0.1.0",
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
+ "license": "MIT OR Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./server": {
17
+ "types": "./dist/server.d.ts",
18
+ "import": "./dist/server.js"
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/mindees/mindees.git",
27
+ "directory": "packages/data"
28
+ },
29
+ "dependencies": {
30
+ "@mindees/core": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "fast-check": "4.8.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "typecheck": "tsc --noEmit"
38
+ }
39
+ }