@serfab/cadre-core 1.17.0 → 1.18.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 +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1176 -564
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1282 -584
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -17
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +89 -51
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory copy of one small control table's rows, kept until the table changes, so the
|
|
3
|
+
* membership, owner, peer-record and admission-chain reads that every flow repeats cost nothing
|
|
4
|
+
* after the first.
|
|
5
|
+
* `ControlDatabase` keeps one per cached table and drops it from its table-change subscription
|
|
6
|
+
* (`docs/architecture.md` → "Membership rows view"); this class holds only the caching rules, so
|
|
7
|
+
* they are testable without a database.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* How long installed rows are served before the next read reloads them, change or no change.
|
|
11
|
+
* The reload is the only path for a change that reaches this node with no storage event: a
|
|
12
|
+
* machine outside the block's cohort (a cadre larger than its replication breadth), or one in
|
|
13
|
+
* the cohort that the writer could not reach when it committed (one side of a NAT pair; the
|
|
14
|
+
* cohort auto-connect scenarios). Longer than Optimystic's read-repair window
|
|
15
|
+
* ({@link OPTIMYSTIC_READ_REPAIR_WINDOW_MS}), so the reload's storage read starts the cohort
|
|
16
|
+
* consult that brings such a change in instead of being served from the local copy, and
|
|
17
|
+
* shorter than any reconcile backstop (`DEFAULT_CONTROL_COHORT_RECONCILE_MS`, 2 minutes), so
|
|
18
|
+
* the backstop pass, the only reader on an idle party, reloads rather than serving kept rows.
|
|
19
|
+
* The reload rate on an idle party is therefore the pass rate, and the staleness bound for
|
|
20
|
+
* such a change is the backstop, not this value.
|
|
21
|
+
*
|
|
22
|
+
* A machine that was offline when a sibling committed is covered sooner than either:
|
|
23
|
+
* `CadreNode` drops every view when a sibling becomes reachable (its identify on a new
|
|
24
|
+
* connection; `ControlDatabase.invalidateMembershipViews`), so the pass that starts then reads
|
|
25
|
+
* storage.
|
|
26
|
+
*/
|
|
27
|
+
export const MEMBERSHIP_VIEW_MAX_AGE_MS = 15_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optimystic's lazy read-repair window (`clusterPolicy.readRepairWindowMs`, which cadre does
|
|
31
|
+
* not set, so this is Optimystic's default): a read of a held block checked less than this
|
|
32
|
+
* long ago is served from the local copy without asking the cohort. A read after it is
|
|
33
|
+
* served locally too, but starts a cohort consult whose landing raises a table change.
|
|
34
|
+
*
|
|
35
|
+
* NOTE: a copy, since db-p2p exports no default for it. If cadre ever sets
|
|
36
|
+
* `readRepairWindowMs` in its cluster policy, derive this from that value.
|
|
37
|
+
*/
|
|
38
|
+
const OPTIMYSTIC_READ_REPAIR_WINDOW_MS = 10_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long after a sibling becomes reachable (its identify on a new connection) `CadreNode`
|
|
42
|
+
* drops the views a second time: one read-repair window, plus a second for timer slop. The
|
|
43
|
+
* drop at the identify is not enough by itself. A node's reads before the sibling arrived
|
|
44
|
+
* (its start pass, with no cohort to ask) stamp the window, so the reload the identify
|
|
45
|
+
* starts is served from the local copy without a consult and the view would keep the
|
|
46
|
+
* pre-connection rows until {@link MEMBERSHIP_VIEW_MAX_AGE_MS}. The reload past the window
|
|
47
|
+
* starts the consults that bring in what the sibling committed while this node was away.
|
|
48
|
+
*/
|
|
49
|
+
export const MEMBERSHIP_VIEW_RECONNECT_RELOAD_MS = OPTIMYSTIC_READ_REPAIR_WINDOW_MS + 1_000;
|
|
50
|
+
|
|
51
|
+
/** One load's rows, and whether the view may keep them. */
|
|
52
|
+
export interface MembershipViewLoad<Row> {
|
|
53
|
+
rows: readonly Row[];
|
|
54
|
+
/**
|
|
55
|
+
* False when no later change event is guaranteed to reach the view for these rows (the
|
|
56
|
+
* read that produced them did not arm the table's change bridge), so they serve only the
|
|
57
|
+
* callers of this load.
|
|
58
|
+
*/
|
|
59
|
+
installable: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads the whole table. `retry` is the caller's: false from a caller holding the control
|
|
64
|
+
* write lock, whose read must not sleep a retry backoff.
|
|
65
|
+
*/
|
|
66
|
+
export type MembershipViewLoader<Row> = (retry: boolean) => Promise<MembershipViewLoad<Row>>;
|
|
67
|
+
|
|
68
|
+
export interface MembershipViewOptions {
|
|
69
|
+
/** Default {@link MEMBERSHIP_VIEW_MAX_AGE_MS}. */
|
|
70
|
+
maxAgeMs?: number;
|
|
71
|
+
/** Monotonic clock in milliseconds; default `performance.now`. */
|
|
72
|
+
now?: () => number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class MembershipView<Row> {
|
|
76
|
+
private rows: readonly Row[] | undefined;
|
|
77
|
+
/** When the load that produced {@link rows} started: the rows are no older than this. */
|
|
78
|
+
private loadedAt = 0;
|
|
79
|
+
/** Bumped by every {@link invalidate}; a load installs only if it is unchanged since the load began. */
|
|
80
|
+
private generation = 0;
|
|
81
|
+
/**
|
|
82
|
+
* The load each `retry` flag's callers share, current generation only. Separate per flag
|
|
83
|
+
* so a caller under the write lock never awaits a retrying load's backoff sleep.
|
|
84
|
+
*/
|
|
85
|
+
private readonly inFlight = new Map<boolean, Promise<readonly Row[]>>();
|
|
86
|
+
private readonly maxAgeMs: number;
|
|
87
|
+
private readonly now: () => number;
|
|
88
|
+
|
|
89
|
+
constructor(private readonly loader: MembershipViewLoader<Row>, options: MembershipViewOptions = {}) {
|
|
90
|
+
this.maxAgeMs = options.maxAgeMs ?? MEMBERSHIP_VIEW_MAX_AGE_MS;
|
|
91
|
+
this.now = options.now ?? (() => performance.now());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The table's rows: the installed copy while it is younger than the maximum age, otherwise a
|
|
96
|
+
* load, shared with every caller of the same `retry` flag that arrives while it runs. A load
|
|
97
|
+
* that raced an {@link invalidate} answers its own callers with what it read, as an uncached
|
|
98
|
+
* read would have, and keeps nothing. A failed load keeps nothing and rejects.
|
|
99
|
+
*/
|
|
100
|
+
read(retry: boolean): Promise<readonly Row[]> {
|
|
101
|
+
if (this.rows !== undefined && this.now() - this.loadedAt < this.maxAgeMs) {
|
|
102
|
+
return Promise.resolve(this.rows);
|
|
103
|
+
}
|
|
104
|
+
return this.inFlight.get(retry) ?? this.startLoad(retry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Read the table without serving, joining or keeping anything. */
|
|
108
|
+
async readThrough(retry: boolean): Promise<readonly Row[]> {
|
|
109
|
+
return (await this.loader(retry)).rows;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Drop the installed rows and every load in flight, so the next read loads afresh and no
|
|
114
|
+
* earlier load installs. Synchronous and flag-setting only: the table-change handler calls
|
|
115
|
+
* it inside the committing call.
|
|
116
|
+
*/
|
|
117
|
+
invalidate(): void {
|
|
118
|
+
this.generation++;
|
|
119
|
+
this.rows = undefined;
|
|
120
|
+
this.inFlight.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private startLoad(retry: boolean): Promise<readonly Row[]> {
|
|
124
|
+
const load = this.load(retry, this.generation, this.now());
|
|
125
|
+
this.inFlight.set(retry, load);
|
|
126
|
+
const forget = (): void => {
|
|
127
|
+
if (this.inFlight.get(retry) === load) {
|
|
128
|
+
this.inFlight.delete(retry);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// Both arms handled, so a failed load rejects only for the callers awaiting it.
|
|
132
|
+
void load.then(forget, forget);
|
|
133
|
+
return load;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async load(retry: boolean, generation: number, startedAt: number): Promise<readonly Row[]> {
|
|
137
|
+
const { rows, installable } = await this.loader(retry);
|
|
138
|
+
if (installable && generation === this.generation) {
|
|
139
|
+
this.rows = rows;
|
|
140
|
+
this.loadedAt = startedAt;
|
|
141
|
+
}
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -368,7 +368,7 @@ export function verifyOwnerGrantReply(
|
|
|
368
368
|
// ── Member side (handler) ────────────────────────────────────────────────────
|
|
369
369
|
|
|
370
370
|
/** The control-database surface a redemption needs; a `ControlDatabase` satisfies it. */
|
|
371
|
-
export type OwnerGrantStore = Pick<ControlDatabase, 'seatOwnerGrant' | 'isOwnerGrantLive' | 'redeemOwnerGrant'>;
|
|
371
|
+
export type OwnerGrantStore = Pick<ControlDatabase, 'queryOwnerGrant' | 'seatOwnerGrant' | 'isOwnerGrantLive' | 'redeemOwnerGrant'>;
|
|
372
372
|
|
|
373
373
|
export interface OwnerGrantHandlerOptions extends MemberRedemptionHandlerOptions {
|
|
374
374
|
store: OwnerGrantStore;
|
|
@@ -385,10 +385,11 @@ const REDEEM_CONSTRAINT_CODES: ReadonlyArray<ConstraintRefusalRow<OwnerGrantReje
|
|
|
385
385
|
* Member side of the grant protocol (`MemberRedemptionHandler` has the shape): after the
|
|
386
386
|
* request's shape, it checks the party and both signatures — nothing about the connecting
|
|
387
387
|
* peer, whose identity is unrelated to the key it seats — then seats the row from the bundle
|
|
388
|
-
* and redeems. An issuer this member does not know as an owner is
|
|
389
|
-
* left: the holder tries the next address, which may hold the
|
|
388
|
+
* when this member holds none and redeems. An issuer this member does not know as an owner is
|
|
389
|
+
* the one retryable refusal left: the holder tries the next address, which may hold the
|
|
390
|
+
* issuer's row.
|
|
390
391
|
*/
|
|
391
|
-
export class OwnerGrantHandler extends MemberRedemptionHandler<OwnerGrantRedeemRequest, OwnerGrantRedeemReply> {
|
|
392
|
+
export class OwnerGrantHandler extends MemberRedemptionHandler<OwnerGrantRedeemRequest, OwnerGrantRedeemReply, OwnerGrantRow> {
|
|
392
393
|
private readonly store: OwnerGrantStore;
|
|
393
394
|
protected readonly busyRefusal = rejection('busy', 'Too many concurrent redemptions');
|
|
394
395
|
protected readonly internalRefusal = rejection('internal', 'Internal redemption error');
|
|
@@ -414,25 +415,25 @@ export class OwnerGrantHandler extends MemberRedemptionHandler<OwnerGrantRedeemR
|
|
|
414
415
|
return null;
|
|
415
416
|
}
|
|
416
417
|
|
|
417
|
-
protected
|
|
418
|
-
return this.store.
|
|
418
|
+
protected readHeld(request: OwnerGrantRedeemRequest): Promise<OwnerGrantRow | null> {
|
|
419
|
+
return this.store.queryOwnerGrant(request.grant.key);
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return constraintRefusal(error, REDEEM_CONSTRAINT_CODES) ?? writeFailureRefusal(error);
|
|
422
|
+
protected isLive(request: OwnerGrantRedeemRequest, held: OwnerGrantRow | null): Promise<boolean> {
|
|
423
|
+
return this.store.isOwnerGrantLive(request.grant, held);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The two writes — the seat only when this member holds no row, and the redemption, carrying the held row — as `CadreInviteHandler.redeemAdmitted`. */
|
|
427
|
+
protected async redeemAdmitted(request: OwnerGrantRedeemRequest, remotePeerId: string, held: OwnerGrantRow | null): Promise<OwnerGrantRedeemReply> {
|
|
428
|
+
if (held === null) {
|
|
429
|
+
const refused = await this.seat(request, remotePeerId);
|
|
430
|
+
if (refused) return refused;
|
|
431
431
|
}
|
|
432
432
|
let alreadyOwner: boolean;
|
|
433
433
|
try {
|
|
434
434
|
const result = await this.store.redeemOwnerGrant({
|
|
435
435
|
grantKey: request.grant.key,
|
|
436
|
+
...(held ? { held } : {}),
|
|
436
437
|
seatedKey: request.ownerKey,
|
|
437
438
|
usageStampId: request.usageStampId,
|
|
438
439
|
grantSig: request.grantSig,
|
|
@@ -447,8 +448,22 @@ export class OwnerGrantHandler extends MemberRedemptionHandler<OwnerGrantRedeemR
|
|
|
447
448
|
log('redeeming grant %s for %s failed: %o', request.grant.key, request.ownerKey, error);
|
|
448
449
|
return constraintRefusal(error, REDEEM_CONSTRAINT_CODES) ?? writeFailureRefusal(error);
|
|
449
450
|
}
|
|
450
|
-
// The row as held: the
|
|
451
|
-
return { accepted: true, partyId: this.partyId, grant: request.grant, alreadyOwner };
|
|
451
|
+
// The row as held: read before the write, or the bundle's copy the seat inserted verbatim or found.
|
|
452
|
+
return { accepted: true, partyId: this.partyId, grant: held ?? request.grant, alreadyOwner };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Seat the bundle's copy of the row; a refusal answered by name, or null once it is held. */
|
|
456
|
+
private async seat(request: OwnerGrantRedeemRequest, remotePeerId: string): Promise<OwnerGrantRejection | null> {
|
|
457
|
+
try {
|
|
458
|
+
await this.store.seatOwnerGrant(request.grant);
|
|
459
|
+
return null;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error instanceof OwnerGrantIssuerUnknownError) {
|
|
462
|
+
return rejection('issuer-unknown', 'The grant\'s issuer is not known as an owner here');
|
|
463
|
+
}
|
|
464
|
+
log('seating grant %s from %s failed: %o', request.grant.key, remotePeerId, error);
|
|
465
|
+
return constraintRefusal(error, REDEEM_CONSTRAINT_CODES) ?? writeFailureRefusal(error);
|
|
466
|
+
}
|
|
452
467
|
}
|
|
453
468
|
}
|
|
454
469
|
|
package/src/peer-addr-book.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* — Optimystic's cluster client, repo client, FRET ping/announce, all funnelling
|
|
5
5
|
* into `libp2p.dialProtocol(peerId, …)` — has an address to use for a sibling
|
|
6
6
|
* whose connection has dropped, instead of failing with `NoValidAddressesError`
|
|
7
|
-
* until the
|
|
7
|
+
* until the reconcile pass the drop starts re-dials it.
|
|
8
8
|
*
|
|
9
9
|
* Shared by the control-cohort reconcile pass and the strand one (both in
|
|
10
10
|
* `CadreNode`): each resolves a peer's addresses its own way and then needs
|
|
@@ -190,9 +190,10 @@ function lastAddressedPeerId(parsed: Multiaddr): string | null {
|
|
|
190
190
|
* that id is the address's FIRST `/p2p/` component (`dedupe-addresses.js`), so a
|
|
191
191
|
* plain address round-trips without its suffix while a relayed one — whose first
|
|
192
192
|
* `/p2p/` names the relay — keeps it. Comparing and de-duplicating on this
|
|
193
|
-
* suffix-stripped form matches either shape.
|
|
193
|
+
* suffix-stripped form matches either shape. `CadreNode.claimNode` compares a QR
|
|
194
|
+
* code's addresses with a connection's remote address on it for the same reason.
|
|
194
195
|
*/
|
|
195
|
-
function addrKey(addr: string, peerId: PeerId): string {
|
|
196
|
+
export function addrKey(addr: string, peerId: PeerId): string {
|
|
196
197
|
const suffix = `/p2p/${peerId.toString()}`;
|
|
197
198
|
return addr.endsWith(suffix) ? addr.slice(0, -suffix.length) : addr;
|
|
198
199
|
}
|