@loro-dev/streams-crdt 0.8.3 → 0.8.4

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
@@ -37,19 +37,28 @@ if (!result.ok) {
37
37
  // result.error is a TransportError with a discriminated `code` field.
38
38
  switch (result.error.code) {
39
39
  case "stream_not_found":
40
- // The stream doesn't exist yet — create it first.
40
+ // 404 — the stream doesn't exist yet. Create it first.
41
41
  break;
42
42
  case "auth_failed":
43
- // Token expired or unauthorized.
43
+ // 401/403 from the server — refresh credentials and retry.
44
44
  break;
45
- case "protocol_error":
46
- case "internal_error":
47
- // Non-retryable: bad server response or local runtime/adapter failure.
45
+ case "auth_provider_error":
46
+ // Your auth callback threw. The live loop will retry automatically.
47
+ // For permanent logout, return undefined from the callback instead.
48
+ break;
49
+ case "server_error":
50
+ // 5xx — transient backend failure. The live loop retries automatically;
51
+ // for one-shot calls (sync/createStream/etc.) you decide whether to
52
+ // retry. `retryAfterMs` honors the server's Retry-After hint.
48
53
  break;
49
54
  case "network_error":
50
55
  case "timeout":
51
56
  // Transient — safe to retry.
52
57
  break;
58
+ case "protocol_error":
59
+ case "internal_error":
60
+ // Non-retryable: bad server response or local runtime/adapter failure.
61
+ break;
53
62
  default:
54
63
  console.error("transport error:", result.error);
55
64
  }
@@ -68,11 +77,73 @@ if (!result.ok && result.error.retryable) {
68
77
  }
69
78
  ```
70
79
 
80
+ #### Surfacing errors to end users
81
+
82
+ HTTP-derived variants (`server_error`, `auth_failed`, `protocol_error`,
83
+ `stream_not_found`, `gone`) carry the response details so apps can show
84
+ actionable messages without parsing the `message` string:
85
+
86
+ ```ts
87
+ const err = result.error;
88
+ if (err.code === "server_error") {
89
+ toast.error(`Server error ${err.status}${err.requestId ? ` (req: ${err.requestId})` : ""}`);
90
+ console.error("backend body:", err.body);
91
+ // err.retryAfterMs may suggest a backoff if the server set Retry-After.
92
+ }
93
+ ```
94
+
95
+ | Code | `status` | `message` | `body` | Other |
96
+ | ---------------------- | -------- | --------- | ------ | ------------------------------ |
97
+ | `stream_not_found` | 404 | yes | yes | — |
98
+ | `auth_failed` | 401/403 | yes | yes | — |
99
+ | `protocol_error` | yes | yes | yes | — |
100
+ | `gone` | 410 | yes | yes | — |
101
+ | `server_error` | 5xx | yes | yes | `requestId?`, `retryAfterMs?` |
102
+ | `timeout` | — | yes | — | `phase: "connect" \| "poll"` |
103
+ | `network_error` | — | yes | — | — |
104
+ | `auth_provider_error` | — | yes | — | `cause?` (original thrown value) |
105
+ | `internal_error` | — | yes | — | — |
106
+ | `apply_incomplete` | — | yes | — | `reason: "loro_pending_updates"` |
107
+
108
+ #### Operation-level retries vs. live-mode retries
109
+
110
+ - `sync()`, `createStream()`, `deleteStream()`, `appendWriteOnly()` return
111
+ after a **single** attempt. The application is responsible for retrying
112
+ `retryable` errors.
113
+ - After `join()` succeeds, the live read/write loops automatically reconnect
114
+ on every `retryable: true` error using exponential backoff
115
+ (`[0, 0.5s, 1s, 2s, 4s, 8s, 15s, 30s]` with ±20% jitter; cap 30s). When a
116
+ 5xx response includes `Retry-After`, the runtime waits at least that long.
117
+ Non-retryable errors transition the subscription to `status: "error"`;
118
+ call `transport.rejoin()` after fixing the cause (e.g., refreshing the auth
119
+ token) to reset retry state and reconnect.
120
+
71
121
  > **Why `Result` instead of `throw`?** Transport failures (network errors,
72
122
  > auth expiry, missing streams) are expected at runtime, not exceptional.
73
123
  > Wrapping them in `Result` makes the error path explicit in the type system
74
124
  > so callers cannot accidentally ignore failures with a missing `try/catch`.
75
125
 
126
+ #### Migration notes
127
+
128
+ For most callers nothing changes — `if (!result.ok) { handle(result.error); }`
129
+ keeps working, and `result.error.retryable` is still the right gate for
130
+ "safe to retry?".
131
+
132
+ Only adjust if any of these apply:
133
+
134
+ - **Your auth callback throws on permanent failure** (e.g., user is logged
135
+ out). Throwing is now treated as a transient error and retried in live
136
+ mode. **Return `undefined` instead** so the server returns 401 and you get
137
+ the terminal `auth_failed`. See "What if the auth callback throws?" below.
138
+ - **You construct `TransportError` literals** (e.g., in tests or fixtures).
139
+ `stream_not_found` now requires `status: 404` + `message`; `gone` now
140
+ requires `status: 410`. TypeScript will flag these.
141
+ - **You exhaustively switch on `code` with a `: never` default.** Add a case
142
+ for `auth_provider_error` (and optionally `server_error` if you handled
143
+ 5xx via `unknown`/`internal_error` before).
144
+ - **You used `"message" in error` to defensively narrow.** All variants now
145
+ carry `message`; the guard is no longer needed.
146
+
76
147
  ## Quick Start (Loro)
77
148
 
78
149
  ```ts
@@ -324,6 +395,32 @@ const transport = new StreamsCrdt({
324
395
  If your callback already returns a fresh token every time, you can ignore the
325
396
  `context` argument.
326
397
 
398
+ ### What if the auth callback throws?
399
+
400
+ Throwing from the callback is treated as a **transient** failure. The runtime
401
+ wraps the error as `auth_provider_error` (`retryable: true`) so a hiccupping
402
+ auth backend doesn't terminate the live read/write loops — they retry under
403
+ the same exponential backoff used for `server_error` and `network_error`.
404
+
405
+ For the "user is permanently logged out" case, **return `undefined`** (or
406
+ `null`/empty string) instead of throwing. The request goes out without an
407
+ `Authorization` header, the server returns 401, and you receive the
408
+ non-retryable `auth_failed` instead — which the application typically routes
409
+ to a sign-in page.
410
+
411
+ ```ts
412
+ auth: async () => {
413
+ try {
414
+ return await fetchTokenFromAuthBackend(); // network → throw → retry
415
+ } catch (e) {
416
+ if (isPermanentLogoutError(e)) {
417
+ return undefined; // → server 401 → auth_failed (terminal)
418
+ }
419
+ throw e; // → auth_provider_error (retryable)
420
+ }
421
+ }
422
+ ```
423
+
327
424
  ## End-to-End Encryption (E2EE)
328
425
 
329
426
  `e2ee` enables end-to-end encryption for CRDT update batches and snapshots
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-Bi5rAZfP.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-JuJ1JqCX.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
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-BjmGvObf.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-BBE3LRAQ.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-Bi5rAZfP.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-JuJ1JqCX.js";
2
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-BjmGvObf.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-BBE3LRAQ.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-Bi5rAZfP.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-JuJ1JqCX.js";
2
2
  import { LoroDoc } from "loro-crdt";
3
3
 
4
4
  //#region src/loro.d.ts
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-BjmGvObf.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-BBE3LRAQ.js";
2
2
  import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
4
  function batchImportUpdates(doc, updates) {
@@ -129,7 +129,8 @@ const DEFAULT_RECONNECT_CONFIG = {
129
129
  2e3,
130
130
  4e3,
131
131
  8e3,
132
- 15e3
132
+ 15e3,
133
+ 3e4
133
134
  ],
134
135
  jitterFraction: .2,
135
136
  maxAttempts: Infinity,
@@ -149,11 +150,15 @@ function applyJitter(baseMs, fraction) {
149
150
  /**
150
151
  * Compute the retry delay for a given attempt number.
151
152
  * Uses the delays table from config, clamping at the last entry.
153
+ *
154
+ * If `minDelayMs` is provided (e.g. from a Retry-After hint), the result is
155
+ * clamped to be at least that value before jitter, so honoring the server's
156
+ * backoff request never falls below what we'd already wait.
152
157
  */
153
- function computeRetryDelay(attempt, config = DEFAULT_RECONNECT_CONFIG) {
158
+ function computeRetryDelay(attempt, config = DEFAULT_RECONNECT_CONFIG, minDelayMs) {
154
159
  const { delays, jitterFraction } = config;
155
- const base = delays[Math.min(attempt, delays.length - 1)];
156
- return applyJitter(base, jitterFraction);
160
+ const scheduled = delays[Math.min(attempt, delays.length - 1)];
161
+ return applyJitter(typeof minDelayMs === "number" && minDelayMs > scheduled ? minDelayMs : scheduled, jitterFraction);
157
162
  }
158
163
  //#endregion
159
164
  //#region src/items.ts
@@ -477,9 +482,16 @@ function createProducerId() {
477
482
  }
478
483
  var HttpError = class extends Error {
479
484
  status;
480
- constructor(message, status) {
485
+ requestId;
486
+ retryAfterMs;
487
+ /** Raw response body text, when readable. Empty bodies are stored as undefined. */
488
+ body;
489
+ constructor(message, status, options) {
481
490
  super(message);
482
491
  this.status = status;
492
+ if (options?.requestId !== void 0) this.requestId = options.requestId;
493
+ if (options?.retryAfterMs !== void 0) this.retryAfterMs = options.retryAfterMs;
494
+ if (options?.body !== void 0 && options.body.length > 0) this.body = options.body;
483
495
  }
484
496
  };
485
497
  var TimeoutError = class extends Error {
@@ -498,6 +510,22 @@ var ProtocolError = class extends Error {
498
510
  this.status = status;
499
511
  }
500
512
  };
513
+ /**
514
+ * Wraps any error thrown by a user-supplied auth provider callback.
515
+ *
516
+ * The runtime treats these as retryable so transient credential-fetch failures
517
+ * (e.g. a hiccupping auth backend) do not terminate the live read/write loops.
518
+ * For permanent "user is logged out" cases, the auth provider should return
519
+ * `undefined` instead of throwing — that lets the request go out without an
520
+ * `Authorization` header so the server returns a real 401 (`auth_failed`).
521
+ */
522
+ var AuthProviderError = class extends Error {
523
+ cause;
524
+ constructor(message, options) {
525
+ super(message);
526
+ if (options?.cause !== void 0) this.cause = options.cause;
527
+ }
528
+ };
501
529
  function isObject(value) {
502
530
  return typeof value === "object" && value != null;
503
531
  }
@@ -603,6 +631,12 @@ function classifyFetchError(err) {
603
631
  retriable: true,
604
632
  atMs: Date.now()
605
633
  };
634
+ if (err instanceof AuthProviderError) return {
635
+ code: "auth_provider",
636
+ message,
637
+ retriable: true,
638
+ atMs: Date.now()
639
+ };
606
640
  if (status === 401 || status === 403) return {
607
641
  code: "auth_failed",
608
642
  message,
@@ -621,6 +655,12 @@ function classifyFetchError(err) {
621
655
  retriable: false,
622
656
  atMs: Date.now()
623
657
  };
658
+ if (typeof status === "number" && status >= 500 && status < 600) return {
659
+ code: "server",
660
+ message,
661
+ retriable: true,
662
+ atMs: Date.now()
663
+ };
624
664
  if (err instanceof ProtocolError) return {
625
665
  code: "protocol",
626
666
  message,
@@ -662,15 +702,28 @@ function toTransportError(error) {
662
702
  reason: "loro_pending_updates",
663
703
  message: error instanceof Error ? error.message : String(error)
664
704
  };
665
- if (isStreamNotFoundError(error)) return {
666
- code: "stream_not_found",
667
- retryable: false
705
+ if (error instanceof AuthProviderError) return {
706
+ code: "auth_provider_error",
707
+ retryable: true,
708
+ message: error.message,
709
+ ...error.cause !== void 0 && { cause: error.cause }
668
710
  };
711
+ if (isStreamNotFoundError(error)) {
712
+ const httpError = error;
713
+ return {
714
+ code: "stream_not_found",
715
+ retryable: false,
716
+ status: 404,
717
+ message: httpError.message,
718
+ ...httpError.body !== void 0 && { body: httpError.body }
719
+ };
720
+ }
669
721
  if (error instanceof HttpError && (error.status === 401 || error.status === 403)) return {
670
722
  code: "auth_failed",
671
723
  retryable: false,
672
724
  status: error.status,
673
- message: error.message
725
+ message: error.message,
726
+ ...error.body !== void 0 && { body: error.body }
674
727
  };
675
728
  if (error instanceof HttpError && [
676
729
  400,
@@ -680,7 +733,8 @@ function toTransportError(error) {
680
733
  code: "protocol_error",
681
734
  retryable: false,
682
735
  status: error.status,
683
- message: error.message
736
+ message: error.message,
737
+ ...error.body !== void 0 && { body: error.body }
684
738
  };
685
739
  if (error instanceof ProtocolError) return {
686
740
  code: "protocol_error",
@@ -697,7 +751,18 @@ function toTransportError(error) {
697
751
  if (error instanceof HttpError && error.status === 410) return {
698
752
  code: "gone",
699
753
  retryable: true,
700
- message: error.message
754
+ status: 410,
755
+ message: error.message,
756
+ ...error.body !== void 0 && { body: error.body }
757
+ };
758
+ if (error instanceof HttpError && error.status >= 500 && error.status < 600) return {
759
+ code: "server_error",
760
+ retryable: true,
761
+ status: error.status,
762
+ message: error.message,
763
+ ...error.body !== void 0 && { body: error.body },
764
+ ...error.requestId !== void 0 && { requestId: error.requestId },
765
+ ...error.retryAfterMs !== void 0 && { retryAfterMs: error.retryAfterMs }
701
766
  };
702
767
  if (isPayloadProtectionError(error)) return {
703
768
  code: "payload_protection_error",
@@ -744,7 +809,13 @@ function normalizeAuthProvider(auth) {
744
809
  return async () => token.length === 0 ? void 0 : token;
745
810
  }
746
811
  return async (context) => {
747
- const token = (await auth(context))?.trim() ?? "";
812
+ let value;
813
+ try {
814
+ value = await auth(context);
815
+ } catch (cause) {
816
+ throw new AuthProviderError(cause instanceof Error ? cause.message : `auth provider threw: ${String(cause)}`, { cause });
817
+ }
818
+ const token = value?.trim() ?? "";
748
819
  return token.length === 0 ? void 0 : token;
749
820
  };
750
821
  }
@@ -1073,8 +1144,12 @@ var StreamsTransportClient = class {
1073
1144
  return headers;
1074
1145
  }
1075
1146
  async responseToEphemeralFailure(response, context) {
1076
- const message = await response.text().catch(() => "");
1077
- return new HttpError(message.length > 0 ? `${context} failed: ${message}` : `${context} failed with status ${response.status}`, response.status);
1147
+ const bodyText = await response.text().catch(() => "");
1148
+ return new HttpError(bodyText.length > 0 ? `${context} failed: ${bodyText}` : `${context} failed with status ${response.status}`, response.status, {
1149
+ requestId: extractRequestIdFromResponseHeaders(response.headers),
1150
+ retryAfterMs: parseRetryAfterMs(response.headers.get("retry-after") ?? void 0),
1151
+ body: bodyText.length > 0 ? bodyText : void 0
1152
+ });
1078
1153
  }
1079
1154
  ephemeralReadEndpoint() {
1080
1155
  const target = new URL(this.streamUrl);
@@ -1089,12 +1164,16 @@ var StreamsTransportClient = class {
1089
1164
  case "not_found":
1090
1165
  case "conflict":
1091
1166
  case "gone":
1092
- case "rate_limited": return new HttpError(error.message, error.status);
1167
+ case "rate_limited": return new HttpError(error.message, error.status, { body: error.bodyText });
1093
1168
  case "timeout": return new TimeoutError(error.phase, error.phase === "poll" ? this.reconnectConfig.pollTimeoutMs : this.reconnectConfig.connectTimeoutMs);
1094
1169
  case "network_error": return error.cause instanceof TypeError ? error.cause : new TypeError(error.message);
1095
1170
  case "protocol_error": return new ProtocolError(error.message, error.status);
1096
1171
  case "unknown":
1097
- if (typeof error.status === "number") return new HttpError(error.message, error.status);
1172
+ if (typeof error.status === "number") return new HttpError(error.message, error.status, {
1173
+ requestId: extractRequestIdFromHeaders(error.headers),
1174
+ retryAfterMs: parseRetryAfterMs(error.headers?.["retry-after"]),
1175
+ body: error.bodyText
1176
+ });
1098
1177
  if (error.cause instanceof TypeError) return error.cause;
1099
1178
  if (error.cause instanceof Error) return error.cause;
1100
1179
  return new Error(error.message);
@@ -1207,6 +1286,41 @@ var StreamsTransportClient = class {
1207
1286
  };
1208
1287
  }
1209
1288
  };
1289
+ const REQUEST_ID_HEADER_NAMES = [
1290
+ "x-request-id",
1291
+ "x-amzn-trace-id",
1292
+ "cf-ray",
1293
+ "x-trace-id"
1294
+ ];
1295
+ function extractRequestIdFromHeaders(headers) {
1296
+ if (headers == null) return;
1297
+ const lowerCased = {};
1298
+ for (const [key, value] of Object.entries(headers)) lowerCased[key.toLowerCase()] = value;
1299
+ for (const name of REQUEST_ID_HEADER_NAMES) {
1300
+ const value = lowerCased[name];
1301
+ if (typeof value === "string" && value.length > 0) return value;
1302
+ }
1303
+ }
1304
+ function extractRequestIdFromResponseHeaders(headers) {
1305
+ for (const name of REQUEST_ID_HEADER_NAMES) {
1306
+ const value = headers.get(name);
1307
+ if (typeof value === "string" && value.length > 0) return value;
1308
+ }
1309
+ }
1310
+ function parseRetryAfterMs(value) {
1311
+ if (value == null) return;
1312
+ const trimmed = value.trim();
1313
+ if (trimmed.length === 0) return;
1314
+ const numeric = Number(trimmed);
1315
+ if (Number.isFinite(numeric)) {
1316
+ if (numeric < 0) return;
1317
+ return Math.round(numeric * 1e3);
1318
+ }
1319
+ const parsed = Date.parse(trimmed);
1320
+ if (Number.isNaN(parsed)) return;
1321
+ const delta = parsed - Date.now();
1322
+ return delta > 0 ? delta : void 0;
1323
+ }
1210
1324
  //#endregion
1211
1325
  //#region src/ephemeral-runtime.ts
1212
1326
  function createNoopAdapter() {
@@ -3060,7 +3174,7 @@ var StreamsCrdt = class StreamsCrdt {
3060
3174
  this.setReadSubStatus(state, "disconnected");
3061
3175
  return;
3062
3176
  }
3063
- const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig);
3177
+ const delay = computeRetryDelay(state.retryAttempt, this.reconnectConfig, error instanceof HttpError ? error.retryAfterMs : void 0);
3064
3178
  state.retryAttempt += 1;
3065
3179
  await sleep(delay, state.retrySleepController.signal);
3066
3180
  }
@@ -3146,7 +3260,7 @@ var StreamsCrdt = class StreamsCrdt {
3146
3260
  return;
3147
3261
  }
3148
3262
  this.setWriteSubStatus(state, "reconnecting");
3149
- const delay = computeRetryDelay(attempts, this.reconnectConfig);
3263
+ const delay = computeRetryDelay(attempts, this.reconnectConfig, error instanceof HttpError ? error.retryAfterMs : void 0);
3150
3264
  attempts += 1;
3151
3265
  await sleep(delay, state.retrySleepController.signal);
3152
3266
  }
@@ -3278,4 +3392,4 @@ function createStreamUrl(input) {
3278
3392
  //#endregion
3279
3393
  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 };
3280
3394
 
3281
- //# sourceMappingURL=stream-id-BjmGvObf.js.map
3395
+ //# sourceMappingURL=stream-id-BBE3LRAQ.js.map