@camera.ui/transport 0.0.1

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.
Files changed (39) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +10 -0
  3. package/dist/contract-d-0gfY8v.js +43 -0
  4. package/dist/core/kernel.d.ts +14 -0
  5. package/dist/core/reducer.d.ts +2 -0
  6. package/dist/core/resolver.d.ts +5 -0
  7. package/dist/core/types.d.ts +109 -0
  8. package/dist/effects/backoff.d.ts +14 -0
  9. package/dist/effects/crossTab.d.ts +14 -0
  10. package/dist/effects/networkChange.d.ts +10 -0
  11. package/dist/effects/persistence.d.ts +31 -0
  12. package/dist/effects/presence.d.ts +17 -0
  13. package/dist/effects/probeLoop.d.ts +30 -0
  14. package/dist/effects/tokenLifecycle.d.ts +39 -0
  15. package/dist/effects/transportSync.d.ts +11 -0
  16. package/dist/effects/transportWatchdog.d.ts +16 -0
  17. package/dist/effects/workerBridge.d.ts +17 -0
  18. package/dist/index.d.ts +30 -0
  19. package/dist/index.js +1101 -0
  20. package/dist/race.d.ts +23 -0
  21. package/dist/testing/fakeTransport.d.ts +24 -0
  22. package/dist/testing/index.d.ts +2 -0
  23. package/dist/testing.js +53 -0
  24. package/dist/transports/contract.d.ts +29 -0
  25. package/dist/transports/http.d.ts +14 -0
  26. package/dist/transports/http.js +131 -0
  27. package/dist/transports/nativeHttp.d.ts +33 -0
  28. package/dist/transports/nativeHttp.js +119 -0
  29. package/dist/transports/nats.d.ts +25 -0
  30. package/dist/transports/nats.js +225 -0
  31. package/dist/transports/socketio.d.ts +19 -0
  32. package/dist/transports/socketio.js +160 -0
  33. package/dist/transports/ws.d.ts +34 -0
  34. package/dist/transports/ws.js +228 -0
  35. package/dist/worker/index.d.ts +3 -0
  36. package/dist/worker/mirror.d.ts +17 -0
  37. package/dist/worker/protocol.d.ts +23 -0
  38. package/dist/worker.js +66 -0
  39. package/package.json +95 -0
package/dist/index.js ADDED
@@ -0,0 +1,1101 @@
1
+ import { i as isTokenOnlyChange, n as isEndpointChange, r as isSameTarget, t as TransportEmitter } from "./contract-d-0gfY8v.js";
2
+ //#region src/core/reducer.ts
3
+ var EMPTY_TRANSPORTS = /* @__PURE__ */ new Map();
4
+ var DEFAULT_BACKOFF_MS = 5e3;
5
+ function reducer(phase, action, ctx) {
6
+ switch (action.type) {
7
+ case "RESET": return phase.kind === "idle" ? phase : { kind: "idle" };
8
+ case "BOOT":
9
+ if (phase.kind !== "idle" && phase.kind !== "offline" && phase.kind !== "needs-auth") return phase;
10
+ return {
11
+ kind: "discovering",
12
+ instanceId: action.instanceId,
13
+ attempt: 1
14
+ };
15
+ case "USER_RETRY":
16
+ if (phase.kind === "offline") return {
17
+ kind: "discovering",
18
+ instanceId: phase.instanceId ?? "",
19
+ attempt: 1
20
+ };
21
+ if (phase.kind === "reconnecting") return {
22
+ kind: "discovering",
23
+ instanceId: phase.instanceId,
24
+ attempt: 1
25
+ };
26
+ if (phase.kind === "needs-auth") return {
27
+ kind: "discovering",
28
+ instanceId: phase.instanceId ?? "",
29
+ attempt: 1
30
+ };
31
+ if (phase.kind === "online") return {
32
+ kind: "discovering",
33
+ instanceId: phase.instanceId,
34
+ attempt: 1
35
+ };
36
+ return phase;
37
+ case "PROBE_SUCCEEDED":
38
+ if (phase.kind === "discovering") return {
39
+ kind: "online",
40
+ instanceId: phase.instanceId,
41
+ target: {
42
+ endpoint: action.endpoint,
43
+ tokens: action.tokens
44
+ },
45
+ transports: EMPTY_TRANSPORTS
46
+ };
47
+ if (phase.kind === "reconnecting") return {
48
+ kind: "online",
49
+ instanceId: phase.instanceId,
50
+ target: {
51
+ endpoint: action.endpoint,
52
+ tokens: action.tokens
53
+ },
54
+ transports: phase.transports
55
+ };
56
+ return phase;
57
+ case "PROBE_FAILED_ALL": {
58
+ if (phase.kind !== "discovering" && phase.kind !== "reconnecting") return phase;
59
+ if (action.error === "needs-auth") return {
60
+ kind: "needs-auth",
61
+ instanceId: phase.instanceId,
62
+ reason: action.error
63
+ };
64
+ const attempt = phase.kind === "discovering" ? phase.attempt : 1;
65
+ const backoff = ctx.retryBackoffMs?.(attempt) ?? DEFAULT_BACKOFF_MS;
66
+ return {
67
+ kind: "offline",
68
+ instanceId: phase.instanceId,
69
+ lastError: action.error,
70
+ nextRetryAt: ctx.now() + backoff
71
+ };
72
+ }
73
+ case "TRANSPORT_UP":
74
+ if (phase.kind === "online") return {
75
+ ...phase,
76
+ transports: setStatus(phase.transports, action.id, { up: true })
77
+ };
78
+ if (phase.kind === "reconnecting") {
79
+ const next = setStatus(phase.transports, action.id, { up: true });
80
+ if (phase.lastTarget && allPhaseGatingUp(next, ctx)) return {
81
+ kind: "online",
82
+ instanceId: phase.instanceId,
83
+ target: phase.lastTarget,
84
+ transports: next
85
+ };
86
+ return {
87
+ ...phase,
88
+ transports: next
89
+ };
90
+ }
91
+ return phase;
92
+ case "TRANSPORT_DOWN":
93
+ if (phase.kind === "online" || phase.kind === "reconnecting") return {
94
+ ...phase,
95
+ transports: setStatus(phase.transports, action.id, {
96
+ up: false,
97
+ lastError: action.reason,
98
+ downSince: ctx.now()
99
+ })
100
+ };
101
+ return phase;
102
+ case "TRANSPORT_DOWN_CONFIRMED":
103
+ if (phase.kind !== "online") return phase;
104
+ if (!ctx.specs.get(action.id)?.phaseGating) return phase;
105
+ return {
106
+ kind: "reconnecting",
107
+ instanceId: phase.instanceId,
108
+ lastTarget: phase.target,
109
+ cause: "transport-down",
110
+ since: ctx.now(),
111
+ transports: setStatus(phase.transports, action.id, {
112
+ up: false,
113
+ lastError: "down-confirmed",
114
+ downSince: ctx.now()
115
+ })
116
+ };
117
+ case "TOKENS_REFRESHED":
118
+ if (phase.kind === "online") {
119
+ if (tokensEqual(phase.target.tokens, action.tokens)) return phase;
120
+ return {
121
+ ...phase,
122
+ target: {
123
+ ...phase.target,
124
+ tokens: action.tokens
125
+ }
126
+ };
127
+ }
128
+ if (phase.kind === "reconnecting" && phase.lastTarget) {
129
+ if (tokensEqual(phase.lastTarget.tokens, action.tokens)) return phase;
130
+ return {
131
+ ...phase,
132
+ lastTarget: {
133
+ ...phase.lastTarget,
134
+ tokens: action.tokens
135
+ }
136
+ };
137
+ }
138
+ return phase;
139
+ case "TOKENS_INVALID":
140
+ if (phase.kind !== "online" && phase.kind !== "reconnecting") return phase;
141
+ if (action.transient === true) {
142
+ const backoff = ctx.retryBackoffMs?.(1) ?? DEFAULT_BACKOFF_MS;
143
+ return {
144
+ kind: "offline",
145
+ instanceId: phase.instanceId,
146
+ lastError: `tokens invalid: ${action.reason}`,
147
+ nextRetryAt: ctx.now() + backoff
148
+ };
149
+ }
150
+ return {
151
+ kind: "needs-auth",
152
+ instanceId: phase.instanceId,
153
+ reason: `tokens invalid: ${action.reason}`
154
+ };
155
+ case "BACKOFF_HINT": {
156
+ if (phase.kind !== "offline") return phase;
157
+ const now = ctx.now();
158
+ const candidate = now + action.retryAfterMs;
159
+ if (candidate <= phase.nextRetryAt) return phase;
160
+ return {
161
+ ...phase,
162
+ nextRetryAt: candidate,
163
+ backoffHint: {
164
+ retryAfterMs: action.retryAfterMs,
165
+ setAt: now,
166
+ source: action.source
167
+ }
168
+ };
169
+ }
170
+ }
171
+ }
172
+ function setStatus(map, id, status) {
173
+ const next = new Map(map);
174
+ next.set(id, status);
175
+ return next;
176
+ }
177
+ function allPhaseGatingUp(map, ctx) {
178
+ for (const [id, spec] of ctx.specs) {
179
+ if (!spec.phaseGating) continue;
180
+ if (!map.get(id)?.up) return false;
181
+ }
182
+ return true;
183
+ }
184
+ function tokensEqual(a, b) {
185
+ return a.access === b.access && a.refresh === b.refresh && a.accessExpiresAt === b.accessExpiresAt && a.refreshExpiresAt === b.refreshExpiresAt && a.proxySession === b.proxySession && a.proxySessionExpiresAt === b.proxySessionExpiresAt && a.proxyRefresh === b.proxyRefresh;
186
+ }
187
+ //#endregion
188
+ //#region src/core/kernel.ts
189
+ function createKernel(options) {
190
+ let current = options.initial ?? { kind: "idle" };
191
+ const listeners = /* @__PURE__ */ new Set();
192
+ const queue = [];
193
+ let dispatching = false;
194
+ let disposed = false;
195
+ function dispatch(action) {
196
+ if (disposed) return;
197
+ if (dispatching) {
198
+ queue.push(action);
199
+ return;
200
+ }
201
+ dispatching = true;
202
+ try {
203
+ let next = action;
204
+ while (next) {
205
+ const prev = current;
206
+ const after = reducer(prev, next, options.context);
207
+ if (after !== prev) {
208
+ current = after;
209
+ for (const listener of [...listeners]) try {
210
+ listener(after, prev, next);
211
+ } catch (err) {
212
+ console.warn("[kernel] listener threw on", next.type, err);
213
+ }
214
+ }
215
+ next = queue.shift();
216
+ }
217
+ } finally {
218
+ dispatching = false;
219
+ }
220
+ }
221
+ function subscribe(listener) {
222
+ listeners.add(listener);
223
+ return () => {
224
+ listeners.delete(listener);
225
+ };
226
+ }
227
+ function dispose() {
228
+ disposed = true;
229
+ listeners.clear();
230
+ queue.length = 0;
231
+ }
232
+ return {
233
+ get phase() {
234
+ return current;
235
+ },
236
+ dispatch,
237
+ subscribe,
238
+ dispose
239
+ };
240
+ }
241
+ //#endregion
242
+ //#region src/core/resolver.ts
243
+ function sortByPriority(endpoints) {
244
+ return [...endpoints].sort((a, b) => (a.priority ?? 99) - (b.priority ?? 99));
245
+ }
246
+ function isSameEndpoint(a, b) {
247
+ return a.mode === b.mode && a.url === b.url;
248
+ }
249
+ function endpointKey(ep) {
250
+ return `${ep.mode}|${ep.url}`;
251
+ }
252
+ //#endregion
253
+ //#region src/effects/backoff.ts
254
+ var DEFAULT_SCHEDULE = [
255
+ 5e3,
256
+ 1e4,
257
+ 3e4,
258
+ 6e4
259
+ ];
260
+ function attachBackoff(options) {
261
+ const schedule = options.schedule ?? DEFAULT_SCHEDULE;
262
+ if (schedule.length === 0) throw new Error("backoff: schedule must have at least one entry");
263
+ const now = options.now ?? (() => Date.now());
264
+ const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
265
+ const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
266
+ let timer;
267
+ let attempt = 0;
268
+ let detached = false;
269
+ function cancelTimer(reason) {
270
+ if (timer !== void 0) {
271
+ clearTimer(timer);
272
+ timer = void 0;
273
+ if (reason) options.onCancelled?.(reason);
274
+ }
275
+ }
276
+ function scheduleFromPhase(phase, isReschedule) {
277
+ cancelTimer(isReschedule ? "rescheduled" : void 0);
278
+ const scheduleDelay = schedule[Math.min(attempt, schedule.length - 1)];
279
+ const phaseDelay = Math.max(0, phase.nextRetryAt - now());
280
+ const delay = Math.max(scheduleDelay, phaseDelay);
281
+ options.onScheduled?.(attempt + 1, delay);
282
+ if (phase.backoffHint && phaseDelay > scheduleDelay) options.onHintApplied?.(delay, phase.backoffHint.source);
283
+ timer = setTimer(() => {
284
+ timer = void 0;
285
+ if (detached) return;
286
+ if (options.kernel.phase.kind !== "offline") return;
287
+ attempt += 1;
288
+ options.onFire?.(attempt);
289
+ options.kernel.dispatch({ type: "USER_RETRY" });
290
+ }, delay);
291
+ }
292
+ const unsubKernel = options.kernel.subscribe((next, prev) => {
293
+ if (next.kind === "offline" && prev.kind !== "offline") {
294
+ scheduleFromPhase(next, false);
295
+ return;
296
+ }
297
+ if (next.kind === "offline" && prev.kind === "offline" && next.nextRetryAt !== prev.nextRetryAt) {
298
+ scheduleFromPhase(next, true);
299
+ return;
300
+ }
301
+ if (prev.kind === "offline" && next.kind !== "offline") cancelTimer("phase-left-offline");
302
+ if (next.kind === "online" || next.kind === "idle") attempt = 0;
303
+ });
304
+ if (options.kernel.phase.kind === "offline") scheduleFromPhase(options.kernel.phase, false);
305
+ return () => {
306
+ detached = true;
307
+ cancelTimer("detach");
308
+ unsubKernel();
309
+ };
310
+ }
311
+ //#endregion
312
+ //#region src/effects/crossTab.ts
313
+ var DEFAULT_KEY$1 = "camera.ui:transport:target";
314
+ function attachCrossTab(options) {
315
+ const key = options.key ?? DEFAULT_KEY$1;
316
+ const source = options.source ?? (typeof window !== "undefined" ? window : void 0);
317
+ if (!source) throw new Error("attachCrossTab: no `source` provided and no global `window` available");
318
+ function handle(event) {
319
+ const e = event;
320
+ if (e.key !== key) return;
321
+ if (e.newValue === null) {
322
+ const k = options.kernel.phase.kind;
323
+ if (k !== "online" && k !== "reconnecting") return;
324
+ options.kernel.dispatch({ type: "RESET" });
325
+ options.onResetReceived?.();
326
+ return;
327
+ }
328
+ let parsed;
329
+ try {
330
+ parsed = JSON.parse(e.newValue);
331
+ } catch (err) {
332
+ options.onError?.("parse", err);
333
+ return;
334
+ }
335
+ if (!parsed?.tokens?.access) return;
336
+ const k = options.kernel.phase.kind;
337
+ if (k !== "online" && k !== "reconnecting") return;
338
+ options.kernel.dispatch({
339
+ type: "TOKENS_REFRESHED",
340
+ tokens: parsed.tokens
341
+ });
342
+ options.onTokensReceived?.(parsed.tokens);
343
+ }
344
+ source.addEventListener("storage", handle);
345
+ return () => {
346
+ source.removeEventListener("storage", handle);
347
+ };
348
+ }
349
+ //#endregion
350
+ //#region src/effects/networkChange.ts
351
+ function attachNetworkChange(options) {
352
+ let detached = false;
353
+ const handler = (event) => {
354
+ if (detached) return;
355
+ options.onChange(options.kernel, event);
356
+ };
357
+ options.source.addEventListener("change", handler);
358
+ return () => {
359
+ detached = true;
360
+ options.source.removeEventListener("change", handler);
361
+ };
362
+ }
363
+ //#endregion
364
+ //#region src/effects/persistence.ts
365
+ var DEFAULT_KEY = "camera.ui:transport:target";
366
+ function attachPersistence(options) {
367
+ const key = options.key ?? DEFAULT_KEY;
368
+ const onError = options.onError ?? (() => {});
369
+ let detached = false;
370
+ let cached = null;
371
+ restore();
372
+ async function restore() {
373
+ let raw;
374
+ try {
375
+ raw = await options.storage.get(key);
376
+ } catch (err) {
377
+ onError("get", err);
378
+ raw = null;
379
+ }
380
+ if (detached) return;
381
+ if (cached !== null) {
382
+ options.onRestore?.(cached);
383
+ return;
384
+ }
385
+ if (!raw) {
386
+ options.onRestore?.(null);
387
+ return;
388
+ }
389
+ try {
390
+ const parsed = JSON.parse(raw);
391
+ if (!parsed?.endpoint?.url || !parsed?.tokens?.access) {
392
+ options.onRestore?.(null);
393
+ return;
394
+ }
395
+ cached = {
396
+ endpoint: parsed.endpoint,
397
+ tokens: parsed.tokens
398
+ };
399
+ options.onRestore?.(cached);
400
+ } catch (err) {
401
+ onError("parse", err);
402
+ options.onRestore?.(null);
403
+ }
404
+ }
405
+ async function persist(target) {
406
+ const payload = {
407
+ endpoint: target.endpoint,
408
+ tokens: target.tokens,
409
+ savedAt: Date.now(),
410
+ version: 1
411
+ };
412
+ cached = {
413
+ endpoint: target.endpoint,
414
+ tokens: target.tokens
415
+ };
416
+ try {
417
+ await options.storage.set(key, JSON.stringify(payload));
418
+ options.onPersist?.(target);
419
+ } catch (err) {
420
+ onError("set", err);
421
+ }
422
+ }
423
+ async function clear() {
424
+ cached = null;
425
+ try {
426
+ await options.storage.del(key);
427
+ options.onClear?.();
428
+ } catch (err) {
429
+ onError("del", err);
430
+ }
431
+ }
432
+ const unsub = options.kernel.subscribe((next, prev) => {
433
+ if (detached) return;
434
+ const nextTarget = next.kind === "online" ? next.target : next.kind === "reconnecting" ? next.lastTarget : null;
435
+ const prevTarget = prev.kind === "online" ? prev.target : prev.kind === "reconnecting" ? prev.lastTarget : null;
436
+ if (nextTarget && nextTarget !== prevTarget) {
437
+ persist(nextTarget);
438
+ return;
439
+ }
440
+ if (next.kind === "idle" && prev.kind !== "idle") clear();
441
+ });
442
+ return {
443
+ detach: () => {
444
+ detached = true;
445
+ unsub();
446
+ },
447
+ peek: () => cached,
448
+ seed: async (target) => {
449
+ if (detached) return;
450
+ await persist(target);
451
+ }
452
+ };
453
+ }
454
+ function localStorageAdapter(scope = globalThis.localStorage) {
455
+ if (!scope) throw new Error("localStorageAdapter: localStorage is not available in this environment");
456
+ return {
457
+ get(k) {
458
+ return scope.getItem(k);
459
+ },
460
+ set(k, v) {
461
+ scope.setItem(k, v);
462
+ },
463
+ del(k) {
464
+ scope.removeItem(k);
465
+ }
466
+ };
467
+ }
468
+ function memoryStorageAdapter(initial = {}) {
469
+ const store = new Map(Object.entries(initial));
470
+ return {
471
+ get(k) {
472
+ return store.get(k) ?? null;
473
+ },
474
+ set(k, v) {
475
+ store.set(k, v);
476
+ },
477
+ del(k) {
478
+ store.delete(k);
479
+ }
480
+ };
481
+ }
482
+ //#endregion
483
+ //#region src/effects/presence.ts
484
+ var defaultOnNetworkOnline = (kernel) => {
485
+ if (kernel.phase.kind === "offline") kernel.dispatch({ type: "USER_RETRY" });
486
+ };
487
+ function attachPresence(options) {
488
+ const networkSource = options.networkSource !== void 0 ? options.networkSource : typeof globalThis !== "undefined" && "window" in globalThis ? globalThis.window : null;
489
+ const visibilitySource = options.visibilitySource !== void 0 ? options.visibilitySource : typeof globalThis !== "undefined" && "document" in globalThis ? globalThis.document : null;
490
+ const onOnline = options.onOnline ?? defaultOnNetworkOnline;
491
+ const onOffline = options.onOffline;
492
+ const cleanups = [];
493
+ if (networkSource) {
494
+ const handleOnline = () => onOnline(options.kernel);
495
+ const handleOffline = () => onOffline?.(options.kernel);
496
+ networkSource.addEventListener("online", handleOnline);
497
+ networkSource.addEventListener("offline", handleOffline);
498
+ cleanups.push(() => {
499
+ networkSource.removeEventListener("online", handleOnline);
500
+ networkSource.removeEventListener("offline", handleOffline);
501
+ });
502
+ }
503
+ if (visibilitySource && (options.onVisibilityVisible || options.onVisibilityHidden)) {
504
+ const handleVisibility = () => {
505
+ if (visibilitySource.visibilityState !== "hidden") options.onVisibilityVisible?.(options.kernel);
506
+ else options.onVisibilityHidden?.(options.kernel);
507
+ };
508
+ visibilitySource.addEventListener("visibilitychange", handleVisibility);
509
+ cleanups.push(() => {
510
+ visibilitySource.removeEventListener("visibilitychange", handleVisibility);
511
+ });
512
+ }
513
+ return () => {
514
+ for (const fn of cleanups) fn();
515
+ cleanups.length = 0;
516
+ };
517
+ }
518
+ //#endregion
519
+ //#region src/race.ts
520
+ var DEFAULT_RACE_TIMEOUT_BY_MODE = {
521
+ "direct-lan": 2e3,
522
+ "direct-wan": 5e3
523
+ };
524
+ var RaceFirstError = class extends Error {
525
+ endpoint;
526
+ cause;
527
+ kind;
528
+ constructor(message, endpoint, cause, kind) {
529
+ super(message);
530
+ this.name = "RaceFirstError";
531
+ this.endpoint = endpoint;
532
+ this.cause = cause;
533
+ this.kind = kind;
534
+ }
535
+ };
536
+ function raceFirst(candidates, options = {}) {
537
+ const { timeoutByMode = (mode) => DEFAULT_RACE_TIMEOUT_BY_MODE[mode] ?? 5e3, shortCircuit, parentSignal } = options;
538
+ return new Promise((resolve, reject) => {
539
+ if (candidates.length === 0) {
540
+ reject(new RaceFirstError("raceFirst: no candidates", {
541
+ url: "",
542
+ mode: "direct-lan"
543
+ }, void 0, "all-failed"));
544
+ return;
545
+ }
546
+ if (parentSignal?.aborted) {
547
+ reject(new RaceFirstError("raceFirst: parent aborted", candidates[0].endpoint, void 0, "aborted"));
548
+ return;
549
+ }
550
+ let settled = false;
551
+ let remaining = candidates.length;
552
+ const lastErrors = /* @__PURE__ */ new Map();
553
+ const abortControllers = [];
554
+ const timers = [];
555
+ function cleanupAllExcept(except) {
556
+ for (const t of timers) clearTimeout(t);
557
+ for (const a of abortControllers) if (a !== except) a.abort();
558
+ }
559
+ function finishSuccess(endpoint, value, except) {
560
+ if (settled) return;
561
+ settled = true;
562
+ cleanupAllExcept(except);
563
+ if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort);
564
+ resolve({
565
+ endpoint,
566
+ value
567
+ });
568
+ }
569
+ function finishFail(error) {
570
+ if (settled) return;
571
+ settled = true;
572
+ cleanupAllExcept(null);
573
+ if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort);
574
+ reject(error);
575
+ }
576
+ const onParentAbort = parentSignal ? () => finishFail(new RaceFirstError("raceFirst: parent aborted", candidates[0].endpoint, void 0, "aborted")) : null;
577
+ if (onParentAbort) parentSignal.addEventListener("abort", onParentAbort);
578
+ candidates.forEach((cand) => {
579
+ const ctrl = new AbortController();
580
+ abortControllers.push(ctrl);
581
+ const delay = timeoutByMode(cand.endpoint.mode);
582
+ const timer = setTimeout(() => ctrl.abort(), delay);
583
+ timers.push(timer);
584
+ cand.run(ctrl.signal).then((value) => finishSuccess(cand.endpoint, value, ctrl), (err) => {
585
+ if (settled) {
586
+ lastErrors.set(cand.endpoint, err);
587
+ return;
588
+ }
589
+ const finalErr = ctrl.signal.aborted && !parentSignal?.aborted ? new RaceFirstError(`timeout (${delay}ms)`, cand.endpoint, err, "all-failed") : err;
590
+ lastErrors.set(cand.endpoint, finalErr);
591
+ if (shortCircuit?.(finalErr)) {
592
+ finishFail(new RaceFirstError("raceFirst: short-circuit", cand.endpoint, finalErr, "short-circuit"));
593
+ return;
594
+ }
595
+ remaining--;
596
+ if (remaining <= 0) {
597
+ const [endpoint, cause] = [...lastErrors.entries()].find(([, e]) => {
598
+ if (e instanceof RaceFirstError) return e.kind !== "all-failed" || !(e.cause instanceof Error && e.cause.message === "aborted");
599
+ return true;
600
+ }) ?? [cand.endpoint, finalErr];
601
+ finishFail(new RaceFirstError("raceFirst: all candidates failed", endpoint, cause, "all-failed"));
602
+ }
603
+ });
604
+ });
605
+ });
606
+ }
607
+ //#endregion
608
+ //#region src/effects/probeLoop.ts
609
+ function makeProbeFailure(kind, message) {
610
+ const err = new Error(message);
611
+ err.kind = kind;
612
+ return err;
613
+ }
614
+ function isProbeFailure(err) {
615
+ return err instanceof Error && typeof err.kind === "string";
616
+ }
617
+ function attachProbeLoop(options) {
618
+ let masterAbort;
619
+ let detached = false;
620
+ function cancel() {
621
+ if (masterAbort) {
622
+ masterAbort.abort();
623
+ masterAbort = void 0;
624
+ }
625
+ }
626
+ async function runRound() {
627
+ if (detached) return;
628
+ cancel();
629
+ const ctrl = new AbortController();
630
+ masterAbort = ctrl;
631
+ let pool;
632
+ try {
633
+ options.onDiscoverStart?.();
634
+ pool = await options.discover(ctrl.signal);
635
+ if (ctrl.signal.aborted) return;
636
+ options.onDiscoverSuccess?.(pool);
637
+ } catch (err) {
638
+ if (ctrl.signal.aborted) return;
639
+ options.onDiscoverError?.(err);
640
+ const reason = err instanceof Error ? `discover: ${err.message}` : "discover failed";
641
+ options.onAllFailed?.(reason);
642
+ options.kernel.dispatch({
643
+ type: "PROBE_FAILED_ALL",
644
+ error: reason
645
+ });
646
+ return;
647
+ }
648
+ if (pool.length === 0) {
649
+ const reason = "discover returned empty pool";
650
+ options.onAllFailed?.(reason);
651
+ options.kernel.dispatch({
652
+ type: "PROBE_FAILED_ALL",
653
+ error: reason
654
+ });
655
+ return;
656
+ }
657
+ const lastTokens = options.lastTarget?.()?.tokens;
658
+ const candidates = pool.map((endpoint) => ({
659
+ endpoint,
660
+ run: async (signal) => {
661
+ options.onProbeStart?.(endpoint);
662
+ try {
663
+ const tokens = await options.probe({
664
+ endpoint,
665
+ lastTokens,
666
+ signal
667
+ });
668
+ options.onProbeSuccess?.(endpoint, tokens);
669
+ return tokens;
670
+ } catch (err) {
671
+ const isLocalTimeout = signal.aborted && !ctrl.signal.aborted && !isProbeFailure(err);
672
+ const timeout = (options.timeoutByMode ?? ((m) => DEFAULT_RACE_TIMEOUT_BY_MODE[m] ?? 5e3))(endpoint.mode);
673
+ const finalErr = isLocalTimeout ? makeProbeFailure("transient", `timeout (${timeout}ms)`) : err;
674
+ options.onProbeError?.(endpoint, finalErr);
675
+ throw finalErr;
676
+ }
677
+ }
678
+ }));
679
+ try {
680
+ const { endpoint, value: tokens } = await raceFirst(candidates, {
681
+ timeoutByMode: options.timeoutByMode,
682
+ parentSignal: ctrl.signal,
683
+ shortCircuit: (err) => isProbeFailure(err) && (err.kind === "needs-auth" || err.kind === "fatal" || err.kind === "aborted")
684
+ });
685
+ if (ctrl.signal.aborted) return;
686
+ options.kernel.dispatch({
687
+ type: "PROBE_SUCCEEDED",
688
+ endpoint,
689
+ tokens
690
+ });
691
+ } catch (err) {
692
+ if (ctrl.signal.aborted) return;
693
+ const underlying = err instanceof RaceFirstError ? err.cause : err;
694
+ if (isProbeFailure(underlying) && underlying.kind === "aborted") {
695
+ setTimeout(() => {
696
+ if (!detached) runRound();
697
+ }, 200);
698
+ return;
699
+ }
700
+ const reason = isProbeFailure(underlying) && underlying.kind === "needs-auth" ? "needs-auth" : underlying instanceof Error ? underlying.message : "all endpoints failed";
701
+ options.onAllFailed?.(reason);
702
+ options.kernel.dispatch({
703
+ type: "PROBE_FAILED_ALL",
704
+ error: reason
705
+ });
706
+ }
707
+ }
708
+ const unsub = options.kernel.subscribe((next, prev) => {
709
+ if (next.kind === "discovering" && prev.kind !== "discovering") runRound();
710
+ else if (next.kind !== "discovering" && prev.kind === "discovering") cancel();
711
+ });
712
+ if (options.kernel.phase.kind === "discovering") runRound();
713
+ return () => {
714
+ detached = true;
715
+ cancel();
716
+ unsub();
717
+ };
718
+ }
719
+ //#endregion
720
+ //#region src/effects/tokenLifecycle.ts
721
+ var DEFAULT_GRACE_MS$1 = 5e3;
722
+ var DEFAULT_MAX_TRANSIENT_RETRIES = 3;
723
+ var DEFAULT_TRANSIENT_RETRY_DELAY_MS = 2e3;
724
+ function attachTokenLifecycle(options) {
725
+ const graceMs = options.graceMs ?? DEFAULT_GRACE_MS$1;
726
+ const isTransient = options.isTransientError ?? (() => false);
727
+ const maxTransientRetries = options.maxTransientRetries ?? DEFAULT_MAX_TRANSIENT_RETRIES;
728
+ const transientRetryDelayMs = options.transientRetryDelayMs ?? DEFAULT_TRANSIENT_RETRY_DELAY_MS;
729
+ const now = options.now ?? (() => Date.now());
730
+ const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
731
+ const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
732
+ let timer;
733
+ let inflight = false;
734
+ let detached = false;
735
+ let transientRetries = 0;
736
+ let pendingAuthError = false;
737
+ const cleanups = [];
738
+ function cancelTimer() {
739
+ if (timer !== void 0) {
740
+ clearTimer(timer);
741
+ timer = void 0;
742
+ }
743
+ }
744
+ function schedule(target) {
745
+ cancelTimer();
746
+ const exp = target.tokens.accessExpiresAt;
747
+ if (!exp) return;
748
+ const delayMs = Math.max(0, exp - now() - graceMs);
749
+ options.onScheduled?.(delayMs, exp);
750
+ timer = setTimer(() => {
751
+ timer = void 0;
752
+ triggerRefresh("proactive");
753
+ }, delayMs);
754
+ }
755
+ async function triggerRefresh(reason) {
756
+ if (detached) {
757
+ options.onTriggerSkipped?.(reason, "detached", options.kernel.phase.kind);
758
+ return;
759
+ }
760
+ if (inflight) {
761
+ if (reason === "auth-error") pendingAuthError = true;
762
+ options.onTriggerSkipped?.(reason, "already-inflight", options.kernel.phase.kind);
763
+ return;
764
+ }
765
+ const phase = options.kernel.phase;
766
+ const target = phase.kind === "online" ? phase.target : phase.kind === "reconnecting" ? phase.lastTarget : null;
767
+ if (!target) {
768
+ options.onTriggerSkipped?.(reason, "no-target", phase.kind);
769
+ return;
770
+ }
771
+ inflight = true;
772
+ options.onRefreshStart?.(reason);
773
+ try {
774
+ const result = await (options.acquireRefreshLock ?? ((fn) => fn()))(async () => {
775
+ const fresh = options.getLatestTokens?.();
776
+ if (fresh?.accessExpiresAt && fresh.accessExpiresAt > now() + graceMs) {
777
+ options.kernel.dispatch({
778
+ type: "TOKENS_REFRESHED",
779
+ tokens: fresh
780
+ });
781
+ return {
782
+ tokens: fresh,
783
+ skipped: true
784
+ };
785
+ }
786
+ const tokens = await options.refresh(target, reason);
787
+ if (detached) return {
788
+ tokens,
789
+ skipped: false
790
+ };
791
+ options.kernel.dispatch({
792
+ type: "TOKENS_REFRESHED",
793
+ tokens
794
+ });
795
+ return {
796
+ tokens,
797
+ skipped: false
798
+ };
799
+ });
800
+ if (detached) return;
801
+ transientRetries = 0;
802
+ if (result.skipped) options.onRefreshSkipped?.(reason, result.tokens);
803
+ else options.onRefreshSuccess?.(reason, result.tokens);
804
+ } catch (err) {
805
+ if (detached) return;
806
+ const transient = isTransient(err);
807
+ if (transient && transientRetries < maxTransientRetries) {
808
+ transientRetries++;
809
+ const retriesLeft = maxTransientRetries - transientRetries;
810
+ options.onRefreshError?.(reason, err, {
811
+ transient: true,
812
+ retriesLeft,
813
+ willRetry: true
814
+ });
815
+ cancelTimer();
816
+ timer = setTimer(() => {
817
+ timer = void 0;
818
+ triggerRefresh(reason);
819
+ }, transientRetryDelayMs);
820
+ } else {
821
+ transientRetries = 0;
822
+ options.onRefreshError?.(reason, err, {
823
+ transient,
824
+ retriesLeft: 0,
825
+ willRetry: false
826
+ });
827
+ options.kernel.dispatch({
828
+ type: "TOKENS_INVALID",
829
+ reason: stringifyError(err),
830
+ transient
831
+ });
832
+ }
833
+ } finally {
834
+ inflight = false;
835
+ if (!detached && pendingAuthError) {
836
+ pendingAuthError = false;
837
+ triggerRefresh("auth-error");
838
+ }
839
+ }
840
+ }
841
+ const unsubKernel = options.kernel.subscribe((next, prev) => {
842
+ if (next.kind === "online") {
843
+ if (prev.kind !== "online" || prev.target.tokens.access !== next.target.tokens.access) {
844
+ transientRetries = 0;
845
+ schedule(next.target);
846
+ }
847
+ } else if (next.kind === "reconnecting") {} else {
848
+ cancelTimer();
849
+ transientRetries = 0;
850
+ }
851
+ });
852
+ cleanups.push(unsubKernel);
853
+ for (const transport of options.transports) {
854
+ const off = transport.on("auth-error", () => {
855
+ triggerRefresh("auth-error");
856
+ });
857
+ cleanups.push(off);
858
+ }
859
+ if (options.kernel.phase.kind === "online") schedule(options.kernel.phase.target);
860
+ function detach() {
861
+ detached = true;
862
+ cancelTimer();
863
+ for (const fn of cleanups) fn();
864
+ }
865
+ function wake() {
866
+ if (detached) return;
867
+ const phase = options.kernel.phase;
868
+ const target = phase.kind === "online" ? phase.target : phase.kind === "reconnecting" ? phase.lastTarget : null;
869
+ if (!target) {
870
+ options.onWakeChecked?.({
871
+ decision: "no-target",
872
+ phase: phase.kind
873
+ });
874
+ return;
875
+ }
876
+ const exp = target.tokens.accessExpiresAt;
877
+ if (!exp) {
878
+ options.onWakeChecked?.({
879
+ decision: "no-expiry",
880
+ phase: phase.kind
881
+ });
882
+ return;
883
+ }
884
+ const remaining = exp - now();
885
+ if (remaining < graceMs) {
886
+ options.onWakeChecked?.({
887
+ decision: "refresh-now",
888
+ remainingMs: remaining,
889
+ phase: phase.kind
890
+ });
891
+ triggerRefresh("proactive");
892
+ } else options.onWakeChecked?.({
893
+ decision: "still-fresh",
894
+ remainingMs: remaining,
895
+ phase: phase.kind
896
+ });
897
+ }
898
+ return {
899
+ detach,
900
+ wake
901
+ };
902
+ }
903
+ function stringifyError(err) {
904
+ if (err instanceof Error) return err.message;
905
+ if (typeof err === "string") return err;
906
+ return "refresh-failed";
907
+ }
908
+ //#endregion
909
+ //#region src/effects/transportSync.ts
910
+ var SKIP = Symbol("transport-sync-skip");
911
+ function syncTargetFor(phase) {
912
+ switch (phase.kind) {
913
+ case "idle":
914
+ case "offline":
915
+ case "needs-auth": return null;
916
+ case "online": return phase.target;
917
+ case "reconnecting": return phase.lastTarget;
918
+ case "discovering": return SKIP;
919
+ }
920
+ }
921
+ function attachTransportSync(options) {
922
+ let detached = false;
923
+ let initialized = false;
924
+ let lastApplied = null;
925
+ function applyAll(target) {
926
+ if (detached) return;
927
+ if (initialized && isSameTarget(lastApplied, target)) return;
928
+ initialized = true;
929
+ lastApplied = target;
930
+ for (const transport of options.transports) transport.apply(target).catch((err) => {
931
+ options.onError?.(transport, target, err);
932
+ });
933
+ options.onApplied?.(target);
934
+ }
935
+ function syncFromPhase(phase) {
936
+ const decision = syncTargetFor(phase);
937
+ if (decision === SKIP) return;
938
+ applyAll(decision);
939
+ }
940
+ syncFromPhase(options.kernel.phase);
941
+ const unsub = options.kernel.subscribe((next) => {
942
+ syncFromPhase(next);
943
+ });
944
+ return () => {
945
+ detached = true;
946
+ unsub();
947
+ };
948
+ }
949
+ //#endregion
950
+ //#region src/effects/transportWatchdog.ts
951
+ var DEFAULT_GRACE_MS = 4e3;
952
+ function attachTransportWatchdog(options) {
953
+ const defaultGraceMs = options.defaultGraceMs ?? DEFAULT_GRACE_MS;
954
+ const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
955
+ const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
956
+ const timers = /* @__PURE__ */ new Map();
957
+ const cleanups = [];
958
+ let detached = false;
959
+ function cancelTimer(id, reason) {
960
+ const handle = timers.get(id);
961
+ if (handle !== void 0) {
962
+ clearTimer(handle);
963
+ timers.delete(id);
964
+ options.onGraceCleared?.(id, reason);
965
+ }
966
+ }
967
+ function cancelAll(reason) {
968
+ for (const id of [...timers.keys()]) cancelTimer(id, reason);
969
+ }
970
+ for (const transport of options.transports) {
971
+ const spec = transport.spec;
972
+ const offUp = transport.on("up", () => {
973
+ if (detached) return;
974
+ cancelTimer(spec.id, "up");
975
+ options.kernel.dispatch({
976
+ type: "TRANSPORT_UP",
977
+ id: spec.id
978
+ });
979
+ });
980
+ const offDown = transport.on("down", (payload) => {
981
+ if (detached) return;
982
+ options.kernel.dispatch({
983
+ type: "TRANSPORT_DOWN",
984
+ id: spec.id,
985
+ reason: payload.reason
986
+ });
987
+ if (!spec.phaseGating) return;
988
+ if (options.kernel.phase.kind !== "online") return;
989
+ if (timers.has(spec.id)) return;
990
+ const graceMs = spec.graceMs ?? defaultGraceMs;
991
+ options.onGraceStarted?.(spec.id, graceMs);
992
+ const handle = setTimer(() => {
993
+ timers.delete(spec.id);
994
+ if (detached) return;
995
+ options.onConfirmed?.(spec.id);
996
+ options.kernel.dispatch({
997
+ type: "TRANSPORT_DOWN_CONFIRMED",
998
+ id: spec.id
999
+ });
1000
+ }, graceMs);
1001
+ timers.set(spec.id, handle);
1002
+ });
1003
+ cleanups.push(offUp, offDown);
1004
+ }
1005
+ const unsubKernel = options.kernel.subscribe((next, prev) => {
1006
+ if (prev.kind === "online" && next.kind !== "online") cancelAll("phase-change");
1007
+ });
1008
+ cleanups.push(unsubKernel);
1009
+ return () => {
1010
+ detached = true;
1011
+ cancelAll("detach");
1012
+ for (const fn of cleanups) fn();
1013
+ };
1014
+ }
1015
+ //#endregion
1016
+ //#region src/effects/workerBridge.ts
1017
+ function attachWorkerBridge(options) {
1018
+ let generation = 0;
1019
+ let detached = false;
1020
+ const hostListenerCleanups = /* @__PURE__ */ new Map();
1021
+ function makeSync(phase) {
1022
+ generation++;
1023
+ return {
1024
+ type: "kernel-sync",
1025
+ generation,
1026
+ phase
1027
+ };
1028
+ }
1029
+ function broadcast(phase) {
1030
+ if (detached) return;
1031
+ const msg = makeSync(phase);
1032
+ let count = 0;
1033
+ for (const host of options.hosts()) try {
1034
+ host.postMessage(msg);
1035
+ count++;
1036
+ maybeAttachHostListener(host);
1037
+ } catch (err) {
1038
+ console.warn("[workerBridge] broadcast postMessage failed", {
1039
+ gen: msg.generation,
1040
+ phase: phase.kind,
1041
+ err
1042
+ });
1043
+ }
1044
+ options.onBroadcast?.(generation, count);
1045
+ }
1046
+ function syncOne(host) {
1047
+ if (detached) return;
1048
+ const msg = makeSync(options.kernel.phase);
1049
+ try {
1050
+ host.postMessage(msg);
1051
+ maybeAttachHostListener(host);
1052
+ options.onSyncHost?.(generation);
1053
+ } catch (err) {
1054
+ console.warn("[workerBridge] syncOne postMessage failed", {
1055
+ gen: msg.generation,
1056
+ err
1057
+ });
1058
+ }
1059
+ }
1060
+ function maybeAttachHostListener(host) {
1061
+ if (!options.listenForResyncRequests) return;
1062
+ if (!host.addEventListener) return;
1063
+ if (hostListenerCleanups.has(host)) return;
1064
+ const listener = (event) => {
1065
+ if (detached) return;
1066
+ if (event.data?.type === "kernel-sync-request") syncOne(host);
1067
+ };
1068
+ host.addEventListener("message", listener);
1069
+ hostListenerCleanups.set(host, () => {
1070
+ host.removeEventListener?.("message", listener);
1071
+ });
1072
+ }
1073
+ const unsubKernel = options.kernel.subscribe((next) => {
1074
+ broadcast(next);
1075
+ });
1076
+ return {
1077
+ detach() {
1078
+ detached = true;
1079
+ unsubKernel();
1080
+ for (const cleanup of hostListenerCleanups.values()) cleanup();
1081
+ hostListenerCleanups.clear();
1082
+ },
1083
+ syncHost(host) {
1084
+ syncOne(host);
1085
+ },
1086
+ syncAll() {
1087
+ broadcast(options.kernel.phase);
1088
+ },
1089
+ revalidateWorkers() {
1090
+ if (detached) return;
1091
+ for (const host of options.hosts()) try {
1092
+ host.postMessage({ type: "kernel-revalidate" });
1093
+ maybeAttachHostListener(host);
1094
+ } catch (err) {
1095
+ console.warn("[workerBridge] revalidate postMessage failed", err);
1096
+ }
1097
+ }
1098
+ };
1099
+ }
1100
+ //#endregion
1101
+ export { DEFAULT_RACE_TIMEOUT_BY_MODE, RaceFirstError, TransportEmitter, attachBackoff, attachCrossTab, attachNetworkChange, attachPersistence, attachPresence, attachProbeLoop, attachTokenLifecycle, attachTransportSync, attachTransportWatchdog, attachWorkerBridge, createKernel, defaultOnNetworkOnline, endpointKey, isEndpointChange, isProbeFailure, isSameEndpoint, isSameTarget, isTokenOnlyChange, localStorageAdapter, makeProbeFailure, memoryStorageAdapter, raceFirst, reducer, sortByPriority };