@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 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
- - `FlockVersionVector` — Flock-specific version vector type
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 IndexedDbRemoteCursorStore, D as RemoteCursorStore, E as RemoteCursorSaveSource, O as createInitialRemoteCursor, S as InMemoryRemoteCursorStore, T as RemoteCursor, _ as TransportRoomStatus, a as CrdtAdapter, b as BeforeRemoteCursorSaveContext, c as JsonValue, d as StreamsAuthProvider, f as StreamsCrdtOptions, g as TransportJoinParams, h as TransportError, i as StreamsCrdt, l as Result, m as TransportDeleteStreamSuccess, n as isValidBucketId, o as CrdtUpdateBatch, p as TransportCreateStreamSuccess, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotUploadOptions, v as TransportSubscription, w as IndexedDbRemoteCursorStoreOptions, x as BeforeRemoteCursorSaveHook, y as TransportSyncSuccess } from "./stream-id-1f2mhtmk.js";
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: FlockLike): CrdtAdapter<JsonObject>;
8
+ declare function createFlockAdapter(flock: Flock): CrdtAdapter<VersionVector$1>;
34
9
  //#endregion
35
- export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, FlockVersionVector, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotUploadOptions, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createFlockAdapter, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
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-CIhmveJD.js";
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 toVersionVector(version) {
4
- const out = {};
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.logicalCounter > current.logicalCounter) {
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 raw of Object.values(bundle.entries)) {
44
- if (typeof raw !== "object" || raw == null || Array.isArray(raw)) continue;
45
- const entry = raw;
46
- if (typeof entry.peerId !== "string") continue;
47
- const logicalCounter = typeof entry.logicalCounter === "number" ? entry.logicalCounter : typeof entry.counter === "number" ? entry.counter : void 0;
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: (base, incoming) => fromVersionVector(mergeVersions(toVersionVector(base), toVersionVector(incoming))),
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
- if (snapshot.byteLength === 0) return {};
93
- const mergedVersion = tryMergeSnapshotFromFile(flock, snapshot);
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 from = toVersionVector(fromVersion);
101
- const bundle = flock.exportJson({ from });
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: fromVersionVector(exportedVersion)
68
+ version: exportedVersion
109
69
  };
110
70
  },
111
71
  applyRemoteUpdates: (updates, remoterVersion) => {
112
- let merged = toVersionVector(remoterVersion);
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 fromVersionVector(merged);
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 peerId = typeof flock.peerId === "function" ? flock.peerId() : void 0;
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(toVersionVector(fromVersionVector(flock.version() ?? {})));
90
+ recordExportFrontier(flock.version());
133
91
  listener({
134
92
  updates: [new TextEncoder().encode(JSON.stringify(batchPayload))],
135
- version: fromVersionVector(extractVersionFromBundle(batchPayload))
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 IndexedDbRemoteCursorStore, D as RemoteCursorStore, E as RemoteCursorSaveSource, O as createInitialRemoteCursor, S as InMemoryRemoteCursorStore, T as RemoteCursor, _ as TransportRoomStatus, a as CrdtAdapter, b as BeforeRemoteCursorSaveContext, c as JsonValue, d as StreamsAuthProvider, f as StreamsCrdtOptions, g as TransportJoinParams, h as TransportError, i as StreamsCrdt, l as Result, m as TransportDeleteStreamSuccess, n as isValidBucketId, o as CrdtUpdateBatch, p as TransportCreateStreamSuccess, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotUploadOptions, v as TransportSubscription, w as IndexedDbRemoteCursorStoreOptions, x as BeforeRemoteCursorSaveHook, y as TransportSyncSuccess } from "./stream-id-1f2mhtmk.js";
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-CIhmveJD.js";
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 IndexedDbRemoteCursorStore, D as RemoteCursorStore, E as RemoteCursorSaveSource, O as createInitialRemoteCursor, S as InMemoryRemoteCursorStore, T as RemoteCursor, _ as TransportRoomStatus, a as CrdtAdapter, b as BeforeRemoteCursorSaveContext, c as JsonValue, d as StreamsAuthProvider, f as StreamsCrdtOptions, g as TransportJoinParams, h as TransportError, i as StreamsCrdt, l as Result, m as TransportDeleteStreamSuccess, n as isValidBucketId, o as CrdtUpdateBatch, p as TransportCreateStreamSuccess, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotUploadOptions, v as TransportSubscription, w as IndexedDbRemoteCursorStoreOptions, x as BeforeRemoteCursorSaveHook, y as TransportSyncSuccess } from "./stream-id-1f2mhtmk.js";
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-CIhmveJD.js";
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 { IndexedDbRemoteCursorStore as C, RemoteCursorStore as D, RemoteCursorSaveSource as E, createInitialRemoteCursor as O, InMemoryRemoteCursorStore as S, RemoteCursor as T, TransportRoomStatus as _, CrdtAdapter as a, BeforeRemoteCursorSaveContext as b, JsonValue as c, StreamsAuthProvider as d, StreamsCrdtOptions as f, TransportJoinParams as g, TransportError as h, StreamsCrdt as i, Result as l, TransportDeleteStreamSuccess as m, isValidBucketId as n, CrdtUpdateBatch as o, TransportCreateStreamSuccess as p, isValidRillId as r, JsonObject as s, createStreamUrl as t, SnapshotUploadOptions as u, TransportSubscription as v, IndexedDbRemoteCursorStoreOptions as w, BeforeRemoteCursorSaveHook as x, TransportSyncSuccess as y };
463
- //# sourceMappingURL=stream-id-1f2mhtmk.d.ts.map
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-CIhmveJD.js.map
2170
+ //# sourceMappingURL=stream-id-YOFZ4I6Y.js.map