@camera.ui/transport 0.0.23 → 0.0.25

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.
@@ -5,11 +5,13 @@ export type RefreshReason = 'proactive' | 'auth-error';
5
5
  export interface TokenLifecycleOptions {
6
6
  readonly kernel: Kernel;
7
7
  readonly transports: readonly Transport[];
8
- readonly refresh: (target: ConnectionTarget, reason: RefreshReason) => Promise<Tokens>;
9
8
  readonly graceMs?: number;
10
- readonly isTransientError?: (err: unknown) => boolean;
11
9
  readonly maxTransientRetries?: number;
12
10
  readonly transientRetryDelayMs?: number;
11
+ readonly maxServerErrorRetries?: number;
12
+ readonly refresh: (target: ConnectionTarget, reason: RefreshReason) => Promise<Tokens>;
13
+ readonly isTransientError?: (err: unknown) => boolean;
14
+ readonly isServerError?: (err: unknown) => boolean;
13
15
  readonly now?: () => number;
14
16
  readonly setTimer?: (cb: () => void, ms: number) => unknown;
15
17
  readonly clearTimer?: (handle: unknown) => void;
@@ -20,6 +22,7 @@ export interface TokenLifecycleOptions {
20
22
  readonly onRefreshSkipped?: (reason: RefreshReason, tokens: Tokens) => void;
21
23
  readonly onRefreshError?: (reason: RefreshReason, error: unknown, info: {
22
24
  transient: boolean;
25
+ serverError: boolean;
23
26
  retriesLeft: number;
24
27
  willRetry: boolean;
25
28
  }) => void;
package/dist/index.js CHANGED
@@ -192,7 +192,7 @@ var RaceFirstError = class extends Error {
192
192
  }
193
193
  };
194
194
  function raceFirst(candidates, options = {}) {
195
- const { timeoutByMode = (mode) => DEFAULT_RACE_TIMEOUT_BY_MODE[mode] ?? 5e3, shortCircuit, parentSignal, prefer } = options;
195
+ const { timeoutByMode = (mode) => DEFAULT_RACE_TIMEOUT_BY_MODE[mode] ?? 5e3, shortCircuit, informative, parentSignal, prefer } = options;
196
196
  const preferGraceMs = options.preferGraceMs ?? DEFAULT_PREFER_GRACE_MS;
197
197
  return new Promise((resolve, reject) => {
198
198
  if (candidates.length === 0) {
@@ -215,7 +215,7 @@ function raceFirst(candidates, options = {}) {
215
215
  const timers = [];
216
216
  function cleanupAllExcept(except) {
217
217
  for (const t of timers) clearTimeout(t);
218
- for (const a of abortControllers) if (a !== except) a.abort();
218
+ for (const a of abortControllers) if (a !== except) a.abort("race-settled");
219
219
  }
220
220
  function finishSuccess(endpoint, value, except) {
221
221
  if (settled) return;
@@ -259,7 +259,7 @@ function raceFirst(candidates, options = {}) {
259
259
  const ctrl = new AbortController();
260
260
  abortControllers.push(ctrl);
261
261
  const delay = timeoutByMode(cand.endpoint.mode);
262
- const timer = setTimeout(() => ctrl.abort(), delay);
262
+ const timer = setTimeout(() => ctrl.abort("race-timeout"), delay);
263
263
  timers.push(timer);
264
264
  cand.run(ctrl.signal).then((value) => handleSuccess(cand.endpoint, value, ctrl), (err) => {
265
265
  if (settled) {
@@ -282,10 +282,11 @@ function raceFirst(candidates, options = {}) {
282
282
  }
283
283
  remaining--;
284
284
  if (remaining <= 0) {
285
- const [endpoint, cause] = [...lastErrors.entries()].find(([, e]) => {
285
+ const usable = [...lastErrors.entries()].filter(([, e]) => {
286
286
  if (e instanceof RaceFirstError) return e.kind !== "all-failed" || !(e.cause instanceof Error && e.cause.message === "aborted");
287
287
  return true;
288
- }) ?? [cand.endpoint, finalErr];
288
+ });
289
+ const [endpoint, cause] = (informative ? usable.find(([, e]) => informative(e instanceof RaceFirstError ? e.cause : e)) : void 0) ?? usable[0] ?? [cand.endpoint, finalErr];
289
290
  finishFail(new RaceFirstError("raceFirst: all candidates failed", endpoint, cause, "all-failed"));
290
291
  }
291
292
  });
@@ -778,9 +779,9 @@ function attachProbeLoop(options) {
778
779
  options.onProbeSuccess?.(endpoint, tokens);
779
780
  return tokens;
780
781
  } catch (err) {
781
- const isLocalTimeout = signal.aborted && !ctrl.signal.aborted && !isProbeFailure(err);
782
- const timeout = (options.timeoutByMode ?? ((m) => DEFAULT_RACE_TIMEOUT_BY_MODE[m] ?? 5e3))(endpoint.mode);
783
- const finalErr = isLocalTimeout ? makeProbeFailure("transient", `timeout (${timeout}ms)`) : err;
782
+ let finalErr = err;
783
+ if (signal.aborted && !ctrl.signal.aborted && !isProbeFailure(err)) if (signal.reason === "race-settled") finalErr = makeProbeFailure("transient", "superseded");
784
+ else finalErr = makeProbeFailure("transient", `timeout (${(options.timeoutByMode ?? ((m) => DEFAULT_RACE_TIMEOUT_BY_MODE[m] ?? 5e3))(endpoint.mode)}ms)`);
784
785
  options.onProbeError?.(endpoint, finalErr);
785
786
  throw finalErr;
786
787
  }
@@ -792,7 +793,8 @@ function attachProbeLoop(options) {
792
793
  parentSignal: ctrl.signal,
793
794
  prefer: options.prefer,
794
795
  preferGraceMs: options.preferGraceMs,
795
- shortCircuit: (err) => isProbeFailure(err) && (err.kind === "needs-auth" || err.kind === "fatal" || err.kind === "aborted")
796
+ shortCircuit: (err) => isProbeFailure(err) && (err.kind === "needs-auth" || err.kind === "fatal"),
797
+ informative: (cause) => !(isProbeFailure(cause) && cause.kind === "aborted")
796
798
  });
797
799
  if (ctrl.signal.aborted) return;
798
800
  options.kernel.dispatch({
@@ -836,9 +838,11 @@ var DEFAULT_TRANSIENT_RETRY_DELAY_MS = 2e3;
836
838
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
837
839
  function attachTokenLifecycle(options) {
838
840
  const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
839
- const isTransient = options.isTransientError ?? (() => false);
840
841
  const maxTransientRetries = options.maxTransientRetries ?? DEFAULT_MAX_TRANSIENT_RETRIES;
842
+ const maxServerErrorRetries = options.maxServerErrorRetries ?? maxTransientRetries;
841
843
  const transientRetryDelayMs = options.transientRetryDelayMs ?? DEFAULT_TRANSIENT_RETRY_DELAY_MS;
844
+ const isTransient = options.isTransientError ?? (() => false);
845
+ const isServerError = options.isServerError ?? (() => false);
842
846
  const now = options.now ?? (() => Date.now());
843
847
  const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
844
848
  const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
@@ -926,11 +930,14 @@ function attachTokenLifecycle(options) {
926
930
  } catch (err) {
927
931
  if (detached) return;
928
932
  const transient = isTransient(err);
929
- if (transient && transientRetries < maxTransientRetries) {
933
+ const serverError = transient && isServerError(err);
934
+ const budget = serverError ? maxServerErrorRetries : maxTransientRetries;
935
+ if (transient && transientRetries < budget) {
930
936
  transientRetries++;
931
- const retriesLeft = maxTransientRetries - transientRetries;
937
+ const retriesLeft = budget - transientRetries;
932
938
  options.onRefreshError?.(reason, err, {
933
939
  transient: true,
940
+ serverError,
934
941
  retriesLeft,
935
942
  willRetry: true
936
943
  });
@@ -943,6 +950,7 @@ function attachTokenLifecycle(options) {
943
950
  transientRetries = 0;
944
951
  options.onRefreshError?.(reason, err, {
945
952
  transient,
953
+ serverError,
946
954
  retriesLeft: 0,
947
955
  willRetry: false
948
956
  });
package/dist/race.d.ts CHANGED
@@ -6,11 +6,12 @@ export interface RaceCandidate<T> {
6
6
  readonly run: (signal: AbortSignal) => Promise<T>;
7
7
  }
8
8
  export interface RaceFirstOptions {
9
+ readonly parentSignal?: AbortSignal;
10
+ readonly preferGraceMs?: number;
9
11
  readonly timeoutByMode?: TimeoutByModeFn;
10
12
  readonly shortCircuit?: (err: unknown) => boolean;
11
- readonly parentSignal?: AbortSignal;
13
+ readonly informative?: (cause: unknown) => boolean;
12
14
  readonly prefer?: (endpoint: Endpoint) => boolean;
13
- readonly preferGraceMs?: number;
14
15
  }
15
16
  export interface RaceFirstResult<T> {
16
17
  readonly endpoint: Endpoint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camera.ui/transport",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "camera.ui transport layer — framework-agnostic connection kernel, reducer state, pluggable transports (HTTP/WS/Socket.IO/NATS), lifecycle effects and worker bridge",
5
5
  "author": "seydx (https://github.com/cameraui/clients)",
6
6
  "type": "module",
@@ -66,7 +66,7 @@
66
66
  "@typescript-eslint/parser": "^8.65.0",
67
67
  "@vue/language-core": "^3.3.8",
68
68
  "eslint": "9.39.2",
69
- "globals": "^17.7.0",
69
+ "globals": "^17.8.0",
70
70
  "jiti": "^2.7.0",
71
71
  "prettier": "^3.9.6",
72
72
  "rimraf": "^6.1.3",