@loro-dev/streams-crdt 0.8.0 → 0.8.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
@@ -118,12 +118,30 @@ const sub = joined.value;
118
118
  checked out or detached, call `checkoutToLatest()` or `attach()` before passing
119
119
  it to `StreamsCrdt`.
120
120
 
121
+ ### Loro pending imports and cursor safety
122
+
123
+ Loro can accept an update whose dependencies are missing and keep it as a
124
+ pending import. Pending imports are not represented in `doc.version()` and are
125
+ not included in snapshots exported from that document. For that reason, the Loro
126
+ adapter must not persist a `RemoteCursor` until all pending imports are resolved
127
+ and the local document is complete at the cursor offset.
128
+
129
+ The transport may continue reading with an in-memory volatile cursor to find the
130
+ missing dependencies. If the server reports up-to-date while Loro still has
131
+ unresolved imports, the client must bootstrap again and only save the bootstrap
132
+ cursor after the batch import is clean. Snapshot upload is skipped while Loro has
133
+ unresolved imports.
134
+
135
+ See [`specs/loro-pending.md`](../../specs/loro-pending.md) for the full behavior
136
+ and implementation checklist.
137
+
121
138
  ## Quick Start (Loro EphemeralStore)
122
139
 
123
140
  Use a separate `EphemeralStreamCrdt` for presence-like Loro `EphemeralStore`
124
141
  data. It bootstraps through the first SSE event, posts local ephemeral updates,
125
- and periodically re-announces `store.encodeAll()` so reconnecting peers can
126
- recover the current live state without replaying old presence history.
142
+ and never republishes state imported from other peers. If a client needs to keep
143
+ its own presence alive, update that client's own keys with `store.set()` so the
144
+ store emits a fresh local update.
127
145
 
128
146
  Ephemeral live delivery is latest-state oriented. The server may keep only a
129
147
  tiny live fanout queue for current SSE subscribers and may disconnect slow
@@ -155,6 +173,65 @@ const joinedPresence = await presence.join();
155
173
  if (!joinedPresence.ok) throw joinedPresence.error;
156
174
  ```
157
175
 
176
+ ### Ephemeral stream URL format
177
+
178
+ An ephemeral stream URL is the durable stream URL for the same document plus a
179
+ channel query parameter:
180
+
181
+ ```txt
182
+ {baseUrl}/ds/{bucketId}/{streamId}?ephemeral={channel}
183
+ ```
184
+
185
+ For example, if the durable Flock document stream is:
186
+
187
+ ```txt
188
+ https://streams-api.loro.dev/ds/my-bucket/flock-cm9vbS0x
189
+ ```
190
+
191
+ then the matching presence stream is:
192
+
193
+ ```txt
194
+ https://streams-api.loro.dev/ds/my-bucket/flock-cm9vbS0x?ephemeral=presence
195
+ ```
196
+
197
+ Use the same `bucketId` and `streamId` as the durable `StreamsCrdt` room. The
198
+ `ephemeral` value names a side-channel for that room, such as `presence`,
199
+ `cursor`, or `selection`. Different channels under the same durable stream are
200
+ separate in-memory rooms. The compatibility spelling `?awareness=presence` is
201
+ also accepted, but new code should use `?ephemeral=presence`.
202
+
203
+ When an app exposes a room link such as `/rooms/design-review-42`, the link
204
+ normally maps to one durable stream id first, and both durable and ephemeral
205
+ sync derive from that same id:
206
+
207
+ ```ts
208
+ const roomId = "design-review-42";
209
+ const durableStreamUrl = createStreamUrl({
210
+ bucketId: "my-bucket",
211
+ streamId: `flock-${roomId}`,
212
+ });
213
+
214
+ const presenceStreamUrl = new URL(durableStreamUrl);
215
+ presenceStreamUrl.searchParams.set("ephemeral", "presence");
216
+
217
+ const flockTransport = new StreamsCrdt({
218
+ streamUrl: durableStreamUrl,
219
+ auth,
220
+ adapter: createFlockAdapter(flock),
221
+ });
222
+
223
+ const presenceTransport = new EphemeralStreamCrdt({
224
+ streamUrl: presenceStreamUrl.toString(),
225
+ auth,
226
+ adaptor: EphemeralStoreAdaptor(store),
227
+ });
228
+ ```
229
+
230
+ `EphemeralStreamCrdt` does not create or delete the durable stream. It posts
231
+ updates to the ephemeral URL and opens SSE on the same URL with `live=sse`.
232
+ Ephemeral state is not persisted with the Flock document stream; it is scoped by
233
+ the URL path plus channel and is meant for current live state only.
234
+
158
235
  ## Quick Start (Flock)
159
236
 
160
237
  ```ts
@@ -799,9 +876,13 @@ const transport = new StreamsCrdt({
799
876
  - `CrdtAdapter` — adapter contract interface
800
877
  - `IsolatedCrdtAdapter` — optional apply-only adapter used for safe 410 recovery
801
878
  - `CrdtUpdateBatch` — local update batch type
879
+ - `CrdtApplyOutcome`, `CrdtApplyReturn`, `CrdtUnresolvedSpan` — adapter apply
880
+ result types used to report incomplete imports such as Loro pending updates
802
881
  - `Result` — Rust-style `Ok | Err` result
803
882
  - `StreamsAuthProvider` — auth callback type
804
883
  - `TransportError` — discriminated error union
884
+ `apply_incomplete` is returned when remote data was read but the CRDT adapter
885
+ still has unresolved dependencies and the cursor cannot be saved.
805
886
  - `TransportCreateStreamSuccess`, `TransportDeleteStreamSuccess`, `TransportSyncSuccess` — result payloads
806
887
  - `TransportJoinParams` — `join()` options
807
888
  - `TransportSubscription` — active live subscription handle
package/dist/flock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as RemoteCursorStore, A as Result, B as TransportJoinParams, C as PayloadProtectionEncryptionOptions, D as PayloadProtectionReadPolicy, E as PayloadProtectionOptions, F as StreamsAuthProvider, G as WriteOnlyAppendResult, H as TransportSnapshotUploadSuccess, I as StreamsCrdtOptions, J as InMemoryRemoteCursorStore, K as BeforeRemoteCursorSaveContext, L as TransportCreateStreamSuccess, M as SnapshotTransformHook, N as SnapshotUploadOptions, O as PayloadProtectionScope, P as StreamsAuthContext, Q as RemoteCursorSaveSource, R as TransportDeleteStreamSuccess, S as JsonValue, T as PayloadProtectionKeyProvider, U as TransportSubscription, V as TransportRoomStatus, W as TransportSyncSuccess, X as IndexedDbRemoteCursorStoreOptions, Y as IndexedDbRemoteCursorStore, Z as RemoteCursor, _ as EphemeralStreamCrdtOptions, a as EphemeralStreamCrdt, b as IsolatedCrdtAdapter, c as CrdtUpdateBatch, d as E2eeKeyProvider, et as createInitialRemoteCursor, f as E2eeOptions, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as PayloadProtectionError, j as SnapshotCodec, k as PayloadProtectionWritePolicy, l as E2eeEncryptionOptions, m as E2eeScope, n as isValidBucketId, o as StreamsCrdt, p as E2eeReadPolicy, q as BeforeRemoteCursorSaveHook, r as isValidRillId, s as CrdtAdapter, t as createStreamUrl, u as E2eeKey, v as EphemeralStreamJoinParams, w as PayloadProtectionKey, x as JsonObject, y as EphemeralStreamSubscription, z as TransportError } from "./stream-id-BXJvwX_O.js";
1
+ import { $ as IndexedDbRemoteCursorStoreOptions, A as PayloadProtectionReadPolicy, B as TransportCreateStreamSuccess, C as IsolatedCrdtAdapter, D as PayloadProtectionKey, E as PayloadProtectionEncryptionOptions, F as SnapshotTransformHook, G as TransportSnapshotUploadSuccess, H as TransportError, I as SnapshotUploadOptions, J as WriteOnlyAppendResult, K as TransportSubscription, L as StreamsAuthContext, M as PayloadProtectionWritePolicy, N as Result, O as PayloadProtectionKeyProvider, P as SnapshotCodec, Q as IndexedDbRemoteCursorStore, R as StreamsAuthProvider, S as EphemeralStreamSubscription, T as JsonValue, U as TransportJoinParams, V as TransportDeleteStreamSuccess, W as TransportRoomStatus, X as BeforeRemoteCursorSaveHook, Y as BeforeRemoteCursorSaveContext, Z as InMemoryRemoteCursorStore, _ as E2eeScope, a as EphemeralStreamCrdt, b as EphemeralStreamCrdtOptions, c as CrdtApplyOutcome, d as CrdtUpdateBatch, et as RemoteCursor, f as E2eeEncryptionOptions, g as E2eeReadPolicy, h as E2eeOptions, i as PayloadProtectionError, j as PayloadProtectionScope, k as PayloadProtectionOptions, l as CrdtApplyReturn, m as E2eeKeyProvider, n as isValidBucketId, nt as RemoteCursorStore, o as StreamsCrdt, p as E2eeKey, q as TransportSyncSuccess, r as isValidRillId, rt as createInitialRemoteCursor, s as CrdtAdapter, t as createStreamUrl, tt as RemoteCursorSaveSource, u as CrdtUnresolvedSpan, v as E2eeWritePolicy, w as JsonObject, x as EphemeralStreamJoinParams, y as EphemeralStreamAdaptor, z as StreamsCrdtOptions } from "./stream-id-Bi5rAZfP.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, E2eeEncryptionOptions, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeKey, E2eeKeyProvider, E2eeOptions, E2eeReadPolicy, E2eeScope, E2eeWritePolicy, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, type ExportBundle, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, PayloadProtectionEncryptionOptions, PayloadProtectionKey, PayloadProtectionKeyProvider, PayloadProtectionOptions, PayloadProtectionReadPolicy, PayloadProtectionScope, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, type VersionVector, WriteOnlyAppendResult, createFlockAdapter, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
10
+ export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtApplyOutcome, CrdtApplyReturn, CrdtUnresolvedSpan, CrdtUpdateBatch, E2eeEncryptionOptions, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeKey, E2eeKeyProvider, E2eeOptions, E2eeReadPolicy, E2eeScope, E2eeWritePolicy, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, type ExportBundle, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, PayloadProtectionEncryptionOptions, PayloadProtectionKey, PayloadProtectionKeyProvider, PayloadProtectionOptions, PayloadProtectionReadPolicy, PayloadProtectionScope, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, type VersionVector, WriteOnlyAppendResult, createFlockAdapter, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
11
11
  //# sourceMappingURL=flock.d.ts.map
package/dist/flock.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CF1egQDz.js";
1
+ import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-BjmGvObf.js";
2
2
  import { Flock } from "@loro-dev/flock-wasm";
3
3
  //#region src/flock.ts
4
4
  function isAfter(entry, current) {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as RemoteCursorStore, A as Result, B as TransportJoinParams, C as PayloadProtectionEncryptionOptions, D as PayloadProtectionReadPolicy, E as PayloadProtectionOptions, F as StreamsAuthProvider, G as WriteOnlyAppendResult, H as TransportSnapshotUploadSuccess, I as StreamsCrdtOptions, J as InMemoryRemoteCursorStore, K as BeforeRemoteCursorSaveContext, L as TransportCreateStreamSuccess, M as SnapshotTransformHook, N as SnapshotUploadOptions, O as PayloadProtectionScope, P as StreamsAuthContext, Q as RemoteCursorSaveSource, R as TransportDeleteStreamSuccess, S as JsonValue, T as PayloadProtectionKeyProvider, U as TransportSubscription, V as TransportRoomStatus, W as TransportSyncSuccess, X as IndexedDbRemoteCursorStoreOptions, Y as IndexedDbRemoteCursorStore, Z as RemoteCursor, _ as EphemeralStreamCrdtOptions, a as EphemeralStreamCrdt, b as IsolatedCrdtAdapter, c as CrdtUpdateBatch, d as E2eeKeyProvider, et as createInitialRemoteCursor, f as E2eeOptions, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as PayloadProtectionError, j as SnapshotCodec, k as PayloadProtectionWritePolicy, l as E2eeEncryptionOptions, m as E2eeScope, n as isValidBucketId, o as StreamsCrdt, p as E2eeReadPolicy, q as BeforeRemoteCursorSaveHook, r as isValidRillId, s as CrdtAdapter, t as createStreamUrl, u as E2eeKey, v as EphemeralStreamJoinParams, w as PayloadProtectionKey, x as JsonObject, y as EphemeralStreamSubscription, z as TransportError } from "./stream-id-BXJvwX_O.js";
2
- export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, E2eeEncryptionOptions, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeKey, E2eeKeyProvider, E2eeOptions, E2eeReadPolicy, E2eeScope, E2eeWritePolicy, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, PayloadProtectionEncryptionOptions, PayloadProtectionKey, PayloadProtectionKeyProvider, PayloadProtectionOptions, PayloadProtectionReadPolicy, PayloadProtectionScope, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, WriteOnlyAppendResult, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
1
+ import { $ as IndexedDbRemoteCursorStoreOptions, A as PayloadProtectionReadPolicy, B as TransportCreateStreamSuccess, C as IsolatedCrdtAdapter, D as PayloadProtectionKey, E as PayloadProtectionEncryptionOptions, F as SnapshotTransformHook, G as TransportSnapshotUploadSuccess, H as TransportError, I as SnapshotUploadOptions, J as WriteOnlyAppendResult, K as TransportSubscription, L as StreamsAuthContext, M as PayloadProtectionWritePolicy, N as Result, O as PayloadProtectionKeyProvider, P as SnapshotCodec, Q as IndexedDbRemoteCursorStore, R as StreamsAuthProvider, S as EphemeralStreamSubscription, T as JsonValue, U as TransportJoinParams, V as TransportDeleteStreamSuccess, W as TransportRoomStatus, X as BeforeRemoteCursorSaveHook, Y as BeforeRemoteCursorSaveContext, Z as InMemoryRemoteCursorStore, _ as E2eeScope, a as EphemeralStreamCrdt, b as EphemeralStreamCrdtOptions, c as CrdtApplyOutcome, d as CrdtUpdateBatch, et as RemoteCursor, f as E2eeEncryptionOptions, g as E2eeReadPolicy, h as E2eeOptions, i as PayloadProtectionError, j as PayloadProtectionScope, k as PayloadProtectionOptions, l as CrdtApplyReturn, m as E2eeKeyProvider, n as isValidBucketId, nt as RemoteCursorStore, o as StreamsCrdt, p as E2eeKey, q as TransportSyncSuccess, r as isValidRillId, rt as createInitialRemoteCursor, s as CrdtAdapter, t as createStreamUrl, tt as RemoteCursorSaveSource, u as CrdtUnresolvedSpan, v as E2eeWritePolicy, w as JsonObject, x as EphemeralStreamJoinParams, y as EphemeralStreamAdaptor, z as StreamsCrdtOptions } from "./stream-id-Bi5rAZfP.js";
2
+ export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtApplyOutcome, CrdtApplyReturn, CrdtUnresolvedSpan, CrdtUpdateBatch, E2eeEncryptionOptions, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeKey, E2eeKeyProvider, E2eeOptions, E2eeReadPolicy, E2eeScope, E2eeWritePolicy, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, PayloadProtectionEncryptionOptions, PayloadProtectionKey, PayloadProtectionKeyProvider, PayloadProtectionOptions, PayloadProtectionReadPolicy, PayloadProtectionScope, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, WriteOnlyAppendResult, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CF1egQDz.js";
1
+ import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-BjmGvObf.js";
2
2
  export { PayloadProtectionError as E2eeError, PayloadProtectionError, EphemeralStreamCrdt, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, StreamsCrdt, createInitialRemoteCursor, createStreamUrl, isValidBucketId, isValidRillId };
package/dist/loro.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as RemoteCursorStore, A as Result, B as TransportJoinParams, C as PayloadProtectionEncryptionOptions, D as PayloadProtectionReadPolicy, E as PayloadProtectionOptions, F as StreamsAuthProvider, G as WriteOnlyAppendResult, H as TransportSnapshotUploadSuccess, I as StreamsCrdtOptions, J as InMemoryRemoteCursorStore, K as BeforeRemoteCursorSaveContext, L as TransportCreateStreamSuccess, M as SnapshotTransformHook, N as SnapshotUploadOptions, O as PayloadProtectionScope, P as StreamsAuthContext, Q as RemoteCursorSaveSource, R as TransportDeleteStreamSuccess, S as JsonValue, T as PayloadProtectionKeyProvider, U as TransportSubscription, V as TransportRoomStatus, W as TransportSyncSuccess, X as IndexedDbRemoteCursorStoreOptions, Y as IndexedDbRemoteCursorStore, Z as RemoteCursor, _ as EphemeralStreamCrdtOptions, a as EphemeralStreamCrdt, b as IsolatedCrdtAdapter, c as CrdtUpdateBatch, d as E2eeKeyProvider, et as createInitialRemoteCursor, f as E2eeOptions, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as PayloadProtectionError, j as SnapshotCodec, k as PayloadProtectionWritePolicy, l as E2eeEncryptionOptions, m as E2eeScope, n as isValidBucketId, o as StreamsCrdt, p as E2eeReadPolicy, q as BeforeRemoteCursorSaveHook, r as isValidRillId, s as CrdtAdapter, t as createStreamUrl, u as E2eeKey, v as EphemeralStreamJoinParams, w as PayloadProtectionKey, x as JsonObject, y as EphemeralStreamSubscription, z as TransportError } from "./stream-id-BXJvwX_O.js";
1
+ import { $ as IndexedDbRemoteCursorStoreOptions, A as PayloadProtectionReadPolicy, B as TransportCreateStreamSuccess, C as IsolatedCrdtAdapter, D as PayloadProtectionKey, E as PayloadProtectionEncryptionOptions, F as SnapshotTransformHook, G as TransportSnapshotUploadSuccess, H as TransportError, I as SnapshotUploadOptions, J as WriteOnlyAppendResult, K as TransportSubscription, L as StreamsAuthContext, M as PayloadProtectionWritePolicy, N as Result, O as PayloadProtectionKeyProvider, P as SnapshotCodec, Q as IndexedDbRemoteCursorStore, R as StreamsAuthProvider, S as EphemeralStreamSubscription, T as JsonValue, U as TransportJoinParams, V as TransportDeleteStreamSuccess, W as TransportRoomStatus, X as BeforeRemoteCursorSaveHook, Y as BeforeRemoteCursorSaveContext, Z as InMemoryRemoteCursorStore, _ as E2eeScope, a as EphemeralStreamCrdt, b as EphemeralStreamCrdtOptions, c as CrdtApplyOutcome, d as CrdtUpdateBatch, et as RemoteCursor, f as E2eeEncryptionOptions, g as E2eeReadPolicy, h as E2eeOptions, i as PayloadProtectionError, j as PayloadProtectionScope, k as PayloadProtectionOptions, l as CrdtApplyReturn, m as E2eeKeyProvider, n as isValidBucketId, nt as RemoteCursorStore, o as StreamsCrdt, p as E2eeKey, q as TransportSyncSuccess, r as isValidRillId, rt as createInitialRemoteCursor, s as CrdtAdapter, t as createStreamUrl, tt as RemoteCursorSaveSource, u as CrdtUnresolvedSpan, v as E2eeWritePolicy, w as JsonObject, x as EphemeralStreamJoinParams, y as EphemeralStreamAdaptor, z as StreamsCrdtOptions } from "./stream-id-Bi5rAZfP.js";
2
2
  import { LoroDoc } from "loro-crdt";
3
3
 
4
4
  //#region src/loro.d.ts
@@ -21,8 +21,6 @@ declare function createLoroDocAdapter(doc: LoroDoc): CrdtAdapter<JsonObject>;
21
21
  * Structural subset of Loro's `EphemeralStore`.
22
22
  */
23
23
  interface LoroEphemeralStoreLike {
24
- /** Encodes the current non-expired store state for bootstrap/lease publish. */
25
- encodeAll(): Uint8Array;
26
24
  /** Applies one opaque Loro ephemeral update. */
27
25
  apply(update: Uint8Array): void;
28
26
  /** Subscribes to locally produced ephemeral updates. */
@@ -43,5 +41,5 @@ interface LoroEphemeralStoreLike {
43
41
  declare function EphemeralStoreAdaptor(store: LoroEphemeralStoreLike): EphemeralStreamAdaptor;
44
42
  declare const createLoroEphemeralStoreAdaptor: typeof EphemeralStoreAdaptor;
45
43
  //#endregion
46
- export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtUpdateBatch, E2eeEncryptionOptions, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeKey, E2eeKeyProvider, E2eeOptions, E2eeReadPolicy, E2eeScope, E2eeWritePolicy, EphemeralStoreAdaptor, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, LoroEphemeralStoreLike, PayloadProtectionEncryptionOptions, PayloadProtectionKey, PayloadProtectionKeyProvider, PayloadProtectionOptions, PayloadProtectionReadPolicy, PayloadProtectionScope, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, WriteOnlyAppendResult, createInitialRemoteCursor, createLoroDocAdapter, createLoroEphemeralStoreAdaptor, createStreamUrl, isValidBucketId, isValidRillId };
44
+ export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtApplyOutcome, CrdtApplyReturn, CrdtUnresolvedSpan, CrdtUpdateBatch, E2eeEncryptionOptions, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeKey, E2eeKeyProvider, E2eeOptions, E2eeReadPolicy, E2eeScope, E2eeWritePolicy, EphemeralStoreAdaptor, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, LoroEphemeralStoreLike, PayloadProtectionEncryptionOptions, PayloadProtectionKey, PayloadProtectionKeyProvider, PayloadProtectionOptions, PayloadProtectionReadPolicy, PayloadProtectionScope, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, WriteOnlyAppendResult, createInitialRemoteCursor, createLoroDocAdapter, createLoroEphemeralStoreAdaptor, createStreamUrl, isValidBucketId, isValidRillId };
47
45
  //# sourceMappingURL=loro.d.ts.map
package/dist/loro.js CHANGED
@@ -1,9 +1,9 @@
1
- import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CF1egQDz.js";
1
+ import { a as InMemoryRemoteCursorStore, c as EphemeralStreamCrdt, i as StreamsCrdt, l as PayloadProtectionError, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-BjmGvObf.js";
2
2
  import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
4
  function batchImportUpdates(doc, updates) {
5
- if ("batchImport" in doc && typeof doc.batchImport === "function") doc.batchImport(updates);
6
- else doc.importBatch(updates);
5
+ if ("batchImport" in doc && typeof doc.batchImport === "function") return doc.batchImport(updates);
6
+ return doc.importBatch(updates);
7
7
  }
8
8
  function toDocVersionRecord(version) {
9
9
  const out = {};
@@ -39,6 +39,106 @@ function assertDocAttached(doc) {
39
39
  if (!doc.isDetached()) return;
40
40
  throw new Error("LoroDoc must be attached before using streams-crdt; call checkoutToLatest() or attach() first");
41
41
  }
42
+ function normalizePeerId(peer) {
43
+ return `${peer}`;
44
+ }
45
+ function isValidSpan(span) {
46
+ return Number.isSafeInteger(span.start) && Number.isSafeInteger(span.end) && span.start < span.end;
47
+ }
48
+ var UnresolvedSpanTracker = class {
49
+ spans = /* @__PURE__ */ new Map();
50
+ addAll(pending) {
51
+ if (pending == null) return;
52
+ for (const [peer, span] of pending) this.add(normalizePeerId(peer), span);
53
+ }
54
+ resolveAll(success) {
55
+ for (const [peer, span] of success) this.resolve(normalizePeerId(peer), span);
56
+ }
57
+ isEmpty() {
58
+ return this.spans.size === 0;
59
+ }
60
+ toArray() {
61
+ const out = [];
62
+ for (const [peer, spans] of this.spans) for (const span of spans) out.push({
63
+ peer,
64
+ start: span.start,
65
+ end: span.end
66
+ });
67
+ return out;
68
+ }
69
+ reset() {
70
+ this.spans.clear();
71
+ }
72
+ add(peer, incoming) {
73
+ if (!isValidSpan(incoming)) return;
74
+ const existing = this.spans.get(peer) ?? [];
75
+ existing.push({
76
+ start: incoming.start,
77
+ end: incoming.end
78
+ });
79
+ existing.sort((a, b) => a.start - b.start || a.end - b.end);
80
+ const merged = [];
81
+ for (const span of existing) {
82
+ const last = merged[merged.length - 1];
83
+ if (last == null || span.start > last.end) merged.push({
84
+ start: span.start,
85
+ end: span.end
86
+ });
87
+ else merged[merged.length - 1] = {
88
+ start: last.start,
89
+ end: Math.max(last.end, span.end)
90
+ };
91
+ }
92
+ this.spans.set(peer, merged);
93
+ }
94
+ resolve(peer, applied) {
95
+ if (!isValidSpan(applied)) return;
96
+ const existing = this.spans.get(peer);
97
+ if (existing == null) return;
98
+ const remaining = [];
99
+ for (const span of existing) {
100
+ if (applied.end <= span.start || applied.start >= span.end) {
101
+ remaining.push(span);
102
+ continue;
103
+ }
104
+ if (span.start < applied.start) remaining.push({
105
+ start: span.start,
106
+ end: applied.start
107
+ });
108
+ if (applied.end < span.end) remaining.push({
109
+ start: applied.end,
110
+ end: span.end
111
+ });
112
+ }
113
+ if (remaining.length === 0) this.spans.delete(peer);
114
+ else this.spans.set(peer, remaining);
115
+ }
116
+ };
117
+ function trackImportStatus(tracker, status) {
118
+ tracker.addAll(status.pending);
119
+ tracker.resolveAll(status.success);
120
+ }
121
+ function versionFromImportSuccess(status) {
122
+ const version = {};
123
+ for (const [peer, span] of status.success) {
124
+ if (!isValidSpan(span)) continue;
125
+ const peerId = normalizePeerId(peer);
126
+ version[peerId] = Math.max(version[peerId] ?? 0, span.end);
127
+ }
128
+ return version;
129
+ }
130
+ function versionFromSnapshotImport(snapshot, status, tracker) {
131
+ if (!tracker.isEmpty()) return versionFromImportSuccess(status);
132
+ return extractVersionFromUpdate(snapshot) ?? versionFromImportSuccess(status);
133
+ }
134
+ function applyOutcome(version, tracker) {
135
+ const unresolved = tracker.toArray();
136
+ return {
137
+ version,
138
+ complete: unresolved.length === 0,
139
+ ...unresolved.length === 0 ? {} : { unresolved }
140
+ };
141
+ }
42
142
  /**
43
143
  * Creates a `CrdtAdapter` for one `LoroDoc`.
44
144
  *
@@ -54,6 +154,7 @@ function assertDocAttached(doc) {
54
154
  * ```
55
155
  */
56
156
  function createLoroDocAdapter(doc) {
157
+ const unresolved = new UnresolvedSpanTracker();
57
158
  return {
58
159
  emptyVersion: () => ({}),
59
160
  mergeVersions: (base, incoming) => mergeDocVersions(base, incoming),
@@ -63,8 +164,10 @@ function createLoroDocAdapter(doc) {
63
164
  },
64
165
  applySnapshot: (snapshot) => {
65
166
  assertDocAttached(doc);
66
- doc.import(snapshot);
67
- return toDocVersionRecord(doc.version());
167
+ const status = doc.import(snapshot);
168
+ unresolved.reset();
169
+ trackImportStatus(unresolved, status);
170
+ return applyOutcome(versionFromSnapshotImport(snapshot, status, unresolved), unresolved);
68
171
  },
69
172
  exportUpdates: (fromVersion) => {
70
173
  assertDocAttached(doc);
@@ -86,30 +189,34 @@ function createLoroDocAdapter(doc) {
86
189
  },
87
190
  applyRemoteUpdates: (updates, remoterVersion) => {
88
191
  assertDocAttached(doc);
89
- if (updates.length > 0) batchImportUpdates(doc, updates);
90
- let version = remoterVersion;
91
- for (const update of updates) {
92
- const next = extractVersionFromUpdate(update);
93
- if (next != null) version = mergeDocVersions(version, next);
192
+ if (updates.length > 0) {
193
+ const status = batchImportUpdates(doc, updates);
194
+ trackImportStatus(unresolved, status);
195
+ return applyOutcome(mergeDocVersions(remoterVersion, versionFromImportSuccess(status)), unresolved);
94
196
  }
95
- return version;
197
+ return applyOutcome(remoterVersion, unresolved);
96
198
  },
97
199
  createIsolatedAdapter: () => {
98
200
  let isolatedDoc = new LoroDoc();
201
+ const isolatedUnresolved = new UnresolvedSpanTracker();
99
202
  return {
100
203
  applySnapshot: (snapshot) => {
101
204
  isolatedDoc = new LoroDoc();
102
- if (snapshot.byteLength > 0) isolatedDoc.import(snapshot);
103
- return toDocVersionRecord(isolatedDoc.version());
205
+ isolatedUnresolved.reset();
206
+ if (snapshot.byteLength > 0) {
207
+ const status = isolatedDoc.import(snapshot);
208
+ trackImportStatus(isolatedUnresolved, status);
209
+ return applyOutcome(versionFromSnapshotImport(snapshot, status, isolatedUnresolved), isolatedUnresolved);
210
+ }
211
+ return applyOutcome({}, isolatedUnresolved);
104
212
  },
105
213
  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);
214
+ if (updates.length > 0) {
215
+ const status = batchImportUpdates(isolatedDoc, updates);
216
+ trackImportStatus(isolatedUnresolved, status);
217
+ return applyOutcome(mergeDocVersions(remoterVersion, versionFromImportSuccess(status)), isolatedUnresolved);
111
218
  }
112
- return version;
219
+ return applyOutcome(remoterVersion, isolatedUnresolved);
113
220
  }
114
221
  };
115
222
  },
@@ -138,12 +245,25 @@ function createLoroDocAdapter(doc) {
138
245
  * ```
139
246
  */
140
247
  function EphemeralStoreAdaptor(store) {
248
+ let applyingRemoteDepth = 0;
141
249
  return {
142
- encodeAll: () => store.encodeAll(),
143
250
  applyRemoteUpdates: (updates) => {
144
- for (const update of updates) store.apply(update);
251
+ applyingRemoteDepth += 1;
252
+ try {
253
+ for (const update of updates) store.apply(update);
254
+ } finally {
255
+ applyingRemoteDepth -= 1;
256
+ }
145
257
  },
146
- subscribeLocalUpdates: (listener) => store.subscribeLocalUpdates(listener)
258
+ subscribeLocalUpdates: (listener) => {
259
+ const unsubscribeLocalUpdates = store.subscribeLocalUpdates((update) => {
260
+ if (applyingRemoteDepth > 0) return;
261
+ listener(update);
262
+ });
263
+ return () => {
264
+ unsubscribeLocalUpdates();
265
+ };
266
+ }
147
267
  };
148
268
  }
149
269
  const createLoroEphemeralStoreAdaptor = EphemeralStoreAdaptor;
package/dist/loro.js.map CHANGED
@@ -1 +1 @@
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\";\nimport type { EphemeralStreamAdaptor } from \"./types\";\n\nexport { EphemeralStreamCrdt } from \"./ephemeral-runtime\";\nexport { StreamsCrdt } from \"./transport-runtime\";\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\n/**\n * Structural subset of Loro's `EphemeralStore`.\n */\nexport interface LoroEphemeralStoreLike {\n /** Encodes the current non-expired store state for bootstrap/lease publish. */\n encodeAll(): Uint8Array;\n /** Applies one opaque Loro ephemeral update. */\n apply(update: Uint8Array): void;\n /** Subscribes to locally produced ephemeral updates. */\n subscribeLocalUpdates(listener: (update: Uint8Array) => void): () => void;\n}\n\n/**\n * Creates an ephemeral stream adaptor for a Loro `EphemeralStore`.\n *\n * @example\n * ```ts\n * const room = new EphemeralStreamCrdt({\n * streamUrl,\n * auth,\n * adaptor: EphemeralStoreAdaptor(store),\n * });\n * ```\n */\nexport function EphemeralStoreAdaptor(\n store: LoroEphemeralStoreLike,\n): EphemeralStreamAdaptor {\n return {\n encodeAll: () => store.encodeAll(),\n applyRemoteUpdates: (updates) => {\n for (const update of updates) {\n store.apply(update);\n }\n },\n subscribeLocalUpdates: (listener) =>\n store.subscribeLocalUpdates(listener),\n };\n}\n\nexport const createLoroEphemeralStoreAdaptor = EphemeralStoreAdaptor;\n"],"mappings":"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"}
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 {\n CrdtAdapter,\n CrdtApplyOutcome,\n CrdtUnresolvedSpan,\n JsonObject,\n} from \"./types\";\nimport type { EphemeralStreamAdaptor } from \"./types\";\n\nexport { EphemeralStreamCrdt } from \"./ephemeral-runtime\";\nexport { StreamsCrdt } from \"./transport-runtime\";\nexport * from \"./shared\";\n\ntype CounterSpan = {\n readonly start: number;\n readonly end: number;\n};\n\ntype LoroImportStatus = {\n readonly success: Map<string | number | bigint, CounterSpan>;\n readonly pending: Map<string | number | bigint, CounterSpan> | null;\n};\n\nfunction batchImportUpdates(\n doc: LoroDoc,\n updates: Uint8Array[],\n): LoroImportStatus {\n if (\"batchImport\" in doc && typeof doc.batchImport === \"function\") {\n return doc.batchImport(updates) as LoroImportStatus;\n }\n return doc.importBatch(updates) as LoroImportStatus;\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\nfunction normalizePeerId(peer: string | number | bigint): PeerId {\n return `${peer}` as PeerId;\n}\n\nfunction isValidSpan(span: CounterSpan): boolean {\n return (\n Number.isSafeInteger(span.start) &&\n Number.isSafeInteger(span.end) &&\n span.start < span.end\n );\n}\n\nclass UnresolvedSpanTracker {\n private readonly spans = new Map<PeerId, CounterSpan[]>();\n\n addAll(pending: LoroImportStatus[\"pending\"]): void {\n if (pending == null) {\n return;\n }\n for (const [peer, span] of pending) {\n this.add(normalizePeerId(peer), span);\n }\n }\n\n resolveAll(success: LoroImportStatus[\"success\"]): void {\n for (const [peer, span] of success) {\n this.resolve(normalizePeerId(peer), span);\n }\n }\n\n isEmpty(): boolean {\n return this.spans.size === 0;\n }\n\n toArray(): CrdtUnresolvedSpan[] {\n const out: CrdtUnresolvedSpan[] = [];\n for (const [peer, spans] of this.spans) {\n for (const span of spans) {\n out.push({ peer, start: span.start, end: span.end });\n }\n }\n return out;\n }\n\n reset(): void {\n this.spans.clear();\n }\n\n private add(peer: PeerId, incoming: CounterSpan): void {\n if (!isValidSpan(incoming)) {\n return;\n }\n const existing = this.spans.get(peer) ?? [];\n existing.push({ start: incoming.start, end: incoming.end });\n existing.sort((a, b) => a.start - b.start || a.end - b.end);\n const merged: CounterSpan[] = [];\n for (const span of existing) {\n const last = merged[merged.length - 1];\n if (last == null || span.start > last.end) {\n merged.push({ start: span.start, end: span.end });\n } else {\n merged[merged.length - 1] = {\n start: last.start,\n end: Math.max(last.end, span.end),\n };\n }\n }\n this.spans.set(peer, merged);\n }\n\n private resolve(peer: PeerId, applied: CounterSpan): void {\n if (!isValidSpan(applied)) {\n return;\n }\n const existing = this.spans.get(peer);\n if (existing == null) {\n return;\n }\n const remaining: CounterSpan[] = [];\n for (const span of existing) {\n if (applied.end <= span.start || applied.start >= span.end) {\n remaining.push(span);\n continue;\n }\n if (span.start < applied.start) {\n remaining.push({ start: span.start, end: applied.start });\n }\n if (applied.end < span.end) {\n remaining.push({ start: applied.end, end: span.end });\n }\n }\n if (remaining.length === 0) {\n this.spans.delete(peer);\n } else {\n this.spans.set(peer, remaining);\n }\n }\n}\n\nfunction trackImportStatus(\n tracker: UnresolvedSpanTracker,\n status: LoroImportStatus,\n): void {\n tracker.addAll(status.pending);\n tracker.resolveAll(status.success);\n}\n\nfunction versionFromImportSuccess(status: LoroImportStatus): DocVersionRecord {\n const version: DocVersionRecord = {};\n for (const [peer, span] of status.success) {\n if (!isValidSpan(span)) {\n continue;\n }\n const peerId = normalizePeerId(peer);\n version[peerId] = Math.max(version[peerId] ?? 0, span.end);\n }\n return version;\n}\n\nfunction versionFromSnapshotImport(\n snapshot: Uint8Array,\n status: LoroImportStatus,\n tracker: UnresolvedSpanTracker,\n): DocVersionRecord {\n if (!tracker.isEmpty()) {\n return versionFromImportSuccess(status);\n }\n return extractVersionFromUpdate(snapshot) ?? versionFromImportSuccess(status);\n}\n\nfunction applyOutcome(\n version: DocVersionRecord,\n tracker: UnresolvedSpanTracker,\n): CrdtApplyOutcome<JsonObject> {\n const unresolved = tracker.toArray();\n return {\n version,\n complete: unresolved.length === 0,\n ...(unresolved.length === 0 ? {} : { unresolved }),\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 const unresolved = new UnresolvedSpanTracker();\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 const status = doc.import(snapshot) as LoroImportStatus;\n unresolved.reset();\n trackImportStatus(unresolved, status);\n return applyOutcome(\n versionFromSnapshotImport(snapshot, status, unresolved),\n unresolved,\n );\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 const status = batchImportUpdates(doc, updates);\n trackImportStatus(unresolved, status);\n const version = mergeDocVersions(\n remoterVersion as DocVersionRecord,\n versionFromImportSuccess(status),\n );\n return applyOutcome(version, unresolved);\n }\n return applyOutcome(remoterVersion as DocVersionRecord, unresolved);\n },\n createIsolatedAdapter: () => {\n let isolatedDoc = new LoroDoc();\n const isolatedUnresolved = new UnresolvedSpanTracker();\n return {\n applySnapshot: (snapshot) => {\n isolatedDoc = new LoroDoc();\n isolatedUnresolved.reset();\n if (snapshot.byteLength > 0) {\n const status = isolatedDoc.import(snapshot) as LoroImportStatus;\n trackImportStatus(isolatedUnresolved, status);\n return applyOutcome(\n versionFromSnapshotImport(snapshot, status, isolatedUnresolved),\n isolatedUnresolved,\n );\n }\n return applyOutcome({}, isolatedUnresolved);\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n if (updates.length > 0) {\n const status = batchImportUpdates(isolatedDoc, updates);\n trackImportStatus(isolatedUnresolved, status);\n const version = mergeDocVersions(\n remoterVersion as DocVersionRecord,\n versionFromImportSuccess(status),\n );\n return applyOutcome(version, isolatedUnresolved);\n }\n return applyOutcome(\n remoterVersion as DocVersionRecord,\n isolatedUnresolved,\n );\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\n/**\n * Structural subset of Loro's `EphemeralStore`.\n */\nexport interface LoroEphemeralStoreLike {\n /** Applies one opaque Loro ephemeral update. */\n apply(update: Uint8Array): void;\n /** Subscribes to locally produced ephemeral updates. */\n subscribeLocalUpdates(listener: (update: Uint8Array) => void): () => void;\n}\n\n/**\n * Creates an ephemeral stream adaptor for a Loro `EphemeralStore`.\n *\n * @example\n * ```ts\n * const room = new EphemeralStreamCrdt({\n * streamUrl,\n * auth,\n * adaptor: EphemeralStoreAdaptor(store),\n * });\n * ```\n */\nexport function EphemeralStoreAdaptor(\n store: LoroEphemeralStoreLike,\n): EphemeralStreamAdaptor {\n let applyingRemoteDepth = 0;\n return {\n applyRemoteUpdates: (updates) => {\n applyingRemoteDepth += 1;\n try {\n for (const update of updates) {\n store.apply(update);\n }\n } finally {\n applyingRemoteDepth -= 1;\n }\n },\n subscribeLocalUpdates: (listener) => {\n const unsubscribeLocalUpdates = store.subscribeLocalUpdates((update) => {\n if (applyingRemoteDepth > 0) {\n return;\n }\n listener(update);\n });\n return () => {\n unsubscribeLocalUpdates();\n };\n },\n };\n}\n\nexport const createLoroEphemeralStoreAdaptor = EphemeralStoreAdaptor;\n"],"mappings":"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"}
@@ -176,6 +176,7 @@ type TransportRoomStatus = "joined" | "reconnecting" | "disconnected" | "error";
176
176
  * - `network_error` — fetch-level failure (DNS, connection refused, etc.).
177
177
  * - `timeout` — connect or poll deadline exceeded.
178
178
  * - `gone` — the server indicated the stream was deleted or moved (410).
179
+ * - `apply_incomplete` — a CRDT adapter could not fully apply remote data.
179
180
  * - `internal_error` — adapter or runtime logic failed locally.
180
181
  * - `unknown` — catch-all for unexpected errors.
181
182
  */
@@ -205,6 +206,11 @@ type TransportError = {
205
206
  readonly code: "gone";
206
207
  readonly retryable: true;
207
208
  readonly message: string;
209
+ } | {
210
+ readonly code: "apply_incomplete";
211
+ readonly retryable: true;
212
+ readonly reason: "loro_pending_updates";
213
+ readonly message: string;
208
214
  } | {
209
215
  readonly code: "internal_error";
210
216
  readonly retryable: false;
@@ -397,12 +403,10 @@ interface TransportSubscription {
397
403
  *
398
404
  * Ephemeral transports bootstrap through the first SSE event, then continue
399
405
  * with live deltas only. They do not expose checkpoints, snapshots, or
400
- * replayable offset history. `encodeAll()` is sent on join/reconnect/heartbeat
401
- * so peers can converge on the caller's current presence state.
406
+ * replayable offset history. Outbound data is limited to updates emitted by
407
+ * `subscribeLocalUpdates()` so imported remote presence is never republished.
402
408
  */
403
409
  interface EphemeralStreamAdaptor {
404
- /** Encodes all currently live local ephemeral state. */
405
- encodeAll(): MaybePromise<Uint8Array | null | undefined>;
406
410
  /** Applies updates received from the remote ephemeral stream. */
407
411
  applyRemoteUpdates(updates: readonly Uint8Array[]): MaybePromise<void>;
408
412
  /** Subscribes to local ephemeral updates. Returns an unsubscribe function. */
@@ -421,6 +425,23 @@ interface CrdtUpdateBatch<TVersion extends JsonObject> {
421
425
  readonly version: TVersion;
422
426
  }
423
427
  type ExportedCrdtUpdateBatch<TVersion extends JsonObject> = CrdtUpdateBatch<TVersion> | null | undefined;
428
+ interface CrdtUnresolvedSpan {
429
+ /** CRDT peer/client id that still has an unresolved counter span. */
430
+ readonly peer: string;
431
+ /** Inclusive start counter. */
432
+ readonly start: number;
433
+ /** Exclusive end counter. */
434
+ readonly end: number;
435
+ }
436
+ interface CrdtApplyOutcome<TVersion extends JsonObject> {
437
+ /** Inferred remote version that is actually applied to local durable CRDT state. */
438
+ readonly version: TVersion;
439
+ /** `false` means remote bytes were seen but cannot yet be durably checkpointed. */
440
+ readonly complete: boolean;
441
+ /** Adapter-specific unresolved dependency spans, when known. */
442
+ readonly unresolved?: readonly CrdtUnresolvedSpan[];
443
+ }
444
+ type CrdtApplyReturn<TVersion extends JsonObject> = TVersion | CrdtApplyOutcome<TVersion>;
424
445
  /**
425
446
  * Isolated apply-only adapter used for 410/bootstrap recovery.
426
447
  *
@@ -428,9 +449,9 @@ type ExportedCrdtUpdateBatch<TVersion extends JsonObject> = CrdtUpdateBatch<TVer
428
449
  */
429
450
  interface IsolatedCrdtAdapter<TVersion extends JsonObject> {
430
451
  /** Imports a full snapshot into isolated state and returns its version. */
431
- applySnapshot(snapshot: Uint8Array): MaybePromise<TVersion>;
452
+ applySnapshot(snapshot: Uint8Array): MaybePromise<CrdtApplyReturn<TVersion>>;
432
453
  /** Applies retained remote updates into isolated state and returns the new version. */
433
- applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<TVersion>;
454
+ applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<CrdtApplyReturn<TVersion>>;
434
455
  }
435
456
  /**
436
457
  * Adapter contract between `StreamsCrdt` and a concrete CRDT runtime.
@@ -443,7 +464,7 @@ interface CrdtAdapter<TVersion extends JsonObject> {
443
464
  /** Exports a full snapshot of the current local CRDT state. */
444
465
  exportSnapshot(): Uint8Array;
445
466
  /** Imports a full snapshot, replacing local state. Returns the version after import. */
446
- applySnapshot(snapshot: Uint8Array): MaybePromise<TVersion>;
467
+ applySnapshot(snapshot: Uint8Array): MaybePromise<CrdtApplyReturn<TVersion>>;
447
468
  /** Exports incremental updates since `fromVersion`. Returns `null`/`undefined` when there are no new updates. */
448
469
  exportUpdates(fromVersion: TVersion): ExportedCrdtUpdateBatch<TVersion>;
449
470
  /**
@@ -460,7 +481,7 @@ interface CrdtAdapter<TVersion extends JsonObject> {
460
481
  * invariant to distinguish self-replay from another writer advancing the
461
482
  * remote version.
462
483
  */
463
- applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<TVersion>;
484
+ applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<CrdtApplyReturn<TVersion>>;
464
485
  /**
465
486
  * Creates an isolated apply-only adapter for 410 recovery.
466
487
  *
@@ -503,11 +524,6 @@ interface EphemeralStreamCrdtOptions {
503
524
  readonly fetch?: (input: string, init?: RequestInit) => Promise<Response>;
504
525
  /** Reconnect policy for the SSE read loop and local POST retry loop. */
505
526
  readonly reconnectConfig?: Partial<ReconnectConfig>;
506
- /**
507
- * Interval for resending `adaptor.encodeAll()`. Set to `false` to disable.
508
- * Defaults to 15 seconds, matching presence-style leases.
509
- */
510
- readonly heartbeatMs?: number | false;
511
527
  /** Enables diagnostic console logging for terminal read/write failures. */
512
528
  readonly debug?: boolean;
513
529
  }
@@ -636,6 +652,7 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
636
652
  private nextProducerSeq;
637
653
  private frozenLocalAppend?;
638
654
  private committedPendingCount?;
655
+ private committedLocalStreamCursor?;
639
656
  private readonly pendingWriteOnly;
640
657
  private writeOnlyCursor;
641
658
  private unsubscribeWriteOnlyLocal?;
@@ -713,8 +730,11 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
713
730
  private bootstrapState;
714
731
  private catchup;
715
732
  private catchupOrGone;
733
+ private recoverLiveIncompleteRemoteApply;
716
734
  private appendLocalBatch;
717
735
  private appendLocalBatchDurably;
736
+ private localAppendReadCursor;
737
+ private catchupLocalAppend;
718
738
  private appendLocalBatchRemote;
719
739
  private getOrCreateFrozenLocalAppend;
720
740
  private appendFrozenLocalBatchRemote;
@@ -723,6 +743,7 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
723
743
  private commitProducerAck;
724
744
  private abandonFrozenLocalAppend;
725
745
  private consumeCommittedPendingCount;
746
+ private consumeCommittedLocalStreamCursor;
726
747
  /**
727
748
  * Ensures the target stream exists, creating it if necessary.
728
749
  * Tolerates 409 Conflict (stream already created concurrently).
@@ -789,8 +810,6 @@ declare class EphemeralStreamCrdt {
789
810
  private readonly client;
790
811
  /** Retry policy shared by the read loop and local write queue. */
791
812
  private readonly reconnectConfig;
792
- /** Period for republishing full local ephemeral state, or `false` to disable it. */
793
- private readonly heartbeatMs;
794
813
  /** Enables diagnostic console errors for failed read/write loops. */
795
814
  private readonly debug;
796
815
  /** Current shared subscription state, if the stream is joined. */
@@ -814,8 +833,6 @@ declare class EphemeralStreamCrdt {
814
833
  private resolveFirstOpen;
815
834
  private rejectFirstOpen;
816
835
  private runReadLoop;
817
- private startHeartbeat;
818
- private enqueueFullState;
819
836
  private enqueueLocal;
820
837
  private flushPendingLocal;
821
838
  private notifySyncWaiters;
@@ -856,5 +873,5 @@ declare function createStreamUrl(input: {
856
873
  baseUrl?: string;
857
874
  }): string;
858
875
  //#endregion
859
- export { RemoteCursorStore as $, Result as A, TransportJoinParams as B, PayloadProtectionEncryptionOptions as C, PayloadProtectionReadPolicy as D, PayloadProtectionOptions as E, StreamsAuthProvider as F, WriteOnlyAppendResult as G, TransportSnapshotUploadSuccess as H, StreamsCrdtOptions as I, InMemoryRemoteCursorStore as J, BeforeRemoteCursorSaveContext as K, TransportCreateStreamSuccess as L, SnapshotTransformHook as M, SnapshotUploadOptions as N, PayloadProtectionScope as O, StreamsAuthContext as P, RemoteCursorSaveSource as Q, TransportDeleteStreamSuccess as R, JsonValue as S, PayloadProtectionKeyProvider as T, TransportSubscription as U, TransportRoomStatus as V, TransportSyncSuccess as W, IndexedDbRemoteCursorStoreOptions as X, IndexedDbRemoteCursorStore as Y, RemoteCursor as Z, EphemeralStreamCrdtOptions as _, EphemeralStreamCrdt as a, IsolatedCrdtAdapter as b, CrdtUpdateBatch as c, E2eeKeyProvider as d, createInitialRemoteCursor as et, E2eeOptions as f, EphemeralStreamAdaptor as g, E2eeWritePolicy as h, PayloadProtectionError as i, SnapshotCodec as j, PayloadProtectionWritePolicy as k, E2eeEncryptionOptions as l, E2eeScope as m, isValidBucketId as n, StreamsCrdt as o, E2eeReadPolicy as p, BeforeRemoteCursorSaveHook as q, isValidRillId as r, CrdtAdapter as s, createStreamUrl as t, E2eeKey as u, EphemeralStreamJoinParams as v, PayloadProtectionKey as w, JsonObject as x, EphemeralStreamSubscription as y, TransportError as z };
860
- //# sourceMappingURL=stream-id-BXJvwX_O.d.ts.map
876
+ export { IndexedDbRemoteCursorStoreOptions as $, PayloadProtectionReadPolicy as A, TransportCreateStreamSuccess as B, IsolatedCrdtAdapter as C, PayloadProtectionKey as D, PayloadProtectionEncryptionOptions as E, SnapshotTransformHook as F, TransportSnapshotUploadSuccess as G, TransportError as H, SnapshotUploadOptions as I, WriteOnlyAppendResult as J, TransportSubscription as K, StreamsAuthContext as L, PayloadProtectionWritePolicy as M, Result as N, PayloadProtectionKeyProvider as O, SnapshotCodec as P, IndexedDbRemoteCursorStore as Q, StreamsAuthProvider as R, EphemeralStreamSubscription as S, JsonValue as T, TransportJoinParams as U, TransportDeleteStreamSuccess as V, TransportRoomStatus as W, BeforeRemoteCursorSaveHook as X, BeforeRemoteCursorSaveContext as Y, InMemoryRemoteCursorStore as Z, E2eeScope as _, EphemeralStreamCrdt as a, EphemeralStreamCrdtOptions as b, CrdtApplyOutcome as c, CrdtUpdateBatch as d, RemoteCursor as et, E2eeEncryptionOptions as f, E2eeReadPolicy as g, E2eeOptions as h, PayloadProtectionError as i, PayloadProtectionScope as j, PayloadProtectionOptions as k, CrdtApplyReturn as l, E2eeKeyProvider as m, isValidBucketId as n, RemoteCursorStore as nt, StreamsCrdt as o, E2eeKey as p, TransportSyncSuccess as q, isValidRillId as r, createInitialRemoteCursor as rt, CrdtAdapter as s, createStreamUrl as t, RemoteCursorSaveSource as tt, CrdtUnresolvedSpan as u, E2eeWritePolicy as v, JsonObject as w, EphemeralStreamJoinParams as x, EphemeralStreamAdaptor as y, StreamsCrdtOptions as z };
877
+ //# sourceMappingURL=stream-id-Bi5rAZfP.d.ts.map