@spectrum-ts/core 5.2.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,6 +69,18 @@ interface ResumableOrderedStreamOptions<TLive, TMissed, TOutput> {
69
69
  maxRetryDelayMs?: number;
70
70
  processLive: (event: TLive) => Promise<ResumableStreamItem<TOutput>>;
71
71
  processMissed: (event: TMissed) => Promise<ResumableStreamItem<TOutput>>;
72
+ /**
73
+ * Invoked when reconnects have failed persistently (the consecutive-failure
74
+ * count has reached a multiple of `PERSISTENT_FAILURE_ERROR_THRESHOLD`) to
75
+ * rebuild state a plain reconnect cannot refresh — e.g. re-mint an auth token
76
+ * the server rejects after a restart. Runs between the failure and the next
77
+ * retry, throttled to once per threshold window, and resets once the stream
78
+ * recovers. Errors thrown are logged and swallowed; the reconnect loop
79
+ * continues either way, so a recover that itself fails is harmless.
80
+ */
81
+ recover?: (error: unknown, failureCount: number) => Promise<void> | void;
82
+ /** Cap on how long a `recover` hook may run before it is abandoned; injectable for tests. */
83
+ recoverTimeoutMs?: number;
72
84
  subscribeLive: (cursor?: string) => CloseableAsyncIterable<TLive>;
73
85
  }
74
86
  /**
package/dist/authoring.js CHANGED
@@ -147,6 +147,18 @@ const closeIterable = async (iterable) => {
147
147
  await iterable.close?.();
148
148
  };
149
149
  const ignoreCleanupError = () => void 0;
150
+ const RECOVER_TIMEOUT_MS = 3e4;
151
+ const runWithTimeout = async (work, timeoutMs, onTimeout) => {
152
+ work.catch(ignoreCleanupError);
153
+ let timer;
154
+ try {
155
+ await Promise.race([work, new Promise((_resolve, reject) => {
156
+ timer = setTimeout(() => reject(onTimeout()), timeoutMs);
157
+ })]);
158
+ } finally {
159
+ if (timer) clearTimeout(timer);
160
+ }
161
+ };
150
162
  const jitterDelay = (delayMs) => delayMs * (.5 + Math.random() * .5);
151
163
  async function* throwOnCursorRejection(source, isCursorRejected) {
152
164
  try {
@@ -178,10 +190,12 @@ const resumableOrderedStream = (options) => stream((emit, end) => {
178
190
  const initialRetryDelayMs = options.initialRetryDelayMs ?? 500;
179
191
  const maxRetryDelayMs = options.maxRetryDelayMs ?? 3e4;
180
192
  const jitter = options.jitter ?? jitterDelay;
193
+ const recoverTimeoutMs = options.recoverTimeoutMs ?? RECOVER_TIMEOUT_MS;
181
194
  const label = options.label;
182
195
  let activeLive;
183
196
  let closed = false;
184
197
  let failedAttempts = 0;
198
+ let lastRecoverAttempt = 0;
185
199
  let lastCursor;
186
200
  let retryDelayMs = initialRetryDelayMs;
187
201
  let sleepTimer;
@@ -197,6 +211,7 @@ const resumableOrderedStream = (options) => stream((emit, end) => {
197
211
  });
198
212
  const noteRecovery = () => {
199
213
  retryDelayMs = initialRetryDelayMs;
214
+ lastRecoverAttempt = 0;
200
215
  if (failedAttempts === 0) return;
201
216
  log.info("stream recovered", {
202
217
  "spectrum.stream.id": streamId,
@@ -267,6 +282,25 @@ const resumableOrderedStream = (options) => stream((emit, end) => {
267
282
  log.warn("stream interrupted; reconnecting", attrs, error);
268
283
  return delayMs;
269
284
  };
285
+ const maybeRecover = async (error) => {
286
+ if (!options.recover || failedAttempts < 5 || failedAttempts - lastRecoverAttempt < 5) return;
287
+ lastRecoverAttempt = failedAttempts;
288
+ try {
289
+ await runWithTimeout(Promise.resolve(options.recover(error, failedAttempts)), recoverTimeoutMs, () => /* @__PURE__ */ new Error(`recover hook did not settle within ${recoverTimeoutMs}ms`));
290
+ log.info("stream recover hook ran", {
291
+ "spectrum.stream.id": streamId,
292
+ "spectrum.stream.label": label,
293
+ "spectrum.stream.attempt": failedAttempts
294
+ });
295
+ } catch (recoverError) {
296
+ log.warn("stream recover hook failed", {
297
+ "spectrum.stream.id": streamId,
298
+ "spectrum.stream.label": label,
299
+ "spectrum.stream.attempt": failedAttempts,
300
+ ...errorAttrs(recoverError)
301
+ }, recoverError);
302
+ }
303
+ };
270
304
  const consumeLive = async () => {
271
305
  const live = options.subscribeLive(lastCursor);
272
306
  activeLive = live;
@@ -366,7 +400,9 @@ const resumableOrderedStream = (options) => stream((emit, end) => {
366
400
  await closeIterable(activeLive).catch(ignoreCleanupError);
367
401
  activeLive = void 0;
368
402
  if (closed) break;
369
- await sleep(handleFailure(error, phase));
403
+ const delayMs = handleFailure(error, phase);
404
+ await maybeRecover(error);
405
+ await sleep(delayMs);
370
406
  }
371
407
  }
372
408
  end();
package/dist/index.js CHANGED
@@ -2994,7 +2994,7 @@ function definePlatform(name, rawDef) {
2994
2994
  }
2995
2995
  //#endregion
2996
2996
  //#region src/build-env.ts
2997
- const SPECTRUM_SDK_VERSION = "5.2.0";
2997
+ const SPECTRUM_SDK_VERSION = "6.1.0";
2998
2998
  //#endregion
2999
2999
  //#region src/utils/provider-packages.ts
3000
3000
  const OFFICIAL_PROVIDER_PACKAGES = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/core",
3
- "version": "5.2.0",
3
+ "version": "6.1.0",
4
4
  "description": "The spectrum-ts runtime — Spectrum, content builders, fusor, and the provider authoring API.",
5
5
  "repository": {
6
6
  "type": "git",