@serfab/cadre-core 0.11.0 → 0.13.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/announce-addrs.d.ts +49 -0
- package/dist/announce-addrs.js +78 -0
- package/dist/announce-addrs.js.map +1 -0
- package/dist/cadre-node.d.ts +607 -53
- package/dist/cadre-node.js +1268 -157
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +174 -23
- package/dist/control-database.js +317 -107
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +103 -0
- package/dist/control-read-retry.js +145 -0
- package/dist/control-read-retry.js.map +1 -0
- package/dist/control-retry.d.ts +101 -0
- package/dist/control-retry.js +108 -0
- package/dist/control-retry.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +50 -6
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +59 -35
- package/dist/control-write-retry.js +21 -92
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +17 -15
- package/dist/delegate-admission.js +17 -15
- package/dist/delegate-admission.js.map +1 -1
- package/dist/enrolled-machine-store-file.d.ts +13 -0
- package/dist/enrolled-machine-store-file.js +41 -0
- package/dist/enrolled-machine-store-file.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +81 -0
- package/dist/enrolled-machine-store.js +225 -0
- package/dist/enrolled-machine-store.js.map +1 -0
- package/dist/file-durable-slot.js +5 -3
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.js +39 -12
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +226 -26
- package/dist/membership-connection-gater.js +316 -30
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +6 -0
- package/dist/node-local-snapshot.js +7 -1
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +146 -56
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/peer-record.d.ts +26 -0
- package/dist/peer-record.js +32 -0
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +145 -22
- package/dist/relay-addrs.js +314 -31
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +20 -15
- package/dist/relay-reservation.js +18 -14
- package/dist/relay-reservation.js.map +1 -1
- package/dist/strand-addr-protocol.js +16 -0
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +12 -0
- package/dist/strand-database.js +16 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +22 -9
- package/dist/strand-formation-manager.js +20 -12
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +83 -9
- package/dist/strand-formation-protocol.js +86 -10
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +151 -17
- package/dist/strand-instance-manager.js +237 -32
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +118 -14
- package/dist/strand-membership-writer.js +203 -15
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +84 -0
- package/dist/strand-network-config.js +136 -0
- package/dist/strand-network-config.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +392 -0
- package/dist/strand-revocation-enforcer.js +533 -0
- package/dist/strand-revocation-enforcer.js.map +1 -0
- package/dist/strand-solicitation.d.ts +10 -3
- package/dist/strand-solicitation.js +12 -7
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +47 -2
- package/dist/strand-wake-protocol.js +76 -6
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +6 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +282 -43
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/announce-addrs.ts +93 -0
- package/src/cadre-node.ts +1366 -171
- package/src/control-database.ts +2628 -2406
- package/src/control-read-retry.ts +167 -0
- package/src/control-retry.ts +190 -0
- package/src/control-schema.ts +50 -6
- package/src/control-write-retry.ts +30 -130
- package/src/delegate-admission.ts +17 -15
- package/src/enrolled-machine-store-file.ts +43 -0
- package/src/enrolled-machine-store.ts +280 -0
- package/src/file-durable-slot.ts +5 -3
- package/src/index.ts +477 -407
- package/src/membership-connection-gater.ts +539 -204
- package/src/node-local-snapshot.ts +13 -1
- package/src/peer-addr-book.ts +251 -250
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +187 -75
- package/src/peer-record.ts +33 -0
- package/src/relay-addrs.ts +436 -111
- package/src/relay-reservation.ts +828 -823
- package/src/strand-addr-protocol.ts +17 -0
- package/src/strand-database.ts +17 -0
- package/src/strand-formation-manager.ts +511 -491
- package/src/strand-formation-protocol.ts +808 -692
- package/src/strand-instance-manager.ts +899 -611
- package/src/strand-membership-writer.ts +232 -15
- package/src/strand-network-config.ts +172 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +484 -472
- package/src/strand-wake-protocol.ts +94 -8
- package/src/strand-watcher.ts +319 -314
- package/src/types.ts +1554 -1309
- package/dist/strand-backfill.js.map +0 -1
|
@@ -154,6 +154,21 @@ export declare function formationConsentMessage(fields: {
|
|
|
154
154
|
peerKey: string;
|
|
155
155
|
disclosure: string;
|
|
156
156
|
}): Uint8Array;
|
|
157
|
+
/**
|
|
158
|
+
* Did this write fail because the strand id is already seated?
|
|
159
|
+
*
|
|
160
|
+
* {@link CadreNode.publishStrand} uses this to tell "my own earlier publish already
|
|
161
|
+
* landed" (re-read, and no-op when the row matches) apart from every other rejection —
|
|
162
|
+
* an unauthorized signer, a retired stamp — which must keep surfacing.
|
|
163
|
+
*
|
|
164
|
+
* Matched by TEXT, not by type: the typed engine error does not survive the trip out of
|
|
165
|
+
* optimystic (same constraint the retry classifiers in `control-write-retry.ts` document).
|
|
166
|
+
* Fails CLOSED — a rewording upstream turns the idempotent branch back into the raw
|
|
167
|
+
* uniqueness error the caller saw before, never into a silent overwrite. The
|
|
168
|
+
* `publish-strand.spec.ts` repeat-publish cases assert against the live engine error, so a
|
|
169
|
+
* reword reddens there.
|
|
170
|
+
*/
|
|
171
|
+
export declare function isStrandIdConflict(error: unknown): boolean;
|
|
157
172
|
/**
|
|
158
173
|
* Guarded tables a node may reap locally once their tombstone has committed — the
|
|
159
174
|
* tables whose `AuthorizedDelete` carries the REAP branch (see the constraint comment on
|
|
@@ -227,12 +242,111 @@ export declare class ControlDatabase {
|
|
|
227
242
|
* having to restate the policy's attempts or classifier.
|
|
228
243
|
*/
|
|
229
244
|
private controlWriteRetryPacing;
|
|
245
|
+
/**
|
|
246
|
+
* Pacing seams for {@link readRows}'s retry — the read twin of
|
|
247
|
+
* {@link controlWriteRetryPacing}, with the same contract: production leaves this
|
|
248
|
+
* empty, specs inject a recorded `sleep` / fake `now` so no test waits out a real
|
|
249
|
+
* backoff. Applied OVER the read policy's defaults but UNDER the call site's `label`.
|
|
250
|
+
*/
|
|
251
|
+
private controlReadRetryPacing;
|
|
230
252
|
constructor(config: ControlDatabaseConfig);
|
|
231
253
|
/**
|
|
232
254
|
* Initialize the database - load schema and register plugins
|
|
233
255
|
*/
|
|
234
256
|
initialize(): Promise<void>;
|
|
235
257
|
private loadSchema;
|
|
258
|
+
/**
|
|
259
|
+
* One UNRETRIED drain of a control read to rows — every read in this class bottoms out
|
|
260
|
+
* here (almost all via {@link readRows}, which adds the transient-failure retry), and
|
|
261
|
+
* this is the ONE place the committed-read opt-in is spelled.
|
|
262
|
+
*
|
|
263
|
+
* Reached only through {@link readRows}, which drops straight here for the call sites
|
|
264
|
+
* that pass `retry: false` — reads issued from INSIDE a locked write body, where a
|
|
265
|
+
* backoff sleep would hold the write lock and the write funnel re-runs the read anyway
|
|
266
|
+
* (see the NOTE on {@link readRows}).
|
|
267
|
+
*
|
|
268
|
+
* A control read normally queues on the database's exec mutex, so it does not answer
|
|
269
|
+
* until whatever statement holds that mutex finishes. When a control WRITE is parked
|
|
270
|
+
* against an unresponsive cohort member that is minutes, and a read that only wants
|
|
271
|
+
* the last committed state waits the whole time for no reason
|
|
272
|
+
* (`complete/control-reads-blocked-by-stalled-write`).
|
|
273
|
+
*
|
|
274
|
+
* Quereus's remedy is `readConcurrency: 'committed'` — a PER-CALL opt-in
|
|
275
|
+
* (`Statement.tryRouteConcurrent` refuses anything that did not ask for it by name)
|
|
276
|
+
* that runs an eligible read off the mutex against each table's last committed state.
|
|
277
|
+
* Routing is best-effort and never an error: an ineligible statement falls back to
|
|
278
|
+
* the serialized path silently — any side-effecting node, any table-valued function
|
|
279
|
+
* call (pure ones included), any table whose module does not declare
|
|
280
|
+
* `readCommittedSnapshot`, or an EXPLICIT transaction being open on this database.
|
|
281
|
+
*
|
|
282
|
+
* **It is asked for only while a write is in flight, and that condition is the whole
|
|
283
|
+
* point.** The opt-in does not merely move the read off the mutex: it connects the
|
|
284
|
+
* table with `_readCommitted`, and the optimystic vtab serves such a connection from
|
|
285
|
+
* a pinned pre-transaction snapshot that NEVER refreshes from the network, where an
|
|
286
|
+
* ordinary read calls `update()` on the collection first. Taking that path
|
|
287
|
+
* unconditionally therefore trades "reads block behind a stalled write" for "reads
|
|
288
|
+
* stop seeing what other machines wrote" — measured, not theorised: with every read
|
|
289
|
+
* opted in, `control-write-degraded-cohort-member` could no longer observe a sibling
|
|
290
|
+
* publishing its own `CadrePeer` row and failed at suite setup on two runs out of two.
|
|
291
|
+
*
|
|
292
|
+
* `getAutocommit()` reports the whole `Database` rather than this call (the same
|
|
293
|
+
* property {@link assertCommitBoundary} leans on), which is exactly what is wanted:
|
|
294
|
+
* false means some writer's transaction is open, which is the only situation where a
|
|
295
|
+
* read would have blocked. The check races that writer harmlessly in both directions
|
|
296
|
+
* — losing it either reads normally (today's behaviour) or reads committed state.
|
|
297
|
+
*
|
|
298
|
+
* What a committed read gives up is observing a write that has not finished
|
|
299
|
+
* committing, and that is safe for every caller here: an AWAITED write is committed by
|
|
300
|
+
* the time it returns, and a read inside {@link inTransaction} is disqualified by the
|
|
301
|
+
* open explicit transaction and stays serialized, so it still sees its own
|
|
302
|
+
* transaction's rows. A read after an UNAWAITED write on this database would be wrong
|
|
303
|
+
* — no such caller exists, and {@link withWriteLock} is what keeps it that way.
|
|
304
|
+
*/
|
|
305
|
+
private readRowsOnce;
|
|
306
|
+
/**
|
|
307
|
+
* {@link readRowsOnce} plus the bounded transient-failure retry
|
|
308
|
+
* ({@link retryControlRead}) — the funnel every UNLOCKED control read goes through, so a
|
|
309
|
+
* read that failed because the cluster could not be asked properly (a stream reset
|
|
310
|
+
* mid-scan, a partially unreachable cohort) is re-presented a moment later instead of
|
|
311
|
+
* surfacing to every reader.
|
|
312
|
+
*
|
|
313
|
+
* MATERIALIZES the read rather than returning a lazy iterator, and that shape is what
|
|
314
|
+
* makes the retry possible at all: the failure happens during ITERATION, not at call
|
|
315
|
+
* time, and a half-consumed iterator cannot be retried without re-yielding rows the
|
|
316
|
+
* caller already saw. Materializing is safe here — every caller either drains to an
|
|
317
|
+
* array anyway or takes the first row of a statement that yields at most one (a
|
|
318
|
+
* primary-key lookup or a `count(1)`); nothing streams.
|
|
319
|
+
*
|
|
320
|
+
* Each ATTEMPT is a fresh {@link readRowsOnce} call, so the committed-read opt-in
|
|
321
|
+
* (`getAutocommit()`) is re-evaluated per attempt — a write can finish between attempts
|
|
322
|
+
* and change the right answer.
|
|
323
|
+
*
|
|
324
|
+
* The retry's budget ({@link CONTROL_READ_RETRY_BUDGET_MS}, 1.5 s) is deliberately far
|
|
325
|
+
* under the write funnel's: the tightest caller deadline over a control read is the
|
|
326
|
+
* inbound admission gate's 2 s FAIL-OPEN timeout, and a read that outlives it spends its
|
|
327
|
+
* retries after the gate has already admitted (see `control-read-retry.ts`).
|
|
328
|
+
*
|
|
329
|
+
* `label` names the read in the retry loop's `Control read [<label>] …` debug lines and
|
|
330
|
+
* nothing else; several reads are in flight concurrently in a real party, so an
|
|
331
|
+
* unlabelled line cannot be attributed — keep new call sites labelled.
|
|
332
|
+
*
|
|
333
|
+
* `retry: false` drops straight to {@link readRowsOnce}: reads issued from INSIDE a
|
|
334
|
+
* locked write body must not retry on their own. `retryControlWrite`'s contract is that
|
|
335
|
+
* backoff sleeps happen with NO lock held; a read retrying its own backoff inside a
|
|
336
|
+
* locked body would sleep holding the write lock (stalling every other local writer),
|
|
337
|
+
* and the write funnel already re-runs the body's reads when it re-runs the body. See
|
|
338
|
+
* {@link queryStampId} / {@link assertSeatRemains} / {@link queryCadrePeers} for the
|
|
339
|
+
* per-call opt-outs.
|
|
340
|
+
*
|
|
341
|
+
* NOTE: nothing structural enforces that opt-out — it is per-call because an unlocked
|
|
342
|
+
* read runs CONCURRENTLY with a locked body, so `this`-state cannot tell the two apart,
|
|
343
|
+
* and there is no async-context primitive available on every target platform (browser,
|
|
344
|
+
* React Native) to carry the answer. Reads reached through a CALLBACK invoked under the
|
|
345
|
+
* lock are the easy miss: `notifyMembershipChanged` runs its listener with the lock
|
|
346
|
+
* held, and its membership read is opted out at {@link queryCadrePeers}. If another
|
|
347
|
+
* under-lock callback seam is ever added, audit its read graph the same way.
|
|
348
|
+
*/
|
|
349
|
+
private readRows;
|
|
236
350
|
/**
|
|
237
351
|
* Query all strands from the control database
|
|
238
352
|
*/
|
|
@@ -304,8 +418,17 @@ export declare class ControlDatabase {
|
|
|
304
418
|
* carried into both, or those suites keep passing against a contract the database no
|
|
305
419
|
* longer honours. Fine while the filter is one `Set.has` on `StampId`; if it grows a
|
|
306
420
|
* second dimension, give the fakes a shared fixture or a contract test instead.
|
|
421
|
+
*
|
|
422
|
+
* `retry: false` is passed by exactly one caller —
|
|
423
|
+
* `CadreNode.refreshAuthorizedControlPeers`, the membership-gate snapshot refresh —
|
|
424
|
+
* because {@link notifyMembershipChanged} invokes that listener with the write lock
|
|
425
|
+
* HELD, and a retrying read there would sleep its backoff holding the lock (see the
|
|
426
|
+
* NOTE on {@link readRows}). It costs nothing: that refresh is best-effort, keeps the
|
|
427
|
+
* previous snapshot on failure, and is re-driven by the next write and by the timed
|
|
428
|
+
* reconcile. BOTH of this method's reads take the flag — retrying either one under the
|
|
429
|
+
* lock is the same defect.
|
|
307
430
|
*/
|
|
308
|
-
queryCadrePeers(): Promise<CadrePeerRow[]>;
|
|
431
|
+
queryCadrePeers(retry?: boolean): Promise<CadrePeerRow[]>;
|
|
309
432
|
/**
|
|
310
433
|
* Every approver public key currently enrolled in `CadreControl.ValidationKey` — the
|
|
311
434
|
* set a `ValidationUrl` redemption's approval signature must be signed by (see
|
|
@@ -330,6 +453,16 @@ export declare class ControlDatabase {
|
|
|
330
453
|
* `table` and its {@link GUARDED_KEY_COLUMN} column are interpolated into the SQL; both
|
|
331
454
|
* come from closed literal unions — no caller-supplied string can reach the statement
|
|
332
455
|
* (same injection-surface discipline as {@link countRows}'s `CONTROL_TABLE_SET` guard).
|
|
456
|
+
*
|
|
457
|
+
* NOTE: `retry: false` is passed by exactly the call sites that run INSIDE a locked
|
|
458
|
+
* write body — {@link deleteGuardedRow} and {@link insertCadrePeer}'s insert-if-absent
|
|
459
|
+
* guard. A read retrying there would sleep its backoff HOLDING the write lock
|
|
460
|
+
* (`retryControlWrite`'s contract is that backoff sleeps happen with no lock held), and
|
|
461
|
+
* the write funnel already re-runs the body's reads when it re-runs the body. The
|
|
462
|
+
* unlocked callers — the public stamp readers, {@link reauthorizeCadrePeer} and
|
|
463
|
+
* {@link reapRevokedRow}, whose stamp reads run BEFORE their locks are taken — keep the
|
|
464
|
+
* default retried path. Explicit per call rather than an ambient flag: unlocked reads
|
|
465
|
+
* run concurrently with a locked body, so `this`-state cannot tell the two apart.
|
|
333
466
|
*/
|
|
334
467
|
private queryStampId;
|
|
335
468
|
/**
|
|
@@ -364,8 +497,11 @@ export declare class ControlDatabase {
|
|
|
364
497
|
* NOTE: re-reads the whole retired set on every call, and the caller runs per inbound
|
|
365
498
|
* gate request while the table only ever grows. Cheap today (a cadre removes peers
|
|
366
499
|
* rarely); if removals ever become routine, cache the set and invalidate it on write.
|
|
500
|
+
*
|
|
501
|
+
* `retry: false` is forwarded by {@link queryCadrePeers} for the one caller that reads
|
|
502
|
+
* under the write lock — see that method's note.
|
|
367
503
|
*/
|
|
368
|
-
queryRevokedStamps(tableName: RevocableTable): Promise<Set<string>>;
|
|
504
|
+
queryRevokedStamps(tableName: RevocableTable, retry?: boolean): Promise<Set<string>>;
|
|
369
505
|
/**
|
|
370
506
|
* Every locally-held `CadreControl.Revocation` tombstone — identity triple plus its
|
|
371
507
|
* `ReissuedAt` counter. Consumed by the cohort-growth re-issue sweep, which
|
|
@@ -386,7 +522,7 @@ export declare class ControlDatabase {
|
|
|
386
522
|
* {@link queryRevokedStamps} rather than inlining the SQL, so tests can interpose on
|
|
387
523
|
* that seam (see {@link queryCadrePeers}).
|
|
388
524
|
*
|
|
389
|
-
* Missing/
|
|
525
|
+
* Missing/null column values are coalesced
|
|
390
526
|
* to their empty form (`''` key/sig, `[]` addrs, `0` stamp) so the caller's
|
|
391
527
|
* verify/freshness gates uniformly reject an unpublished or malformed row.
|
|
392
528
|
* The split `addrs` re-join to the exact stored `Multiaddr` (split-on-`,` is
|
|
@@ -421,7 +557,7 @@ export declare class ControlDatabase {
|
|
|
421
557
|
/**
|
|
422
558
|
* Read a single peer's device push token (the full `DeviceToken` row) by PeerId.
|
|
423
559
|
*
|
|
424
|
-
* Returns null when no row exists. Missing/
|
|
560
|
+
* Returns null when no row exists. Missing/null column values are coalesced to
|
|
425
561
|
* their empty form (`''` token/sig, `0` stamp) so the caller's verify/freshness
|
|
426
562
|
* gates uniformly reject an unpublished or malformed row. `platform` is returned
|
|
427
563
|
* verbatim (the resolver validates it against {@link PushPlatform} and re-verifies
|
|
@@ -451,6 +587,10 @@ export declare class ControlDatabase {
|
|
|
451
587
|
/**
|
|
452
588
|
* Insert a strand into the control database using an owner signature.
|
|
453
589
|
*
|
|
590
|
+
* Fails with a `Strand.Id` uniqueness violation when the id is already seated — the
|
|
591
|
+
* caller decides whether that is a duplicate of its own earlier write (idempotent) or a
|
|
592
|
+
* genuine conflict; see {@link isStrandIdConflict} and `CadreNode.publishStrand`.
|
|
593
|
+
*
|
|
454
594
|
* The owner signs the canonical row-bound authorization message (see
|
|
455
595
|
* {@link buildAuthorizationMessage}) — NOT a bare stamp — so the signature is bound to
|
|
456
596
|
* this strand's contents and cannot be transplanted onto an attacker-chosen row. The
|
|
@@ -890,7 +1030,8 @@ export declare class ControlDatabase {
|
|
|
890
1030
|
* {@link assertCommitBoundary} (any `CadrePeer` path) or silently join each other's
|
|
891
1031
|
* open transaction — a torn commit if either side rolls back. This lock closes that
|
|
892
1032
|
* class: every public write method of this class runs its statement(s) under it, and
|
|
893
|
-
* `SeedBootstrapService`
|
|
1033
|
+
* `SeedBootstrapService` reaches it through {@link execWrite} rather than touching the
|
|
1034
|
+
* `Database` directly, so it is covered too. The race is
|
|
894
1035
|
* real, not theoretical: the background self-record publish on a control connection
|
|
895
1036
|
* opening (`CadreNode.drainPendingControlReplication` → `registerSelf`) collided with
|
|
896
1037
|
* a foreground `authorizePeer` — both `CadrePeer` inserts — and tripped the boundary
|
|
@@ -1008,7 +1149,8 @@ export declare class ControlDatabase {
|
|
|
1008
1149
|
* `StrandId` binds the invite to a pre-existing host strand (provision-then-record):
|
|
1009
1150
|
* when set, a responder redeeming this token records a `FormationUsage` against that
|
|
1010
1151
|
* strand and returns it (see {@link ControlFormationUsageRecorder.resolveStrand}); a
|
|
1011
|
-
* null `StrandId` (the default)
|
|
1152
|
+
* null `StrandId` (the default) takes the unbound responder-provisions path: the
|
|
1153
|
+
* responder provisions a fresh open strand and atomically records its one consent row.
|
|
1012
1154
|
* Like ValidationUrl it is a nullable bound field, signed as `''` when absent.
|
|
1013
1155
|
*
|
|
1014
1156
|
* The ExpiresAt and TotalUses message fields must byte-match what the (auto-deferred,
|
|
@@ -1173,8 +1315,12 @@ export declare class ControlDatabase {
|
|
|
1173
1315
|
* Read a `FormationInvite` row by token, or null when absent. `expiresAtMs` is
|
|
1174
1316
|
* the parsed epoch-ms of the stored `datetime` (null when the invite never
|
|
1175
1317
|
* expires); the caller compares it against the wall clock for freshness.
|
|
1318
|
+
*
|
|
1319
|
+
* `retry: false` is passed only by {@link assertSeatRemains}, which runs INSIDE a
|
|
1320
|
+
* locked write body — same per-call opt-out, and for the same reason, as
|
|
1321
|
+
* {@link queryStampId}'s. Every unlocked caller keeps the default retried path.
|
|
1176
1322
|
*/
|
|
1177
|
-
queryFormationInvite(token: string): Promise<{
|
|
1323
|
+
queryFormationInvite(token: string, retry?: boolean): Promise<{
|
|
1178
1324
|
token: string;
|
|
1179
1325
|
sAppId: string;
|
|
1180
1326
|
expiresAtMs: number | null;
|
|
@@ -1185,23 +1331,28 @@ export declare class ControlDatabase {
|
|
|
1185
1331
|
/**
|
|
1186
1332
|
* Count `FormationUsage` rows recorded against a token (uses consumed so far).
|
|
1187
1333
|
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1334
|
+
* This count IS the seat cap (`enforceFormationUseCap`), so an under-report admits a seat
|
|
1335
|
+
* the invitation never paid for. It is served by a table SCAN, deliberately: a bare
|
|
1336
|
+
* equality on a non-key column is a shape the optimystic vtab declines to claim, so the
|
|
1337
|
+
* engine applies the filter itself over a full read of the table.
|
|
1338
|
+
*
|
|
1339
|
+
* NOTE: it was briefly an index seek, and that is what the scan is protecting against.
|
|
1340
|
+
* `formation-unique-token-redesign` took `Token` out of the primary key and declared
|
|
1341
|
+
* `FormationUsageByToken` purely to spare this scan, recording as a tripwire that an
|
|
1342
|
+
* under-reporting descent would admit an unpaid-for seat — "a failure mode a scan could
|
|
1343
|
+
* not produce". The tripwire fired. Across machines a secondary-index descent returns only
|
|
1344
|
+
* the rows the READING machine wrote, so this count under-reported without bound on a
|
|
1345
|
+
* multi-machine party and the cap over-admitted. The index was removed 2026-08-25; the
|
|
1346
|
+
* measurement is recorded in the block comment at the `FormationUsage` declaration in
|
|
1347
|
+
* `schemas/control.qsql`, and the engine defect in
|
|
1348
|
+
* `blocked/secondary-index-seek-blind-to-sibling-rows`. Do NOT re-declare the index to
|
|
1349
|
+
* speed this up while that ticket is open.
|
|
1350
|
+
*
|
|
1351
|
+
* `retry: false` is passed only by {@link assertSeatRemains}, which runs INSIDE a
|
|
1352
|
+
* locked write body — same per-call opt-out, and for the same reason, as
|
|
1353
|
+
* {@link queryStampId}'s. Every unlocked caller keeps the default retried path.
|
|
1203
1354
|
*/
|
|
1204
|
-
countFormationUsage(token: string): Promise<number>;
|
|
1355
|
+
countFormationUsage(token: string, retry?: boolean): Promise<number>;
|
|
1205
1356
|
/**
|
|
1206
1357
|
* Is any `FormationInvite` row still redeemable — unexpired AND with usage
|
|
1207
1358
|
* below its `TotalUses`? A null `ExpiresAt` never expires and a null
|