@loro-dev/streams-crdt 0.4.0 → 0.5.0

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
@@ -10,7 +10,7 @@ Transport layer for synchronizing CRDT state over
10
10
  | Class | Responsibility |
11
11
  | --- | --- |
12
12
  | `StreamsCrdt` | Transport runtime bound to **one** stream URL. Handles auth, reconnect, bootstrap, SSE/long-poll, binary framing, and remote cursor persistence. |
13
- | `CrdtAdapter` | Binding for **one** local CRDT instance. Handles snapshot export/import, version tracking, local update export, and remote update application. |
13
+ | `CrdtAdapter` | Binding for **one** local CRDT instance. Handles snapshot export/import, version tracking, local update export, remote update application, and optional isolated 410 recovery inference. |
14
14
 
15
15
  ### Operations
16
16
 
@@ -463,6 +463,7 @@ const transport = new StreamsCrdt({
463
463
  - `StreamsCrdt` — transport runtime class
464
464
  - `StreamsCrdtOptions` — constructor options
465
465
  - `CrdtAdapter` — adapter contract interface
466
+ - `IsolatedCrdtAdapter` — optional apply-only adapter used for safe 410 recovery
466
467
  - `CrdtUpdateBatch` — local update batch type
467
468
  - `Result` — Rust-style `Ok | Err` result
468
469
  - `StreamsAuthProvider` — auth callback type
package/dist/flock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as RemoteCursorStore, C as BeforeRemoteCursorSaveContext, D as IndexedDbRemoteCursorStoreOptions, E as IndexedDbRemoteCursorStore, O as RemoteCursor, S as TransportSyncSuccess, T as InMemoryRemoteCursorStore, _ as TransportDeleteStreamSuccess, a as CrdtAdapter, b as TransportRoomStatus, c as JsonValue, d as SnapshotTransformHook, f as SnapshotUploadOptions, g as TransportCreateStreamSuccess, h as StreamsCrdtOptions, i as StreamsCrdt, j as createInitialRemoteCursor, k as RemoteCursorSaveSource, l as Result, m as StreamsAuthProvider, n as isValidBucketId, o as CrdtUpdateBatch, p as StreamsAuthContext, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotCodec, v as TransportError, w as BeforeRemoteCursorSaveHook, x as TransportSubscription, y as TransportJoinParams } from "./stream-id-CSottEoY.js";
1
+ import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-BCCfzKNm.js";
2
2
  import { ExportBundle, Flock, VersionVector, VersionVector as VersionVector$1 } from "@loro-dev/flock-wasm";
3
3
 
4
4
  //#region src/flock.d.ts
@@ -7,5 +7,5 @@ import { ExportBundle, Flock, VersionVector, VersionVector as VersionVector$1 }
7
7
  */
8
8
  declare function createFlockAdapter(flock: Flock): CrdtAdapter<VersionVector$1>;
9
9
  //#endregion
10
- export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, type ExportBundle, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, type VersionVector, createFlockAdapter, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
10
+ export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, type ExportBundle, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, type VersionVector, createFlockAdapter, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
11
11
  //# sourceMappingURL=flock.d.ts.map
package/dist/flock.js CHANGED
@@ -1,4 +1,5 @@
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-TsPj50Jj.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-nPEdFo7c.js";
2
+ import { Flock } from "@loro-dev/flock-wasm";
2
3
  //#region src/flock.ts
3
4
  function isAfter(entry, current) {
4
5
  return entry.physicalTime > current.physicalTime || entry.physicalTime === current.physicalTime && entry.logicalCounter > current.logicalCounter;
@@ -41,6 +42,40 @@ function hasEntries(bundle) {
41
42
  return Object.keys(bundle.entries).length > 0;
42
43
  }
43
44
  /**
45
+ * Try to merge parsed bundles into a single combined bundle.
46
+ * Falls back to per-bundle import when any key appears in more than one
47
+ * bundle, so Flock's LWW conflict resolution sees every entry.
48
+ */
49
+ function batchImportBundles(importFn, bundles) {
50
+ if (bundles.length === 0) return;
51
+ if (bundles.length === 1) {
52
+ importFn(bundles[0]);
53
+ return;
54
+ }
55
+ const seenKeys = /* @__PURE__ */ new Set();
56
+ let hasCollision = false;
57
+ for (const bundle of bundles) {
58
+ for (const key of Object.keys(bundle.entries)) {
59
+ if (seenKeys.has(key)) {
60
+ hasCollision = true;
61
+ break;
62
+ }
63
+ seenKeys.add(key);
64
+ }
65
+ if (hasCollision) break;
66
+ }
67
+ if (hasCollision) {
68
+ for (const bundle of bundles) importFn(bundle);
69
+ return;
70
+ }
71
+ const combined = {
72
+ ...bundles[0],
73
+ entries: { ...bundles[0].entries }
74
+ };
75
+ for (let i = 1; i < bundles.length; i++) Object.assign(combined.entries, bundles[i].entries);
76
+ importFn(combined);
77
+ }
78
+ /**
44
79
  * Creates a `CrdtAdapter` for one Flock replica.
45
80
  */
46
81
  function createFlockAdapter(flock) {
@@ -70,14 +105,37 @@ function createFlockAdapter(flock) {
70
105
  },
71
106
  applyRemoteUpdates: (updates, remoterVersion) => {
72
107
  let merged = remoterVersion;
108
+ const bundles = [];
73
109
  for (const update of updates) {
74
110
  const bundle = JSON.parse(new TextDecoder().decode(update));
75
111
  if (!hasEntries(bundle)) continue;
76
- flock.importJson(bundle);
112
+ bundles.push(bundle);
77
113
  merged = mergeVersions(merged, extractVersionFromBundle(bundle));
78
114
  }
115
+ batchImportBundles((b) => flock.importJson(b), bundles);
79
116
  return merged;
80
117
  },
118
+ createIsolatedAdapter: () => {
119
+ let isolated = new Flock("streams-crdt-isolated");
120
+ return {
121
+ applySnapshot: (snapshot) => {
122
+ isolated = snapshot.byteLength === 0 ? new Flock("streams-crdt-isolated") : Flock.fromFile(snapshot, "streams-crdt-isolated");
123
+ return isolated.version();
124
+ },
125
+ applyRemoteUpdates: (updates, remoterVersion) => {
126
+ let merged = remoterVersion;
127
+ const bundles = [];
128
+ for (const update of updates) {
129
+ const bundle = JSON.parse(new TextDecoder().decode(update));
130
+ if (!hasEntries(bundle)) continue;
131
+ bundles.push(bundle);
132
+ merged = mergeVersions(merged, extractVersionFromBundle(bundle));
133
+ }
134
+ batchImportBundles((b) => isolated.importJson(b), bundles);
135
+ return merged;
136
+ }
137
+ };
138
+ },
81
139
  subscribeLocalUpdates: (listener) => {
82
140
  return flock.subscribe((batch) => {
83
141
  if (batch.source !== "local") return;
package/dist/flock.js.map CHANGED
@@ -1 +1 @@
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 const exportedVersion = extractVersionFromBundle(batchPayload);\n recordExportFrontier(exportedVersion);\n listener({\n updates: [new TextEncoder().encode(JSON.stringify(batchPayload))],\n version: exportedVersion,\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 * Try to merge parsed bundles into a single combined bundle.\n * Falls back to per-bundle import when any key appears in more than one\n * bundle, so Flock's LWW conflict resolution sees every entry.\n */\nfunction batchImportBundles(\n importFn: (bundle: ExportBundle) => void,\n bundles: ExportBundle[],\n): void {\n if (bundles.length === 0) {\n return;\n }\n if (bundles.length === 1) {\n importFn(bundles[0]!);\n return;\n }\n\n // Detect key collisions across bundles.\n const seenKeys = new Set<string>();\n let hasCollision = false;\n for (const bundle of bundles) {\n for (const key of Object.keys(bundle.entries)) {\n if (seenKeys.has(key)) {\n hasCollision = true;\n break;\n }\n seenKeys.add(key);\n }\n if (hasCollision) {\n break;\n }\n }\n\n if (hasCollision) {\n for (const bundle of bundles) {\n importFn(bundle);\n }\n return;\n }\n\n // No collisions — merge entries into the first bundle and import once.\n const combined: ExportBundle = {\n ...bundles[0]!,\n entries: { ...bundles[0]!.entries },\n };\n for (let i = 1; i < bundles.length; i++) {\n Object.assign(combined.entries, bundles[i]!.entries);\n }\n importFn(combined);\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 const bundles: ExportBundle[] = [];\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 bundles.push(bundle);\n merged = mergeVersions(merged, extractVersionFromBundle(bundle));\n }\n batchImportBundles((b) => flock.importJson(b), bundles);\n return merged;\n },\n createIsolatedAdapter: () => {\n let isolated = new Flock(\"streams-crdt-isolated\");\n return {\n applySnapshot: (snapshot) => {\n isolated =\n snapshot.byteLength === 0\n ? new Flock(\"streams-crdt-isolated\")\n : Flock.fromFile(snapshot, \"streams-crdt-isolated\");\n return isolated.version();\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n let merged = remoterVersion;\n const bundles: ExportBundle[] = [];\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 bundles.push(bundle);\n merged = mergeVersions(merged, extractVersionFromBundle(bundle));\n }\n batchImportBundles((b) => isolated.importJson(b), bundles);\n return merged;\n },\n };\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 const exportedVersion = extractVersionFromBundle(batchPayload);\n recordExportFrontier(exportedVersion);\n listener({\n updates: [new TextEncoder().encode(JSON.stringify(batchPayload))],\n version: exportedVersion,\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 { A as RemoteCursorStore, C as BeforeRemoteCursorSaveContext, D as IndexedDbRemoteCursorStoreOptions, E as IndexedDbRemoteCursorStore, O as RemoteCursor, S as TransportSyncSuccess, T as InMemoryRemoteCursorStore, _ as TransportDeleteStreamSuccess, a as CrdtAdapter, b as TransportRoomStatus, c as JsonValue, d as SnapshotTransformHook, f as SnapshotUploadOptions, g as TransportCreateStreamSuccess, h as StreamsCrdtOptions, i as StreamsCrdt, j as createInitialRemoteCursor, k as RemoteCursorSaveSource, l as Result, m as StreamsAuthProvider, n as isValidBucketId, o as CrdtUpdateBatch, p as StreamsAuthContext, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotCodec, v as TransportError, w as BeforeRemoteCursorSaveHook, x as TransportSubscription, y as TransportJoinParams } from "./stream-id-CSottEoY.js";
2
- export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
1
+ import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-BCCfzKNm.js";
2
+ export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, 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-TsPj50Jj.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-nPEdFo7c.js";
2
2
  export { InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, StreamsCrdt, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
package/dist/loro.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as RemoteCursorStore, C as BeforeRemoteCursorSaveContext, D as IndexedDbRemoteCursorStoreOptions, E as IndexedDbRemoteCursorStore, O as RemoteCursor, S as TransportSyncSuccess, T as InMemoryRemoteCursorStore, _ as TransportDeleteStreamSuccess, a as CrdtAdapter, b as TransportRoomStatus, c as JsonValue, d as SnapshotTransformHook, f as SnapshotUploadOptions, g as TransportCreateStreamSuccess, h as StreamsCrdtOptions, i as StreamsCrdt, j as createInitialRemoteCursor, k as RemoteCursorSaveSource, l as Result, m as StreamsAuthProvider, n as isValidBucketId, o as CrdtUpdateBatch, p as StreamsAuthContext, r as isValidRillId, s as JsonObject, t as createStreamUrl, u as SnapshotCodec, v as TransportError, w as BeforeRemoteCursorSaveHook, x as TransportSubscription, y as TransportJoinParams } from "./stream-id-CSottEoY.js";
1
+ import { A as RemoteCursorSaveSource, C as TransportSyncSuccess, D as IndexedDbRemoteCursorStore, E as InMemoryRemoteCursorStore, M as createInitialRemoteCursor, O as IndexedDbRemoteCursorStoreOptions, S as TransportSubscription, T as BeforeRemoteCursorSaveHook, _ as TransportCreateStreamSuccess, a as CrdtAdapter, b as TransportJoinParams, c as JsonObject, d as SnapshotCodec, f as SnapshotTransformHook, g as StreamsCrdtOptions, h as StreamsAuthProvider, i as StreamsCrdt, j as RemoteCursorStore, k as RemoteCursor, l as JsonValue, m as StreamsAuthContext, n as isValidBucketId, o as CrdtUpdateBatch, p as SnapshotUploadOptions, r as isValidRillId, s as IsolatedCrdtAdapter, t as createStreamUrl, u as Result, v as TransportDeleteStreamSuccess, w as BeforeRemoteCursorSaveContext, x as TransportRoomStatus, y as TransportError } from "./stream-id-BCCfzKNm.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, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSubscription, TransportSyncSuccess, createInitialRemoteCursor, createLoroDocAdapter, createStreamUrl, isValidBucketId, isValidRillId };
21
+ export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, 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,6 +1,10 @@
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-TsPj50Jj.js";
2
- import { VersionVector, decodeImportBlobMeta } from "loro-crdt";
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-nPEdFo7c.js";
2
+ import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
+ function batchImportUpdates(doc, updates) {
5
+ if ("batchImport" in doc && typeof doc.batchImport === "function") doc.batchImport(updates);
6
+ else doc.importBatch(updates);
7
+ }
4
8
  function toDocVersionRecord(version) {
5
9
  const out = {};
6
10
  for (const [peer, counter] of version.toJSON()) out[peer] = counter;
@@ -82,7 +86,7 @@ function createLoroDocAdapter(doc) {
82
86
  },
83
87
  applyRemoteUpdates: (updates, remoterVersion) => {
84
88
  assertDocAttached(doc);
85
- if (updates.length > 0) doc.importBatch(updates);
89
+ if (updates.length > 0) batchImportUpdates(doc, updates);
86
90
  let version = remoterVersion;
87
91
  for (const update of updates) {
88
92
  const next = extractVersionFromUpdate(update);
@@ -90,6 +94,25 @@ function createLoroDocAdapter(doc) {
90
94
  }
91
95
  return version;
92
96
  },
97
+ createIsolatedAdapter: () => {
98
+ let isolatedDoc = new LoroDoc();
99
+ return {
100
+ applySnapshot: (snapshot) => {
101
+ isolatedDoc = new LoroDoc();
102
+ if (snapshot.byteLength > 0) isolatedDoc.import(snapshot);
103
+ return toDocVersionRecord(isolatedDoc.version());
104
+ },
105
+ applyRemoteUpdates: (updates, remoterVersion) => {
106
+ if (updates.length > 0) batchImportUpdates(isolatedDoc, updates);
107
+ let version = remoterVersion;
108
+ for (const update of updates) {
109
+ const next = extractVersionFromUpdate(update);
110
+ if (next != null) version = mergeDocVersions(version, next);
111
+ }
112
+ return version;
113
+ }
114
+ };
115
+ },
93
116
  subscribeLocalUpdates: (listener) => {
94
117
  assertDocAttached(doc);
95
118
  return doc.subscribeLocalUpdates((update) => {
package/dist/loro.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"loro.js","names":[],"sources":["../src/loro.ts"],"sourcesContent":["import {\n decodeImportBlobMeta,\n type LoroDoc,\n VersionVector,\n} from \"loro-crdt\";\n\nimport type { CrdtAdapter, JsonObject } from \"./types\";\n\nexport { StreamsCrdt } from \"./transport\";\nexport * from \"./shared\";\n\ntype PeerId = `${number}`;\ntype DocVersionRecord = Record<PeerId, number>;\n\nfunction toDocVersionRecord(version: VersionVector): DocVersionRecord {\n const out = {} as DocVersionRecord;\n for (const [peer, counter] of version.toJSON()) {\n out[peer as PeerId] = counter;\n }\n return out;\n}\n\nfunction toDocVersionMap(version: JsonObject): Map<PeerId, number> {\n const out = new Map<PeerId, number>();\n for (const [peer, counter] of Object.entries(version)) {\n if (typeof counter === \"number\" && Number.isFinite(counter)) {\n out.set(peer as PeerId, counter);\n }\n }\n return out;\n}\n\nfunction mergeDocVersions(\n base: DocVersionRecord,\n incoming: DocVersionRecord,\n): DocVersionRecord {\n const merged = new Map<PeerId, number>();\n for (const [peer, counter] of Object.entries(base)) {\n merged.set(peer as PeerId, counter);\n }\n for (const [peer, counter] of Object.entries(incoming)) {\n const peerId = peer as PeerId;\n merged.set(peerId, Math.max(merged.get(peerId) ?? 0, counter));\n }\n return toDocVersionRecord(VersionVector.parseJSON(merged));\n}\n\nfunction hasUpdatesSince(\n fromVersion: DocVersionRecord,\n currentVersion: DocVersionRecord,\n): boolean {\n for (const [peer, counter] of Object.entries(currentVersion)) {\n if (counter > (fromVersion[peer as PeerId] ?? 0)) {\n return true;\n }\n }\n return false;\n}\n\nfunction extractVersionFromUpdate(update: Uint8Array): DocVersionRecord | null {\n try {\n const meta = decodeImportBlobMeta(update, false);\n return toDocVersionRecord(meta.partialEndVersionVector);\n } catch {\n return null;\n }\n}\n\nfunction assertDocAttached(doc: LoroDoc): void {\n if (!doc.isDetached()) {\n return;\n }\n throw new Error(\n \"LoroDoc must be attached before using streams-crdt; \" +\n \"call checkoutToLatest() or attach() first\",\n );\n}\n\n/**\n * Creates a `CrdtAdapter` for one `LoroDoc`.\n *\n * The `LoroDoc` must be attached (not detached or checked out to a historical\n * version). If the document is detached, call `doc.checkoutToLatest()` or\n * `doc.attach()` before passing it here.\n *\n * @example\n * ```ts\n * const doc = new LoroDoc();\n * const adapter = createLoroDocAdapter(doc);\n * const transport = new StreamsCrdt({ streamUrl, adapter, auth });\n * ```\n */\nexport function createLoroDocAdapter(\n doc: LoroDoc,\n): CrdtAdapter<JsonObject> {\n return {\n emptyVersion: () => ({}),\n mergeVersions: (base, incoming) =>\n mergeDocVersions(base as DocVersionRecord, incoming as DocVersionRecord),\n exportSnapshot: () => {\n assertDocAttached(doc);\n return doc.export({ mode: \"snapshot\" });\n },\n applySnapshot: (snapshot) => {\n assertDocAttached(doc);\n doc.import(snapshot);\n return toDocVersionRecord(doc.version());\n },\n exportUpdates: (fromVersion) => {\n assertDocAttached(doc);\n const currentVersion = toDocVersionRecord(doc.version());\n if (\n !hasUpdatesSince(\n fromVersion as DocVersionRecord,\n currentVersion,\n )\n ) {\n return undefined;\n }\n const updates = [\n doc.export({\n mode: \"update\",\n from: VersionVector.parseJSON(toDocVersionMap(fromVersion)),\n }),\n ].filter((update) => update.byteLength > 0);\n if (updates.length === 0) {\n return undefined;\n }\n let version: DocVersionRecord = {};\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return {\n updates,\n version,\n };\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n assertDocAttached(doc);\n if (updates.length > 0) {\n doc.importBatch(updates);\n }\n let version = remoterVersion as DocVersionRecord;\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return version;\n },\n subscribeLocalUpdates: (listener) => {\n assertDocAttached(doc);\n return doc.subscribeLocalUpdates((update) => {\n const version = extractVersionFromUpdate(update) ?? {};\n listener({ updates: [update], version });\n });\n },\n };\n}\n"],"mappings":";;;AAcA,SAAS,mBAAmB,SAA0C;CACpE,MAAM,MAAM,EAAE;AACd,MAAK,MAAM,CAAC,MAAM,YAAY,QAAQ,QAAQ,CAC5C,KAAI,QAAkB;AAExB,QAAO;;AAGT,SAAS,gBAAgB,SAA0C;CACjE,MAAM,sBAAM,IAAI,KAAqB;AACrC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,QAAQ,CACnD,KAAI,OAAO,YAAY,YAAY,OAAO,SAAS,QAAQ,CACzD,KAAI,IAAI,MAAgB,QAAQ;AAGpC,QAAO;;AAGT,SAAS,iBACP,MACA,UACkB;CAClB,MAAM,yBAAS,IAAI,KAAqB;AACxC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,KAAK,CAChD,QAAO,IAAI,MAAgB,QAAQ;AAErC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,SAAS,EAAE;EACtD,MAAM,SAAS;AACf,SAAO,IAAI,QAAQ,KAAK,IAAI,OAAO,IAAI,OAAO,IAAI,GAAG,QAAQ,CAAC;;AAEhE,QAAO,mBAAmB,cAAc,UAAU,OAAO,CAAC;;AAG5D,SAAS,gBACP,aACA,gBACS;AACT,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,eAAe,CAC1D,KAAI,WAAW,YAAY,SAAmB,GAC5C,QAAO;AAGX,QAAO;;AAGT,SAAS,yBAAyB,QAA6C;AAC7E,KAAI;AAEF,SAAO,mBADM,qBAAqB,QAAQ,MAAM,CACjB,wBAAwB;SACjD;AACN,SAAO;;;AAIX,SAAS,kBAAkB,KAAoB;AAC7C,KAAI,CAAC,IAAI,YAAY,CACnB;AAEF,OAAM,IAAI,MACR,gGAED;;;;;;;;;;;;;;;;AAiBH,SAAgB,qBACd,KACyB;AACzB,QAAO;EACL,qBAAqB,EAAE;EACvB,gBAAgB,MAAM,aACpB,iBAAiB,MAA0B,SAA6B;EAC1E,sBAAsB;AACpB,qBAAkB,IAAI;AACtB,UAAO,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;;EAEzC,gBAAgB,aAAa;AAC3B,qBAAkB,IAAI;AACtB,OAAI,OAAO,SAAS;AACpB,UAAO,mBAAmB,IAAI,SAAS,CAAC;;EAE1C,gBAAgB,gBAAgB;AAC9B,qBAAkB,IAAI;AAEtB,OACE,CAAC,gBACC,aAHmB,mBAAmB,IAAI,SAAS,CAAC,CAKrD,CAED;GAEF,MAAM,UAAU,CACd,IAAI,OAAO;IACT,MAAM;IACN,MAAM,cAAc,UAAU,gBAAgB,YAAY,CAAC;IAC5D,CAAC,CACH,CAAC,QAAQ,WAAW,OAAO,aAAa,EAAE;AAC3C,OAAI,QAAQ,WAAW,EACrB;GAEF,IAAI,UAA4B,EAAE;AAClC,QAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,OAAO,yBAAyB,OAAO;AAC7C,QAAI,QAAQ,KACV,WAAU,iBAAiB,SAAS,KAAK;;AAG7C,UAAO;IACL;IACA;IACD;;EAEH,qBAAqB,SAAS,mBAAmB;AAC/C,qBAAkB,IAAI;AACtB,OAAI,QAAQ,SAAS,EACnB,KAAI,YAAY,QAAQ;GAE1B,IAAI,UAAU;AACd,QAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,OAAO,yBAAyB,OAAO;AAC7C,QAAI,QAAQ,KACV,WAAU,iBAAiB,SAAS,KAAK;;AAG7C,UAAO;;EAET,wBAAwB,aAAa;AACnC,qBAAkB,IAAI;AACtB,UAAO,IAAI,uBAAuB,WAAW;IAC3C,MAAM,UAAU,yBAAyB,OAAO,IAAI,EAAE;AACtD,aAAS;KAAE,SAAS,CAAC,OAAO;KAAE;KAAS,CAAC;KACxC;;EAEL"}
1
+ {"version":3,"file":"loro.js","names":[],"sources":["../src/loro.ts"],"sourcesContent":["import {\n decodeImportBlobMeta,\n LoroDoc,\n VersionVector,\n} from \"loro-crdt\";\n\nimport type { CrdtAdapter, JsonObject } from \"./types\";\n\nexport { StreamsCrdt } from \"./transport\";\nexport * from \"./shared\";\n\nfunction batchImportUpdates(doc: LoroDoc, updates: Uint8Array[]): void {\n if (\"batchImport\" in doc && typeof doc.batchImport === \"function\") {\n doc.batchImport(updates);\n } else {\n doc.importBatch(updates);\n }\n}\n\ntype PeerId = `${number}`;\ntype DocVersionRecord = Record<PeerId, number>;\n\nfunction toDocVersionRecord(version: VersionVector): DocVersionRecord {\n const out = {} as DocVersionRecord;\n for (const [peer, counter] of version.toJSON()) {\n out[peer as PeerId] = counter;\n }\n return out;\n}\n\nfunction toDocVersionMap(version: JsonObject): Map<PeerId, number> {\n const out = new Map<PeerId, number>();\n for (const [peer, counter] of Object.entries(version)) {\n if (typeof counter === \"number\" && Number.isFinite(counter)) {\n out.set(peer as PeerId, counter);\n }\n }\n return out;\n}\n\nfunction mergeDocVersions(\n base: DocVersionRecord,\n incoming: DocVersionRecord,\n): DocVersionRecord {\n const merged = new Map<PeerId, number>();\n for (const [peer, counter] of Object.entries(base)) {\n merged.set(peer as PeerId, counter);\n }\n for (const [peer, counter] of Object.entries(incoming)) {\n const peerId = peer as PeerId;\n merged.set(peerId, Math.max(merged.get(peerId) ?? 0, counter));\n }\n return toDocVersionRecord(VersionVector.parseJSON(merged));\n}\n\nfunction hasUpdatesSince(\n fromVersion: DocVersionRecord,\n currentVersion: DocVersionRecord,\n): boolean {\n for (const [peer, counter] of Object.entries(currentVersion)) {\n if (counter > (fromVersion[peer as PeerId] ?? 0)) {\n return true;\n }\n }\n return false;\n}\n\nfunction extractVersionFromUpdate(update: Uint8Array): DocVersionRecord | null {\n try {\n const meta = decodeImportBlobMeta(update, false);\n return toDocVersionRecord(meta.partialEndVersionVector);\n } catch {\n return null;\n }\n}\n\nfunction assertDocAttached(doc: LoroDoc): void {\n if (!doc.isDetached()) {\n return;\n }\n throw new Error(\n \"LoroDoc must be attached before using streams-crdt; \" +\n \"call checkoutToLatest() or attach() first\",\n );\n}\n\n/**\n * Creates a `CrdtAdapter` for one `LoroDoc`.\n *\n * The `LoroDoc` must be attached (not detached or checked out to a historical\n * version). If the document is detached, call `doc.checkoutToLatest()` or\n * `doc.attach()` before passing it here.\n *\n * @example\n * ```ts\n * const doc = new LoroDoc();\n * const adapter = createLoroDocAdapter(doc);\n * const transport = new StreamsCrdt({ streamUrl, adapter, auth });\n * ```\n */\nexport function createLoroDocAdapter(\n doc: LoroDoc,\n): CrdtAdapter<JsonObject> {\n return {\n emptyVersion: () => ({}),\n mergeVersions: (base, incoming) =>\n mergeDocVersions(base as DocVersionRecord, incoming as DocVersionRecord),\n exportSnapshot: () => {\n assertDocAttached(doc);\n return doc.export({ mode: \"snapshot\" });\n },\n applySnapshot: (snapshot) => {\n assertDocAttached(doc);\n doc.import(snapshot);\n return toDocVersionRecord(doc.version());\n },\n exportUpdates: (fromVersion) => {\n assertDocAttached(doc);\n const currentVersion = toDocVersionRecord(doc.version());\n if (\n !hasUpdatesSince(\n fromVersion as DocVersionRecord,\n currentVersion,\n )\n ) {\n return undefined;\n }\n const updates = [\n doc.export({\n mode: \"update\",\n from: VersionVector.parseJSON(toDocVersionMap(fromVersion)),\n }),\n ].filter((update) => update.byteLength > 0);\n if (updates.length === 0) {\n return undefined;\n }\n let version: DocVersionRecord = {};\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return {\n updates,\n version,\n };\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n assertDocAttached(doc);\n if (updates.length > 0) {\n batchImportUpdates(doc, updates);\n }\n let version = remoterVersion as DocVersionRecord;\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return version;\n },\n createIsolatedAdapter: () => {\n let isolatedDoc = new LoroDoc();\n return {\n applySnapshot: (snapshot) => {\n isolatedDoc = new LoroDoc();\n if (snapshot.byteLength > 0) {\n isolatedDoc.import(snapshot);\n }\n return toDocVersionRecord(isolatedDoc.version());\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n if (updates.length > 0) {\n batchImportUpdates(isolatedDoc, updates);\n }\n let version = remoterVersion as DocVersionRecord;\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return version;\n },\n };\n },\n subscribeLocalUpdates: (listener) => {\n assertDocAttached(doc);\n return doc.subscribeLocalUpdates((update) => {\n const version = extractVersionFromUpdate(update) ?? {};\n listener({ updates: [update], version });\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -304,6 +304,17 @@ interface CrdtUpdateBatch<TVersion extends JsonObject> {
304
304
  readonly version: TVersion;
305
305
  }
306
306
  type ExportedCrdtUpdateBatch<TVersion extends JsonObject> = CrdtUpdateBatch<TVersion> | null | undefined;
307
+ /**
308
+ * Isolated apply-only adapter used for 410/bootstrap recovery.
309
+ *
310
+ * Implementations must not mutate the caller's live CRDT state.
311
+ */
312
+ interface IsolatedCrdtAdapter<TVersion extends JsonObject> {
313
+ /** Imports a full snapshot into isolated state and returns its version. */
314
+ applySnapshot(snapshot: Uint8Array): MaybePromise<TVersion>;
315
+ /** Applies retained remote updates into isolated state and returns the new version. */
316
+ applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<TVersion>;
317
+ }
307
318
  /**
308
319
  * Adapter contract between `StreamsCrdt` and a concrete CRDT runtime.
309
320
  */
@@ -333,6 +344,13 @@ interface CrdtAdapter<TVersion extends JsonObject> {
333
344
  * remote version.
334
345
  */
335
346
  applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<TVersion>;
347
+ /**
348
+ * Creates an isolated apply-only adapter for 410 recovery.
349
+ *
350
+ * When present, transport uses it to infer the remote version from
351
+ * bootstrap data without mutating the live CRDT instance first.
352
+ */
353
+ createIsolatedAdapter?(): MaybePromise<IsolatedCrdtAdapter<TVersion>>;
336
354
  /** Subscribes to local CRDT mutations. Returns an unsubscribe function. */
337
355
  subscribeLocalUpdates(listener: (batch: CrdtUpdateBatch<TVersion>) => void): () => void;
338
356
  /** Optional headers to include when creating the stream (e.g. retention config). */
@@ -422,6 +440,8 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
422
440
  private readonly createStreamIfMissing;
423
441
  private readonly client;
424
442
  private readonly cursorManager;
443
+ private readonly volatileCursorManager;
444
+ private readonly decodeSnapshot?;
425
445
  private readonly reconnectConfig;
426
446
  private readonly debug;
427
447
  private readonly beforeRemoteCursorSave?;
@@ -431,6 +451,11 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
431
451
  private nextProducerSeq;
432
452
  private joinState?;
433
453
  private opQueue;
454
+ /**
455
+ * Internal catch-up result used when callers need to handle 410 by
456
+ * preserving local state before rebuilding through bootstrap.
457
+ */
458
+ private static readonly GONE_CATCHUP;
434
459
  /**
435
460
  * Creates a transport for one stream URL and one local adapter instance.
436
461
  */
@@ -474,12 +499,18 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
474
499
  private startJoin;
475
500
  private syncOnce;
476
501
  private performInitialJoinSync;
477
- private bootstrapIntoState;
502
+ private createInitialCursor;
503
+ private resolveInitialRemoteState;
504
+ private bootstrapState;
478
505
  private catchup;
506
+ private catchupOrGone;
479
507
  private appendLocalBatch;
480
508
  private exportUpdates;
481
509
  private applyBootstrapItems;
482
510
  private applyRemoteItems;
511
+ private createCursorManager;
512
+ private createIsolatedCursorManager;
513
+ private recoverFromGoneWithLocalBootstrap;
483
514
  private runReadLoop;
484
515
  /** Maximum encoded size (bytes) for a single batched append. */
485
516
  private static readonly MAX_BATCH_BYTES;
@@ -524,5 +555,5 @@ declare function createStreamUrl(input: {
524
555
  baseUrl?: string;
525
556
  }): string;
526
557
  //#endregion
527
- export { RemoteCursorStore as A, BeforeRemoteCursorSaveContext as C, IndexedDbRemoteCursorStoreOptions as D, IndexedDbRemoteCursorStore as E, RemoteCursor as O, TransportSyncSuccess as S, InMemoryRemoteCursorStore as T, TransportDeleteStreamSuccess as _, CrdtAdapter as a, TransportRoomStatus as b, JsonValue as c, SnapshotTransformHook as d, SnapshotUploadOptions as f, TransportCreateStreamSuccess as g, StreamsCrdtOptions as h, StreamsCrdt as i, createInitialRemoteCursor as j, RemoteCursorSaveSource as k, Result as l, StreamsAuthProvider as m, isValidBucketId as n, CrdtUpdateBatch as o, StreamsAuthContext as p, isValidRillId as r, JsonObject as s, createStreamUrl as t, SnapshotCodec as u, TransportError as v, BeforeRemoteCursorSaveHook as w, TransportSubscription as x, TransportJoinParams as y };
528
- //# sourceMappingURL=stream-id-CSottEoY.d.ts.map
558
+ export { RemoteCursorSaveSource as A, TransportSyncSuccess as C, IndexedDbRemoteCursorStore as D, InMemoryRemoteCursorStore as E, createInitialRemoteCursor as M, IndexedDbRemoteCursorStoreOptions as O, TransportSubscription as S, BeforeRemoteCursorSaveHook as T, TransportCreateStreamSuccess as _, CrdtAdapter as a, TransportJoinParams as b, JsonObject as c, SnapshotCodec as d, SnapshotTransformHook as f, StreamsCrdtOptions as g, StreamsAuthProvider as h, StreamsCrdt as i, RemoteCursorStore as j, RemoteCursor as k, JsonValue as l, StreamsAuthContext as m, isValidBucketId as n, CrdtUpdateBatch as o, SnapshotUploadOptions as p, isValidRillId as r, IsolatedCrdtAdapter as s, createStreamUrl as t, Result as u, TransportDeleteStreamSuccess as v, BeforeRemoteCursorSaveContext as w, TransportRoomStatus as x, TransportError as y };
559
+ //# sourceMappingURL=stream-id-BCCfzKNm.d.ts.map