@camera.ui/transport 0.0.21 → 0.0.23
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 +16 -0
- package/dist/effects/networkChange.d.ts +1 -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 +542 -353
- 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,16 +466,85 @@ 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
|
+
if (options.isReachable?.()) {
|
|
499
|
+
options.onHold?.("probe failed but a channel is up — holding");
|
|
500
|
+
arm();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
options.onOffline?.("degraded: endpoint unreachable");
|
|
504
|
+
options.kernel.dispatch({
|
|
505
|
+
type: "PROBE_FAILED_ALL",
|
|
506
|
+
error: "degraded: endpoint unreachable"
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
arm();
|
|
511
|
+
}
|
|
512
|
+
const unsub = options.signal.subscribe((next) => {
|
|
513
|
+
if (next.kind === "degraded") arm();
|
|
514
|
+
else disarm();
|
|
515
|
+
});
|
|
516
|
+
if (options.signal.current.kind === "degraded") arm();
|
|
517
|
+
return () => {
|
|
518
|
+
detached = true;
|
|
519
|
+
disarm();
|
|
520
|
+
unsub();
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
378
524
|
//#region src/effects/networkChange.ts
|
|
379
525
|
function attachNetworkChange(options) {
|
|
526
|
+
const debounceMs = options.debounceMs ?? 0;
|
|
380
527
|
let detached = false;
|
|
528
|
+
let timer = null;
|
|
381
529
|
const handler = (event) => {
|
|
382
530
|
if (detached) return;
|
|
383
|
-
|
|
531
|
+
if (debounceMs <= 0) {
|
|
532
|
+
options.onChange(options.kernel, event);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (timer !== null) clearTimeout(timer);
|
|
536
|
+
timer = setTimeout(() => {
|
|
537
|
+
timer = null;
|
|
538
|
+
if (!detached) options.onChange(options.kernel, event);
|
|
539
|
+
}, debounceMs);
|
|
384
540
|
};
|
|
385
541
|
options.source.addEventListener("change", handler);
|
|
386
542
|
return () => {
|
|
387
543
|
detached = true;
|
|
544
|
+
if (timer !== null) {
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
timer = null;
|
|
547
|
+
}
|
|
388
548
|
options.source.removeEventListener("change", handler);
|
|
389
549
|
};
|
|
390
550
|
}
|
|
@@ -459,8 +619,15 @@ function attachPersistence(options) {
|
|
|
459
619
|
}
|
|
460
620
|
const unsub = options.kernel.subscribe((next, prev) => {
|
|
461
621
|
if (detached) return;
|
|
462
|
-
|
|
463
|
-
|
|
622
|
+
if (next.kind === "discovering" && next.pendingTokens && (prev.kind !== "discovering" || prev.pendingTokens !== next.pendingTokens)) {
|
|
623
|
+
if (cached) persist({
|
|
624
|
+
endpoint: cached.endpoint,
|
|
625
|
+
tokens: next.pendingTokens
|
|
626
|
+
});
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const nextTarget = next.kind === "online" ? next.target : null;
|
|
630
|
+
const prevTarget = prev.kind === "online" ? prev.target : null;
|
|
464
631
|
if (nextTarget && nextTarget !== prevTarget) {
|
|
465
632
|
persist(nextTarget);
|
|
466
633
|
return;
|
|
@@ -548,95 +715,6 @@ function attachPresence(options) {
|
|
|
548
715
|
};
|
|
549
716
|
}
|
|
550
717
|
//#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
718
|
//#region src/effects/probeLoop.ts
|
|
641
719
|
function makeProbeFailure(kind, message) {
|
|
642
720
|
const err = new Error(message);
|
|
@@ -712,6 +790,8 @@ function attachProbeLoop(options) {
|
|
|
712
790
|
const { endpoint, value: tokens } = await raceFirst(candidates, {
|
|
713
791
|
timeoutByMode: options.timeoutByMode,
|
|
714
792
|
parentSignal: ctrl.signal,
|
|
793
|
+
prefer: options.prefer,
|
|
794
|
+
preferGraceMs: options.preferGraceMs,
|
|
715
795
|
shortCircuit: (err) => isProbeFailure(err) && (err.kind === "needs-auth" || err.kind === "fatal" || err.kind === "aborted")
|
|
716
796
|
});
|
|
717
797
|
if (ctrl.signal.aborted) return;
|
|
@@ -749,52 +829,13 @@ function attachProbeLoop(options) {
|
|
|
749
829
|
};
|
|
750
830
|
}
|
|
751
831
|
//#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
832
|
//#region src/effects/tokenLifecycle.ts
|
|
792
|
-
var DEFAULT_GRACE_MS
|
|
833
|
+
var DEFAULT_GRACE_MS = 5e3;
|
|
793
834
|
var DEFAULT_MAX_TRANSIENT_RETRIES = 3;
|
|
794
835
|
var DEFAULT_TRANSIENT_RETRY_DELAY_MS = 2e3;
|
|
795
836
|
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
796
837
|
function attachTokenLifecycle(options) {
|
|
797
|
-
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS
|
|
838
|
+
const graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
798
839
|
const isTransient = options.isTransientError ?? (() => false);
|
|
799
840
|
const maxTransientRetries = options.maxTransientRetries ?? DEFAULT_MAX_TRANSIENT_RETRIES;
|
|
800
841
|
const transientRetryDelayMs = options.transientRetryDelayMs ?? DEFAULT_TRANSIENT_RETRY_DELAY_MS;
|
|
@@ -835,7 +876,7 @@ function attachTokenLifecycle(options) {
|
|
|
835
876
|
return;
|
|
836
877
|
}
|
|
837
878
|
const phase = options.kernel.phase;
|
|
838
|
-
const target = phase.kind === "online" ? phase.target :
|
|
879
|
+
const target = phase.kind === "online" ? phase.target : null;
|
|
839
880
|
if (!target) {
|
|
840
881
|
options.onTriggerSkipped?.(reason, "no-target", phase.kind);
|
|
841
882
|
return;
|
|
@@ -856,7 +897,7 @@ function attachTokenLifecycle(options) {
|
|
|
856
897
|
};
|
|
857
898
|
}
|
|
858
899
|
const livePhase = options.kernel.phase;
|
|
859
|
-
const base = (livePhase.kind === "online" ? livePhase.target :
|
|
900
|
+
const base = (livePhase.kind === "online" ? livePhase.target : null) ?? target;
|
|
860
901
|
const refreshTarget = fresh ? {
|
|
861
902
|
...base,
|
|
862
903
|
tokens: fresh
|
|
@@ -925,7 +966,7 @@ function attachTokenLifecycle(options) {
|
|
|
925
966
|
transientRetries = 0;
|
|
926
967
|
schedule(next.target);
|
|
927
968
|
}
|
|
928
|
-
} else
|
|
969
|
+
} else {
|
|
929
970
|
cancelTimer();
|
|
930
971
|
transientRetries = 0;
|
|
931
972
|
}
|
|
@@ -946,7 +987,7 @@ function attachTokenLifecycle(options) {
|
|
|
946
987
|
function wake() {
|
|
947
988
|
if (detached) return;
|
|
948
989
|
const phase = options.kernel.phase;
|
|
949
|
-
const target = phase.kind === "online" ? phase.target :
|
|
990
|
+
const target = phase.kind === "online" ? phase.target : null;
|
|
950
991
|
if (!target) {
|
|
951
992
|
options.onWakeChecked?.({
|
|
952
993
|
decision: "no-target",
|
|
@@ -989,29 +1030,58 @@ function stringifyError(err) {
|
|
|
989
1030
|
//#endregion
|
|
990
1031
|
//#region src/effects/transportSync.ts
|
|
991
1032
|
var SKIP = Symbol("transport-sync-skip");
|
|
1033
|
+
var DEFAULT_RETRY_MS = 2e3;
|
|
992
1034
|
function syncTargetFor(phase) {
|
|
993
1035
|
switch (phase.kind) {
|
|
994
1036
|
case "idle":
|
|
995
1037
|
case "offline":
|
|
996
1038
|
case "needs-auth": return null;
|
|
997
1039
|
case "online": return phase.target;
|
|
998
|
-
case "reconnecting": return phase.lastTarget;
|
|
999
1040
|
case "discovering": return SKIP;
|
|
1000
1041
|
}
|
|
1001
1042
|
}
|
|
1002
1043
|
function attachTransportSync(options) {
|
|
1003
1044
|
let detached = false;
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
for (const transport of options.transports) transport.apply(target).catch((err) => {
|
|
1045
|
+
const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
1046
|
+
const applied = /* @__PURE__ */ new Map();
|
|
1047
|
+
const retryTimers = /* @__PURE__ */ new Map();
|
|
1048
|
+
let lastNotified;
|
|
1049
|
+
function applyOne(transport, target) {
|
|
1050
|
+
applied.set(transport, target);
|
|
1051
|
+
transport.apply(target).catch((err) => {
|
|
1012
1052
|
options.onError?.(transport, target, err);
|
|
1053
|
+
if (applied.get(transport) === target) {
|
|
1054
|
+
applied.delete(transport);
|
|
1055
|
+
scheduleRetry(transport);
|
|
1056
|
+
}
|
|
1013
1057
|
});
|
|
1014
|
-
|
|
1058
|
+
}
|
|
1059
|
+
function scheduleRetry(transport) {
|
|
1060
|
+
if (detached || retryTimers.has(transport)) return;
|
|
1061
|
+
const timer = setTimeout(() => {
|
|
1062
|
+
retryTimers.delete(transport);
|
|
1063
|
+
if (detached) return;
|
|
1064
|
+
const decision = syncTargetFor(options.kernel.phase);
|
|
1065
|
+
if (decision === SKIP) {
|
|
1066
|
+
scheduleRetry(transport);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (applied.has(transport) && isSameTarget(applied.get(transport), decision)) return;
|
|
1070
|
+
options.onRetry?.(transport, decision);
|
|
1071
|
+
applyOne(transport, decision);
|
|
1072
|
+
}, retryMs);
|
|
1073
|
+
retryTimers.set(transport, timer);
|
|
1074
|
+
}
|
|
1075
|
+
function applyAll(target) {
|
|
1076
|
+
if (detached) return;
|
|
1077
|
+
for (const transport of options.transports) {
|
|
1078
|
+
if (applied.has(transport) && isSameTarget(applied.get(transport), target)) continue;
|
|
1079
|
+
applyOne(transport, target);
|
|
1080
|
+
}
|
|
1081
|
+
if (lastNotified === void 0 || !isSameTarget(lastNotified, target)) {
|
|
1082
|
+
lastNotified = target;
|
|
1083
|
+
options.onApplied?.(target);
|
|
1084
|
+
}
|
|
1015
1085
|
}
|
|
1016
1086
|
function syncFromPhase(phase) {
|
|
1017
1087
|
const decision = syncTargetFor(phase);
|
|
@@ -1024,76 +1094,12 @@ function attachTransportSync(options) {
|
|
|
1024
1094
|
});
|
|
1025
1095
|
return () => {
|
|
1026
1096
|
detached = true;
|
|
1097
|
+
for (const timer of retryTimers.values()) clearTimeout(timer);
|
|
1098
|
+
retryTimers.clear();
|
|
1027
1099
|
unsub();
|
|
1028
1100
|
};
|
|
1029
1101
|
}
|
|
1030
1102
|
//#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
1103
|
//#region src/effects/workerBridge.ts
|
|
1098
1104
|
var log = new Logger("workerBridge");
|
|
1099
1105
|
function attachWorkerBridge(options) {
|
|
@@ -1180,4 +1186,187 @@ function attachWorkerBridge(options) {
|
|
|
1180
1186
|
};
|
|
1181
1187
|
}
|
|
1182
1188
|
//#endregion
|
|
1183
|
-
|
|
1189
|
+
//#region src/journal.ts
|
|
1190
|
+
var DEFAULT_CAPACITY = 1e3;
|
|
1191
|
+
var MAX_DETAIL_LENGTH = 300;
|
|
1192
|
+
function stringifyDetail(detail) {
|
|
1193
|
+
if (detail === void 0 || detail === null) return void 0;
|
|
1194
|
+
let text;
|
|
1195
|
+
if (typeof detail === "string") text = detail;
|
|
1196
|
+
else if (detail instanceof Error) text = detail.message;
|
|
1197
|
+
else try {
|
|
1198
|
+
text = JSON.stringify(detail);
|
|
1199
|
+
} catch {
|
|
1200
|
+
text = String(detail);
|
|
1201
|
+
}
|
|
1202
|
+
return text.length > MAX_DETAIL_LENGTH ? `${text.slice(0, MAX_DETAIL_LENGTH)}…` : text;
|
|
1203
|
+
}
|
|
1204
|
+
function formatTime(t) {
|
|
1205
|
+
const d = new Date(t);
|
|
1206
|
+
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")}`;
|
|
1207
|
+
}
|
|
1208
|
+
function createConnectionJournal(options = {}) {
|
|
1209
|
+
const capacity = options.capacity ?? DEFAULT_CAPACITY;
|
|
1210
|
+
const now = options.now ?? (() => Date.now());
|
|
1211
|
+
const buffer = [];
|
|
1212
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1213
|
+
let head = 0;
|
|
1214
|
+
let seq = 0;
|
|
1215
|
+
function record(scope, msg, detail) {
|
|
1216
|
+
const entry = {
|
|
1217
|
+
seq: seq++,
|
|
1218
|
+
t: now(),
|
|
1219
|
+
scope,
|
|
1220
|
+
msg,
|
|
1221
|
+
detail: stringifyDetail(detail)
|
|
1222
|
+
};
|
|
1223
|
+
if (buffer.length < capacity) buffer.push(entry);
|
|
1224
|
+
else {
|
|
1225
|
+
buffer[head] = entry;
|
|
1226
|
+
head = (head + 1) % capacity;
|
|
1227
|
+
}
|
|
1228
|
+
for (const listener of [...listeners]) try {
|
|
1229
|
+
listener(entry);
|
|
1230
|
+
} catch {}
|
|
1231
|
+
}
|
|
1232
|
+
function list() {
|
|
1233
|
+
return [...buffer.slice(head), ...buffer.slice(0, head)];
|
|
1234
|
+
}
|
|
1235
|
+
function clear() {
|
|
1236
|
+
buffer.length = 0;
|
|
1237
|
+
head = 0;
|
|
1238
|
+
}
|
|
1239
|
+
function subscribe(listener) {
|
|
1240
|
+
listeners.add(listener);
|
|
1241
|
+
return () => {
|
|
1242
|
+
listeners.delete(listener);
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function exportText() {
|
|
1246
|
+
const entries = list();
|
|
1247
|
+
return [`connection journal — exported ${new Date(now()).toISOString()} (${entries.length} entries)`, ...entries.map((e) => {
|
|
1248
|
+
const detail = e.detail ? ` — ${e.detail}` : "";
|
|
1249
|
+
return `${formatTime(e.t)} [${e.scope}] ${e.msg}${detail}`;
|
|
1250
|
+
})].join("\n");
|
|
1251
|
+
}
|
|
1252
|
+
return {
|
|
1253
|
+
record,
|
|
1254
|
+
list,
|
|
1255
|
+
clear,
|
|
1256
|
+
subscribe,
|
|
1257
|
+
exportText
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/signal.ts
|
|
1262
|
+
var DEFAULT_DEBOUNCE_MS = 1500;
|
|
1263
|
+
function signalEquals(a, b) {
|
|
1264
|
+
if (a.kind !== b.kind) return false;
|
|
1265
|
+
if (a.kind === "degraded" && b.kind === "degraded") return a.channels.join(",") === b.channels.join(",");
|
|
1266
|
+
if (a.kind === "offline" && b.kind === "offline") return a.retryAt === b.retryAt;
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
function createConnectionSignal(options) {
|
|
1270
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1271
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1272
|
+
const cleanups = [];
|
|
1273
|
+
let hasBeenOnline = false;
|
|
1274
|
+
let anyChannelEverUp = false;
|
|
1275
|
+
let pendingTimer = null;
|
|
1276
|
+
let pendingSignal = null;
|
|
1277
|
+
let disposed = false;
|
|
1278
|
+
function compute() {
|
|
1279
|
+
const phase = options.kernel.phase;
|
|
1280
|
+
switch (phase.kind) {
|
|
1281
|
+
case "idle": return { kind: "connecting" };
|
|
1282
|
+
case "needs-auth": return { kind: "needs-auth" };
|
|
1283
|
+
case "offline": return {
|
|
1284
|
+
kind: "offline",
|
|
1285
|
+
retryAt: phase.nextRetryAt
|
|
1286
|
+
};
|
|
1287
|
+
case "discovering": return hasBeenOnline ? { kind: "offline" } : { kind: "connecting" };
|
|
1288
|
+
case "online": {
|
|
1289
|
+
const channels = options.transports.filter((t) => t.spec.phaseGating && !t.health().up).map((t) => t.spec.id);
|
|
1290
|
+
if (channels.length === 0) return { kind: "online" };
|
|
1291
|
+
return anyChannelEverUp ? {
|
|
1292
|
+
kind: "degraded",
|
|
1293
|
+
channels
|
|
1294
|
+
} : { kind: "connecting" };
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
let current = compute();
|
|
1299
|
+
function clearPending() {
|
|
1300
|
+
if (pendingTimer !== null) {
|
|
1301
|
+
clearTimeout(pendingTimer);
|
|
1302
|
+
pendingTimer = null;
|
|
1303
|
+
pendingSignal = null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function apply(next) {
|
|
1307
|
+
clearPending();
|
|
1308
|
+
if (signalEquals(current, next)) return;
|
|
1309
|
+
current = next;
|
|
1310
|
+
for (const listener of [...listeners]) try {
|
|
1311
|
+
listener(next);
|
|
1312
|
+
} catch {}
|
|
1313
|
+
}
|
|
1314
|
+
function recompute() {
|
|
1315
|
+
if (disposed) return;
|
|
1316
|
+
const raw = compute();
|
|
1317
|
+
if (signalEquals(current, raw)) {
|
|
1318
|
+
clearPending();
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (raw.kind === "online" || raw.kind === "connecting" || raw.kind === "needs-auth") {
|
|
1322
|
+
apply(raw);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (pendingSignal && pendingSignal.kind === raw.kind) {
|
|
1326
|
+
pendingSignal = raw;
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
clearPending();
|
|
1330
|
+
pendingSignal = raw;
|
|
1331
|
+
pendingTimer = setTimeout(() => {
|
|
1332
|
+
pendingTimer = null;
|
|
1333
|
+
const held = pendingSignal;
|
|
1334
|
+
pendingSignal = null;
|
|
1335
|
+
if (held) apply(compute().kind === held.kind ? compute() : held);
|
|
1336
|
+
}, debounceMs);
|
|
1337
|
+
}
|
|
1338
|
+
cleanups.push(options.kernel.subscribe((next) => {
|
|
1339
|
+
if (next.kind === "online") hasBeenOnline = true;
|
|
1340
|
+
else if (next.kind === "idle") {
|
|
1341
|
+
hasBeenOnline = false;
|
|
1342
|
+
anyChannelEverUp = false;
|
|
1343
|
+
}
|
|
1344
|
+
recompute();
|
|
1345
|
+
}));
|
|
1346
|
+
for (const transport of options.transports) {
|
|
1347
|
+
cleanups.push(transport.on("up", () => {
|
|
1348
|
+
anyChannelEverUp = true;
|
|
1349
|
+
recompute();
|
|
1350
|
+
}));
|
|
1351
|
+
cleanups.push(transport.on("down", () => recompute()));
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
get current() {
|
|
1355
|
+
return current;
|
|
1356
|
+
},
|
|
1357
|
+
raw: () => compute(),
|
|
1358
|
+
recompute,
|
|
1359
|
+
subscribe(listener) {
|
|
1360
|
+
listeners.add(listener);
|
|
1361
|
+
return () => listeners.delete(listener);
|
|
1362
|
+
},
|
|
1363
|
+
dispose() {
|
|
1364
|
+
disposed = true;
|
|
1365
|
+
clearPending();
|
|
1366
|
+
for (const fn of cleanups) fn();
|
|
1367
|
+
listeners.clear();
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
//#endregion
|
|
1372
|
+
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 };
|