@loro-dev/streams-crdt 0.1.0 → 0.1.2
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/README.md +98 -1
- package/dist/flock.d.ts +4 -29
- package/dist/flock.js +39 -81
- package/dist/flock.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/loro.d.ts +2 -2
- package/dist/loro.js +1 -1
- package/dist/{stream-id-1f2mhtmk.d.ts → stream-id-BfuQIfcB.d.ts} +36 -4
- package/dist/{stream-id-CIhmveJD.js → stream-id-YOFZ4I6Y.js} +22 -3
- package/dist/stream-id-YOFZ4I6Y.js.map +1 -0
- package/dist/zstd-core-l_BzmODD.js +24 -0
- package/dist/zstd-core-l_BzmODD.js.map +1 -0
- package/dist/zstd-worker.d.ts +1 -0
- package/dist/zstd-worker.js +24 -0
- package/dist/zstd-worker.js.map +1 -0
- package/dist/zstd.d.ts +8 -0
- package/dist/zstd.js +92 -0
- package/dist/zstd.js.map +1 -0
- package/package.json +14 -3
- package/dist/stream-id-CIhmveJD.js.map +0 -1
package/README.md
CHANGED
|
@@ -295,6 +295,91 @@ The required ordering is:
|
|
|
295
295
|
If you cannot provide steps 1 and 3, do not use a persisted remote cursor
|
|
296
296
|
store.
|
|
297
297
|
|
|
298
|
+
## Snapshot Codec Hooks
|
|
299
|
+
|
|
300
|
+
Use `snapshotCodec` when you want transport to transform full snapshots during
|
|
301
|
+
upload and bootstrap:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
const transport = new StreamsCrdt({
|
|
305
|
+
streamUrl,
|
|
306
|
+
adapter: createLoroDocAdapter(doc),
|
|
307
|
+
snapshotCodec: {
|
|
308
|
+
// Sync or async are both allowed.
|
|
309
|
+
compress: async (snapshot) => snapshot,
|
|
310
|
+
decompress: (snapshot) => snapshot,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Behavior
|
|
316
|
+
|
|
317
|
+
- `snapshotCodec.compress(snapshot)` runs right before a snapshot is uploaded to
|
|
318
|
+
Loro Streams
|
|
319
|
+
- `snapshotCodec.decompress(snapshot)` runs right after a non-empty snapshot is
|
|
320
|
+
downloaded from Loro Streams and right before `adapter.applySnapshot(...)`
|
|
321
|
+
- Both hooks must be provided together
|
|
322
|
+
- Both hooks may return either `Uint8Array` or `Promise<Uint8Array>`
|
|
323
|
+
- These hooks affect **full snapshots only**. Incremental update batches are
|
|
324
|
+
unchanged
|
|
325
|
+
|
|
326
|
+
### Compatibility
|
|
327
|
+
|
|
328
|
+
Transport does not version or negotiate snapshot codecs for you.
|
|
329
|
+
|
|
330
|
+
If you enable `snapshotCodec`, you must make sure `decompress` can still read
|
|
331
|
+
any older snapshots that are already stored for that stream. This is especially
|
|
332
|
+
important when:
|
|
333
|
+
|
|
334
|
+
- existing snapshots were uploaded without compression
|
|
335
|
+
- you change the compression format later
|
|
336
|
+
- you roll out the codec gradually across clients
|
|
337
|
+
|
|
338
|
+
### `@loro-dev/streams-crdt/zstd`
|
|
339
|
+
|
|
340
|
+
`@loro-dev/streams-crdt/zstd` is an optional helper entry point built on top of
|
|
341
|
+
[`@bokuweb/zstd-wasm`](https://www.npmjs.com/package/@bokuweb/zstd-wasm).
|
|
342
|
+
|
|
343
|
+
Install the optional peer dependency first:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
pnpm add @loro-dev/streams-crdt @bokuweb/zstd-wasm
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Then wire the exported hooks directly into `snapshotCodec`:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { LoroDoc } from "loro-crdt";
|
|
353
|
+
import {
|
|
354
|
+
createLoroDocAdapter,
|
|
355
|
+
createStreamUrl,
|
|
356
|
+
StreamsCrdt,
|
|
357
|
+
} from "@loro-dev/streams-crdt/loro";
|
|
358
|
+
import {
|
|
359
|
+
compress,
|
|
360
|
+
decompress,
|
|
361
|
+
} from "@loro-dev/streams-crdt/zstd";
|
|
362
|
+
|
|
363
|
+
const doc = new LoroDoc();
|
|
364
|
+
const transport = new StreamsCrdt({
|
|
365
|
+
streamUrl: createStreamUrl({
|
|
366
|
+
bucketId: "my-bucket",
|
|
367
|
+
streamId: "doc-1",
|
|
368
|
+
}),
|
|
369
|
+
adapter: createLoroDocAdapter(doc),
|
|
370
|
+
snapshotCodec: { compress, decompress },
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`@loro-dev/streams-crdt/zstd` exports:
|
|
375
|
+
|
|
376
|
+
- `compress(snapshot)` — async; uses an extra Worker when available and falls
|
|
377
|
+
back to the current thread otherwise
|
|
378
|
+
- `compressInWorker(snapshot)` — async; worker-only compression helper
|
|
379
|
+
- `compressOnCurrentThread(snapshot)` — async; current-thread compression
|
|
380
|
+
helper
|
|
381
|
+
- `decompress(snapshot)` — async; current-thread decompression helper
|
|
382
|
+
|
|
298
383
|
## Automatic Snapshot Upload
|
|
299
384
|
|
|
300
385
|
The transport can upload snapshots automatically during `join()` when callers
|
|
@@ -348,6 +433,7 @@ const transport = new StreamsCrdt({
|
|
|
348
433
|
| `@loro-dev/streams-crdt` | Transport core, cursor stores, ID helpers, all shared types |
|
|
349
434
|
| `@loro-dev/streams-crdt/loro` | Everything above + `createLoroDocAdapter(doc)` |
|
|
350
435
|
| `@loro-dev/streams-crdt/flock` | Everything above + `createFlockAdapter(flock)` |
|
|
436
|
+
| `@loro-dev/streams-crdt/zstd` | Snapshot `compress` / `decompress` hooks backed by `@bokuweb/zstd-wasm` |
|
|
351
437
|
|
|
352
438
|
## Public API Reference
|
|
353
439
|
|
|
@@ -364,6 +450,7 @@ const transport = new StreamsCrdt({
|
|
|
364
450
|
- `TransportJoinParams` — `join()` options
|
|
365
451
|
- `TransportSubscription` — active live subscription handle
|
|
366
452
|
- `TransportRoomStatus` — `"joined" | "reconnecting" | "disconnected" | "error"`
|
|
453
|
+
- `SnapshotCodec`, `SnapshotTransformHook` — full snapshot encode/decode hooks
|
|
367
454
|
- `SnapshotUploadOptions` — snapshot upload configuration
|
|
368
455
|
- `RemoteCursor` — replay progress metadata
|
|
369
456
|
- `RemoteCursorStore` — abstract cursor storage interface
|
|
@@ -387,7 +474,17 @@ const transport = new StreamsCrdt({
|
|
|
387
474
|
`@loro-dev/streams-crdt/flock` re-exports everything from the root plus:
|
|
388
475
|
|
|
389
476
|
- `createFlockAdapter(flock)` — creates a `CrdtAdapter` for one `Flock` replica
|
|
390
|
-
- `
|
|
477
|
+
- `VersionVector`, `ExportBundle` — re-exported from `@loro-dev/flock-wasm`
|
|
478
|
+
|
|
479
|
+
### Zstd Entry Point
|
|
480
|
+
|
|
481
|
+
`@loro-dev/streams-crdt/zstd` exports:
|
|
482
|
+
|
|
483
|
+
- `compress(snapshot)` — async snapshot compression helper
|
|
484
|
+
- `compressInWorker(snapshot)` — async worker-only snapshot compression helper
|
|
485
|
+
- `compressOnCurrentThread(snapshot)` — async current-thread snapshot
|
|
486
|
+
compression helper
|
|
487
|
+
- `decompress(snapshot)` — async snapshot decompression helper
|
|
391
488
|
|
|
392
489
|
## Notes
|
|
393
490
|
|
package/dist/flock.d.ts
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as createInitialRemoteCursor, C as BeforeRemoteCursorSaveHook, D as RemoteCursor, E as IndexedDbRemoteCursorStoreOptions, O as RemoteCursorSaveSource, S as BeforeRemoteCursorSaveContext, T as IndexedDbRemoteCursorStore, _ as TransportError, a as CrdtAdapter, b as TransportSubscription, c as JsonValue, d as SnapshotTransformHook, f as SnapshotUploadOptions, g as TransportDeleteStreamSuccess, h as TransportCreateStreamSuccess, i as StreamsCrdt, k as RemoteCursorStore, l as Result, m as StreamsCrdtOptions, n as isValidBucketId, o as CrdtUpdateBatch, p as StreamsAuthProvider, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotCodec, v as TransportJoinParams, w as InMemoryRemoteCursorStore, x as TransportSyncSuccess, y as TransportRoomStatus } from "./stream-id-BfuQIfcB.js";
|
|
2
|
+
import { ExportBundle, Flock, VersionVector, VersionVector as VersionVector$1 } from "@loro-dev/flock-wasm";
|
|
2
3
|
|
|
3
4
|
//#region src/flock.d.ts
|
|
4
|
-
type FlockVersionEntry = {
|
|
5
|
-
logicalCounter: number;
|
|
6
|
-
physicalTime?: number;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Flock version vector shape used by the built-in Flock adapter.
|
|
10
|
-
*/
|
|
11
|
-
type FlockVersionVector = Record<string, FlockVersionEntry>;
|
|
12
|
-
type FlockExportBundle = {
|
|
13
|
-
readonly version?: number;
|
|
14
|
-
readonly entries: Record<string, unknown>;
|
|
15
|
-
};
|
|
16
|
-
type FlockLike = {
|
|
17
|
-
exportJson(options?: {
|
|
18
|
-
from?: FlockVersionVector;
|
|
19
|
-
peerId?: string;
|
|
20
|
-
}): FlockExportBundle;
|
|
21
|
-
importJson(bundle: FlockExportBundle): unknown;
|
|
22
|
-
merge?(replica: unknown): unknown;
|
|
23
|
-
peerId?(): string;
|
|
24
|
-
version(): unknown;
|
|
25
|
-
exportFile?(): Uint8Array;
|
|
26
|
-
subscribe?(listener: (batch: {
|
|
27
|
-
source?: string;
|
|
28
|
-
}) => void): () => void;
|
|
29
|
-
};
|
|
30
5
|
/**
|
|
31
6
|
* Creates a `CrdtAdapter` for one Flock replica.
|
|
32
7
|
*/
|
|
33
|
-
declare function createFlockAdapter(flock:
|
|
8
|
+
declare function createFlockAdapter(flock: Flock): CrdtAdapter<VersionVector$1>;
|
|
34
9
|
//#endregion
|
|
35
|
-
export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch,
|
|
10
|
+
export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, type ExportBundle, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, type VersionVector, createFlockAdapter, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
|
|
36
11
|
//# sourceMappingURL=flock.d.ts.map
|
package/dist/flock.js
CHANGED
|
@@ -1,74 +1,45 @@
|
|
|
1
|
-
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-
|
|
1
|
+
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-YOFZ4I6Y.js";
|
|
2
2
|
//#region src/flock.ts
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
for (const [peer, raw] of Object.entries(version)) {
|
|
6
|
-
if (typeof raw !== "object" || raw == null || Array.isArray(raw)) continue;
|
|
7
|
-
const logicalCounter = typeof raw.logicalCounter === "number" ? raw.logicalCounter : void 0;
|
|
8
|
-
if (logicalCounter == null || !Number.isFinite(logicalCounter)) continue;
|
|
9
|
-
out[peer] = typeof raw.physicalTime === "number" && Number.isFinite(raw.physicalTime) ? {
|
|
10
|
-
logicalCounter,
|
|
11
|
-
physicalTime: raw.physicalTime
|
|
12
|
-
} : { logicalCounter };
|
|
13
|
-
}
|
|
14
|
-
return out;
|
|
15
|
-
}
|
|
16
|
-
function fromVersionVector(version) {
|
|
17
|
-
const out = {};
|
|
18
|
-
for (const [peer, entry] of Object.entries(version)) out[peer] = entry.physicalTime === void 0 ? { logicalCounter: entry.logicalCounter } : {
|
|
19
|
-
logicalCounter: entry.logicalCounter,
|
|
20
|
-
physicalTime: entry.physicalTime
|
|
21
|
-
};
|
|
22
|
-
return out;
|
|
3
|
+
function isAfter(entry, current) {
|
|
4
|
+
return entry.physicalTime > current.physicalTime || entry.physicalTime === current.physicalTime && entry.logicalCounter > current.logicalCounter;
|
|
23
5
|
}
|
|
24
6
|
function mergeVersions(base, incoming) {
|
|
25
7
|
const merged = { ...base };
|
|
26
8
|
for (const [peer, entry] of Object.entries(incoming)) {
|
|
9
|
+
if (entry == null) continue;
|
|
27
10
|
const current = merged[peer];
|
|
28
|
-
if (current == null || entry
|
|
29
|
-
merged[peer] = { ...entry };
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (entry.logicalCounter < current.logicalCounter) continue;
|
|
33
|
-
const physicalTime = Math.max(current.physicalTime ?? Number.NEGATIVE_INFINITY, entry.physicalTime ?? Number.NEGATIVE_INFINITY);
|
|
34
|
-
merged[peer] = physicalTime === Number.NEGATIVE_INFINITY ? { logicalCounter: current.logicalCounter } : {
|
|
35
|
-
logicalCounter: current.logicalCounter,
|
|
36
|
-
physicalTime
|
|
37
|
-
};
|
|
11
|
+
if (current == null || isAfter(entry, current)) merged[peer] = { ...entry };
|
|
38
12
|
}
|
|
39
13
|
return merged;
|
|
40
14
|
}
|
|
15
|
+
function parseClock(record) {
|
|
16
|
+
const firstComma = record.c.indexOf(",");
|
|
17
|
+
const secondComma = record.c.indexOf(",", firstComma + 1);
|
|
18
|
+
if (firstComma < 0 || secondComma < 0) return null;
|
|
19
|
+
const physicalTime = Number(record.c.slice(0, firstComma));
|
|
20
|
+
const logicalCounter = Number(record.c.slice(firstComma + 1, secondComma));
|
|
21
|
+
if (!Number.isFinite(physicalTime) || !Number.isFinite(logicalCounter)) return null;
|
|
22
|
+
return {
|
|
23
|
+
peerId: record.c.slice(secondComma + 1),
|
|
24
|
+
entry: {
|
|
25
|
+
physicalTime,
|
|
26
|
+
logicalCounter
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
41
30
|
function extractVersionFromBundle(bundle) {
|
|
42
31
|
const out = {};
|
|
43
|
-
for (const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (logicalCounter == null || !Number.isFinite(logicalCounter)) continue;
|
|
49
|
-
out[entry.peerId] = typeof entry.physicalTime === "number" && Number.isFinite(entry.physicalTime) ? {
|
|
50
|
-
logicalCounter,
|
|
51
|
-
physicalTime: entry.physicalTime
|
|
52
|
-
} : { logicalCounter };
|
|
32
|
+
for (const record of Object.values(bundle.entries)) {
|
|
33
|
+
const parsed = parseClock(record);
|
|
34
|
+
if (parsed == null) continue;
|
|
35
|
+
const current = out[parsed.peerId];
|
|
36
|
+
if (current == null || isAfter(parsed.entry, current)) out[parsed.peerId] = parsed.entry;
|
|
53
37
|
}
|
|
54
38
|
return out;
|
|
55
39
|
}
|
|
56
40
|
function hasEntries(bundle) {
|
|
57
41
|
return Object.keys(bundle.entries).length > 0;
|
|
58
42
|
}
|
|
59
|
-
function tryMergeSnapshotFromFile(flock, snapshot) {
|
|
60
|
-
if (typeof flock.merge !== "function") return null;
|
|
61
|
-
const ctor = flock.constructor;
|
|
62
|
-
if (ctor == null || typeof ctor.fromFile !== "function") return null;
|
|
63
|
-
try {
|
|
64
|
-
const peerId = typeof flock.peerId === "function" ? flock.peerId() : void 0;
|
|
65
|
-
const replica = ctor.fromFile(snapshot, peerId);
|
|
66
|
-
flock.merge(replica);
|
|
67
|
-
return toVersionVector(flock.version() ?? {});
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
43
|
/**
|
|
73
44
|
* Creates a `CrdtAdapter` for one Flock replica.
|
|
74
45
|
*/
|
|
@@ -80,59 +51,46 @@ function createFlockAdapter(flock) {
|
|
|
80
51
|
};
|
|
81
52
|
return {
|
|
82
53
|
emptyVersion: () => ({}),
|
|
83
|
-
mergeVersions
|
|
84
|
-
exportSnapshot: () =>
|
|
85
|
-
if (typeof flock.exportFile === "function") return flock.exportFile();
|
|
86
|
-
return new TextEncoder().encode(JSON.stringify({
|
|
87
|
-
version: 0,
|
|
88
|
-
entries: {}
|
|
89
|
-
}));
|
|
90
|
-
},
|
|
54
|
+
mergeVersions,
|
|
55
|
+
exportSnapshot: () => flock.exportFile(),
|
|
91
56
|
applySnapshot: (snapshot) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (mergedVersion != null) return fromVersionVector(mergedVersion);
|
|
95
|
-
const bundle = JSON.parse(new TextDecoder().decode(snapshot));
|
|
96
|
-
if (hasEntries(bundle)) flock.importJson(bundle);
|
|
97
|
-
return fromVersionVector(extractVersionFromBundle(bundle));
|
|
57
|
+
flock.importFile(snapshot);
|
|
58
|
+
return flock.version();
|
|
98
59
|
},
|
|
99
60
|
exportUpdates: (fromVersion) => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
recordExportFrontier(from);
|
|
61
|
+
const bundle = flock.exportJson(fromVersion);
|
|
62
|
+
recordExportFrontier(fromVersion);
|
|
103
63
|
if (!hasEntries(bundle)) return;
|
|
104
64
|
const exportedVersion = extractVersionFromBundle(bundle);
|
|
105
65
|
recordExportFrontier(exportedVersion);
|
|
106
66
|
return {
|
|
107
67
|
updates: [new TextEncoder().encode(JSON.stringify(bundle))],
|
|
108
|
-
version:
|
|
68
|
+
version: exportedVersion
|
|
109
69
|
};
|
|
110
70
|
},
|
|
111
71
|
applyRemoteUpdates: (updates, remoterVersion) => {
|
|
112
|
-
let merged =
|
|
72
|
+
let merged = remoterVersion;
|
|
113
73
|
for (const update of updates) {
|
|
114
74
|
const bundle = JSON.parse(new TextDecoder().decode(update));
|
|
115
75
|
if (!hasEntries(bundle)) continue;
|
|
116
76
|
flock.importJson(bundle);
|
|
117
77
|
merged = mergeVersions(merged, extractVersionFromBundle(bundle));
|
|
118
78
|
}
|
|
119
|
-
return
|
|
79
|
+
return merged;
|
|
120
80
|
},
|
|
121
81
|
subscribeLocalUpdates: (listener) => {
|
|
122
|
-
if (typeof flock.subscribe !== "function") return () => void 0;
|
|
123
82
|
return flock.subscribe((batch) => {
|
|
124
83
|
if (batch.source !== "local") return;
|
|
125
|
-
const drain = () => {
|
|
126
|
-
const
|
|
127
|
-
const batchPayload = flock.exportJson({
|
|
84
|
+
const drain = async () => {
|
|
85
|
+
const batchPayload = await flock.exportJson({
|
|
128
86
|
from: lastExportedVersion,
|
|
129
|
-
peerId
|
|
87
|
+
peerId: flock.peerId()
|
|
130
88
|
});
|
|
131
89
|
if (!hasEntries(batchPayload)) return;
|
|
132
|
-
recordExportFrontier(
|
|
90
|
+
recordExportFrontier(flock.version());
|
|
133
91
|
listener({
|
|
134
92
|
updates: [new TextEncoder().encode(JSON.stringify(batchPayload))],
|
|
135
|
-
version:
|
|
93
|
+
version: extractVersionFromBundle(batchPayload)
|
|
136
94
|
});
|
|
137
95
|
};
|
|
138
96
|
localExportQueue = localExportQueue.then(drain).catch(() => {});
|
package/dist/flock.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flock.js","names":[],"sources":["../src/flock.ts"],"sourcesContent":["import type { JsonObject, CrdtAdapter } from \"./types\";\n\nexport { StreamsCrdt } from \"./transport\";\nexport * from \"./shared\";\n\ntype FlockVersionEntry = {\n logicalCounter: number;\n physicalTime?: number;\n};\n\n/**\n * Flock version vector shape used by the built-in Flock adapter.\n */\nexport type FlockVersionVector = Record<string, FlockVersionEntry>;\n\ntype FlockExportBundle = {\n readonly version?: number;\n readonly entries: Record<string, unknown>;\n};\n\ntype FlockLike = {\n exportJson(options?: {\n from?: FlockVersionVector;\n peerId?: string;\n }): FlockExportBundle;\n importJson(bundle: FlockExportBundle): unknown;\n merge?(replica: unknown): unknown;\n peerId?(): string;\n version(): unknown;\n exportFile?(): Uint8Array;\n subscribe?(listener: (batch: { source?: string }) => void): () => void;\n};\n\nfunction toVersionVector(version: JsonObject): FlockVersionVector {\n const out: FlockVersionVector = {};\n for (const [peer, raw] of Object.entries(version)) {\n if (typeof raw !== \"object\" || raw == null || Array.isArray(raw)) {\n continue;\n }\n const logicalCounter =\n typeof raw.logicalCounter === \"number\" ? raw.logicalCounter : undefined;\n if (logicalCounter == null || !Number.isFinite(logicalCounter)) {\n continue;\n }\n out[peer] =\n typeof raw.physicalTime === \"number\" && Number.isFinite(raw.physicalTime)\n ? { logicalCounter, physicalTime: raw.physicalTime }\n : { logicalCounter };\n }\n return out;\n}\n\nfunction fromVersionVector(version: FlockVersionVector): JsonObject {\n const out: Record<string, JsonObject> = {};\n for (const [peer, entry] of Object.entries(version)) {\n out[peer] =\n entry.physicalTime === undefined\n ? { logicalCounter: entry.logicalCounter }\n : {\n logicalCounter: entry.logicalCounter,\n physicalTime: entry.physicalTime,\n };\n }\n return out;\n}\n\nfunction mergeVersions(\n base: FlockVersionVector,\n incoming: FlockVersionVector,\n): FlockVersionVector {\n const merged: FlockVersionVector = { ...base };\n for (const [peer, entry] of Object.entries(incoming)) {\n const current = merged[peer];\n if (current == null || entry.logicalCounter > current.logicalCounter) {\n merged[peer] = { ...entry };\n continue;\n }\n if (entry.logicalCounter < current.logicalCounter) {\n continue;\n }\n const physicalTime = Math.max(\n current.physicalTime ?? Number.NEGATIVE_INFINITY,\n entry.physicalTime ?? Number.NEGATIVE_INFINITY,\n );\n merged[peer] =\n physicalTime === Number.NEGATIVE_INFINITY\n ? { logicalCounter: current.logicalCounter }\n : { logicalCounter: current.logicalCounter, physicalTime };\n }\n return merged;\n}\n\nfunction extractVersionFromBundle(\n bundle: FlockExportBundle,\n): FlockVersionVector {\n const out: FlockVersionVector = {};\n for (const raw of Object.values(bundle.entries)) {\n if (typeof raw !== \"object\" || raw == null || Array.isArray(raw)) {\n continue;\n }\n const entry = raw as {\n peerId?: unknown;\n logicalCounter?: unknown;\n counter?: unknown;\n physicalTime?: unknown;\n };\n if (typeof entry.peerId !== \"string\") {\n continue;\n }\n const logicalCounter =\n typeof entry.logicalCounter === \"number\"\n ? entry.logicalCounter\n : typeof entry.counter === \"number\"\n ? entry.counter\n : undefined;\n if (logicalCounter == null || !Number.isFinite(logicalCounter)) {\n continue;\n }\n out[entry.peerId] =\n typeof entry.physicalTime === \"number\" &&\n Number.isFinite(entry.physicalTime)\n ? { logicalCounter, physicalTime: entry.physicalTime }\n : { logicalCounter };\n }\n return out;\n}\n\nfunction hasEntries(bundle: FlockExportBundle): boolean {\n return Object.keys(bundle.entries).length > 0;\n}\n\nfunction tryMergeSnapshotFromFile(\n flock: FlockLike,\n snapshot: Uint8Array,\n): FlockVersionVector | null {\n if (typeof flock.merge !== \"function\") {\n return null;\n }\n const ctor = (\n flock as unknown as {\n readonly constructor?: {\n fromFile?: (bytes: Uint8Array, peerId?: string) => unknown;\n };\n }\n ).constructor;\n if (ctor == null || typeof ctor.fromFile !== \"function\") {\n return null;\n }\n try {\n const peerId =\n typeof flock.peerId === \"function\" ? flock.peerId() : undefined;\n const replica = ctor.fromFile(snapshot, peerId);\n flock.merge(replica);\n return toVersionVector((flock.version() as JsonObject) ?? {});\n } catch {\n return null;\n }\n}\n\n/**\n * Creates a `CrdtAdapter` for one Flock replica.\n */\nexport function createFlockAdapter(flock: FlockLike): CrdtAdapter<JsonObject> {\n let lastExportedVersion: FlockVersionVector = {};\n let localExportQueue = Promise.resolve();\n\n const recordExportFrontier = (version: FlockVersionVector): void => {\n lastExportedVersion = mergeVersions(lastExportedVersion, version);\n };\n\n return {\n emptyVersion: () => ({}),\n mergeVersions: (base, incoming) =>\n fromVersionVector(\n mergeVersions(toVersionVector(base), toVersionVector(incoming)),\n ),\n exportSnapshot: () => {\n if (typeof flock.exportFile === \"function\") {\n return flock.exportFile();\n }\n return new TextEncoder().encode(\n JSON.stringify({ version: 0, entries: {} }),\n );\n },\n applySnapshot: (snapshot) => {\n if (snapshot.byteLength === 0) {\n return {};\n }\n const mergedVersion = tryMergeSnapshotFromFile(flock, snapshot);\n if (mergedVersion != null) {\n return fromVersionVector(mergedVersion);\n }\n const bundle = JSON.parse(\n new TextDecoder().decode(snapshot),\n ) as FlockExportBundle;\n if (hasEntries(bundle)) {\n flock.importJson(bundle);\n }\n return fromVersionVector(extractVersionFromBundle(bundle));\n },\n exportUpdates: (fromVersion) => {\n const from = toVersionVector(fromVersion);\n const bundle = flock.exportJson({ from });\n recordExportFrontier(from);\n if (!hasEntries(bundle)) {\n return undefined;\n }\n const exportedVersion = extractVersionFromBundle(bundle);\n recordExportFrontier(exportedVersion);\n return {\n updates: [new TextEncoder().encode(JSON.stringify(bundle))],\n version: fromVersionVector(exportedVersion),\n };\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n let merged = toVersionVector(remoterVersion);\n for (const update of updates) {\n const bundle = JSON.parse(\n new TextDecoder().decode(update),\n ) as FlockExportBundle;\n if (!hasEntries(bundle)) {\n continue;\n }\n flock.importJson(bundle);\n merged = mergeVersions(merged, extractVersionFromBundle(bundle));\n }\n return fromVersionVector(merged);\n },\n subscribeLocalUpdates: (listener) => {\n if (typeof flock.subscribe !== \"function\") {\n return () => undefined;\n }\n const unsubscribe = flock.subscribe((batch) => {\n if (batch.source !== \"local\") {\n return;\n }\n const drain = () => {\n const peerId =\n typeof flock.peerId === \"function\" ? flock.peerId() : undefined;\n const batchPayload = flock.exportJson({\n from: lastExportedVersion,\n peerId,\n });\n if (!hasEntries(batchPayload)) {\n return;\n }\n const currentVersion = toVersionVector(\n fromVersionVector((flock.version() as FlockVersionVector) ?? {}),\n );\n recordExportFrontier(currentVersion);\n listener({\n updates: [new TextEncoder().encode(JSON.stringify(batchPayload))],\n version: fromVersionVector(extractVersionFromBundle(batchPayload)),\n });\n };\n\n localExportQueue = localExportQueue.then(drain).catch(() => {\n /* swallow to keep queue alive */\n });\n });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"flock.js","names":[],"sources":["../src/flock.ts"],"sourcesContent":["import { Flock } from \"@loro-dev/flock-wasm\";\nimport type {\n EventBatch,\n ExportBundle,\n ExportRecord,\n VersionVector,\n VersionVectorEntry,\n} from \"@loro-dev/flock-wasm\";\nimport type { CrdtAdapter } from \"./types\";\n\nexport { StreamsCrdt } from \"./transport\";\nexport * from \"./shared\";\nexport type { ExportBundle, VersionVector } from \"@loro-dev/flock-wasm\";\n\nfunction isAfter(\n entry: VersionVectorEntry,\n current: VersionVectorEntry,\n): boolean {\n return (\n entry.physicalTime > current.physicalTime ||\n (entry.physicalTime === current.physicalTime &&\n entry.logicalCounter > current.logicalCounter)\n );\n}\n\nfunction mergeVersions(\n base: VersionVector,\n incoming: VersionVector,\n): VersionVector {\n const merged: VersionVector = { ...base };\n for (const [peer, entry] of Object.entries(incoming)) {\n if (entry == null) {\n continue;\n }\n const current = merged[peer];\n if (current == null || isAfter(entry, current)) {\n merged[peer] = { ...entry };\n }\n }\n return merged;\n}\n\nfunction parseClock(\n record: ExportRecord,\n): { peerId: string; entry: VersionVectorEntry } | null {\n const firstComma = record.c.indexOf(\",\");\n const secondComma = record.c.indexOf(\",\", firstComma + 1);\n if (firstComma < 0 || secondComma < 0) {\n return null;\n }\n\n const physicalTime = Number(record.c.slice(0, firstComma));\n const logicalCounter = Number(record.c.slice(firstComma + 1, secondComma));\n if (!Number.isFinite(physicalTime) || !Number.isFinite(logicalCounter)) {\n return null;\n }\n\n return {\n peerId: record.c.slice(secondComma + 1),\n entry: { physicalTime, logicalCounter },\n };\n}\n\nfunction extractVersionFromBundle(bundle: ExportBundle): VersionVector {\n const out: VersionVector = {};\n for (const record of Object.values(bundle.entries)) {\n const parsed = parseClock(record);\n if (parsed == null) {\n continue;\n }\n const current = out[parsed.peerId];\n if (current == null || isAfter(parsed.entry, current)) {\n out[parsed.peerId] = parsed.entry;\n }\n }\n return out;\n}\n\nfunction hasEntries(bundle: ExportBundle): boolean {\n return Object.keys(bundle.entries).length > 0;\n}\n\n/**\n * Creates a `CrdtAdapter` for one Flock replica.\n */\nexport function createFlockAdapter(flock: Flock): CrdtAdapter<VersionVector> {\n let lastExportedVersion: VersionVector = {};\n let localExportQueue = Promise.resolve();\n\n const recordExportFrontier = (version: VersionVector): void => {\n lastExportedVersion = mergeVersions(lastExportedVersion, version);\n };\n\n return {\n emptyVersion: () => ({}),\n mergeVersions,\n exportSnapshot: () => flock.exportFile(),\n applySnapshot: (snapshot) => {\n flock.importFile(snapshot);\n return flock.version();\n },\n exportUpdates: (fromVersion) => {\n const bundle = flock.exportJson(fromVersion);\n recordExportFrontier(fromVersion);\n if (!hasEntries(bundle)) {\n return undefined;\n }\n const exportedVersion = extractVersionFromBundle(bundle);\n recordExportFrontier(exportedVersion);\n return {\n updates: [new TextEncoder().encode(JSON.stringify(bundle))],\n version: exportedVersion,\n };\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n let merged = remoterVersion;\n for (const update of updates) {\n const bundle = JSON.parse(\n new TextDecoder().decode(update),\n ) as ExportBundle;\n if (!hasEntries(bundle)) {\n continue;\n }\n flock.importJson(bundle);\n merged = mergeVersions(merged, extractVersionFromBundle(bundle));\n }\n return merged;\n },\n subscribeLocalUpdates: (listener) => {\n const unsubscribe = flock.subscribe((batch) => {\n if (batch.source !== \"local\") {\n return;\n }\n const drain = async () => {\n const batchPayload = await flock.exportJson({\n from: lastExportedVersion,\n peerId: flock.peerId(),\n });\n if (!hasEntries(batchPayload)) {\n return;\n }\n recordExportFrontier(flock.version());\n listener({\n updates: [new TextEncoder().encode(JSON.stringify(batchPayload))],\n version: extractVersionFromBundle(batchPayload),\n });\n };\n\n localExportQueue = localExportQueue.then(drain).catch(() => {\n /* swallow to keep queue alive */\n });\n });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotUploadOptions, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
|
|
1
|
+
import { A as createInitialRemoteCursor, C as BeforeRemoteCursorSaveHook, D as RemoteCursor, E as IndexedDbRemoteCursorStoreOptions, O as RemoteCursorSaveSource, S as BeforeRemoteCursorSaveContext, T as IndexedDbRemoteCursorStore, _ as TransportError, a as CrdtAdapter, b as TransportSubscription, c as JsonValue, d as SnapshotTransformHook, f as SnapshotUploadOptions, g as TransportDeleteStreamSuccess, h as TransportCreateStreamSuccess, i as StreamsCrdt, k as RemoteCursorStore, l as Result, m as StreamsCrdtOptions, n as isValidBucketId, o as CrdtUpdateBatch, p as StreamsAuthProvider, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotCodec, v as TransportJoinParams, w as InMemoryRemoteCursorStore, x as TransportSyncSuccess, y as TransportRoomStatus } from "./stream-id-BfuQIfcB.js";
|
|
2
|
+
export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-
|
|
1
|
+
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-YOFZ4I6Y.js";
|
|
2
2
|
export { InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, StreamsCrdt, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
|
package/dist/loro.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as createInitialRemoteCursor, C as BeforeRemoteCursorSaveHook, D as RemoteCursor, E as IndexedDbRemoteCursorStoreOptions, O as RemoteCursorSaveSource, S as BeforeRemoteCursorSaveContext, T as IndexedDbRemoteCursorStore, _ as TransportError, a as CrdtAdapter, b as TransportSubscription, c as JsonValue, d as SnapshotTransformHook, f as SnapshotUploadOptions, g as TransportDeleteStreamSuccess, h as TransportCreateStreamSuccess, i as StreamsCrdt, k as RemoteCursorStore, l as Result, m as StreamsCrdtOptions, n as isValidBucketId, o as CrdtUpdateBatch, p as StreamsAuthProvider, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotCodec, v as TransportJoinParams, w as InMemoryRemoteCursorStore, x as TransportSyncSuccess, y as TransportRoomStatus } from "./stream-id-BfuQIfcB.js";
|
|
2
2
|
import { LoroDoc } from "loro-crdt";
|
|
3
3
|
|
|
4
4
|
//#region src/loro.d.ts
|
|
@@ -18,5 +18,5 @@ import { LoroDoc } from "loro-crdt";
|
|
|
18
18
|
*/
|
|
19
19
|
declare function createLoroDocAdapter(doc: LoroDoc): CrdtAdapter<JsonObject>;
|
|
20
20
|
//#endregion
|
|
21
|
-
export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotUploadOptions, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createLoroDocAdapter, createStreamUrl, isValidBucketId, isValidRillId };
|
|
21
|
+
export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createLoroDocAdapter, createStreamUrl, isValidBucketId, isValidRillId };
|
|
22
22
|
//# sourceMappingURL=loro.d.ts.map
|
package/dist/loro.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-
|
|
1
|
+
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-YOFZ4I6Y.js";
|
|
2
2
|
import { VersionVector, decodeImportBlobMeta } from "loro-crdt";
|
|
3
3
|
//#region src/loro.ts
|
|
4
4
|
function toDocVersionRecord(version) {
|
|
@@ -119,10 +119,13 @@ type JsonPrimitive = string | number | boolean | null;
|
|
|
119
119
|
*/
|
|
120
120
|
type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
121
121
|
/**
|
|
122
|
-
* JSON object shape used by public transport types.
|
|
122
|
+
* JSON-like object shape used by public transport types.
|
|
123
|
+
*
|
|
124
|
+
* Sparse records may expose `undefined` for omitted keys at runtime (for
|
|
125
|
+
* example Flock's `VersionVector` index signature), so object fields allow it.
|
|
123
126
|
*/
|
|
124
127
|
type JsonObject = {
|
|
125
|
-
readonly [key: string]: JsonValue;
|
|
128
|
+
readonly [key: string]: JsonValue | undefined;
|
|
126
129
|
};
|
|
127
130
|
/**
|
|
128
131
|
* Authorization source for gateway requests.
|
|
@@ -239,6 +242,24 @@ interface SnapshotUploadOptions {
|
|
|
239
242
|
/** Minimum byte delta since the last remote snapshot before uploading. */
|
|
240
243
|
readonly minBytesSinceRemoteSnapshot?: number;
|
|
241
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Transforms a remote snapshot payload during upload or bootstrap.
|
|
247
|
+
*/
|
|
248
|
+
type SnapshotTransformHook = (snapshot: Uint8Array) => MaybePromise<Uint8Array>;
|
|
249
|
+
/**
|
|
250
|
+
* Optional snapshot codec hooks used only for full snapshot upload/bootstrap.
|
|
251
|
+
*
|
|
252
|
+
* `compress` runs before transport uploads a snapshot to Loro Streams.
|
|
253
|
+
* `decompress` runs after transport downloads a non-empty snapshot and before
|
|
254
|
+
* the adapter imports it.
|
|
255
|
+
*
|
|
256
|
+
* Callers own compatibility with any snapshots that were already uploaded
|
|
257
|
+
* without this codec or with an older codec revision.
|
|
258
|
+
*/
|
|
259
|
+
interface SnapshotCodec {
|
|
260
|
+
readonly compress: SnapshotTransformHook;
|
|
261
|
+
readonly decompress: SnapshotTransformHook;
|
|
262
|
+
}
|
|
242
263
|
/**
|
|
243
264
|
* Active live subscription returned by `join()`.
|
|
244
265
|
*
|
|
@@ -334,6 +355,17 @@ interface StreamsCrdtOptions<TVersion extends JsonObject> {
|
|
|
334
355
|
* hooks.
|
|
335
356
|
*/
|
|
336
357
|
readonly beforeRemoteCursorSaveHooks?: ReadonlyArray<BeforeRemoteCursorSaveHook<TVersion>>;
|
|
358
|
+
/**
|
|
359
|
+
* Optional codec hooks for full snapshots only.
|
|
360
|
+
*
|
|
361
|
+
* Both hooks must be provided together. If set, transport compresses the
|
|
362
|
+
* adapter snapshot before upload and decompresses downloaded snapshots before
|
|
363
|
+
* `applySnapshot()`.
|
|
364
|
+
*
|
|
365
|
+
* Callers must keep compatibility with any older snapshots already stored on
|
|
366
|
+
* the server.
|
|
367
|
+
*/
|
|
368
|
+
readonly snapshotCodec?: SnapshotCodec;
|
|
337
369
|
readonly snapshotUpload?: SnapshotUploadOptions;
|
|
338
370
|
readonly reconnectConfig?: Partial<ReconnectConfig>;
|
|
339
371
|
readonly debug?: boolean;
|
|
@@ -459,5 +491,5 @@ declare function createStreamUrl(input: {
|
|
|
459
491
|
baseUrl?: string;
|
|
460
492
|
}): string;
|
|
461
493
|
//#endregion
|
|
462
|
-
export {
|
|
463
|
-
//# sourceMappingURL=stream-id-
|
|
494
|
+
export { createInitialRemoteCursor as A, BeforeRemoteCursorSaveHook as C, RemoteCursor as D, IndexedDbRemoteCursorStoreOptions as E, RemoteCursorSaveSource as O, BeforeRemoteCursorSaveContext as S, IndexedDbRemoteCursorStore as T, TransportError as _, CrdtAdapter as a, TransportSubscription as b, JsonValue as c, SnapshotTransformHook as d, SnapshotUploadOptions as f, TransportDeleteStreamSuccess as g, TransportCreateStreamSuccess as h, StreamsCrdt as i, RemoteCursorStore as k, Result as l, StreamsCrdtOptions as m, isValidBucketId as n, CrdtUpdateBatch as o, StreamsAuthProvider as p, isValidRillId as r, JsonObject as s, createStreamUrl as t, SnapshotCodec as u, TransportJoinParams as v, InMemoryRemoteCursorStore as w, TransportSyncSuccess as x, TransportRoomStatus as y };
|
|
495
|
+
//# sourceMappingURL=stream-id-BfuQIfcB.d.ts.map
|
|
@@ -589,6 +589,18 @@ function normalizeSnapshotUploadConfig(config) {
|
|
|
589
589
|
minBytesSinceRemoteSnapshot: BigInt(Math.floor(minBytes))
|
|
590
590
|
};
|
|
591
591
|
}
|
|
592
|
+
function assertSnapshotBytes(value, label) {
|
|
593
|
+
if (value instanceof Uint8Array) return value;
|
|
594
|
+
throw new Error(`${label} must return Uint8Array`);
|
|
595
|
+
}
|
|
596
|
+
function normalizeSnapshotCodec(codec) {
|
|
597
|
+
if (codec == null) return;
|
|
598
|
+
if (typeof codec.compress !== "function" || typeof codec.decompress !== "function") throw new Error("snapshotCodec.compress and snapshotCodec.decompress must be provided together");
|
|
599
|
+
return {
|
|
600
|
+
compress: async (snapshot) => assertSnapshotBytes(await codec.compress(snapshot), "snapshotCodec.compress"),
|
|
601
|
+
decompress: async (snapshot) => assertSnapshotBytes(await codec.decompress(snapshot), "snapshotCodec.decompress")
|
|
602
|
+
};
|
|
603
|
+
}
|
|
592
604
|
function jsonValueEquals(a, b) {
|
|
593
605
|
if (a === b) return true;
|
|
594
606
|
if (typeof a !== typeof b || a == null || b == null) return false;
|
|
@@ -1003,14 +1015,16 @@ var StreamsTransportClient = class {
|
|
|
1003
1015
|
//#region src/transport-cursor.ts
|
|
1004
1016
|
var TransportCursorManager = class {
|
|
1005
1017
|
adapter;
|
|
1018
|
+
decodeSnapshot;
|
|
1006
1019
|
saveRemoteCursor;
|
|
1007
1020
|
constructor(options) {
|
|
1008
1021
|
this.adapter = options.adapter;
|
|
1022
|
+
this.decodeSnapshot = options.decodeSnapshot;
|
|
1009
1023
|
this.saveRemoteCursor = options.saveRemoteCursor;
|
|
1010
1024
|
}
|
|
1011
1025
|
async applyBootstrapPayload(cursor, snapshot, updates, nextOffset) {
|
|
1012
1026
|
let nextVersion = cursor.serverLowerBoundVersion;
|
|
1013
|
-
if (snapshot.byteLength > 0) nextVersion = await this.adapter.applySnapshot(snapshot);
|
|
1027
|
+
if (snapshot.byteLength > 0) nextVersion = await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot));
|
|
1014
1028
|
if (updates.length > 0) nextVersion = await this.adapter.applyRemoteUpdates([...updates], nextVersion);
|
|
1015
1029
|
return await this.saveRemoteCursor({
|
|
1016
1030
|
...cursor,
|
|
@@ -1194,6 +1208,7 @@ function createTransportSnapshotState() {
|
|
|
1194
1208
|
var TransportSnapshotManager = class {
|
|
1195
1209
|
adapter;
|
|
1196
1210
|
config;
|
|
1211
|
+
encodeSnapshot;
|
|
1197
1212
|
readSnapshotOffsetValue;
|
|
1198
1213
|
putSnapshotAt;
|
|
1199
1214
|
enqueueExclusive;
|
|
@@ -1203,6 +1218,7 @@ var TransportSnapshotManager = class {
|
|
|
1203
1218
|
constructor(options) {
|
|
1204
1219
|
this.adapter = options.adapter;
|
|
1205
1220
|
this.config = options.config;
|
|
1221
|
+
this.encodeSnapshot = options.encodeSnapshot;
|
|
1206
1222
|
this.readSnapshotOffsetValue = options.readSnapshotOffsetValue;
|
|
1207
1223
|
this.putSnapshotAt = options.putSnapshotAt;
|
|
1208
1224
|
this.enqueueExclusive = options.enqueueExclusive;
|
|
@@ -1273,7 +1289,7 @@ var TransportSnapshotManager = class {
|
|
|
1273
1289
|
if (tailBytes <= snapshotBytes) return;
|
|
1274
1290
|
if (tailBytes - snapshotBytes < config.minBytesSinceRemoteSnapshot) return;
|
|
1275
1291
|
const snapshot = this.adapter.exportSnapshot();
|
|
1276
|
-
await this.putSnapshotAt(state.cursor.nextOffset, snapshot);
|
|
1292
|
+
await this.putSnapshotAt(state.cursor.nextOffset, this.encodeSnapshot == null ? snapshot : await this.encodeSnapshot(snapshot));
|
|
1277
1293
|
} catch (error) {
|
|
1278
1294
|
this.logError("snapshot upload failed", error);
|
|
1279
1295
|
}
|
|
@@ -1367,6 +1383,7 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1367
1383
|
};
|
|
1368
1384
|
this.debug = options.debug ?? false;
|
|
1369
1385
|
this.beforeRemoteCursorSave = composeBeforeRemoteCursorSaveHooks(options.beforeRemoteCursorSave, ...options.beforeRemoteCursorSaveHooks ?? []);
|
|
1386
|
+
const snapshotCodec = normalizeSnapshotCodec(options.snapshotCodec);
|
|
1370
1387
|
const snapshotUploadConfig = normalizeSnapshotUploadConfig(options.snapshotUpload);
|
|
1371
1388
|
this.client = new StreamsTransportClient({
|
|
1372
1389
|
streamUrl: this.streamUrl,
|
|
@@ -1377,11 +1394,13 @@ var StreamsCrdt = class StreamsCrdt {
|
|
|
1377
1394
|
});
|
|
1378
1395
|
this.cursorManager = new TransportCursorManager({
|
|
1379
1396
|
adapter: this.adapter,
|
|
1397
|
+
decodeSnapshot: snapshotCodec?.decompress,
|
|
1380
1398
|
saveRemoteCursor: (cursor, source) => this.saveRemoteCursor(cursor, source)
|
|
1381
1399
|
});
|
|
1382
1400
|
this.snapshotManager = new TransportSnapshotManager({
|
|
1383
1401
|
adapter: this.adapter,
|
|
1384
1402
|
config: snapshotUploadConfig,
|
|
1403
|
+
encodeSnapshot: snapshotCodec?.compress,
|
|
1385
1404
|
readSnapshotOffsetValue: () => this.readSnapshotOffsetValue(),
|
|
1386
1405
|
putSnapshotAt: (snapshotOffset, snapshot) => this.putSnapshotAt(snapshotOffset, snapshot),
|
|
1387
1406
|
enqueueExclusive: (task) => this.enqueueExclusive(task),
|
|
@@ -2148,4 +2167,4 @@ function createStreamUrl(input) {
|
|
|
2148
2167
|
//#endregion
|
|
2149
2168
|
export { InMemoryRemoteCursorStore as a, StreamsCrdt as i, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
|
|
2150
2169
|
|
|
2151
|
-
//# sourceMappingURL=stream-id-
|
|
2170
|
+
//# sourceMappingURL=stream-id-YOFZ4I6Y.js.map
|