@loro-dev/streams-crdt 0.9.1 → 0.11.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
@@ -19,6 +19,7 @@ Transport layer for synchronizing CRDT state over
19
19
  | `createStream()` | Creates the stream on the server. Returns `{ created: true }` on first call, `{ created: false }` if it already exists. |
20
20
  | `deleteStream()` | Deletes the stream. Closes any active `join()` first. |
21
21
  | `sync()` | One-shot bootstrap/catch-up cycle. Syncs remote state into local CRDT and pushes any local changes to the server. Does **not** enter live mode. |
22
+ | `catchup()` | Pull-only remote catch-up. Applies remote stream updates to the local CRDT but does not intentionally append new local updates. If live reads from `join()` are already healthy, returns immediately unless `{ force: true }` is passed. |
22
23
  | `join()` | Initial sync **plus** live subscription. Internally performs the same sync as `sync()`, then enters a persistent SSE/long-poll read loop and starts forwarding local writes. No need to call `sync()` before `join()`. |
23
24
  | `close()` | Closes any active join and releases resources. If this instance was using `appendWriteOnly()`, that write-only path is also torn down and cannot be used again on the same instance. |
24
25
 
@@ -107,7 +108,8 @@ if (err.code === "server_error") {
107
108
 
108
109
  #### Operation-level retries vs. live-mode retries
109
110
 
110
- - `sync()`, `createStream()`, `deleteStream()`, `appendWriteOnly()` return
111
+ - `sync()`, `catchup()`, `createStream()`, `deleteStream()`,
112
+ `appendWriteOnly()` return
111
113
  after a **single** attempt. The application is responsible for retrying
112
114
  `retryable` errors.
113
115
  - After `join()` succeeds, the live read/write loops automatically reconnect
@@ -189,6 +191,47 @@ const sub = joined.value;
189
191
  checked out or detached, call `checkoutToLatest()` or `attach()` before passing
190
192
  it to `StreamsCrdt`.
191
193
 
194
+ ### Request shard origins
195
+
196
+ Browsers can still show head-of-line blocking symptoms when large
197
+ bootstrap/catch-up downloads and normal app requests contend on the same
198
+ connection. `shardUrls` lets the transport replace only the request origin for
199
+ selected operations while preserving the stream path and query:
200
+
201
+ ```ts
202
+ const transport = new StreamsCrdt({
203
+ streamUrl,
204
+ auth: async () => "<gateway-jwt>",
205
+ adapter: createLoroDocAdapter(doc),
206
+ shardUrls: {
207
+ bootstrap: [
208
+ "https://control-a.streams-api-proxy.loro.dev",
209
+ "https://control-b.streams-api-proxy.loro.dev",
210
+ ],
211
+ catchup: [
212
+ "https://control-a.streams-api-proxy.loro.dev",
213
+ "https://control-b.streams-api-proxy.loro.dev",
214
+ ],
215
+ largePost: [
216
+ "https://write-a.streams-api-proxy.loro.dev",
217
+ "https://write-b.streams-api-proxy.loro.dev",
218
+ ],
219
+ largePostMinBytes: 64 * 1024,
220
+ },
221
+ });
222
+ ```
223
+
224
+ Shard entries must be origin URLs with no path, query, or hash. Each transport
225
+ instance rotates through the configured origins per operation, with a randomized
226
+ starting point so multiple tabs are less likely to all begin on the same shard.
227
+ Bootstrap applies to `GET <stream>/bootstrap`; catch-up applies to non-SSE
228
+ offset reads, including long-poll fallback; SSE live reads stay on the original
229
+ `streamUrl`. `largePost` applies to non-empty append POST bodies at or above
230
+ `largePostMinBytes` bytes. The default threshold is `0`, so every non-empty
231
+ append POST can use `largePost` when that pool is configured. Verify actual
232
+ connection separation with browser NetLog: browsers may coalesce compatible
233
+ HTTP/2 or HTTP/3 origins when DNS, certificate, and transport settings allow it.
234
+
192
235
  ### Loro pending imports and cursor safety
193
236
 
194
237
  Loro can accept an update whose dependencies are missing and keep it as a
@@ -330,17 +373,47 @@ if (!joined.ok) throw joined.error;
330
373
  const sub = joined.value;
331
374
  ```
332
375
 
333
- ## When to Use `sync()` vs `join()`
376
+ ## When to Use `catchup()`, `sync()`, and `join()`
377
+
378
+ Use **`catchup()`** when you only want to pull remote updates into the local
379
+ CRDT:
380
+
381
+ ```ts
382
+ // Pull remote changes without uploading local pending changes.
383
+ const caughtUp = await transport.catchup();
384
+ if (!caughtUp.ok) throw caughtUp.error;
385
+ // caughtUp.value.cursor contains the replay progress after the pull.
386
+ ```
334
387
 
335
- Use **`sync()`** when you need a one-shot catch-up without entering live mode:
388
+ `catchup()` is safe to call while a room is live. When `join()` has a healthy
389
+ SSE/long-poll read loop, `catchup()` returns immediately because the live loop
390
+ is already catching up continuously. Pass `{ force: true }` to issue one
391
+ explicit non-SSE offset read anyway, for example after app foreground or device
392
+ wake:
336
393
 
337
394
  ```ts
338
- // Offline-first: catch up on app start, then work offline.
395
+ await transport.catchup({ force: true });
396
+ ```
397
+
398
+ If the live read side of the room is reconnecting, disconnected, or in error,
399
+ `catchup()` skips the live backoff path and tries one immediate non-SSE
400
+ catch-up read from the room's current cursor. A successful call updates the
401
+ local cursor and nudges the live loops to reconnect, but it does not by itself
402
+ guarantee that local pending writes have reached the server.
403
+
404
+ Use **`sync()`** when you need a one-shot bidirectional sync without entering
405
+ live mode:
406
+
407
+ ```ts
408
+ // Offline-first: pull remote changes, upload local changes, then work offline.
339
409
  const synced = await transport.sync();
340
410
  if (!synced.ok) throw synced.error;
341
411
  // synced.value.cursor contains the replay progress after sync.
342
412
  ```
343
413
 
414
+ Use `sync()` or `subscription.waitUntilSynced()` when the caller needs local
415
+ pending changes uploaded to the server.
416
+
344
417
  Use **`join()`** for real-time collaboration. It does everything `sync()` does
345
418
  and then keeps the connection alive for continuous bidirectional sync:
346
419
 
@@ -980,7 +1053,9 @@ const transport = new StreamsCrdt({
980
1053
  - `TransportError` — discriminated error union
981
1054
  `apply_incomplete` is returned when remote data was read but the CRDT adapter
982
1055
  still has unresolved dependencies and the cursor cannot be saved.
983
- - `TransportCreateStreamSuccess`, `TransportDeleteStreamSuccess`, `TransportSyncSuccess` — result payloads
1056
+ - `TransportCatchupParams` — `catchup()` options
1057
+ - `TransportCreateStreamSuccess`, `TransportDeleteStreamSuccess`,
1058
+ `TransportCatchupSuccess`, `TransportSyncSuccess` — result payloads
984
1059
  - `TransportJoinParams` — `join()` options
985
1060
  - `TransportSubscription` — active live subscription handle
986
1061
  - `TransportRoomStatus` — `"joined" | "reconnecting" | "disconnected" | "error"`
package/dist/flock.d.ts CHANGED
@@ -1,4 +1,4 @@
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-JuJ1JqCX.js";
1
+ import { $ as BeforeRemoteCursorSaveHook, A as PayloadProtectionReadPolicy, B as StreamsCrdtShardUrlsOptions, C as IsolatedCrdtAdapter, D as PayloadProtectionKey, E as PayloadProtectionEncryptionOptions, F as SnapshotTransformHook, G as TransportError, H as TransportCatchupSuccess, I as SnapshotUploadOptions, J as TransportSnapshotUploadSuccess, K as TransportJoinParams, L as StreamsAuthContext, M as PayloadProtectionWritePolicy, N as Result, O as PayloadProtectionKeyProvider, P as SnapshotCodec, Q as BeforeRemoteCursorSaveContext, R as StreamsAuthProvider, S as EphemeralStreamSubscription, T as JsonValue, U as TransportCreateStreamSuccess, V as TransportCatchupParams, W as TransportDeleteStreamSuccess, X as TransportSyncSuccess, Y as TransportSubscription, Z as WriteOnlyAppendResult, _ as E2eeScope, a as EphemeralStreamCrdt, at as RemoteCursorStore, b as EphemeralStreamCrdtOptions, c as CrdtApplyOutcome, d as CrdtUpdateBatch, et as InMemoryRemoteCursorStore, f as E2eeEncryptionOptions, g as E2eeReadPolicy, h as E2eeOptions, i as PayloadProtectionError, it as RemoteCursorSaveSource, j as PayloadProtectionScope, k as PayloadProtectionOptions, l as CrdtApplyReturn, m as E2eeKeyProvider, n as isValidBucketId, nt as IndexedDbRemoteCursorStoreOptions, o as StreamsCrdt, ot as createInitialRemoteCursor, p as E2eeKey, q as TransportRoomStatus, r as isValidRillId, rt as RemoteCursor, s as CrdtAdapter, t as createStreamUrl, tt as IndexedDbRemoteCursorStore, u as CrdtUnresolvedSpan, v as E2eeWritePolicy, w as JsonObject, x as EphemeralStreamJoinParams, y as EphemeralStreamAdaptor, z as StreamsCrdtOptions } from "./stream-id-DAAMJlvT.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, 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 };
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, StreamsCrdtShardUrlsOptions, TransportCatchupParams, TransportCatchupSuccess, 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-2eJsM9CJ.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-fOF9ZqCH.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 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-JuJ1JqCX.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 };
1
+ import { $ as BeforeRemoteCursorSaveHook, A as PayloadProtectionReadPolicy, B as StreamsCrdtShardUrlsOptions, C as IsolatedCrdtAdapter, D as PayloadProtectionKey, E as PayloadProtectionEncryptionOptions, F as SnapshotTransformHook, G as TransportError, H as TransportCatchupSuccess, I as SnapshotUploadOptions, J as TransportSnapshotUploadSuccess, K as TransportJoinParams, L as StreamsAuthContext, M as PayloadProtectionWritePolicy, N as Result, O as PayloadProtectionKeyProvider, P as SnapshotCodec, Q as BeforeRemoteCursorSaveContext, R as StreamsAuthProvider, S as EphemeralStreamSubscription, T as JsonValue, U as TransportCreateStreamSuccess, V as TransportCatchupParams, W as TransportDeleteStreamSuccess, X as TransportSyncSuccess, Y as TransportSubscription, Z as WriteOnlyAppendResult, _ as E2eeScope, a as EphemeralStreamCrdt, at as RemoteCursorStore, b as EphemeralStreamCrdtOptions, c as CrdtApplyOutcome, d as CrdtUpdateBatch, et as InMemoryRemoteCursorStore, f as E2eeEncryptionOptions, g as E2eeReadPolicy, h as E2eeOptions, i as PayloadProtectionError, it as RemoteCursorSaveSource, j as PayloadProtectionScope, k as PayloadProtectionOptions, l as CrdtApplyReturn, m as E2eeKeyProvider, n as isValidBucketId, nt as IndexedDbRemoteCursorStoreOptions, o as StreamsCrdt, ot as createInitialRemoteCursor, p as E2eeKey, q as TransportRoomStatus, r as isValidRillId, rt as RemoteCursor, s as CrdtAdapter, t as createStreamUrl, tt as IndexedDbRemoteCursorStore, u as CrdtUnresolvedSpan, v as E2eeWritePolicy, w as JsonObject, x as EphemeralStreamJoinParams, y as EphemeralStreamAdaptor, z as StreamsCrdtOptions } from "./stream-id-DAAMJlvT.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, StreamsCrdtShardUrlsOptions, TransportCatchupParams, TransportCatchupSuccess, 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-2eJsM9CJ.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-fOF9ZqCH.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 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-JuJ1JqCX.js";
1
+ import { $ as BeforeRemoteCursorSaveHook, A as PayloadProtectionReadPolicy, B as StreamsCrdtShardUrlsOptions, C as IsolatedCrdtAdapter, D as PayloadProtectionKey, E as PayloadProtectionEncryptionOptions, F as SnapshotTransformHook, G as TransportError, H as TransportCatchupSuccess, I as SnapshotUploadOptions, J as TransportSnapshotUploadSuccess, K as TransportJoinParams, L as StreamsAuthContext, M as PayloadProtectionWritePolicy, N as Result, O as PayloadProtectionKeyProvider, P as SnapshotCodec, Q as BeforeRemoteCursorSaveContext, R as StreamsAuthProvider, S as EphemeralStreamSubscription, T as JsonValue, U as TransportCreateStreamSuccess, V as TransportCatchupParams, W as TransportDeleteStreamSuccess, X as TransportSyncSuccess, Y as TransportSubscription, Z as WriteOnlyAppendResult, _ as E2eeScope, a as EphemeralStreamCrdt, at as RemoteCursorStore, b as EphemeralStreamCrdtOptions, c as CrdtApplyOutcome, d as CrdtUpdateBatch, et as InMemoryRemoteCursorStore, f as E2eeEncryptionOptions, g as E2eeReadPolicy, h as E2eeOptions, i as PayloadProtectionError, it as RemoteCursorSaveSource, j as PayloadProtectionScope, k as PayloadProtectionOptions, l as CrdtApplyReturn, m as E2eeKeyProvider, n as isValidBucketId, nt as IndexedDbRemoteCursorStoreOptions, o as StreamsCrdt, ot as createInitialRemoteCursor, p as E2eeKey, q as TransportRoomStatus, r as isValidRillId, rt as RemoteCursor, s as CrdtAdapter, t as createStreamUrl, tt as IndexedDbRemoteCursorStore, u as CrdtUnresolvedSpan, v as E2eeWritePolicy, w as JsonObject, x as EphemeralStreamJoinParams, y as EphemeralStreamAdaptor, z as StreamsCrdtOptions } from "./stream-id-DAAMJlvT.js";
2
2
  import { LoroDoc } from "loro-crdt";
3
3
 
4
4
  //#region src/loro.d.ts
@@ -41,5 +41,5 @@ interface LoroEphemeralStoreLike {
41
41
  declare function EphemeralStoreAdaptor(store: LoroEphemeralStoreLike): EphemeralStreamAdaptor;
42
42
  declare const createLoroEphemeralStoreAdaptor: typeof EphemeralStoreAdaptor;
43
43
  //#endregion
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 };
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, StreamsCrdtShardUrlsOptions, TransportCatchupParams, TransportCatchupSuccess, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, WriteOnlyAppendResult, createInitialRemoteCursor, createLoroDocAdapter, createLoroEphemeralStoreAdaptor, createStreamUrl, isValidBucketId, isValidRillId };
45
45
  //# sourceMappingURL=loro.d.ts.map
package/dist/loro.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-2eJsM9CJ.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-fOF9ZqCH.js";
2
2
  import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
4
  function batchImportUpdates(doc, updates) {
@@ -356,6 +356,24 @@ interface TransportDeleteStreamSuccess {
356
356
  interface TransportSyncSuccess<TVersion extends JsonObject = JsonObject> {
357
357
  readonly cursor: RemoteCursor<TVersion>;
358
358
  }
359
+ /**
360
+ * Optional parameters for a `catchup()` call.
361
+ */
362
+ interface TransportCatchupParams {
363
+ /**
364
+ * When `false` or omitted, an active healthy live read loop is treated as
365
+ * already catching up and `catchup()` returns immediately with the current
366
+ * cursor. When `true`, the transport performs one non-SSE offset read even
367
+ * if live SSE/long-poll is currently healthy.
368
+ */
369
+ readonly force?: boolean;
370
+ }
371
+ /**
372
+ * Result payload returned by `catchup()`.
373
+ */
374
+ interface TransportCatchupSuccess<TVersion extends JsonObject = JsonObject> {
375
+ readonly cursor: RemoteCursor<TVersion>;
376
+ }
359
377
  /**
360
378
  * Result payload returned by the internal snapshot upload test hook.
361
379
  */
@@ -395,6 +413,32 @@ interface SnapshotUploadOptions {
395
413
  /** Minimum byte delta since the last remote snapshot before uploading. */
396
414
  readonly minBytesSinceRemoteSnapshot?: number;
397
415
  }
416
+ interface StreamsCrdtShardUrlsOptions {
417
+ /**
418
+ * Origin URLs used for `GET <stream>/bootstrap`.
419
+ *
420
+ * Each entry should be an origin URL such as
421
+ * `https://control-a.streams-api-proxy.loro.dev`. The transport keeps the
422
+ * stream path and query from `streamUrl` and only replaces the request origin.
423
+ */
424
+ readonly bootstrap?: readonly string[];
425
+ /**
426
+ * Origin URLs used for non-SSE catch-up reads:
427
+ * `GET <stream>?offset=...` and long-poll fallback reads.
428
+ */
429
+ readonly catchup?: readonly string[];
430
+ /**
431
+ * Origin URLs used for large `POST <stream>` append requests.
432
+ */
433
+ readonly largePost?: readonly string[];
434
+ /**
435
+ * Minimum request body size before `largePost` routing applies.
436
+ *
437
+ * Defaults to 0, meaning every non-empty append POST can use `largePost`
438
+ * when that shard pool is configured.
439
+ */
440
+ readonly largePostMinBytes?: number;
441
+ }
398
442
  /**
399
443
  * Transforms a remote snapshot payload during upload or bootstrap.
400
444
  */
@@ -697,6 +741,8 @@ interface StreamsCrdtOptions<TVersion extends JsonObject> {
697
741
  */
698
742
  readonly payloadProtection?: PayloadProtectionOptions;
699
743
  readonly snapshotUpload?: SnapshotUploadOptions;
744
+ /** Optional origin pools for routing bootstrap/catch-up and large append requests. */
745
+ readonly shardUrls?: StreamsCrdtShardUrlsOptions;
700
746
  readonly reconnectConfig?: Partial<ReconnectConfig>;
701
747
  readonly debug?: boolean;
702
748
  }
@@ -704,6 +750,21 @@ interface StreamsCrdtLike<TVersion extends JsonObject> {
704
750
  createStream(): Promise<Result<TransportCreateStreamSuccess, TransportError>>;
705
751
  deleteStream(): Promise<Result<TransportDeleteStreamSuccess, TransportError>>;
706
752
  sync(): Promise<Result<TransportSyncSuccess<TVersion>, TransportError>>;
753
+ /**
754
+ * Pulls remote stream updates into the local CRDT without intentionally
755
+ * appending new local updates.
756
+ *
757
+ * If a healthy `join()` live read loop is active, this is a fast no-op by
758
+ * default because the loop is already catching up continuously. Pass
759
+ * `{ force: true }` to issue one explicit non-SSE offset read anyway.
760
+ *
761
+ * If the live read side of a join is reconnecting, disconnected, or in
762
+ * error, `catchup()` attempts one immediate non-SSE offset read from the
763
+ * room's current cursor and then nudges the live loops to reconnect. Use
764
+ * `sync()` or `subscription.waitUntilSynced()` when the caller also needs
765
+ * local pending changes uploaded to the server.
766
+ */
767
+ catchup(params?: TransportCatchupParams): Promise<Result<TransportCatchupSuccess<TVersion>, TransportError>>;
707
768
  /**
708
769
  * Appends only locally observed batches without reading remote state first.
709
770
  *
@@ -812,6 +873,16 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
812
873
  * Runs one bootstrap/catch-up cycle without entering live mode.
813
874
  */
814
875
  sync(): Promise<Result<TransportSyncSuccess<TVersion>, TransportError>>;
876
+ /**
877
+ * Pulls remote updates into the local CRDT without intentionally appending
878
+ * new local updates.
879
+ *
880
+ * Healthy live reads already catch up continuously, so an active `join()`
881
+ * returns immediately unless `force: true` is passed. Reconnecting or
882
+ * terminal live reads perform one immediate non-SSE offset read and then
883
+ * nudge the room to reconnect.
884
+ */
885
+ catchup(params?: TransportCatchupParams): Promise<Result<TransportCatchupSuccess<TVersion>, TransportError>>;
815
886
  /**
816
887
  * Appends local CRDT updates without reading the stream first.
817
888
  *
@@ -831,8 +902,10 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
831
902
  private performInitialJoinSync;
832
903
  private createInitialCursor;
833
904
  private resolveInitialRemoteState;
905
+ private resolvePullOnlyRemoteState;
906
+ private catchupJoinState;
834
907
  private bootstrapState;
835
- private catchup;
908
+ private catchupFromCursor;
836
909
  private catchupOrGone;
837
910
  private recoverLiveIncompleteRemoteApply;
838
911
  private appendLocalBatch;
@@ -977,5 +1050,5 @@ declare function createStreamUrl(input: {
977
1050
  baseUrl?: string;
978
1051
  }): string;
979
1052
  //#endregion
980
- 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 };
981
- //# sourceMappingURL=stream-id-JuJ1JqCX.d.ts.map
1053
+ export { BeforeRemoteCursorSaveHook as $, PayloadProtectionReadPolicy as A, StreamsCrdtShardUrlsOptions as B, IsolatedCrdtAdapter as C, PayloadProtectionKey as D, PayloadProtectionEncryptionOptions as E, SnapshotTransformHook as F, TransportError as G, TransportCatchupSuccess as H, SnapshotUploadOptions as I, TransportSnapshotUploadSuccess as J, TransportJoinParams as K, StreamsAuthContext as L, PayloadProtectionWritePolicy as M, Result as N, PayloadProtectionKeyProvider as O, SnapshotCodec as P, BeforeRemoteCursorSaveContext as Q, StreamsAuthProvider as R, EphemeralStreamSubscription as S, JsonValue as T, TransportCreateStreamSuccess as U, TransportCatchupParams as V, TransportDeleteStreamSuccess as W, TransportSyncSuccess as X, TransportSubscription as Y, WriteOnlyAppendResult as Z, E2eeScope as _, EphemeralStreamCrdt as a, RemoteCursorStore as at, EphemeralStreamCrdtOptions as b, CrdtApplyOutcome as c, CrdtUpdateBatch as d, InMemoryRemoteCursorStore as et, E2eeEncryptionOptions as f, E2eeReadPolicy as g, E2eeOptions as h, PayloadProtectionError as i, RemoteCursorSaveSource as it, PayloadProtectionScope as j, PayloadProtectionOptions as k, CrdtApplyReturn as l, E2eeKeyProvider as m, isValidBucketId as n, IndexedDbRemoteCursorStoreOptions as nt, StreamsCrdt as o, createInitialRemoteCursor as ot, E2eeKey as p, TransportRoomStatus as q, isValidRillId as r, RemoteCursor as rt, CrdtAdapter as s, createStreamUrl as t, IndexedDbRemoteCursorStore as tt, CrdtUnresolvedSpan as u, E2eeWritePolicy as v, JsonObject as w, EphemeralStreamJoinParams as x, EphemeralStreamAdaptor as y, StreamsCrdtOptions as z };
1054
+ //# sourceMappingURL=stream-id-DAAMJlvT.d.ts.map
@@ -881,6 +881,56 @@ function resolveNextOffsetAfterConcurrentAdvance(requestOffset, currentOffset, r
881
881
  }
882
882
  //#endregion
883
883
  //#region src/transport-client.ts
884
+ const bodySizeEncoder = new TextEncoder();
885
+ function normalizeShardOrigins(urls, label) {
886
+ if (urls == null) return;
887
+ const out = [];
888
+ for (const raw of urls) {
889
+ const trimmed = raw.trim();
890
+ if (trimmed.length === 0) continue;
891
+ const url = new URL(trimmed);
892
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label} shard URL must use http or https`);
893
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error(`${label} shard URL must be an origin URL`);
894
+ out.push(url.origin);
895
+ }
896
+ return out.length === 0 ? void 0 : out;
897
+ }
898
+ function normalizeShardUrlsOptions(options) {
899
+ if (options == null) return;
900
+ const largePostMinBytes = options.largePostMinBytes ?? 0;
901
+ if (!Number.isSafeInteger(largePostMinBytes) || largePostMinBytes < 0) throw new Error("shardUrls.largePostMinBytes must be a non-negative safe integer");
902
+ const normalized = {
903
+ bootstrap: normalizeShardOrigins(options.bootstrap, "shardUrls.bootstrap"),
904
+ catchup: normalizeShardOrigins(options.catchup, "shardUrls.catchup"),
905
+ largePost: normalizeShardOrigins(options.largePost, "shardUrls.largePost"),
906
+ largePostMinBytes
907
+ };
908
+ if (normalized.bootstrap == null && normalized.catchup == null && normalized.largePost == null) return;
909
+ return normalized;
910
+ }
911
+ function requestBodyByteLength(body) {
912
+ if (body == null) return 0;
913
+ if (typeof body === "string") return bodySizeEncoder.encode(body).byteLength;
914
+ if (body instanceof ArrayBuffer) return body.byteLength;
915
+ if (ArrayBuffer.isView(body)) return body.byteLength;
916
+ if (typeof Blob !== "undefined" && body instanceof Blob) return body.size;
917
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return bodySizeEncoder.encode(body.toString()).byteLength;
918
+ }
919
+ function createRandomShardStart(originCount) {
920
+ if (originCount <= 1) return 0;
921
+ try {
922
+ const crypto = globalThis.crypto;
923
+ if (crypto != null) {
924
+ const values = new Uint32Array(1);
925
+ crypto.getRandomValues(values);
926
+ return (values[0] ?? 0) % originCount;
927
+ }
928
+ } catch {}
929
+ return Math.floor(Math.random() * originCount);
930
+ }
931
+ function normalizedStreamPath(streamUrl) {
932
+ return streamUrl.pathname.endsWith("/") ? streamUrl.pathname.slice(0, -1) : streamUrl.pathname;
933
+ }
884
934
  /**
885
935
  * Low-level HTTP and SSE client for one stream.
886
936
  *
@@ -894,24 +944,62 @@ var StreamsTransportClient = class {
894
944
  fetchImpl;
895
945
  authProvider;
896
946
  reconnectConfig;
947
+ shardUrls;
948
+ shardCursorIndexes = {};
897
949
  authRefreshInFlight;
898
950
  constructor(options) {
899
951
  this.streamUrl = options.streamUrl;
900
952
  this.adapter = options.adapter;
953
+ this.fetchImpl = options.fetchImpl;
954
+ this.shardUrls = normalizeShardUrlsOptions(options.shardUrls);
901
955
  this.client = new StreamsClient({
902
956
  url: options.streamUrl,
903
957
  auth: options.authProvider,
904
- fetch: options.fetchImpl,
958
+ fetch: this.fetchRouted.bind(this),
905
959
  retry: { maxAttempts: 0 },
906
960
  timeout: {
907
961
  connectTimeoutMs: options.reconnectConfig.connectTimeoutMs,
908
962
  pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
909
963
  }
910
964
  });
911
- this.fetchImpl = options.fetchImpl;
912
965
  this.authProvider = options.authProvider;
913
966
  this.reconnectConfig = options.reconnectConfig;
914
967
  }
968
+ fetchRouted(input, init) {
969
+ return this.fetchImpl(this.routeRequestUrl(input, init), init);
970
+ }
971
+ routeRequestUrl(input, init) {
972
+ const shardUrls = this.shardUrls;
973
+ if (shardUrls == null) return input;
974
+ const operation = this.classifyShardOperation(input, init, shardUrls);
975
+ if (operation == null) return input;
976
+ const origins = operation === "bootstrap" ? shardUrls.bootstrap : operation === "catchup" ? shardUrls.catchup : shardUrls.largePost;
977
+ if (origins == null || origins.length === 0) return input;
978
+ const target = new URL(input);
979
+ const shardOrigin = new URL(this.nextShardOrigin(operation, origins));
980
+ target.protocol = shardOrigin.protocol;
981
+ target.host = shardOrigin.host;
982
+ target.username = "";
983
+ target.password = "";
984
+ return target.toString();
985
+ }
986
+ nextShardOrigin(operation, origins) {
987
+ const currentIndex = this.shardCursorIndexes[operation] ?? createRandomShardStart(origins.length);
988
+ this.shardCursorIndexes[operation] = (currentIndex + 1) % origins.length;
989
+ return origins[currentIndex % origins.length];
990
+ }
991
+ classifyShardOperation(input, init, shardUrls) {
992
+ const method = (init?.method ?? "GET").toUpperCase();
993
+ const requestUrl = new URL(input);
994
+ const streamUrl = new URL(this.streamUrl);
995
+ const streamPath = normalizedStreamPath(streamUrl);
996
+ if (method === "GET" && requestUrl.pathname === `${streamPath}/bootstrap`) return "bootstrap";
997
+ if (method === "GET" && requestUrl.pathname === streamUrl.pathname && requestUrl.searchParams.has("offset") && requestUrl.searchParams.get("live") !== "sse") return "catchup";
998
+ if (method !== "POST" || requestUrl.pathname !== streamUrl.pathname) return;
999
+ const bodyLength = requestBodyByteLength(init?.body);
1000
+ if (bodyLength == null || bodyLength === 0 || bodyLength < shardUrls.largePostMinBytes) return;
1001
+ return "large-post";
1002
+ }
915
1003
  async createStreamRequest() {
916
1004
  const adapterHeaders = this.adapter.getCreateStreamHeaders ? await this.adapter.getCreateStreamHeaders() : void 0;
917
1005
  const result = await this.client.create({ headers: adapterHeaders });
@@ -2209,7 +2297,8 @@ var StreamsCrdt = class StreamsCrdt {
2209
2297
  adapter: this.adapter,
2210
2298
  authProvider,
2211
2299
  fetchImpl,
2212
- reconnectConfig: this.reconnectConfig
2300
+ reconnectConfig: this.reconnectConfig,
2301
+ shardUrls: options.shardUrls
2213
2302
  });
2214
2303
  this.cursorManager = this.createCursorManager(this.adapter, (cursor, source) => this.saveRemoteCursor(cursor, source));
2215
2304
  this.volatileCursorManager = this.createCursorManager(this.adapter, async (cursor) => cursor);
@@ -2406,6 +2495,34 @@ var StreamsCrdt = class StreamsCrdt {
2406
2495
  }
2407
2496
  }
2408
2497
  /**
2498
+ * Pulls remote updates into the local CRDT without intentionally appending
2499
+ * new local updates.
2500
+ *
2501
+ * Healthy live reads already catch up continuously, so an active `join()`
2502
+ * returns immediately unless `force: true` is passed. Reconnecting or
2503
+ * terminal live reads perform one immediate non-SSE offset read and then
2504
+ * nudge the room to reconnect.
2505
+ */
2506
+ async catchup(params = {}) {
2507
+ try {
2508
+ this.disableWriteOnlyMode("appendWriteOnly cannot be used after catchup() on the same transport");
2509
+ const state = this.joinState;
2510
+ if (state != null && !state.closed) return {
2511
+ ok: true,
2512
+ value: { cursor: await this.catchupJoinState(state, params) }
2513
+ };
2514
+ return {
2515
+ ok: true,
2516
+ value: { cursor: (await this.resolvePullOnlyRemoteState()).cursor }
2517
+ };
2518
+ } catch (error) {
2519
+ return {
2520
+ ok: false,
2521
+ error: toTransportError(error)
2522
+ };
2523
+ }
2524
+ }
2525
+ /**
2409
2526
  * Appends local CRDT updates without reading the stream first.
2410
2527
  *
2411
2528
  * Use this for write-only actors whose auth token may POST to the stream but
@@ -2658,6 +2775,40 @@ var StreamsCrdt = class StreamsCrdt {
2658
2775
  return await this.bootstrapState(this.createInitialCursor());
2659
2776
  }
2660
2777
  }
2778
+ async resolvePullOnlyRemoteState() {
2779
+ const loaded = await this.remoteCursorStore.load(this.streamUrl);
2780
+ const cursor = loaded ?? this.createInitialCursor();
2781
+ try {
2782
+ if (loaded == null) return await this.bootstrapState(cursor);
2783
+ return await this.catchupFromCursor(cursor);
2784
+ } catch (error) {
2785
+ if (!isStreamNotFoundError(error) || !this.createStreamIfMissing) throw error;
2786
+ await this.ensureStreamExists();
2787
+ return await this.bootstrapState(this.createInitialCursor());
2788
+ }
2789
+ }
2790
+ async catchupJoinState(state, params) {
2791
+ if (state.readSubStatus === "connecting") {
2792
+ await state.started;
2793
+ return state.cursor;
2794
+ }
2795
+ if (state.readSubStatus === "ok" && params.force !== true) return state.cursor;
2796
+ const cursor = await this.enqueueExclusive(async () => {
2797
+ if (state.closed) throw new Error("subscription closed");
2798
+ const caughtUp = await this.catchupFromCursor(state.cursor, void 0, state.streamCursor, (previousVersion, nextVersion) => {
2799
+ this.snapshotManager.observeRemoteVersion(state, previousVersion, nextVersion);
2800
+ });
2801
+ state.cursor = caughtUp.cursor;
2802
+ state.streamCursor = caughtUp.streamCursor;
2803
+ return state.cursor;
2804
+ });
2805
+ if (state.closed) return cursor;
2806
+ state.retryAttempt = 0;
2807
+ state.lastError = void 0;
2808
+ if (state.readSubStatus !== "ok" || state.writeSubStatus === "error") this.rejoin();
2809
+ else if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
2810
+ return cursor;
2811
+ }
2661
2812
  async bootstrapState(cursor, options = {}) {
2662
2813
  const cursorManager = options.cursorManager ?? this.cursorManager;
2663
2814
  const result = await this.client.bootstrap();
@@ -2677,7 +2828,7 @@ var StreamsCrdt = class StreamsCrdt {
2677
2828
  streamCursor: bootstrapCursor
2678
2829
  };
2679
2830
  }
2680
- async catchup(cursor, live, streamCursor, onRemoteVersionApplied) {
2831
+ async catchupFromCursor(cursor, live, streamCursor, onRemoteVersionApplied) {
2681
2832
  const result = await this.catchupOrGone(cursor, {
2682
2833
  live,
2683
2834
  streamCursor,
@@ -3393,4 +3544,4 @@ function createStreamUrl(input) {
3393
3544
  //#endregion
3394
3545
  export { InMemoryRemoteCursorStore as a, EphemeralStreamCrdt as c, StreamsCrdt as i, PayloadProtectionError as l, isValidBucketId as n, IndexedDbRemoteCursorStore as o, isValidRillId as r, createInitialRemoteCursor as s, createStreamUrl as t };
3395
3546
 
3396
- //# sourceMappingURL=stream-id-2eJsM9CJ.js.map
3547
+ //# sourceMappingURL=stream-id-fOF9ZqCH.js.map