@lunora/replica 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (58) hide show
  1. package/LICENSE.md +214 -0
  2. package/README.md +2 -0
  3. package/dist/adapters/better-sqlite3.d.mts +14 -13
  4. package/dist/adapters/better-sqlite3.d.ts +14 -13
  5. package/dist/adapters/better-sqlite3.mjs +1 -29
  6. package/dist/adapters/sqlite-wasm.d.mts +29 -18
  7. package/dist/adapters/sqlite-wasm.d.ts +29 -18
  8. package/dist/adapters/sqlite-wasm.mjs +1 -56
  9. package/dist/adapters/sqljs.d.mts +10 -9
  10. package/dist/adapters/sqljs.d.ts +10 -9
  11. package/dist/adapters/sqljs.mjs +1 -55
  12. package/dist/index.d.mts +646 -531
  13. package/dist/index.d.ts +646 -531
  14. package/dist/index.mjs +1 -20
  15. package/dist/packem_shared/EventEmitter-ovTsLeAj.mjs +1 -0
  16. package/dist/packem_shared/EventLog-DmlRY_4Z.mjs +1 -0
  17. package/dist/packem_shared/EventLogDO-Cb-7iN9w.mjs +1 -0
  18. package/dist/packem_shared/EventLogDOClient-C6gA3b3u.mjs +1 -0
  19. package/dist/packem_shared/EventSource-B5UTlkl9.mjs +1 -0
  20. package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +1 -0
  21. package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
  22. package/dist/packem_shared/LocalMirror-Q7cPH_rX.mjs +4 -0
  23. package/dist/packem_shared/MaterializerRuntime-CqGGjSxl.mjs +1 -0
  24. package/dist/packem_shared/SubscriptionManager-CbSjG_GA.mjs +1 -0
  25. package/dist/packem_shared/applyDiff-BUzddc6r.mjs +1 -0
  26. package/dist/packem_shared/applyDiffToDb-1M2I3BHi.mjs +1 -0
  27. package/dist/packem_shared/classifyChanges-IoOGDVVx.mjs +1 -0
  28. package/dist/packem_shared/defineEvents-CJZV8Bgi.mjs +1 -0
  29. package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
  30. package/dist/packem_shared/isClientSeq-DSXBJskD.mjs +1 -0
  31. package/dist/packem_shared/local-mirror.d-CyGOpUES.d.ts +530 -0
  32. package/dist/packem_shared/local-mirror.d-DTavX_y0.d.mts +530 -0
  33. package/dist/packem_shared/subscribeToMirror-Dru_AYBu.mjs +1 -0
  34. package/dist/packem_shared/{types.d-VfJ76cK4.d.mts → types.d-CkMkSwLJ.d.mts} +10 -9
  35. package/dist/packem_shared/{types.d-VfJ76cK4.d.ts → types.d-CkMkSwLJ.d.ts} +10 -9
  36. package/dist/react.d.mts +59 -57
  37. package/dist/react.d.ts +59 -57
  38. package/dist/react.mjs +1 -15
  39. package/package.json +2 -2
  40. package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
  41. package/dist/packem_shared/EventLog-zMy7AYP4.mjs +0 -162
  42. package/dist/packem_shared/EventLogDO-CZYUvvSr.mjs +0 -235
  43. package/dist/packem_shared/EventLogDOClient-DGiEdi96.mjs +0 -86
  44. package/dist/packem_shared/EventSource-DfV4VoRD.mjs +0 -195
  45. package/dist/packem_shared/EventsSync-DkVbU0WV.mjs +0 -91
  46. package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
  47. package/dist/packem_shared/LocalMirror-GeJ26eNe.mjs +0 -188
  48. package/dist/packem_shared/MaterializerRuntime-HqNXqJxp.mjs +0 -204
  49. package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
  50. package/dist/packem_shared/applyDiff-BtbIl1D3.mjs +0 -40
  51. package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
  52. package/dist/packem_shared/classifyChanges-aZmkxgVI.mjs +0 -38
  53. package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
  54. package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
  55. package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
  56. package/dist/packem_shared/local-mirror.d-Bp19ueGy.d.mts +0 -412
  57. package/dist/packem_shared/local-mirror.d-GhuZAKgm.d.ts +0 -412
  58. package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
@@ -1,235 +0,0 @@
1
- const toArray = (cursor) => {
2
- if (typeof cursor.toArray === "function") {
3
- return cursor.toArray();
4
- }
5
- if (typeof cursor[Symbol.iterator] === "function") {
6
- return [...cursor];
7
- }
8
- return [];
9
- };
10
- const rowsToEntries = (cursor) => {
11
- const rows = toArray(cursor);
12
- return rows.map((row) => {
13
- return {
14
- seq: row.seq,
15
- type: row.type,
16
- payload: JSON.parse(row.payload),
17
- timestamp: row.timestamp,
18
- clientId: row.client_id ?? void 0,
19
- sessionId: row.session_id ?? void 0,
20
- parentSeqNum: row.parent_seq ?? void 0
21
- };
22
- });
23
- };
24
- class EventLogDO {
25
- state;
26
- env;
27
- /** Whether the `events` table has been created. */
28
- #initialized = false;
29
- constructor(state, env) {
30
- this.state = state;
31
- this.env = env;
32
- }
33
- // ── Fetch (RPC dispatch) ──────────────────────────────────────────
34
- async fetch(request) {
35
- this.#ensureTable();
36
- const url = new URL(request.url);
37
- try {
38
- if (request.method === "POST" && url.pathname === "/append") {
39
- return await this.#handleAppend(request);
40
- }
41
- if (request.method === "GET" && url.pathname === "/since") {
42
- return this.#handleSince(url);
43
- }
44
- if (request.method === "GET" && url.pathname === "/range") {
45
- return this.#handleRange(url);
46
- }
47
- if (request.method === "GET" && url.pathname === "/size") {
48
- return this.#handleSize();
49
- }
50
- if (request.method === "GET" && url.pathname === "/state") {
51
- return this.#handleState();
52
- }
53
- } catch (error) {
54
- console.error("[event-log-do] request failed:", error);
55
- return Response.json(
56
- {
57
- error: {
58
- code: "INTERNAL_ERROR",
59
- message: "internal error"
60
- }
61
- },
62
- { status: 500, headers: { "content-type": "application/json" } }
63
- );
64
- }
65
- return Response.json(
66
- { error: { code: "NOT_FOUND", message: "unknown route" } },
67
- {
68
- status: 404,
69
- headers: { "content-type": "application/json" }
70
- }
71
- );
72
- }
73
- // ── Handlers ──────────────────────────────────────────────────────
74
- /** POST /append — insert events, return entries with assigned seqs. */
75
- async #handleAppend(request) {
76
- let body;
77
- try {
78
- body = await request.json();
79
- } catch {
80
- return Response.json(
81
- { error: { code: "BAD_REQUEST", message: "invalid JSON body" } },
82
- {
83
- status: 400,
84
- headers: { "content-type": "application/json" }
85
- }
86
- );
87
- }
88
- if (!Array.isArray(body.events) || body.events.length === 0 || body.events.some((eventRecord) => typeof eventRecord.type !== "string" || eventRecord.type.length === 0)) {
89
- return Response.json(
90
- { error: { code: "BAD_REQUEST", message: "events[] with a non-empty string `type` required" } },
91
- {
92
- status: 400,
93
- headers: { "content-type": "application/json" }
94
- }
95
- );
96
- }
97
- const entries = [];
98
- const { sql } = this.state.storage;
99
- const now = Date.now();
100
- for (const eventRecord of body.events) {
101
- const seq = EventLogDO.#nextSeq(sql);
102
- const entry = {
103
- seq,
104
- type: eventRecord.type,
105
- payload: eventRecord.payload,
106
- timestamp: eventRecord.timestamp ?? now
107
- };
108
- EventLogDO.#insertEvent(sql, entry);
109
- entries.push(entry);
110
- }
111
- const response = { entries };
112
- return Response.json(response, {
113
- status: 200,
114
- headers: { "content-type": "application/json" }
115
- });
116
- }
117
- /** GET /since?seq=N — return entries with seq >= N. */
118
- #handleSince(url) {
119
- const seqParameter = url.searchParams.get("seq");
120
- const sinceSeq = seqParameter === null ? 0 : Number(seqParameter);
121
- if (!Number.isFinite(sinceSeq) || sinceSeq < 0) {
122
- return Response.json(
123
- { error: { code: "BAD_REQUEST", message: "invalid seq" } },
124
- {
125
- status: 400,
126
- headers: { "content-type": "application/json" }
127
- }
128
- );
129
- }
130
- const { sql } = this.state.storage;
131
- const cursor = sql.exec(
132
- "SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC",
133
- sinceSeq
134
- );
135
- const entries = rowsToEntries(cursor);
136
- return Response.json(
137
- { entries },
138
- {
139
- status: 200,
140
- headers: { "content-type": "application/json" }
141
- }
142
- );
143
- }
144
- /** GET /range?from=N&amp;limit=M — paginated read (default limit 50). */
145
- #handleRange(url) {
146
- const fromParameter = url.searchParams.get("from");
147
- const fromSeq = fromParameter === null ? 0 : Number(fromParameter);
148
- const limitParameter = url.searchParams.get("limit");
149
- const limit = limitParameter === null ? 50 : Number(limitParameter);
150
- if (!Number.isFinite(fromSeq) || fromSeq < 0 || !Number.isFinite(limit) || limit < 1 || limit > 1e3) {
151
- return Response.json(
152
- { error: { code: "BAD_REQUEST", message: "invalid from/limit" } },
153
- {
154
- status: 400,
155
- headers: { "content-type": "application/json" }
156
- }
157
- );
158
- }
159
- const { sql } = this.state.storage;
160
- const cursor = sql.exec(
161
- "SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC LIMIT ?",
162
- fromSeq,
163
- limit + 1
164
- // Fetch one extra to detect hasMore
165
- );
166
- const allRows = rowsToEntries(cursor);
167
- const hasMore = allRows.length > limit;
168
- const entries = hasMore ? allRows.slice(0, limit) : allRows;
169
- const response = { entries, hasMore };
170
- return Response.json(response, {
171
- status: 200,
172
- headers: { "content-type": "application/json" }
173
- });
174
- }
175
- /** GET /size — return the number of stored events. */
176
- #handleSize() {
177
- const { sql } = this.state.storage;
178
- const cursor = sql.exec("SELECT COUNT(*) AS count FROM events");
179
- const rows = toArray(cursor);
180
- const count = rows[0]?.count ?? 0;
181
- return Response.json(
182
- { count },
183
- {
184
- status: 200,
185
- headers: { "content-type": "application/json" }
186
- }
187
- );
188
- }
189
- /** GET /state — return the full event log state. */
190
- #handleState() {
191
- const { sql } = this.state.storage;
192
- const cursor = sql.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events ORDER BY seq ASC");
193
- const entries = rowsToEntries(cursor);
194
- const nextSeq = (entries.at(-1)?.seq ?? -1) + 1;
195
- return Response.json({ entries, nextSeq }, { status: 200, headers: { "content-type": "application/json" } });
196
- }
197
- // ── Internal ──────────────────────────────────────────────────────
198
- #ensureTable() {
199
- if (this.#initialized) {
200
- return;
201
- }
202
- const { sql } = this.state.storage;
203
- sql.exec(
204
- "CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, payload TEXT NOT NULL, timestamp INTEGER NOT NULL, client_id TEXT, session_id TEXT, parent_seq INTEGER)"
205
- );
206
- this.#initialized = true;
207
- }
208
- /** Get the next available sequence number. */
209
- static #nextSeq(sql) {
210
- const cursor = sql.exec("SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM events");
211
- const rows = toArray(cursor);
212
- return rows[0]?.next_seq ?? 0;
213
- }
214
- /** Insert a single event entry. */
215
- /* eslint-disable unicorn/no-null -- SQLite bindings use `null` for missing optional columns */
216
- static #insertEvent(sql, entry) {
217
- let parentSeq = null;
218
- if (typeof entry.parentSeqNum === "number") {
219
- parentSeq = entry.parentSeqNum;
220
- }
221
- sql.exec(
222
- "INSERT INTO events (seq, type, payload, timestamp, client_id, session_id, parent_seq) VALUES (?, ?, ?, ?, ?, ?, ?)",
223
- entry.seq,
224
- entry.type,
225
- JSON.stringify(entry.payload),
226
- entry.timestamp,
227
- entry.clientId ?? null,
228
- entry.sessionId ?? null,
229
- parentSeq
230
- );
231
- }
232
- /* eslint-enable unicorn/no-null */
233
- }
234
-
235
- export { EventLogDO };
@@ -1,86 +0,0 @@
1
- class EventLogDOClient {
2
- #fetch;
3
- constructor(options) {
4
- this.#fetch = options.fetch;
5
- }
6
- // ── Append ─────────────────────────────────────────────────────────
7
- /**
8
- * Append one or more events to the log.
9
- * @returns The persisted entries with their assigned `seq` numbers.
10
- */
11
- async append(events) {
12
- const body = JSON.stringify({ events });
13
- const response = await this.#fetch(
14
- new Request("https://do/append", {
15
- method: "POST",
16
- headers: { "content-type": "application/json" },
17
- body
18
- })
19
- );
20
- if (!response.ok) {
21
- throw await EventLogDOClient.#toError(response, "append");
22
- }
23
- const data = await response.json();
24
- return data.entries;
25
- }
26
- // ── Read / replay ───────────────────────────────────────────────────
27
- /**
28
- * Fetch all entries with `seq >= sinceSeq`.
29
- *
30
- * Pass `sinceSeq = 0` to fetch the entire log.
31
- */
32
- async getSince(sinceSeq) {
33
- const response = await this.#fetch(new Request(`https://do/since?seq=${String(sinceSeq)}`));
34
- if (!response.ok) {
35
- throw await EventLogDOClient.#toError(response, "getSince");
36
- }
37
- const data = await response.json();
38
- return data.entries;
39
- }
40
- /**
41
- * Fetch a paginated range of entries.
42
- * @returns `{ entries, hasMore }` — `hasMore` is `true` when another
43
- * page exists (i.e. the DO returned `limit + 1` rows).
44
- */
45
- async getRange(fromSeq, limit = 50) {
46
- const response = await this.#fetch(new Request(`https://do/range?from=${String(fromSeq)}&limit=${String(limit)}`));
47
- if (!response.ok) {
48
- throw await EventLogDOClient.#toError(response, "getRange");
49
- }
50
- return await response.json();
51
- }
52
- /**
53
- * Return the total number of entries currently in the log.
54
- */
55
- async getSize() {
56
- const response = await this.#fetch(new Request("https://do/size"));
57
- if (!response.ok) {
58
- throw await EventLogDOClient.#toError(response, "getSize");
59
- }
60
- const data = await response.json();
61
- return data.count;
62
- }
63
- /**
64
- * Return the full log state — all entries plus the next seq number.
65
- */
66
- async getState() {
67
- const response = await this.#fetch(new Request("https://do/state"));
68
- if (!response.ok) {
69
- throw await EventLogDOClient.#toError(response, "getState");
70
- }
71
- return await response.json();
72
- }
73
- // ── Internal ────────────────────────────────────────────────────────
74
- /** Parse an error response body and return a descriptive `Error`. */
75
- static async #toError(response, method) {
76
- try {
77
- const body = await response.json();
78
- const message = body.error?.message ?? response.statusText;
79
- return new Error(`EventLogDO.${method} failed (${String(response.status)}): ${message}`);
80
- } catch {
81
- return new Error(`EventLogDO.${method} failed (${String(response.status)}): ${response.statusText}`);
82
- }
83
- }
84
- }
85
-
86
- export { EventLogDOClient };
@@ -1,195 +0,0 @@
1
- import { EventEmitter } from './EventEmitter-CMZfct03.mjs';
2
- import { EventLog } from './EventLog-zMy7AYP4.mjs';
3
-
4
- class EventSource {
5
- // eslint-disable-next-line unicorn/prefer-event-target -- EventEmitter is the library's typed public API
6
- emitter = new EventEmitter();
7
- log = new EventLog();
8
- #state;
9
- #reducer;
10
- #replayed = false;
11
- #unknownEventHandling;
12
- /**
13
- * Watermark over the EXTERNAL source log: the highest source `seq` already
14
- * applied by {@link EventSource.replayFromLog}, or `-1` when nothing has been applied.
15
- * Tracked separately from `this.log.nextSeq` (the destination log, which
16
- * `applyEvent` and each replay append advance independently) so a repeated
17
- * `replayFromLog` neither skips unseen source entries nor reprocesses
18
- * already-applied ones.
19
- */
20
- #lastAppliedSeq = -1;
21
- constructor(initialState, reducer, options) {
22
- this.#state = { ...initialState };
23
- this.#reducer = reducer;
24
- this.#unknownEventHandling = options?.unknownEventHandling ?? "warn";
25
- }
26
- // ── Public API ────────────────────────────────────────────────────
27
- /**
28
- * The current derived state. Read-only snapshot; mutate through events.
29
- */
30
- get state() {
31
- return this.#state;
32
- }
33
- /**
34
- * Whether the initial replay from an existing log has completed.
35
- */
36
- get replayed() {
37
- return this.#replayed;
38
- }
39
- applyEvent(typeOrEvent, payload, options) {
40
- let type;
41
- let pl;
42
- let resolvedOptions;
43
- if (typeof typeOrEvent === "string") {
44
- type = typeOrEvent;
45
- pl = payload;
46
- resolvedOptions = options;
47
- } else {
48
- type = typeOrEvent.type;
49
- pl = typeOrEvent.payload;
50
- resolvedOptions = payload;
51
- }
52
- const entry = this.log.append(type, pl, void 0, resolvedOptions);
53
- this.#applyEntry(entry);
54
- return entry;
55
- }
56
- /**
57
- * Replay all entries from an existing {@link EventLog} to bootstrap
58
- * the current state.
59
- *
60
- * Idempotent across calls: only source entries past the `#lastAppliedSeq`
61
- * watermark are applied, so re-invoking picks up just the new entries.
62
- * @param log The external log to replay from.
63
- */
64
- replayFromLog(log) {
65
- const entries = log.getSince(this.#lastAppliedSeq + 1);
66
- for (const entry of entries) {
67
- try {
68
- this.#state = this.#reducer(this.#state, entry);
69
- this.log.append(entry.type, entry.payload, entry.tableDiffs, {
70
- clientId: entry.clientId,
71
- sessionId: entry.sessionId,
72
- parentSeqNum: entry.parentSeqNum
73
- });
74
- } catch (error) {
75
- this.emitter.emit("replay-error", {
76
- entry,
77
- error: error instanceof Error ? error : new Error(String(error))
78
- });
79
- } finally {
80
- this.#lastAppliedSeq = entry.seq;
81
- }
82
- }
83
- this.#replayed = true;
84
- this.emitter.emit("ready", { entryCount: this.log.size });
85
- }
86
- /**
87
- * Reset the runtime to a base state, optionally resuming from a watermark.
88
- *
89
- * Useful after loading a snapshot from the DO: pass the snapshot's state as
90
- * `initialState` and its highest applied source `seq` as `resumeFromSeq`, so
91
- * the next {@link replayFromLog} applies ONLY the events after the snapshot
92
- * (`getSince(resumeFromSeq + 1)`) rather than replaying the whole log on top
93
- * of the snapshot — which would double-apply non-idempotent reducers.
94
- *
95
- * Omit `resumeFromSeq` (default `-1`) for a full reset that replays from the
96
- * beginning.
97
- * @param initialState The base state to reset to (e.g. a loaded snapshot).
98
- * @param resumeFromSeq Highest source `seq` already baked into `initialState`, or `-1` to replay all.
99
- */
100
- reset(initialState, resumeFromSeq = -1) {
101
- this.#state = { ...initialState };
102
- this.#lastAppliedSeq = resumeFromSeq;
103
- this.#replayed = resumeFromSeq >= 0;
104
- }
105
- /**
106
- * Return an async generator that yields every event as it is applied,
107
- * starting from the events currently in the log and continuing with
108
- * every future `applyEvent` / `replayFromLog` call.
109
- *
110
- * The generator runs indefinitely — it never returns. Callers should
111
- * break out of the `for await` loop or use an `AbortSignal` to stop.
112
- * @example
113
- * ```ts
114
- * for await (const entry of source.events()) {
115
- * console.log("event applied:", entry);
116
- * }
117
- * ```
118
- */
119
- async *events(signal) {
120
- const buffer = [];
121
- let wake;
122
- const unsub = this.emitter.on("state-changed", ({ entry }) => {
123
- buffer.push(entry);
124
- wake?.();
125
- });
126
- try {
127
- let watermark = 0;
128
- while (watermark < this.log.size) {
129
- if (signal?.aborted) {
130
- return;
131
- }
132
- for (const entry of this.log.getSince(watermark)) {
133
- yield entry;
134
- }
135
- watermark = this.log.nextSeq;
136
- }
137
- while (!signal?.aborted) {
138
- while (buffer.length > 0) {
139
- const nextEntry = buffer.shift();
140
- if (nextEntry) {
141
- yield nextEntry;
142
- }
143
- }
144
- if (buffer.length === 0) {
145
- await new Promise((resolve) => {
146
- wake = resolve;
147
- });
148
- }
149
- }
150
- } finally {
151
- unsub();
152
- }
153
- }
154
- // ── Internal ──────────────────────────────────────────────────────
155
- #applyEntry(entry) {
156
- const stateBefore = this.#state;
157
- try {
158
- this.#state = this.#reducer(this.#state, entry);
159
- } catch (error) {
160
- this.emitter.emit("replay-error", {
161
- entry,
162
- error: error instanceof Error ? error : new Error(String(error))
163
- });
164
- return;
165
- }
166
- if (this.#state === stateBefore && !this.#handleUnknown(entry)) {
167
- return;
168
- }
169
- this.emitter.emit("state-changed", { state: this.#state, entry });
170
- }
171
- #handleUnknown(entry) {
172
- const strategy = this.#unknownEventHandling;
173
- if (typeof strategy === "function") {
174
- return strategy(entry);
175
- }
176
- switch (strategy) {
177
- case "ignore": {
178
- return false;
179
- }
180
- case "fail": {
181
- throw new Error(
182
- `EventSource: unhandled event type "${entry.type}" (seq ${String(entry.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`
183
- );
184
- }
185
- default: {
186
- console.warn(
187
- `[EventSource] unhandled event type "${entry.type}" (seq ${String(entry.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`
188
- );
189
- return false;
190
- }
191
- }
192
- }
193
- }
194
-
195
- export { EventSource };
@@ -1,91 +0,0 @@
1
- class EventsSync {
2
- #options;
3
- /** The highest `seq + 1` that has been applied. Starts at `0`. */
4
- #watermark = 0;
5
- #timer;
6
- /** Guards against overlapping poll cycles (e.g. slow fetch). */
7
- #running = false;
8
- // ── Constructor ─────────────────────────────────────────────────────
9
- constructor(options) {
10
- this.#options = options;
11
- }
12
- // ── Public API ──────────────────────────────────────────────────────
13
- /**
14
- * The current watermark — the next `seq` the sync will fetch from.
15
- *
16
- * Starts at `0` (fetch everything). Advances to `max(seq) + 1` after
17
- * each successful poll cycle.
18
- */
19
- get watermark() {
20
- return this.#watermark;
21
- }
22
- /**
23
- * Start polling for new events on the configured interval.
24
- *
25
- * Does nothing if polling is already active.
26
- * Does **not** perform an initial sync — call {@link sync} once if you
27
- * need to catch up immediately.
28
- */
29
- start() {
30
- if (this.#timer !== void 0) {
31
- return;
32
- }
33
- const ms = this.#options.pollInterval ?? 5e3;
34
- this.#timer = setInterval(() => {
35
- this.#poll().catch(() => void 0);
36
- }, ms);
37
- }
38
- /**
39
- * Stop polling for new events.
40
- *
41
- * Safe to call when not started.
42
- */
43
- stop() {
44
- if (this.#timer !== void 0) {
45
- clearInterval(this.#timer);
46
- this.#timer = void 0;
47
- }
48
- }
49
- /**
50
- * Perform a one-shot sync: fetch events since the current watermark,
51
- * apply them through the state machine, and push diffs to the mirror.
52
- * @returns The number of events that were fetched and applied.
53
- */
54
- async sync() {
55
- return this.#poll();
56
- }
57
- // ── Internal ────────────────────────────────────────────────────────
58
- /**
59
- * One poll cycle: fetch → apply → diff → mirror.
60
- */
61
- async #poll() {
62
- if (this.#running) {
63
- return 0;
64
- }
65
- this.#running = true;
66
- try {
67
- const events = await this.#options.fetchEventsSince(this.#watermark);
68
- if (events.length === 0) {
69
- return 0;
70
- }
71
- this.#options.applyEvents(events);
72
- const lastEvent = events[events.length - 1];
73
- if (lastEvent) {
74
- this.#watermark = lastEvent.seq + 1;
75
- }
76
- const diffs = this.#options.getTableDiffs();
77
- for (const diff of diffs) {
78
- this.#options.mirror.applyDiff(diff);
79
- }
80
- return events.length;
81
- } catch (error) {
82
- const onError = this.#options.onError ?? console.error;
83
- onError(error);
84
- return 0;
85
- } finally {
86
- this.#running = false;
87
- }
88
- }
89
- }
90
-
91
- export { EventsSync };
@@ -1,24 +0,0 @@
1
- class InMemorySnapshotStore {
2
- #store = /* @__PURE__ */ new Map();
3
- save(key, snapshot) {
4
- this.#store.set(key, structuredClone(snapshot));
5
- return Promise.resolve();
6
- }
7
- load(key) {
8
- const value = this.#store.get(key);
9
- return Promise.resolve(value === void 0 ? null : structuredClone(value));
10
- }
11
- list() {
12
- return Promise.resolve([...this.#store.keys()]);
13
- }
14
- delete(key) {
15
- this.#store.delete(key);
16
- return Promise.resolve();
17
- }
18
- clear() {
19
- this.#store.clear();
20
- return Promise.resolve();
21
- }
22
- }
23
-
24
- export { InMemorySnapshotStore };