@loro-dev/streams-crdt 0.16.0 → 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({
@@ -3175,12 +3338,49 @@ var StreamsCrdt = class StreamsCrdt {
3175
3338
  }
3176
3339
  }
3177
3340
  async syncOnce(operation, parentSignal) {
3178
- let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState(operation, parentSignal))).cursor;
3341
+ return this.enqueueExclusive(async () => {
3342
+ await atErrorBoundary({
3343
+ source: "crdt",
3344
+ stage: "crdt_export"
3345
+ }, () => this.adapter.flushLocalExports?.());
3346
+ return this.syncOnceExclusive(operation, parentSignal);
3347
+ });
3348
+ }
3349
+ /** Caller owns opQueue; never wait for the background flush from here. */
3350
+ async syncOnceExclusive(operation, parentSignal) {
3351
+ const state = this.joinState;
3352
+ if (state != null && !state.closed) while (!state.closed && state.pendingLocal.length > 0) await this.flushPendingLocalBatchExclusive(state);
3353
+ if ((state == null || state.closed) && this.frozenLocalAppend?.source === "pending") {
3354
+ const frozen = this.frozenLocalAppend;
3355
+ const appended = await this.appendFrozenLocalBatchRemote(frozen);
3356
+ await this.catchupLocalAppend(frozen);
3357
+ this.commitProducerAck(appended.producerAck);
3358
+ }
3359
+ const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState(operation, parentSignal));
3360
+ let cursor = initial.cursor;
3361
+ if (state != null && !state.closed) {
3362
+ state.cursor = cursor;
3363
+ state.streamCursor = initial.streamCursor;
3364
+ while (!state.closed && state.pendingLocal.length > 0) {
3365
+ await this.flushPendingLocalBatchExclusive(state);
3366
+ cursor = state.cursor;
3367
+ }
3368
+ }
3179
3369
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
3180
- if (localBatch.batch != null) cursor = (await this.appendLocalBatchDurably(cursor, localBatch.batch)).cursor;
3370
+ if (localBatch.batch != null) {
3371
+ const appended = await this.appendLocalBatchDurably(cursor, localBatch.batch);
3372
+ cursor = appended.cursor;
3373
+ if (state != null && !state.closed) {
3374
+ state.cursor = cursor;
3375
+ state.streamCursor = appended.streamCursor;
3376
+ }
3377
+ }
3181
3378
  return cursor;
3182
3379
  }
3183
3380
  async performInitialJoinSync(preferredLiveMode, parentSignal) {
3381
+ return this.enqueueExclusive(() => this.performInitialJoinSyncExclusive(preferredLiveMode, parentSignal));
3382
+ }
3383
+ async performInitialJoinSyncExclusive(preferredLiveMode, parentSignal) {
3184
3384
  const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState("join", parentSignal));
3185
3385
  let cursor = initial.cursor;
3186
3386
  let localExportRefVersion = cursor.serverLowerBoundVersion;
@@ -3190,9 +3390,7 @@ var StreamsCrdt = class StreamsCrdt {
3190
3390
  localExportRefVersion = exportedLocal.nextRefVersion;
3191
3391
  const exportedBatch = exportedLocal.batch;
3192
3392
  if (exportedBatch != null) {
3193
- const appendCursor = cursor;
3194
- const appendStreamCursor = streamCursor;
3195
- const committed = await this.enqueueExclusive(() => this.appendLocalBatchDurably(appendCursor, exportedBatch, void 0, "direct", appendStreamCursor));
3393
+ const committed = await this.appendLocalBatchDurably(cursor, exportedBatch, void 0, "direct", streamCursor);
3196
3394
  cursor = committed.cursor;
3197
3395
  streamCursor = committed.streamCursor;
3198
3396
  }
@@ -3208,11 +3406,17 @@ var StreamsCrdt = class StreamsCrdt {
3208
3406
  return createInitialRemoteCursor({
3209
3407
  streamUrl: this.streamUrl,
3210
3408
  nextOffset: "-1",
3211
- serverLowerBoundVersion: this.adapter.emptyVersion()
3409
+ serverLowerBoundVersion: atSyncErrorBoundary({
3410
+ source: "crdt",
3411
+ stage: "crdt_export"
3412
+ }, () => this.adapter.emptyVersion())
3212
3413
  });
3213
3414
  }
3214
3415
  async resolveInitialRemoteState(operation, parentSignal) {
3215
- 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));
3216
3420
  const cursor = loaded ?? this.createInitialCursor();
3217
3421
  const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
3218
3422
  try {
@@ -3237,7 +3441,10 @@ var StreamsCrdt = class StreamsCrdt {
3237
3441
  }
3238
3442
  }
3239
3443
  async resolvePullOnlyRemoteState(operation, parentSignal) {
3240
- 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));
3241
3448
  const cursor = loaded ?? this.createInitialCursor();
3242
3449
  const runInitial = (task) => this.runInitialSyncWithDeadline(operation, task, parentSignal);
3243
3450
  try {
@@ -3279,7 +3486,7 @@ var StreamsCrdt = class StreamsCrdt {
3279
3486
  }
3280
3487
  async bootstrapState(cursor, options = {}) {
3281
3488
  const cursorManager = options.cursorManager ?? this.cursorManager;
3282
- const result = await this.client.bootstrap(options.signal, options.timeoutMode);
3489
+ const result = await atErrorBoundary({ stage: "bootstrap" }, () => this.client.bootstrap(options.signal, options.timeoutMode));
3283
3490
  let saved = await cursorManager.applyBootstrapPayload(cursor, result.snapshot, result.updates, result.nextOffset, result.upToDate, result.snapshotOffset);
3284
3491
  let bootstrapCursor = result.streamCursor;
3285
3492
  if (!result.upToDate) {
@@ -3316,13 +3523,16 @@ var StreamsCrdt = class StreamsCrdt {
3316
3523
  const cursorManager = options.cursorManager ?? this.cursorManager;
3317
3524
  while (true) {
3318
3525
  const requestOffset = current.nextOffset;
3319
- 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));
3320
3527
  if (response.kind === "gone") {
3321
3528
  if (goneStrategy === "return-gone") return StreamsCrdt.GONE_CATCHUP;
3322
3529
  return await this.bootstrapState({
3323
3530
  ...current,
3324
3531
  nextOffset: "-1",
3325
- serverLowerBoundVersion: this.adapter.emptyVersion()
3532
+ serverLowerBoundVersion: atSyncErrorBoundary({
3533
+ source: "crdt",
3534
+ stage: "crdt_export"
3535
+ }, () => this.adapter.emptyVersion())
3326
3536
  }, {
3327
3537
  cursorManager,
3328
3538
  signal: options.signal,
@@ -3337,7 +3547,10 @@ var StreamsCrdt = class StreamsCrdt {
3337
3547
  if (error instanceof CrdtApplyIncompleteError && result.upToDate) return await this.bootstrapState({
3338
3548
  ...current,
3339
3549
  nextOffset: "-1",
3340
- serverLowerBoundVersion: this.adapter.emptyVersion()
3550
+ serverLowerBoundVersion: atSyncErrorBoundary({
3551
+ source: "crdt",
3552
+ stage: "crdt_export"
3553
+ }, () => this.adapter.emptyVersion())
3341
3554
  }, {
3342
3555
  cursorManager,
3343
3556
  signal: options.signal,
@@ -3358,7 +3571,10 @@ var StreamsCrdt = class StreamsCrdt {
3358
3571
  const bootstrapped = await this.bootstrapState({
3359
3572
  ...state.cursor,
3360
3573
  nextOffset: "-1",
3361
- serverLowerBoundVersion: this.adapter.emptyVersion()
3574
+ serverLowerBoundVersion: atSyncErrorBoundary({
3575
+ source: "crdt",
3576
+ stage: "crdt_export"
3577
+ }, () => this.adapter.emptyVersion())
3362
3578
  }, { signal: state.requestController?.signal });
3363
3579
  state.cursor = bootstrapped.cursor;
3364
3580
  state.streamCursor = bootstrapped.streamCursor;
@@ -3374,7 +3590,7 @@ var StreamsCrdt = class StreamsCrdt {
3374
3590
  streamUrl: this.streamUrl,
3375
3591
  code: classified.code
3376
3592
  });
3377
- state.lastError = toTransportError(error);
3593
+ state.lastError = toTransportError(error, "live");
3378
3594
  if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
3379
3595
  if (classified.retriable && !isAuthOrProtocolError(error)) {
3380
3596
  state.incompleteRecoveryAttempt += 1;
@@ -3407,7 +3623,10 @@ var StreamsCrdt = class StreamsCrdt {
3407
3623
  localAppendReadCursor(frozen) {
3408
3624
  return {
3409
3625
  ...frozen.baseCursor,
3410
- 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))
3411
3630
  };
3412
3631
  }
3413
3632
  async catchupLocalAppend(frozen, streamCursor) {
@@ -3439,7 +3658,10 @@ var StreamsCrdt = class StreamsCrdt {
3439
3658
  baseCursor: cursor,
3440
3659
  batch,
3441
3660
  pendingCount,
3442
- 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)),
3443
3665
  producerSession: this.createCurrentProducerSession()
3444
3666
  };
3445
3667
  this.frozenLocalAppend = frozen;
@@ -3450,7 +3672,7 @@ var StreamsCrdt = class StreamsCrdt {
3450
3672
  for (let attempt = 0; attempt < 3; attempt += 1) {
3451
3673
  let result;
3452
3674
  try {
3453
- result = await this.client.appendUpdates(frozen.wireUpdates, frozen.producerSession);
3675
+ result = await atErrorBoundary({ stage: "upload" }, () => this.client.appendUpdates(frozen.wireUpdates, frozen.producerSession));
3454
3676
  } catch (error) {
3455
3677
  if (isStreamNotFoundError(error) && this.createStreamIfMissing) {
3456
3678
  await this.ensureStreamExists();
@@ -3463,7 +3685,10 @@ var StreamsCrdt = class StreamsCrdt {
3463
3685
  cursor: {
3464
3686
  ...frozen.baseCursor,
3465
3687
  nextOffset: result.nextOffset,
3466
- 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))
3467
3692
  },
3468
3693
  producerAck: {
3469
3694
  epoch: result.ackEpoch,
@@ -3538,9 +3763,15 @@ var StreamsCrdt = class StreamsCrdt {
3538
3763
  }
3539
3764
  }
3540
3765
  exportUpdates(fromVersion) {
3541
- const exported = this.adapter.exportUpdates(fromVersion);
3766
+ const exported = atSyncErrorBoundary({
3767
+ source: "crdt",
3768
+ stage: "crdt_export"
3769
+ }, () => this.adapter.exportUpdates(fromVersion));
3542
3770
  if (exported == null) return { nextRefVersion: fromVersion };
3543
- 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));
3544
3775
  if (exported.updates.length === 0) return { nextRefVersion };
3545
3776
  return {
3546
3777
  batch: exported,
@@ -3597,10 +3828,16 @@ var StreamsCrdt = class StreamsCrdt {
3597
3828
  const bootstrapCursor = {
3598
3829
  ...cursor,
3599
3830
  nextOffset: "-1",
3600
- serverLowerBoundVersion: this.adapter.emptyVersion()
3831
+ serverLowerBoundVersion: atSyncErrorBoundary({
3832
+ source: "crdt",
3833
+ stage: "crdt_export"
3834
+ }, () => this.adapter.emptyVersion())
3601
3835
  };
3602
3836
  if (isolatedCursorManager != null) return await this.inferRemoteAndAppendLocal(bootstrapCursor, isolatedCursorManager, operation, parentSignal);
3603
- const preservedLocalSnapshot = this.adapter.exportSnapshot();
3837
+ const preservedLocalSnapshot = atSyncErrorBoundary({
3838
+ source: "crdt",
3839
+ stage: "crdt_export"
3840
+ }, () => this.adapter.exportSnapshot());
3604
3841
  let restoredLocalSnapshot = false;
3605
3842
  try {
3606
3843
  const inferredRemote = await this.runInitialSyncWithDeadline(operation, (signal) => this.bootstrapState(bootstrapCursor, {
@@ -3608,7 +3845,10 @@ var StreamsCrdt = class StreamsCrdt {
3608
3845
  signal,
3609
3846
  timeoutMode: "initial"
3610
3847
  }), parentSignal);
3611
- await this.adapter.applySnapshot(preservedLocalSnapshot);
3848
+ await atErrorBoundary({
3849
+ source: "crdt",
3850
+ stage: "crdt_apply"
3851
+ }, async () => this.adapter.applySnapshot(preservedLocalSnapshot));
3612
3852
  restoredLocalSnapshot = true;
3613
3853
  let pendingAppend;
3614
3854
  if (this.frozenLocalAppend != null) {
@@ -3625,7 +3865,10 @@ var StreamsCrdt = class StreamsCrdt {
3625
3865
  if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
3626
3866
  return result;
3627
3867
  } catch (error) {
3628
- 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));
3629
3872
  throw error;
3630
3873
  }
3631
3874
  }
@@ -3712,7 +3955,7 @@ var StreamsCrdt = class StreamsCrdt {
3712
3955
  streamUrl: this.streamUrl,
3713
3956
  attempts: state.retryAttempt
3714
3957
  });
3715
- state.lastError = toTransportError(error);
3958
+ state.lastError = toTransportError(error, "live");
3716
3959
  this.setReadSubStatus(state, "disconnected");
3717
3960
  return false;
3718
3961
  }
@@ -3745,12 +3988,15 @@ var StreamsCrdt = class StreamsCrdt {
3745
3988
  try {
3746
3989
  if (state.liveMode === "long-poll") {
3747
3990
  const requestOffset = state.cursor.nextOffset;
3748
- 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));
3749
3992
  if (response.kind === "gone") {
3750
3993
  const bootstrapped = await this.bootstrapState({
3751
3994
  ...state.cursor,
3752
3995
  nextOffset: "-1",
3753
- serverLowerBoundVersion: this.adapter.emptyVersion()
3996
+ serverLowerBoundVersion: atSyncErrorBoundary({
3997
+ source: "crdt",
3998
+ stage: "crdt_export"
3999
+ }, () => this.adapter.emptyVersion())
3754
4000
  }, { signal: capturedController?.signal });
3755
4001
  state.cursor = bootstrapped.cursor;
3756
4002
  state.streamCursor = bootstrapped.streamCursor;
@@ -3837,14 +4083,17 @@ var StreamsCrdt = class StreamsCrdt {
3837
4083
  streamUrl: this.streamUrl,
3838
4084
  code: "stream_not_found"
3839
4085
  });
3840
- state.lastError = toTransportError(notFoundError);
4086
+ state.lastError = toTransportError(notFoundError, "live");
3841
4087
  this.setReadSubStatus(state, "error");
3842
4088
  return;
3843
4089
  }
3844
4090
  const bootstrapped = await this.bootstrapState({
3845
4091
  ...state.cursor,
3846
4092
  nextOffset: "-1",
3847
- serverLowerBoundVersion: this.adapter.emptyVersion()
4093
+ serverLowerBoundVersion: atSyncErrorBoundary({
4094
+ source: "crdt",
4095
+ stage: "crdt_export"
4096
+ }, () => this.adapter.emptyVersion())
3848
4097
  }, { signal: capturedController?.signal });
3849
4098
  state.cursor = bootstrapped.cursor;
3850
4099
  state.streamCursor = bootstrapped.streamCursor;
@@ -3901,7 +4150,7 @@ var StreamsCrdt = class StreamsCrdt {
3901
4150
  streamUrl: this.streamUrl,
3902
4151
  code: flushClassified.code
3903
4152
  });
3904
- state.lastError = toTransportError(flushError);
4153
+ state.lastError = toTransportError(flushError, "live");
3905
4154
  this.setReadSubStatus(state, "error");
3906
4155
  return;
3907
4156
  }
@@ -3916,7 +4165,7 @@ var StreamsCrdt = class StreamsCrdt {
3916
4165
  streamUrl: this.streamUrl,
3917
4166
  code: classified.code
3918
4167
  });
3919
- state.lastError = toTransportError(error);
4168
+ state.lastError = toTransportError(error, "live");
3920
4169
  this.setReadSubStatus(state, "error");
3921
4170
  return;
3922
4171
  }
@@ -3949,7 +4198,10 @@ var StreamsCrdt = class StreamsCrdt {
3949
4198
  const candidateUpdates = [...allUpdates, ...next.updates];
3950
4199
  if (await this.measureAppendBodyByteLength(candidateUpdates) > maxBytes) break;
3951
4200
  allUpdates.push(...next.updates);
3952
- mergedVersion = this.adapter.mergeVersions(mergedVersion, next.version);
4201
+ mergedVersion = atSyncErrorBoundary({
4202
+ source: "crdt",
4203
+ stage: "crdt_export"
4204
+ }, () => this.adapter.mergeVersions(mergedVersion, next.version));
3953
4205
  count += 1;
3954
4206
  }
3955
4207
  if (count === 1) return {
@@ -3971,6 +4223,50 @@ var StreamsCrdt = class StreamsCrdt {
3971
4223
  return this.drainQueuedBatch(this.pendingWriteOnly);
3972
4224
  }
3973
4225
  /**
4226
+ * One FIFO transaction, called only while holding opQueue. Selecting the
4227
+ * head and committing its ACK/count must share the lock: sync may drain
4228
+ * the queue while a background writer is queued or sleeping between retries.
4229
+ */
4230
+ async flushPendingLocalBatchExclusive(state) {
4231
+ while (!state.closed) {
4232
+ const drained = await this.drainPendingBatch(state);
4233
+ if (drained == null || state.closed) return false;
4234
+ try {
4235
+ let committedCount;
4236
+ const initial = await this.finalizeDirectLocalAppendIfNeeded({
4237
+ cursor: state.cursor,
4238
+ streamCursor: state.streamCursor
4239
+ });
4240
+ state.cursor = initial.cursor;
4241
+ state.streamCursor = initial.streamCursor;
4242
+ if (this.cursorManager.isCursorComplete(state.cursor)) {
4243
+ state.cursor = await this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor);
4244
+ const streamCursor = this.consumeCommittedLocalStreamCursor();
4245
+ if (streamCursor != null) state.streamCursor = streamCursor;
4246
+ committedCount = this.consumeCommittedPendingCount() ?? drained.count;
4247
+ if (!state.closed) this.snapshotManager.schedule(state);
4248
+ } else {
4249
+ const appended = await this.appendLocalBatchRemote(state.cursor, drained.batch, drained.count, "pending");
4250
+ this.commitProducerAck(appended.producerAck);
4251
+ committedCount = appended.pendingCount ?? drained.count;
4252
+ }
4253
+ for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
4254
+ this.notifySyncWaiters(state, committedCount);
4255
+ state.flushHeadBatchOnly = false;
4256
+ state.lastWriteError = void 0;
4257
+ this.setWriteSubStatus(state, "ok");
4258
+ return true;
4259
+ } catch (error) {
4260
+ if (isPayloadTooLargeError(error) && drained.count > 1 && this.abandonPendingFrozenLocalAppend()) {
4261
+ state.flushHeadBatchOnly = true;
4262
+ continue;
4263
+ }
4264
+ throw error;
4265
+ }
4266
+ }
4267
+ return false;
4268
+ }
4269
+ /**
3974
4270
  * Drains `pendingLocal` to the server in strict FIFO order.
3975
4271
  *
3976
4272
  * FIFO invariant: batches are only popped after a durable append, and a
@@ -3995,36 +4291,14 @@ var StreamsCrdt = class StreamsCrdt {
3995
4291
  this.clearWriteRetryTimer(state);
3996
4292
  state.flushingLocal = true;
3997
4293
  try {
3998
- outer: while (!state.closed) {
3999
- const drained = await this.drainPendingBatch(state);
4000
- if (drained == null) return;
4294
+ while (!state.closed) {
4001
4295
  let attempts = 0;
4002
4296
  while (!state.closed) try {
4003
- let committedCount;
4004
- if (this.cursorManager.isCursorComplete(state.cursor)) {
4005
- state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor));
4006
- const committedStreamCursor = this.consumeCommittedLocalStreamCursor();
4007
- if (committedStreamCursor != null) state.streamCursor = committedStreamCursor;
4008
- committedCount = this.consumeCommittedPendingCount() ?? drained.count;
4009
- this.snapshotManager.schedule(state);
4010
- } else {
4011
- const appended = await this.enqueueExclusive(async () => this.appendLocalBatchRemote(state.cursor, drained.batch, drained.count, "pending"));
4012
- this.commitProducerAck(appended.producerAck);
4013
- committedCount = appended.pendingCount ?? drained.count;
4014
- }
4015
- for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
4016
- this.notifySyncWaiters(state, committedCount);
4017
- state.flushHeadBatchOnly = false;
4018
- state.lastWriteError = void 0;
4019
- this.setWriteSubStatus(state, "ok");
4297
+ if (!await this.enqueueExclusive(() => this.flushPendingLocalBatchExclusive(state))) return;
4020
4298
  break;
4021
4299
  } catch (error) {
4022
- if (isPayloadTooLargeError(error) && drained.count > 1 && this.abandonPendingFrozenLocalAppend()) {
4023
- state.flushHeadBatchOnly = true;
4024
- continue outer;
4025
- }
4026
4300
  const classified = classifyFetchError(error);
4027
- const transportError = toTransportError(error);
4301
+ const transportError = toTransportError(error, "upload");
4028
4302
  if (!classified.retriable || isAuthOrProtocolError(error)) {
4029
4303
  if (isPayloadTooLargeError(error)) state.flushHeadBatchOnly = true;
4030
4304
  this.logError("local append failed", error, {
@@ -4201,4 +4475,4 @@ function isValidRillId(id) {
4201
4475
  //#endregion
4202
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 };
4203
4477
 
4204
- //# sourceMappingURL=stream-id-DwQujw_H.js.map
4478
+ //# sourceMappingURL=stream-id-Dlnc1PNH.js.map