@serfab/cadre-core 1.18.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cadre-node.d.ts +65 -2
  2. package/dist/cadre-node.js +120 -4
  3. package/dist/cadre-node.js.map +1 -1
  4. package/dist/control-read-retry.d.ts +19 -16
  5. package/dist/control-read-retry.js +22 -54
  6. package/dist/control-read-retry.js.map +1 -1
  7. package/dist/control-retry.d.ts +50 -6
  8. package/dist/control-retry.js +53 -6
  9. package/dist/control-retry.js.map +1 -1
  10. package/dist/control-write-retry.d.ts +24 -16
  11. package/dist/control-write-retry.js +58 -30
  12. package/dist/control-write-retry.js.map +1 -1
  13. package/dist/edge-node/config.d.ts +8 -2
  14. package/dist/edge-node/config.js +5 -1
  15. package/dist/edge-node/config.js.map +1 -1
  16. package/dist/edge-node/edge-join.js +2 -0
  17. package/dist/edge-node/edge-join.js.map +1 -1
  18. package/dist/edge-node/edge-node.d.ts +10 -0
  19. package/dist/edge-node/edge-node.js +52 -8
  20. package/dist/edge-node/edge-node.js.map +1 -1
  21. package/dist/edge-node/options.d.ts +11 -2
  22. package/dist/edge-node/options.js +11 -4
  23. package/dist/edge-node/options.js.map +1 -1
  24. package/dist/edge-node/platform.d.ts +15 -3
  25. package/dist/edge-node/strands.js +3 -2
  26. package/dist/edge-node/strands.js.map +1 -1
  27. package/dist/index.d.ts +5 -3
  28. package/dist/index.js +7 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/peer-join-backfill.d.ts +8 -11
  31. package/dist/peer-join-backfill.js +22 -35
  32. package/dist/peer-join-backfill.js.map +1 -1
  33. package/dist/self-address-update.d.ts +13 -0
  34. package/dist/self-address-update.js +19 -0
  35. package/dist/self-address-update.js.map +1 -0
  36. package/dist/strand-database.d.ts +56 -0
  37. package/dist/strand-database.js +87 -0
  38. package/dist/strand-database.js.map +1 -1
  39. package/dist/strand-instance-manager.d.ts +67 -2
  40. package/dist/strand-instance-manager.js +105 -4
  41. package/dist/strand-instance-manager.js.map +1 -1
  42. package/dist/strand-membership-reconciler.d.ts +13 -1
  43. package/dist/strand-membership-reconciler.js +23 -14
  44. package/dist/strand-membership-reconciler.js.map +1 -1
  45. package/dist/strand-membership-writer.d.ts +16 -2
  46. package/dist/strand-membership-writer.js +54 -8
  47. package/dist/strand-membership-writer.js.map +1 -1
  48. package/dist/strand-offline.d.ts +131 -0
  49. package/dist/strand-offline.js +142 -0
  50. package/dist/strand-offline.js.map +1 -0
  51. package/dist/types.d.ts +35 -0
  52. package/dist/types.js.map +1 -1
  53. package/package.json +8 -8
  54. package/src/cadre-node.ts +11456 -11326
  55. package/src/control-read-retry.ts +22 -58
  56. package/src/control-retry.ts +54 -6
  57. package/src/control-write-retry.ts +58 -30
  58. package/src/edge-node/config.ts +11 -2
  59. package/src/edge-node/edge-join.ts +1 -0
  60. package/src/edge-node/edge-node.ts +55 -6
  61. package/src/edge-node/options.ts +20 -4
  62. package/src/edge-node/platform.ts +15 -3
  63. package/src/edge-node/strands.ts +3 -2
  64. package/src/index.ts +917 -899
  65. package/src/peer-join-backfill.ts +33 -43
  66. package/src/self-address-update.ts +19 -0
  67. package/src/strand-database.ts +466 -352
  68. package/src/strand-instance-manager.ts +2210 -2092
  69. package/src/strand-membership-reconciler.ts +29 -14
  70. package/src/strand-membership-writer.ts +61 -14
  71. package/src/strand-offline.ts +247 -0
  72. package/src/types.ts +2960 -2926
@@ -1,6 +1,7 @@
1
1
  import type { Libp2p, PeerId } from '@libp2p/interface';
2
2
  import type { Multiaddr } from '@multiformats/multiaddr';
3
3
  import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, FoundStrandOptions, FoundStrandBackgroundResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, SeedAckMessage, SeedDeliveryTarget, AddDroneOptions, DroneInitResult, OpenInvitation, FormStrandResult, StrandFormationDisclosure, StrandMembershipInvite, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, CadreInviteStatus, CadreMachine, CadreOwnerKey, MachineRemovalPlan, MachineRemovalResult, OwnerKeyRemovalPlan, OwnerKeyRemovalResult, RemoveOwnerOptions, CadreRemoval, CadreInvitationReach, CreateCadreInvitationOptions, CreateCadreInvitationResult, RedeemCadreInvitationOptions, RedeemCadreInvitationResult, CreateOwnerGrantOptions, CreateOwnerGrantResult, OwnerGrantStatus, RedeemOwnerGrantOptions, RedeemOwnerGrantResult, JoinWithOwnerGrantResult, ResolveDeviceTokenOpts, PendingJoinStatus } from './types.js';
4
+ import { type StrandLeaveOptions, type StrandOfflineMode } from './strand-offline.js';
4
5
  import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
5
6
  import { type OwnerSign, type OwnerSigner } from './owner-signer.js';
6
7
  import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
@@ -2441,6 +2442,17 @@ export declare class CadreNode implements SAppIdLookup {
2441
2442
  * using. Emits `strand:writable` after the timers are settled.
2442
2443
  */
2443
2444
  private handleStrandWritable;
2445
+ /**
2446
+ * A status from a strand's offline queue (`StartStrandConfig.offline`). The queue sending counts
2447
+ * as activity ({@link isSendingStatus}), so a check-in window that finds the cohort reachable stays
2448
+ * up while the queue drains, and the idle timer does not cut a drain short. Recorded through the
2449
+ * hibernation manager, not {@link recordStrandActivity}, whose push fan-out would wake other phones.
2450
+ *
2451
+ * Not on a strand being quiesced: activity there wakes it again once the quiesce ends, which would
2452
+ * undo a `hibernateAll` made as the app went to the background. A send that quiesce cuts short was
2453
+ * journaled before it went out, and the next build's restore re-sends it under the same action id.
2454
+ */
2455
+ private handleOfflineStatus;
2444
2456
  private handleStrandIdle;
2445
2457
  /**
2446
2458
  * Hibernate a strand: release its strand-network resources via the strand
@@ -2595,6 +2607,24 @@ export declare class CadreNode implements SAppIdLookup {
2595
2607
  whenStrandWritable(strandId: string, options?: {
2596
2608
  timeoutMs?: number;
2597
2609
  }): Promise<StrandInstance>;
2610
+ /**
2611
+ * Switch a strand between offline mode, where a commit resolves once the transaction is queued on
2612
+ * this machine, and online mode (docs/strands.md → "Offline mode"). Affects transactions begun from
2613
+ * now on; one already open keeps the acknowledgement it began with. The mode belongs to the strand:
2614
+ * a hibernating strand takes it at its next build, and it lasts across hibernation and every
2615
+ * rebuild until the process exits, after which the strand starts in `strandOffline.initialMode`.
2616
+ *
2617
+ * @throws StrandOfflineUnavailableError when `CadreNodeConfig.strandOffline` is not configured.
2618
+ * @throws when this node does not run the strand.
2619
+ */
2620
+ setStrandOfflineMode(strandId: string, mode: StrandOfflineMode): void;
2621
+ /**
2622
+ * A strand's offline mode ({@link setStrandOfflineMode}); `'online'` on a node without
2623
+ * `strandOffline`. The queue's own view, with what it holds, is `strand.database.offline.queueState()`.
2624
+ *
2625
+ * @throws when this node does not run the strand.
2626
+ */
2627
+ getStrandOfflineMode(strandId: string): StrandOfflineMode;
2598
2628
  /**
2599
2629
  * Publish a strand row to the shared control database under this node's own
2600
2630
  * owner identity, so other cadre members discover it via control-network
@@ -2845,14 +2875,22 @@ export declare class CadreNode implements SAppIdLookup {
2845
2875
  * caller ever needs removal to mean "and erase the local copy", that is a separate purge
2846
2876
  * step, not a widening of this method.
2847
2877
  *
2878
+ * Refuses while the strand's offline queue on this machine holds transactions, unless
2879
+ * `options.discardQueued` (docs/strands.md → "Leaving a strand with unsent transactions"). The
2880
+ * check and the discard run before the control-plane delete, so a delete refused afterwards
2881
+ * leaves the discarded transactions discarded.
2882
+ *
2848
2883
  * @param strandId - The `Strand` row id to remove (as passed to {@link publishStrand}).
2884
+ * @param options - `discardQueued`: withdraw the queued transactions that were never sent, and remove.
2885
+ * @throws StrandQueueNotEmptyError when the queue holds transactions and `discardQueued` is not
2886
+ * set; nothing was removed.
2849
2887
  * @throws if the node is not started, has no owner signer wired, the id is blank, or
2850
2888
  * the signer is not an enrolled owner (the schema's `Strand.AuthorizedDelete` rejects
2851
2889
  * the write and the row survives). A rejection does NOT imply the row survived: the
2852
2890
  * local stop runs after the control-plane delete has already committed, so a failure
2853
2891
  * there throws over a completed removal.
2854
2892
  */
2855
- unpublishStrand(strandId: string): Promise<void>;
2893
+ unpublishStrand(strandId: string, options?: StrandLeaveOptions): Promise<void>;
2856
2894
  /**
2857
2895
  * Seat this party's own strand membership identity key — the `StrandPartyKey` row —
2858
2896
  * for `strandId`, minting a fresh one when none is given, and adopting the stored one
@@ -3400,11 +3438,36 @@ export declare class CadreNode implements SAppIdLookup {
3400
3438
  * The other members of the strand are not told, and this party's membership row in the
3401
3439
  * strand stays, so a later re-formation reuses the same identity.
3402
3440
  *
3441
+ * Refuses while the strand's offline queue on this machine holds transactions, unless
3442
+ * `options.discardQueued` (docs/strands.md → "Leaving a strand with unsent transactions"). The
3443
+ * check and the discard run before the party-wide row is removed, so a removal refused
3444
+ * afterwards leaves the discarded transactions discarded.
3445
+ *
3446
+ * @param options - `discardQueued`: withdraw the queued transactions that were never sent, and leave.
3447
+ * @throws StrandQueueNotEmptyError when the queue holds transactions and `discardQueued` is not
3448
+ * set; nothing was left.
3403
3449
  * @throws when a party-wide row exists and this machine cannot sign its removal (not an
3404
3450
  * enrolled owner) — {@link stopStrand} stops the strand on this machine only — or when
3405
3451
  * the party-wide table cannot be read.
3406
3452
  */
3407
- forgetJoinedStrand(strandId: string): Promise<void>;
3453
+ forgetJoinedStrand(strandId: string, options?: StrandLeaveOptions): Promise<void>;
3454
+ /**
3455
+ * Before a leave changes anything: refuse while the strand's offline queue holds transactions,
3456
+ * unless the app asked to discard them (docs/strands.md → "Leaving a strand with unsent
3457
+ * transactions"). Transactions Optimystic will not withdraw stay in the journal, named in one
3458
+ * warning, and the leave goes on: they may already be saved, and a cohort that never returns
3459
+ * would otherwise block the leave forever.
3460
+ *
3461
+ * @throws StrandQueueNotEmptyError when the queue holds transactions and `discardQueued` is not set.
3462
+ * @throws when a hibernating strand has to be woken to discard and the wake fails.
3463
+ */
3464
+ private settleQueueBeforeLeave;
3465
+ /**
3466
+ * Discard from the strand's live queue, waking a hibernating strand first (its queue is back
3467
+ * once a build restores it from the journal), and return what was kept. `undefined` when the
3468
+ * wake built no database (no seed peer has answered), so there was no queue to discard from.
3469
+ */
3470
+ private discardQueuedForLeave;
3408
3471
  /**
3409
3472
  * Record `row` as a join from another party when this party's control database names it
3410
3473
  * neither as its own strand nor as a party-wide join — the one case nothing else would
@@ -9,6 +9,7 @@ import { controlClusterPolicy, CONTROL_REPLICATION_BREADTH, DEFAULT_CHECKIN_WIND
9
9
  import { generatePrivateKey } from '@optimystic/quereus-plugin-crypto';
10
10
  import { ed25519KeyPairFromLibp2p, ed25519PublicKeyFromPrivate, requireEd25519PublicKeyB64 } from './ed25519-key.js';
11
11
  import { strandTransportKey } from './strand-transport-key.js';
12
+ import { isSendingStatus, StrandOfflineUnavailableError, StrandQueueNotEmptyError } from './strand-offline.js';
12
13
  import { assertScopeKeyCharset, assertStrandScopeKey, controlStorageScope, InvalidStrandIdError, isValidStrandScopeKey } from './storage-scope.js';
13
14
  import { generateStrandMemberKey, strandMemberKeyPair } from './strand-member-key.js';
14
15
  import { assertNotPreSplitStrand, issueInvite, PreSplitStrandIdentityError, readStrandHeaderSAppId } from './strand-membership-writer.js';
@@ -58,6 +59,7 @@ import { StrandWakeService, dialWake } from './strand-wake-protocol.js';
58
59
  import { StrandAddrService, collectStrandAddrs } from './strand-addr-protocol.js';
59
60
  import { DelegateAdmissionStore, extractCircuitRelayTargets, dueRelayAnnounces, prunePeerStrandKeys, peerStrandKey, recordRelayRefusals, relaysNotRefusing } from './delegate-admission.js';
60
61
  import { relayCircuitAddrs, resolveListenAddrs, resolveTransportOptions, RelayReservationFailedError } from './relay-addrs.js';
62
+ import { selfAddressesChanged } from './self-address-update.js';
61
63
  import { nodeTransportsOption } from './node-transports.js';
62
64
  import { resolveRelayServer } from './relay-server.js';
63
65
  import { replacesAdvertisedAddrs, resolveAnnounceAddrs } from './announce-addrs.js';
@@ -3243,8 +3245,11 @@ export class CadreNode {
3243
3245
  }, { label: 'control-reconcile', backstopMs });
3244
3246
  // Address churn re-publishes the self record AND re-checks the control cohort:
3245
3247
  // a relay-reservation rotation / NAT change can drop a sibling connection, so
3246
- // the next pass should re-observe and re-dial it.
3247
- this.selfPeerUpdateHandler = () => {
3248
+ // the next pass should re-observe and re-dial it. Only address churn: libp2p also
3249
+ // fires this for a protocol handler registered or removed (see selfAddressesChanged).
3250
+ this.selfPeerUpdateHandler = (event) => {
3251
+ if (!selfAddressesChanged(event.detail))
3252
+ return;
3248
3253
  republish('self:peer:update');
3249
3254
  this.controlReconcileRuns?.trigger(0);
3250
3255
  };
@@ -5238,6 +5243,23 @@ export class CadreNode {
5238
5243
  void this.recordReplicaSAppId(strandId);
5239
5244
  this.emit('strand:writable', { strandId });
5240
5245
  }
5246
+ /**
5247
+ * A status from a strand's offline queue (`StartStrandConfig.offline`). The queue sending counts
5248
+ * as activity ({@link isSendingStatus}), so a check-in window that finds the cohort reachable stays
5249
+ * up while the queue drains, and the idle timer does not cut a drain short. Recorded through the
5250
+ * hibernation manager, not {@link recordStrandActivity}, whose push fan-out would wake other phones.
5251
+ *
5252
+ * Not on a strand being quiesced: activity there wakes it again once the quiesce ends, which would
5253
+ * undo a `hibernateAll` made as the app went to the background. A send that quiesce cuts short was
5254
+ * journaled before it went out, and the next build's restore re-sends it under the same action id.
5255
+ */
5256
+ handleOfflineStatus(event) {
5257
+ const instance = this.strandManager.getInstance(event.strandId);
5258
+ if (instance && isSendingStatus(event.status) && !this.strandManager.isQuiescing(event.strandId)) {
5259
+ this.hibernationManager.recordActivity(instance);
5260
+ }
5261
+ this.emit('strand:offline-status', event);
5262
+ }
5241
5263
  // Hibernation callbacks
5242
5264
  async handleStrandIdle(strandId) {
5243
5265
  const instance = this.strandManager.getInstance(strandId);
@@ -5640,6 +5662,31 @@ export class CadreNode {
5640
5662
  timeoutMs: options?.timeoutMs ?? this.config.strandFirstSync?.timeoutMs
5641
5663
  });
5642
5664
  }
5665
+ /**
5666
+ * Switch a strand between offline mode, where a commit resolves once the transaction is queued on
5667
+ * this machine, and online mode (docs/strands.md → "Offline mode"). Affects transactions begun from
5668
+ * now on; one already open keeps the acknowledgement it began with. The mode belongs to the strand:
5669
+ * a hibernating strand takes it at its next build, and it lasts across hibernation and every
5670
+ * rebuild until the process exits, after which the strand starts in `strandOffline.initialMode`.
5671
+ *
5672
+ * @throws StrandOfflineUnavailableError when `CadreNodeConfig.strandOffline` is not configured.
5673
+ * @throws when this node does not run the strand.
5674
+ */
5675
+ setStrandOfflineMode(strandId, mode) {
5676
+ if (!this.config.strandOffline) {
5677
+ throw new StrandOfflineUnavailableError(strandId);
5678
+ }
5679
+ this.strandManager.setOfflineMode(strandId, mode);
5680
+ }
5681
+ /**
5682
+ * A strand's offline mode ({@link setStrandOfflineMode}); `'online'` on a node without
5683
+ * `strandOffline`. The queue's own view, with what it holds, is `strand.database.offline.queueState()`.
5684
+ *
5685
+ * @throws when this node does not run the strand.
5686
+ */
5687
+ getStrandOfflineMode(strandId) {
5688
+ return this.strandManager.offlineMode(strandId);
5689
+ }
5643
5690
  /**
5644
5691
  * Publish a strand row to the shared control database under this node's own
5645
5692
  * owner identity, so other cadre members discover it via control-network
@@ -6008,16 +6055,25 @@ export class CadreNode {
6008
6055
  * caller ever needs removal to mean "and erase the local copy", that is a separate purge
6009
6056
  * step, not a widening of this method.
6010
6057
  *
6058
+ * Refuses while the strand's offline queue on this machine holds transactions, unless
6059
+ * `options.discardQueued` (docs/strands.md → "Leaving a strand with unsent transactions"). The
6060
+ * check and the discard run before the control-plane delete, so a delete refused afterwards
6061
+ * leaves the discarded transactions discarded.
6062
+ *
6011
6063
  * @param strandId - The `Strand` row id to remove (as passed to {@link publishStrand}).
6064
+ * @param options - `discardQueued`: withdraw the queued transactions that were never sent, and remove.
6065
+ * @throws StrandQueueNotEmptyError when the queue holds transactions and `discardQueued` is not
6066
+ * set; nothing was removed.
6012
6067
  * @throws if the node is not started, has no owner signer wired, the id is blank, or
6013
6068
  * the signer is not an enrolled owner (the schema's `Strand.AuthorizedDelete` rejects
6014
6069
  * the write and the row survives). A rejection does NOT imply the row survived: the
6015
6070
  * local stop runs after the control-plane delete has already committed, so a failure
6016
6071
  * there throws over a completed removal.
6017
6072
  */
6018
- async unpublishStrand(strandId) {
6073
+ async unpublishStrand(strandId, options) {
6019
6074
  const signer = this.requireOwnerSigner(`unpublish strand ${strandId}`);
6020
6075
  const trimmed = requireNonBlank(strandId, 'strand id');
6076
+ await this.settleQueueBeforeLeave(trimmed, options);
6021
6077
  // One approval: the strand delete and its party key's, each with its tombstone.
6022
6078
  const removed = await signer.approve({ partyId: this.partyId, action: `Remove the strand ${trimmed} from the cadre` }, (sign) => this.controlDatabase.deleteStrand(trimmed, signer.publicKey, sign));
6023
6079
  // Gate on `removed`: the absent-row no-op wrote nothing, so warning about an
@@ -6454,6 +6510,14 @@ export class CadreNode {
6454
6510
  onWritable: (writableStrandId) => this.handleStrandWritable(writableStrandId),
6455
6511
  onCommitDurability: (commit) => this.emit('strand:commit-durability', commit),
6456
6512
  onCommitHeld: (held) => this.emit('strand:commit-held', held),
6513
+ offline: this.config.strandOffline && {
6514
+ ...this.config.strandOffline,
6515
+ onOfflineStatus: (event) => this.handleOfflineStatus(event),
6516
+ onQueueState: (event) => this.emit('strand:queue-state', event),
6517
+ onAppSchemaDeferred: (event) => this.emit('strand:app-schema-deferred', event),
6518
+ onAppSchemaApplied: (event) => this.emit('strand:app-schema-applied', event),
6519
+ onAppSchemaFailed: (event) => this.emit('strand:error', event)
6520
+ },
6457
6521
  // Re-announce the delegate to ONE relay before the strand node's reservation
6458
6522
  // supervisor re-drives it (see announceDelegateToRelay). Retained with the
6459
6523
  // launch config, so a hibernation wake's rebuilt supervisors get it too.
@@ -7128,18 +7192,70 @@ export class CadreNode {
7128
7192
  * The other members of the strand are not told, and this party's membership row in the
7129
7193
  * strand stays, so a later re-formation reuses the same identity.
7130
7194
  *
7195
+ * Refuses while the strand's offline queue on this machine holds transactions, unless
7196
+ * `options.discardQueued` (docs/strands.md → "Leaving a strand with unsent transactions"). The
7197
+ * check and the discard run before the party-wide row is removed, so a removal refused
7198
+ * afterwards leaves the discarded transactions discarded.
7199
+ *
7200
+ * @param options - `discardQueued`: withdraw the queued transactions that were never sent, and leave.
7201
+ * @throws StrandQueueNotEmptyError when the queue holds transactions and `discardQueued` is not
7202
+ * set; nothing was left.
7131
7203
  * @throws when a party-wide row exists and this machine cannot sign its removal (not an
7132
7204
  * enrolled owner) — {@link stopStrand} stops the strand on this machine only — or when
7133
7205
  * the party-wide table cannot be read.
7134
7206
  */
7135
- async forgetJoinedStrand(strandId) {
7207
+ async forgetJoinedStrand(strandId, options) {
7136
7208
  if (!this._running) {
7137
7209
  throw new Error('CadreNode not running');
7138
7210
  }
7211
+ await this.settleQueueBeforeLeave(strandId, options);
7139
7212
  await this.joinedStrands.leave(strandId);
7140
7213
  this.forgetStrandPeers(strandId, 'forgetJoinedStrand');
7141
7214
  await this.stopStrand(strandId);
7142
7215
  }
7216
+ /**
7217
+ * Before a leave changes anything: refuse while the strand's offline queue holds transactions,
7218
+ * unless the app asked to discard them (docs/strands.md → "Leaving a strand with unsent
7219
+ * transactions"). Transactions Optimystic will not withdraw stay in the journal, named in one
7220
+ * warning, and the leave goes on: they may already be saved, and a cohort that never returns
7221
+ * would otherwise block the leave forever.
7222
+ *
7223
+ * @throws StrandQueueNotEmptyError when the queue holds transactions and `discardQueued` is not set.
7224
+ * @throws when a hibernating strand has to be woken to discard and the wake fails.
7225
+ */
7226
+ async settleQueueBeforeLeave(strandId, options) {
7227
+ const queued = this.strandManager.queuedTransactionIds(strandId);
7228
+ if (queued.length === 0) {
7229
+ return;
7230
+ }
7231
+ if (options?.discardQueued !== true) {
7232
+ throw new StrandQueueNotEmptyError(strandId, queued);
7233
+ }
7234
+ const kept = await this.discardQueuedForLeave(strandId);
7235
+ if (kept === undefined) {
7236
+ console.warn(`[sereus] strand ${strandId}: leaving with ${queued.length} queued transaction(s) not discarded ` +
7237
+ `(${queued.join(', ')}): the strand's database could not be built to reach its queue. They stay in ` +
7238
+ 'this machine\'s offline journal and come back if this machine attaches the strand again.');
7239
+ }
7240
+ else if (kept.length > 0) {
7241
+ console.warn(`[sereus] strand ${strandId}: leaving with ${kept.length} queued transaction(s) that could not be ` +
7242
+ `discarded (${kept.join(', ')}): each was sent, or is being sent, and the strand's members may already ` +
7243
+ 'hold it. They stay in this machine\'s offline journal and settle if this machine attaches the strand again.');
7244
+ }
7245
+ }
7246
+ /**
7247
+ * Discard from the strand's live queue, waking a hibernating strand first (its queue is back
7248
+ * once a build restores it from the journal), and return what was kept. `undefined` when the
7249
+ * wake built no database (no seed peer has answered), so there was no queue to discard from.
7250
+ */
7251
+ async discardQueuedForLeave(strandId) {
7252
+ const kept = this.strandManager.discardQueuedTransactions(strandId);
7253
+ if (kept !== undefined) {
7254
+ return kept;
7255
+ }
7256
+ await this.wakeStrand(strandId);
7257
+ return this.strandManager.discardQueuedTransactions(strandId);
7258
+ }
7143
7259
  /**
7144
7260
  * Record `row` as a join from another party when this party's control database names it
7145
7261
  * neither as its own strand nor as a party-wide join — the one case nothing else would