wire-mesh-core 4.1.4 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/adapters/tls-transport.cjs +2 -0
- package/dist/adapters/tls-transport.mjs +2 -0
- package/dist/domain/async-queue.cjs +48 -12
- package/dist/domain/async-queue.d.cts +1 -1
- package/dist/domain/async-queue.d.mts +1 -1
- package/dist/domain/async-queue.mjs +48 -12
- package/dist/domain/bulk.cjs +50 -10
- package/dist/domain/bulk.d.cts +4 -0
- package/dist/domain/bulk.d.mts +4 -0
- package/dist/domain/bulk.mjs +50 -11
- package/dist/domain/capability-request.cjs +2 -5
- package/dist/domain/capability-request.mjs +1 -4
- package/dist/domain/consume-incoming.cjs +96 -0
- package/dist/domain/consume-incoming.d.cts +27 -0
- package/dist/domain/consume-incoming.d.mts +27 -0
- package/dist/domain/consume-incoming.mjs +93 -0
- package/dist/domain/direct-manage-request.cjs +25 -12
- package/dist/domain/direct-manage-request.mjs +25 -12
- package/dist/domain/fault-report.cjs +136 -0
- package/dist/domain/fault-report.d.cts +25 -0
- package/dist/domain/fault-report.d.mts +25 -0
- package/dist/domain/fault-report.mjs +129 -0
- package/dist/domain/gossip-expansion.cjs +1 -1
- package/dist/domain/gossip-expansion.mjs +1 -1
- package/dist/domain/mesh-session.cjs +9 -13
- package/dist/domain/mesh-session.mjs +9 -13
- package/dist/domain/ping-round-trips.cjs +21 -8
- package/dist/domain/ping-round-trips.mjs +21 -8
- package/dist/timers-DTJNuBa8.cjs +12 -0
- package/dist/timers-Q1_kDTyY.mjs +7 -0
- package/package.json +9 -1
- package/wasm-dist/wire_mesh_threshold_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IncomingManageRequest, ManageOutcome } from "./mesh-session.mjs";
|
|
2
|
+
//#region src/domain/consume-incoming.d.ts
|
|
3
|
+
/** What a consumer is told about a failure. `item` is the item whose handling failed, or undefined when the stream itself failed. `suppressed` is how many failures from the same source were not reported before this report, not counting the one it carries (see FAULT_REPORT_INTERVAL_MS); it also counts failures of sources the reporter has stopped tracking. */
|
|
4
|
+
export type ConsumeFault<T> = (error: unknown, item: T | undefined, suppressed: number) => void | Promise<void>;
|
|
5
|
+
export interface ConsumeOptions<T> {
|
|
6
|
+
/** Told of a failure, at most once per source per FAULT_REPORT_INTERVAL_MS, so a peer cannot drive one report per failing item. A throw from this callback, or a promise it returns that rejects, is reported once to the console and dropped: reporting a fault never ends the loop either. Omit to drop failures silently. */
|
|
7
|
+
onFault?: ConsumeFault<T> | undefined;
|
|
8
|
+
/** Names the source an item's failure is counted against, so that one peer's failures do not hide another's. Items share one source when omitted. */
|
|
9
|
+
sourceOf?: ((item: T) => string) | undefined;
|
|
10
|
+
/** Stops the loop: the stream's iterator is closed when the signal aborts, and the loop ends at the next item boundary. See consumeEach for what that does to an item already taken. */
|
|
11
|
+
signal?: AbortSignal | undefined;
|
|
12
|
+
/** The clock the report interval is read from. */
|
|
13
|
+
now?: (() => number) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Handles each item of `source` in order with `handle`. Resolves when the stream ends, fails or is stopped, and never rejects.
|
|
17
|
+
*
|
|
18
|
+
* Stopping with `signal` closes the stream's iterator at once and ends the loop at the next item boundary. An item the loop has already taken is handled to completion, never discarded, so a stream whose `return()` withdraws a pending wait (the session's own streams do) loses nothing to a later reader. A source whose `return()` cannot settle a pending `next()` (an async generator) ends the loop when that `next()` settles. A failure of the stream itself, or of closing it, is reported once and at once: it is not coalesced with item failures, because it ends the loop and no later report could carry it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function consumeEach<T>(source: Readonly<AsyncIterable<T>>, handle: (item: T) => void | Promise<void>, options?: Readonly<ConsumeOptions<T>>): Promise<void>;
|
|
21
|
+
/** The refusal sent for a request whose handling failed before it was answered. It is a fixed code with no text from the failure, so a peer learns nothing of this side's internals; the code is this repository's convention for it, beside the codes a handler itself answers with. */
|
|
22
|
+
export declare const HANDLER_FAILED_OUTCOME: ManageOutcome;
|
|
23
|
+
/**
|
|
24
|
+
* consumeEach for incoming manage-requests. A request whose handling fails before it was answered is answered with `internal-error`, so its sender is not left waiting for a response that will never come; if that answer cannot be sent either (the connection has gone), the request is dropped, and either way the failure is reported once. A request the handler already answered is not answered again.
|
|
25
|
+
*/
|
|
26
|
+
export declare function serveManageRequests(source: Readonly<AsyncIterable<IncomingManageRequest>>, handle: (incoming: IncomingManageRequest) => Promise<void>, options?: Readonly<ConsumeOptions<IncomingManageRequest>>): Promise<void>;
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { deviceIdToHex } from "./device-id.mjs";
|
|
2
|
+
import { createFaultReporter, deliverContained, describeFault } from "./fault-report.mjs";
|
|
3
|
+
//#region src/domain/consume-incoming.ts
|
|
4
|
+
/**
|
|
5
|
+
* The one way to consume a session's incoming streams (incomingManageRequests, incomingDataFrames, revocationAnnouncements, coordinatorFrames, events, or any other async iterable of items): a failure while handling one item is contained to that item, reported once, and the loop goes on to the next, and no rejection escapes to whoever started the loop. A consumer written as a bare `for await` loop ends for good on the first item whose handling throws, and when it was started with `void` the rejection is unhandled, which ends a Node process by default.
|
|
6
|
+
*
|
|
7
|
+
* A failure of the stream itself (the iterator throws, as it does when the session behind it has gone) ends the loop and is reported once with no item. A stream that simply finishes ends the loop without a report.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Handles each item of `source` in order with `handle`. Resolves when the stream ends, fails or is stopped, and never rejects.
|
|
11
|
+
*
|
|
12
|
+
* Stopping with `signal` closes the stream's iterator at once and ends the loop at the next item boundary. An item the loop has already taken is handled to completion, never discarded, so a stream whose `return()` withdraws a pending wait (the session's own streams do) loses nothing to a later reader. A source whose `return()` cannot settle a pending `next()` (an async generator) ends the loop when that `next()` settles. A failure of the stream itself, or of closing it, is reported once and at once: it is not coalesced with item failures, because it ends the loop and no later report could carry it.
|
|
13
|
+
*/
|
|
14
|
+
async function consumeEach(source, handle, options = {}) {
|
|
15
|
+
const { onFault, signal } = options;
|
|
16
|
+
const reporter = createFaultReporter(options.now);
|
|
17
|
+
const reportItem = (error, item) => {
|
|
18
|
+
if (onFault === void 0) return;
|
|
19
|
+
try {
|
|
20
|
+
const origin = options.sourceOf?.(item) ?? "";
|
|
21
|
+
reporter(origin, async (suppressed) => onFault(error, item, suppressed));
|
|
22
|
+
} catch (failure) {
|
|
23
|
+
console.error(`could not report a failure: ${describeFault(failure)}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const reportStream = (error) => {
|
|
27
|
+
if (onFault !== void 0) deliverContained(async (suppressed) => onFault(error, void 0, suppressed), 0);
|
|
28
|
+
};
|
|
29
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
30
|
+
let closing;
|
|
31
|
+
const close = () => {
|
|
32
|
+
closing ??= iterator.return?.().catch(reportStream);
|
|
33
|
+
};
|
|
34
|
+
signal?.addEventListener("abort", close, { once: true });
|
|
35
|
+
/** Takes and handles one item. Resolves true when there may be another, false when the stream ended or was stopped; rejects when the stream itself failed. */
|
|
36
|
+
async function step() {
|
|
37
|
+
if (signal?.aborted === true) return false;
|
|
38
|
+
const result = await iterator.next();
|
|
39
|
+
if (result.done === true) return false;
|
|
40
|
+
try {
|
|
41
|
+
await handle(result.value);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
reportItem(error, result.value);
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
await new Promise((resolve) => {
|
|
48
|
+
const pump = () => {
|
|
49
|
+
step().then((more) => {
|
|
50
|
+
if (more) pump();
|
|
51
|
+
else resolve();
|
|
52
|
+
}, (error) => {
|
|
53
|
+
reportStream(error);
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
pump();
|
|
58
|
+
});
|
|
59
|
+
signal?.removeEventListener("abort", close);
|
|
60
|
+
if (signal?.aborted === true) close();
|
|
61
|
+
await closing;
|
|
62
|
+
}
|
|
63
|
+
/** The refusal sent for a request whose handling failed before it was answered. It is a fixed code with no text from the failure, so a peer learns nothing of this side's internals; the code is this repository's convention for it, beside the codes a handler itself answers with. */
|
|
64
|
+
const HANDLER_FAILED_OUTCOME = {
|
|
65
|
+
result: "error",
|
|
66
|
+
code: "internal-error"
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* consumeEach for incoming manage-requests. A request whose handling fails before it was answered is answered with `internal-error`, so its sender is not left waiting for a response that will never come; if that answer cannot be sent either (the connection has gone), the request is dropped, and either way the failure is reported once. A request the handler already answered is not answered again.
|
|
70
|
+
*/
|
|
71
|
+
async function serveManageRequests(source, handle, options = {}) {
|
|
72
|
+
await consumeEach(source, async (incoming) => {
|
|
73
|
+
const state = { answered: false };
|
|
74
|
+
const tracked = {
|
|
75
|
+
...incoming,
|
|
76
|
+
respond: async (outcome) => {
|
|
77
|
+
state.answered = true;
|
|
78
|
+
await incoming.respond(outcome);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
await handle(tracked);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (!state.answered) await incoming.respond(HANDLER_FAILED_OUTCOME).catch(() => void 0);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}, {
|
|
88
|
+
...options,
|
|
89
|
+
sourceOf: options.sourceOf ?? ((incoming) => incoming.fromDevice === void 0 ? "" : deviceIdToHex(incoming.fromDevice))
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { HANDLER_FAILED_OUTCOME, consumeEach, serveManageRequests };
|
|
@@ -41,20 +41,33 @@ async function sendDirectManageRequest(transport, address, command, scope, optio
|
|
|
41
41
|
scope,
|
|
42
42
|
...options.token !== void 0 ? { token: options.token } : {}
|
|
43
43
|
};
|
|
44
|
-
|
|
44
|
+
try {
|
|
45
|
+
await link.send(frame);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
await link.close().catch(() => void 0);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
45
50
|
const responsePromise = waitForDirectManageResponse(link, requestId);
|
|
46
51
|
let outcome;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
try {
|
|
53
|
+
if (options.timeoutMs === void 0) outcome = await responsePromise;
|
|
54
|
+
else {
|
|
55
|
+
let timer;
|
|
56
|
+
outcome = await Promise.race([responsePromise, new Promise((resolve) => {
|
|
57
|
+
timer = setTimeout(() => {
|
|
58
|
+
resolve({
|
|
59
|
+
result: "error",
|
|
60
|
+
code: "timeout"
|
|
61
|
+
});
|
|
62
|
+
}, options.timeoutMs);
|
|
63
|
+
})]).finally(() => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
});
|
|
66
|
+
responsePromise.catch(() => void 0);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
await link.close().catch(() => void 0);
|
|
70
|
+
throw error;
|
|
58
71
|
}
|
|
59
72
|
await link.close();
|
|
60
73
|
return outcome;
|
|
@@ -40,20 +40,33 @@ async function sendDirectManageRequest(transport, address, command, scope, optio
|
|
|
40
40
|
scope,
|
|
41
41
|
...options.token !== void 0 ? { token: options.token } : {}
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
try {
|
|
44
|
+
await link.send(frame);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await link.close().catch(() => void 0);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
44
49
|
const responsePromise = waitForDirectManageResponse(link, requestId);
|
|
45
50
|
let outcome;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
try {
|
|
52
|
+
if (options.timeoutMs === void 0) outcome = await responsePromise;
|
|
53
|
+
else {
|
|
54
|
+
let timer;
|
|
55
|
+
outcome = await Promise.race([responsePromise, new Promise((resolve) => {
|
|
56
|
+
timer = setTimeout(() => {
|
|
57
|
+
resolve({
|
|
58
|
+
result: "error",
|
|
59
|
+
code: "timeout"
|
|
60
|
+
});
|
|
61
|
+
}, options.timeoutMs);
|
|
62
|
+
})]).finally(() => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
});
|
|
65
|
+
responsePromise.catch(() => void 0);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await link.close().catch(() => void 0);
|
|
69
|
+
throw error;
|
|
57
70
|
}
|
|
58
71
|
await link.close();
|
|
59
72
|
return outcome;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_timers = require("../timers-DTJNuBa8.cjs");
|
|
3
|
+
//#region src/domain/fault-report.ts
|
|
4
|
+
/** The shortest time between two reports for one source. */
|
|
5
|
+
const FAULT_REPORT_INTERVAL_MS = 1e3;
|
|
6
|
+
/** The most sources whose report windows are kept at once; past it the oldest window goes, its pending count delivered first if the budget allows. */
|
|
7
|
+
const MAX_TRACKED_FAULT_SOURCES = 1024;
|
|
8
|
+
/** The most reports delivered across all sources in one interval. A source beyond it is counted like one inside its own interval, so the total is bounded however many distinct sources a peer presents. */
|
|
9
|
+
const MAX_FAULT_REPORTS_PER_INTERVAL = 64;
|
|
10
|
+
/** The most characters of a fault's text that a log line or console report carries, since the text can come from what a peer sent. */
|
|
11
|
+
const MAX_FAULT_TEXT_CHARS = 256;
|
|
12
|
+
/** A fault as one bounded, quoted line of text: control characters are escaped, so it cannot start a new line, and it is cut to MAX_FAULT_TEXT_CHARS. */
|
|
13
|
+
function describeFault(error) {
|
|
14
|
+
return JSON.stringify(String(error).slice(0, 256));
|
|
15
|
+
}
|
|
16
|
+
/** Runs `deliver` so that nothing it does reaches the caller: a throw, or a returned promise that rejects, is reported once to the console and dropped. */
|
|
17
|
+
function deliverContained(deliver, suppressed) {
|
|
18
|
+
const report = (error) => {
|
|
19
|
+
console.error(`the fault callback failed: ${describeFault(error)}`);
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
const result = deliver(suppressed);
|
|
23
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
report(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A clock that never steps backwards, which the interval arithmetic relies on. */
|
|
29
|
+
function monotonicNow() {
|
|
30
|
+
return performance.now();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds a reporter that delivers a source's first fault at once and then at most one more per FAULT_REPORT_INTERVAL_MS, with at most MAX_FAULT_REPORTS_PER_INTERVAL reports across all sources. A fault inside a source's interval is counted, and the most recent one is delivered when the interval ends, carrying the count of the others; a source that falls quiet is therefore still told. Each report's `suppressed` counts the faults of its source that were not reported before it, not the one it carries.
|
|
34
|
+
*
|
|
35
|
+
* A source forgotten to keep the number of tracked sources bounded loses nothing silently: the faults it still had pending are added to the count the next delivered report carries, from whichever source.
|
|
36
|
+
*
|
|
37
|
+
* `now` is the clock the intervals are read from; it must not step backwards.
|
|
38
|
+
*/
|
|
39
|
+
function createFaultReporter(now = monotonicNow) {
|
|
40
|
+
const windows = /* @__PURE__ */ new Map();
|
|
41
|
+
const budget = {
|
|
42
|
+
since: Number.NEGATIVE_INFINITY,
|
|
43
|
+
used: 0
|
|
44
|
+
};
|
|
45
|
+
const forgotten = { count: 0 };
|
|
46
|
+
/** Takes one report from the interval's budget, if it has one left. */
|
|
47
|
+
function admit(at) {
|
|
48
|
+
if (at - budget.since >= 1e3) {
|
|
49
|
+
budget.since = at;
|
|
50
|
+
budget.used = 0;
|
|
51
|
+
}
|
|
52
|
+
if (budget.used >= 64) return false;
|
|
53
|
+
budget.used += 1;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/** What a delivered report adds for faults of sources that were forgotten, which it takes over. */
|
|
57
|
+
function takeForgotten() {
|
|
58
|
+
const count = forgotten.count;
|
|
59
|
+
forgotten.count = 0;
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
function arm(source) {
|
|
63
|
+
const window = windows.get(source);
|
|
64
|
+
if (window === void 0 || window.timer !== void 0) return;
|
|
65
|
+
const delay = Math.max(0, window.since + FAULT_REPORT_INTERVAL_MS - now());
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
flush(source);
|
|
68
|
+
}, delay);
|
|
69
|
+
require_timers.unrefIfSupported(timer);
|
|
70
|
+
window.timer = timer;
|
|
71
|
+
}
|
|
72
|
+
/** Delivers a window's most recent pending fault at the end of its interval, or waits for the next interval when the budget is spent. */
|
|
73
|
+
function flush(source) {
|
|
74
|
+
const window = windows.get(source);
|
|
75
|
+
if (window === void 0) return;
|
|
76
|
+
window.timer = void 0;
|
|
77
|
+
const { latest } = window;
|
|
78
|
+
if (window.pending === 0 || latest === void 0) return;
|
|
79
|
+
const at = now();
|
|
80
|
+
window.since = at;
|
|
81
|
+
if (!admit(at)) {
|
|
82
|
+
arm(source);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const others = window.pending - 1;
|
|
86
|
+
window.pending = 0;
|
|
87
|
+
window.latest = void 0;
|
|
88
|
+
deliverContained(latest, others + takeForgotten());
|
|
89
|
+
}
|
|
90
|
+
function forgetOldest() {
|
|
91
|
+
const oldest = windows.entries().next();
|
|
92
|
+
if (oldest.done === true) return;
|
|
93
|
+
const [source, window] = oldest.value;
|
|
94
|
+
clearTimeout(window.timer);
|
|
95
|
+
windows.delete(source);
|
|
96
|
+
forgotten.count += window.pending;
|
|
97
|
+
}
|
|
98
|
+
return (source, deliver) => {
|
|
99
|
+
const at = now();
|
|
100
|
+
const window = windows.get(source);
|
|
101
|
+
if (window !== void 0 && at - window.since < 1e3) {
|
|
102
|
+
window.pending += 1;
|
|
103
|
+
window.latest = deliver;
|
|
104
|
+
arm(source);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const carried = window?.pending ?? 0;
|
|
108
|
+
clearTimeout(window?.timer);
|
|
109
|
+
windows.delete(source);
|
|
110
|
+
const next = {
|
|
111
|
+
since: at,
|
|
112
|
+
pending: carried,
|
|
113
|
+
latest: window?.latest,
|
|
114
|
+
timer: void 0
|
|
115
|
+
};
|
|
116
|
+
windows.set(source, next);
|
|
117
|
+
if (windows.size > 1024) forgetOldest();
|
|
118
|
+
if (!admit(at)) {
|
|
119
|
+
next.pending += 1;
|
|
120
|
+
next.latest = deliver;
|
|
121
|
+
arm(source);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
next.pending = 0;
|
|
125
|
+
next.latest = void 0;
|
|
126
|
+
deliverContained(deliver, carried + takeForgotten());
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
exports.FAULT_REPORT_INTERVAL_MS = FAULT_REPORT_INTERVAL_MS;
|
|
131
|
+
exports.MAX_FAULT_REPORTS_PER_INTERVAL = MAX_FAULT_REPORTS_PER_INTERVAL;
|
|
132
|
+
exports.MAX_FAULT_TEXT_CHARS = MAX_FAULT_TEXT_CHARS;
|
|
133
|
+
exports.MAX_TRACKED_FAULT_SOURCES = MAX_TRACKED_FAULT_SOURCES;
|
|
134
|
+
exports.createFaultReporter = createFaultReporter;
|
|
135
|
+
exports.deliverContained = deliverContained;
|
|
136
|
+
exports.describeFault = describeFault;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/domain/fault-report.d.ts
|
|
2
|
+
/** The shortest time between two reports for one source. */
|
|
3
|
+
export declare const FAULT_REPORT_INTERVAL_MS = 1000;
|
|
4
|
+
/** The most sources whose report windows are kept at once; past it the oldest window goes, its pending count delivered first if the budget allows. */
|
|
5
|
+
export declare const MAX_TRACKED_FAULT_SOURCES = 1024;
|
|
6
|
+
/** The most reports delivered across all sources in one interval. A source beyond it is counted like one inside its own interval, so the total is bounded however many distinct sources a peer presents. */
|
|
7
|
+
export declare const MAX_FAULT_REPORTS_PER_INTERVAL = 64;
|
|
8
|
+
/** The most characters of a fault's text that a log line or console report carries, since the text can come from what a peer sent. */
|
|
9
|
+
export declare const MAX_FAULT_TEXT_CHARS = 256;
|
|
10
|
+
/** A fault as one bounded, quoted line of text: control characters are escaped, so it cannot start a new line, and it is cut to MAX_FAULT_TEXT_CHARS. */
|
|
11
|
+
export declare function describeFault(error: unknown): string;
|
|
12
|
+
/** Delivers one report, given how many faults were suppressed since the previous report for its source, not counting the fault this report carries. A report that follows the loss of an evicted source's pending faults also counts those. */
|
|
13
|
+
export type FaultDelivery = (suppressed: number) => void | Promise<void>;
|
|
14
|
+
export type FaultReporter = (source: string, deliver: FaultDelivery) => void;
|
|
15
|
+
/** Runs `deliver` so that nothing it does reaches the caller: a throw, or a returned promise that rejects, is reported once to the console and dropped. */
|
|
16
|
+
export declare function deliverContained(deliver: FaultDelivery, suppressed: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Builds a reporter that delivers a source's first fault at once and then at most one more per FAULT_REPORT_INTERVAL_MS, with at most MAX_FAULT_REPORTS_PER_INTERVAL reports across all sources. A fault inside a source's interval is counted, and the most recent one is delivered when the interval ends, carrying the count of the others; a source that falls quiet is therefore still told. Each report's `suppressed` counts the faults of its source that were not reported before it, not the one it carries.
|
|
19
|
+
*
|
|
20
|
+
* A source forgotten to keep the number of tracked sources bounded loses nothing silently: the faults it still had pending are added to the count the next delivered report carries, from whichever source.
|
|
21
|
+
*
|
|
22
|
+
* `now` is the clock the intervals are read from; it must not step backwards.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createFaultReporter(now?: () => number): FaultReporter;
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/domain/fault-report.d.ts
|
|
2
|
+
/** The shortest time between two reports for one source. */
|
|
3
|
+
export declare const FAULT_REPORT_INTERVAL_MS = 1000;
|
|
4
|
+
/** The most sources whose report windows are kept at once; past it the oldest window goes, its pending count delivered first if the budget allows. */
|
|
5
|
+
export declare const MAX_TRACKED_FAULT_SOURCES = 1024;
|
|
6
|
+
/** The most reports delivered across all sources in one interval. A source beyond it is counted like one inside its own interval, so the total is bounded however many distinct sources a peer presents. */
|
|
7
|
+
export declare const MAX_FAULT_REPORTS_PER_INTERVAL = 64;
|
|
8
|
+
/** The most characters of a fault's text that a log line or console report carries, since the text can come from what a peer sent. */
|
|
9
|
+
export declare const MAX_FAULT_TEXT_CHARS = 256;
|
|
10
|
+
/** A fault as one bounded, quoted line of text: control characters are escaped, so it cannot start a new line, and it is cut to MAX_FAULT_TEXT_CHARS. */
|
|
11
|
+
export declare function describeFault(error: unknown): string;
|
|
12
|
+
/** Delivers one report, given how many faults were suppressed since the previous report for its source, not counting the fault this report carries. A report that follows the loss of an evicted source's pending faults also counts those. */
|
|
13
|
+
export type FaultDelivery = (suppressed: number) => void | Promise<void>;
|
|
14
|
+
export type FaultReporter = (source: string, deliver: FaultDelivery) => void;
|
|
15
|
+
/** Runs `deliver` so that nothing it does reaches the caller: a throw, or a returned promise that rejects, is reported once to the console and dropped. */
|
|
16
|
+
export declare function deliverContained(deliver: FaultDelivery, suppressed: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Builds a reporter that delivers a source's first fault at once and then at most one more per FAULT_REPORT_INTERVAL_MS, with at most MAX_FAULT_REPORTS_PER_INTERVAL reports across all sources. A fault inside a source's interval is counted, and the most recent one is delivered when the interval ends, carrying the count of the others; a source that falls quiet is therefore still told. Each report's `suppressed` counts the faults of its source that were not reported before it, not the one it carries.
|
|
19
|
+
*
|
|
20
|
+
* A source forgotten to keep the number of tracked sources bounded loses nothing silently: the faults it still had pending are added to the count the next delivered report carries, from whichever source.
|
|
21
|
+
*
|
|
22
|
+
* `now` is the clock the intervals are read from; it must not step backwards.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createFaultReporter(now?: () => number): FaultReporter;
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { t as unrefIfSupported } from "../timers-Q1_kDTyY.mjs";
|
|
2
|
+
//#region src/domain/fault-report.ts
|
|
3
|
+
/** The shortest time between two reports for one source. */
|
|
4
|
+
const FAULT_REPORT_INTERVAL_MS = 1e3;
|
|
5
|
+
/** The most sources whose report windows are kept at once; past it the oldest window goes, its pending count delivered first if the budget allows. */
|
|
6
|
+
const MAX_TRACKED_FAULT_SOURCES = 1024;
|
|
7
|
+
/** The most reports delivered across all sources in one interval. A source beyond it is counted like one inside its own interval, so the total is bounded however many distinct sources a peer presents. */
|
|
8
|
+
const MAX_FAULT_REPORTS_PER_INTERVAL = 64;
|
|
9
|
+
/** The most characters of a fault's text that a log line or console report carries, since the text can come from what a peer sent. */
|
|
10
|
+
const MAX_FAULT_TEXT_CHARS = 256;
|
|
11
|
+
/** A fault as one bounded, quoted line of text: control characters are escaped, so it cannot start a new line, and it is cut to MAX_FAULT_TEXT_CHARS. */
|
|
12
|
+
function describeFault(error) {
|
|
13
|
+
return JSON.stringify(String(error).slice(0, 256));
|
|
14
|
+
}
|
|
15
|
+
/** Runs `deliver` so that nothing it does reaches the caller: a throw, or a returned promise that rejects, is reported once to the console and dropped. */
|
|
16
|
+
function deliverContained(deliver, suppressed) {
|
|
17
|
+
const report = (error) => {
|
|
18
|
+
console.error(`the fault callback failed: ${describeFault(error)}`);
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
const result = deliver(suppressed);
|
|
22
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
report(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** A clock that never steps backwards, which the interval arithmetic relies on. */
|
|
28
|
+
function monotonicNow() {
|
|
29
|
+
return performance.now();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds a reporter that delivers a source's first fault at once and then at most one more per FAULT_REPORT_INTERVAL_MS, with at most MAX_FAULT_REPORTS_PER_INTERVAL reports across all sources. A fault inside a source's interval is counted, and the most recent one is delivered when the interval ends, carrying the count of the others; a source that falls quiet is therefore still told. Each report's `suppressed` counts the faults of its source that were not reported before it, not the one it carries.
|
|
33
|
+
*
|
|
34
|
+
* A source forgotten to keep the number of tracked sources bounded loses nothing silently: the faults it still had pending are added to the count the next delivered report carries, from whichever source.
|
|
35
|
+
*
|
|
36
|
+
* `now` is the clock the intervals are read from; it must not step backwards.
|
|
37
|
+
*/
|
|
38
|
+
function createFaultReporter(now = monotonicNow) {
|
|
39
|
+
const windows = /* @__PURE__ */ new Map();
|
|
40
|
+
const budget = {
|
|
41
|
+
since: Number.NEGATIVE_INFINITY,
|
|
42
|
+
used: 0
|
|
43
|
+
};
|
|
44
|
+
const forgotten = { count: 0 };
|
|
45
|
+
/** Takes one report from the interval's budget, if it has one left. */
|
|
46
|
+
function admit(at) {
|
|
47
|
+
if (at - budget.since >= 1e3) {
|
|
48
|
+
budget.since = at;
|
|
49
|
+
budget.used = 0;
|
|
50
|
+
}
|
|
51
|
+
if (budget.used >= 64) return false;
|
|
52
|
+
budget.used += 1;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** What a delivered report adds for faults of sources that were forgotten, which it takes over. */
|
|
56
|
+
function takeForgotten() {
|
|
57
|
+
const count = forgotten.count;
|
|
58
|
+
forgotten.count = 0;
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
function arm(source) {
|
|
62
|
+
const window = windows.get(source);
|
|
63
|
+
if (window === void 0 || window.timer !== void 0) return;
|
|
64
|
+
const delay = Math.max(0, window.since + FAULT_REPORT_INTERVAL_MS - now());
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
flush(source);
|
|
67
|
+
}, delay);
|
|
68
|
+
unrefIfSupported(timer);
|
|
69
|
+
window.timer = timer;
|
|
70
|
+
}
|
|
71
|
+
/** Delivers a window's most recent pending fault at the end of its interval, or waits for the next interval when the budget is spent. */
|
|
72
|
+
function flush(source) {
|
|
73
|
+
const window = windows.get(source);
|
|
74
|
+
if (window === void 0) return;
|
|
75
|
+
window.timer = void 0;
|
|
76
|
+
const { latest } = window;
|
|
77
|
+
if (window.pending === 0 || latest === void 0) return;
|
|
78
|
+
const at = now();
|
|
79
|
+
window.since = at;
|
|
80
|
+
if (!admit(at)) {
|
|
81
|
+
arm(source);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const others = window.pending - 1;
|
|
85
|
+
window.pending = 0;
|
|
86
|
+
window.latest = void 0;
|
|
87
|
+
deliverContained(latest, others + takeForgotten());
|
|
88
|
+
}
|
|
89
|
+
function forgetOldest() {
|
|
90
|
+
const oldest = windows.entries().next();
|
|
91
|
+
if (oldest.done === true) return;
|
|
92
|
+
const [source, window] = oldest.value;
|
|
93
|
+
clearTimeout(window.timer);
|
|
94
|
+
windows.delete(source);
|
|
95
|
+
forgotten.count += window.pending;
|
|
96
|
+
}
|
|
97
|
+
return (source, deliver) => {
|
|
98
|
+
const at = now();
|
|
99
|
+
const window = windows.get(source);
|
|
100
|
+
if (window !== void 0 && at - window.since < 1e3) {
|
|
101
|
+
window.pending += 1;
|
|
102
|
+
window.latest = deliver;
|
|
103
|
+
arm(source);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const carried = window?.pending ?? 0;
|
|
107
|
+
clearTimeout(window?.timer);
|
|
108
|
+
windows.delete(source);
|
|
109
|
+
const next = {
|
|
110
|
+
since: at,
|
|
111
|
+
pending: carried,
|
|
112
|
+
latest: window?.latest,
|
|
113
|
+
timer: void 0
|
|
114
|
+
};
|
|
115
|
+
windows.set(source, next);
|
|
116
|
+
if (windows.size > 1024) forgetOldest();
|
|
117
|
+
if (!admit(at)) {
|
|
118
|
+
next.pending += 1;
|
|
119
|
+
next.latest = deliver;
|
|
120
|
+
arm(source);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
next.pending = 0;
|
|
124
|
+
next.latest = void 0;
|
|
125
|
+
deliverContained(deliver, carried + takeForgotten());
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { FAULT_REPORT_INTERVAL_MS, MAX_FAULT_REPORTS_PER_INTERVAL, MAX_FAULT_TEXT_CHARS, MAX_TRACKED_FAULT_SOURCES, createFaultReporter, deliverContained, describeFault };
|
|
@@ -7,6 +7,7 @@ const require_gossip_extensions = require("../gossip-extensions-BBYgMZ2N.cjs");
|
|
|
7
7
|
const require_domain_peer_advert = require("./peer-advert.cjs");
|
|
8
8
|
const require_domain_async_queue = require("./async-queue.cjs");
|
|
9
9
|
const require_domain_channel_exhausted_error = require("./channel-exhausted-error.cjs");
|
|
10
|
+
const require_domain_fault_report = require("./fault-report.cjs");
|
|
10
11
|
const require_domain_handshake = require("./handshake.cjs");
|
|
11
12
|
const require_domain_hub_mailbox = require("./hub-mailbox.cjs");
|
|
12
13
|
const require_domain_ping_round_trips = require("./ping-round-trips.cjs");
|
|
@@ -280,17 +281,9 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
|
|
|
280
281
|
function createRelayChannels(identity, sendHello, options = {}) {
|
|
281
282
|
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
282
283
|
const channels = /* @__PURE__ */ new Map();
|
|
283
|
-
/** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it
|
|
284
|
+
/** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it goes through the shared containment in fault-report.ts. */
|
|
284
285
|
function reportFault(error) {
|
|
285
|
-
|
|
286
|
-
console.error("the local fault callback failed", faultError);
|
|
287
|
-
};
|
|
288
|
-
try {
|
|
289
|
-
const result = options.onFault?.(error);
|
|
290
|
-
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
291
|
-
} catch (faultError) {
|
|
292
|
-
report(faultError);
|
|
293
|
-
}
|
|
286
|
+
require_domain_fault_report.deliverContained(async () => options.onFault?.(error), 0);
|
|
294
287
|
}
|
|
295
288
|
/** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
|
|
296
289
|
function abandon(entry, error) {
|
|
@@ -503,7 +496,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
503
496
|
}
|
|
504
497
|
/** The "not connected" guard every send* method on the public session needs, deduplicated into one place rather than repeated inline at each call site: throws unless there is a live connection AND the session's own state machine agrees it is "connected" (transmit's own connection===null check alone doesn't cover the latter). Returns the connection itself, narrowed non-null, for a caller that needs to send directly on it (sendPing/sendPingMeasureRtt); a caller that only needs the throwing side effect (sendManageRequest and friends, which go through transmit instead) calls this and discards the result. */
|
|
505
498
|
function requireConnectedLink() {
|
|
506
|
-
if (connection === null || state.status !== "connected") throw new Error("not connected");
|
|
499
|
+
if (feedCancelled || connection === null || state.status !== "connected") throw new Error("not connected");
|
|
507
500
|
return connection;
|
|
508
501
|
}
|
|
509
502
|
/** Sends a frame. Through a relay it is sealed for toDevice on their secure channel first (spec/secure-channel.cddl) and only then wrapped as relay-data, so the hub carries ciphertext. This is the single choke point every outbound manage-request and manage-response passes through, so a consumer of sendManageRequest or respond never needs its own relay logic. */
|
|
@@ -834,7 +827,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
834
827
|
},
|
|
835
828
|
async sendManageRequest(command, scope, targetDevice, token, timeoutMs) {
|
|
836
829
|
requireConnectedLink();
|
|
837
|
-
if (targetDevice !== void 0)
|
|
830
|
+
if (targetDevice !== void 0) {
|
|
831
|
+
await ensureRelayPairing(targetDevice);
|
|
832
|
+
requireConnectedLink();
|
|
833
|
+
}
|
|
838
834
|
const frame = buildManageRequest(command, scope, token);
|
|
839
835
|
const requestId = frame["request-id"];
|
|
840
836
|
const outcome = new Promise((resolve, reject) => {
|
|
@@ -844,6 +840,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
844
840
|
expectedFrom: targetDevice
|
|
845
841
|
});
|
|
846
842
|
});
|
|
843
|
+
outcome.catch(() => void 0);
|
|
847
844
|
recordFrame({
|
|
848
845
|
direction: "sent",
|
|
849
846
|
frame
|
|
@@ -853,7 +850,6 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
853
850
|
await transmit(frame, targetDevice !== void 0, targetDevice);
|
|
854
851
|
} catch (error) {
|
|
855
852
|
pendingManageRequests.delete(requestId);
|
|
856
|
-
outcome.catch(() => void 0);
|
|
857
853
|
throw error;
|
|
858
854
|
}
|
|
859
855
|
emit();
|