@serfab/cadre-core 1.16.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 +4 -3
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +81 -142
- package/dist/cadre-invite-protocol.js +168 -458
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +1089 -475
- package/dist/cadre-node.js +2170 -1037
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +88 -32
- package/dist/control-cohort.js +91 -33
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +856 -318
- package/dist/control-database.js +1422 -701
- 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 +11 -4
- package/dist/control-protocol-guard.js +20 -5
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +97 -0
- package/dist/control-schema-version.js +225 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +262 -78
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +45 -10
- package/dist/control-write-retry.js +92 -22
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- 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 +27 -17
- package/dist/index.js +39 -17
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +29 -12
- package/dist/joined-strand-store.js +66 -27
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-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/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +256 -0
- package/dist/member-redemption.js +490 -0
- package/dist/member-redemption.js.map +1 -0
- 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-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +251 -0
- package/dist/owner-grant-protocol.js +344 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- 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-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.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/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +162 -133
- package/dist/seed-bootstrap.js +300 -283
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +24 -2
- package/dist/strand-database.js +29 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -88
- package/dist/strand-first-sync-gate.js +288 -121
- 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 +27 -18
- 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 +45 -22
- package/dist/strand-membership-writer.js +226 -131
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +36 -32
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.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 +374 -150
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +17 -8
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +740 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +2402 -1079
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +204 -128
- package/src/control-database.ts +1749 -820
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +23 -6
- package/src/control-schema-version.ts +301 -0
- package/src/control-schema.ts +273 -78
- package/src/control-write-retry.ts +641 -567
- package/src/delegate-admission.ts +351 -314
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +899 -789
- package/src/joined-strand-store.ts +73 -28
- package/src/key-store.ts +10 -4
- package/src/link-budget.ts +9 -7
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +649 -0
- package/src/membership-view.ts +144 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +547 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-authorization.ts +558 -488
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +343 -349
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +352 -319
- package/src/strand-first-sync-gate.ts +334 -133
- 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 -1974
- package/src/strand-move.ts +50 -38
- package/src/strand-peer-redial.ts +2 -3
- 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 +391 -152
|
@@ -59,6 +59,8 @@ export const CONTROL_TABLES = [
|
|
|
59
59
|
'FormationUsage',
|
|
60
60
|
'CadreInvite',
|
|
61
61
|
'CadreInviteUsage',
|
|
62
|
+
'OwnerGrant',
|
|
63
|
+
'OwnerGrantUsage',
|
|
62
64
|
'Revocation',
|
|
63
65
|
] as const;
|
|
64
66
|
|
|
@@ -76,17 +78,17 @@ export type ControlTable = typeof CONTROL_TABLES[number];
|
|
|
76
78
|
* {@link ControlTable} rather than a fresh literal list, so a renamed table is a
|
|
77
79
|
* compile error here instead of a silently dead branch.
|
|
78
80
|
*
|
|
79
|
-
* `CadreInvite`
|
|
80
|
-
* withdrawal over a row that stays (`NotRevoked` refuses re-seating
|
|
81
|
-
* row; there is no `RevocationRecorded`, since nothing is ever removed), so
|
|
82
|
-
* revocable without being removable — `control-database.ts`'s `RemovableTable`
|
|
83
|
-
* excludes
|
|
81
|
+
* `CadreInvite` and `OwnerGrant` are the two members whose rows are never deleted: a
|
|
82
|
+
* tombstone there is a withdrawal over a row that stays (`NotRevoked` refuses re-seating
|
|
83
|
+
* the withdrawn row; there is no `RevocationRecorded`, since nothing is ever removed), so
|
|
84
|
+
* they are revocable without being removable — `control-database.ts`'s `RemovableTable`
|
|
85
|
+
* excludes them.
|
|
84
86
|
*
|
|
85
87
|
* Lives in this import-free module rather than beside its main consumer
|
|
86
88
|
* (`control-database.ts`) so the lightweight signers — `peer-authorization.ts`'s
|
|
87
89
|
* `revocationDigest` — can type against it without pulling in the runtime.
|
|
88
90
|
*/
|
|
89
|
-
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' | 'JoinRequest' | 'DeviceToken' | 'CadreInvite'>;
|
|
91
|
+
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' | 'JoinRequest' | 'DeviceToken' | 'CadreInvite' | 'OwnerGrant'>;
|
|
90
92
|
|
|
91
93
|
/**
|
|
92
94
|
* What a signature authorizes, scoped to one table rule — or, for
|
|
@@ -122,10 +124,11 @@ export type BoundControlDomain = Exclude<ControlDomain, UnboundControlDomain>;
|
|
|
122
124
|
* owner key.
|
|
123
125
|
* - `'consent'` — a peer self-signs its OWN `FormationUsage` or
|
|
124
126
|
* `CadreInviteUsage` redemption (the joiner proving it agreed to join), with
|
|
125
|
-
* its own key
|
|
126
|
-
* the
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* its own key, or the key an `OwnerGrantUsage` seats signs that it is held.
|
|
128
|
+
* Distinct from the approver's `'vouch'` over the same table so the two
|
|
129
|
+
* stored signatures are never interchangeable.
|
|
130
|
+
* - `'redeem'` — the holder of a `CadreInvite`'s or `OwnerGrant`'s private key
|
|
131
|
+
* proves possession on the usage row. Distinct from the `'consent'` over the
|
|
129
132
|
* same fields: the two signatures are by different keys and neither may stand
|
|
130
133
|
* in for the other.
|
|
131
134
|
* - `'reissue'` — an owner re-writes an existing `Revocation` tombstone,
|
|
@@ -145,7 +148,6 @@ export type ControlAction = 'add' | 'remove' | 'vouch' | 'publish' | 'consent' |
|
|
|
145
148
|
export interface CadreInviteSignedFields {
|
|
146
149
|
key: string;
|
|
147
150
|
peerId: string | null;
|
|
148
|
-
grantsOwner: boolean;
|
|
149
151
|
expiresAt: string | null;
|
|
150
152
|
totalUses: number | null;
|
|
151
153
|
stampId: string;
|
|
@@ -156,14 +158,45 @@ export interface CadreInviteSignedFields {
|
|
|
156
158
|
* (`control-database.ts`'s `cadreInviteAddMessage`, raw bytes) and the verifier
|
|
157
159
|
* (`peer-authorization.ts`'s `cadreInviteAddDigest`, base64url) so the two cannot
|
|
158
160
|
* drift. SQL mirror, in `CadreInvite.AuthorizedInsert`:
|
|
159
|
-
* `new.Key, coalesce(new.PeerId, ''), cast(new.
|
|
160
|
-
* coalesce(cast(new.
|
|
161
|
+
* `new.Key, coalesce(new.PeerId, ''), coalesce(cast(new.ExpiresAt as text), ''),
|
|
162
|
+
* coalesce(cast(new.TotalUses as text), ''), new.StampId`.
|
|
161
163
|
*/
|
|
162
164
|
export function cadreInviteRowFields(row: CadreInviteSignedFields): string[] {
|
|
163
165
|
return [
|
|
164
166
|
row.key,
|
|
165
167
|
row.peerId ?? '',
|
|
166
|
-
row.
|
|
168
|
+
row.expiresAt ?? '',
|
|
169
|
+
row.totalUses === null ? '' : String(row.totalUses),
|
|
170
|
+
row.stampId,
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The `OwnerGrant` columns its `'add'` signature binds, in the schema's order and with
|
|
176
|
+
* the row's stored forms (`expiresAt` canonical, as {@link CadreInviteSignedFields}).
|
|
177
|
+
* `label` is the label the seated `OwnerKey` row must carry: the issuer's choice, bound
|
|
178
|
+
* here so the holder cannot seat the key under another name. Structurally satisfied by
|
|
179
|
+
* an `OwnerGrantRow` (`types.ts`), which adds the issuer pair the signature does not cover.
|
|
180
|
+
*/
|
|
181
|
+
export interface OwnerGrantSignedFields {
|
|
182
|
+
key: string;
|
|
183
|
+
label: string | null;
|
|
184
|
+
expiresAt: string | null;
|
|
185
|
+
totalUses: number | null;
|
|
186
|
+
stampId: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The row-field half of an `OwnerGrant` `'add'` digest, shared by the signer
|
|
191
|
+
* (`control-database.ts`'s `ownerGrantAddMessage`) and the verifier
|
|
192
|
+
* (`peer-authorization.ts`'s `ownerGrantAddDigest`), as {@link cadreInviteRowFields} is.
|
|
193
|
+
* SQL mirror, in `OwnerGrant.AuthorizedInsert`: `new.Key, coalesce(new.Label, ''),
|
|
194
|
+
* coalesce(cast(new.ExpiresAt as text), ''), coalesce(cast(new.TotalUses as text), ''), new.StampId`.
|
|
195
|
+
*/
|
|
196
|
+
export function ownerGrantRowFields(row: OwnerGrantSignedFields): string[] {
|
|
197
|
+
return [
|
|
198
|
+
row.key,
|
|
199
|
+
row.label ?? '',
|
|
167
200
|
row.expiresAt ?? '',
|
|
168
201
|
row.totalUses === null ? '' : String(row.totalUses),
|
|
169
202
|
row.stampId,
|
package/src/control-cohort.ts
CHANGED
|
@@ -1,128 +1,204 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Control-cohort dial selection: the pure policy behind
|
|
3
|
-
* {@link CadreNode.reconcileControlCohort}. Decides WHICH known cadre siblings a
|
|
4
|
-
* node proactively dials each pass to keep the `CadreControl` collections' cohort
|
|
5
|
-
* connected (and thus replicating), without devolving into an N² mesh.
|
|
6
|
-
*
|
|
7
|
-
* FRET only needs a *connected* graph with the block clusters reachable, not a
|
|
8
|
-
* full mesh.
|
|
9
|
-
* that hold the control blocks, so the policy is **backbone-preferential
|
|
10
|
-
* bounded out-degree**:
|
|
11
|
-
*
|
|
12
|
-
* - **Always dial every backbone (
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* For a small party (members ≤ degree) this degenerates to a full mesh, which is
|
|
20
|
-
* correct, cheap, and matches what the convergence tests do by hand; for a large
|
|
21
|
-
* party it bounds out-degree to `backbone + targetDegree`.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Control-cohort dial selection: the pure policy behind
|
|
3
|
+
* {@link CadreNode.reconcileControlCohort}. Decides WHICH known cadre siblings a
|
|
4
|
+
* node proactively dials each pass to keep the `CadreControl` collections' cohort
|
|
5
|
+
* connected (and thus replicating), without devolving into an N² mesh.
|
|
6
|
+
*
|
|
7
|
+
* FRET only needs a *connected* graph with the block clusters reachable, not a
|
|
8
|
+
* full mesh. Always-on (`storage` profile) machines are the stable, dialable
|
|
9
|
+
* backbone that hold the control blocks, so the policy is **backbone-preferential
|
|
10
|
+
* with a bounded out-degree**:
|
|
11
|
+
*
|
|
12
|
+
* - **Always dial every backbone (always-on) member** — a party runs few of them,
|
|
13
|
+
* and they are the machines that stay reachable while phones sleep or roam.
|
|
14
|
+
* - **Fill the remainder** up to `targetDegree` with the other members (phones,
|
|
15
|
+
* and machines that have not yet published a profile), ordered
|
|
16
|
+
* deterministically (peerId sort) so every node makes the same stable choice
|
|
17
|
+
* across passes and load spreads predictably.
|
|
18
|
+
*
|
|
19
|
+
* For a small party (members ≤ degree) this degenerates to a full mesh, which is
|
|
20
|
+
* correct, cheap, and matches what the convergence tests do by hand; for a large
|
|
21
|
+
* party it bounds out-degree to `backbone + targetDegree`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { CohortPeerRow } from './strand-cohort.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default backstop for {@link CadreNode.reconcileControlCohort} (ms): a pass runs this long
|
|
28
|
+
* after the last one settled whether or not anything woke it. Passes normally run on a
|
|
29
|
+
* change — a membership table changing, a sibling connecting or disconnecting, an address
|
|
30
|
+
* change — and on the retries a pass arms for itself ({@link nextReconcileWakeMs}); the
|
|
31
|
+
* backstop bounds how stale this node can be after a change it hears nothing of: one that
|
|
32
|
+
* reaches it with no storage event, which happens to a machine outside the block's cohort
|
|
33
|
+
* (a cadre larger than `CONTROL_REPLICATION_BREADTH`) and to one in the cohort that the
|
|
34
|
+
* writer could not reach when it committed. The membership rows view's maximum age
|
|
35
|
+
* (`MEMBERSHIP_VIEW_MAX_AGE_MS`, 15 s) is shorter than this, so the backstop pass reads
|
|
36
|
+
* storage and starts the cohort consult that brings such a change in.
|
|
37
|
+
*
|
|
38
|
+
* `network.controlCohort.reconcileMs` overrides it. An interval between a pass settling and
|
|
39
|
+
* the next start, not a deadline on a pass: passes are single-flight, and one unreachable
|
|
40
|
+
* sibling's dial alone can take the whole per-peer dial budget (86 s at the default
|
|
41
|
+
* declared link).
|
|
42
|
+
*
|
|
43
|
+
* NOTE: a machine whose storage takes no event for a change — one outside the block's cohort
|
|
44
|
+
* in a cadre larger than `CONTROL_REPLICATION_BREADTH` (16 machines), or one the writer could
|
|
45
|
+
* not reach when it committed — sees it only at this backstop, where the change-driven path
|
|
46
|
+
* serves a machine the commit reached within the 50 ms coalescing window. If a party ever
|
|
47
|
+
* needs a tighter bound, lower the backstop for its nodes or give the plugin's change bridge
|
|
48
|
+
* a path for outside-the-cohort reads.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 120_000;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* First redial delay after a pass's dial of a selected sibling, or a cold-start bootstrap peer,
|
|
54
|
+
* fails (ms). Doubles per consecutive such pass and is capped at the backstop, so a
|
|
55
|
+
* sibling that is offline costs one dial per backstop once the ladder saturates, and a
|
|
56
|
+
* stranded cold-start node keeps trying forever (15 s, 30 s, 60 s, then every backstop).
|
|
57
|
+
*/
|
|
58
|
+
export const CONTROL_COHORT_REDIAL_MS = 15_000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How often an owner retries a node it has just claimed while that node restarts into the
|
|
62
|
+
* owner's party (ms; `CadreNode.claimNode`). A poll step, not a deadline: the retries run for
|
|
63
|
+
* {@link CONTROL_COHORT_REDIAL_MS} after the node accepts the claim, the delay of the reconcile
|
|
64
|
+
* pass's first redial, which would otherwise be the owner's next dial of the node.
|
|
65
|
+
*/
|
|
66
|
+
export const CLAIM_RESTART_REDIAL_STEP_MS = 1_000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default cap on the number of **non-backbone** members a single reconcile pass
|
|
70
|
+
* proactively dials. Backbone (always-on) members are always dialed and do NOT
|
|
71
|
+
* count against this cap.
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
|
|
74
|
+
|
|
75
|
+
// The per-sibling and per-address dial time limits live in `peer-dial.ts`
|
|
76
|
+
// (`DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS`,
|
|
77
|
+
// `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS`), beside the dial they
|
|
78
|
+
// bound, which `seed-bootstrap.ts` also uses and cannot import from here.
|
|
79
|
+
|
|
80
|
+
/** Outcome of {@link selectControlCohortDials}: who to dial + what the cap dropped. */
|
|
81
|
+
export interface ControlCohortSelection {
|
|
82
|
+
/**
|
|
83
|
+
* Siblings to dial this pass, backbone first then the bounded non-backbone
|
|
84
|
+
* fill. Already self-excluded (the caller must pass self-excluded `siblings`).
|
|
85
|
+
* Still includes peers that may turn out to be already-connected — the caller
|
|
86
|
+
* diffs against live connections separately.
|
|
87
|
+
*/
|
|
88
|
+
dials: CohortPeerRow[];
|
|
89
|
+
/** Non-backbone members dropped by the `targetDegree` cap (0 when none). */
|
|
90
|
+
cappedNonBackbone: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What one {@link CadreNode.reconcileControlCohort} pass did. */
|
|
94
|
+
export interface ControlCohortReconcileResult {
|
|
95
|
+
/**
|
|
96
|
+
* Peer ids this pass dialled and whose dial resolved, in dial order: selected
|
|
97
|
+
* siblings on the steady-state pass, retained bootstrap peers on the cold-start
|
|
98
|
+
* pass (the pass takes one branch or the other, never both). A peer already
|
|
99
|
+
* connected when the pass began is never listed — the pass skips it. A resolved
|
|
100
|
+
* dial can still be refused moments later by the remote's membership gate, so
|
|
101
|
+
* an entry means "this pass opened a connection", not "that connection is still
|
|
102
|
+
* open".
|
|
103
|
+
*/
|
|
104
|
+
dialed: string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Stable peerId ordering so the bounded fill is identical across passes/nodes. */
|
|
108
|
+
function byPeerId(a: CohortPeerRow, b: CohortPeerRow): number {
|
|
109
|
+
if (a.peerId < b.peerId) return -1;
|
|
110
|
+
if (a.peerId > b.peerId) return 1;
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Backbone-preferential, bounded-out-degree selection of control-cohort siblings
|
|
116
|
+
* to dial.
|
|
117
|
+
*
|
|
118
|
+
* A member is **backbone** iff its `CadrePeer` row declares the `storage` profile
|
|
119
|
+
* (an always-on machine). All backbone members are selected; the rest fill up to
|
|
120
|
+
* `targetDegree` in deterministic peerId order. A member whose profile is still
|
|
121
|
+
* unknown (it has not self-published) is not backbone until it publishes one.
|
|
122
|
+
* A profile is a dial preference only: it is self-declared, and a machine
|
|
123
|
+
* misdeclaring one costs extra dials, never access.
|
|
124
|
+
*
|
|
125
|
+
* @param siblings - cadre members EXCLUDING self (the caller filters self out).
|
|
126
|
+
* @param targetDegree - cap on non-backbone dials (negative is treated as 0).
|
|
127
|
+
*/
|
|
128
|
+
export function selectControlCohortDials(
|
|
129
|
+
siblings: CohortPeerRow[],
|
|
130
|
+
targetDegree: number,
|
|
131
|
+
): ControlCohortSelection {
|
|
132
|
+
const backbone: CohortPeerRow[] = [];
|
|
133
|
+
const rest: CohortPeerRow[] = [];
|
|
134
|
+
|
|
135
|
+
// NOTE: the backbone is uncapped, so out-degree grows with the number of always-on
|
|
136
|
+
// machines (and with any sibling that declares `storage` without being one); if a party
|
|
137
|
+
// ever runs more than a handful, cap the backbone too.
|
|
138
|
+
for (const peer of siblings) {
|
|
139
|
+
if (peer.profile === 'storage') {
|
|
140
|
+
backbone.push(peer);
|
|
141
|
+
} else {
|
|
142
|
+
rest.push(peer);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
backbone.sort(byPeerId);
|
|
147
|
+
rest.sort(byPeerId);
|
|
148
|
+
|
|
149
|
+
const cap = Math.max(0, targetDegree);
|
|
150
|
+
const fill = rest.slice(0, cap);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
dials: [...backbone, ...fill],
|
|
154
|
+
cappedNonBackbone: rest.length - fill.length,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** What {@link nextReconcileWakeMs} decides from, all times epoch milliseconds. */
|
|
159
|
+
export interface ReconcileWakeInput {
|
|
160
|
+
/**
|
|
161
|
+
* Consecutive passes, this one included, whose dial of a selected sibling or a cold-start
|
|
162
|
+
* bootstrap peer failed; 0 when none of this pass's dials did.
|
|
163
|
+
*/
|
|
164
|
+
redialStreak: number;
|
|
165
|
+
/** When the pass that just ended began. */
|
|
166
|
+
passStartedAt: number;
|
|
167
|
+
/** Every (sibling, strand) strand-address due time this node holds. */
|
|
168
|
+
strandAddrDueAt: Iterable<number>;
|
|
169
|
+
now: number;
|
|
170
|
+
/** The scheduler's backstop (ms); no wake is later than it. */
|
|
171
|
+
backstopMs: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* How long after a pass the next one should run for the pass's own reasons, or `undefined`
|
|
176
|
+
* when the backstop covers everything:
|
|
177
|
+
*
|
|
178
|
+
* - **Redial.** A streak of unconnected passes wakes after
|
|
179
|
+
* `min(CONTROL_COHORT_REDIAL_MS × 2^(streak − 1), backstop)`.
|
|
180
|
+
* - **Strand addresses.** The earliest strand-address due time LATER THAN the pass's start,
|
|
181
|
+
* when it is sooner than the backstop, wakes when it falls due (now at the latest). A due
|
|
182
|
+
* time at or before the start belongs to a (sibling, strand) the pass skipped for another
|
|
183
|
+
* reason — the strand stopped, the sibling disconnected — and waking for it would spin; the
|
|
184
|
+
* strand watcher's launch retry keeps the same rule.
|
|
185
|
+
*
|
|
186
|
+
* The smaller wins.
|
|
187
|
+
*/
|
|
188
|
+
export function nextReconcileWakeMs(input: ReconcileWakeInput): number | undefined {
|
|
189
|
+
const { redialStreak, passStartedAt, strandAddrDueAt, now, backstopMs } = input;
|
|
190
|
+
let wake: number | undefined;
|
|
191
|
+
if (redialStreak > 0) {
|
|
192
|
+
wake = Math.min(CONTROL_COHORT_REDIAL_MS * 2 ** (redialStreak - 1), backstopMs);
|
|
193
|
+
}
|
|
194
|
+
for (const dueAt of strandAddrDueAt) {
|
|
195
|
+
if (dueAt <= passStartedAt) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const delay = Math.max(0, dueAt - now);
|
|
199
|
+
if (delay < backstopMs && (wake === undefined || delay < wake)) {
|
|
200
|
+
wake = delay;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return wake;
|
|
204
|
+
}
|