@econ-v1/domain 7.0.53 → 7.0.55
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.
|
@@ -12,6 +12,7 @@ export function isPotentiallyTrustworthyOrigin(url: RuntimeUrl): boolean;
|
|
|
12
12
|
/** @typedef {HomeNodeClient & { close?: () => void | Promise<void> }} ClosableHomeNodeClient */
|
|
13
13
|
/** @typedef {{ abort(): void, readonly signal: AbortSignalPort }} AbortControllerPort */
|
|
14
14
|
/** @typedef {{ readonly candidate: DirectHomeNodeCandidate, readonly controller: AbortControllerPort, readonly promise: Promise<HomeNodeClient> }} DialAttempt */
|
|
15
|
+
/** @typedef {{ readonly abort: AbortControllerPort, readonly promise: Promise<HomeNodeClient> }} SharedRun */
|
|
15
16
|
/** @typedef {{ readonly attempt: DialAttempt, readonly client: HomeNodeClient, readonly success: true }} DialSuccess */
|
|
16
17
|
/** @typedef {{ readonly attempt: DialAttempt, readonly success: false }} DialFailure */
|
|
17
18
|
export class TransportManager {
|
|
@@ -55,7 +56,17 @@ export class TransportManager {
|
|
|
55
56
|
markReconnecting(attempt: number, nextRetryAt: string): void;
|
|
56
57
|
/** @param {TransportOfflineReason} [reason] @returns {void} */
|
|
57
58
|
markOffline(reason?: TransportOfflineReason): void;
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Stops everything in flight. This is a global teardown primitive — `close()` and
|
|
61
|
+
* `disconnect()` on the seam above call it — never a per-caller cancel, so it stays
|
|
62
|
+
* "abort all runs" rather than becoming per-run. What did have to change is the
|
|
63
|
+
* bookkeeping: with concurrent runs possible, a single `#runAbort` slot held only the
|
|
64
|
+
* most recently started one, and an older run would have kept walking
|
|
65
|
+
* `DIRECT_TIER_ORDER` — `#connect` re-checks its own run signal between tiers, and that
|
|
66
|
+
* signal was never aborted — dialling fresh candidates after the caller asked for
|
|
67
|
+
* silence. Aborting every registered run closes that gap.
|
|
68
|
+
* @returns {void}
|
|
69
|
+
*/
|
|
59
70
|
abort(): void;
|
|
60
71
|
#private;
|
|
61
72
|
}
|
|
@@ -92,6 +103,10 @@ export type DialAttempt = {
|
|
|
92
103
|
readonly controller: AbortControllerPort;
|
|
93
104
|
readonly promise: Promise<HomeNodeClient>;
|
|
94
105
|
};
|
|
106
|
+
export type SharedRun = {
|
|
107
|
+
readonly abort: AbortControllerPort;
|
|
108
|
+
readonly promise: Promise<HomeNodeClient>;
|
|
109
|
+
};
|
|
95
110
|
export type DialSuccess = {
|
|
96
111
|
readonly attempt: DialAttempt;
|
|
97
112
|
readonly client: HomeNodeClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport-manager.d.ts","sourceRoot":"","sources":["../../src/services/transport-manager.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transport-manager.d.ts","sourceRoot":"","sources":["../../src/services/transport-manager.js"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@econ-v1/domain",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.55",
|
|
4
4
|
"description": "Platform-neutral domain contracts and errors for Node clients",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "Node contributors",
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@econ-v1/ports": "7.0.
|
|
60
|
+
"@econ-v1/ports": "7.0.55"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -24,6 +24,7 @@ const DIRECT_TIER_ORDER = ["lan-direct", "lan-webtransport", "wan-direct"];
|
|
|
24
24
|
/** @typedef {HomeNodeClient & { close?: () => void | Promise<void> }} ClosableHomeNodeClient */
|
|
25
25
|
/** @typedef {{ abort(): void, readonly signal: AbortSignalPort }} AbortControllerPort */
|
|
26
26
|
/** @typedef {{ readonly candidate: DirectHomeNodeCandidate, readonly controller: AbortControllerPort, readonly promise: Promise<HomeNodeClient> }} DialAttempt */
|
|
27
|
+
/** @typedef {{ readonly abort: AbortControllerPort, readonly promise: Promise<HomeNodeClient> }} SharedRun */
|
|
27
28
|
/** @typedef {{ readonly attempt: DialAttempt, readonly client: HomeNodeClient, readonly success: true }} DialSuccess */
|
|
28
29
|
/** @typedef {{ readonly attempt: DialAttempt, readonly success: false }} DialFailure */
|
|
29
30
|
|
|
@@ -40,10 +41,15 @@ export class TransportManager {
|
|
|
40
41
|
#now;
|
|
41
42
|
/** @readonly @type {(state: TransportState) => void} */
|
|
42
43
|
#onState;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
/**
|
|
45
|
+
* In-flight dials, keyed by the exact candidate set each was created for (see
|
|
46
|
+
* `#shareRun`). A map rather than a single slot because two callers may legitimately
|
|
47
|
+
* be dialling DIFFERENT ladders at the same moment — the supervisor's LAN promotion
|
|
48
|
+
* probe and a kernel read that wants the full ladder — and neither may inherit the
|
|
49
|
+
* other's result.
|
|
50
|
+
* @readonly @type {Map<string, SharedRun>}
|
|
51
|
+
*/
|
|
52
|
+
#runs = new Map();
|
|
47
53
|
/** @type {DirectHomeNodeCandidate | undefined} */
|
|
48
54
|
#selectedCandidate;
|
|
49
55
|
/** @readonly @type {Set<AbortControllerPort>} */
|
|
@@ -181,30 +187,64 @@ export class TransportManager {
|
|
|
181
187
|
});
|
|
182
188
|
}
|
|
183
189
|
|
|
184
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Stops everything in flight. This is a global teardown primitive — `close()` and
|
|
192
|
+
* `disconnect()` on the seam above call it — never a per-caller cancel, so it stays
|
|
193
|
+
* "abort all runs" rather than becoming per-run. What did have to change is the
|
|
194
|
+
* bookkeeping: with concurrent runs possible, a single `#runAbort` slot held only the
|
|
195
|
+
* most recently started one, and an older run would have kept walking
|
|
196
|
+
* `DIRECT_TIER_ORDER` — `#connect` re-checks its own run signal between tiers, and that
|
|
197
|
+
* signal was never aborted — dialling fresh candidates after the caller asked for
|
|
198
|
+
* silence. Aborting every registered run closes that gap.
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
185
201
|
abort() {
|
|
186
|
-
this.#
|
|
202
|
+
for (const run of this.#runs.values()) run.abort.abort();
|
|
187
203
|
for (const controller of this.#controllers) controller.abort();
|
|
188
204
|
this.markOffline();
|
|
189
205
|
}
|
|
190
206
|
|
|
191
207
|
/**
|
|
208
|
+
* Collapses concurrent callers asking for THE SAME dial onto one run, and only those.
|
|
209
|
+
*
|
|
210
|
+
* This used to share `this.#run` with whoever asked next, ignoring the `candidates`
|
|
211
|
+
* argument entirely on the early return. That is a correctness bug, not an
|
|
212
|
+
* optimization: the supervisor's LAN promotion probe calls
|
|
213
|
+
* `connect({ higherPriorityThan: "wan-direct" })` every ~10s, which filters the ladder
|
|
214
|
+
* down to the LAN tiers. On a normal deployment those tiers are empty, so `#connect`
|
|
215
|
+
* finds no tier with members and rejects IMMEDIATELY, with no I/O at all — and that
|
|
216
|
+
* rejected run was then handed to every later caller, including
|
|
217
|
+
* `ManagedHomeNodeClient`'s bare `connect()` that wanted the full ladder and would have
|
|
218
|
+
* reached the node. Measured in a browser against a live node: reads succeeded during
|
|
219
|
+
* page load, the first promotion tick poisoned the slot, and for the remaining 92s of
|
|
220
|
+
* the window the client issued ZERO fetches while every read failed and was recorded
|
|
221
|
+
* against the node as unreachable — the chip flipped to Offline at ~89s with the node
|
|
222
|
+
* up and answering the whole time.
|
|
223
|
+
*
|
|
224
|
+
* So the shared run is keyed by the candidate set it was created for. A caller that
|
|
225
|
+
* asked for a different set gets its own dial; a caller that asked for the same set
|
|
226
|
+
* still gets the in-flight one, which is the single-activation contract
|
|
227
|
+
* `ManagedHomeNodeClient#ensureClient` depends on to avoid duplicate connections. Do
|
|
228
|
+
* not widen this back to unconditional sharing.
|
|
229
|
+
*
|
|
192
230
|
* @param {readonly DirectHomeNodeCandidate[]} candidates
|
|
193
231
|
* @param {boolean} publishState
|
|
194
232
|
* @returns {Promise<HomeNodeClient>}
|
|
195
233
|
*/
|
|
196
234
|
#shareRun(candidates, publishState) {
|
|
197
|
-
|
|
235
|
+
const key = candidateSetKey(candidates);
|
|
236
|
+
const shared = this.#runs.get(key);
|
|
237
|
+
if (shared !== undefined) return shared.promise;
|
|
198
238
|
const abort = createRuntimeAbortController();
|
|
199
239
|
const connection = this.#connect(candidates, publishState, abort.signal);
|
|
200
240
|
/** @type {Promise<HomeNodeClient>} */
|
|
201
|
-
/** @type {Promise<HomeNodeClient>} */
|
|
202
241
|
const run = connection.finally(() => {
|
|
203
|
-
|
|
204
|
-
|
|
242
|
+
// Keyed on the same entry, so a settled run can never be matched by a later
|
|
243
|
+
// caller, and a run started after this one for the same key is never evicted
|
|
244
|
+
// by its predecessor settling.
|
|
245
|
+
if (this.#runs.get(key)?.promise === run) this.#runs.delete(key);
|
|
205
246
|
});
|
|
206
|
-
this.#
|
|
207
|
-
this.#runAbort = abort;
|
|
247
|
+
this.#runs.set(key, { abort, promise: run });
|
|
208
248
|
return run;
|
|
209
249
|
}
|
|
210
250
|
|
|
@@ -357,6 +397,30 @@ function wholeAttempt(attempt) {
|
|
|
357
397
|
return Number.isFinite(attempt) ? Math.max(1, Math.trunc(attempt)) : 1;
|
|
358
398
|
}
|
|
359
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Identity of a requested dial, for `#shareRun`'s in-flight registry.
|
|
402
|
+
*
|
|
403
|
+
* Deliberately the same `[path, address, id]` triple `normalizeDirectCandidates` already
|
|
404
|
+
* keys its dedup on, so "the same candidate" means one thing in this file rather than
|
|
405
|
+
* two. Every set reaching here is a filtered view of the normalized ladder, so the
|
|
406
|
+
* addresses are already canonical and the order is the ladder's — two callers asking for
|
|
407
|
+
* the same thing produce byte-identical keys. `JSON.stringify` of the whole list (not a
|
|
408
|
+
* join) keeps an empty set at `"[]"`, which no non-empty set can collide with: the
|
|
409
|
+
* empty-set run is exactly the one that rejects without dialling, and it must never be
|
|
410
|
+
* mistaken for a request to dial the ladder.
|
|
411
|
+
* @param {readonly DirectHomeNodeCandidate[]} candidates
|
|
412
|
+
* @returns {string}
|
|
413
|
+
*/
|
|
414
|
+
function candidateSetKey(candidates) {
|
|
415
|
+
return JSON.stringify(
|
|
416
|
+
candidates.map((candidate) => [
|
|
417
|
+
candidate.path,
|
|
418
|
+
candidate.address,
|
|
419
|
+
candidate.id,
|
|
420
|
+
]),
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
360
424
|
/**
|
|
361
425
|
* @param {readonly DirectHomeNodeCandidate[]} candidates
|
|
362
426
|
* @returns {readonly DirectHomeNodeCandidate[]}
|