@lunora/replica 1.0.0-alpha.7 → 1.0.0-alpha.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE.md +214 -0
  2. package/dist/adapters/better-sqlite3.mjs +1 -29
  3. package/dist/adapters/sqlite-wasm.d.mts +2 -2
  4. package/dist/adapters/sqlite-wasm.d.ts +2 -2
  5. package/dist/adapters/sqlite-wasm.mjs +1 -44
  6. package/dist/adapters/sqljs.mjs +1 -55
  7. package/dist/index.d.mts +43 -16
  8. package/dist/index.d.ts +43 -16
  9. package/dist/index.mjs +1 -20
  10. package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
  11. package/dist/packem_shared/EventLog-B1-yhArT.mjs +1 -0
  12. package/dist/packem_shared/EventLogDO-CaZvpgJN.mjs +1 -0
  13. package/dist/packem_shared/EventLogDOClient-DGDMj5L5.mjs +1 -0
  14. package/dist/packem_shared/EventSource-BC0hKJSA.mjs +1 -0
  15. package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +1 -0
  16. package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
  17. package/dist/packem_shared/LocalMirror-BIURA-He.mjs +4 -0
  18. package/dist/packem_shared/MaterializerRuntime-D0HOAe24.mjs +1 -0
  19. package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
  20. package/dist/packem_shared/applyDiff-Da160K7_.mjs +1 -0
  21. package/dist/packem_shared/applyDiffToDb-C6ek5Elp.mjs +1 -0
  22. package/dist/packem_shared/classifyChanges-ioMfjjbU.mjs +1 -0
  23. package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
  24. package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
  25. package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
  26. package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-ByrYTq4z.d.ts} +2 -2
  27. package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-Cip3QuMf.d.mts} +2 -2
  28. package/dist/packem_shared/subscribeToMirror-CX10AaP3.mjs +1 -0
  29. package/dist/react.d.mts +41 -14
  30. package/dist/react.d.ts +41 -14
  31. package/dist/react.mjs +1 -15
  32. package/package.json +1 -1
  33. package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
  34. package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
  35. package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
  36. package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
  37. package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
  38. package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
  39. package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
  40. package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
  41. package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
  42. package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
  43. package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
  44. package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
  45. package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
  46. package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
  47. package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
  48. package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
  49. package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
@@ -1,224 +0,0 @@
1
- const defineMaterializer = (definition) => {
2
- let state = definition.initial();
3
- return {
4
- def: definition,
5
- get state() {
6
- return Object.freeze(state);
7
- },
8
- setState(newState) {
9
- state = newState;
10
- },
11
- apply(entry) {
12
- state = definition.handle(state, entry);
13
- },
14
- reset() {
15
- state = definition.initial();
16
- }
17
- };
18
- };
19
- class MaterializerRuntime {
20
- #materializers;
21
- #snapshotStore;
22
- #doClient;
23
- #unknownEventHandling;
24
- /**
25
- * Per-materializer watermark: the seq of the next event each materializer
26
- * (by index, parallel to `#materializers`) has NOT yet applied. Starts at
27
- * `0` for every materializer and advances independently — a materializer
28
- * with no snapshot stays at `0` even when a sibling has recovered to a
29
- * much higher watermark, so catch-up never skips events for it (REPLICA-04).
30
- */
31
- #watermarks;
32
- constructor(materializers, options = {}) {
33
- this.#materializers = [...materializers];
34
- this.#watermarks = this.#materializers.map(() => 0);
35
- this.#snapshotStore = options.snapshotStore;
36
- this.#doClient = options.doClient;
37
- this.#unknownEventHandling = options.unknownEventHandling ?? "warn";
38
- }
39
- // ── Public API ──────────────────────────────────────────────────────
40
- /**
41
- * The lowest per-materializer watermark — the seq of the next event that
42
- * at least one materializer has not yet applied. `0` when there are no
43
- * materializers.
44
- */
45
- get appliedSeq() {
46
- return this.#watermarks.length > 0 ? Math.min(...this.#watermarks) : 0;
47
- }
48
- /**
49
- * Replay a batch of entries, applying each entry only to the
50
- * materializers whose own watermark is behind it — a materializer at or
51
- * past an entry's seq (e.g. recovered from a snapshot, or already caught
52
- * up) skips it, so no materializer ever double-applies an event.
53
- * @returns The number of entries applied to at least one materializer.
54
- */
55
- applyEntries(entries) {
56
- let count = 0;
57
- for (const entry of entries) {
58
- let appliedToAny = false;
59
- let anyChanged = false;
60
- for (const [i, materializer] of this.#materializers.entries()) {
61
- const watermark = this.#watermarks[i] ?? 0;
62
- if (entry.seq < watermark) {
63
- continue;
64
- }
65
- const stateBefore = materializer.state;
66
- materializer.apply(entry);
67
- appliedToAny = true;
68
- if (materializer.state !== stateBefore) {
69
- anyChanged = true;
70
- }
71
- this.#watermarks[i] = entry.seq + 1;
72
- }
73
- if (!appliedToAny) {
74
- continue;
75
- }
76
- if (!anyChanged) {
77
- this.#handleUnknownEvent(entry);
78
- }
79
- count += 1;
80
- }
81
- return count;
82
- }
83
- /**
84
- * Apply the configured {@link UnknownEventHandling} strategy for an event
85
- * that no materializer handled.
86
- */
87
- #handleUnknownEvent(entry) {
88
- const strategy = this.#unknownEventHandling;
89
- if (typeof strategy === "function") {
90
- strategy(entry);
91
- return;
92
- }
93
- switch (strategy) {
94
- case "ignore": {
95
- return;
96
- }
97
- case "fail": {
98
- throw new Error(
99
- `MaterializerRuntime: unhandled event type "${entry.type}" (seq ${String(entry.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`
100
- );
101
- }
102
- default: {
103
- console.warn(
104
- `[MaterializerRuntime] unhandled event type "${entry.type}" (seq ${String(entry.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`
105
- );
106
- }
107
- }
108
- }
109
- /**
110
- * Attempt to recover materialized state from a snapshot store.
111
- *
112
- * When a snapshot is found for a materializer, its state AND its own
113
- * watermark are restored from that snapshot. A materializer with no
114
- * snapshot keeps its current watermark (`0` for a fresh runtime) — it
115
- * does NOT inherit another materializer's watermark, so it still catches
116
- * up from the very beginning (REPLICA-04: previously a shared watermark
117
- * was bumped to the MAX across snapshots, permanently skipping events 0..N
118
- * for any un-snapshotted or lagging materializer).
119
- * @returns The highest snapshot `appliedSeq` across all materializers, or
120
- * `0` — kept for backward compatibility; callers that need the fetch
121
- * watermark for catch-up should use the per-materializer minimum instead
122
- * (see `initialize`).
123
- */
124
- async recoverFromSnapshots() {
125
- if (!this.#snapshotStore) {
126
- return 0;
127
- }
128
- let maxSeq = 0;
129
- for (const [i, materializer] of this.#materializers.entries()) {
130
- const raw = await this.#snapshotStore.load(materializer.def.name);
131
- if (raw !== null && typeof raw === "object") {
132
- const snapshot = raw;
133
- if (snapshot.state !== void 0) {
134
- materializer.setState(snapshot.state);
135
- }
136
- this.#watermarks[i] = snapshot.appliedSeq;
137
- if (snapshot.appliedSeq > maxSeq) {
138
- maxSeq = snapshot.appliedSeq;
139
- }
140
- }
141
- }
142
- return maxSeq;
143
- }
144
- /**
145
- * Persist the current state of all materializers as snapshots, each
146
- * tagged with ITS OWN watermark (not a shared one).
147
- */
148
- async persistSnapshots() {
149
- if (!this.#snapshotStore) {
150
- return;
151
- }
152
- for (const [i, materializer] of this.#materializers.entries()) {
153
- await this.#snapshotStore.save(materializer.def.name, {
154
- appliedSeq: this.#watermarks[i] ?? 0,
155
- state: materializer.state
156
- });
157
- }
158
- }
159
- // ── DO-backed lifecycle (when a doClient is provided) ──────────────
160
- /**
161
- * Bootstrap the runtime from the EventLogDO.
162
- *
163
- * 1. Recover materialized state from snapshots (if a snapshotStore is
164
- * configured).
165
- * 2. Fetch all entries since the MINIMUM per-materializer watermark from
166
- * the DO — not the maximum — so a materializer with no snapshot (or a
167
- * lower one) still receives every event it hasn't seen (REPLICA-04).
168
- * 3. Apply them through the materializers; `applyEntries` skips each
169
- * entry for any materializer already past it, so nothing is double-applied.
170
- *
171
- * Call this once on startup / after the DO binding is available.
172
- * @returns The number of entries applied during catch-up.
173
- */
174
- async initialize() {
175
- if (!this.#doClient) {
176
- return 0;
177
- }
178
- await this.recoverFromSnapshots();
179
- const minWatermark = this.#watermarks.length > 0 ? Math.min(...this.#watermarks) : 0;
180
- const entries = await this.#doClient.getSince(minWatermark);
181
- if (entries.length === 0) {
182
- return 0;
183
- }
184
- return this.applyEntries(entries);
185
- }
186
- /**
187
- * Append an event to the EventLogDO and apply it through all
188
- * materializers.
189
- *
190
- * This is a convenience over calling `doClient.append(...)` +
191
- * `runtime.applyEntries(...)` yourself — it persists the event
192
- * **then** applies the returned entry (with its assigned seq).
193
- * @returns The persisted entry with its DO-assigned `seq`.
194
- */
195
- async appendEvent(input) {
196
- if (!this.#doClient) {
197
- throw new Error("MaterializerRuntime.appendEvent requires a doClient — pass one in the constructor options.");
198
- }
199
- const persisted = await this.#doClient.append([input]);
200
- const entry = persisted[0];
201
- if (!entry) {
202
- throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");
203
- }
204
- this.applyEntries([entry]);
205
- return entry;
206
- }
207
- /**
208
- * Reset all materializers to their initial state and clear snapshots.
209
- */
210
- reset() {
211
- for (const [i, materializer] of this.#materializers.entries()) {
212
- this.#watermarks[i] = 0;
213
- materializer.reset();
214
- }
215
- }
216
- /**
217
- * The list of registered materializers.
218
- */
219
- get materializers() {
220
- return this.#materializers;
221
- }
222
- }
223
-
224
- export { MaterializerRuntime, defineMaterializer };
@@ -1,75 +0,0 @@
1
- class SubscriptionManager {
2
- #subscriptions = /* @__PURE__ */ new Map();
3
- #nextId = 0;
4
- // ── Registration ──────────────────────────────────────────────────
5
- /**
6
- * Subscribe to every state change emitted by the event source.
7
- * @returns Unsubscribe function.
8
- */
9
- onStateChange(callback) {
10
- const id = String(this.#nextId);
11
- this.#nextId += 1;
12
- const sub = { kind: "state", id, callback };
13
- this.#subscriptions.set(id, sub);
14
- return () => {
15
- this.#subscriptions.delete(id);
16
- };
17
- }
18
- /**
19
- * Subscribe to a specific event type.
20
- * @param eventType The event type to listen for (matches `entry.type`).
21
- * @param callback Invoked with each matching entry.
22
- * @returns Unsubscribe function.
23
- */
24
- onEvent(eventType, callback) {
25
- const id = String(this.#nextId);
26
- this.#nextId += 1;
27
- const sub = { kind: "event", id, eventType, callback };
28
- this.#subscriptions.set(id, sub);
29
- return () => {
30
- this.#subscriptions.delete(id);
31
- };
32
- }
33
- // ── Notification ──────────────────────────────────────────────────
34
- /**
35
- * Notify all state-change subscribers with the current state.
36
- */
37
- notifyState(state) {
38
- for (const sub of this.#subscriptions.values()) {
39
- if (sub.kind === "state") {
40
- try {
41
- sub.callback(state);
42
- } catch {
43
- }
44
- }
45
- }
46
- }
47
- /**
48
- * Notify event-type subscribers whose `eventType` matches.
49
- */
50
- notifyEvent(entry) {
51
- for (const sub of this.#subscriptions.values()) {
52
- if (sub.kind === "event" && sub.eventType === entry.type) {
53
- try {
54
- sub.callback(entry);
55
- } catch {
56
- }
57
- }
58
- }
59
- }
60
- // ── Introspection ─────────────────────────────────────────────────
61
- /**
62
- * Return the total number of active subscriptions.
63
- */
64
- get size() {
65
- return this.#subscriptions.size;
66
- }
67
- /**
68
- * Remove all subscriptions.
69
- */
70
- clear() {
71
- this.#subscriptions.clear();
72
- }
73
- }
74
-
75
- export { SubscriptionManager };
@@ -1,67 +0,0 @@
1
- const canonicalizeForHash = (value) => {
2
- if (Array.isArray(value)) {
3
- return value.map((item) => canonicalizeForHash(item));
4
- }
5
- if (value !== null && typeof value === "object") {
6
- const record = value;
7
- const sortedKeys = Object.keys(record).toSorted((a, b) => a.localeCompare(b));
8
- const result = {};
9
- for (const key of sortedKeys) {
10
- result[key] = canonicalizeForHash(record[key]);
11
- }
12
- return result;
13
- }
14
- return value;
15
- };
16
- const deriveInsertId = (diff, changeIndex, data) => {
17
- const diffIdentity = diff.id ?? String(diff.timestamp);
18
- const input = `${diff.table}::${diffIdentity}::${String(changeIndex)}::${JSON.stringify(canonicalizeForHash(data))}`;
19
- let hash = 0xcbf29ce484222325n;
20
- const prime = 0x00000100000001b3n;
21
- const mask64 = 0xffffffffffffffffn;
22
- for (let index = 0; index < input.length; index += 1) {
23
- hash ^= BigInt(input.codePointAt(index) ?? 0);
24
- hash = hash * prime & mask64;
25
- }
26
- return `row-${hash.toString(16).padStart(16, "0")}`;
27
- };
28
- const applyDiff = (current, diff) => {
29
- const next = new Map(current);
30
- for (const [changeIndex, change] of diff.changes.entries()) {
31
- switch (change.type) {
32
- case "delete": {
33
- next.delete(change.id);
34
- break;
35
- }
36
- case "insert": {
37
- const rawId = change.data.id;
38
- const id = typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : deriveInsertId(diff, changeIndex, change.data);
39
- next.set(id, { ...change.data, id });
40
- break;
41
- }
42
- case "update": {
43
- const existing = next.get(change.id);
44
- if (existing) {
45
- next.set(change.id, { ...existing, ...change.data });
46
- }
47
- break;
48
- }
49
- }
50
- }
51
- return next;
52
- };
53
- const applyDiffs = (current, diffs) => {
54
- let result = new Map(current);
55
- for (const diff of diffs) {
56
- result = applyDiff(result, diff);
57
- }
58
- return result;
59
- };
60
- const applyDiffToSnapshot = (snapshot, diff) => {
61
- const next = new Map(snapshot);
62
- const tableMap = next.get(diff.table) ?? /* @__PURE__ */ new Map();
63
- next.set(diff.table, applyDiff(tableMap, diff));
64
- return next;
65
- };
66
-
67
- export { applyDiff, applyDiffToSnapshot, applyDiffs };
@@ -1,58 +0,0 @@
1
- const escapeIdentifier = (id) => `\`${id.replaceAll("`", "``")}\``;
2
- const setClause = (keys) => keys.map((key) => `${escapeIdentifier(key)} = ?`).join(", ");
3
- const colList = (keys) => `(${keys.map((key) => escapeIdentifier(key)).join(", ")})`;
4
- const valueList = (count) => `(${Array.from({ length: count }).fill("?").join(", ")})`;
5
- const applySingleDiff = (database, diff, pkColumn) => {
6
- if (diff.changes.length === 0) {
7
- return;
8
- }
9
- const table = escapeIdentifier(diff.table);
10
- const pk = escapeIdentifier(pkColumn);
11
- for (const change of diff.changes) {
12
- switch (change.type) {
13
- case "delete": {
14
- database.exec(`DELETE FROM ${table} WHERE ${pk} = ?`, [change.id]);
15
- break;
16
- }
17
- case "insert": {
18
- const { data } = change;
19
- const keys = Object.keys(data);
20
- if (keys.length === 0) {
21
- continue;
22
- }
23
- const sql = `INSERT OR REPLACE INTO ${table} ${colList(keys)} VALUES ${valueList(keys.length)}`;
24
- const values = keys.map((k) => data[k]);
25
- database.exec(sql, values);
26
- break;
27
- }
28
- case "update": {
29
- const { data } = change;
30
- const keys = Object.keys(data);
31
- if (keys.length === 0) {
32
- continue;
33
- }
34
- const sql = `UPDATE ${table} SET ${setClause(keys)} WHERE ${pk} = ?`;
35
- const values = [...keys.map((k) => data[k]), change.id];
36
- database.exec(sql, values);
37
- break;
38
- }
39
- }
40
- }
41
- };
42
- const applyDiffToDatabase = (database, diff, pkColumn) => {
43
- database.transaction(() => {
44
- applySingleDiff(database, diff, pkColumn ?? "id");
45
- });
46
- };
47
- const applyDiffsToDatabase = (database, diffs) => {
48
- if (diffs.length === 0) {
49
- return;
50
- }
51
- database.transaction(() => {
52
- for (const diff of diffs) {
53
- applySingleDiff(database, diff, "id");
54
- }
55
- });
56
- };
57
-
58
- export { applyDiffToDatabase as applyDiffToDb, applyDiffsToDatabase as applyDiffsToDb, escapeIdentifier };
@@ -1,41 +0,0 @@
1
- const createTableDiff = (table, changes, timestamp, id) => {
2
- return {
3
- table,
4
- changes,
5
- timestamp: timestamp ?? Date.now(),
6
- id: id ?? crypto.randomUUID()
7
- };
8
- };
9
- const isDiffEmpty = (diff) => diff.changes.length === 0;
10
- const diffSize = (diff) => diff.changes.length;
11
- const classifyChanges = (diff) => {
12
- const inserts = [];
13
- const updates = [];
14
- const deletes = [];
15
- for (const change of diff.changes) {
16
- if (change.type === "insert") {
17
- inserts.push(change);
18
- } else if (change.type === "update") {
19
- updates.push(change);
20
- } else {
21
- deletes.push(change);
22
- }
23
- }
24
- return { inserts, updates, deletes };
25
- };
26
- const mergeDiffs = (diffs) => {
27
- if (diffs.length === 0) {
28
- return null;
29
- }
30
- const first = diffs[0];
31
- const last = diffs[diffs.length - 1];
32
- const mergedId = `merge:${diffs.map((d) => d.id ?? String(d.timestamp)).join("|")}`;
33
- return createTableDiff(
34
- first.table,
35
- diffs.flatMap((d) => d.changes),
36
- last.timestamp,
37
- mergedId
38
- );
39
- };
40
-
41
- export { classifyChanges, createTableDiff, diffSize, isDiffEmpty, mergeDiffs };
@@ -1,28 +0,0 @@
1
- const defineEvents = (definition, options) => {
2
- const result = {};
3
- const typeMap = {};
4
- const prefix = options?.version ? `${options.version}.` : "";
5
- for (const [namespace, events] of Object.entries(definition)) {
6
- const nsObject = {};
7
- for (const [name] of Object.entries(events)) {
8
- const qualifiedType = `${prefix}${namespace}.${name}`;
9
- typeMap[qualifiedType] = void 0;
10
- const factory = Object.assign(
11
- (payload) => {
12
- return {
13
- type: qualifiedType,
14
- payload,
15
- timestamp: Date.now()
16
- };
17
- },
18
- { type: qualifiedType }
19
- );
20
- nsObject[name] = factory;
21
- }
22
- result[namespace] = nsObject;
23
- }
24
- result._types = typeMap;
25
- return result;
26
- };
27
-
28
- export { defineEvents };
@@ -1,6 +0,0 @@
1
- const eventsContext = (client) => {
2
- const facade = client;
3
- return async ({ ctx: _context, next }) => next({ ctx: { events: facade } });
4
- };
5
-
6
- export { eventsContext };
@@ -1,5 +0,0 @@
1
- const isGlobalSeq = (seq) => typeof seq === "number";
2
- const isClientSeq = (seq) => typeof seq !== "number" && "rebaseGeneration" in seq;
3
- const isInputEvent = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string" && "payload" in value && "timestamp" in value;
4
-
5
- export { isClientSeq, isGlobalSeq, isInputEvent };
@@ -1,45 +0,0 @@
1
- const deriveTableName = (functionRef) => `fn_${functionRef.replaceAll(/[/:.]/g, "_")}`;
2
- const asRowArray = (data) => {
3
- if (Array.isArray(data)) {
4
- return data;
5
- }
6
- if (data !== null && typeof data === "object") {
7
- return [data];
8
- }
9
- return [];
10
- };
11
- const subscribeToMirror = (client, mirror, functionRef, args, shardKey) => {
12
- const tableName = deriveTableName(functionRef.__lunoraRef);
13
- mirror.registerTable(tableName, {});
14
- let knownIds = /* @__PURE__ */ new Set();
15
- return client.subscribe(
16
- functionRef,
17
- args,
18
- (data) => {
19
- const rows = asRowArray(data);
20
- const nextIds = /* @__PURE__ */ new Set();
21
- const changes = [];
22
- for (const row of rows) {
23
- const record = row;
24
- const rawId = record.id;
25
- if (typeof rawId === "string" || typeof rawId === "number") {
26
- nextIds.add(String(rawId));
27
- }
28
- changes.push({ type: "insert", data: record });
29
- }
30
- for (const id of knownIds) {
31
- if (!nextIds.has(id)) {
32
- changes.push({ type: "delete", id });
33
- }
34
- }
35
- knownIds = nextIds;
36
- if (changes.length === 0) {
37
- return;
38
- }
39
- mirror.applyDiff({ table: tableName, changes, timestamp: Date.now() });
40
- },
41
- { shardKey }
42
- );
43
- };
44
-
45
- export { subscribeToMirror };