@camera.ui/transport 0.0.21 → 0.0.22
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-ClWJVOpq.js +14 -0
- package/dist/{contract-d-0gfY8v.js → contract-dt1m8U-o.js} +1 -6
- package/dist/core/kernel.d.ts +1 -0
- package/dist/core/types.d.ts +5 -24
- package/dist/effects/backgroundProbe.d.ts +20 -0
- package/dist/effects/degradedRecovery.d.ts +14 -0
- package/dist/effects/probeLoop.d.ts +2 -0
- package/dist/effects/transportSync.d.ts +2 -0
- package/dist/index.d.ts +15 -10
- package/dist/index.js +522 -352
- package/dist/journal.d.ts +19 -0
- package/dist/race.d.ts +2 -0
- package/dist/signal.d.ts +30 -0
- package/dist/testing/fakeTransport.d.ts +3 -0
- package/dist/testing.js +15 -1
- package/dist/transports/closeCodes.d.ts +8 -0
- package/dist/transports/contract.d.ts +1 -1
- package/dist/transports/http.js +1 -1
- package/dist/transports/nats.d.ts +2 -1
- package/dist/transports/nats.js +46 -6
- package/dist/transports/socketio.d.ts +3 -1
- package/dist/transports/socketio.js +36 -26
- package/dist/transports/ws.js +6 -5
- package/dist/worker.js +0 -1
- package/package.json +3 -3
- package/dist/core/resolver.d.ts +0 -5
- package/dist/effects/reconnectWatchdog.d.ts +0 -10
- package/dist/effects/transportWatchdog.d.ts +0 -16
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as isEndpointChange, r as isSameTarget, t as TransportEmitter } from "./contract-dt1m8U-o.js";
|
|
2
|
+
import { t as classifyClose } from "./closeCodes-ClWJVOpq.js";
|
|
2
3
|
import { Logger } from "@camera.ui/logger";
|
|
3
4
|
//#region src/core/reducer.ts
|
|
4
|
-
var EMPTY_TRANSPORTS = /* @__PURE__ */ new Map();
|
|
5
5
|
var DEFAULT_BACKOFF_MS = 5e3;
|
|
6
6
|
function reducer(phase, action, ctx) {
|
|
7
7
|
switch (action.type) {
|
|
@@ -10,124 +10,36 @@ function reducer(phase, action, ctx) {
|
|
|
10
10
|
if (phase.kind !== "idle" && phase.kind !== "offline" && phase.kind !== "needs-auth") return phase;
|
|
11
11
|
return {
|
|
12
12
|
kind: "discovering",
|
|
13
|
-
instanceId: action.instanceId
|
|
14
|
-
attempt: 1
|
|
13
|
+
instanceId: action.instanceId
|
|
15
14
|
};
|
|
16
15
|
case "USER_RETRY":
|
|
17
|
-
if (phase.kind === "offline") return {
|
|
16
|
+
if (phase.kind === "offline" || phase.kind === "needs-auth") return {
|
|
18
17
|
kind: "discovering",
|
|
19
|
-
instanceId: phase.instanceId ?? ""
|
|
20
|
-
attempt: 1
|
|
21
|
-
};
|
|
22
|
-
if (phase.kind === "reconnecting") return {
|
|
23
|
-
kind: "discovering",
|
|
24
|
-
instanceId: phase.instanceId,
|
|
25
|
-
attempt: 1,
|
|
26
|
-
transports: phase.transports
|
|
27
|
-
};
|
|
28
|
-
if (phase.kind === "needs-auth") return {
|
|
29
|
-
kind: "discovering",
|
|
30
|
-
instanceId: phase.instanceId ?? "",
|
|
31
|
-
attempt: 1
|
|
32
|
-
};
|
|
33
|
-
if (phase.kind === "online") return {
|
|
34
|
-
kind: "discovering",
|
|
35
|
-
instanceId: phase.instanceId,
|
|
36
|
-
attempt: 1,
|
|
37
|
-
transports: phase.transports
|
|
18
|
+
instanceId: phase.instanceId ?? ""
|
|
38
19
|
};
|
|
39
20
|
return phase;
|
|
40
21
|
case "PROBE_SUCCEEDED":
|
|
41
|
-
if (phase.kind
|
|
42
|
-
|
|
43
|
-
instanceId: phase.instanceId,
|
|
44
|
-
target: {
|
|
45
|
-
endpoint: action.endpoint,
|
|
46
|
-
tokens: action.tokens
|
|
47
|
-
},
|
|
48
|
-
transports: phase.transports ?? EMPTY_TRANSPORTS
|
|
49
|
-
};
|
|
50
|
-
if (phase.kind === "reconnecting") return {
|
|
22
|
+
if (phase.kind !== "discovering") return phase;
|
|
23
|
+
return {
|
|
51
24
|
kind: "online",
|
|
52
25
|
instanceId: phase.instanceId,
|
|
53
26
|
target: {
|
|
54
27
|
endpoint: action.endpoint,
|
|
55
28
|
tokens: action.tokens
|
|
56
|
-
}
|
|
57
|
-
transports: phase.transports
|
|
29
|
+
}
|
|
58
30
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (phase.kind !== "discovering" && phase.kind !== "reconnecting") return phase;
|
|
31
|
+
case "PROBE_FAILED_ALL":
|
|
32
|
+
if (phase.kind !== "discovering" && phase.kind !== "online") return phase;
|
|
62
33
|
if (action.error === "needs-auth") return {
|
|
63
34
|
kind: "needs-auth",
|
|
64
35
|
instanceId: phase.instanceId,
|
|
65
36
|
reason: action.error
|
|
66
37
|
};
|
|
67
|
-
const attempt = phase.kind === "discovering" ? phase.attempt : 1;
|
|
68
|
-
const backoff = ctx.retryBackoffMs?.(attempt) ?? DEFAULT_BACKOFF_MS;
|
|
69
38
|
return {
|
|
70
39
|
kind: "offline",
|
|
71
40
|
instanceId: phase.instanceId,
|
|
72
41
|
lastError: action.error,
|
|
73
|
-
nextRetryAt: ctx.now() +
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
case "TRANSPORT_UP":
|
|
77
|
-
if (phase.kind === "online") return {
|
|
78
|
-
...phase,
|
|
79
|
-
transports: setStatus(phase.transports, action.id, { up: true })
|
|
80
|
-
};
|
|
81
|
-
if (phase.kind === "reconnecting") {
|
|
82
|
-
const next = setStatus(phase.transports, action.id, { up: true });
|
|
83
|
-
if (phase.lastTarget && allPhaseGatingUp(next, ctx)) return {
|
|
84
|
-
kind: "online",
|
|
85
|
-
instanceId: phase.instanceId,
|
|
86
|
-
target: phase.lastTarget,
|
|
87
|
-
transports: next
|
|
88
|
-
};
|
|
89
|
-
return {
|
|
90
|
-
...phase,
|
|
91
|
-
transports: next
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
if (phase.kind === "discovering") return {
|
|
95
|
-
...phase,
|
|
96
|
-
transports: setStatus(phase.transports ?? EMPTY_TRANSPORTS, action.id, { up: true })
|
|
97
|
-
};
|
|
98
|
-
return phase;
|
|
99
|
-
case "TRANSPORT_DOWN":
|
|
100
|
-
if (phase.kind === "online" || phase.kind === "reconnecting") return {
|
|
101
|
-
...phase,
|
|
102
|
-
transports: setStatus(phase.transports, action.id, {
|
|
103
|
-
up: false,
|
|
104
|
-
lastError: action.reason,
|
|
105
|
-
downSince: ctx.now()
|
|
106
|
-
})
|
|
107
|
-
};
|
|
108
|
-
if (phase.kind === "discovering") return {
|
|
109
|
-
...phase,
|
|
110
|
-
transports: setStatus(phase.transports ?? EMPTY_TRANSPORTS, action.id, {
|
|
111
|
-
up: false,
|
|
112
|
-
lastError: action.reason,
|
|
113
|
-
downSince: ctx.now()
|
|
114
|
-
})
|
|
115
|
-
};
|
|
116
|
-
return phase;
|
|
117
|
-
case "TRANSPORT_DOWN_CONFIRMED":
|
|
118
|
-
if (phase.kind !== "online") return phase;
|
|
119
|
-
if (!ctx.specs.get(action.id)?.phaseGating) return phase;
|
|
120
|
-
return {
|
|
121
|
-
kind: "reconnecting",
|
|
122
|
-
instanceId: phase.instanceId,
|
|
123
|
-
lastTarget: phase.target,
|
|
124
|
-
cause: "transport-down",
|
|
125
|
-
since: ctx.now(),
|
|
126
|
-
transports: setStatus(phase.transports, action.id, {
|
|
127
|
-
up: false,
|
|
128
|
-
lastError: "down-confirmed",
|
|
129
|
-
downSince: ctx.now()
|
|
130
|
-
})
|
|
42
|
+
nextRetryAt: ctx.now() + DEFAULT_BACKOFF_MS
|
|
131
43
|
};
|
|
132
44
|
case "TOKENS_REFRESHED":
|
|
133
45
|
if (phase.kind === "online") {
|
|
@@ -140,33 +52,46 @@ function reducer(phase, action, ctx) {
|
|
|
140
52
|
}
|
|
141
53
|
};
|
|
142
54
|
}
|
|
143
|
-
if (phase.kind === "
|
|
144
|
-
if (tokensEqual(phase.
|
|
55
|
+
if (phase.kind === "discovering") {
|
|
56
|
+
if (phase.pendingTokens && tokensEqual(phase.pendingTokens, action.tokens)) return phase;
|
|
145
57
|
return {
|
|
146
58
|
...phase,
|
|
147
|
-
|
|
148
|
-
...phase.lastTarget,
|
|
149
|
-
tokens: action.tokens
|
|
150
|
-
}
|
|
59
|
+
pendingTokens: action.tokens
|
|
151
60
|
};
|
|
152
61
|
}
|
|
153
62
|
return phase;
|
|
154
63
|
case "TOKENS_INVALID":
|
|
155
|
-
if (phase.kind !== "online"
|
|
156
|
-
if (action.transient === true) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
nextRetryAt: ctx.now() + backoff
|
|
163
|
-
};
|
|
164
|
-
}
|
|
64
|
+
if (phase.kind !== "online") return phase;
|
|
65
|
+
if (action.transient === true) return {
|
|
66
|
+
kind: "offline",
|
|
67
|
+
instanceId: phase.instanceId,
|
|
68
|
+
lastError: `tokens invalid: ${action.reason}`,
|
|
69
|
+
nextRetryAt: ctx.now() + DEFAULT_BACKOFF_MS
|
|
70
|
+
};
|
|
165
71
|
return {
|
|
166
72
|
kind: "needs-auth",
|
|
167
73
|
instanceId: phase.instanceId,
|
|
168
74
|
reason: `tokens invalid: ${action.reason}`
|
|
169
75
|
};
|
|
76
|
+
case "ENDPOINT_SWAP":
|
|
77
|
+
if (phase.kind !== "online") return phase;
|
|
78
|
+
if (phase.target.endpoint.url === action.endpoint.url && phase.target.endpoint.mode === action.endpoint.mode) {
|
|
79
|
+
if (tokensEqual(phase.target.tokens, action.tokens)) return phase;
|
|
80
|
+
return {
|
|
81
|
+
...phase,
|
|
82
|
+
target: {
|
|
83
|
+
...phase.target,
|
|
84
|
+
tokens: action.tokens
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...phase,
|
|
90
|
+
target: {
|
|
91
|
+
endpoint: action.endpoint,
|
|
92
|
+
tokens: action.tokens
|
|
93
|
+
}
|
|
94
|
+
};
|
|
170
95
|
case "BACKOFF_HINT": {
|
|
171
96
|
if (phase.kind !== "offline") return phase;
|
|
172
97
|
const now = ctx.now();
|
|
@@ -184,18 +109,6 @@ function reducer(phase, action, ctx) {
|
|
|
184
109
|
}
|
|
185
110
|
}
|
|
186
111
|
}
|
|
187
|
-
function setStatus(map, id, status) {
|
|
188
|
-
const next = new Map(map);
|
|
189
|
-
next.set(id, status);
|
|
190
|
-
return next;
|
|
191
|
-
}
|
|
192
|
-
function allPhaseGatingUp(map, ctx) {
|
|
193
|
-
for (const [id, spec] of ctx.specs) {
|
|
194
|
-
if (!spec.phaseGating) continue;
|
|
195
|
-
if (!map.get(id)?.up) return false;
|
|
196
|
-
}
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
112
|
function tokensEqual(a, b) {
|
|
200
113
|
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;
|
|
201
114
|
}
|
|
@@ -220,6 +133,11 @@ function createKernel(options) {
|
|
|
220
133
|
while (next) {
|
|
221
134
|
const prev = current;
|
|
222
135
|
const after = reducer(prev, next, options.context);
|
|
136
|
+
if (options.onAction) try {
|
|
137
|
+
options.onAction(next, prev, after);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
log$1.warn("onAction threw on", next.type, err);
|
|
140
|
+
}
|
|
223
141
|
if (after !== prev) {
|
|
224
142
|
current = after;
|
|
225
143
|
for (const listener of [...listeners]) try {
|
|
@@ -255,15 +173,190 @@ function createKernel(options) {
|
|
|
255
173
|
};
|
|
256
174
|
}
|
|
257
175
|
//#endregion
|
|
258
|
-
//#region src/
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
176
|
+
//#region src/race.ts
|
|
177
|
+
var DEFAULT_RACE_TIMEOUT_BY_MODE = {
|
|
178
|
+
"direct-lan": 2e3,
|
|
179
|
+
"direct-wan": 5e3
|
|
180
|
+
};
|
|
181
|
+
var DEFAULT_PREFER_GRACE_MS = 400;
|
|
182
|
+
var RaceFirstError = class extends Error {
|
|
183
|
+
endpoint;
|
|
184
|
+
cause;
|
|
185
|
+
kind;
|
|
186
|
+
constructor(message, endpoint, cause, kind) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = "RaceFirstError";
|
|
189
|
+
this.endpoint = endpoint;
|
|
190
|
+
this.cause = cause;
|
|
191
|
+
this.kind = kind;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
function raceFirst(candidates, options = {}) {
|
|
195
|
+
const { timeoutByMode = (mode) => DEFAULT_RACE_TIMEOUT_BY_MODE[mode] ?? 5e3, shortCircuit, parentSignal, prefer } = options;
|
|
196
|
+
const preferGraceMs = options.preferGraceMs ?? DEFAULT_PREFER_GRACE_MS;
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
if (candidates.length === 0) {
|
|
199
|
+
reject(new RaceFirstError("raceFirst: no candidates", {
|
|
200
|
+
url: "",
|
|
201
|
+
mode: "direct-lan"
|
|
202
|
+
}, void 0, "all-failed"));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (parentSignal?.aborted) {
|
|
206
|
+
reject(new RaceFirstError("raceFirst: parent aborted", candidates[0].endpoint, void 0, "aborted"));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let settled = false;
|
|
210
|
+
let remaining = candidates.length;
|
|
211
|
+
let pendingPreferred = prefer ? candidates.filter((c) => prefer(c.endpoint)).length : 0;
|
|
212
|
+
let held = null;
|
|
213
|
+
const lastErrors = /* @__PURE__ */ new Map();
|
|
214
|
+
const abortControllers = [];
|
|
215
|
+
const timers = [];
|
|
216
|
+
function cleanupAllExcept(except) {
|
|
217
|
+
for (const t of timers) clearTimeout(t);
|
|
218
|
+
for (const a of abortControllers) if (a !== except) a.abort();
|
|
219
|
+
}
|
|
220
|
+
function finishSuccess(endpoint, value, except) {
|
|
221
|
+
if (settled) return;
|
|
222
|
+
settled = true;
|
|
223
|
+
cleanupAllExcept(except);
|
|
224
|
+
if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort);
|
|
225
|
+
resolve({
|
|
226
|
+
endpoint,
|
|
227
|
+
value
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function settleHeld() {
|
|
231
|
+
if (!held) return;
|
|
232
|
+
finishSuccess(held.endpoint, held.value, held.ctrl);
|
|
233
|
+
}
|
|
234
|
+
function handleSuccess(endpoint, value, ctrl) {
|
|
235
|
+
if (settled) return;
|
|
236
|
+
if (!prefer || prefer(endpoint) || pendingPreferred <= 0) {
|
|
237
|
+
finishSuccess(endpoint, value, ctrl);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (held) return;
|
|
241
|
+
held = {
|
|
242
|
+
endpoint,
|
|
243
|
+
value,
|
|
244
|
+
ctrl
|
|
245
|
+
};
|
|
246
|
+
const holdTimer = setTimeout(settleHeld, preferGraceMs);
|
|
247
|
+
timers.push(holdTimer);
|
|
248
|
+
}
|
|
249
|
+
function finishFail(error) {
|
|
250
|
+
if (settled) return;
|
|
251
|
+
settled = true;
|
|
252
|
+
cleanupAllExcept(null);
|
|
253
|
+
if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort);
|
|
254
|
+
reject(error);
|
|
255
|
+
}
|
|
256
|
+
const onParentAbort = parentSignal ? () => finishFail(new RaceFirstError("raceFirst: parent aborted", candidates[0].endpoint, void 0, "aborted")) : null;
|
|
257
|
+
if (onParentAbort) parentSignal.addEventListener("abort", onParentAbort);
|
|
258
|
+
candidates.forEach((cand) => {
|
|
259
|
+
const ctrl = new AbortController();
|
|
260
|
+
abortControllers.push(ctrl);
|
|
261
|
+
const delay = timeoutByMode(cand.endpoint.mode);
|
|
262
|
+
const timer = setTimeout(() => ctrl.abort(), delay);
|
|
263
|
+
timers.push(timer);
|
|
264
|
+
cand.run(ctrl.signal).then((value) => handleSuccess(cand.endpoint, value, ctrl), (err) => {
|
|
265
|
+
if (settled) {
|
|
266
|
+
lastErrors.set(cand.endpoint, err);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (prefer?.(cand.endpoint)) {
|
|
270
|
+
pendingPreferred--;
|
|
271
|
+
if (held && pendingPreferred <= 0) {
|
|
272
|
+
lastErrors.set(cand.endpoint, err);
|
|
273
|
+
settleHeld();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const finalErr = ctrl.signal.aborted && !parentSignal?.aborted ? new RaceFirstError(`timeout (${delay}ms)`, cand.endpoint, err, "all-failed") : err;
|
|
278
|
+
lastErrors.set(cand.endpoint, finalErr);
|
|
279
|
+
if (shortCircuit?.(finalErr)) {
|
|
280
|
+
finishFail(new RaceFirstError("raceFirst: short-circuit", cand.endpoint, finalErr, "short-circuit"));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
remaining--;
|
|
284
|
+
if (remaining <= 0) {
|
|
285
|
+
const [endpoint, cause] = [...lastErrors.entries()].find(([, e]) => {
|
|
286
|
+
if (e instanceof RaceFirstError) return e.kind !== "all-failed" || !(e.cause instanceof Error && e.cause.message === "aborted");
|
|
287
|
+
return true;
|
|
288
|
+
}) ?? [cand.endpoint, finalErr];
|
|
289
|
+
finishFail(new RaceFirstError("raceFirst: all candidates failed", endpoint, cause, "all-failed"));
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
264
294
|
}
|
|
265
|
-
|
|
266
|
-
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/effects/backgroundProbe.ts
|
|
297
|
+
function createBackgroundProbe(options) {
|
|
298
|
+
let inflight = null;
|
|
299
|
+
let disposed = false;
|
|
300
|
+
async function round() {
|
|
301
|
+
if (options.kernel.phase.kind !== "online") {
|
|
302
|
+
options.onResult?.("skipped", `phase=${options.kernel.phase.kind}`);
|
|
303
|
+
return "skipped";
|
|
304
|
+
}
|
|
305
|
+
const ctrl = new AbortController();
|
|
306
|
+
try {
|
|
307
|
+
const pool = await options.discover(ctrl.signal);
|
|
308
|
+
if (disposed || options.kernel.phase.kind !== "online") return "skipped";
|
|
309
|
+
if (pool.length === 0) {
|
|
310
|
+
options.onResult?.("failed", "empty pool");
|
|
311
|
+
return "failed";
|
|
312
|
+
}
|
|
313
|
+
const lastTokens = options.lastTarget?.()?.tokens;
|
|
314
|
+
const { endpoint, value: tokens } = await raceFirst(pool.map((endpoint) => ({
|
|
315
|
+
endpoint,
|
|
316
|
+
run: (signal) => options.probe({
|
|
317
|
+
endpoint,
|
|
318
|
+
lastTokens,
|
|
319
|
+
signal
|
|
320
|
+
})
|
|
321
|
+
})), {
|
|
322
|
+
timeoutByMode: options.timeoutByMode,
|
|
323
|
+
parentSignal: ctrl.signal,
|
|
324
|
+
prefer: options.prefer,
|
|
325
|
+
preferGraceMs: options.preferGraceMs
|
|
326
|
+
});
|
|
327
|
+
if (disposed) return "skipped";
|
|
328
|
+
const phase = options.kernel.phase;
|
|
329
|
+
if (phase.kind !== "online") {
|
|
330
|
+
options.onResult?.("skipped", `phase=${phase.kind}`);
|
|
331
|
+
return "skipped";
|
|
332
|
+
}
|
|
333
|
+
const same = phase.target.endpoint.url === endpoint.url && phase.target.endpoint.mode === endpoint.mode;
|
|
334
|
+
options.kernel.dispatch({
|
|
335
|
+
type: "ENDPOINT_SWAP",
|
|
336
|
+
endpoint,
|
|
337
|
+
tokens
|
|
338
|
+
});
|
|
339
|
+
const outcome = same ? "same" : "swap";
|
|
340
|
+
options.onResult?.(outcome, endpoint.url);
|
|
341
|
+
return outcome;
|
|
342
|
+
} catch (err) {
|
|
343
|
+
options.onResult?.("failed", err instanceof Error ? err.message : String(err));
|
|
344
|
+
return "failed";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
run() {
|
|
349
|
+
if (disposed) return Promise.resolve("skipped");
|
|
350
|
+
if (inflight) return inflight;
|
|
351
|
+
inflight = round().finally(() => {
|
|
352
|
+
inflight = null;
|
|
353
|
+
});
|
|
354
|
+
return inflight;
|
|
355
|
+
},
|
|
356
|
+
dispose() {
|
|
357
|
+
disposed = true;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
267
360
|
}
|
|
268
361
|
//#endregion
|
|
269
362
|
//#region src/effects/backoff.ts
|
|
@@ -338,8 +431,7 @@ function attachCrossTab(options) {
|
|
|
338
431
|
if (e.key !== key) return;
|
|
339
432
|
if (e.newValue === null) {
|
|
340
433
|
options.absorb?.(null);
|
|
341
|
-
|
|
342
|
-
if (k !== "online" && k !== "reconnecting") return;
|
|
434
|
+
if (options.kernel.phase.kind !== "online") return;
|
|
343
435
|
options.kernel.dispatch({ type: "RESET" });
|
|
344
436
|
options.onResetReceived?.();
|
|
345
437
|
return;
|
|
@@ -352,8 +444,7 @@ function attachCrossTab(options) {
|
|
|
352
444
|
return;
|
|
353
445
|
}
|
|
354
446
|
if (!parsed?.tokens?.access) return;
|
|
355
|
-
|
|
356
|
-
if (k !== "online" && k !== "reconnecting") {
|
|
447
|
+
if (options.kernel.phase.kind !== "online") {
|
|
357
448
|
if (parsed.endpoint?.url) {
|
|
358
449
|
options.absorb?.({
|
|
359
450
|
endpoint: parsed.endpoint,
|
|
@@ -375,6 +466,56 @@ function attachCrossTab(options) {
|
|
|
375
466
|
};
|
|
376
467
|
}
|
|
377
468
|
//#endregion
|
|
469
|
+
//#region src/effects/degradedRecovery.ts
|
|
470
|
+
var DEFAULT_GRACE_MS$1 = 1e4;
|
|
471
|
+
function attachDegradedRecovery(options) {
|
|
472
|
+
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS$1;
|
|
473
|
+
let timer = null;
|
|
474
|
+
let round = 0;
|
|
475
|
+
let detached = false;
|
|
476
|
+
function disarm() {
|
|
477
|
+
if (timer !== null) {
|
|
478
|
+
clearTimeout(timer);
|
|
479
|
+
timer = null;
|
|
480
|
+
}
|
|
481
|
+
round = 0;
|
|
482
|
+
}
|
|
483
|
+
function arm() {
|
|
484
|
+
if (detached || timer !== null) return;
|
|
485
|
+
timer = setTimeout(() => {
|
|
486
|
+
timer = null;
|
|
487
|
+
escalate();
|
|
488
|
+
}, graceMs);
|
|
489
|
+
}
|
|
490
|
+
async function escalate() {
|
|
491
|
+
if (detached || options.signal.current.kind !== "degraded") return;
|
|
492
|
+
round++;
|
|
493
|
+
options.onEscalate?.(round);
|
|
494
|
+
options.ensureAll();
|
|
495
|
+
const outcome = await options.probe();
|
|
496
|
+
if (detached || options.signal.current.kind !== "degraded") return;
|
|
497
|
+
if (outcome === "failed") {
|
|
498
|
+
options.onOffline?.("degraded: endpoint unreachable");
|
|
499
|
+
options.kernel.dispatch({
|
|
500
|
+
type: "PROBE_FAILED_ALL",
|
|
501
|
+
error: "degraded: endpoint unreachable"
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
arm();
|
|
506
|
+
}
|
|
507
|
+
const unsub = options.signal.subscribe((next) => {
|
|
508
|
+
if (next.kind === "degraded") arm();
|
|
509
|
+
else disarm();
|
|
510
|
+
});
|
|
511
|
+
if (options.signal.current.kind === "degraded") arm();
|
|
512
|
+
return () => {
|
|
513
|
+
detached = true;
|
|
514
|
+
disarm();
|
|
515
|
+
unsub();
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
378
519
|
//#region src/effects/networkChange.ts
|
|
379
520
|
function attachNetworkChange(options) {
|
|
380
521
|
let detached = false;
|
|
@@ -459,8 +600,15 @@ function attachPersistence(options) {
|
|
|
459
600
|
}
|
|
460
601
|
const unsub = options.kernel.subscribe((next, prev) => {
|
|
461
602
|
if (detached) return;
|
|
462
|
-
|
|
463
|
-
|
|
603
|
+
if (next.kind === "discovering" && next.pendingTokens && (prev.kind !== "discovering" || prev.pendingTokens !== next.pendingTokens)) {
|
|
604
|
+
if (cached) persist({
|
|
605
|
+
endpoint: cached.endpoint,
|
|
606
|
+
tokens: next.pendingTokens
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const nextTarget = next.kind === "online" ? next.target : null;
|
|
611
|
+
const prevTarget = prev.kind === "online" ? prev.target : null;
|
|
464
612
|
if (nextTarget && nextTarget !== prevTarget) {
|
|
465
613
|
persist(nextTarget);
|
|
466
614
|
return;
|
|
@@ -548,95 +696,6 @@ function attachPresence(options) {
|
|
|
548
696
|
};
|
|
549
697
|
}
|
|
550
698
|
//#endregion
|
|
551
|
-
//#region src/race.ts
|
|
552
|
-
var DEFAULT_RACE_TIMEOUT_BY_MODE = {
|
|
553
|
-
"direct-lan": 2e3,
|
|
554
|
-
"direct-wan": 5e3
|
|
555
|
-
};
|
|
556
|
-
var RaceFirstError = class extends Error {
|
|
557
|
-
endpoint;
|
|
558
|
-
cause;
|
|
559
|
-
kind;
|
|
560
|
-
constructor(message, endpoint, cause, kind) {
|
|
561
|
-
super(message);
|
|
562
|
-
this.name = "RaceFirstError";
|
|
563
|
-
this.endpoint = endpoint;
|
|
564
|
-
this.cause = cause;
|
|
565
|
-
this.kind = kind;
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
|
-
function raceFirst(candidates, options = {}) {
|
|
569
|
-
const { timeoutByMode = (mode) => DEFAULT_RACE_TIMEOUT_BY_MODE[mode] ?? 5e3, shortCircuit, parentSignal } = options;
|
|
570
|
-
return new Promise((resolve, reject) => {
|
|
571
|
-
if (candidates.length === 0) {
|
|
572
|
-
reject(new RaceFirstError("raceFirst: no candidates", {
|
|
573
|
-
url: "",
|
|
574
|
-
mode: "direct-lan"
|
|
575
|
-
}, void 0, "all-failed"));
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (parentSignal?.aborted) {
|
|
579
|
-
reject(new RaceFirstError("raceFirst: parent aborted", candidates[0].endpoint, void 0, "aborted"));
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
let settled = false;
|
|
583
|
-
let remaining = candidates.length;
|
|
584
|
-
const lastErrors = /* @__PURE__ */ new Map();
|
|
585
|
-
const abortControllers = [];
|
|
586
|
-
const timers = [];
|
|
587
|
-
function cleanupAllExcept(except) {
|
|
588
|
-
for (const t of timers) clearTimeout(t);
|
|
589
|
-
for (const a of abortControllers) if (a !== except) a.abort();
|
|
590
|
-
}
|
|
591
|
-
function finishSuccess(endpoint, value, except) {
|
|
592
|
-
if (settled) return;
|
|
593
|
-
settled = true;
|
|
594
|
-
cleanupAllExcept(except);
|
|
595
|
-
if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort);
|
|
596
|
-
resolve({
|
|
597
|
-
endpoint,
|
|
598
|
-
value
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
function finishFail(error) {
|
|
602
|
-
if (settled) return;
|
|
603
|
-
settled = true;
|
|
604
|
-
cleanupAllExcept(null);
|
|
605
|
-
if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort);
|
|
606
|
-
reject(error);
|
|
607
|
-
}
|
|
608
|
-
const onParentAbort = parentSignal ? () => finishFail(new RaceFirstError("raceFirst: parent aborted", candidates[0].endpoint, void 0, "aborted")) : null;
|
|
609
|
-
if (onParentAbort) parentSignal.addEventListener("abort", onParentAbort);
|
|
610
|
-
candidates.forEach((cand) => {
|
|
611
|
-
const ctrl = new AbortController();
|
|
612
|
-
abortControllers.push(ctrl);
|
|
613
|
-
const delay = timeoutByMode(cand.endpoint.mode);
|
|
614
|
-
const timer = setTimeout(() => ctrl.abort(), delay);
|
|
615
|
-
timers.push(timer);
|
|
616
|
-
cand.run(ctrl.signal).then((value) => finishSuccess(cand.endpoint, value, ctrl), (err) => {
|
|
617
|
-
if (settled) {
|
|
618
|
-
lastErrors.set(cand.endpoint, err);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
const finalErr = ctrl.signal.aborted && !parentSignal?.aborted ? new RaceFirstError(`timeout (${delay}ms)`, cand.endpoint, err, "all-failed") : err;
|
|
622
|
-
lastErrors.set(cand.endpoint, finalErr);
|
|
623
|
-
if (shortCircuit?.(finalErr)) {
|
|
624
|
-
finishFail(new RaceFirstError("raceFirst: short-circuit", cand.endpoint, finalErr, "short-circuit"));
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
remaining--;
|
|
628
|
-
if (remaining <= 0) {
|
|
629
|
-
const [endpoint, cause] = [...lastErrors.entries()].find(([, e]) => {
|
|
630
|
-
if (e instanceof RaceFirstError) return e.kind !== "all-failed" || !(e.cause instanceof Error && e.cause.message === "aborted");
|
|
631
|
-
return true;
|
|
632
|
-
}) ?? [cand.endpoint, finalErr];
|
|
633
|
-
finishFail(new RaceFirstError("raceFirst: all candidates failed", endpoint, cause, "all-failed"));
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
//#endregion
|
|
640
699
|
//#region src/effects/probeLoop.ts
|
|
641
700
|
function makeProbeFailure(kind, message) {
|
|
642
701
|
const err = new Error(message);
|
|
@@ -712,6 +771,8 @@ function attachProbeLoop(options) {
|
|
|
712
771
|
const { endpoint, value: tokens } = await raceFirst(candidates, {
|
|
713
772
|
timeoutByMode: options.timeoutByMode,
|
|
714
773
|
parentSignal: ctrl.signal,
|
|
774
|
+
prefer: options.prefer,
|
|
775
|
+
preferGraceMs: options.preferGraceMs,
|
|
715
776
|
shortCircuit: (err) => isProbeFailure(err) && (err.kind === "needs-auth" || err.kind === "fatal" || err.kind === "aborted")
|
|
716
777
|
});
|
|
717
778
|
if (ctrl.signal.aborted) return;
|
|
@@ -749,52 +810,13 @@ function attachProbeLoop(options) {
|
|
|
749
810
|
};
|
|
750
811
|
}
|
|
751
812
|
//#endregion
|
|
752
|
-
//#region src/effects/reconnectWatchdog.ts
|
|
753
|
-
var DEFAULT_ESCALATE_AFTER_MS = 12e3;
|
|
754
|
-
function attachReconnectWatchdog(options) {
|
|
755
|
-
const escalateAfterMs = options.escalateAfterMs ?? DEFAULT_ESCALATE_AFTER_MS;
|
|
756
|
-
const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
757
|
-
const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
|
|
758
|
-
let timer;
|
|
759
|
-
let attempt = 0;
|
|
760
|
-
let detached = false;
|
|
761
|
-
function cancelTimer() {
|
|
762
|
-
if (timer !== void 0) {
|
|
763
|
-
clearTimer(timer);
|
|
764
|
-
timer = void 0;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
function arm() {
|
|
768
|
-
cancelTimer();
|
|
769
|
-
timer = setTimer(() => {
|
|
770
|
-
timer = void 0;
|
|
771
|
-
if (detached) return;
|
|
772
|
-
if (options.kernel.phase.kind !== "reconnecting") return;
|
|
773
|
-
attempt += 1;
|
|
774
|
-
options.onEscalate?.(attempt);
|
|
775
|
-
options.kernel.dispatch({ type: "USER_RETRY" });
|
|
776
|
-
}, escalateAfterMs);
|
|
777
|
-
}
|
|
778
|
-
const unsubKernel = options.kernel.subscribe((next, prev) => {
|
|
779
|
-
if (next.kind === "reconnecting" && prev.kind !== "reconnecting") arm();
|
|
780
|
-
else if (next.kind !== "reconnecting" && prev.kind === "reconnecting") cancelTimer();
|
|
781
|
-
if (next.kind === "online" || next.kind === "idle") attempt = 0;
|
|
782
|
-
});
|
|
783
|
-
if (options.kernel.phase.kind === "reconnecting") arm();
|
|
784
|
-
return () => {
|
|
785
|
-
detached = true;
|
|
786
|
-
cancelTimer();
|
|
787
|
-
unsubKernel();
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
//#endregion
|
|
791
813
|
//#region src/effects/tokenLifecycle.ts
|
|
792
|
-
var DEFAULT_GRACE_MS
|
|
814
|
+
var DEFAULT_GRACE_MS = 5e3;
|
|
793
815
|
var DEFAULT_MAX_TRANSIENT_RETRIES = 3;
|
|
794
816
|
var DEFAULT_TRANSIENT_RETRY_DELAY_MS = 2e3;
|
|
795
817
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
796
818
|
function attachTokenLifecycle(options) {
|
|
797
|
-
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS
|
|
819
|
+
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
798
820
|
const isTransient = options.isTransientError ?? (() => false);
|
|
799
821
|
const maxTransientRetries = options.maxTransientRetries ?? DEFAULT_MAX_TRANSIENT_RETRIES;
|
|
800
822
|
const transientRetryDelayMs = options.transientRetryDelayMs ?? DEFAULT_TRANSIENT_RETRY_DELAY_MS;
|
|
@@ -835,7 +857,7 @@ function attachTokenLifecycle(options) {
|
|
|
835
857
|
return;
|
|
836
858
|
}
|
|
837
859
|
const phase = options.kernel.phase;
|
|
838
|
-
const target = phase.kind === "online" ? phase.target :
|
|
860
|
+
const target = phase.kind === "online" ? phase.target : null;
|
|
839
861
|
if (!target) {
|
|
840
862
|
options.onTriggerSkipped?.(reason, "no-target", phase.kind);
|
|
841
863
|
return;
|
|
@@ -856,7 +878,7 @@ function attachTokenLifecycle(options) {
|
|
|
856
878
|
};
|
|
857
879
|
}
|
|
858
880
|
const livePhase = options.kernel.phase;
|
|
859
|
-
const base = (livePhase.kind === "online" ? livePhase.target :
|
|
881
|
+
const base = (livePhase.kind === "online" ? livePhase.target : null) ?? target;
|
|
860
882
|
const refreshTarget = fresh ? {
|
|
861
883
|
...base,
|
|
862
884
|
tokens: fresh
|
|
@@ -925,7 +947,7 @@ function attachTokenLifecycle(options) {
|
|
|
925
947
|
transientRetries = 0;
|
|
926
948
|
schedule(next.target);
|
|
927
949
|
}
|
|
928
|
-
} else
|
|
950
|
+
} else {
|
|
929
951
|
cancelTimer();
|
|
930
952
|
transientRetries = 0;
|
|
931
953
|
}
|
|
@@ -946,7 +968,7 @@ function attachTokenLifecycle(options) {
|
|
|
946
968
|
function wake() {
|
|
947
969
|
if (detached) return;
|
|
948
970
|
const phase = options.kernel.phase;
|
|
949
|
-
const target = phase.kind === "online" ? phase.target :
|
|
971
|
+
const target = phase.kind === "online" ? phase.target : null;
|
|
950
972
|
if (!target) {
|
|
951
973
|
options.onWakeChecked?.({
|
|
952
974
|
decision: "no-target",
|
|
@@ -989,29 +1011,58 @@ function stringifyError(err) {
|
|
|
989
1011
|
//#endregion
|
|
990
1012
|
//#region src/effects/transportSync.ts
|
|
991
1013
|
var SKIP = Symbol("transport-sync-skip");
|
|
1014
|
+
var DEFAULT_RETRY_MS = 2e3;
|
|
992
1015
|
function syncTargetFor(phase) {
|
|
993
1016
|
switch (phase.kind) {
|
|
994
1017
|
case "idle":
|
|
995
1018
|
case "offline":
|
|
996
1019
|
case "needs-auth": return null;
|
|
997
1020
|
case "online": return phase.target;
|
|
998
|
-
case "reconnecting": return phase.lastTarget;
|
|
999
1021
|
case "discovering": return SKIP;
|
|
1000
1022
|
}
|
|
1001
1023
|
}
|
|
1002
1024
|
function attachTransportSync(options) {
|
|
1003
1025
|
let detached = false;
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
for (const transport of options.transports) transport.apply(target).catch((err) => {
|
|
1026
|
+
const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
1027
|
+
const applied = /* @__PURE__ */ new Map();
|
|
1028
|
+
const retryTimers = /* @__PURE__ */ new Map();
|
|
1029
|
+
let lastNotified;
|
|
1030
|
+
function applyOne(transport, target) {
|
|
1031
|
+
applied.set(transport, target);
|
|
1032
|
+
transport.apply(target).catch((err) => {
|
|
1012
1033
|
options.onError?.(transport, target, err);
|
|
1034
|
+
if (applied.get(transport) === target) {
|
|
1035
|
+
applied.delete(transport);
|
|
1036
|
+
scheduleRetry(transport);
|
|
1037
|
+
}
|
|
1013
1038
|
});
|
|
1014
|
-
|
|
1039
|
+
}
|
|
1040
|
+
function scheduleRetry(transport) {
|
|
1041
|
+
if (detached || retryTimers.has(transport)) return;
|
|
1042
|
+
const timer = setTimeout(() => {
|
|
1043
|
+
retryTimers.delete(transport);
|
|
1044
|
+
if (detached) return;
|
|
1045
|
+
const decision = syncTargetFor(options.kernel.phase);
|
|
1046
|
+
if (decision === SKIP) {
|
|
1047
|
+
scheduleRetry(transport);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (applied.has(transport) && isSameTarget(applied.get(transport), decision)) return;
|
|
1051
|
+
options.onRetry?.(transport, decision);
|
|
1052
|
+
applyOne(transport, decision);
|
|
1053
|
+
}, retryMs);
|
|
1054
|
+
retryTimers.set(transport, timer);
|
|
1055
|
+
}
|
|
1056
|
+
function applyAll(target) {
|
|
1057
|
+
if (detached) return;
|
|
1058
|
+
for (const transport of options.transports) {
|
|
1059
|
+
if (applied.has(transport) && isSameTarget(applied.get(transport), target)) continue;
|
|
1060
|
+
applyOne(transport, target);
|
|
1061
|
+
}
|
|
1062
|
+
if (lastNotified === void 0 || !isSameTarget(lastNotified, target)) {
|
|
1063
|
+
lastNotified = target;
|
|
1064
|
+
options.onApplied?.(target);
|
|
1065
|
+
}
|
|
1015
1066
|
}
|
|
1016
1067
|
function syncFromPhase(phase) {
|
|
1017
1068
|
const decision = syncTargetFor(phase);
|
|
@@ -1024,76 +1075,12 @@ function attachTransportSync(options) {
|
|
|
1024
1075
|
});
|
|
1025
1076
|
return () => {
|
|
1026
1077
|
detached = true;
|
|
1078
|
+
for (const timer of retryTimers.values()) clearTimeout(timer);
|
|
1079
|
+
retryTimers.clear();
|
|
1027
1080
|
unsub();
|
|
1028
1081
|
};
|
|
1029
1082
|
}
|
|
1030
1083
|
//#endregion
|
|
1031
|
-
//#region src/effects/transportWatchdog.ts
|
|
1032
|
-
var DEFAULT_GRACE_MS = 4e3;
|
|
1033
|
-
function attachTransportWatchdog(options) {
|
|
1034
|
-
const defaultGraceMs = options.defaultGraceMs ?? DEFAULT_GRACE_MS;
|
|
1035
|
-
const setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
1036
|
-
const clearTimer = options.clearTimer ?? ((h) => clearTimeout(h));
|
|
1037
|
-
const timers = /* @__PURE__ */ new Map();
|
|
1038
|
-
const cleanups = [];
|
|
1039
|
-
let detached = false;
|
|
1040
|
-
function cancelTimer(id, reason) {
|
|
1041
|
-
const handle = timers.get(id);
|
|
1042
|
-
if (handle !== void 0) {
|
|
1043
|
-
clearTimer(handle);
|
|
1044
|
-
timers.delete(id);
|
|
1045
|
-
options.onGraceCleared?.(id, reason);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
function cancelAll(reason) {
|
|
1049
|
-
for (const id of [...timers.keys()]) cancelTimer(id, reason);
|
|
1050
|
-
}
|
|
1051
|
-
for (const transport of options.transports) {
|
|
1052
|
-
const spec = transport.spec;
|
|
1053
|
-
const offUp = transport.on("up", () => {
|
|
1054
|
-
if (detached) return;
|
|
1055
|
-
cancelTimer(spec.id, "up");
|
|
1056
|
-
options.kernel.dispatch({
|
|
1057
|
-
type: "TRANSPORT_UP",
|
|
1058
|
-
id: spec.id
|
|
1059
|
-
});
|
|
1060
|
-
});
|
|
1061
|
-
const offDown = transport.on("down", (payload) => {
|
|
1062
|
-
if (detached) return;
|
|
1063
|
-
options.kernel.dispatch({
|
|
1064
|
-
type: "TRANSPORT_DOWN",
|
|
1065
|
-
id: spec.id,
|
|
1066
|
-
reason: payload.reason
|
|
1067
|
-
});
|
|
1068
|
-
if (!spec.phaseGating) return;
|
|
1069
|
-
if (options.kernel.phase.kind !== "online") return;
|
|
1070
|
-
if (timers.has(spec.id)) return;
|
|
1071
|
-
const graceMs = spec.graceMs ?? defaultGraceMs;
|
|
1072
|
-
options.onGraceStarted?.(spec.id, graceMs);
|
|
1073
|
-
const handle = setTimer(() => {
|
|
1074
|
-
timers.delete(spec.id);
|
|
1075
|
-
if (detached) return;
|
|
1076
|
-
options.onConfirmed?.(spec.id);
|
|
1077
|
-
options.kernel.dispatch({
|
|
1078
|
-
type: "TRANSPORT_DOWN_CONFIRMED",
|
|
1079
|
-
id: spec.id
|
|
1080
|
-
});
|
|
1081
|
-
}, graceMs);
|
|
1082
|
-
timers.set(spec.id, handle);
|
|
1083
|
-
});
|
|
1084
|
-
cleanups.push(offUp, offDown);
|
|
1085
|
-
}
|
|
1086
|
-
const unsubKernel = options.kernel.subscribe((next, prev) => {
|
|
1087
|
-
if (prev.kind === "online" && next.kind !== "online") cancelAll("phase-change");
|
|
1088
|
-
});
|
|
1089
|
-
cleanups.push(unsubKernel);
|
|
1090
|
-
return () => {
|
|
1091
|
-
detached = true;
|
|
1092
|
-
cancelAll("detach");
|
|
1093
|
-
for (const fn of cleanups) fn();
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
//#endregion
|
|
1097
1084
|
//#region src/effects/workerBridge.ts
|
|
1098
1085
|
var log = new Logger("workerBridge");
|
|
1099
1086
|
function attachWorkerBridge(options) {
|
|
@@ -1180,4 +1167,187 @@ function attachWorkerBridge(options) {
|
|
|
1180
1167
|
};
|
|
1181
1168
|
}
|
|
1182
1169
|
//#endregion
|
|
1183
|
-
|
|
1170
|
+
//#region src/journal.ts
|
|
1171
|
+
var DEFAULT_CAPACITY = 1e3;
|
|
1172
|
+
var MAX_DETAIL_LENGTH = 300;
|
|
1173
|
+
function stringifyDetail(detail) {
|
|
1174
|
+
if (detail === void 0 || detail === null) return void 0;
|
|
1175
|
+
let text;
|
|
1176
|
+
if (typeof detail === "string") text = detail;
|
|
1177
|
+
else if (detail instanceof Error) text = detail.message;
|
|
1178
|
+
else try {
|
|
1179
|
+
text = JSON.stringify(detail);
|
|
1180
|
+
} catch {
|
|
1181
|
+
text = String(detail);
|
|
1182
|
+
}
|
|
1183
|
+
return text.length > MAX_DETAIL_LENGTH ? `${text.slice(0, MAX_DETAIL_LENGTH)}…` : text;
|
|
1184
|
+
}
|
|
1185
|
+
function formatTime(t) {
|
|
1186
|
+
const d = new Date(t);
|
|
1187
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}.${String(d.getMilliseconds()).padStart(3, "0")}`;
|
|
1188
|
+
}
|
|
1189
|
+
function createConnectionJournal(options = {}) {
|
|
1190
|
+
const capacity = options.capacity ?? DEFAULT_CAPACITY;
|
|
1191
|
+
const now = options.now ?? (() => Date.now());
|
|
1192
|
+
const buffer = [];
|
|
1193
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1194
|
+
let head = 0;
|
|
1195
|
+
let seq = 0;
|
|
1196
|
+
function record(scope, msg, detail) {
|
|
1197
|
+
const entry = {
|
|
1198
|
+
seq: seq++,
|
|
1199
|
+
t: now(),
|
|
1200
|
+
scope,
|
|
1201
|
+
msg,
|
|
1202
|
+
detail: stringifyDetail(detail)
|
|
1203
|
+
};
|
|
1204
|
+
if (buffer.length < capacity) buffer.push(entry);
|
|
1205
|
+
else {
|
|
1206
|
+
buffer[head] = entry;
|
|
1207
|
+
head = (head + 1) % capacity;
|
|
1208
|
+
}
|
|
1209
|
+
for (const listener of [...listeners]) try {
|
|
1210
|
+
listener(entry);
|
|
1211
|
+
} catch {}
|
|
1212
|
+
}
|
|
1213
|
+
function list() {
|
|
1214
|
+
return [...buffer.slice(head), ...buffer.slice(0, head)];
|
|
1215
|
+
}
|
|
1216
|
+
function clear() {
|
|
1217
|
+
buffer.length = 0;
|
|
1218
|
+
head = 0;
|
|
1219
|
+
}
|
|
1220
|
+
function subscribe(listener) {
|
|
1221
|
+
listeners.add(listener);
|
|
1222
|
+
return () => {
|
|
1223
|
+
listeners.delete(listener);
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function exportText() {
|
|
1227
|
+
const entries = list();
|
|
1228
|
+
return [`connection journal — exported ${new Date(now()).toISOString()} (${entries.length} entries)`, ...entries.map((e) => {
|
|
1229
|
+
const detail = e.detail ? ` — ${e.detail}` : "";
|
|
1230
|
+
return `${formatTime(e.t)} [${e.scope}] ${e.msg}${detail}`;
|
|
1231
|
+
})].join("\n");
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
record,
|
|
1235
|
+
list,
|
|
1236
|
+
clear,
|
|
1237
|
+
subscribe,
|
|
1238
|
+
exportText
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/signal.ts
|
|
1243
|
+
var DEFAULT_DEBOUNCE_MS = 1500;
|
|
1244
|
+
function signalEquals(a, b) {
|
|
1245
|
+
if (a.kind !== b.kind) return false;
|
|
1246
|
+
if (a.kind === "degraded" && b.kind === "degraded") return a.channels.join(",") === b.channels.join(",");
|
|
1247
|
+
if (a.kind === "offline" && b.kind === "offline") return a.retryAt === b.retryAt;
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
function createConnectionSignal(options) {
|
|
1251
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1252
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1253
|
+
const cleanups = [];
|
|
1254
|
+
let hasBeenOnline = false;
|
|
1255
|
+
let anyChannelEverUp = false;
|
|
1256
|
+
let pendingTimer = null;
|
|
1257
|
+
let pendingSignal = null;
|
|
1258
|
+
let disposed = false;
|
|
1259
|
+
function compute() {
|
|
1260
|
+
const phase = options.kernel.phase;
|
|
1261
|
+
switch (phase.kind) {
|
|
1262
|
+
case "idle": return { kind: "connecting" };
|
|
1263
|
+
case "needs-auth": return { kind: "needs-auth" };
|
|
1264
|
+
case "offline": return {
|
|
1265
|
+
kind: "offline",
|
|
1266
|
+
retryAt: phase.nextRetryAt
|
|
1267
|
+
};
|
|
1268
|
+
case "discovering": return hasBeenOnline ? { kind: "offline" } : { kind: "connecting" };
|
|
1269
|
+
case "online": {
|
|
1270
|
+
const channels = options.transports.filter((t) => t.spec.phaseGating && !t.health().up).map((t) => t.spec.id);
|
|
1271
|
+
if (channels.length === 0) return { kind: "online" };
|
|
1272
|
+
return anyChannelEverUp ? {
|
|
1273
|
+
kind: "degraded",
|
|
1274
|
+
channels
|
|
1275
|
+
} : { kind: "connecting" };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
let current = compute();
|
|
1280
|
+
function clearPending() {
|
|
1281
|
+
if (pendingTimer !== null) {
|
|
1282
|
+
clearTimeout(pendingTimer);
|
|
1283
|
+
pendingTimer = null;
|
|
1284
|
+
pendingSignal = null;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function apply(next) {
|
|
1288
|
+
clearPending();
|
|
1289
|
+
if (signalEquals(current, next)) return;
|
|
1290
|
+
current = next;
|
|
1291
|
+
for (const listener of [...listeners]) try {
|
|
1292
|
+
listener(next);
|
|
1293
|
+
} catch {}
|
|
1294
|
+
}
|
|
1295
|
+
function recompute() {
|
|
1296
|
+
if (disposed) return;
|
|
1297
|
+
const raw = compute();
|
|
1298
|
+
if (signalEquals(current, raw)) {
|
|
1299
|
+
clearPending();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (raw.kind === "online" || raw.kind === "connecting" || raw.kind === "needs-auth") {
|
|
1303
|
+
apply(raw);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (pendingSignal && pendingSignal.kind === raw.kind) {
|
|
1307
|
+
pendingSignal = raw;
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
clearPending();
|
|
1311
|
+
pendingSignal = raw;
|
|
1312
|
+
pendingTimer = setTimeout(() => {
|
|
1313
|
+
pendingTimer = null;
|
|
1314
|
+
const held = pendingSignal;
|
|
1315
|
+
pendingSignal = null;
|
|
1316
|
+
if (held) apply(compute().kind === held.kind ? compute() : held);
|
|
1317
|
+
}, debounceMs);
|
|
1318
|
+
}
|
|
1319
|
+
cleanups.push(options.kernel.subscribe((next) => {
|
|
1320
|
+
if (next.kind === "online") hasBeenOnline = true;
|
|
1321
|
+
else if (next.kind === "idle") {
|
|
1322
|
+
hasBeenOnline = false;
|
|
1323
|
+
anyChannelEverUp = false;
|
|
1324
|
+
}
|
|
1325
|
+
recompute();
|
|
1326
|
+
}));
|
|
1327
|
+
for (const transport of options.transports) {
|
|
1328
|
+
cleanups.push(transport.on("up", () => {
|
|
1329
|
+
anyChannelEverUp = true;
|
|
1330
|
+
recompute();
|
|
1331
|
+
}));
|
|
1332
|
+
cleanups.push(transport.on("down", () => recompute()));
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
get current() {
|
|
1336
|
+
return current;
|
|
1337
|
+
},
|
|
1338
|
+
raw: () => compute(),
|
|
1339
|
+
recompute,
|
|
1340
|
+
subscribe(listener) {
|
|
1341
|
+
listeners.add(listener);
|
|
1342
|
+
return () => listeners.delete(listener);
|
|
1343
|
+
},
|
|
1344
|
+
dispose() {
|
|
1345
|
+
disposed = true;
|
|
1346
|
+
clearPending();
|
|
1347
|
+
for (const fn of cleanups) fn();
|
|
1348
|
+
listeners.clear();
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
//#endregion
|
|
1353
|
+
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 };
|