wire-mesh-core 3.11.0 → 4.0.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.
@@ -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: (this.incumbent?.term ?? -1) + 1,
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
- /** 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. */
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
- /** 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. */
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: (this.incumbent?.term ?? -1) + 1,
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "3.11.0",
3
+ "version": "4.0.0",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",