@serfab/cadre-core 1.3.0 → 1.4.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 +5 -3
- package/dist/cadre-node.d.ts +32 -19
- package/dist/cadre-node.js +120 -31
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +4 -4
- package/dist/control-formation-recorder.js +6 -6
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +18 -5
- package/dist/membership-connection-gater.js +50 -10
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/storage-scope.d.ts +50 -12
- package/dist/storage-scope.js +85 -12
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-formation-manager.js +2 -1
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-id.d.ts +35 -0
- package/dist/strand-id.js +46 -0
- package/dist/strand-id.js.map +1 -0
- package/dist/strand-instance-manager.js +19 -2
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-network-config.d.ts +3 -3
- package/dist/strand-network-config.js +3 -3
- package/dist/strand-solicitation.js +2 -1
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +45 -0
- package/dist/strand-watcher.js +74 -13
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +94 -15
- package/dist/types.js +59 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/cadre-node.ts +126 -31
- package/src/control-formation-recorder.ts +6 -6
- package/src/index.ts +563 -554
- package/src/membership-connection-gater.ts +56 -11
- package/src/storage-scope.ts +163 -78
- package/src/strand-formation-manager.ts +647 -646
- package/src/strand-id.ts +48 -0
- package/src/strand-instance-manager.ts +20 -2
- package/src/strand-network-config.ts +250 -250
- package/src/strand-solicitation.ts +500 -499
- package/src/strand-watcher.ts +388 -325
- package/src/types.ts +97 -15
package/README.md
CHANGED
|
@@ -236,9 +236,11 @@ A **scope key** is what the factory receives. It is the strand id for each stran
|
|
|
236
236
|
which holds one party's own records and must not be shared between parties. Every key is
|
|
237
237
|
opaque and already safe as a file, directory or database name (always within
|
|
238
238
|
`[A-Za-z0-9._-]`): use it verbatim, do not parse it. `isControlStorageScope(scope)` tells
|
|
239
|
-
the two kinds apart.
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
the two kinds apart. The control key holds the charset by base64url encoding; a strand
|
|
240
|
+
id holds it by check — `assertStrandScopeKey` runs on every strand launch, so a strand
|
|
241
|
+
whose row replicated in from another node with an unusable id is refused rather than
|
|
242
|
+
handed to the factory, and on `publishStrand`, so this node never writes such an id into
|
|
243
|
+
the party's control database to begin with.
|
|
242
244
|
|
|
243
245
|
Available storage implementations:
|
|
244
246
|
| Package | Environment | Description |
|
package/dist/cadre-node.d.ts
CHANGED
|
@@ -279,8 +279,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
279
279
|
* dead entry behind; recovery today is a fresh invitation.
|
|
280
280
|
*
|
|
281
281
|
* NOTE: entries are kept for the node's lifetime, including across a
|
|
282
|
-
* {@link stopStrand} — a stopped strand may be
|
|
283
|
-
*
|
|
282
|
+
* {@link stopStrand} — a stopped strand may be claimed again with {@link addStrand}
|
|
283
|
+
* (the watcher itself will not re-offer it: the stop suppresses the id for the rest of
|
|
284
|
+
* the session) and must still get its seed. Each strand's list is capped at
|
|
285
|
+
* `MAX_STRAND_ADDRS`, but the
|
|
284
286
|
* number of KEYS is bounded only by the strands this node has ever formed as an
|
|
285
287
|
* initiator (not by time); if a node ever forms strands at scale, evict on
|
|
286
288
|
* `unpublishStrand` or cap the map.
|
|
@@ -732,6 +734,12 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
732
734
|
* derivation from it (`db-p2p` namespaces all of the node's protocol ids as
|
|
733
735
|
* `/optimystic/<networkName>/...`), so the node options and the block-transfer
|
|
734
736
|
* protocol prefix the control backfill dials can never drift apart.
|
|
737
|
+
*
|
|
738
|
+
* NOTE: the party id goes in UNENCODED here, unlike in `controlStorageScope`. Safe
|
|
739
|
+
* today because a party id is locally configured rather than replicated in, and both
|
|
740
|
+
* ends of a connection derive this string identically — an odd party id yields an odd
|
|
741
|
+
* but consistent protocol id, not a mismatch or an escaped name. If a party id ever
|
|
742
|
+
* arrives from the network, encode it here as the storage scope key already does.
|
|
735
743
|
*/
|
|
736
744
|
private controlNetworkName;
|
|
737
745
|
/**
|
|
@@ -1862,17 +1870,17 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1862
1870
|
*
|
|
1863
1871
|
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
1864
1872
|
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
1865
|
-
* automatic relaunch need it.
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
1873
|
+
* automatic relaunch need it. A failed launch here hands the strand back to that
|
|
1874
|
+
* relaunch — `StrandWatcher.forgetStrand` drops the id from the watcher's
|
|
1875
|
+
* `knownStrands` and records the backoff, so a later poll re-attempts it on the same
|
|
1876
|
+
* `pollInterval * 2^(failures-1)` ladder a watcher-driven failure gets. Because the
|
|
1877
|
+
* config stays registered, the retry takes `handleStrandAdded`'s auto-launch branch:
|
|
1878
|
+
* what the app sees is `strand:error` per failed retry and `strand:started` when one
|
|
1879
|
+
* succeeds, never a second `strand:discovered`. {@link detachStrand} (reached via
|
|
1880
|
+
* {@link stopStrand}) is what abandons a strand for good, and its stop suppresses the
|
|
1881
|
+
* strand in the watcher so this retry cannot resurrect it — a call to THIS method lifts
|
|
1882
|
+
* that suppression again, since an explicit claim is a deliberate reversal of the
|
|
1883
|
+
* deliberate stop.
|
|
1876
1884
|
*/
|
|
1877
1885
|
addStrand(config: StrandConfig): Promise<StrandInstance>;
|
|
1878
1886
|
/**
|
|
@@ -1945,7 +1953,8 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1945
1953
|
* @returns The live `Strand` row — the one just inserted, or the matching one already
|
|
1946
1954
|
* there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
|
|
1947
1955
|
* on a repeat it is the stored key, not the argument.
|
|
1948
|
-
* @throws if the node is not started, exposes no owner signing key, the id is blank
|
|
1956
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank or
|
|
1957
|
+
* unusable as a storage scope key (`InvalidStrandIdError` — see `storage-scope.ts`), a
|
|
1949
1958
|
* row with the same id holds different content, or the control DB rejects the
|
|
1950
1959
|
* (unauthorized) insert.
|
|
1951
1960
|
*/
|
|
@@ -2465,11 +2474,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2465
2474
|
* Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
|
|
2466
2475
|
* stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
|
|
2467
2476
|
* intact, so on the next node RESTART the strand is rediscovered and surfaces as
|
|
2468
|
-
* `strand:discovered` again —
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
* {@link unpublishStrand}.
|
|
2477
|
+
* `strand:discovered` again — but never again in THIS session: the stop suppresses the
|
|
2478
|
+
* id in the watcher (`StrandWatcher.suppressStrand`) and drops it from
|
|
2479
|
+
* {@link getDiscoveredStrands}, so neither a later poll nor a drain can undo a
|
|
2480
|
+
* deliberate stop. Only an explicit {@link addStrand} does, which is the caller
|
|
2481
|
+
* reversing its own decision. Party-wide removal is {@link unpublishStrand}.
|
|
2473
2482
|
*/
|
|
2474
2483
|
stopStrand(strandId: string): Promise<void>;
|
|
2475
2484
|
/**
|
|
@@ -2490,6 +2499,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2490
2499
|
* readings are the intended one: `handleStrandRemoved` arrives because the control row
|
|
2491
2500
|
* is gone, and an explicit {@link stopStrand} is a deliberate abandonment — neither
|
|
2492
2501
|
* strand may be re-offered to a later `getDiscoveredStrands()` drain.
|
|
2502
|
+
*
|
|
2503
|
+
* The watcher-level suppression that makes a stop permanent is deliberately NOT here,
|
|
2504
|
+
* only in {@link stopStrand}: the two callers diverge on it. A vanished control row
|
|
2505
|
+
* that reappears is a fresh strand and must be offered again.
|
|
2493
2506
|
*/
|
|
2494
2507
|
private detachStrand;
|
|
2495
2508
|
/**
|
package/dist/cadre-node.js
CHANGED
|
@@ -4,11 +4,11 @@ import { peerIdFromString, peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
|
4
4
|
import { createLibp2pNode } from '@optimystic/db-p2p';
|
|
5
5
|
import { wrapStorageWithCache, disposeStorageCache } from '@serfab/quereus-plugin-sereus';
|
|
6
6
|
import { multiaddr } from '@multiformats/multiaddr';
|
|
7
|
-
import { controlClusterPolicy, CONTROL_REPLICATION_BREADTH, DEFAULT_CHECKIN_WINDOW_MS } from './types.js';
|
|
7
|
+
import { controlClusterPolicy, CONTROL_REPLICATION_BREADTH, DEFAULT_CHECKIN_WINDOW_MS, DEFAULT_CONNECTION_MONITOR } from './types.js';
|
|
8
8
|
import { sign } from '@optimystic/quereus-plugin-crypto';
|
|
9
9
|
import { ed25519KeyPairFromLibp2p, ed25519PublicKeyFromPrivate, requireEd25519PublicKeyB64 } from './ed25519-key.js';
|
|
10
10
|
import { strandTransportKey } from './strand-transport-key.js';
|
|
11
|
-
import { controlStorageScope } from './storage-scope.js';
|
|
11
|
+
import { assertScopeKeyCharset, assertStrandScopeKey, controlStorageScope, InvalidStrandIdError, isValidStrandScopeKey } from './storage-scope.js';
|
|
12
12
|
import { generateStrandMemberKey, strandMemberKeyPair } from './strand-member-key.js';
|
|
13
13
|
import { assertNotPreSplitStrand, issueInvite, PreSplitStrandIdentityError } from './strand-membership-writer.js';
|
|
14
14
|
import { MEMBERSHIP_INVITE_TTL_MS } from './strand-formation-manager.js';
|
|
@@ -394,8 +394,10 @@ export class CadreNode {
|
|
|
394
394
|
* dead entry behind; recovery today is a fresh invitation.
|
|
395
395
|
*
|
|
396
396
|
* NOTE: entries are kept for the node's lifetime, including across a
|
|
397
|
-
* {@link stopStrand} — a stopped strand may be
|
|
398
|
-
*
|
|
397
|
+
* {@link stopStrand} — a stopped strand may be claimed again with {@link addStrand}
|
|
398
|
+
* (the watcher itself will not re-offer it: the stop suppresses the id for the rest of
|
|
399
|
+
* the session) and must still get its seed. Each strand's list is capped at
|
|
400
|
+
* `MAX_STRAND_ADDRS`, but the
|
|
399
401
|
* number of KEYS is bounded only by the strands this node has ever formed as an
|
|
400
402
|
* initiator (not by time); if a node ever forms strands at scale, evict on
|
|
401
403
|
* `unpublishStrand` or cap the map.
|
|
@@ -1184,6 +1186,12 @@ export class CadreNode {
|
|
|
1184
1186
|
* derivation from it (`db-p2p` namespaces all of the node's protocol ids as
|
|
1185
1187
|
* `/optimystic/<networkName>/...`), so the node options and the block-transfer
|
|
1186
1188
|
* protocol prefix the control backfill dials can never drift apart.
|
|
1189
|
+
*
|
|
1190
|
+
* NOTE: the party id goes in UNENCODED here, unlike in `controlStorageScope`. Safe
|
|
1191
|
+
* today because a party id is locally configured rather than replicated in, and both
|
|
1192
|
+
* ends of a connection derive this string identically — an odd party id yields an odd
|
|
1193
|
+
* but consistent protocol id, not a mismatch or an escaped name. If a party id ever
|
|
1194
|
+
* arrives from the network, encode it here as the storage scope key already does.
|
|
1187
1195
|
*/
|
|
1188
1196
|
controlNetworkName() {
|
|
1189
1197
|
return `control-${this.config.controlNetwork.partyId}`;
|
|
@@ -1293,6 +1301,11 @@ export class CadreNode {
|
|
|
1293
1301
|
// party's records, so two parties on one device must not land in one store.
|
|
1294
1302
|
// Same key as the cache label, which also makes the shared pool's `stats()` readable.
|
|
1295
1303
|
const scope = controlStorageScope(this.config.controlNetwork.partyId);
|
|
1304
|
+
// The second seam that hands a scope key to an embedder's provider; the first
|
|
1305
|
+
// (`StrandInstanceManager.startStrand`) asserts the stricter strand rule. Holds by
|
|
1306
|
+
// construction today — base64url is inside the charset — so this guards a future
|
|
1307
|
+
// edit to `controlStorageScope`, not a reachable input.
|
|
1308
|
+
assertScopeKeyCharset(scope);
|
|
1296
1309
|
const resolved = typeof provider === 'function' ? provider(scope) : provider;
|
|
1297
1310
|
this.controlStorage = wrapStorageWithCache(resolved, scope);
|
|
1298
1311
|
return this.controlStorage;
|
|
@@ -1388,6 +1401,11 @@ export class CadreNode {
|
|
|
1388
1401
|
...(identityKey && { privateKey: identityKey }),
|
|
1389
1402
|
...(network?.transports && { transports: network.transports }),
|
|
1390
1403
|
...(network?.noiseCrypto && { noiseCrypto: network.noiseCrypto }),
|
|
1404
|
+
// Always present, unlike the conditional spreads around it: an absent
|
|
1405
|
+
// `connectionMonitor` would leave libp2p's 5s ping deadline, which drops a peer
|
|
1406
|
+
// whose event loop is saturated by pure-JS Noise crypto. A configured value
|
|
1407
|
+
// replaces the default whole (see DEFAULT_CONNECTION_MONITOR).
|
|
1408
|
+
connectionMonitor: network?.connectionMonitor ?? DEFAULT_CONNECTION_MONITOR,
|
|
1391
1409
|
// `{ wsPort }` when a listen entry names WebSocket, otherwise `{}` — and always
|
|
1392
1410
|
// `{}` when `network.transports` is set, since the embedder owns transport policy
|
|
1393
1411
|
// then. Spread NEXT to `transports` because the two answer the same question.
|
|
@@ -3670,6 +3688,23 @@ export class CadreNode {
|
|
|
3670
3688
|
}
|
|
3671
3689
|
async handleStrandAdded(strand) {
|
|
3672
3690
|
log('Handling strand added from control network: %s', strand.Id);
|
|
3691
|
+
// A replicated row carries whatever id the founding node wrote, and the id becomes a
|
|
3692
|
+
// storage scope key and a libp2p protocol prefix. Checked FIRST — above the
|
|
3693
|
+
// discovery branch below — so a malformed id is never offered to the hosting app as
|
|
3694
|
+
// a strand it could join; that app's `addStrand` could only fail on it.
|
|
3695
|
+
//
|
|
3696
|
+
// Suppressed rather than retried: the id is a property of the row, so every later
|
|
3697
|
+
// attempt fails identically, and without this the watcher's ladder would re-emit
|
|
3698
|
+
// `strand:error` every five minutes for the life of the process. The throw still
|
|
3699
|
+
// matters — the watcher's catch runs `forgetStrand`, dropping the id from
|
|
3700
|
+
// `knownStrands`, so its removed-strand loop never detaches a strand that never ran.
|
|
3701
|
+
if (!isValidStrandScopeKey(strand.Id)) {
|
|
3702
|
+
const error = new InvalidStrandIdError(strand.Id);
|
|
3703
|
+
log('Refusing strand %s: %s', strand.Id, error.message);
|
|
3704
|
+
this.emit('strand:error', { strandId: strand.Id, error });
|
|
3705
|
+
this.strandWatcher?.suppressStrand(strand.Id);
|
|
3706
|
+
throw error;
|
|
3707
|
+
}
|
|
3673
3708
|
// Check if we have sApp config for this strand
|
|
3674
3709
|
const sAppConfig = this.sAppConfigs.get(strand.Id);
|
|
3675
3710
|
if (!sAppConfig) {
|
|
@@ -3681,10 +3716,12 @@ export class CadreNode {
|
|
|
3681
3716
|
// already present) keeps auto-starting below, unchanged.
|
|
3682
3717
|
//
|
|
3683
3718
|
// Recorded BEFORE the emit so a handler that synchronously drains
|
|
3684
|
-
// `getDiscoveredStrands()` sees this strand too. The watcher
|
|
3685
|
-
//
|
|
3686
|
-
//
|
|
3687
|
-
//
|
|
3719
|
+
// `getDiscoveredStrands()` sees this strand too. The watcher does not offer the
|
|
3720
|
+
// same strand twice — its `knownStrands` retains the id, and the one thing that
|
|
3721
|
+
// un-retains it (a failed `addStrand`, via `StrandWatcher.forgetStrand`) leaves the
|
|
3722
|
+
// sApp config registered, so the retry takes the auto-launch branch below rather
|
|
3723
|
+
// than this one. So this map — not the event — is what a late subscriber reads.
|
|
3724
|
+
// See the `strand:discovered` doc in types.ts.
|
|
3688
3725
|
this.discoveredStrands.set(strand.Id, strand);
|
|
3689
3726
|
this.emit('strand:discovered', { strandId: strand.Id, strand });
|
|
3690
3727
|
return;
|
|
@@ -4084,23 +4121,26 @@ export class CadreNode {
|
|
|
4084
4121
|
*
|
|
4085
4122
|
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
4086
4123
|
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
4087
|
-
* automatic relaunch need it.
|
|
4088
|
-
*
|
|
4089
|
-
*
|
|
4090
|
-
*
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
4093
|
-
*
|
|
4094
|
-
*
|
|
4095
|
-
*
|
|
4096
|
-
*
|
|
4097
|
-
*
|
|
4124
|
+
* automatic relaunch need it. A failed launch here hands the strand back to that
|
|
4125
|
+
* relaunch — `StrandWatcher.forgetStrand` drops the id from the watcher's
|
|
4126
|
+
* `knownStrands` and records the backoff, so a later poll re-attempts it on the same
|
|
4127
|
+
* `pollInterval * 2^(failures-1)` ladder a watcher-driven failure gets. Because the
|
|
4128
|
+
* config stays registered, the retry takes `handleStrandAdded`'s auto-launch branch:
|
|
4129
|
+
* what the app sees is `strand:error` per failed retry and `strand:started` when one
|
|
4130
|
+
* succeeds, never a second `strand:discovered`. {@link detachStrand} (reached via
|
|
4131
|
+
* {@link stopStrand}) is what abandons a strand for good, and its stop suppresses the
|
|
4132
|
+
* strand in the watcher so this retry cannot resurrect it — a call to THIS method lifts
|
|
4133
|
+
* that suppression again, since an explicit claim is a deliberate reversal of the
|
|
4134
|
+
* deliberate stop.
|
|
4098
4135
|
*/
|
|
4099
4136
|
async addStrand(config) {
|
|
4100
4137
|
if (!this._running) {
|
|
4101
4138
|
throw new Error('CadreNode not running');
|
|
4102
4139
|
}
|
|
4103
4140
|
const { strandRow, sAppConfig, founder, partyMemberPrivateKey } = config;
|
|
4141
|
+
// Before anything is recorded: an unusable id must not leave a registered sApp
|
|
4142
|
+
// config behind, and must not lift a suppression this node set deliberately.
|
|
4143
|
+
assertStrandScopeKey(strandRow.Id);
|
|
4104
4144
|
if (partyMemberPrivateKey !== undefined && strandRow.Type !== 'c') {
|
|
4105
4145
|
// Only a closed strand seats a Member/Manager, so an open launch would drop this
|
|
4106
4146
|
// key silently. Refuse instead: the caller has confused the party identity key
|
|
@@ -4113,13 +4153,41 @@ export class CadreNode {
|
|
|
4113
4153
|
// unclaimed backlog — a later `getDiscoveredStrands()` drain must not re-offer it.
|
|
4114
4154
|
this.sAppConfigs.set(strandRow.Id, sAppConfig);
|
|
4115
4155
|
this.discoveredStrands.delete(strandRow.Id);
|
|
4156
|
+
// A claim also overrides any earlier `stopStrand` of this id: the watcher must track
|
|
4157
|
+
// the strand again, or a party-wide removal would never stop it here.
|
|
4158
|
+
this.strandWatcher?.unsuppressStrand(strandRow.Id);
|
|
4116
4159
|
log('Registered sAppConfig for strand %s (sApp: %s, founder: %s)', strandRow.Id, sAppConfig.id, founder ?? 'derived');
|
|
4117
4160
|
// An unset `founder` is DERIVED from the row inside launchStrand (this node founds
|
|
4118
4161
|
// iff the row's FounderOwnerKey is its own owner key); an explicit flag wins — the
|
|
4119
4162
|
// formation/responder flows pass one deliberately, since their consent-seated rows
|
|
4120
4163
|
// carry a null column. See StrandConfig.founder. Same rule for the explicit
|
|
4121
4164
|
// partyMemberPrivateKey: it wins over the StrandPartyKey control-row read.
|
|
4122
|
-
|
|
4165
|
+
// Only the launch is wrapped, NOT the first-sync wait below: a wait that times out
|
|
4166
|
+
// rejects with the retryable StrandAwaitingFirstSyncError and deliberately leaves the
|
|
4167
|
+
// instance running, so there is nothing to re-offer and forgetting it would only have
|
|
4168
|
+
// the watcher re-enter a launch for a strand that is already up.
|
|
4169
|
+
let instance;
|
|
4170
|
+
try {
|
|
4171
|
+
instance = await this.launchStrand(strandRow, sAppConfig, founder, partyMemberPrivateKey);
|
|
4172
|
+
}
|
|
4173
|
+
catch (error) {
|
|
4174
|
+
// Hand the strand back to the watcher's retry ladder. Harmless on the founder path
|
|
4175
|
+
// (`foundStrand` → `publishStrand` + this call), where the watcher may never have
|
|
4176
|
+
// offered the strand: the `knownStrands` delete is a no-op and the recorded backoff
|
|
4177
|
+
// only delays the watcher's own first attempt by one poll interval, which is what
|
|
4178
|
+
// should happen right after an attempt that just failed.
|
|
4179
|
+
//
|
|
4180
|
+
// NOTE: the retry relaunches from the CONTROL row plus the registered config, not
|
|
4181
|
+
// from the row and arguments passed here — so a caller that enriched either (a
|
|
4182
|
+
// synthetic `MemberPrivateKey`, an explicit `founder` or `partyMemberPrivateKey`)
|
|
4183
|
+
// is retried with less than it asked for. Inert today: founder-ness re-derives from
|
|
4184
|
+
// `FounderOwnerKey`, the party key re-reads from `StrandPartyKey`, and the row's
|
|
4185
|
+
// shared `MemberPrivateKey` is read only by the founder bootstrap. If a joiner
|
|
4186
|
+
// launch ever starts depending on that key, gate this hand-back on the passed row
|
|
4187
|
+
// matching the control one.
|
|
4188
|
+
this.strandWatcher?.forgetStrand(strandRow.Id);
|
|
4189
|
+
throw error;
|
|
4190
|
+
}
|
|
4123
4191
|
// The first-sync write gate: a JOINING machine's database is withheld until it has
|
|
4124
4192
|
// received the strand's Header from another member, because a write before that
|
|
4125
4193
|
// forks every table it touches (see strand-first-sync-gate.ts). Waiting here is what
|
|
@@ -4208,7 +4276,8 @@ export class CadreNode {
|
|
|
4208
4276
|
* @returns The live `Strand` row — the one just inserted, or the matching one already
|
|
4209
4277
|
* there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
|
|
4210
4278
|
* on a repeat it is the stored key, not the argument.
|
|
4211
|
-
* @throws if the node is not started, exposes no owner signing key, the id is blank
|
|
4279
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank or
|
|
4280
|
+
* unusable as a storage scope key (`InvalidStrandIdError` — see `storage-scope.ts`), a
|
|
4212
4281
|
* row with the same id holds different content, or the control DB rejects the
|
|
4213
4282
|
* (unauthorized) insert.
|
|
4214
4283
|
*/
|
|
@@ -4217,6 +4286,11 @@ export class CadreNode {
|
|
|
4217
4286
|
// Trim/reject here so the id that lands matches the one unpublishStrand looks up: it
|
|
4218
4287
|
// trims too, and an untrimmed row would be unreachable by the same string.
|
|
4219
4288
|
const trimmed = requireNonBlank(strandId, 'strand id');
|
|
4289
|
+
// Refuse an unusable id HERE, not only at launch. `addStrand` already asserts, but it
|
|
4290
|
+
// runs after this row is written, and this row replicates: an id no node can turn into
|
|
4291
|
+
// a storage scope key would otherwise reach the whole party's control database and be
|
|
4292
|
+
// declined once per member. Asserted after the trim so the id checked is the id stored.
|
|
4293
|
+
assertStrandScopeKey(trimmed);
|
|
4220
4294
|
// FounderOwnerKey records THIS machine as the row's publisher (the schema pins it to
|
|
4221
4295
|
// the signing owner), which is what later lets a relaunch derive founder-ness.
|
|
4222
4296
|
const desired = {
|
|
@@ -4457,13 +4531,20 @@ export class CadreNode {
|
|
|
4457
4531
|
// one never cleared).
|
|
4458
4532
|
await this.strandManager.clearOwnMemberPeerBinding(trimmed);
|
|
4459
4533
|
// Converge locally now rather than waiting up to a poll interval. The watcher fires
|
|
4460
|
-
// onStrandRemoved for a strand it tracked; the explicit
|
|
4461
|
-
// strandFilter never admitted this strand (the watcher never knew it, so it will
|
|
4462
|
-
// fire). StrandInstanceManager.stopStrand no-ops on an unknown id, so the two
|
|
4463
|
-
// cannot double-stop into an error.
|
|
4534
|
+
// onStrandRemoved for a strand it tracked; the explicit teardown below covers a node
|
|
4535
|
+
// whose strandFilter never admitted this strand (the watcher never knew it, so it will
|
|
4536
|
+
// never fire). StrandInstanceManager.stopStrand no-ops on an unknown id, so the two
|
|
4537
|
+
// paths cannot double-stop into an error.
|
|
4538
|
+
//
|
|
4539
|
+
// detachStrand, NOT stopStrand: this is a party-wide removal, not a local abandonment
|
|
4540
|
+
// of a strand that still exists, so it must not SUPPRESS the id in the watcher. The
|
|
4541
|
+
// row is already gone — there is nothing to suppress — and an id re-published before
|
|
4542
|
+
// the next poll's suppression cleanup would then never be offered again this session.
|
|
4543
|
+
// (Same reasoning as handleStrandRemoved, which also avoids stopStrand's `_running`
|
|
4544
|
+
// guard; here the guard is already satisfied by requireOwnerSigningKey above.)
|
|
4464
4545
|
await this.strandWatcher?.forcePoll();
|
|
4465
4546
|
if (this.strandManager.getInstance(trimmed)) {
|
|
4466
|
-
await this.
|
|
4547
|
+
await this.detachStrand(trimmed);
|
|
4467
4548
|
}
|
|
4468
4549
|
log('Unpublished strand %s from control DB under owner %s', trimmed, signingKey.publicKeyB64);
|
|
4469
4550
|
}
|
|
@@ -5313,16 +5394,20 @@ export class CadreNode {
|
|
|
5313
5394
|
* Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
|
|
5314
5395
|
* stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
|
|
5315
5396
|
* intact, so on the next node RESTART the strand is rediscovered and surfaces as
|
|
5316
|
-
* `strand:discovered` again —
|
|
5317
|
-
*
|
|
5318
|
-
*
|
|
5319
|
-
*
|
|
5320
|
-
* {@link unpublishStrand}.
|
|
5397
|
+
* `strand:discovered` again — but never again in THIS session: the stop suppresses the
|
|
5398
|
+
* id in the watcher (`StrandWatcher.suppressStrand`) and drops it from
|
|
5399
|
+
* {@link getDiscoveredStrands}, so neither a later poll nor a drain can undo a
|
|
5400
|
+
* deliberate stop. Only an explicit {@link addStrand} does, which is the caller
|
|
5401
|
+
* reversing its own decision. Party-wide removal is {@link unpublishStrand}.
|
|
5321
5402
|
*/
|
|
5322
5403
|
async stopStrand(strandId) {
|
|
5323
5404
|
if (!this._running) {
|
|
5324
5405
|
throw new Error('CadreNode not running');
|
|
5325
5406
|
}
|
|
5407
|
+
// Suppressed HERE and not in detachStrand: the other caller of that method is
|
|
5408
|
+
// `handleStrandRemoved`, which arrives precisely because the control row is gone, and
|
|
5409
|
+
// a row that later reappears is a fresh strand that should be offered again.
|
|
5410
|
+
this.strandWatcher?.suppressStrand(strandId);
|
|
5326
5411
|
await this.detachStrand(strandId);
|
|
5327
5412
|
}
|
|
5328
5413
|
/**
|
|
@@ -5343,6 +5428,10 @@ export class CadreNode {
|
|
|
5343
5428
|
* readings are the intended one: `handleStrandRemoved` arrives because the control row
|
|
5344
5429
|
* is gone, and an explicit {@link stopStrand} is a deliberate abandonment — neither
|
|
5345
5430
|
* strand may be re-offered to a later `getDiscoveredStrands()` drain.
|
|
5431
|
+
*
|
|
5432
|
+
* The watcher-level suppression that makes a stop permanent is deliberately NOT here,
|
|
5433
|
+
* only in {@link stopStrand}: the two callers diverge on it. A vanished control row
|
|
5434
|
+
* that reappears is a fresh strand and must be offered again.
|
|
5346
5435
|
*/
|
|
5347
5436
|
async detachStrand(strandId) {
|
|
5348
5437
|
this.hibernationManager.untrackStrand(strandId);
|