@lunora/replica 1.0.0-alpha.4 → 1.0.0-alpha.5
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/adapters/sqlite-wasm.d.mts +18 -8
- package/dist/adapters/sqlite-wasm.d.ts +18 -8
- package/dist/adapters/sqlite-wasm.mjs +12 -24
- package/dist/index.d.mts +79 -18
- package/dist/index.d.ts +79 -18
- package/dist/index.mjs +9 -9
- package/dist/packem_shared/EventLog-CnK-3Wge.mjs +264 -0
- package/dist/packem_shared/{EventLogDO-CZYUvvSr.mjs → EventLogDO-DqlsVx0H.mjs} +155 -9
- package/dist/packem_shared/{EventLogDOClient-DGiEdi96.mjs → EventLogDOClient-F4FO8Si4.mjs} +8 -2
- package/dist/packem_shared/{EventSource-DfV4VoRD.mjs → EventSource-D5yO9_aI.mjs} +48 -22
- package/dist/packem_shared/{EventsSync-DkVbU0WV.mjs → EventsSync-BP36tC9O.mjs} +49 -17
- package/dist/packem_shared/{LocalMirror-GeJ26eNe.mjs → LocalMirror-a-5jEqFN.mjs} +34 -3
- package/dist/packem_shared/{MaterializerRuntime-HqNXqJxp.mjs → MaterializerRuntime-BoIrsMYB.mjs} +65 -45
- package/dist/packem_shared/applyDiff-98tKzmiW.mjs +67 -0
- package/dist/packem_shared/{classifyChanges-aZmkxgVI.mjs → classifyChanges-RcqLBpLs.mjs} +6 -3
- package/dist/packem_shared/{local-mirror.d-Cd8tAg-W.d.ts → local-mirror.d-ByIjd7sW.d.ts} +94 -3
- package/dist/packem_shared/{local-mirror.d-BUeOe5KC.d.mts → local-mirror.d-DL1XJBB3.d.mts} +94 -3
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs +2 -2
- package/package.json +1 -1
- package/dist/packem_shared/EventLog-zMy7AYP4.mjs +0 -162
- package/dist/packem_shared/applyDiff-BtbIl1D3.mjs +0 -40
|
@@ -28,16 +28,29 @@ type RowChange = {
|
|
|
28
28
|
interface TableDiff {
|
|
29
29
|
/** Ordered row changes — insert/update/delete, earliest first. */
|
|
30
30
|
readonly changes: ReadonlyArray<RowChange>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional stable identity for this diff, distinct from `timestamp`
|
|
33
|
+
* (multiple diffs can legitimately share a millisecond, so `timestamp`
|
|
34
|
+
* alone is not a unique diff identity). Used by `deriveInsertId` in
|
|
35
|
+
* `apply-diff.ts` to derive deterministic row ids for id-less inserts:
|
|
36
|
+
* replaying the SAME diff (same `id`) must always mint the SAME id,
|
|
37
|
+
* while two DIFFERENT diffs emitted in the same millisecond must not
|
|
38
|
+
* alias onto the same one. `createTableDiff` auto-generates one when
|
|
39
|
+
* omitted; diffs built as plain object literals (bypassing the helper)
|
|
40
|
+
* simply fall back to `timestamp` for that derivation.
|
|
41
|
+
*/
|
|
42
|
+
readonly id?: string;
|
|
31
43
|
/** Logical table name (matches the schema table name). */
|
|
32
44
|
readonly table: string;
|
|
33
45
|
/** Monotonic server timestamp (ms since epoch) when this diff was emitted. */
|
|
34
46
|
readonly timestamp: number;
|
|
35
47
|
}
|
|
36
48
|
/**
|
|
37
|
-
* Create a {@link TableDiff} with a snapshot of the current time
|
|
49
|
+
* Create a {@link TableDiff} with a snapshot of the current time and a
|
|
50
|
+
* fresh stable `id` (unless one is explicitly provided).
|
|
38
51
|
* @experimental
|
|
39
52
|
*/
|
|
40
|
-
declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number) => TableDiff;
|
|
53
|
+
declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number, id?: string) => TableDiff;
|
|
41
54
|
/**
|
|
42
55
|
* Return `true` when the diff contains no row changes.
|
|
43
56
|
* @experimental
|
|
@@ -212,6 +225,37 @@ interface AppendOptions {
|
|
|
212
225
|
readonly parentSeqNum?: Seq;
|
|
213
226
|
/** Session identifier within the client. */
|
|
214
227
|
readonly sessionId?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Override the entry's `timestamp` instead of stamping `Date.now()` at
|
|
230
|
+
* append time.
|
|
231
|
+
*
|
|
232
|
+
* Used by callers (e.g. {@link import("./event-source").EventSource | EventSource})
|
|
233
|
+
* that must commit the EXACT entry a reducer already observed — a
|
|
234
|
+
* second, independently-drawn `Date.now()` at append time could produce
|
|
235
|
+
* a persisted entry a timestamp-dependent reducer cannot reproduce on
|
|
236
|
+
* replay.
|
|
237
|
+
*/
|
|
238
|
+
readonly timestamp?: number;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Options for constructing an {@link EventLog}.
|
|
242
|
+
* @experimental
|
|
243
|
+
*/
|
|
244
|
+
interface EventLogOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Cap the number of entries retained in memory (REPLICA-06). When an
|
|
247
|
+
* append would exceed the cap, the OLDEST entries are evicted (ring
|
|
248
|
+
* buffer) — a `getSince`/`getFrom` call for a watermark below the oldest
|
|
249
|
+
* retained `seq` then returns only what's left, silently missing
|
|
250
|
+
* anything evicted.
|
|
251
|
+
*
|
|
252
|
+
* `undefined` (the default) preserves the original unbounded behavior.
|
|
253
|
+
* Set this only when you have another durable source of truth for
|
|
254
|
+
* anything older than the cap (a snapshot, a server-side `EventLogDO`) —
|
|
255
|
+
* see {@link EventLog#truncateBelow} for the caller-driven equivalent
|
|
256
|
+
* tied to snapshot persistence.
|
|
257
|
+
*/
|
|
258
|
+
readonly maxEntries?: number;
|
|
215
259
|
}
|
|
216
260
|
/**
|
|
217
261
|
* An append-only, in-memory event log for local event sourcing.
|
|
@@ -226,6 +270,7 @@ interface AppendOptions {
|
|
|
226
270
|
*/
|
|
227
271
|
declare class EventLog {
|
|
228
272
|
#private;
|
|
273
|
+
constructor(options?: EventLogOptions);
|
|
229
274
|
/**
|
|
230
275
|
* Append a new entry to the log.
|
|
231
276
|
*
|
|
@@ -254,6 +299,10 @@ declare class EventLog {
|
|
|
254
299
|
* This is the restore counterpart of {@link EventLog#snapshot}.
|
|
255
300
|
* Restores `headSeq` from the snapshot so auto-parenting continues
|
|
256
301
|
* after restore.
|
|
302
|
+
*
|
|
303
|
+
* Runs `#enforceCap()` after restoring so a snapshot captured under a
|
|
304
|
+
* different (or no) `maxEntries` can never leave this log over its
|
|
305
|
+
* configured capacity.
|
|
257
306
|
*/
|
|
258
307
|
load(snapshot: EventLogSnapshot): void;
|
|
259
308
|
/**
|
|
@@ -289,6 +338,24 @@ declare class EventLog {
|
|
|
289
338
|
/** Remove all entries (primarily for testing). */
|
|
290
339
|
clear(): void;
|
|
291
340
|
/**
|
|
341
|
+
* Discard all entries with `seq < floorSeq` (REPLICA-06).
|
|
342
|
+
*
|
|
343
|
+
* `headSeq`/`nextSeq` are untouched (they're independent counters), so
|
|
344
|
+
* appends after a truncation continue the same sequence uninterrupted.
|
|
345
|
+
*
|
|
346
|
+
* **Caller-driven, not automatic**: only call this after the truncated
|
|
347
|
+
* range has already been durably captured elsewhere (a snapshot, a
|
|
348
|
+
* server-side `EventLogDO`) — truncating without such a floor makes any
|
|
349
|
+
* future `getSince`/`getFrom`/`EventSource.replayFromLog` call for a
|
|
350
|
+
* watermark below `floorSeq` silently miss the discarded entries. This is
|
|
351
|
+
* the hook the caller ties to snapshot persistence; the log itself has no
|
|
352
|
+
* concept of "already durably persisted".
|
|
353
|
+
*
|
|
354
|
+
* `floorSeq` must be a non-negative safe integer — `NaN` would make
|
|
355
|
+
* every comparison false and silently clear the entire log.
|
|
356
|
+
*/
|
|
357
|
+
truncateBelow(floorSeq: number): void;
|
|
358
|
+
/**
|
|
292
359
|
* Return an async generator that yields every entry starting from
|
|
293
360
|
* `fromSeq` (default `0` = all entries).
|
|
294
361
|
*
|
|
@@ -314,6 +381,18 @@ interface LocalMirrorOptions {
|
|
|
314
381
|
/** Platform-specific SQLite adapter. */
|
|
315
382
|
readonly db: SqliteAdapter;
|
|
316
383
|
/**
|
|
384
|
+
* Cap the mirror's internal {@link EventLog} to this many entries
|
|
385
|
+
* (REPLICA-06). Every applied diff is recorded in the log — with no cap,
|
|
386
|
+
* a long-running client accumulates one entry per diff forever.
|
|
387
|
+
*
|
|
388
|
+
* `undefined` (the default) preserves unbounded retention. Set this when
|
|
389
|
+
* catch-up replication only ever needs a bounded recent window; older
|
|
390
|
+
* entries are silently evicted (oldest-first) once the cap is exceeded.
|
|
391
|
+
* See {@link EventLog#truncateBelow} for caller-driven truncation tied to
|
|
392
|
+
* a snapshot instead.
|
|
393
|
+
*/
|
|
394
|
+
readonly maxEventLogEntries?: number;
|
|
395
|
+
/**
|
|
317
396
|
* Table schemas the mirror should manage.
|
|
318
397
|
*
|
|
319
398
|
* On first use the mirror creates any missing tables automatically
|
|
@@ -399,6 +478,13 @@ declare class LocalMirror {
|
|
|
399
478
|
*/
|
|
400
479
|
get db(): SqliteAdapter;
|
|
401
480
|
/**
|
|
481
|
+
* Monotonically increasing version counter, bumped on every operation
|
|
482
|
+
* that changes mirrored data (`applyDiff`, `clearData`). Use this — not
|
|
483
|
+
* `eventLog.size` — as a `useSyncExternalStore` snapshot so operations
|
|
484
|
+
* that don't append to the log still trigger a re-render.
|
|
485
|
+
*/
|
|
486
|
+
get version(): number;
|
|
487
|
+
/**
|
|
402
488
|
* Apply a server-side diff to the local SQLite mirror.
|
|
403
489
|
*
|
|
404
490
|
* The diff is applied in a transaction and recorded in the event log
|
|
@@ -420,6 +506,11 @@ declare class LocalMirror {
|
|
|
420
506
|
/**
|
|
421
507
|
* Delete every row from all known tables (preserves the event log
|
|
422
508
|
* and schema). Useful when re-syncing from scratch.
|
|
509
|
+
*
|
|
510
|
+
* Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
|
|
511
|
+
* (REPLICA-09) even though nothing is appended to the event log — a
|
|
512
|
+
* consumer keyed only on `eventLog.size` would otherwise never learn the
|
|
513
|
+
* mirror was cleared and keep rendering deleted rows.
|
|
423
514
|
*/
|
|
424
515
|
clearData(): void;
|
|
425
516
|
/**
|
|
@@ -436,4 +527,4 @@ declare class LocalMirror {
|
|
|
436
527
|
*/
|
|
437
528
|
get mirroredTables(): ReadonlyArray<string>;
|
|
438
529
|
}
|
|
439
|
-
export { AppendOptions as A, ClientSeq as C, EventLogEntry as E, GlobalSeq as G, InputEvent as I, LocalMirror as L, MirrorTableDef as M, RowChange as R, Seq as S, TableDiff as T, EventLog as a,
|
|
530
|
+
export { AppendOptions as A, ClientSeq as C, EventLogEntry as E, GlobalSeq as G, InputEvent as I, LocalMirror as L, MirrorTableDef as M, RowChange as R, Seq as S, TableDiff as T, EventLog as a, EventLogOptions as b, EventLogSnapshot as c, LocalMirrorOptions as d, classifyChanges as e, createTableDiff as f, diffSize as g, isDiffEmpty as h, isClientSeq as i, isGlobalSeq as j, isInputEvent as k, mergeDiffs as m };
|
package/dist/react.d.mts
CHANGED
package/dist/react.d.ts
CHANGED
package/dist/react.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { useSyncExternalStore } from 'react';
|
|
|
2
2
|
|
|
3
3
|
const useLocalQuery = (mirror, sql, params, _options) => {
|
|
4
4
|
const subscribe = (onStoreChange) => mirror.onChange(onStoreChange);
|
|
5
|
-
const getSnapshot = () => mirror.
|
|
6
|
-
const getServerSnapshot = () => mirror.
|
|
5
|
+
const getSnapshot = () => mirror.version;
|
|
6
|
+
const getServerSnapshot = () => mirror.version;
|
|
7
7
|
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
8
8
|
try {
|
|
9
9
|
return mirror.query(sql, params);
|
package/package.json
CHANGED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
class EventLog {
|
|
2
|
-
#entries = [];
|
|
3
|
-
#nextSeq = 0;
|
|
4
|
-
// eslint-disable-next-line unicorn/no-null -- public contract uses `null` for an empty log head
|
|
5
|
-
#headSeq = null;
|
|
6
|
-
append(typeOrEvent, payload, tableDiffs, options) {
|
|
7
|
-
const type = typeof typeOrEvent === "string" ? typeOrEvent : typeOrEvent.type;
|
|
8
|
-
const pl = typeof typeOrEvent === "string" ? payload : typeOrEvent.payload;
|
|
9
|
-
let diffs;
|
|
10
|
-
let resolvedOptions;
|
|
11
|
-
if (typeof typeOrEvent === "string") {
|
|
12
|
-
diffs = tableDiffs;
|
|
13
|
-
resolvedOptions = options;
|
|
14
|
-
} else {
|
|
15
|
-
diffs = void 0;
|
|
16
|
-
resolvedOptions = payload;
|
|
17
|
-
}
|
|
18
|
-
const parentSeqNumber = resolvedOptions?.parentSeqNum ?? this.#headSeq ?? void 0;
|
|
19
|
-
const seq = this.#nextSeq;
|
|
20
|
-
this.#nextSeq += 1;
|
|
21
|
-
const entry = {
|
|
22
|
-
seq,
|
|
23
|
-
type,
|
|
24
|
-
payload: pl,
|
|
25
|
-
timestamp: Date.now(),
|
|
26
|
-
tableDiffs: diffs,
|
|
27
|
-
clientId: resolvedOptions?.clientId,
|
|
28
|
-
sessionId: resolvedOptions?.sessionId,
|
|
29
|
-
parentSeqNum: parentSeqNumber
|
|
30
|
-
};
|
|
31
|
-
this.#entries.push(entry);
|
|
32
|
-
this.#headSeq = entry.seq;
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Atomically append multiple events to the log.
|
|
37
|
-
*
|
|
38
|
-
* All events are assigned sequential global sequence numbers and
|
|
39
|
-
* automatically wired as a causal chain (each event's `parentSeqNum`
|
|
40
|
-
* points to the previous event in the batch, or to the log head for
|
|
41
|
-
* the first event).
|
|
42
|
-
* @param events An array of events to commit atomically.
|
|
43
|
-
* @returns The newly created entries in order.
|
|
44
|
-
*/
|
|
45
|
-
commitAll(events) {
|
|
46
|
-
if (events.length === 0) {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
const entries = [];
|
|
50
|
-
for (const event of events) {
|
|
51
|
-
const { type } = event;
|
|
52
|
-
const payload = "payload" in event ? event.payload : void 0;
|
|
53
|
-
const ts = "timestamp" in event ? event.timestamp : Date.now();
|
|
54
|
-
const parentSeqNumber = entries.at(-1)?.seq ?? this.#headSeq ?? void 0;
|
|
55
|
-
const seq = this.#nextSeq;
|
|
56
|
-
this.#nextSeq += 1;
|
|
57
|
-
const entry = {
|
|
58
|
-
seq,
|
|
59
|
-
type,
|
|
60
|
-
payload,
|
|
61
|
-
timestamp: ts,
|
|
62
|
-
parentSeqNum: parentSeqNumber
|
|
63
|
-
};
|
|
64
|
-
this.#entries.push(entry);
|
|
65
|
-
entries.push(entry);
|
|
66
|
-
}
|
|
67
|
-
this.#headSeq = entries.at(-1)?.seq ?? this.#headSeq;
|
|
68
|
-
return entries;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Replace the log contents with a previously captured snapshot.
|
|
72
|
-
* This is the restore counterpart of {@link EventLog#snapshot}.
|
|
73
|
-
* Restores `headSeq` from the snapshot so auto-parenting continues
|
|
74
|
-
* after restore.
|
|
75
|
-
*/
|
|
76
|
-
load(snapshot) {
|
|
77
|
-
this.#entries = [...snapshot.entries];
|
|
78
|
-
this.#nextSeq = snapshot.nextSeq;
|
|
79
|
-
this.#headSeq = snapshot.headSeq;
|
|
80
|
-
}
|
|
81
|
-
// ── Queries ───────────────────────────────────────────────────────
|
|
82
|
-
/**
|
|
83
|
-
* Return **all** entries whose `seq >= sinceSeq`.
|
|
84
|
-
* Useful for catch-up: "give me everything since my last watermark".
|
|
85
|
-
*/
|
|
86
|
-
getSince(sinceSeq) {
|
|
87
|
-
if (sinceSeq <= 0) {
|
|
88
|
-
return [...this.#entries];
|
|
89
|
-
}
|
|
90
|
-
const first = this.#entries.findIndex((entry) => entry.seq >= sinceSeq);
|
|
91
|
-
return first === -1 ? [] : this.#entries.slice(first);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Paginated read starting at `fromSeq`.
|
|
95
|
-
* @returns `{ entries, hasMore }` where `hasMore` is `true` when more
|
|
96
|
-
* entries exist beyond the requested page.
|
|
97
|
-
*/
|
|
98
|
-
getFrom(fromSeq, limit = 50) {
|
|
99
|
-
const first = this.#entries.findIndex((entry) => entry.seq >= fromSeq);
|
|
100
|
-
if (first === -1) {
|
|
101
|
-
return { entries: [], hasMore: false };
|
|
102
|
-
}
|
|
103
|
-
const slice = this.#entries.slice(first, first + limit);
|
|
104
|
-
return {
|
|
105
|
-
entries: slice,
|
|
106
|
-
hasMore: first + limit < this.#entries.length
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Return all entries as a snapshot suitable for serialisation.
|
|
111
|
-
*/
|
|
112
|
-
snapshot() {
|
|
113
|
-
return {
|
|
114
|
-
entries: [...this.#entries],
|
|
115
|
-
nextSeq: this.#nextSeq,
|
|
116
|
-
headSeq: this.#headSeq
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
/** Number of entries currently in the log. */
|
|
120
|
-
get size() {
|
|
121
|
-
return this.#entries.length;
|
|
122
|
-
}
|
|
123
|
-
/** The next sequence number that will be assigned. */
|
|
124
|
-
get nextSeq() {
|
|
125
|
-
return this.#nextSeq;
|
|
126
|
-
}
|
|
127
|
-
/** Return `true` when there are no entries. */
|
|
128
|
-
get isEmpty() {
|
|
129
|
-
return this.#entries.length === 0;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The sequence number of the last (most recent) entry, or `null`
|
|
133
|
-
* when the log is empty. Used internally for auto-parenting and
|
|
134
|
-
* exposed for consumers that need the causal head.
|
|
135
|
-
*/
|
|
136
|
-
get headSeq() {
|
|
137
|
-
return this.#headSeq;
|
|
138
|
-
}
|
|
139
|
-
/** Remove all entries (primarily for testing). */
|
|
140
|
-
clear() {
|
|
141
|
-
this.#entries = [];
|
|
142
|
-
this.#nextSeq = 0;
|
|
143
|
-
this.#headSeq = null;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Return an async generator that yields every entry starting from
|
|
147
|
-
* `fromSeq` (default `0` = all entries).
|
|
148
|
-
*
|
|
149
|
-
* Because `EventLog` is purely in-memory, the generator yields all
|
|
150
|
-
* matching entries synchronously on first iteration and then completes.
|
|
151
|
-
* For a streaming / push-based variant see {@link EventSource.events}.
|
|
152
|
-
*/
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- kept async so callers can uniformly `for await` over any event stream
|
|
154
|
-
async *events(fromSeq = 0) {
|
|
155
|
-
const entries = this.getSince(fromSeq);
|
|
156
|
-
for (const entry of entries) {
|
|
157
|
-
yield entry;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export { EventLog };
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
const applyDiff = (current, diff) => {
|
|
2
|
-
const next = new Map(current);
|
|
3
|
-
for (const change of diff.changes) {
|
|
4
|
-
switch (change.type) {
|
|
5
|
-
case "delete": {
|
|
6
|
-
next.delete(change.id);
|
|
7
|
-
break;
|
|
8
|
-
}
|
|
9
|
-
case "insert": {
|
|
10
|
-
const rawId = change.data.id;
|
|
11
|
-
const id = typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : crypto.randomUUID();
|
|
12
|
-
next.set(id, { ...change.data, id });
|
|
13
|
-
break;
|
|
14
|
-
}
|
|
15
|
-
case "update": {
|
|
16
|
-
const existing = next.get(change.id);
|
|
17
|
-
if (existing) {
|
|
18
|
-
next.set(change.id, { ...existing, ...change.data });
|
|
19
|
-
}
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return next;
|
|
25
|
-
};
|
|
26
|
-
const applyDiffs = (current, diffs) => {
|
|
27
|
-
let result = new Map(current);
|
|
28
|
-
for (const diff of diffs) {
|
|
29
|
-
result = applyDiff(result, diff);
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
};
|
|
33
|
-
const applyDiffToSnapshot = (snapshot, diff) => {
|
|
34
|
-
const next = new Map(snapshot);
|
|
35
|
-
const tableMap = next.get(diff.table) ?? /* @__PURE__ */ new Map();
|
|
36
|
-
next.set(diff.table, applyDiff(tableMap, diff));
|
|
37
|
-
return next;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export { applyDiff, applyDiffToSnapshot, applyDiffs };
|