@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.
- package/LICENSE.md +214 -0
- package/dist/adapters/better-sqlite3.mjs +1 -29
- package/dist/adapters/sqlite-wasm.d.mts +2 -2
- package/dist/adapters/sqlite-wasm.d.ts +2 -2
- package/dist/adapters/sqlite-wasm.mjs +1 -44
- package/dist/adapters/sqljs.mjs +1 -55
- package/dist/index.d.mts +43 -16
- package/dist/index.d.ts +43 -16
- package/dist/index.mjs +1 -20
- package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
- package/dist/packem_shared/EventLog-B1-yhArT.mjs +1 -0
- package/dist/packem_shared/EventLogDO-CaZvpgJN.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-DGDMj5L5.mjs +1 -0
- package/dist/packem_shared/EventSource-BC0hKJSA.mjs +1 -0
- package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +1 -0
- package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
- package/dist/packem_shared/LocalMirror-BIURA-He.mjs +4 -0
- package/dist/packem_shared/MaterializerRuntime-D0HOAe24.mjs +1 -0
- package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
- package/dist/packem_shared/applyDiff-Da160K7_.mjs +1 -0
- package/dist/packem_shared/applyDiffToDb-C6ek5Elp.mjs +1 -0
- package/dist/packem_shared/classifyChanges-ioMfjjbU.mjs +1 -0
- package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
- package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
- package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
- package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-ByrYTq4z.d.ts} +2 -2
- package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-Cip3QuMf.d.mts} +2 -2
- package/dist/packem_shared/subscribeToMirror-CX10AaP3.mjs +1 -0
- package/dist/react.d.mts +41 -14
- package/dist/react.d.ts +41 -14
- package/dist/react.mjs +1 -15
- package/package.json +1 -1
- package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
- package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
- package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
- package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
- package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
- package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
- package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
- package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
- package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
- package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
- package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
- package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
- package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
- package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
- package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
- package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
- 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,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 };
|