@camera.ui/transport 0.0.29 → 0.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.
- package/dist/closeCodes-CltSRjdJ.js +30 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/effects/backgroundProbe.d.ts +1 -0
- package/dist/effects/crossTab.d.ts +6 -0
- package/dist/effects/degradedRecovery.d.ts +1 -0
- package/dist/effects/persistence.d.ts +2 -0
- package/dist/effects/probeLoop.d.ts +1 -0
- package/dist/effects/tokenLifecycle.d.ts +8 -3
- package/dist/effects/workerBridge.d.ts +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +378 -82
- package/dist/signal.d.ts +2 -0
- package/dist/testing/fakeTransport.d.ts +2 -1
- package/dist/testing.js +7 -3
- package/dist/transports/closeCodes.d.ts +2 -0
- package/dist/transports/contract.d.ts +2 -0
- package/dist/transports/http.d.ts +1 -0
- package/dist/transports/http.js +70 -3
- package/dist/transports/nativeHttp.d.ts +3 -1
- package/dist/transports/nativeHttp.js +118 -14
- package/dist/transports/nats.js +185 -86
- package/dist/transports/socketio.d.ts +4 -1
- package/dist/transports/socketio.js +168 -67
- package/dist/transports/ws.d.ts +0 -1
- package/dist/transports/ws.js +39 -13
- package/package.json +15 -14
- package/dist/closeCodes-ClWJVOpq.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { n as refusedByServer, t as classifyClose } from "./closeCodes-CltSRjdJ.js";
|
|
1
2
|
import { n as isEndpointChange, r as isSameTarget, t as TransportEmitter } from "./contract-dt1m8U-o.js";
|
|
2
|
-
import { t as classifyClose } from "./closeCodes-ClWJVOpq.js";
|
|
3
3
|
import { Logger } from "@camera.ui/logger";
|
|
4
4
|
//#region src/core/reducer.ts
|
|
5
5
|
var DEFAULT_BACKOFF_MS = 5e3;
|
|
@@ -8,15 +8,15 @@ function reducer(phase, action, ctx) {
|
|
|
8
8
|
case "RESET": return phase.kind === "idle" ? phase : { kind: "idle" };
|
|
9
9
|
case "BOOT":
|
|
10
10
|
if (phase.kind !== "idle" && phase.kind !== "offline" && phase.kind !== "needs-auth") return phase;
|
|
11
|
-
return {
|
|
11
|
+
return withPending({
|
|
12
12
|
kind: "discovering",
|
|
13
13
|
instanceId: action.instanceId
|
|
14
|
-
};
|
|
14
|
+
}, phase);
|
|
15
15
|
case "USER_RETRY":
|
|
16
|
-
if (phase.kind === "offline" || phase.kind === "needs-auth") return {
|
|
16
|
+
if (phase.kind === "offline" || phase.kind === "needs-auth") return withPending({
|
|
17
17
|
kind: "discovering",
|
|
18
18
|
instanceId: phase.instanceId ?? ""
|
|
19
|
-
};
|
|
19
|
+
}, phase);
|
|
20
20
|
return phase;
|
|
21
21
|
case "PROBE_SUCCEEDED":
|
|
22
22
|
if (phase.kind !== "discovering") return phase;
|
|
@@ -52,7 +52,7 @@ function reducer(phase, action, ctx) {
|
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
-
if (phase.kind === "discovering") {
|
|
55
|
+
if (phase.kind === "discovering" || phase.kind === "offline") {
|
|
56
56
|
if (phase.pendingTokens && tokensEqual(phase.pendingTokens, action.tokens)) return phase;
|
|
57
57
|
return {
|
|
58
58
|
...phase,
|
|
@@ -96,7 +96,7 @@ function reducer(phase, action, ctx) {
|
|
|
96
96
|
if (phase.kind !== "offline") return phase;
|
|
97
97
|
const now = ctx.now();
|
|
98
98
|
const candidate = now + action.retryAfterMs;
|
|
99
|
-
if (candidate <= phase.nextRetryAt) return phase;
|
|
99
|
+
if (phase.backoffHint && candidate <= phase.nextRetryAt) return phase;
|
|
100
100
|
return {
|
|
101
101
|
...phase,
|
|
102
102
|
nextRetryAt: candidate,
|
|
@@ -109,6 +109,12 @@ function reducer(phase, action, ctx) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
function withPending(next, prev) {
|
|
113
|
+
return prev.kind === "offline" && prev.pendingTokens ? {
|
|
114
|
+
...next,
|
|
115
|
+
pendingTokens: prev.pendingTokens
|
|
116
|
+
} : next;
|
|
117
|
+
}
|
|
112
118
|
function tokensEqual(a, b) {
|
|
113
119
|
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;
|
|
114
120
|
}
|
|
@@ -311,8 +317,12 @@ function createBackgroundProbe(options) {
|
|
|
311
317
|
options.onResult?.("failed", "empty pool");
|
|
312
318
|
return "failed";
|
|
313
319
|
}
|
|
320
|
+
if (options.ready) {
|
|
321
|
+
await options.ready().catch(() => void 0);
|
|
322
|
+
if (disposed || options.kernel.phase.kind !== "online") return "skipped";
|
|
323
|
+
}
|
|
314
324
|
const lastTokens = options.lastTarget?.()?.tokens;
|
|
315
|
-
const { endpoint, value
|
|
325
|
+
const { endpoint, value } = await raceFirst(pool.map((endpoint) => ({
|
|
316
326
|
endpoint,
|
|
317
327
|
run: (signal) => options.probe({
|
|
318
328
|
endpoint,
|
|
@@ -333,10 +343,17 @@ function createBackgroundProbe(options) {
|
|
|
333
343
|
}
|
|
334
344
|
const current = phase.target.endpoint;
|
|
335
345
|
const same = current.url === endpoint.url && current.mode === endpoint.mode;
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
const preferredWinner = options.prefer?.(endpoint) ?? false;
|
|
347
|
+
const preferredCurrent = options.prefer?.(current) ?? false;
|
|
348
|
+
if (!same && options.isCurrentHealthy?.() === true && !(preferredWinner && !preferredCurrent)) {
|
|
349
|
+
if (value !== lastTokens) options.kernel.dispatch({
|
|
350
|
+
type: "TOKENS_REFRESHED",
|
|
351
|
+
tokens: value
|
|
352
|
+
});
|
|
353
|
+
options.onResult?.("kept", preferredWinner === preferredCurrent ? `${current.url} (same class as ${endpoint.url})` : current.url);
|
|
338
354
|
return "kept";
|
|
339
355
|
}
|
|
356
|
+
const tokens = value === lastTokens ? phase.target.tokens : value;
|
|
340
357
|
options.kernel.dispatch({
|
|
341
358
|
type: "ENDPOINT_SWAP",
|
|
342
359
|
endpoint,
|
|
@@ -379,8 +396,10 @@ function attachBackoff(options) {
|
|
|
379
396
|
const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
380
397
|
const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
|
|
381
398
|
let timer;
|
|
399
|
+
let firesAt = 0;
|
|
382
400
|
let attempt = 0;
|
|
383
401
|
let detached = false;
|
|
402
|
+
const ownRetry = { type: "USER_RETRY" };
|
|
384
403
|
function cancelTimer(reason) {
|
|
385
404
|
if (timer !== void 0) {
|
|
386
405
|
clearTimer(timer);
|
|
@@ -388,34 +407,42 @@ function attachBackoff(options) {
|
|
|
388
407
|
if (reason) options.onCancelled?.(reason);
|
|
389
408
|
}
|
|
390
409
|
}
|
|
410
|
+
function fastDelay() {
|
|
411
|
+
const quick = options.firstAttemptDelayMs?.();
|
|
412
|
+
return typeof quick === "number" && quick >= 0 ? quick : void 0;
|
|
413
|
+
}
|
|
391
414
|
function scheduleFromPhase(phase, isReschedule) {
|
|
392
415
|
cancelTimer(isReschedule ? "rescheduled" : void 0);
|
|
393
416
|
const baseDelay = schedule[Math.min(attempt, schedule.length - 1)];
|
|
394
|
-
const quick = attempt === 0 ?
|
|
395
|
-
const scheduleDelay =
|
|
396
|
-
const phaseDelay = Math.max(0, phase.nextRetryAt - now());
|
|
417
|
+
const quick = attempt === 0 ? fastDelay() : void 0;
|
|
418
|
+
const scheduleDelay = quick === void 0 ? baseDelay : Math.min(quick, baseDelay);
|
|
419
|
+
const phaseDelay = phase.backoffHint ? Math.max(0, phase.nextRetryAt - now()) : 0;
|
|
397
420
|
const delay = Math.max(scheduleDelay, phaseDelay);
|
|
398
421
|
options.onScheduled?.(attempt + 1, delay);
|
|
399
422
|
if (phase.backoffHint && phaseDelay > scheduleDelay) options.onHintApplied?.(delay, phase.backoffHint.source);
|
|
423
|
+
firesAt = now() + delay;
|
|
400
424
|
timer = setTimer(() => {
|
|
401
425
|
timer = void 0;
|
|
402
426
|
if (detached) return;
|
|
403
427
|
if (options.kernel.phase.kind !== "offline") return;
|
|
404
428
|
attempt += 1;
|
|
405
429
|
options.onFire?.(attempt);
|
|
406
|
-
options.kernel.dispatch(
|
|
430
|
+
options.kernel.dispatch(ownRetry);
|
|
407
431
|
}, delay);
|
|
408
432
|
}
|
|
409
|
-
const unsubKernel = options.kernel.subscribe((next, prev) => {
|
|
433
|
+
const unsubKernel = options.kernel.subscribe((next, prev, action) => {
|
|
410
434
|
if (next.kind === "offline" && prev.kind !== "offline") {
|
|
411
435
|
scheduleFromPhase(next, false);
|
|
412
436
|
return;
|
|
413
437
|
}
|
|
414
|
-
if (next.kind === "offline" && prev.kind === "offline" && next.
|
|
415
|
-
scheduleFromPhase(next, true);
|
|
438
|
+
if (next.kind === "offline" && prev.kind === "offline" && next.backoffHint !== prev.backoffHint) {
|
|
439
|
+
if (next.backoffHint && next.nextRetryAt > firesAt) scheduleFromPhase(next, true);
|
|
416
440
|
return;
|
|
417
441
|
}
|
|
418
|
-
if (prev.kind === "offline" && next.kind !== "offline")
|
|
442
|
+
if (prev.kind === "offline" && next.kind !== "offline") {
|
|
443
|
+
cancelTimer("phase-left-offline");
|
|
444
|
+
if (action.type === "USER_RETRY" && action !== ownRetry && fastDelay() !== void 0) attempt = 0;
|
|
445
|
+
}
|
|
419
446
|
if (next.kind === "online" || next.kind === "idle") attempt = 0;
|
|
420
447
|
});
|
|
421
448
|
if (options.kernel.phase.kind === "offline") scheduleFromPhase(options.kernel.phase, false);
|
|
@@ -450,7 +477,16 @@ function attachCrossTab(options) {
|
|
|
450
477
|
return;
|
|
451
478
|
}
|
|
452
479
|
if (!parsed?.tokens?.access) return;
|
|
453
|
-
|
|
480
|
+
const payload = {
|
|
481
|
+
endpoint: parsed.endpoint,
|
|
482
|
+
tokens: parsed.tokens
|
|
483
|
+
};
|
|
484
|
+
const phase = options.kernel.phase;
|
|
485
|
+
if (options.accept && !options.accept(payload, phase.kind === "online" ? phase.target : null)) {
|
|
486
|
+
options.onTokensIgnored?.(payload);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (phase.kind !== "online") {
|
|
454
490
|
if (parsed.endpoint?.url) {
|
|
455
491
|
options.absorb?.({
|
|
456
492
|
endpoint: parsed.endpoint,
|
|
@@ -474,24 +510,37 @@ function attachCrossTab(options) {
|
|
|
474
510
|
//#endregion
|
|
475
511
|
//#region src/effects/degradedRecovery.ts
|
|
476
512
|
var DEFAULT_GRACE_MS$1 = 1e4;
|
|
513
|
+
var DEFAULT_MAX_BACKOFF_MS = 3e5;
|
|
477
514
|
function attachDegradedRecovery(options) {
|
|
478
515
|
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS$1;
|
|
516
|
+
const maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
479
517
|
let timer = null;
|
|
480
518
|
let round = 0;
|
|
519
|
+
let backoff = 0;
|
|
520
|
+
let channels = "";
|
|
481
521
|
let detached = false;
|
|
482
|
-
function
|
|
522
|
+
function clearTimer() {
|
|
483
523
|
if (timer !== null) {
|
|
484
524
|
clearTimeout(timer);
|
|
485
525
|
timer = null;
|
|
486
526
|
}
|
|
527
|
+
}
|
|
528
|
+
function disarm() {
|
|
529
|
+
clearTimer();
|
|
487
530
|
round = 0;
|
|
531
|
+
backoff = 0;
|
|
532
|
+
channels = "";
|
|
488
533
|
}
|
|
489
534
|
function arm() {
|
|
490
535
|
if (detached || timer !== null) return;
|
|
491
536
|
timer = setTimeout(() => {
|
|
492
537
|
timer = null;
|
|
493
538
|
escalate();
|
|
494
|
-
}, graceMs);
|
|
539
|
+
}, Math.min(graceMs * 2 ** backoff, maxBackoffMs));
|
|
540
|
+
}
|
|
541
|
+
function rearm() {
|
|
542
|
+
backoff = options.isReachable?.() ? backoff + 1 : 0;
|
|
543
|
+
arm();
|
|
495
544
|
}
|
|
496
545
|
async function escalate() {
|
|
497
546
|
if (detached || options.signal.current.kind !== "degraded") return;
|
|
@@ -503,7 +552,7 @@ function attachDegradedRecovery(options) {
|
|
|
503
552
|
if (outcome === "failed") {
|
|
504
553
|
if (options.isReachable?.()) {
|
|
505
554
|
options.onHold?.("probe failed but a channel is up — holding");
|
|
506
|
-
|
|
555
|
+
rearm();
|
|
507
556
|
return;
|
|
508
557
|
}
|
|
509
558
|
options.onOffline?.("degraded: endpoint unreachable");
|
|
@@ -513,13 +562,31 @@ function attachDegradedRecovery(options) {
|
|
|
513
562
|
});
|
|
514
563
|
return;
|
|
515
564
|
}
|
|
516
|
-
|
|
565
|
+
rearm();
|
|
566
|
+
}
|
|
567
|
+
function lostWanTarget() {
|
|
568
|
+
const phase = options.kernel.phase;
|
|
569
|
+
return phase.kind === "online" && phase.target.endpoint.mode === "direct-wan" && options.isReachable?.() === false;
|
|
570
|
+
}
|
|
571
|
+
function onDegraded(signal) {
|
|
572
|
+
const next = signal.channels.join(",");
|
|
573
|
+
if (next !== channels) {
|
|
574
|
+
channels = next;
|
|
575
|
+
if (backoff > 0) {
|
|
576
|
+
backoff = 0;
|
|
577
|
+
clearTimer();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (round === 0 && lostWanTarget()) {
|
|
581
|
+
disarm();
|
|
582
|
+
escalate();
|
|
583
|
+
} else arm();
|
|
517
584
|
}
|
|
518
585
|
const unsub = options.signal.subscribe((next) => {
|
|
519
|
-
if (next.kind === "degraded")
|
|
586
|
+
if (next.kind === "degraded") onDegraded(next);
|
|
520
587
|
else disarm();
|
|
521
588
|
});
|
|
522
|
-
if (options.signal.current.kind === "degraded")
|
|
589
|
+
if (options.signal.current.kind === "degraded") onDegraded(options.signal.current);
|
|
523
590
|
return () => {
|
|
524
591
|
detached = true;
|
|
525
592
|
disarm();
|
|
@@ -557,19 +624,30 @@ function attachNetworkChange(options) {
|
|
|
557
624
|
//#endregion
|
|
558
625
|
//#region src/effects/persistence.ts
|
|
559
626
|
var DEFAULT_KEY = "camera.ui:transport:target";
|
|
627
|
+
var DEFAULT_READY_TIMEOUT_MS = 3e3;
|
|
560
628
|
function attachPersistence(options) {
|
|
561
629
|
const key = options.key ?? DEFAULT_KEY;
|
|
562
630
|
const onError = options.onError ?? (() => {});
|
|
631
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
563
632
|
let detached = false;
|
|
564
633
|
let cached = null;
|
|
565
|
-
|
|
634
|
+
let readyTimer;
|
|
635
|
+
let settleReady;
|
|
636
|
+
const ready = new Promise((resolve) => {
|
|
637
|
+
settleReady = () => {
|
|
638
|
+
clearTimeout(readyTimer);
|
|
639
|
+
resolve();
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
if (readyTimeoutMs > 0 && Number.isFinite(readyTimeoutMs)) readyTimer = setTimeout(settleReady, readyTimeoutMs);
|
|
643
|
+
restore().then(settleReady, settleReady);
|
|
566
644
|
async function restore() {
|
|
567
|
-
let raw;
|
|
568
|
-
try {
|
|
645
|
+
let raw = null;
|
|
646
|
+
for (let attempt = 0; attempt < 2; attempt++) try {
|
|
569
647
|
raw = await options.storage.get(key);
|
|
648
|
+
break;
|
|
570
649
|
} catch (err) {
|
|
571
650
|
onError("get", err);
|
|
572
|
-
raw = null;
|
|
573
651
|
}
|
|
574
652
|
if (detached) return;
|
|
575
653
|
if (cached !== null) {
|
|
@@ -625,10 +703,11 @@ function attachPersistence(options) {
|
|
|
625
703
|
}
|
|
626
704
|
const unsub = options.kernel.subscribe((next, prev) => {
|
|
627
705
|
if (detached) return;
|
|
628
|
-
|
|
706
|
+
const pending = pendingTokensOf(next);
|
|
707
|
+
if (pending && pending !== pendingTokensOf(prev)) {
|
|
629
708
|
if (cached) persist({
|
|
630
709
|
endpoint: cached.endpoint,
|
|
631
|
-
tokens:
|
|
710
|
+
tokens: pending
|
|
632
711
|
});
|
|
633
712
|
return;
|
|
634
713
|
}
|
|
@@ -643,8 +722,10 @@ function attachPersistence(options) {
|
|
|
643
722
|
return {
|
|
644
723
|
detach: () => {
|
|
645
724
|
detached = true;
|
|
725
|
+
settleReady();
|
|
646
726
|
unsub();
|
|
647
727
|
},
|
|
728
|
+
ready,
|
|
648
729
|
peek: () => cached,
|
|
649
730
|
seed: async (target) => {
|
|
650
731
|
if (detached) return;
|
|
@@ -656,8 +737,14 @@ function attachPersistence(options) {
|
|
|
656
737
|
}
|
|
657
738
|
};
|
|
658
739
|
}
|
|
659
|
-
function
|
|
660
|
-
|
|
740
|
+
function pendingTokensOf(phase) {
|
|
741
|
+
return phase.kind === "discovering" || phase.kind === "offline" ? phase.pendingTokens : void 0;
|
|
742
|
+
}
|
|
743
|
+
function localStorageAdapter(scope) {
|
|
744
|
+
if (!scope) try {
|
|
745
|
+
scope = globalThis.localStorage;
|
|
746
|
+
} catch {}
|
|
747
|
+
if (!scope) return memoryStorageAdapter();
|
|
661
748
|
return {
|
|
662
749
|
get(k) {
|
|
663
750
|
return scope.getItem(k);
|
|
@@ -730,10 +817,18 @@ function makeProbeFailure(kind, message) {
|
|
|
730
817
|
function isProbeFailure(err) {
|
|
731
818
|
return err instanceof Error && typeof err.kind === "string";
|
|
732
819
|
}
|
|
820
|
+
var CANCELED_RERUN_MS = 200;
|
|
821
|
+
var MAX_CANCELED_ROUNDS = 3;
|
|
733
822
|
function attachProbeLoop(options) {
|
|
734
823
|
let masterAbort;
|
|
735
824
|
let detached = false;
|
|
825
|
+
let canceledRounds = 0;
|
|
826
|
+
let rerunTimer;
|
|
736
827
|
function cancel() {
|
|
828
|
+
if (rerunTimer !== void 0) {
|
|
829
|
+
clearTimeout(rerunTimer);
|
|
830
|
+
rerunTimer = void 0;
|
|
831
|
+
}
|
|
737
832
|
if (masterAbort) {
|
|
738
833
|
masterAbort.abort();
|
|
739
834
|
masterAbort = void 0;
|
|
@@ -744,6 +839,8 @@ function attachProbeLoop(options) {
|
|
|
744
839
|
cancel();
|
|
745
840
|
const ctrl = new AbortController();
|
|
746
841
|
masterAbort = ctrl;
|
|
842
|
+
const startPhase = options.kernel.phase;
|
|
843
|
+
const pendingAtStart = startPhase.kind === "discovering" ? startPhase.pendingTokens : void 0;
|
|
747
844
|
let pool;
|
|
748
845
|
try {
|
|
749
846
|
options.onDiscoverStart?.();
|
|
@@ -770,6 +867,10 @@ function attachProbeLoop(options) {
|
|
|
770
867
|
});
|
|
771
868
|
return;
|
|
772
869
|
}
|
|
870
|
+
if (options.ready) {
|
|
871
|
+
await options.ready().catch(() => void 0);
|
|
872
|
+
if (ctrl.signal.aborted) return;
|
|
873
|
+
}
|
|
773
874
|
const lastTokens = options.lastTarget?.()?.tokens;
|
|
774
875
|
const candidates = pool.map((endpoint) => ({
|
|
775
876
|
endpoint,
|
|
@@ -795,7 +896,7 @@ function attachProbeLoop(options) {
|
|
|
795
896
|
}
|
|
796
897
|
}));
|
|
797
898
|
try {
|
|
798
|
-
const { endpoint, value
|
|
899
|
+
const { endpoint, value } = await raceFirst(candidates, {
|
|
799
900
|
timeoutByMode: options.timeoutByMode,
|
|
800
901
|
parentSignal: ctrl.signal,
|
|
801
902
|
prefer: options.prefer,
|
|
@@ -804,6 +905,9 @@ function attachProbeLoop(options) {
|
|
|
804
905
|
informative: (cause) => !(isProbeFailure(cause) && cause.kind === "aborted")
|
|
805
906
|
});
|
|
806
907
|
if (ctrl.signal.aborted) return;
|
|
908
|
+
const phase = options.kernel.phase;
|
|
909
|
+
const refreshed = phase.kind === "discovering" && phase.pendingTokens !== pendingAtStart ? phase.pendingTokens : void 0;
|
|
910
|
+
const tokens = value === lastTokens && refreshed ? refreshed : value;
|
|
807
911
|
options.kernel.dispatch({
|
|
808
912
|
type: "PROBE_SUCCEEDED",
|
|
809
913
|
endpoint,
|
|
@@ -812,13 +916,15 @@ function attachProbeLoop(options) {
|
|
|
812
916
|
} catch (err) {
|
|
813
917
|
if (ctrl.signal.aborted) return;
|
|
814
918
|
const underlying = err instanceof RaceFirstError ? err.cause : err;
|
|
815
|
-
|
|
816
|
-
|
|
919
|
+
const allCanceled = isProbeFailure(underlying) && underlying.kind === "aborted";
|
|
920
|
+
if (allCanceled && ++canceledRounds < MAX_CANCELED_ROUNDS) {
|
|
921
|
+
rerunTimer = setTimeout(() => {
|
|
922
|
+
rerunTimer = void 0;
|
|
817
923
|
if (!detached && options.kernel.phase.kind === "discovering") runRound();
|
|
818
|
-
},
|
|
924
|
+
}, CANCELED_RERUN_MS);
|
|
819
925
|
return;
|
|
820
926
|
}
|
|
821
|
-
const reason = isProbeFailure(underlying) && underlying.kind === "needs-auth" ? "needs-auth" : underlying instanceof Error ? underlying.message : "all endpoints failed";
|
|
927
|
+
const reason = allCanceled ? `${underlying.message} (${canceledRounds} rounds, certificate blocked?)` : isProbeFailure(underlying) && underlying.kind === "needs-auth" ? "needs-auth" : underlying instanceof Error ? underlying.message : "all endpoints failed";
|
|
822
928
|
options.onAllFailed?.(reason);
|
|
823
929
|
options.kernel.dispatch({
|
|
824
930
|
type: "PROBE_FAILED_ALL",
|
|
@@ -827,6 +933,7 @@ function attachProbeLoop(options) {
|
|
|
827
933
|
}
|
|
828
934
|
}
|
|
829
935
|
const unsub = options.kernel.subscribe((next, prev) => {
|
|
936
|
+
if (next.kind !== prev.kind) canceledRounds = 0;
|
|
830
937
|
if (next.kind === "discovering" && prev.kind !== "discovering") runRound();
|
|
831
938
|
else if (next.kind !== "discovering" && prev.kind === "discovering") cancel();
|
|
832
939
|
});
|
|
@@ -842,22 +949,36 @@ function attachProbeLoop(options) {
|
|
|
842
949
|
var DEFAULT_GRACE_MS = 5e3;
|
|
843
950
|
var DEFAULT_MAX_TRANSIENT_RETRIES = 3;
|
|
844
951
|
var DEFAULT_TRANSIENT_RETRY_DELAY_MS = 2e3;
|
|
952
|
+
var DEFAULT_MIN_REFRESH_INTERVAL_MS = 3e4;
|
|
953
|
+
var DEFAULT_REACHABLE_RETRY_DELAYS_MS = [1e4, 3e4];
|
|
954
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
|
|
955
|
+
var AUTH_ERROR_RETRY_GAP_MS = 5e3;
|
|
845
956
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
846
957
|
function attachTokenLifecycle(options) {
|
|
847
958
|
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
848
959
|
const maxTransientRetries = options.maxTransientRetries ?? DEFAULT_MAX_TRANSIENT_RETRIES;
|
|
849
960
|
const maxServerErrorRetries = options.maxServerErrorRetries ?? maxTransientRetries;
|
|
850
961
|
const transientRetryDelayMs = options.transientRetryDelayMs ?? DEFAULT_TRANSIENT_RETRY_DELAY_MS;
|
|
962
|
+
const minRefreshIntervalMs = options.minRefreshIntervalMs ?? DEFAULT_MIN_REFRESH_INTERVAL_MS;
|
|
963
|
+
const reachableRetryDelaysMs = options.reachableRetryDelaysMs?.length ? options.reachableRetryDelaysMs : DEFAULT_REACHABLE_RETRY_DELAYS_MS;
|
|
964
|
+
const lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
965
|
+
const lockWaitBounded = Number.isFinite(lockTimeoutMs) && lockTimeoutMs > 0;
|
|
851
966
|
const isTransient = options.isTransientError ?? (() => false);
|
|
852
967
|
const isServerError = options.isServerError ?? (() => false);
|
|
853
968
|
const now = options.now ?? (() => Date.now());
|
|
854
969
|
const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
855
970
|
const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
|
|
856
971
|
let timer;
|
|
857
|
-
let inflight =
|
|
972
|
+
let inflight = null;
|
|
858
973
|
let detached = false;
|
|
859
974
|
let transientRetries = 0;
|
|
975
|
+
let slowRetries = 0;
|
|
976
|
+
let lastSlowFailureAt = 0;
|
|
860
977
|
let pendingAuthError = false;
|
|
978
|
+
let refreshFloorUntil;
|
|
979
|
+
let rotatedAccess;
|
|
980
|
+
let rejectedAccess;
|
|
981
|
+
let stopLockWait;
|
|
861
982
|
const cleanups = [];
|
|
862
983
|
function cancelTimer() {
|
|
863
984
|
if (timer !== void 0) {
|
|
@@ -865,11 +986,66 @@ function attachTokenLifecycle(options) {
|
|
|
865
986
|
timer = void 0;
|
|
866
987
|
}
|
|
867
988
|
}
|
|
989
|
+
function retryLater(reason, delayMs) {
|
|
990
|
+
cancelTimer();
|
|
991
|
+
timer = setTimer(() => {
|
|
992
|
+
timer = void 0;
|
|
993
|
+
triggerRefresh(reason);
|
|
994
|
+
}, delayMs);
|
|
995
|
+
}
|
|
996
|
+
function insideRefreshFloor() {
|
|
997
|
+
return refreshFloorUntil !== void 0 && now() < refreshFloorUntil;
|
|
998
|
+
}
|
|
999
|
+
function refreshFloorFor(tokens) {
|
|
1000
|
+
const lifetimeMs = tokens.accessExpiresAt ? tokens.accessExpiresAt - now() : void 0;
|
|
1001
|
+
if (lifetimeMs === void 0 || lifetimeMs <= graceMs) return minRefreshIntervalMs;
|
|
1002
|
+
return Math.min(minRefreshIntervalMs, Math.max(graceMs, lifetimeMs - graceMs));
|
|
1003
|
+
}
|
|
1004
|
+
function needsNoRefresh(tokens) {
|
|
1005
|
+
if (tokens.access === rotatedAccess && insideRefreshFloor()) return true;
|
|
1006
|
+
if (tokens.access === rejectedAccess && tokens.refresh) return false;
|
|
1007
|
+
return !!tokens.accessExpiresAt && tokens.accessExpiresAt > now() + graceMs;
|
|
1008
|
+
}
|
|
1009
|
+
function isCurrentAccess(access) {
|
|
1010
|
+
const phase = options.kernel.phase;
|
|
1011
|
+
return phase.kind === "online" && phase.target.tokens.access === access || options.getLatestTokens?.()?.access === access;
|
|
1012
|
+
}
|
|
1013
|
+
function underRefreshLock(fn) {
|
|
1014
|
+
const acquire = options.acquireRefreshLock;
|
|
1015
|
+
if (!acquire) return fn();
|
|
1016
|
+
if (!lockWaitBounded) return acquire(fn);
|
|
1017
|
+
return new Promise((resolve, reject) => {
|
|
1018
|
+
const abort = new AbortController();
|
|
1019
|
+
let waiting = true;
|
|
1020
|
+
function stopWaiting() {
|
|
1021
|
+
waiting = false;
|
|
1022
|
+
clearTimer(handle);
|
|
1023
|
+
if (stopLockWait === giveUp) stopLockWait = void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function giveUp() {
|
|
1026
|
+
if (!waiting) return;
|
|
1027
|
+
stopWaiting();
|
|
1028
|
+
abort.abort();
|
|
1029
|
+
resolve(null);
|
|
1030
|
+
}
|
|
1031
|
+
const handle = setTimer(giveUp, Math.min(lockTimeoutMs, MAX_TIMER_DELAY_MS));
|
|
1032
|
+
stopLockWait = giveUp;
|
|
1033
|
+
acquire(() => {
|
|
1034
|
+
if (!waiting) return Promise.resolve(null);
|
|
1035
|
+
stopWaiting();
|
|
1036
|
+
return fn();
|
|
1037
|
+
}, abort.signal).then(resolve, (err) => {
|
|
1038
|
+
stopWaiting();
|
|
1039
|
+
reject(err);
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
868
1043
|
function schedule(target) {
|
|
869
1044
|
cancelTimer();
|
|
870
1045
|
const exp = target.tokens.accessExpiresAt;
|
|
871
1046
|
if (!exp) return;
|
|
872
|
-
const
|
|
1047
|
+
const floorAt = refreshFloorUntil ?? 0;
|
|
1048
|
+
const delayMs = Math.min(Math.max(0, exp - now() - graceMs, floorAt - now()), MAX_TIMER_DELAY_MS);
|
|
873
1049
|
options.onScheduled?.(delayMs, exp);
|
|
874
1050
|
timer = setTimer(() => {
|
|
875
1051
|
timer = void 0;
|
|
@@ -879,25 +1055,28 @@ function attachTokenLifecycle(options) {
|
|
|
879
1055
|
async function triggerRefresh(reason) {
|
|
880
1056
|
if (detached) {
|
|
881
1057
|
options.onTriggerSkipped?.(reason, "detached", options.kernel.phase.kind);
|
|
882
|
-
return;
|
|
1058
|
+
return false;
|
|
883
1059
|
}
|
|
884
1060
|
if (inflight) {
|
|
885
1061
|
if (reason === "auth-error") pendingAuthError = true;
|
|
886
1062
|
options.onTriggerSkipped?.(reason, "already-inflight", options.kernel.phase.kind);
|
|
887
|
-
return;
|
|
1063
|
+
return inflight;
|
|
888
1064
|
}
|
|
889
1065
|
const phase = options.kernel.phase;
|
|
890
1066
|
const target = phase.kind === "online" ? phase.target : null;
|
|
891
1067
|
if (!target) {
|
|
892
1068
|
options.onTriggerSkipped?.(reason, "no-target", phase.kind);
|
|
893
|
-
return;
|
|
1069
|
+
return false;
|
|
894
1070
|
}
|
|
895
|
-
|
|
1071
|
+
let settle;
|
|
1072
|
+
let tokenComing = false;
|
|
1073
|
+
let lockTimedOut = false;
|
|
1074
|
+
inflight = new Promise((resolve) => settle = resolve);
|
|
896
1075
|
options.onRefreshStart?.(reason);
|
|
897
1076
|
try {
|
|
898
|
-
|
|
1077
|
+
let result = await underRefreshLock(async () => {
|
|
899
1078
|
const fresh = options.getLatestTokens?.();
|
|
900
|
-
if (fresh
|
|
1079
|
+
if (fresh && needsNoRefresh(fresh)) {
|
|
901
1080
|
options.kernel.dispatch({
|
|
902
1081
|
type: "TOKENS_REFRESHED",
|
|
903
1082
|
tokens: fresh
|
|
@@ -927,16 +1106,34 @@ function attachTokenLifecycle(options) {
|
|
|
927
1106
|
skipped: false
|
|
928
1107
|
};
|
|
929
1108
|
});
|
|
930
|
-
if (detached) return;
|
|
1109
|
+
if (detached) return false;
|
|
1110
|
+
if (!result) {
|
|
1111
|
+
const fresh = options.getLatestTokens?.();
|
|
1112
|
+
if (!fresh || !needsNoRefresh(fresh)) {
|
|
1113
|
+
lockTimedOut = true;
|
|
1114
|
+
throw new Error(`refresh lock not granted within ${lockTimeoutMs} ms`);
|
|
1115
|
+
}
|
|
1116
|
+
options.kernel.dispatch({
|
|
1117
|
+
type: "TOKENS_REFRESHED",
|
|
1118
|
+
tokens: fresh
|
|
1119
|
+
});
|
|
1120
|
+
result = {
|
|
1121
|
+
tokens: fresh,
|
|
1122
|
+
skipped: true
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
931
1125
|
transientRetries = 0;
|
|
1126
|
+
slowRetries = 0;
|
|
1127
|
+
const live = options.kernel.phase;
|
|
1128
|
+
tokenComing = live.kind === "online" && live.target.tokens.access !== target.tokens.access;
|
|
932
1129
|
if (result.skipped) {
|
|
933
1130
|
options.onRefreshSkipped?.(reason, result.tokens);
|
|
934
1131
|
const p = options.kernel.phase;
|
|
935
1132
|
if (p.kind === "online" && timer === void 0) schedule(p.target);
|
|
936
1133
|
} else options.onRefreshSuccess?.(reason, result.tokens);
|
|
937
1134
|
} catch (err) {
|
|
938
|
-
if (detached) return;
|
|
939
|
-
const transient = isTransient(err);
|
|
1135
|
+
if (detached) return false;
|
|
1136
|
+
const transient = lockTimedOut || isTransient(err);
|
|
940
1137
|
const serverError = transient && isServerError(err);
|
|
941
1138
|
const budget = serverError ? maxServerErrorRetries : maxTransientRetries;
|
|
942
1139
|
if (transient && transientRetries < budget) {
|
|
@@ -948,13 +1145,22 @@ function attachTokenLifecycle(options) {
|
|
|
948
1145
|
retriesLeft,
|
|
949
1146
|
willRetry: true
|
|
950
1147
|
});
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1148
|
+
tokenComing = true;
|
|
1149
|
+
retryLater(reason, transientRetryDelayMs);
|
|
1150
|
+
} else if (transient && options.isReachable?.() === true) {
|
|
1151
|
+
const delayMs = reachableRetryDelaysMs[Math.min(slowRetries, reachableRetryDelaysMs.length - 1)];
|
|
1152
|
+
slowRetries++;
|
|
1153
|
+
lastSlowFailureAt = now();
|
|
1154
|
+
options.onRefreshError?.(reason, err, {
|
|
1155
|
+
transient: true,
|
|
1156
|
+
serverError,
|
|
1157
|
+
retriesLeft: 0,
|
|
1158
|
+
willRetry: true
|
|
1159
|
+
});
|
|
1160
|
+
retryLater(reason, delayMs);
|
|
956
1161
|
} else {
|
|
957
1162
|
transientRetries = 0;
|
|
1163
|
+
slowRetries = 0;
|
|
958
1164
|
options.onRefreshError?.(reason, err, {
|
|
959
1165
|
transient,
|
|
960
1166
|
serverError,
|
|
@@ -968,28 +1174,45 @@ function attachTokenLifecycle(options) {
|
|
|
968
1174
|
});
|
|
969
1175
|
}
|
|
970
1176
|
} finally {
|
|
971
|
-
inflight =
|
|
1177
|
+
inflight = null;
|
|
1178
|
+
settle(tokenComing);
|
|
972
1179
|
if (!detached && pendingAuthError) {
|
|
973
1180
|
pendingAuthError = false;
|
|
974
|
-
|
|
1181
|
+
triggerOnAuthError();
|
|
975
1182
|
}
|
|
976
1183
|
}
|
|
1184
|
+
return tokenComing;
|
|
1185
|
+
}
|
|
1186
|
+
function triggerOnAuthError() {
|
|
1187
|
+
if (slowRetries > 0 && now() - lastSlowFailureAt < AUTH_ERROR_RETRY_GAP_MS) {
|
|
1188
|
+
options.onTriggerSkipped?.("auth-error", "throttled", options.kernel.phase.kind);
|
|
1189
|
+
return Promise.resolve(false);
|
|
1190
|
+
}
|
|
1191
|
+
return triggerRefresh("auth-error");
|
|
977
1192
|
}
|
|
978
1193
|
const unsubKernel = options.kernel.subscribe((next, prev) => {
|
|
979
1194
|
if (next.kind === "online") {
|
|
980
1195
|
if (prev.kind !== "online" || prev.target.tokens.access !== next.target.tokens.access) {
|
|
1196
|
+
if (prev.kind === "online") {
|
|
1197
|
+
refreshFloorUntil = now() + refreshFloorFor(next.target.tokens);
|
|
1198
|
+
rotatedAccess = next.target.tokens.access;
|
|
1199
|
+
}
|
|
981
1200
|
transientRetries = 0;
|
|
1201
|
+
slowRetries = 0;
|
|
982
1202
|
schedule(next.target);
|
|
983
1203
|
}
|
|
984
1204
|
} else {
|
|
985
1205
|
cancelTimer();
|
|
986
1206
|
transientRetries = 0;
|
|
1207
|
+
slowRetries = 0;
|
|
987
1208
|
}
|
|
988
1209
|
});
|
|
989
1210
|
cleanups.push(unsubKernel);
|
|
990
1211
|
for (const transport of options.transports) {
|
|
991
|
-
const off = transport.on("auth-error", () => {
|
|
992
|
-
|
|
1212
|
+
const off = transport.on("auth-error", (event) => {
|
|
1213
|
+
if (event.access && isCurrentAccess(event.access)) rejectedAccess = event.access;
|
|
1214
|
+
const refresh = triggerOnAuthError();
|
|
1215
|
+
event.holdFor?.(refresh);
|
|
993
1216
|
});
|
|
994
1217
|
cleanups.push(off);
|
|
995
1218
|
}
|
|
@@ -997,6 +1220,7 @@ function attachTokenLifecycle(options) {
|
|
|
997
1220
|
function detach() {
|
|
998
1221
|
detached = true;
|
|
999
1222
|
cancelTimer();
|
|
1223
|
+
stopLockWait?.();
|
|
1000
1224
|
for (const fn of cleanups) fn();
|
|
1001
1225
|
}
|
|
1002
1226
|
function wake() {
|
|
@@ -1019,7 +1243,12 @@ function attachTokenLifecycle(options) {
|
|
|
1019
1243
|
return;
|
|
1020
1244
|
}
|
|
1021
1245
|
const remaining = exp - now();
|
|
1022
|
-
if (remaining < graceMs) {
|
|
1246
|
+
if (remaining < graceMs && insideRefreshFloor()) options.onWakeChecked?.({
|
|
1247
|
+
decision: "throttled",
|
|
1248
|
+
remainingMs: remaining,
|
|
1249
|
+
phase: phase.kind
|
|
1250
|
+
});
|
|
1251
|
+
else if (remaining < graceMs) {
|
|
1023
1252
|
options.onWakeChecked?.({
|
|
1024
1253
|
decision: "refresh-now",
|
|
1025
1254
|
remainingMs: remaining,
|
|
@@ -1037,6 +1266,24 @@ function attachTokenLifecycle(options) {
|
|
|
1037
1266
|
wake
|
|
1038
1267
|
};
|
|
1039
1268
|
}
|
|
1269
|
+
function localAccessExpiry(access, serverExpiresAt, receivedAt = Date.now()) {
|
|
1270
|
+
const claims = decodeJwtClaims(access);
|
|
1271
|
+
const iat = claims?.iat;
|
|
1272
|
+
const exp = claims?.exp;
|
|
1273
|
+
if (typeof iat === "number" && typeof exp === "number" && exp > iat) return receivedAt + (exp - iat) * 1e3;
|
|
1274
|
+
return serverExpiresAt;
|
|
1275
|
+
}
|
|
1276
|
+
function decodeJwtClaims(token) {
|
|
1277
|
+
const payload = token.split(".")[1];
|
|
1278
|
+
if (!payload) return null;
|
|
1279
|
+
try {
|
|
1280
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
1281
|
+
const claims = JSON.parse(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "=")));
|
|
1282
|
+
return typeof claims === "object" && claims !== null ? claims : null;
|
|
1283
|
+
} catch {
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1040
1287
|
function stringifyError(err) {
|
|
1041
1288
|
if (err instanceof Error) return err.message;
|
|
1042
1289
|
if (typeof err === "string") return err;
|
|
@@ -1065,10 +1312,9 @@ function attachTransportSync(options) {
|
|
|
1065
1312
|
applied.set(transport, target);
|
|
1066
1313
|
transport.apply(target).catch((err) => {
|
|
1067
1314
|
options.onError?.(transport, target, err);
|
|
1068
|
-
if (applied.get(transport)
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1315
|
+
if (applied.get(transport) !== target || refusedByServer(err)) return;
|
|
1316
|
+
applied.delete(transport);
|
|
1317
|
+
scheduleRetry(transport);
|
|
1072
1318
|
});
|
|
1073
1319
|
}
|
|
1074
1320
|
function scheduleRetry(transport) {
|
|
@@ -1134,17 +1380,22 @@ function attachWorkerBridge(options) {
|
|
|
1134
1380
|
if (detached) return;
|
|
1135
1381
|
const msg = makeSync(phase);
|
|
1136
1382
|
let count = 0;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1383
|
+
const present = /* @__PURE__ */ new Set();
|
|
1384
|
+
for (const host of options.hosts()) {
|
|
1385
|
+
present.add(host);
|
|
1386
|
+
try {
|
|
1387
|
+
host.postMessage(msg);
|
|
1388
|
+
count++;
|
|
1389
|
+
maybeAttachHostListener(host);
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
log.warn("broadcast postMessage failed", {
|
|
1392
|
+
gen: msg.generation,
|
|
1393
|
+
phase: phase.kind,
|
|
1394
|
+
err
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1147
1397
|
}
|
|
1398
|
+
forgetDeparted(present);
|
|
1148
1399
|
options.onBroadcast?.(generation, count);
|
|
1149
1400
|
}
|
|
1150
1401
|
function syncOne(host) {
|
|
@@ -1207,6 +1458,13 @@ function attachWorkerBridge(options) {
|
|
|
1207
1458
|
host.removeEventListener?.("message", listener);
|
|
1208
1459
|
});
|
|
1209
1460
|
}
|
|
1461
|
+
function forgetHost(host) {
|
|
1462
|
+
hostListenerCleanups.get(host)?.();
|
|
1463
|
+
hostListenerCleanups.delete(host);
|
|
1464
|
+
}
|
|
1465
|
+
function forgetDeparted(present) {
|
|
1466
|
+
for (const host of hostListenerCleanups.keys()) if (!present.has(host)) forgetHost(host);
|
|
1467
|
+
}
|
|
1210
1468
|
const unsubKernel = options.kernel.subscribe((next) => {
|
|
1211
1469
|
broadcast(next);
|
|
1212
1470
|
});
|
|
@@ -1225,13 +1483,19 @@ function attachWorkerBridge(options) {
|
|
|
1225
1483
|
},
|
|
1226
1484
|
revalidateWorkers() {
|
|
1227
1485
|
if (detached) return;
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1486
|
+
const present = /* @__PURE__ */ new Set();
|
|
1487
|
+
for (const host of options.hosts()) {
|
|
1488
|
+
present.add(host);
|
|
1489
|
+
try {
|
|
1490
|
+
host.postMessage({ type: "kernel-revalidate" });
|
|
1491
|
+
maybeAttachHostListener(host);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
log.warn("revalidate postMessage failed", err);
|
|
1494
|
+
}
|
|
1233
1495
|
}
|
|
1234
|
-
|
|
1496
|
+
forgetDeparted(present);
|
|
1497
|
+
},
|
|
1498
|
+
forgetHost
|
|
1235
1499
|
};
|
|
1236
1500
|
}
|
|
1237
1501
|
//#endregion
|
|
@@ -1309,18 +1573,22 @@ function createConnectionJournal(options = {}) {
|
|
|
1309
1573
|
//#endregion
|
|
1310
1574
|
//#region src/signal.ts
|
|
1311
1575
|
var DEFAULT_DEBOUNCE_MS = 1500;
|
|
1576
|
+
var DEFAULT_FIRST_CONNECT_TIMEOUT_MS = 15e3;
|
|
1312
1577
|
function signalEquals(a, b) {
|
|
1313
1578
|
if (a.kind !== b.kind) return false;
|
|
1314
|
-
if (a.kind === "degraded" && b.kind === "degraded") return a.channels.join(",") === b.channels.join(",");
|
|
1579
|
+
if (a.kind === "degraded" && b.kind === "degraded") return a.reason === b.reason && a.channels.join(",") === b.channels.join(",");
|
|
1315
1580
|
if (a.kind === "offline" && b.kind === "offline") return a.retryAt === b.retryAt;
|
|
1316
1581
|
return true;
|
|
1317
1582
|
}
|
|
1318
1583
|
function createConnectionSignal(options) {
|
|
1319
1584
|
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1585
|
+
const firstConnectTimeoutMs = options.firstConnectTimeoutMs ?? DEFAULT_FIRST_CONNECT_TIMEOUT_MS;
|
|
1320
1586
|
const listeners = /* @__PURE__ */ new Set();
|
|
1321
1587
|
const cleanups = [];
|
|
1322
1588
|
let hasBeenOnline = false;
|
|
1323
1589
|
let anyChannelEverUp = false;
|
|
1590
|
+
let firstConnectOverdue = false;
|
|
1591
|
+
let firstConnectTimer = null;
|
|
1324
1592
|
let pendingTimer = null;
|
|
1325
1593
|
let pendingSignal = null;
|
|
1326
1594
|
let disposed = false;
|
|
@@ -1337,14 +1605,38 @@ function createConnectionSignal(options) {
|
|
|
1337
1605
|
case "online": {
|
|
1338
1606
|
const channels = options.transports.filter((t) => t.spec.phaseGating && !t.health().up).map((t) => t.spec.id);
|
|
1339
1607
|
if (channels.length === 0) return { kind: "online" };
|
|
1340
|
-
|
|
1608
|
+
if (anyChannelEverUp) return {
|
|
1341
1609
|
kind: "degraded",
|
|
1342
1610
|
channels
|
|
1611
|
+
};
|
|
1612
|
+
return firstConnectOverdue ? {
|
|
1613
|
+
kind: "degraded",
|
|
1614
|
+
channels,
|
|
1615
|
+
reason: "first-connect"
|
|
1343
1616
|
} : { kind: "connecting" };
|
|
1344
1617
|
}
|
|
1345
1618
|
}
|
|
1346
1619
|
}
|
|
1347
1620
|
let current = compute();
|
|
1621
|
+
function clearFirstConnectTimer() {
|
|
1622
|
+
if (firstConnectTimer !== null) {
|
|
1623
|
+
clearTimeout(firstConnectTimer);
|
|
1624
|
+
firstConnectTimer = null;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
function watchFirstConnect() {
|
|
1628
|
+
if (options.kernel.phase.kind !== "online" || anyChannelEverUp) {
|
|
1629
|
+
clearFirstConnectTimer();
|
|
1630
|
+
firstConnectOverdue = false;
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (firstConnectTimer !== null || firstConnectOverdue || !(firstConnectTimeoutMs > 0 && Number.isFinite(firstConnectTimeoutMs))) return;
|
|
1634
|
+
firstConnectTimer = setTimeout(() => {
|
|
1635
|
+
firstConnectTimer = null;
|
|
1636
|
+
firstConnectOverdue = true;
|
|
1637
|
+
apply(compute());
|
|
1638
|
+
}, firstConnectTimeoutMs);
|
|
1639
|
+
}
|
|
1348
1640
|
function clearPending() {
|
|
1349
1641
|
if (pendingTimer !== null) {
|
|
1350
1642
|
clearTimeout(pendingTimer);
|
|
@@ -1390,15 +1682,18 @@ function createConnectionSignal(options) {
|
|
|
1390
1682
|
hasBeenOnline = false;
|
|
1391
1683
|
anyChannelEverUp = false;
|
|
1392
1684
|
}
|
|
1685
|
+
watchFirstConnect();
|
|
1393
1686
|
recompute();
|
|
1394
1687
|
}));
|
|
1395
1688
|
for (const transport of options.transports) {
|
|
1396
1689
|
cleanups.push(transport.on("up", () => {
|
|
1397
1690
|
anyChannelEverUp = true;
|
|
1691
|
+
watchFirstConnect();
|
|
1398
1692
|
recompute();
|
|
1399
1693
|
}));
|
|
1400
1694
|
cleanups.push(transport.on("down", () => recompute()));
|
|
1401
1695
|
}
|
|
1696
|
+
watchFirstConnect();
|
|
1402
1697
|
return {
|
|
1403
1698
|
get current() {
|
|
1404
1699
|
return current;
|
|
@@ -1412,10 +1707,11 @@ function createConnectionSignal(options) {
|
|
|
1412
1707
|
dispose() {
|
|
1413
1708
|
disposed = true;
|
|
1414
1709
|
clearPending();
|
|
1710
|
+
clearFirstConnectTimer();
|
|
1415
1711
|
for (const fn of cleanups) fn();
|
|
1416
1712
|
listeners.clear();
|
|
1417
1713
|
}
|
|
1418
1714
|
};
|
|
1419
1715
|
}
|
|
1420
1716
|
//#endregion
|
|
1421
|
-
export { TransportEmitter, attachBackoff, attachCrossTab, attachDegradedRecovery, attachNetworkChange, attachPersistence, attachPresence, attachProbeLoop, attachTokenLifecycle, attachTransportSync, attachWorkerBridge, classifyClose, createBackgroundProbe, createConnectionJournal, createConnectionSignal, createKernel, isEndpointChange, isProbeFailure, isSameTarget, localStorageAdapter, makeProbeFailure, memoryStorageAdapter, raceFirst, reducer };
|
|
1717
|
+
export { TransportEmitter, attachBackoff, attachCrossTab, attachDegradedRecovery, attachNetworkChange, attachPersistence, attachPresence, attachProbeLoop, attachTokenLifecycle, attachTransportSync, attachWorkerBridge, classifyClose, createBackgroundProbe, createConnectionJournal, createConnectionSignal, createKernel, isEndpointChange, isProbeFailure, isSameTarget, localAccessExpiry, localStorageAdapter, makeProbeFailure, memoryStorageAdapter, raceFirst, reducer };
|