@loro-dev/streams-crdt 0.16.1 → 0.16.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.
@@ -1,4 +1,4 @@
1
- import { StreamsClient, decodeSsePayload, readSseEvents } from "@loro-dev/streams-client";
1
+ import { StreamsClient, decodeSsePayload, readSseEvents, thrownToStreamError } from "@loro-dev/streams-client";
2
2
  //#region src/deque.ts
3
3
  const DEFAULT_COMPACT_THRESHOLD = 64;
4
4
  const DEFAULT_MIN_CAPACITY = 16;
@@ -171,6 +171,51 @@ function computeRetryDelay(attempt, config = DEFAULT_RECONNECT_CONFIG, minDelayM
171
171
  return applyJitter(typeof minDelayMs === "number" && minDelayMs > scheduled ? minDelayMs : scheduled, jitterFraction);
172
172
  }
173
173
  //#endregion
174
+ //#region src/error-context.ts
175
+ const contexts = /* @__PURE__ */ new WeakMap();
176
+ var ThrownValue = class extends Error {
177
+ constructor(original) {
178
+ super("Local operation failed");
179
+ this.original = original;
180
+ }
181
+ };
182
+ function errorContext(error) {
183
+ return typeof error === "object" && error != null ? contexts.get(error) : void 0;
184
+ }
185
+ function originalThrown(error) {
186
+ return error instanceof ThrownValue ? error.original : error;
187
+ }
188
+ function recordErrorContext(error, context, replace = false) {
189
+ const tagged = typeof error === "object" && error != null ? error : new ThrownValue(error);
190
+ contexts.set(tagged, replace ? context : {
191
+ ...context,
192
+ ...contexts.get(tagged)
193
+ });
194
+ return tagged;
195
+ }
196
+ async function atErrorBoundary(context, task) {
197
+ const startedAt = Date.now();
198
+ try {
199
+ return await task();
200
+ } catch (error) {
201
+ throw recordErrorContext(error, {
202
+ ...context,
203
+ elapsedMs: Math.max(0, Date.now() - startedAt)
204
+ }, context.source != null);
205
+ }
206
+ }
207
+ function atSyncErrorBoundary(context, task) {
208
+ const startedAt = Date.now();
209
+ try {
210
+ return task();
211
+ } catch (error) {
212
+ throw recordErrorContext(error, {
213
+ ...context,
214
+ elapsedMs: Math.max(0, Date.now() - startedAt)
215
+ }, context.source != null);
216
+ }
217
+ }
218
+ //#endregion
174
219
  //#region src/items.ts
175
220
  const BINARY_CONTENT_TYPE = "application/octet-stream";
176
221
  function encodeItems(items) {
@@ -698,7 +743,7 @@ function classifyFetchError(err) {
698
743
  retriable: true,
699
744
  atMs: Date.now()
700
745
  };
701
- if (err instanceof TypeError) return {
746
+ if (errorContext(err)?.originalCode === "network_error") return {
702
747
  code: "network",
703
748
  message,
704
749
  retriable: true,
@@ -720,7 +765,33 @@ function classifyFetchError(err) {
720
765
  function isStreamNotFoundError(error) {
721
766
  return error instanceof HttpError && error.status === 404;
722
767
  }
723
- function toTransportError(error) {
768
+ function toTransportError(error, operation = "unknown") {
769
+ const evidence = errorContext(error);
770
+ const result = classifyTransportError(originalThrown(error), evidence?.originalCode === "network_error");
771
+ const source = isPayloadProtectionError(error) ? "payload_protection" : evidence?.source ?? (error instanceof AuthProviderError ? "auth" : error instanceof HttpError ? "http" : "unknown");
772
+ return {
773
+ ...result,
774
+ context: {
775
+ stage: "unknown",
776
+ originalCode: result.code,
777
+ ...error instanceof HttpError && { status: error.status },
778
+ ...error instanceof TimeoutError && {
779
+ phase: error.timeoutType,
780
+ timeoutMs: error.timeoutMs
781
+ },
782
+ ...error instanceof InitialSyncTimeoutError && {
783
+ stage: "initial_sync",
784
+ timeoutMs: error.timeoutMs,
785
+ elapsedMs: error.elapsedMs
786
+ },
787
+ ...evidence,
788
+ source,
789
+ retryable: result.retryable,
790
+ operation
791
+ }
792
+ };
793
+ }
794
+ function classifyTransportError(error, fromFetch) {
724
795
  if (isCrdtApplyIncompleteError(error)) return {
725
796
  code: "apply_incomplete",
726
797
  retryable: true,
@@ -810,10 +881,10 @@ function toTransportError(error) {
810
881
  reason: error.reason,
811
882
  message: payloadProtectionFailureMessage(error.reason)
812
883
  };
813
- if (error instanceof TypeError) return {
884
+ if (fromFetch) return {
814
885
  code: "network_error",
815
886
  retryable: true,
816
- message: error.message
887
+ message: error instanceof Error ? error.message : "Fetch failed"
817
888
  };
818
889
  if (error instanceof Error) return {
819
890
  code: "internal_error",
@@ -1057,7 +1128,10 @@ var StreamsTransportClient = class {
1057
1128
  return "large-post";
1058
1129
  }
1059
1130
  async createStreamRequest() {
1060
- const adapterHeaders = this.adapter.getCreateStreamHeaders ? await this.adapter.getCreateStreamHeaders() : void 0;
1131
+ const adapterHeaders = this.adapter.getCreateStreamHeaders ? await atErrorBoundary({
1132
+ source: "crdt",
1133
+ stage: "crdt_export"
1134
+ }, this.adapter.getCreateStreamHeaders.bind(this.adapter)) : void 0;
1061
1135
  const result = await this.client.create({
1062
1136
  headers: adapterHeaders,
1063
1137
  ttlSeconds: this.streamTtlSeconds
@@ -1246,7 +1320,7 @@ var StreamsTransportClient = class {
1246
1320
  const dataEncoding = response.headers.get("Stream-SSE-Data-Encoding") ?? void 0;
1247
1321
  const consume = async () => {
1248
1322
  let sawBootstrap = false;
1249
- for await (const event of readSseEvents(response.body, { onActivity })) {
1323
+ for await (const event of this.translateSseEvents(readSseEvents(response.body, { onActivity }))) {
1250
1324
  if (!sawBootstrap) {
1251
1325
  if (event.event !== "bootstrap") throw new ProtocolError(`ephemeral sse must start with bootstrap, got '${event.event}'`);
1252
1326
  await onBootstrap(decodeSsePayload(event.data, dataEncoding));
@@ -1302,10 +1376,13 @@ var StreamsTransportClient = class {
1302
1376
  controller.abort(new TimeoutError(phase, timeoutMs));
1303
1377
  }, timeoutMs);
1304
1378
  try {
1305
- return await this.fetchImpl(input, {
1379
+ return await atErrorBoundary({
1380
+ source: "fetch",
1381
+ originalCode: "network_error"
1382
+ }, () => this.fetchImpl(input, {
1306
1383
  ...init,
1307
1384
  signal
1308
- });
1385
+ }));
1309
1386
  } catch (error) {
1310
1387
  if (controller.signal.aborted && controller.signal.reason instanceof TimeoutError) throw controller.signal.reason;
1311
1388
  throw error;
@@ -1358,7 +1435,30 @@ var StreamsTransportClient = class {
1358
1435
  target.searchParams.set("live", "sse");
1359
1436
  return target.toString();
1360
1437
  }
1438
+ async *translateSseEvents(events) {
1439
+ try {
1440
+ yield* events;
1441
+ } catch (error) {
1442
+ throw this.toTransportFailure(thrownToStreamError(error, "live-sse"));
1443
+ }
1444
+ }
1361
1445
  toTransportFailure(error) {
1446
+ const failure = this.convertTransportFailure(error);
1447
+ if (!(failure instanceof InitialSyncTimeoutError)) {
1448
+ const { operation, ...context } = error.context ?? {
1449
+ source: "unknown",
1450
+ operation: "unknown",
1451
+ originalCode: error.code
1452
+ };
1453
+ recordErrorContext(failure, {
1454
+ ...context,
1455
+ requestOperation: operation,
1456
+ stage: operation === "bootstrap" ? "bootstrap" : operation === "read-once" ? "catch_up" : operation === "append" || operation === "append-cas" ? "upload" : "unknown"
1457
+ }, true);
1458
+ }
1459
+ return failure;
1460
+ }
1461
+ convertTransportFailure(error) {
1362
1462
  switch (error.code) {
1363
1463
  case "bad_request":
1364
1464
  case "unauthorized":
@@ -1366,13 +1466,16 @@ var StreamsTransportClient = class {
1366
1466
  case "not_found":
1367
1467
  case "conflict":
1368
1468
  case "gone":
1369
- case "rate_limited": return new HttpError(error.message, error.status, { body: error.bodyText });
1370
- case "timeout": return new TimeoutError(error.phase, error.phase === "poll" ? this.reconnectConfig.pollTimeoutMs : this.reconnectConfig.connectTimeoutMs);
1371
- case "network_error": return error.cause instanceof TypeError ? error.cause : new TypeError(error.message);
1469
+ case "rate_limited": return new HttpError(error.message, error.status, {
1470
+ body: error.bodyText,
1471
+ requestId: error.context?.requestId
1472
+ });
1473
+ case "timeout": return new TimeoutError(error.phase, error.context?.timeoutMs ?? (error.phase === "poll" ? this.reconnectConfig.pollTimeoutMs : this.reconnectConfig.connectTimeoutMs));
1474
+ case "network_error": return error.cause instanceof Error ? error.cause : new TypeError(error.message);
1372
1475
  case "protocol_error": return new ProtocolError(error.message, error.status);
1373
1476
  case "unknown":
1374
1477
  if (typeof error.status === "number") return new HttpError(error.message, error.status, {
1375
- requestId: extractRequestIdFromHeaders(error.headers),
1478
+ requestId: error.context?.requestId ?? extractRequestIdFromHeaders(error.headers),
1376
1479
  retryAfterMs: parseRetryAfterMs(error.headers?.["retry-after"]),
1377
1480
  body: error.bodyText
1378
1481
  });
@@ -1441,7 +1544,7 @@ var StreamsTransportClient = class {
1441
1544
  latestCursor,
1442
1545
  paused: false
1443
1546
  };
1444
- for await (const event of session.events) {
1547
+ for await (const event of this.translateSseEvents(session.events)) {
1445
1548
  if (event.event === "data") {
1446
1549
  pendingDataEvents.push(event.data);
1447
1550
  continue;
@@ -2036,11 +2139,17 @@ async function persistRemoteCursor(input) {
2036
2139
  ...input.cursor,
2037
2140
  updatedAtMs: (input.now ?? Date.now)()
2038
2141
  };
2039
- if (input.beforeRemoteCursorSave != null) await input.beforeRemoteCursorSave({
2142
+ if (input.beforeRemoteCursorSave != null) await atErrorBoundary({
2143
+ source: "durability_hook",
2144
+ stage: "durability_hook"
2145
+ }, input.beforeRemoteCursorSave.bind(void 0, {
2040
2146
  cursor: next,
2041
2147
  source: input.source
2042
- });
2043
- await input.remoteCursorStore.save(next);
2148
+ }));
2149
+ await atErrorBoundary({
2150
+ source: "cursor_store",
2151
+ stage: "cursor_save"
2152
+ }, () => input.remoteCursorStore.save(next));
2044
2153
  return next;
2045
2154
  }
2046
2155
  function key(streamUrl) {
@@ -2270,14 +2379,24 @@ var TransportCursorManager = class {
2270
2379
  let unresolved = [];
2271
2380
  if (snapshot.byteLength > 0) {
2272
2381
  if (typeof snapshotOffset !== "string" || snapshotOffset.length === 0 || snapshotOffset.length > 1024 || snapshotOffset === "now" || snapshotOffset === "-1") throw new Error("bootstrap snapshot offset does not match an admitted continuation");
2273
- const outcome = normalizeApplyReturn(await this.adapter.applySnapshot(this.decodeSnapshot == null ? snapshot : await this.decodeSnapshot(snapshot, snapshotOffset)));
2382
+ const clearSnapshot = this.decodeSnapshot == null ? snapshot : await atErrorBoundary({
2383
+ source: "local",
2384
+ stage: "payload_decode"
2385
+ }, this.decodeSnapshot.bind(this, snapshot, snapshotOffset));
2386
+ const outcome = normalizeApplyReturn(await atErrorBoundary({
2387
+ source: "crdt",
2388
+ stage: "crdt_apply"
2389
+ }, () => this.adapter.applySnapshot(clearSnapshot)));
2274
2390
  nextVersion = outcome.version;
2275
2391
  complete = outcome.complete;
2276
2392
  unresolved = outcome.unresolved ?? [];
2277
2393
  }
2278
2394
  const clearUpdates = await this.decodeRemoteUpdateItems(updates);
2279
2395
  if (clearUpdates.length > 0) {
2280
- const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
2396
+ const outcome = normalizeApplyReturn(await atErrorBoundary({
2397
+ source: "crdt",
2398
+ stage: "crdt_apply"
2399
+ }, async () => this.adapter.applyRemoteUpdates(clearUpdates, nextVersion)));
2281
2400
  nextVersion = outcome.version;
2282
2401
  complete = outcome.complete;
2283
2402
  unresolved = outcome.unresolved ?? [];
@@ -2305,7 +2424,10 @@ var TransportCursorManager = class {
2305
2424
  let unresolved = this.incompleteCursors.get(cursor) ?? [];
2306
2425
  const clearUpdates = await this.decodeRemoteUpdateItems(result.updates);
2307
2426
  if (clearUpdates.length > 0) {
2308
- const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
2427
+ const outcome = normalizeApplyReturn(await atErrorBoundary({
2428
+ source: "crdt",
2429
+ stage: "crdt_apply"
2430
+ }, async () => this.adapter.applyRemoteUpdates(clearUpdates, nextVersion)));
2309
2431
  nextVersion = outcome.version;
2310
2432
  complete = outcome.complete;
2311
2433
  unresolved = outcome.unresolved ?? [];
@@ -2324,7 +2446,10 @@ var TransportCursorManager = class {
2324
2446
  let unresolved = this.incompleteCursors.get(cursor) ?? [];
2325
2447
  const clearUpdates = await this.decodeRemoteUpdateItems(updates);
2326
2448
  if (clearUpdates.length > 0) {
2327
- const outcome = normalizeApplyReturn(await this.adapter.applyRemoteUpdates(clearUpdates, nextVersion));
2449
+ const outcome = normalizeApplyReturn(await atErrorBoundary({
2450
+ source: "crdt",
2451
+ stage: "crdt_apply"
2452
+ }, async () => this.adapter.applyRemoteUpdates(clearUpdates, nextVersion)));
2328
2453
  nextVersion = outcome.version;
2329
2454
  complete = outcome.complete;
2330
2455
  unresolved = outcome.unresolved ?? [];
@@ -2346,7 +2471,10 @@ var TransportCursorManager = class {
2346
2471
  }
2347
2472
  async decodeRemoteUpdateItems(items) {
2348
2473
  if (items.length === 0) return [];
2349
- return this.decodeUpdateItems == null ? [...items] : await this.decodeUpdateItems(items);
2474
+ return this.decodeUpdateItems == null ? [...items] : await atErrorBoundary({
2475
+ source: "local",
2476
+ stage: "payload_decode"
2477
+ }, this.decodeUpdateItems.bind(this, items));
2350
2478
  }
2351
2479
  };
2352
2480
  //#endregion
@@ -2453,8 +2581,14 @@ var TransportSnapshotManager = class {
2453
2581
  }
2454
2582
  if (tailBytes <= snapshotBytes) return;
2455
2583
  if (tailBytes - snapshotBytes < config.minBytesSinceRemoteSnapshot) return;
2456
- const snapshot = this.adapter.exportSnapshot();
2457
- await this.putSnapshotAt(state.cursor.nextOffset, this.encodeSnapshot == null ? snapshot : await this.encodeSnapshot(snapshot, state.cursor.nextOffset));
2584
+ const snapshot = atSyncErrorBoundary({
2585
+ source: "crdt",
2586
+ stage: "crdt_export"
2587
+ }, () => this.adapter.exportSnapshot());
2588
+ await this.putSnapshotAt(state.cursor.nextOffset, this.encodeSnapshot == null ? snapshot : await atErrorBoundary({
2589
+ source: "local",
2590
+ stage: "payload_encode"
2591
+ }, this.encodeSnapshot.bind(this, snapshot, state.cursor.nextOffset)));
2458
2592
  } catch (error) {
2459
2593
  this.logError("snapshot upload failed", error);
2460
2594
  }
@@ -2681,7 +2815,7 @@ var StreamsCrdt = class StreamsCrdt {
2681
2815
  state.started = this.startJoin(state);
2682
2816
  state.started.catch((error) => {
2683
2817
  if (state.closed) return;
2684
- const transportError = toTransportError(error);
2818
+ const transportError = toTransportError(error, "live");
2685
2819
  if (transportError.retryable) {
2686
2820
  state.initialSyncFailed = true;
2687
2821
  state.lastError = transportError;
@@ -2739,7 +2873,7 @@ var StreamsCrdt = class StreamsCrdt {
2739
2873
  } catch (error) {
2740
2874
  return {
2741
2875
  ok: false,
2742
- error: toTransportError(error)
2876
+ error: toTransportError(error, "snapshot")
2743
2877
  };
2744
2878
  }
2745
2879
  }
@@ -2751,10 +2885,19 @@ var StreamsCrdt = class StreamsCrdt {
2751
2885
  */
2752
2886
  async uploadSnapshotForTesting() {
2753
2887
  try {
2754
- await this.adapter.flushLocalExports?.();
2888
+ await atErrorBoundary({
2889
+ source: "crdt",
2890
+ stage: "crdt_export"
2891
+ }, () => this.adapter.flushLocalExports?.());
2755
2892
  const cursor = await this.syncOnce("sync");
2756
- const rawSnapshot = this.adapter.exportSnapshot();
2757
- const snapshot = this.encodeSnapshot == null ? rawSnapshot : await this.encodeSnapshot(rawSnapshot, cursor.nextOffset);
2893
+ const rawSnapshot = atSyncErrorBoundary({
2894
+ source: "crdt",
2895
+ stage: "crdt_export"
2896
+ }, () => this.adapter.exportSnapshot());
2897
+ const snapshot = this.encodeSnapshot == null ? rawSnapshot : await atErrorBoundary({
2898
+ source: "local",
2899
+ stage: "payload_encode"
2900
+ }, this.encodeSnapshot.bind(this, rawSnapshot, cursor.nextOffset));
2758
2901
  await this.client.putSnapshotAt(cursor.nextOffset, snapshot);
2759
2902
  return {
2760
2903
  ok: true,
@@ -2766,7 +2909,7 @@ var StreamsCrdt = class StreamsCrdt {
2766
2909
  } catch (error) {
2767
2910
  return {
2768
2911
  ok: false,
2769
- error: toTransportError(error)
2912
+ error: toTransportError(error, "snapshot")
2770
2913
  };
2771
2914
  }
2772
2915
  }
@@ -2782,7 +2925,7 @@ var StreamsCrdt = class StreamsCrdt {
2782
2925
  } catch (error) {
2783
2926
  return {
2784
2927
  ok: false,
2785
- error: toTransportError(error)
2928
+ error: toTransportError(error, "create")
2786
2929
  };
2787
2930
  }
2788
2931
  }
@@ -2796,7 +2939,10 @@ var StreamsCrdt = class StreamsCrdt {
2796
2939
  ok: true,
2797
2940
  value: { deleted: await this.enqueueExclusive(async () => {
2798
2941
  const deleted = await this.client.deleteStreamRequest();
2799
- await this.remoteCursorStore.delete?.(this.streamUrl);
2942
+ await atErrorBoundary({
2943
+ source: "cursor_store",
2944
+ stage: "cursor_delete"
2945
+ }, () => this.remoteCursorStore.delete?.(this.streamUrl));
2800
2946
  this.producerEpoch = 0;
2801
2947
  this.nextProducerSeq = 0;
2802
2948
  this.frozenLocalAppend = void 0;
@@ -2808,7 +2954,7 @@ var StreamsCrdt = class StreamsCrdt {
2808
2954
  } catch (error) {
2809
2955
  return {
2810
2956
  ok: false,
2811
- error: toTransportError(error)
2957
+ error: toTransportError(error, "delete")
2812
2958
  };
2813
2959
  }
2814
2960
  }
@@ -2825,7 +2971,7 @@ var StreamsCrdt = class StreamsCrdt {
2825
2971
  } catch (error) {
2826
2972
  return {
2827
2973
  ok: false,
2828
- error: toTransportError(error)
2974
+ error: toTransportError(error, "sync")
2829
2975
  };
2830
2976
  }
2831
2977
  }
@@ -2853,7 +2999,7 @@ var StreamsCrdt = class StreamsCrdt {
2853
2999
  } catch (error) {
2854
3000
  return {
2855
3001
  ok: false,
2856
- error: toTransportError(error)
3002
+ error: toTransportError(error, "catchup")
2857
3003
  };
2858
3004
  }
2859
3005
  }
@@ -2868,7 +3014,10 @@ var StreamsCrdt = class StreamsCrdt {
2868
3014
  return await this.enqueueExclusive(async () => {
2869
3015
  try {
2870
3016
  if (this.writeOnlyDisabledReason != null) throw new Error(this.writeOnlyDisabledReason);
2871
- await this.adapter.flushLocalExports?.();
3017
+ await atErrorBoundary({
3018
+ source: "crdt",
3019
+ stage: "crdt_export"
3020
+ }, () => this.adapter.flushLocalExports?.());
2872
3021
  const drained = await this.drainPendingWriteOnlyBatch();
2873
3022
  if (drained == null) return {
2874
3023
  ok: true,
@@ -2893,7 +3042,7 @@ var StreamsCrdt = class StreamsCrdt {
2893
3042
  } catch (error) {
2894
3043
  return {
2895
3044
  ok: false,
2896
- error: toTransportError(error)
3045
+ error: toTransportError(error, "upload")
2897
3046
  };
2898
3047
  }
2899
3048
  });
@@ -2918,14 +3067,17 @@ var StreamsCrdt = class StreamsCrdt {
2918
3067
  } catch (error) {
2919
3068
  return {
2920
3069
  ok: false,
2921
- error: toTransportError(error)
3070
+ error: toTransportError(error, "join")
2922
3071
  };
2923
3072
  }
2924
3073
  }
2925
3074
  const state = createJoinState(createInitialRemoteCursor({
2926
3075
  streamUrl: this.streamUrl,
2927
3076
  nextOffset: "-1",
2928
- serverLowerBoundVersion: this.adapter.emptyVersion()
3077
+ serverLowerBoundVersion: atSyncErrorBoundary({
3078
+ source: "crdt",
3079
+ stage: "crdt_export"
3080
+ }, () => this.adapter.emptyVersion())
2929
3081
  }));
2930
3082
  const statusListener = params?.onStatusChange;
2931
3083
  if (statusListener != null) state.statusListeners.add(statusListener);
@@ -2938,7 +3090,7 @@ var StreamsCrdt = class StreamsCrdt {
2938
3090
  value: this.createSubscription(state, params)
2939
3091
  };
2940
3092
  } catch (error) {
2941
- const transportError = toTransportError(error);
3093
+ const transportError = toTransportError(error, "join");
2942
3094
  if (transportError.retryable && !state.closed) {
2943
3095
  state.initialSyncFailed = true;
2944
3096
  state.lastError = transportError;
@@ -3008,7 +3160,7 @@ var StreamsCrdt = class StreamsCrdt {
3008
3160
  if (state.readSubStatus === "connecting") try {
3009
3161
  await state.started;
3010
3162
  } catch (error) {
3011
- const te = toTransportError(error);
3163
+ const te = toTransportError(error, "join");
3012
3164
  throw new Error(te.code ? `initial sync failed: ${te.code}` : "initial sync failed");
3013
3165
  }
3014
3166
  if (state.writeSubStatus === "error") {
@@ -3016,7 +3168,10 @@ var StreamsCrdt = class StreamsCrdt {
3016
3168
  const msg = state.lastError?.code ? `local append failed: ${state.lastError.code}` : "local append failed";
3017
3169
  throw new Error(msg);
3018
3170
  }
3019
- await this.adapter.flushLocalExports?.();
3171
+ await atErrorBoundary({
3172
+ source: "crdt",
3173
+ stage: "crdt_export"
3174
+ }, () => this.adapter.flushLocalExports?.());
3020
3175
  if (state.closed) throw new Error("subscription closed");
3021
3176
  const remaining = state.pendingLocal.length;
3022
3177
  if (remaining === 0) return;
@@ -3059,6 +3214,11 @@ var StreamsCrdt = class StreamsCrdt {
3059
3214
  if (typeof hardTimeoutMs === "number" && Number.isFinite(hardTimeoutMs) && hardTimeoutMs > 0) hardTimeoutPromise = new Promise((_, reject) => {
3060
3215
  hardTimer = setTimeout(() => {
3061
3216
  hardTimeoutError = new InitialSyncTimeoutError(operation, hardTimeoutMs, Date.now() - startedAtMs);
3217
+ recordErrorContext(hardTimeoutError, {
3218
+ source: "deadline",
3219
+ stage: "initial_sync",
3220
+ originalCode: "initial_sync_timeout"
3221
+ });
3062
3222
  hardController.abort(hardTimeoutError);
3063
3223
  reject(hardTimeoutError);
3064
3224
  }, hardTimeoutMs);
@@ -3107,7 +3267,7 @@ var StreamsCrdt = class StreamsCrdt {
3107
3267
  } catch (error) {
3108
3268
  if (state.closed) return;
3109
3269
  this.snapshotManager.dispose(state);
3110
- state.lastError = toTransportError(error);
3270
+ state.lastError = toTransportError(error, "join");
3111
3271
  this.logError("join first sync failed", error, { streamUrl: this.streamUrl });
3112
3272
  state.readSubStatus = "error";
3113
3273
  state.writeSubStatus = "error";
@@ -3155,7 +3315,10 @@ var StreamsCrdt = class StreamsCrdt {
3155
3315
  this.commitProducerAck(ack.producerAck);
3156
3316
  didAppend = true;
3157
3317
  }
3158
- await this.adapter.flushLocalExports?.();
3318
+ await atErrorBoundary({
3319
+ source: "crdt",
3320
+ stage: "crdt_export"
3321
+ }, () => this.adapter.flushLocalExports?.());
3159
3322
  const localBatch = this.exportUpdates(appliedVersion);
3160
3323
  if (localBatch.batch != null) {
3161
3324
  const appendedBatch = await this.appendLocalBatchRemote({
@@ -3176,7 +3339,10 @@ var StreamsCrdt = class StreamsCrdt {
3176
3339
  }
3177
3340
  async syncOnce(operation, parentSignal) {
3178
3341
  return this.enqueueExclusive(async () => {
3179
- await this.adapter.flushLocalExports?.();
3342
+ await atErrorBoundary({
3343
+ source: "crdt",
3344
+ stage: "crdt_export"
3345
+ }, () => this.adapter.flushLocalExports?.());
3180
3346
  return this.syncOnceExclusive(operation, parentSignal);
3181
3347
  });
3182
3348
  }
@@ -3240,11 +3406,17 @@ var StreamsCrdt = class StreamsCrdt {
3240
3406
  return createInitialRemoteCursor({
3241
3407
  streamUrl: this.streamUrl,
3242
3408
  nextOffset: "-1",
3243
- serverLowerBoundVersion: this.adapter.emptyVersion()
3409
+ serverLowerBoundVersion: atSyncErrorBoundary({
3410
+ source: "crdt",
3411
+ stage: "crdt_export"
3412
+ }, () => this.adapter.emptyVersion())
3244
3413
  });
3245
3414
  }
3246
3415
  async resolveInitialRemoteState(operation, parentSignal) {
3247
- const loaded = await this.remoteCursorStore.load(this.streamUrl);
3416
+ const loaded = await atErrorBoundary({
3417
+ source: "cursor_store",
3418
+ stage: "cursor_load"
3419
+ }, () => this.remoteCursorStore.load(this.streamUrl));
3248
3420
  const cursor = loaded ?? this.createInitialCursor();
3249
3421
  const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
3250
3422
  try {
@@ -3269,7 +3441,10 @@ var StreamsCrdt = class StreamsCrdt {
3269
3441
  }
3270
3442
  }
3271
3443
  async resolvePullOnlyRemoteState(operation, parentSignal) {
3272
- const loaded = await this.remoteCursorStore.load(this.streamUrl);
3444
+ const loaded = await atErrorBoundary({
3445
+ source: "cursor_store",
3446
+ stage: "cursor_load"
3447
+ }, () => this.remoteCursorStore.load(this.streamUrl));
3273
3448
  const cursor = loaded ?? this.createInitialCursor();
3274
3449
  const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
3275
3450
  try {
@@ -3311,7 +3486,7 @@ var StreamsCrdt = class StreamsCrdt {
3311
3486
  }
3312
3487
  async bootstrapState(cursor, options = {}) {
3313
3488
  const cursorManager = options.cursorManager ?? this.cursorManager;
3314
- const result = await this.client.bootstrap(options.signal, options.timeoutMode);
3489
+ const result = await atErrorBoundary({ stage: "bootstrap" }, () => this.client.bootstrap(options.signal, options.timeoutMode));
3315
3490
  let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate, result.snapshotOffset);
3316
3491
  let bootstrapCursor = result.streamCursor;
3317
3492
  if (!result.upToDate) {
@@ -3348,13 +3523,16 @@ var StreamsCrdt = class StreamsCrdt {
3348
3523
  const cursorManager = options.cursorManager ?? this.cursorManager;
3349
3524
  while (true) {
3350
3525
  const requestOffset = current.nextOffset;
3351
- const response = await this.client.readFromOffset(requestOffset, currentStreamCursor, options.live, options.signal, options.timeoutMode);
3526
+ const response = await atErrorBoundary({ stage: "catch_up" }, () => this.client.readFromOffset(requestOffset, currentStreamCursor, options.live, options.signal, options.timeoutMode));
3352
3527
  if (response.kind === "gone") {
3353
3528
  if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
3354
3529
  return await this.bootstrapState({
3355
3530
  ...current,
3356
3531
  nextOffset: "-1",
3357
- serverLowerBoundVersion: this.adapter.emptyVersion()
3532
+ serverLowerBoundVersion: atSyncErrorBoundary({
3533
+ source: "crdt",
3534
+ stage: "crdt_export"
3535
+ }, () => this.adapter.emptyVersion())
3358
3536
  }, {
3359
3537
  cursorManager,
3360
3538
  signal: options.signal,
@@ -3369,7 +3547,10 @@ var StreamsCrdt = class StreamsCrdt {
3369
3547
  if (error instanceof CrdtApplyIncompleteError && result.upToDate) return await this.bootstrapState({
3370
3548
  ...current,
3371
3549
  nextOffset: "-1",
3372
- serverLowerBoundVersion: this.adapter.emptyVersion()
3550
+ serverLowerBoundVersion: atSyncErrorBoundary({
3551
+ source: "crdt",
3552
+ stage: "crdt_export"
3553
+ }, () => this.adapter.emptyVersion())
3373
3554
  }, {
3374
3555
  cursorManager,
3375
3556
  signal: options.signal,
@@ -3390,7 +3571,10 @@ var StreamsCrdt = class StreamsCrdt {
3390
3571
  const bootstrapped = await this.bootstrapState({
3391
3572
  ...state.cursor,
3392
3573
  nextOffset: "-1",
3393
- serverLowerBoundVersion: this.adapter.emptyVersion()
3574
+ serverLowerBoundVersion: atSyncErrorBoundary({
3575
+ source: "crdt",
3576
+ stage: "crdt_export"
3577
+ }, () => this.adapter.emptyVersion())
3394
3578
  }, { signal: state.requestController?.signal });
3395
3579
  state.cursor = bootstrapped.cursor;
3396
3580
  state.streamCursor = bootstrapped.streamCursor;
@@ -3406,7 +3590,7 @@ var StreamsCrdt = class StreamsCrdt {
3406
3590
  streamUrl: this.streamUrl,
3407
3591
  code: classified.code
3408
3592
  });
3409
- state.lastError = toTransportError(error);
3593
+ state.lastError = toTransportError(error, "live");
3410
3594
  if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
3411
3595
  if (classified.retriable && !isAuthOrProtocolError(error)) {
3412
3596
  state.incompleteRecoveryAttempt += 1;
@@ -3439,7 +3623,10 @@ var StreamsCrdt = class StreamsCrdt {
3439
3623
  localAppendReadCursor(frozen) {
3440
3624
  return {
3441
3625
  ...frozen.baseCursor,
3442
- serverLowerBoundVersion: this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version)
3626
+ serverLowerBoundVersion: atSyncErrorBoundary({
3627
+ source: "crdt",
3628
+ stage: "crdt_export"
3629
+ }, () => this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version))
3443
3630
  };
3444
3631
  }
3445
3632
  async catchupLocalAppend(frozen, streamCursor) {
@@ -3471,7 +3658,10 @@ var StreamsCrdt = class StreamsCrdt {
3471
3658
  baseCursor: cursor,
3472
3659
  batch,
3473
3660
  pendingCount,
3474
- wireUpdates: this.encodeUpdateItems == null ? batch.updates : await this.encodeUpdateItems(batch.updates),
3661
+ wireUpdates: this.encodeUpdateItems == null ? batch.updates : await atErrorBoundary({
3662
+ source: "local",
3663
+ stage: "payload_encode"
3664
+ }, this.encodeUpdateItems.bind(this, batch.updates)),
3475
3665
  producerSession: this.createCurrentProducerSession()
3476
3666
  };
3477
3667
  this.frozenLocalAppend = frozen;
@@ -3482,7 +3672,7 @@ var StreamsCrdt = class StreamsCrdt {
3482
3672
  for (let attempt = 0; attempt < 3; attempt += 1) {
3483
3673
  let result;
3484
3674
  try {
3485
- result = await this.client.appendUpdates(frozen.wireUpdates, frozen.producerSession);
3675
+ result = await atErrorBoundary({ stage: "upload" }, () => this.client.appendUpdates(frozen.wireUpdates, frozen.producerSession));
3486
3676
  } catch (error) {
3487
3677
  if (isStreamNotFoundError(error) && this.createStreamIfMissing) {
3488
3678
  await this.ensureStreamExists();
@@ -3495,7 +3685,10 @@ var StreamsCrdt = class StreamsCrdt {
3495
3685
  cursor: {
3496
3686
  ...frozen.baseCursor,
3497
3687
  nextOffset: result.nextOffset,
3498
- serverLowerBoundVersion: this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version)
3688
+ serverLowerBoundVersion: atSyncErrorBoundary({
3689
+ source: "crdt",
3690
+ stage: "crdt_export"
3691
+ }, () => this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version))
3499
3692
  },
3500
3693
  producerAck: {
3501
3694
  epoch: result.ackEpoch,
@@ -3570,9 +3763,15 @@ var StreamsCrdt = class StreamsCrdt {
3570
3763
  }
3571
3764
  }
3572
3765
  exportUpdates(fromVersion) {
3573
- const exported = this.adapter.exportUpdates(fromVersion);
3766
+ const exported = atSyncErrorBoundary({
3767
+ source: "crdt",
3768
+ stage: "crdt_export"
3769
+ }, () => this.adapter.exportUpdates(fromVersion));
3574
3770
  if (exported == null) return { nextRefVersion: fromVersion };
3575
- const nextRefVersion = this.adapter.mergeVersions(fromVersion, exported.version);
3771
+ const nextRefVersion = atSyncErrorBoundary({
3772
+ source: "crdt",
3773
+ stage: "crdt_export"
3774
+ }, () => this.adapter.mergeVersions(fromVersion, exported.version));
3576
3775
  if (exported.updates.length === 0) return { nextRefVersion };
3577
3776
  return {
3578
3777
  batch: exported,
@@ -3629,10 +3828,16 @@ var StreamsCrdt = class StreamsCrdt {
3629
3828
  const bootstrapCursor = {
3630
3829
  ...cursor,
3631
3830
  nextOffset: "-1",
3632
- serverLowerBoundVersion: this.adapter.emptyVersion()
3831
+ serverLowerBoundVersion: atSyncErrorBoundary({
3832
+ source: "crdt",
3833
+ stage: "crdt_export"
3834
+ }, () => this.adapter.emptyVersion())
3633
3835
  };
3634
3836
  if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager, operation, parentSignal);
3635
- const preservedLocalSnapshot = this.adapter.exportSnapshot();
3837
+ const preservedLocalSnapshot = atSyncErrorBoundary({
3838
+ source: "crdt",
3839
+ stage: "crdt_export"
3840
+ }, () => this.adapter.exportSnapshot());
3636
3841
  let restoredLocalSnapshot = false;
3637
3842
  try {
3638
3843
  const inferredRemote = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
@@ -3640,7 +3845,10 @@ var StreamsCrdt = class StreamsCrdt {
3640
3845
  signal,
3641
3846
  timeoutMode: "initial"
3642
3847
  }), parentSignal);
3643
- await this.adapter.applySnapshot(preservedLocalSnapshot);
3848
+ await atErrorBoundary({
3849
+ source: "crdt",
3850
+ stage: "crdt_apply"
3851
+ }, async () => this.adapter.applySnapshot(preservedLocalSnapshot));
3644
3852
  restoredLocalSnapshot = true;
3645
3853
  let pendingAppend;
3646
3854
  if (this.frozenLocalAppend != null) {
@@ -3657,7 +3865,10 @@ var StreamsCrdt = class StreamsCrdt {
3657
3865
  if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
3658
3866
  return result;
3659
3867
  } catch (error) {
3660
- if (!restoredLocalSnapshot) await this.adapter.applySnapshot(preservedLocalSnapshot);
3868
+ if (!restoredLocalSnapshot) await atErrorBoundary({
3869
+ source: "crdt",
3870
+ stage: "crdt_apply"
3871
+ }, async () => this.adapter.applySnapshot(preservedLocalSnapshot));
3661
3872
  throw error;
3662
3873
  }
3663
3874
  }
@@ -3744,7 +3955,7 @@ var StreamsCrdt = class StreamsCrdt {
3744
3955
  streamUrl: this.streamUrl,
3745
3956
  attempts: state.retryAttempt
3746
3957
  });
3747
- state.lastError = toTransportError(error);
3958
+ state.lastError = toTransportError(error, "live");
3748
3959
  this.setReadSubStatus(state, "disconnected");
3749
3960
  return false;
3750
3961
  }
@@ -3777,12 +3988,15 @@ var StreamsCrdt = class StreamsCrdt {
3777
3988
  try {
3778
3989
  if (state.liveMode === "long-poll") {
3779
3990
  const requestOffset = state.cursor.nextOffset;
3780
- const response = await this.client.readFromOffset(requestOffset, state.streamCursor, "long-poll", capturedController?.signal);
3991
+ const response = await atErrorBoundary({ stage: "catch_up" }, () => this.client.readFromOffset(requestOffset, state.streamCursor, "long-poll", capturedController?.signal));
3781
3992
  if (response.kind === "gone") {
3782
3993
  const bootstrapped = await this.bootstrapState({
3783
3994
  ...state.cursor,
3784
3995
  nextOffset: "-1",
3785
- serverLowerBoundVersion: this.adapter.emptyVersion()
3996
+ serverLowerBoundVersion: atSyncErrorBoundary({
3997
+ source: "crdt",
3998
+ stage: "crdt_export"
3999
+ }, () => this.adapter.emptyVersion())
3786
4000
  }, { signal: capturedController?.signal });
3787
4001
  state.cursor = bootstrapped.cursor;
3788
4002
  state.streamCursor = bootstrapped.streamCursor;
@@ -3869,14 +4083,17 @@ var StreamsCrdt = class StreamsCrdt {
3869
4083
  streamUrl: this.streamUrl,
3870
4084
  code: "stream_not_found"
3871
4085
  });
3872
- state.lastError = toTransportError(notFoundError);
4086
+ state.lastError = toTransportError(notFoundError, "live");
3873
4087
  this.setReadSubStatus(state, "error");
3874
4088
  return;
3875
4089
  }
3876
4090
  const bootstrapped = await this.bootstrapState({
3877
4091
  ...state.cursor,
3878
4092
  nextOffset: "-1",
3879
- serverLowerBoundVersion: this.adapter.emptyVersion()
4093
+ serverLowerBoundVersion: atSyncErrorBoundary({
4094
+ source: "crdt",
4095
+ stage: "crdt_export"
4096
+ }, () => this.adapter.emptyVersion())
3880
4097
  }, { signal: capturedController?.signal });
3881
4098
  state.cursor = bootstrapped.cursor;
3882
4099
  state.streamCursor = bootstrapped.streamCursor;
@@ -3933,7 +4150,7 @@ var StreamsCrdt = class StreamsCrdt {
3933
4150
  streamUrl: this.streamUrl,
3934
4151
  code: flushClassified.code
3935
4152
  });
3936
- state.lastError = toTransportError(flushError);
4153
+ state.lastError = toTransportError(flushError, "live");
3937
4154
  this.setReadSubStatus(state, "error");
3938
4155
  return;
3939
4156
  }
@@ -3948,7 +4165,7 @@ var StreamsCrdt = class StreamsCrdt {
3948
4165
  streamUrl: this.streamUrl,
3949
4166
  code: classified.code
3950
4167
  });
3951
- state.lastError = toTransportError(error);
4168
+ state.lastError = toTransportError(error, "live");
3952
4169
  this.setReadSubStatus(state, "error");
3953
4170
  return;
3954
4171
  }
@@ -3981,7 +4198,10 @@ var StreamsCrdt = class StreamsCrdt {
3981
4198
  const candidateUpdates = [...allUpdates, ...next.updates];
3982
4199
  if (await this.measureAppendBodyByteLength(candidateUpdates) > maxBytes) break;
3983
4200
  allUpdates.push(...next.updates);
3984
- mergedVersion = this.adapter.mergeVersions(mergedVersion, next.version);
4201
+ mergedVersion = atSyncErrorBoundary({
4202
+ source: "crdt",
4203
+ stage: "crdt_export"
4204
+ }, () => this.adapter.mergeVersions(mergedVersion, next.version));
3985
4205
  count += 1;
3986
4206
  }
3987
4207
  if (count === 1) return {
@@ -4078,7 +4298,7 @@ var StreamsCrdt = class StreamsCrdt {
4078
4298
  break;
4079
4299
  } catch (error) {
4080
4300
  const classified = classifyFetchError(error);
4081
- const transportError = toTransportError(error);
4301
+ const transportError = toTransportError(error, "upload");
4082
4302
  if (!classified.retriable || isAuthOrProtocolError(error)) {
4083
4303
  if (isPayloadTooLargeError(error)) state.flushHeadBatchOnly = true;
4084
4304
  this.logError("local append failed", error, {
@@ -4255,4 +4475,4 @@ function isValidRillId(id) {
4255
4475
  //#endregion
4256
4476
  export { createInitialRemoteCursor as a, PayloadProtectionError as c, IndexedDbRemoteCursorStore as i, StreamsCrdt as n, EphemeralStreamCrdt as o, InMemoryRemoteCursorStore as r, LocalAppendFailedError as s, isValidRillId as t };
4257
4477
 
4258
- //# sourceMappingURL=stream-id-BERJQKYV.js.map
4478
+ //# sourceMappingURL=stream-id-Dlnc1PNH.js.map