wire-mesh-core 3.11.0 → 4.0.1
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/domain/coordinator-election.cjs +12 -3
- package/dist/domain/coordinator-election.d.cts +7 -3
- package/dist/domain/coordinator-election.d.mts +7 -3
- package/dist/domain/coordinator-election.mjs +12 -4
- package/dist/domain/mesh-session.cjs +33 -18
- package/dist/domain/mesh-session.d.cts +10 -1
- package/dist/domain/mesh-session.d.mts +10 -1
- package/dist/domain/mesh-session.mjs +33 -19
- package/package.json +1 -1
|
@@ -11,6 +11,10 @@ function compareDeviceIds(a, b) {
|
|
|
11
11
|
}
|
|
12
12
|
return a.length - b.length;
|
|
13
13
|
}
|
|
14
|
+
/** Whether a claim at this term can be superseded by a later claim: a non-negative safe integer strictly below Number.MAX_SAFE_INTEGER, so that term + 1 is still an exactly represented, strictly greater integer. Above that ceiling a double cannot count on (2 ** 53 + 1 === 2 ** 53), and an incumbent there could never be superseded, so a device that named itself holder would keep the role for good even after it had gone. */
|
|
15
|
+
function isSupersedableTerm(term) {
|
|
16
|
+
return Number.isSafeInteger(term) && term >= 0 && term < Number.MAX_SAFE_INTEGER;
|
|
17
|
+
}
|
|
14
18
|
var CoordinatorElection = class {
|
|
15
19
|
ownDevice;
|
|
16
20
|
incumbent;
|
|
@@ -26,12 +30,15 @@ var CoordinatorElection = class {
|
|
|
26
30
|
return this.incumbent !== void 0 && compareDeviceIds(this.incumbent.coordinator, this.ownDevice) === 0;
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
|
-
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
33
|
+
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Throws a RangeError when that term is not one peers accept (isSupersedableTerm), which only happens when the incumbent already sits at the highest term the election accepts. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
30
34
|
*/
|
|
31
35
|
claim(capacityHint) {
|
|
36
|
+
const lastTerm = this.incumbent?.term ?? -1;
|
|
37
|
+
const term = lastTerm + 1;
|
|
38
|
+
if (!isSupersedableTerm(term)) throw new RangeError(`the incumbent's term ${String(lastTerm)} leaves no term a claim can be raised to`);
|
|
32
39
|
const frame = {
|
|
33
40
|
type: "coordinator",
|
|
34
|
-
term
|
|
41
|
+
term,
|
|
35
42
|
coordinator: this.ownDevice,
|
|
36
43
|
...capacityHint !== void 0 ? { "capacity-hint": capacityHint } : {}
|
|
37
44
|
};
|
|
@@ -55,9 +62,10 @@ var CoordinatorElection = class {
|
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
|
-
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it).
|
|
65
|
+
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it); "rejected" means the claim's term is not supersedable (isSupersedableTerm), so it is dropped unevaluated and the caller should neither gossip nor answer it.
|
|
59
66
|
*/
|
|
60
67
|
evaluate(frame) {
|
|
68
|
+
if (!isSupersedableTerm(frame.term)) return { outcome: "rejected" };
|
|
61
69
|
const previous = this.incumbent;
|
|
62
70
|
if (previous === void 0 || frame.term > previous.term || frame.term === previous.term && compareDeviceIds(frame.coordinator, previous.coordinator) < 0) {
|
|
63
71
|
this.incumbent = {
|
|
@@ -79,3 +87,4 @@ var CoordinatorElection = class {
|
|
|
79
87
|
//#endregion
|
|
80
88
|
exports.CoordinatorElection = CoordinatorElection;
|
|
81
89
|
exports.compareDeviceIds = compareDeviceIds;
|
|
90
|
+
exports.isSupersedableTerm = isSupersedableTerm;
|
|
@@ -8,13 +8,17 @@ export interface CoordinatorClaim {
|
|
|
8
8
|
}
|
|
9
9
|
/** Bytewise "lowest device-id" comparison, the equal-term tiebreak the spec names: shorter is lower when one id is a prefix of the other, and equal ids compare equal. Device-ids are a fixed 32 bytes today, so the prefix branch is completeness rather than a live case. */
|
|
10
10
|
export declare function compareDeviceIds(a: DeviceId, b: DeviceId): number;
|
|
11
|
-
/**
|
|
11
|
+
/** Whether a claim at this term can be superseded by a later claim: a non-negative safe integer strictly below Number.MAX_SAFE_INTEGER, so that term + 1 is still an exactly represented, strictly greater integer. Above that ceiling a double cannot count on (2 ** 53 + 1 === 2 ** 53), and an incumbent there could never be superseded, so a device that named itself holder would keep the role for good even after it had gone. */
|
|
12
|
+
export declare function isSupersedableTerm(term: number): boolean;
|
|
13
|
+
/** What evaluating an incoming claim concluded, so the caller can react without re-deriving the comparison: "accepted" means this claim is now the incumbent (it superseded a previous one, or there was none), "retained" means the incumbent survived (the incoming claim was stale, or lost the equal-term tiebreak), and the incumbent is returned either way so a caller squashing a stale claim re-announces exactly what it already holds. "rejected" means the claim's term is not one a later claim can supersede (isSupersedableTerm), so it was never compared: the incumbent, if any, is untouched, and there is nothing to answer it with. */
|
|
12
14
|
export type EvaluationOutcome = {
|
|
13
15
|
outcome: "accepted";
|
|
14
16
|
incumbent: CoordinatorClaim;
|
|
15
17
|
} | {
|
|
16
18
|
outcome: "retained";
|
|
17
19
|
incumbent: CoordinatorClaim;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: "rejected";
|
|
18
22
|
};
|
|
19
23
|
export interface CoordinatorElectionOptions {
|
|
20
24
|
/** This device's own id: the coordinator named by a claim this side mints. */
|
|
@@ -29,7 +33,7 @@ export declare class CoordinatorElection {
|
|
|
29
33
|
/** Whether this side's own device is the incumbent. */
|
|
30
34
|
isSelf(): boolean;
|
|
31
35
|
/**
|
|
32
|
-
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
36
|
+
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Throws a RangeError when that term is not one peers accept (isSupersedableTerm), which only happens when the incumbent already sits at the highest term the election accepts. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
33
37
|
*/
|
|
34
38
|
claim(capacityHint?: number): CoordinatorFrame;
|
|
35
39
|
/**
|
|
@@ -37,7 +41,7 @@ export declare class CoordinatorElection {
|
|
|
37
41
|
*/
|
|
38
42
|
announceCurrent(): CoordinatorFrame | undefined;
|
|
39
43
|
/**
|
|
40
|
-
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it).
|
|
44
|
+
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it); "rejected" means the claim's term is not supersedable (isSupersedableTerm), so it is dropped unevaluated and the caller should neither gossip nor answer it.
|
|
41
45
|
*/
|
|
42
46
|
evaluate(frame: Readonly<CoordinatorFrame>): EvaluationOutcome;
|
|
43
47
|
}
|
|
@@ -8,13 +8,17 @@ export interface CoordinatorClaim {
|
|
|
8
8
|
}
|
|
9
9
|
/** Bytewise "lowest device-id" comparison, the equal-term tiebreak the spec names: shorter is lower when one id is a prefix of the other, and equal ids compare equal. Device-ids are a fixed 32 bytes today, so the prefix branch is completeness rather than a live case. */
|
|
10
10
|
export declare function compareDeviceIds(a: DeviceId, b: DeviceId): number;
|
|
11
|
-
/**
|
|
11
|
+
/** Whether a claim at this term can be superseded by a later claim: a non-negative safe integer strictly below Number.MAX_SAFE_INTEGER, so that term + 1 is still an exactly represented, strictly greater integer. Above that ceiling a double cannot count on (2 ** 53 + 1 === 2 ** 53), and an incumbent there could never be superseded, so a device that named itself holder would keep the role for good even after it had gone. */
|
|
12
|
+
export declare function isSupersedableTerm(term: number): boolean;
|
|
13
|
+
/** What evaluating an incoming claim concluded, so the caller can react without re-deriving the comparison: "accepted" means this claim is now the incumbent (it superseded a previous one, or there was none), "retained" means the incumbent survived (the incoming claim was stale, or lost the equal-term tiebreak), and the incumbent is returned either way so a caller squashing a stale claim re-announces exactly what it already holds. "rejected" means the claim's term is not one a later claim can supersede (isSupersedableTerm), so it was never compared: the incumbent, if any, is untouched, and there is nothing to answer it with. */
|
|
12
14
|
export type EvaluationOutcome = {
|
|
13
15
|
outcome: "accepted";
|
|
14
16
|
incumbent: CoordinatorClaim;
|
|
15
17
|
} | {
|
|
16
18
|
outcome: "retained";
|
|
17
19
|
incumbent: CoordinatorClaim;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: "rejected";
|
|
18
22
|
};
|
|
19
23
|
export interface CoordinatorElectionOptions {
|
|
20
24
|
/** This device's own id: the coordinator named by a claim this side mints. */
|
|
@@ -29,7 +33,7 @@ export declare class CoordinatorElection {
|
|
|
29
33
|
/** Whether this side's own device is the incumbent. */
|
|
30
34
|
isSelf(): boolean;
|
|
31
35
|
/**
|
|
32
|
-
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
36
|
+
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Throws a RangeError when that term is not one peers accept (isSupersedableTerm), which only happens when the incumbent already sits at the highest term the election accepts. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
33
37
|
*/
|
|
34
38
|
claim(capacityHint?: number): CoordinatorFrame;
|
|
35
39
|
/**
|
|
@@ -37,7 +41,7 @@ export declare class CoordinatorElection {
|
|
|
37
41
|
*/
|
|
38
42
|
announceCurrent(): CoordinatorFrame | undefined;
|
|
39
43
|
/**
|
|
40
|
-
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it).
|
|
44
|
+
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it); "rejected" means the claim's term is not supersedable (isSupersedableTerm), so it is dropped unevaluated and the caller should neither gossip nor answer it.
|
|
41
45
|
*/
|
|
42
46
|
evaluate(frame: Readonly<CoordinatorFrame>): EvaluationOutcome;
|
|
43
47
|
}
|
|
@@ -10,6 +10,10 @@ function compareDeviceIds(a, b) {
|
|
|
10
10
|
}
|
|
11
11
|
return a.length - b.length;
|
|
12
12
|
}
|
|
13
|
+
/** Whether a claim at this term can be superseded by a later claim: a non-negative safe integer strictly below Number.MAX_SAFE_INTEGER, so that term + 1 is still an exactly represented, strictly greater integer. Above that ceiling a double cannot count on (2 ** 53 + 1 === 2 ** 53), and an incumbent there could never be superseded, so a device that named itself holder would keep the role for good even after it had gone. */
|
|
14
|
+
function isSupersedableTerm(term) {
|
|
15
|
+
return Number.isSafeInteger(term) && term >= 0 && term < Number.MAX_SAFE_INTEGER;
|
|
16
|
+
}
|
|
13
17
|
var CoordinatorElection = class {
|
|
14
18
|
ownDevice;
|
|
15
19
|
incumbent;
|
|
@@ -25,12 +29,15 @@ var CoordinatorElection = class {
|
|
|
25
29
|
return this.incumbent !== void 0 && compareDeviceIds(this.incumbent.coordinator, this.ownDevice) === 0;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
|
-
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
32
|
+
* Claims the role for this side's own device at a term above every term seen so far (lastTerm + 1, so a first-ever claim is term 0), and returns the frame to gossip. Throws a RangeError when that term is not one peers accept (isSupersedableTerm), which only happens when the incumbent already sits at the highest term the election accepts. Claiming over a claim this side already holds is a deliberate takeover: it raises the term, which is exactly what a peer recovering the role after losing track of the mesh should do, and what a routine refresh should not (announceCurrent exists for that).
|
|
29
33
|
*/
|
|
30
34
|
claim(capacityHint) {
|
|
35
|
+
const lastTerm = this.incumbent?.term ?? -1;
|
|
36
|
+
const term = lastTerm + 1;
|
|
37
|
+
if (!isSupersedableTerm(term)) throw new RangeError(`the incumbent's term ${String(lastTerm)} leaves no term a claim can be raised to`);
|
|
31
38
|
const frame = {
|
|
32
39
|
type: "coordinator",
|
|
33
|
-
term
|
|
40
|
+
term,
|
|
34
41
|
coordinator: this.ownDevice,
|
|
35
42
|
...capacityHint !== void 0 ? { "capacity-hint": capacityHint } : {}
|
|
36
43
|
};
|
|
@@ -54,9 +61,10 @@ var CoordinatorElection = class {
|
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
|
-
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it).
|
|
64
|
+
* Evaluates an incoming claim against the one this side currently accepts: a strictly higher term always wins; an equal term breaks by lowest device-id, so an incoming claim naming a lower device-id than the incumbent's takes the role and one naming a higher device-id loses; a lower term never wins. "accepted" means the incumbent changed (the caller should gossip the new incumbent onward so the supersession propagates); "retained" means it did not (the caller may answer a stale claim by re-gossiping announceCurrent(), and should drop a lost equal-term claim it originated, since the tiebreak has settled it); "rejected" means the claim's term is not supersedable (isSupersedableTerm), so it is dropped unevaluated and the caller should neither gossip nor answer it.
|
|
58
65
|
*/
|
|
59
66
|
evaluate(frame) {
|
|
67
|
+
if (!isSupersedableTerm(frame.term)) return { outcome: "rejected" };
|
|
60
68
|
const previous = this.incumbent;
|
|
61
69
|
if (previous === void 0 || frame.term > previous.term || frame.term === previous.term && compareDeviceIds(frame.coordinator, previous.coordinator) < 0) {
|
|
62
70
|
this.incumbent = {
|
|
@@ -76,4 +84,4 @@ var CoordinatorElection = class {
|
|
|
76
84
|
}
|
|
77
85
|
};
|
|
78
86
|
//#endregion
|
|
79
|
-
export { CoordinatorElection, compareDeviceIds };
|
|
87
|
+
export { CoordinatorElection, compareDeviceIds, isSupersedableTerm };
|
|
@@ -285,6 +285,15 @@ function createRelayChannels(identity, sendHello) {
|
|
|
285
285
|
const MS_PER_SECOND = 1e3;
|
|
286
286
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
287
287
|
const HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
288
|
+
/**
|
|
289
|
+
* How many entries a session's frameLog keeps: the log exists for a console's
|
|
290
|
+
* recent-activity view, not as a session history, so it holds the most recent
|
|
291
|
+
* window of frames rather than every frame a long-lived session ever crossed.
|
|
292
|
+
* Without the bound, a busy session (a relay hub pairing, or any peer on a
|
|
293
|
+
* chatty mesh) retains every frame for its whole life, which on an
|
|
294
|
+
* always-on node grows without limit.
|
|
295
|
+
*/
|
|
296
|
+
const FRAME_LOG_WINDOW = 256;
|
|
288
297
|
function localHandshake(domains) {
|
|
289
298
|
return {
|
|
290
299
|
type: "handshake",
|
|
@@ -301,6 +310,11 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
301
310
|
let handshake = { status: "pending" };
|
|
302
311
|
const directory = /* @__PURE__ */ new Map();
|
|
303
312
|
const frameLog = [];
|
|
313
|
+
/** Keeps the frame log at its recent-activity window: newest entries at the end, the oldest dropped once the window is full. */
|
|
314
|
+
function recordFrame(entry) {
|
|
315
|
+
frameLog.push(entry);
|
|
316
|
+
if (frameLog.length > 256) frameLog.splice(0, frameLog.length - 256);
|
|
317
|
+
}
|
|
304
318
|
let feedCancelled = false;
|
|
305
319
|
const eventQueue = require_domain_async_queue.createAsyncQueue();
|
|
306
320
|
let handshakeTimer = null;
|
|
@@ -317,7 +331,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
317
331
|
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
318
332
|
const relayChannels = createRelayChannels(identity, async (peer, hello) => {
|
|
319
333
|
if (connection === null) throw new Error("not connected");
|
|
320
|
-
|
|
334
|
+
recordFrame({
|
|
321
335
|
direction: "sent",
|
|
322
336
|
frame: hello
|
|
323
337
|
});
|
|
@@ -399,7 +413,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
399
413
|
"request-id": requestId,
|
|
400
414
|
outcome
|
|
401
415
|
};
|
|
402
|
-
|
|
416
|
+
recordFrame({
|
|
403
417
|
direction: "sent",
|
|
404
418
|
frame: response
|
|
405
419
|
});
|
|
@@ -417,7 +431,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
417
431
|
if (frame.type === "relay-data") {
|
|
418
432
|
const inner = require_adapters_frame_codec.tryDecodeFrame(frame.payload);
|
|
419
433
|
if (inner?.type === "secure-hello") {
|
|
420
|
-
|
|
434
|
+
recordFrame({
|
|
421
435
|
direction: "received",
|
|
422
436
|
frame: inner
|
|
423
437
|
});
|
|
@@ -427,13 +441,13 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
427
441
|
if (inner?.type === "secure-data") {
|
|
428
442
|
const opened = await relayChannels.open(inner, frame["from-device"]);
|
|
429
443
|
if (opened?.frame.type === "manage-response") {
|
|
430
|
-
|
|
444
|
+
recordFrame({
|
|
431
445
|
direction: "received",
|
|
432
446
|
frame: opened.frame
|
|
433
447
|
});
|
|
434
448
|
applyManageResponse(opened.frame, opened.from);
|
|
435
449
|
} else if (opened !== void 0 && require_domain_hub_mailbox.isDataFrame(opened.frame)) {
|
|
436
|
-
|
|
450
|
+
recordFrame({
|
|
437
451
|
direction: "received",
|
|
438
452
|
frame: opened.frame
|
|
439
453
|
});
|
|
@@ -443,7 +457,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
443
457
|
...frame["to-device"] !== void 0 ? { toDevice: frame["to-device"] } : {}
|
|
444
458
|
});
|
|
445
459
|
} else if (opened?.frame.type === "manage-request") {
|
|
446
|
-
|
|
460
|
+
recordFrame({
|
|
447
461
|
direction: "received",
|
|
448
462
|
frame: opened.frame
|
|
449
463
|
});
|
|
@@ -454,13 +468,13 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
454
468
|
}
|
|
455
469
|
return;
|
|
456
470
|
}
|
|
457
|
-
|
|
471
|
+
recordFrame({
|
|
458
472
|
direction: "received",
|
|
459
473
|
frame
|
|
460
474
|
});
|
|
461
475
|
return;
|
|
462
476
|
}
|
|
463
|
-
|
|
477
|
+
recordFrame({
|
|
464
478
|
direction: "received",
|
|
465
479
|
frame
|
|
466
480
|
});
|
|
@@ -490,7 +504,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
490
504
|
type: "relay-connect",
|
|
491
505
|
"target-device": targetDevice
|
|
492
506
|
};
|
|
493
|
-
|
|
507
|
+
recordFrame({
|
|
494
508
|
direction: "sent",
|
|
495
509
|
frame: relayConnect
|
|
496
510
|
});
|
|
@@ -595,14 +609,14 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
595
609
|
address,
|
|
596
610
|
handshake
|
|
597
611
|
};
|
|
598
|
-
|
|
612
|
+
recordFrame({
|
|
599
613
|
direction: "sent",
|
|
600
614
|
frame: localHandshakeSent
|
|
601
615
|
});
|
|
602
616
|
await connection.send(localHandshakeSent);
|
|
603
617
|
emit();
|
|
604
618
|
const selfAdvert = await buildSelfAdvert();
|
|
605
|
-
|
|
619
|
+
recordFrame({
|
|
606
620
|
direction: "sent",
|
|
607
621
|
frame: selfAdvert
|
|
608
622
|
});
|
|
@@ -660,7 +674,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
660
674
|
async sendPing() {
|
|
661
675
|
const link = requireConnectedLink();
|
|
662
676
|
const ping = { type: "ping" };
|
|
663
|
-
|
|
677
|
+
recordFrame({
|
|
664
678
|
direction: "sent",
|
|
665
679
|
frame: ping
|
|
666
680
|
});
|
|
@@ -671,7 +685,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
671
685
|
const link = requireConnectedLink();
|
|
672
686
|
return pingRoundTrips.sendAndAwait(clock.now(), async () => {
|
|
673
687
|
const ping = { type: "ping" };
|
|
674
|
-
|
|
688
|
+
recordFrame({
|
|
675
689
|
direction: "sent",
|
|
676
690
|
frame: ping
|
|
677
691
|
});
|
|
@@ -694,7 +708,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
694
708
|
expectedFrom: targetDevice
|
|
695
709
|
});
|
|
696
710
|
});
|
|
697
|
-
|
|
711
|
+
recordFrame({
|
|
698
712
|
direction: "sent",
|
|
699
713
|
frame
|
|
700
714
|
});
|
|
@@ -731,7 +745,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
731
745
|
type: "revocation-announce",
|
|
732
746
|
entries: [...entries]
|
|
733
747
|
};
|
|
734
|
-
|
|
748
|
+
recordFrame({
|
|
735
749
|
direction: "sent",
|
|
736
750
|
frame
|
|
737
751
|
});
|
|
@@ -741,7 +755,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
741
755
|
async sendCoordinatorClaim(frame) {
|
|
742
756
|
requireConnectedLink();
|
|
743
757
|
const claim = { ...frame };
|
|
744
|
-
|
|
758
|
+
recordFrame({
|
|
745
759
|
direction: "sent",
|
|
746
760
|
frame: claim
|
|
747
761
|
});
|
|
@@ -751,7 +765,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
751
765
|
async sendGossipUpdate(extensions) {
|
|
752
766
|
requireConnectedLink();
|
|
753
767
|
const frame = await buildSelfAdvert(extensions);
|
|
754
|
-
|
|
768
|
+
recordFrame({
|
|
755
769
|
direction: "sent",
|
|
756
770
|
frame
|
|
757
771
|
});
|
|
@@ -764,7 +778,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
764
778
|
async sendDataFrame(frame, targetDevice) {
|
|
765
779
|
requireConnectedLink();
|
|
766
780
|
if (targetDevice !== void 0) await ensureRelayPairing(targetDevice);
|
|
767
|
-
|
|
781
|
+
recordFrame({
|
|
768
782
|
direction: "sent",
|
|
769
783
|
frame
|
|
770
784
|
});
|
|
@@ -821,6 +835,7 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
821
835
|
};
|
|
822
836
|
}
|
|
823
837
|
//#endregion
|
|
838
|
+
exports.FRAME_LOG_WINDOW = FRAME_LOG_WINDOW;
|
|
824
839
|
exports.HANDSHAKE_TIMEOUT_MS = HANDSHAKE_TIMEOUT_MS;
|
|
825
840
|
exports.acceptMeshSession = acceptMeshSession;
|
|
826
841
|
exports.createMeshSession = createMeshSession;
|
|
@@ -70,13 +70,22 @@ export interface SessionEvent {
|
|
|
70
70
|
state: ConnectionState;
|
|
71
71
|
/** The peer directory as of this event: latest peer-advert per device-id, in first-heard order. */
|
|
72
72
|
directory: readonly DirectoryEntry[];
|
|
73
|
-
/**
|
|
73
|
+
/** The most recent frames that crossed the connection, sent or received, in order: at most FRAME_LOG_WINDOW entries, the recent-activity window a console shows rather than a session history. */
|
|
74
74
|
frameLog: readonly FrameLogEntry[];
|
|
75
75
|
}
|
|
76
76
|
export interface DirectoryEntry {
|
|
77
77
|
device: DeviceId;
|
|
78
78
|
advert: PeerAdvert;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* How many entries a session's frameLog keeps: the log exists for a console's
|
|
82
|
+
* recent-activity view, not as a session history, so it holds the most recent
|
|
83
|
+
* window of frames rather than every frame a long-lived session ever crossed.
|
|
84
|
+
* Without the bound, a busy session (a relay hub pairing, or any peer on a
|
|
85
|
+
* chatty mesh) retains every frame for its whole life, which on an
|
|
86
|
+
* always-on node grows without limit.
|
|
87
|
+
*/
|
|
88
|
+
export declare const FRAME_LOG_WINDOW = 256;
|
|
80
89
|
export interface FrameLogEntry {
|
|
81
90
|
direction: "sent" | "received";
|
|
82
91
|
frame: Frame;
|
|
@@ -70,13 +70,22 @@ export interface SessionEvent {
|
|
|
70
70
|
state: ConnectionState;
|
|
71
71
|
/** The peer directory as of this event: latest peer-advert per device-id, in first-heard order. */
|
|
72
72
|
directory: readonly DirectoryEntry[];
|
|
73
|
-
/**
|
|
73
|
+
/** The most recent frames that crossed the connection, sent or received, in order: at most FRAME_LOG_WINDOW entries, the recent-activity window a console shows rather than a session history. */
|
|
74
74
|
frameLog: readonly FrameLogEntry[];
|
|
75
75
|
}
|
|
76
76
|
export interface DirectoryEntry {
|
|
77
77
|
device: DeviceId;
|
|
78
78
|
advert: PeerAdvert;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* How many entries a session's frameLog keeps: the log exists for a console's
|
|
82
|
+
* recent-activity view, not as a session history, so it holds the most recent
|
|
83
|
+
* window of frames rather than every frame a long-lived session ever crossed.
|
|
84
|
+
* Without the bound, a busy session (a relay hub pairing, or any peer on a
|
|
85
|
+
* chatty mesh) retains every frame for its whole life, which on an
|
|
86
|
+
* always-on node grows without limit.
|
|
87
|
+
*/
|
|
88
|
+
export declare const FRAME_LOG_WINDOW = 256;
|
|
80
89
|
export interface FrameLogEntry {
|
|
81
90
|
direction: "sent" | "received";
|
|
82
91
|
frame: Frame;
|
|
@@ -284,6 +284,15 @@ function createRelayChannels(identity, sendHello) {
|
|
|
284
284
|
const MS_PER_SECOND = 1e3;
|
|
285
285
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
286
286
|
const HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
287
|
+
/**
|
|
288
|
+
* How many entries a session's frameLog keeps: the log exists for a console's
|
|
289
|
+
* recent-activity view, not as a session history, so it holds the most recent
|
|
290
|
+
* window of frames rather than every frame a long-lived session ever crossed.
|
|
291
|
+
* Without the bound, a busy session (a relay hub pairing, or any peer on a
|
|
292
|
+
* chatty mesh) retains every frame for its whole life, which on an
|
|
293
|
+
* always-on node grows without limit.
|
|
294
|
+
*/
|
|
295
|
+
const FRAME_LOG_WINDOW = 256;
|
|
287
296
|
function localHandshake(domains) {
|
|
288
297
|
return {
|
|
289
298
|
type: "handshake",
|
|
@@ -300,6 +309,11 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
300
309
|
let handshake = { status: "pending" };
|
|
301
310
|
const directory = /* @__PURE__ */ new Map();
|
|
302
311
|
const frameLog = [];
|
|
312
|
+
/** Keeps the frame log at its recent-activity window: newest entries at the end, the oldest dropped once the window is full. */
|
|
313
|
+
function recordFrame(entry) {
|
|
314
|
+
frameLog.push(entry);
|
|
315
|
+
if (frameLog.length > 256) frameLog.splice(0, frameLog.length - 256);
|
|
316
|
+
}
|
|
303
317
|
let feedCancelled = false;
|
|
304
318
|
const eventQueue = createAsyncQueue();
|
|
305
319
|
let handshakeTimer = null;
|
|
@@ -316,7 +330,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
316
330
|
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
317
331
|
const relayChannels = createRelayChannels(identity, async (peer, hello) => {
|
|
318
332
|
if (connection === null) throw new Error("not connected");
|
|
319
|
-
|
|
333
|
+
recordFrame({
|
|
320
334
|
direction: "sent",
|
|
321
335
|
frame: hello
|
|
322
336
|
});
|
|
@@ -398,7 +412,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
398
412
|
"request-id": requestId,
|
|
399
413
|
outcome
|
|
400
414
|
};
|
|
401
|
-
|
|
415
|
+
recordFrame({
|
|
402
416
|
direction: "sent",
|
|
403
417
|
frame: response
|
|
404
418
|
});
|
|
@@ -416,7 +430,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
416
430
|
if (frame.type === "relay-data") {
|
|
417
431
|
const inner = tryDecodeFrame(frame.payload);
|
|
418
432
|
if (inner?.type === "secure-hello") {
|
|
419
|
-
|
|
433
|
+
recordFrame({
|
|
420
434
|
direction: "received",
|
|
421
435
|
frame: inner
|
|
422
436
|
});
|
|
@@ -426,13 +440,13 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
426
440
|
if (inner?.type === "secure-data") {
|
|
427
441
|
const opened = await relayChannels.open(inner, frame["from-device"]);
|
|
428
442
|
if (opened?.frame.type === "manage-response") {
|
|
429
|
-
|
|
443
|
+
recordFrame({
|
|
430
444
|
direction: "received",
|
|
431
445
|
frame: opened.frame
|
|
432
446
|
});
|
|
433
447
|
applyManageResponse(opened.frame, opened.from);
|
|
434
448
|
} else if (opened !== void 0 && isDataFrame(opened.frame)) {
|
|
435
|
-
|
|
449
|
+
recordFrame({
|
|
436
450
|
direction: "received",
|
|
437
451
|
frame: opened.frame
|
|
438
452
|
});
|
|
@@ -442,7 +456,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
442
456
|
...frame["to-device"] !== void 0 ? { toDevice: frame["to-device"] } : {}
|
|
443
457
|
});
|
|
444
458
|
} else if (opened?.frame.type === "manage-request") {
|
|
445
|
-
|
|
459
|
+
recordFrame({
|
|
446
460
|
direction: "received",
|
|
447
461
|
frame: opened.frame
|
|
448
462
|
});
|
|
@@ -453,13 +467,13 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
453
467
|
}
|
|
454
468
|
return;
|
|
455
469
|
}
|
|
456
|
-
|
|
470
|
+
recordFrame({
|
|
457
471
|
direction: "received",
|
|
458
472
|
frame
|
|
459
473
|
});
|
|
460
474
|
return;
|
|
461
475
|
}
|
|
462
|
-
|
|
476
|
+
recordFrame({
|
|
463
477
|
direction: "received",
|
|
464
478
|
frame
|
|
465
479
|
});
|
|
@@ -489,7 +503,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
489
503
|
type: "relay-connect",
|
|
490
504
|
"target-device": targetDevice
|
|
491
505
|
};
|
|
492
|
-
|
|
506
|
+
recordFrame({
|
|
493
507
|
direction: "sent",
|
|
494
508
|
frame: relayConnect
|
|
495
509
|
});
|
|
@@ -594,14 +608,14 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
594
608
|
address,
|
|
595
609
|
handshake
|
|
596
610
|
};
|
|
597
|
-
|
|
611
|
+
recordFrame({
|
|
598
612
|
direction: "sent",
|
|
599
613
|
frame: localHandshakeSent
|
|
600
614
|
});
|
|
601
615
|
await connection.send(localHandshakeSent);
|
|
602
616
|
emit();
|
|
603
617
|
const selfAdvert = await buildSelfAdvert();
|
|
604
|
-
|
|
618
|
+
recordFrame({
|
|
605
619
|
direction: "sent",
|
|
606
620
|
frame: selfAdvert
|
|
607
621
|
});
|
|
@@ -659,7 +673,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
659
673
|
async sendPing() {
|
|
660
674
|
const link = requireConnectedLink();
|
|
661
675
|
const ping = { type: "ping" };
|
|
662
|
-
|
|
676
|
+
recordFrame({
|
|
663
677
|
direction: "sent",
|
|
664
678
|
frame: ping
|
|
665
679
|
});
|
|
@@ -670,7 +684,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
670
684
|
const link = requireConnectedLink();
|
|
671
685
|
return pingRoundTrips.sendAndAwait(clock.now(), async () => {
|
|
672
686
|
const ping = { type: "ping" };
|
|
673
|
-
|
|
687
|
+
recordFrame({
|
|
674
688
|
direction: "sent",
|
|
675
689
|
frame: ping
|
|
676
690
|
});
|
|
@@ -693,7 +707,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
693
707
|
expectedFrom: targetDevice
|
|
694
708
|
});
|
|
695
709
|
});
|
|
696
|
-
|
|
710
|
+
recordFrame({
|
|
697
711
|
direction: "sent",
|
|
698
712
|
frame
|
|
699
713
|
});
|
|
@@ -730,7 +744,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
730
744
|
type: "revocation-announce",
|
|
731
745
|
entries: [...entries]
|
|
732
746
|
};
|
|
733
|
-
|
|
747
|
+
recordFrame({
|
|
734
748
|
direction: "sent",
|
|
735
749
|
frame
|
|
736
750
|
});
|
|
@@ -740,7 +754,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
740
754
|
async sendCoordinatorClaim(frame) {
|
|
741
755
|
requireConnectedLink();
|
|
742
756
|
const claim = { ...frame };
|
|
743
|
-
|
|
757
|
+
recordFrame({
|
|
744
758
|
direction: "sent",
|
|
745
759
|
frame: claim
|
|
746
760
|
});
|
|
@@ -750,7 +764,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
750
764
|
async sendGossipUpdate(extensions) {
|
|
751
765
|
requireConnectedLink();
|
|
752
766
|
const frame = await buildSelfAdvert(extensions);
|
|
753
|
-
|
|
767
|
+
recordFrame({
|
|
754
768
|
direction: "sent",
|
|
755
769
|
frame
|
|
756
770
|
});
|
|
@@ -763,7 +777,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
763
777
|
async sendDataFrame(frame, targetDevice) {
|
|
764
778
|
requireConnectedLink();
|
|
765
779
|
if (targetDevice !== void 0) await ensureRelayPairing(targetDevice);
|
|
766
|
-
|
|
780
|
+
recordFrame({
|
|
767
781
|
direction: "sent",
|
|
768
782
|
frame
|
|
769
783
|
});
|
|
@@ -820,4 +834,4 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
820
834
|
};
|
|
821
835
|
}
|
|
822
836
|
//#endregion
|
|
823
|
-
export { HANDSHAKE_TIMEOUT_MS, acceptMeshSession, createMeshSession };
|
|
837
|
+
export { FRAME_LOG_WINDOW, HANDSHAKE_TIMEOUT_MS, acceptMeshSession, createMeshSession };
|