@serfab/cadre-core 0.12.0 → 1.0.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 +23 -8
- package/dist/bootstrap-peer-store-file.js +2 -2
- package/dist/bootstrap-peer-store.d.ts +32 -12
- package/dist/bootstrap-peer-store.js +41 -15
- package/dist/bootstrap-peer-store.js.map +1 -1
- package/dist/cadre-node.d.ts +883 -102
- package/dist/cadre-node.js +1800 -209
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +13 -26
- package/dist/control-cohort.js +0 -26
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +318 -44
- package/dist/control-database.js +516 -74
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +40 -11
- package/dist/control-read-retry.js +48 -15
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +58 -3
- package/dist/control-retry.js +58 -3
- package/dist/control-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +133 -21
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +168 -29
- package/dist/control-write-retry.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 +17 -8
- package/dist/index.js +54 -9
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +15 -1
- package/dist/membership-connection-gater.js +9 -3
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +21 -7
- package/dist/node-local-snapshot.js +30 -9
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -0
- package/dist/peer-dial.js +176 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/relay-addrs.d.ts +160 -49
- package/dist/relay-addrs.js +287 -60
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +71 -3
- package/dist/relay-reservation.js +116 -8
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +30 -1
- package/dist/seed-bootstrap.js +62 -17
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +62 -0
- package/dist/storage-scope.js +75 -0
- package/dist/storage-scope.js.map +1 -0
- package/dist/strand-database.d.ts +41 -11
- package/dist/strand-database.js +48 -18
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +152 -0
- package/dist/strand-first-sync-gate.js +208 -0
- package/dist/strand-first-sync-gate.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +99 -5
- package/dist/strand-formation-manager.js +119 -8
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +97 -3
- package/dist/strand-formation-protocol.js +108 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +389 -19
- package/dist/strand-instance-manager.js +678 -65
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +20 -12
- package/dist/strand-member-key.js +20 -12
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.js +3 -0
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +361 -0
- package/dist/strand-membership-reconciler.js +548 -0
- package/dist/strand-membership-reconciler.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +143 -11
- package/dist/strand-membership-writer.js +419 -135
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +116 -0
- package/dist/strand-network-config.js +192 -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 +17 -1
- package/dist/strand-solicitation.js +13 -2
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +11 -7
- package/dist/strand-wake-protocol.js +20 -52
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +12 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/timed-step.d.ts +12 -0
- package/dist/timed-step.js +28 -0
- package/dist/timed-step.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +19 -0
- package/dist/timeout-scheduler.js +22 -0
- package/dist/timeout-scheduler.js.map +1 -0
- package/dist/types.d.ts +472 -46
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +1926 -228
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +586 -74
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +133 -21
- package/src/control-write-retry.ts +525 -383
- 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 +134 -15
- package/src/membership-connection-gater.ts +9 -3
- package/src/node-local-snapshot.ts +38 -9
- package/src/peer-addr-book.ts +251 -250
- package/src/peer-dial.ts +215 -0
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
- package/src/relay-addrs.ts +458 -218
- package/src/relay-reservation.ts +951 -828
- package/src/seed-bootstrap.ts +76 -18
- package/src/storage-scope.ts +78 -0
- package/src/strand-database.ts +61 -21
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +162 -8
- package/src/strand-formation-protocol.ts +850 -692
- package/src/strand-instance-manager.ts +877 -74
- package/src/strand-member-key.ts +20 -12
- package/src/strand-member-registry.ts +3 -0
- package/src/strand-membership-reconciler.ts +680 -0
- package/src/strand-membership-writer.ts +550 -190
- package/src/strand-network-config.ts +250 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +499 -471
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +325 -314
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +487 -52
- package/dist/strand-backfill.js.map +0 -1
|
@@ -14,8 +14,19 @@ import type { ControlRetryOptions } from './control-retry.js';
|
|
|
14
14
|
* a moment later, which the degraded-cohort-member scenario measured as safe (a failed
|
|
15
15
|
* write rolls back, nothing half-commits) and effective (the next write commits in ~1 s).
|
|
16
16
|
*
|
|
17
|
-
* A write somebody actually REJECTED is never retried
|
|
18
|
-
*
|
|
17
|
+
* A write somebody actually REJECTED is never retried on the strength of the rejection itself —
|
|
18
|
+
* no matcher here claims a rejection message, at any phase, because re-presenting one would
|
|
19
|
+
* re-present a spent signature against a cohort that already said no. Rejections are re-presented
|
|
20
|
+
* anyway when they arrive inside the transactor's promise-phase `[block:` aggregate, which
|
|
21
|
+
* {@link isUncommittedTransactorAggregate} claims on the WRAPPER alone, whatever the cause inside
|
|
22
|
+
* says. Safe because nothing has committed at that phase, and deliberately kept — one of the
|
|
23
|
+
* remaining promise-phase rejections is exactly the kind a re-presentation fixes. See the
|
|
24
|
+
* accepted-tradeoff `NOTE:` there for which, and why no blanket rejection veto is wanted.
|
|
25
|
+
*
|
|
26
|
+
* A failure that says some of the write may be STORED is a different matter, and never
|
|
27
|
+
* re-presented whatever text it carries: {@link reportsPossiblyStoredWrite} vetoes it by type
|
|
28
|
+
* before any matcher runs. The one half-landed write that IS re-presented is a torn write the
|
|
29
|
+
* library marks final — not saved and unable to land ({@link isFinalTornWrite}).
|
|
19
30
|
*
|
|
20
31
|
* TWO policies ship from here, and the split is deliberate: the default
|
|
21
32
|
* ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
|
|
@@ -63,6 +74,17 @@ export declare const SCHEMA_INIT_ATTEMPTS = 5;
|
|
|
63
74
|
* attempts, measured in `control-write-degraded-cohort-member.integration.ts`), which
|
|
64
75
|
* already exceeds this budget when attempt 1 returns — so that case is surfaced immediately
|
|
65
76
|
* and retry adds ZERO latency to the case where it cannot help.
|
|
77
|
+
*
|
|
78
|
+
* NOTE: a failed commit attempt now also pays a cancel discharge before its error returns
|
|
79
|
+
* (optimystic `TransactorSource.transact`'s catch, added by upstream
|
|
80
|
+
* `1-a-failed-attempt-must-discharge-its-own-pend`) — bounded by six rounds and
|
|
81
|
+
* `abortOrCancelTimeoutMs`, which every collection this repo opens sets to 5 s
|
|
82
|
+
* (`../optimystic/packages/quereus-plugin-optimystic/src/optimystic-adapter/collection-factory.ts`).
|
|
83
|
+
* Two failed attempts whose cancels each run their full budget would consume this whole 10 s
|
|
84
|
+
* ceiling and cut the three-attempt policy to two. Not observed — every measured round of the
|
|
85
|
+
* transient-reset case committed on attempt 3 of 3 — so this is a condition to watch, not work
|
|
86
|
+
* to do: if that case ever starts failing with `failed after 2/3 attempt(s)`, the cancel
|
|
87
|
+
* discharge is where the time went.
|
|
66
88
|
*/
|
|
67
89
|
export declare const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
|
|
68
90
|
/**
|
|
@@ -101,6 +123,42 @@ export declare const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
|
|
|
101
123
|
* token on the live aggregate; if it reddens there, an upstream reformat is the first thing to
|
|
102
124
|
* check.
|
|
103
125
|
*
|
|
126
|
+
* NOTE: the discriminator is the prefix AND the token within ONE message — never relax it to
|
|
127
|
+
* "contains `[block:`". Beyond the `get` and `pend` sites above, Optimystic renders `[block:` in
|
|
128
|
+
* one further place — a third formatter, outside the aggregate's three sites — `dischargeCancel`
|
|
129
|
+
* (`Cancel of action <id> did not discharge <n> block(s): …`), which does not raise the
|
|
130
|
+
* `Some peers did not complete:` prefix; only the conjunction keeps a cancel fault from
|
|
131
|
+
* classifying as a retriable get/pend (checked against optimystic `c56c2bd4`, 2026-09-16).
|
|
132
|
+
*
|
|
133
|
+
* NOTE: accepted tradeoff — a REJECTION raised while promises are still being collected reaches
|
|
134
|
+
* this repo wrapped in this same aggregate, and this matcher claims it on the wrapper alone,
|
|
135
|
+
* whatever the cause inside says. That re-presents such a rejection up to two more times inside
|
|
136
|
+
* the 10 s budget. Kept deliberately, and re-decided on 2026-09-17 against the current upstream
|
|
137
|
+
* behaviour rather than inherited:
|
|
138
|
+
*
|
|
139
|
+
* - Contention is no longer a rejection at all. "Another write holds this block right now"
|
|
140
|
+
* (`pending conflict`) is now a `held` verdict that counts toward neither approvals nor
|
|
141
|
+
* rejections (`validatePendOperations`,
|
|
142
|
+
* `../optimystic/packages/db-p2p/src/cluster/cluster-repo.ts`), so the rejection this tradeoff
|
|
143
|
+
* was originally written about does not arrive any more. It reaches this funnel as
|
|
144
|
+
* `SyncRetryExhaustedError` instead, which {@link reportsPossiblyStoredWrite} declines by type
|
|
145
|
+
* — upstream documents resubmitting after it as unsafe (the attempt that spent the budget may
|
|
146
|
+
* have left its log entry standing), and its collection sync has already spent ten retries on
|
|
147
|
+
* the race. The torn-write rule beside it: a `TornActionError` is re-presented only when
|
|
148
|
+
* `final` is true; any other torn write is declined.
|
|
149
|
+
* - What remains rejectable at the promise phase is stale revision, block-unavailable,
|
|
150
|
+
* membership-not-admitted, and a configured validator's refusal. Only the first is worth a
|
|
151
|
+
* retry — and it is worth one: this loop re-runs the WHOLE write body, reads included
|
|
152
|
+
* (`ControlDatabase.withWriteLock`'s contract), so attempt 2 presents a pend against the
|
|
153
|
+
* revision that made attempt 1 stale. A blanket "decline any chain reporting a validator
|
|
154
|
+
* rejection" rule would remove that retry to save two wasted attempts on the other three.
|
|
155
|
+
*
|
|
156
|
+
* Revisit if a re-presentation ever becomes expensive relative to what it buys — a rejection class
|
|
157
|
+
* that costs a full cohort round-trip to re-present, or a write body whose reads are no longer
|
|
158
|
+
* cheap — or when upstream offers a typed refusal surface to classify on instead of this text
|
|
159
|
+
* (`../optimystic/tickets/backlog/debt-a-downstream-repo-classifies-retries-by-parsing-our-error-text`).
|
|
160
|
+
* Contention is no longer the reason to revisit.
|
|
161
|
+
*
|
|
104
162
|
* An aggregate whose details came out EMPTY (possible when `formatBatchStatuses` has no
|
|
105
163
|
* batches to format) carries neither token, matches nothing here, and is not retried — an
|
|
106
164
|
* unattributable failure is not a proven non-commit.
|
|
@@ -114,16 +172,18 @@ export declare function isUncommittedTransactorAggregate(message: string): boole
|
|
|
114
172
|
* Did this control write fail because the cluster cohort did not ANSWER — i.e. is
|
|
115
173
|
* re-presenting the SAME signed write a moment later the right response?
|
|
116
174
|
*
|
|
117
|
-
* Classifies by MESSAGE, walking the `cause` chain with the shared
|
|
118
|
-
* (Quereus' own `unwrapError` underneath) —
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
175
|
+
* Classifies the transient cases by MESSAGE, walking the `cause` chain with the shared
|
|
176
|
+
* {@link chainMessages} (Quereus' own `unwrapError` underneath) — the transactor and the
|
|
177
|
+
* coordinator both throw bare `Error`s, not recognisable by type. By the time one reaches
|
|
178
|
+
* `ControlDatabase` it is wrapped in a `QuereusError` (the real chain is `QuereusError` → `Error`
|
|
179
|
+
* → `Error`), so the match must work at any depth. Anything that is not an `Error` is never
|
|
180
|
+
* retried.
|
|
123
181
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
182
|
+
* Two vetoes run over the whole chain before any matcher: {@link reportsPossiblyStoredWrite}
|
|
183
|
+
* (by type — a spent sync budget, a non-final torn write, a partial commit) and
|
|
184
|
+
* {@link reportsIndeterminateCommit} (by text — a commit-phase batch). A chain either veto flags,
|
|
185
|
+
* at any level, is never retried on the strength of some other level looking transient. The one
|
|
186
|
+
* typed failure claimed as retriable is a final torn write ({@link isFinalTornWrite}).
|
|
127
187
|
*
|
|
128
188
|
* NOTE: this depends on engine/transactor error TEXT. The messages producible without a
|
|
129
189
|
* network are driven from the REAL engine in `control-formation-seat-budget.spec.ts`, so
|
|
@@ -181,5 +241,14 @@ export declare const SCHEMA_INIT_RETRY_POLICY: Readonly<ControlWriteRetryOptions
|
|
|
181
241
|
* On exhaustion (attempts or budget) the LAST error is rethrown unchanged — never wrapped,
|
|
182
242
|
* so the exact messages downstream code and the degraded-cohort-member scenario assert on
|
|
183
243
|
* survive. A non-retriable failure propagates from the attempt that raised it.
|
|
244
|
+
*
|
|
245
|
+
* Either way — declined or exhausted — an `options.onAbandon` observer is notified exactly
|
|
246
|
+
* once before the rethrow (a `ControlRetryAbandonment`). Only WRITES pass one: an abandoned
|
|
247
|
+
* read throws to a caller that is awaiting it, so nothing is lost silently there
|
|
248
|
+
* (`control-read-retry.ts` says so at its own seam), while an abandoned write may have no
|
|
249
|
+
* caller at all — a background self-record republish is `void`-ed with a `debug` catch, so
|
|
250
|
+
* its failure reaches nobody unless something is listening here.
|
|
251
|
+
* `ControlDatabase.setControlWriteAbandonedListener` is the only thing that passes one in
|
|
252
|
+
* production; `retryControlWrite` itself neither supplies nor requires an observer.
|
|
184
253
|
*/
|
|
185
254
|
export declare function retryControlWrite<T>(attempt: () => Promise<T>, options?: ControlWriteRetryOptions): Promise<T>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CoordinatorPartialCommitError, SyncRetryExhaustedError, TornActionError } from '@optimystic/db-core';
|
|
2
|
+
import { PartialCommitError } from '@optimystic/quereus-plugin-optimystic';
|
|
3
|
+
import { causeChain, chainMessages, retryControlOperation } from './control-retry.js';
|
|
2
4
|
/**
|
|
3
5
|
* Bounded retry for TRANSIENT control-write failures — the classifier and loop behind
|
|
4
6
|
* `ControlDatabase.lockedWithRetry`, the single funnel every local control write passes
|
|
@@ -14,8 +16,19 @@ import { chainMessages, retryControlOperation } from './control-retry.js';
|
|
|
14
16
|
* a moment later, which the degraded-cohort-member scenario measured as safe (a failed
|
|
15
17
|
* write rolls back, nothing half-commits) and effective (the next write commits in ~1 s).
|
|
16
18
|
*
|
|
17
|
-
* A write somebody actually REJECTED is never retried
|
|
18
|
-
*
|
|
19
|
+
* A write somebody actually REJECTED is never retried on the strength of the rejection itself —
|
|
20
|
+
* no matcher here claims a rejection message, at any phase, because re-presenting one would
|
|
21
|
+
* re-present a spent signature against a cohort that already said no. Rejections are re-presented
|
|
22
|
+
* anyway when they arrive inside the transactor's promise-phase `[block:` aggregate, which
|
|
23
|
+
* {@link isUncommittedTransactorAggregate} claims on the WRAPPER alone, whatever the cause inside
|
|
24
|
+
* says. Safe because nothing has committed at that phase, and deliberately kept — one of the
|
|
25
|
+
* remaining promise-phase rejections is exactly the kind a re-presentation fixes. See the
|
|
26
|
+
* accepted-tradeoff `NOTE:` there for which, and why no blanket rejection veto is wanted.
|
|
27
|
+
*
|
|
28
|
+
* A failure that says some of the write may be STORED is a different matter, and never
|
|
29
|
+
* re-presented whatever text it carries: {@link reportsPossiblyStoredWrite} vetoes it by type
|
|
30
|
+
* before any matcher runs. The one half-landed write that IS re-presented is a torn write the
|
|
31
|
+
* library marks final — not saved and unable to land ({@link isFinalTornWrite}).
|
|
19
32
|
*
|
|
20
33
|
* TWO policies ship from here, and the split is deliberate: the default
|
|
21
34
|
* ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
|
|
@@ -63,6 +76,17 @@ export const SCHEMA_INIT_ATTEMPTS = 5;
|
|
|
63
76
|
* attempts, measured in `control-write-degraded-cohort-member.integration.ts`), which
|
|
64
77
|
* already exceeds this budget when attempt 1 returns — so that case is surfaced immediately
|
|
65
78
|
* and retry adds ZERO latency to the case where it cannot help.
|
|
79
|
+
*
|
|
80
|
+
* NOTE: a failed commit attempt now also pays a cancel discharge before its error returns
|
|
81
|
+
* (optimystic `TransactorSource.transact`'s catch, added by upstream
|
|
82
|
+
* `1-a-failed-attempt-must-discharge-its-own-pend`) — bounded by six rounds and
|
|
83
|
+
* `abortOrCancelTimeoutMs`, which every collection this repo opens sets to 5 s
|
|
84
|
+
* (`../optimystic/packages/quereus-plugin-optimystic/src/optimystic-adapter/collection-factory.ts`).
|
|
85
|
+
* Two failed attempts whose cancels each run their full budget would consume this whole 10 s
|
|
86
|
+
* ceiling and cut the three-attempt policy to two. Not observed — every measured round of the
|
|
87
|
+
* transient-reset case committed on attempt 3 of 3 — so this is a condition to watch, not work
|
|
88
|
+
* to do: if that case ever starts failing with `failed after 2/3 attempt(s)`, the cancel
|
|
89
|
+
* discharge is where the time went.
|
|
66
90
|
*/
|
|
67
91
|
export const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
|
|
68
92
|
/**
|
|
@@ -110,9 +134,14 @@ const COMMIT_BATCH_TOKEN = '[blocks:';
|
|
|
110
134
|
* matched ONLY with a ZERO rejection count. The same message with a non-zero count means a
|
|
111
135
|
* member actually voted no (that branch carries `membership-not-admitted` rejections), and
|
|
112
136
|
* retrying it would re-present a spent signature to a cohort that already refused it. This one
|
|
113
|
-
* is raised while collecting PROMISES, before any commit, so re-presenting it is safe.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
137
|
+
* is raised while collecting PROMISES, before any commit, so re-presenting it is safe.
|
|
138
|
+
*
|
|
139
|
+
* This regex itself never matches a decisive rejection (`ValidatorRejectionError`, `Transaction
|
|
140
|
+
* rejected by validators`) — that text carries no `Failed to get super-majority:` prefix. But the
|
|
141
|
+
* composite classifier is not this matcher alone: a rejection raised while promises are still
|
|
142
|
+
* being collected reaches this repo wrapped in the transactor's `Some peers did not complete:
|
|
143
|
+
* …[block:…]` aggregate, and {@link isUncommittedTransactorAggregate} matches that wrapper on its
|
|
144
|
+
* own, whatever the cause inside it says — see the accepted-tradeoff `NOTE:` there.
|
|
116
145
|
*/
|
|
117
146
|
const SUPER_MAJORITY_SHORTFALL_UNANSWERED = /Failed to get super-majority: \d+\/\d+ approvals \(needed \d+, 0 rejections\)/;
|
|
118
147
|
/**
|
|
@@ -151,6 +180,42 @@ const SUPER_MAJORITY_SHORTFALL_UNANSWERED = /Failed to get super-majority: \d+\/
|
|
|
151
180
|
* token on the live aggregate; if it reddens there, an upstream reformat is the first thing to
|
|
152
181
|
* check.
|
|
153
182
|
*
|
|
183
|
+
* NOTE: the discriminator is the prefix AND the token within ONE message — never relax it to
|
|
184
|
+
* "contains `[block:`". Beyond the `get` and `pend` sites above, Optimystic renders `[block:` in
|
|
185
|
+
* one further place — a third formatter, outside the aggregate's three sites — `dischargeCancel`
|
|
186
|
+
* (`Cancel of action <id> did not discharge <n> block(s): …`), which does not raise the
|
|
187
|
+
* `Some peers did not complete:` prefix; only the conjunction keeps a cancel fault from
|
|
188
|
+
* classifying as a retriable get/pend (checked against optimystic `c56c2bd4`, 2026-09-16).
|
|
189
|
+
*
|
|
190
|
+
* NOTE: accepted tradeoff — a REJECTION raised while promises are still being collected reaches
|
|
191
|
+
* this repo wrapped in this same aggregate, and this matcher claims it on the wrapper alone,
|
|
192
|
+
* whatever the cause inside says. That re-presents such a rejection up to two more times inside
|
|
193
|
+
* the 10 s budget. Kept deliberately, and re-decided on 2026-09-17 against the current upstream
|
|
194
|
+
* behaviour rather than inherited:
|
|
195
|
+
*
|
|
196
|
+
* - Contention is no longer a rejection at all. "Another write holds this block right now"
|
|
197
|
+
* (`pending conflict`) is now a `held` verdict that counts toward neither approvals nor
|
|
198
|
+
* rejections (`validatePendOperations`,
|
|
199
|
+
* `../optimystic/packages/db-p2p/src/cluster/cluster-repo.ts`), so the rejection this tradeoff
|
|
200
|
+
* was originally written about does not arrive any more. It reaches this funnel as
|
|
201
|
+
* `SyncRetryExhaustedError` instead, which {@link reportsPossiblyStoredWrite} declines by type
|
|
202
|
+
* — upstream documents resubmitting after it as unsafe (the attempt that spent the budget may
|
|
203
|
+
* have left its log entry standing), and its collection sync has already spent ten retries on
|
|
204
|
+
* the race. The torn-write rule beside it: a `TornActionError` is re-presented only when
|
|
205
|
+
* `final` is true; any other torn write is declined.
|
|
206
|
+
* - What remains rejectable at the promise phase is stale revision, block-unavailable,
|
|
207
|
+
* membership-not-admitted, and a configured validator's refusal. Only the first is worth a
|
|
208
|
+
* retry — and it is worth one: this loop re-runs the WHOLE write body, reads included
|
|
209
|
+
* (`ControlDatabase.withWriteLock`'s contract), so attempt 2 presents a pend against the
|
|
210
|
+
* revision that made attempt 1 stale. A blanket "decline any chain reporting a validator
|
|
211
|
+
* rejection" rule would remove that retry to save two wasted attempts on the other three.
|
|
212
|
+
*
|
|
213
|
+
* Revisit if a re-presentation ever becomes expensive relative to what it buys — a rejection class
|
|
214
|
+
* that costs a full cohort round-trip to re-present, or a write body whose reads are no longer
|
|
215
|
+
* cheap — or when upstream offers a typed refusal surface to classify on instead of this text
|
|
216
|
+
* (`../optimystic/tickets/backlog/debt-a-downstream-repo-classifies-retries-by-parsing-our-error-text`).
|
|
217
|
+
* Contention is no longer the reason to revisit.
|
|
218
|
+
*
|
|
154
219
|
* An aggregate whose details came out EMPTY (possible when `formatBatchStatuses` has no
|
|
155
220
|
* batches to format) carries neither token, matches nothing here, and is not retried — an
|
|
156
221
|
* unattributable failure is not a proven non-commit.
|
|
@@ -177,6 +242,59 @@ export function isUncommittedTransactorAggregate(message) {
|
|
|
177
242
|
function reportsIndeterminateCommit(messages) {
|
|
178
243
|
return messages.some(message => message.includes(COMMIT_BATCH_TOKEN));
|
|
179
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Does any link of the `cause` chain say some of this write may be stored, or may still be?
|
|
247
|
+
* Such a failure is never re-presented — a re-run could store the write twice.
|
|
248
|
+
*
|
|
249
|
+
* - `SyncRetryExhaustedError` (and its `SyncRevisionStalledError` subclass): upstream documents
|
|
250
|
+
* resubmitting as unsafe, because the attempt that spent the budget may have left its log
|
|
251
|
+
* entry standing.
|
|
252
|
+
* - `TornActionError` without `final: true`: the write's log entry is stored and nobody could
|
|
253
|
+
* establish that the rest can never land — it may be saved already, or land later.
|
|
254
|
+
* - `CoordinatorPartialCommitError` / `PartialCommitError`: another collection or tree of the
|
|
255
|
+
* same transaction is already durable, and re-running the whole body would apply it twice.
|
|
256
|
+
*
|
|
257
|
+
* Every one of these embeds text from elsewhere in its message (a responder's refusal, the
|
|
258
|
+
* underlying failure), which can carry the pend-phase aggregate's prefix and `[block:` token —
|
|
259
|
+
* so this veto runs before any text matcher.
|
|
260
|
+
*
|
|
261
|
+
* Matched by TYPE: each survives the bridge's and Quereus' rewraps on `cause`. An error built by a
|
|
262
|
+
* second loaded copy of `@optimystic/db-core` (or of the plugin, for `PartialCommitError`) fails
|
|
263
|
+
* `instanceof`, and then this veto does not fire — falling back to the text classifiers, which is
|
|
264
|
+
* how these failures were classified before the veto existed. The asymmetry with
|
|
265
|
+
* {@link isFinalTornWrite} is deliberate: there, a missed `instanceof` means no retry, which is the
|
|
266
|
+
* safe side. No text fallback parses `TornActionError`'s closing sentence, since upstream says its
|
|
267
|
+
* wording is for log lines only.
|
|
268
|
+
*
|
|
269
|
+
* NOTE: the plugin is registered through its `/plugin` entry (`control-database.ts`) but
|
|
270
|
+
* `PartialCommitError` is imported from its root entry; the two are one class only because the
|
|
271
|
+
* plugin's build emits both entries over one shared chunk. If upstream ever bundles the entries
|
|
272
|
+
* separately, this `instanceof` stops matching with every spec still green — import the class from
|
|
273
|
+
* the entry the plugin is registered through.
|
|
274
|
+
*/
|
|
275
|
+
function reportsPossiblyStoredWrite(links) {
|
|
276
|
+
return links.some(link => link instanceof SyncRetryExhaustedError
|
|
277
|
+
|| (link instanceof TornActionError && link.final !== true)
|
|
278
|
+
|| link instanceof CoordinatorPartialCommitError
|
|
279
|
+
|| link instanceof PartialCommitError);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* A torn write the library marks FINAL: not saved, unable to land, its pending records confirmed
|
|
283
|
+
* cancelled — so submitting it again stores it once.
|
|
284
|
+
*
|
|
285
|
+
* Worth re-presenting, not merely safe to: upstream raises it mostly when a rival holds the
|
|
286
|
+
* revision this write's log entry claimed, which is contention, and the loop re-runs the write
|
|
287
|
+
* body's reads, so the next attempt builds on the rival's revision — the same argument that keeps
|
|
288
|
+
* stale-revision rejections retried (the accepted-tradeoff `NOTE:` on
|
|
289
|
+
* {@link isUncommittedTransactorAggregate}).
|
|
290
|
+
*
|
|
291
|
+
* NOTE: `CoordinatorStaleLossError` (db-core; "nothing durably committed, safe to re-drive") is not
|
|
292
|
+
* claimed, so it falls to the text matchers; it escapes only after the coordinator's own retry budget
|
|
293
|
+
* ran out. If control writes are seen abandoned on it, claim it here by type beside this one.
|
|
294
|
+
*/
|
|
295
|
+
function isFinalTornWrite(link) {
|
|
296
|
+
return link instanceof TornActionError && link.final === true;
|
|
297
|
+
}
|
|
180
298
|
/** The coordinator's shortfall, raised pre-commit, with nobody having voted no. */
|
|
181
299
|
function isUnansweredSuperMajorityShortfall(message) {
|
|
182
300
|
return SUPER_MAJORITY_SHORTFALL_UNANSWERED.test(message);
|
|
@@ -193,13 +311,14 @@ function isUnansweredSuperMajorityShortfall(message) {
|
|
|
193
311
|
* one connection again, which a cold-starting node dialling its bootstrap peers is actively
|
|
194
312
|
* working on.
|
|
195
313
|
*
|
|
196
|
-
* NOTE: matched by TEXT, not by type
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* `
|
|
200
|
-
* (`quereus-plugin-optimystic/src/optimystic-module.ts`)
|
|
201
|
-
*
|
|
202
|
-
*
|
|
314
|
+
* NOTE: matched by TEXT, not by type. `FindCoordinatorError` (`code: 'SELF_COORDINATION_BLOCKED'`)
|
|
315
|
+
* is exported from `@optimystic/db-p2p`, and when this matcher was written the error OBJECT did not
|
|
316
|
+
* survive the trip: `OptimysticVirtualTable.initialize` rethrew as `new Error(message)` with no
|
|
317
|
+
* `cause`. It now rethrows through `rewrapAsQueryError`, which keeps `cause`
|
|
318
|
+
* (`quereus-plugin-optimystic/src/optimystic-module.ts`); whether every Quereus wrapper above it on
|
|
319
|
+
* the schema-init path keeps it too has not been checked, and the text still arrives either way.
|
|
320
|
+
* Like every matcher here this fails CLOSED — a rewording upstream stops the retry engaging, it
|
|
321
|
+
* never makes it unsafe.
|
|
203
322
|
*/
|
|
204
323
|
const SELF_COORDINATION_GRACE_REFUSAL = /Self-coordination blocked: grace-period-not-elapsed\. No coordinator available for key\./;
|
|
205
324
|
/** Self-coordination refused because the node's last connection dropped moments ago. */
|
|
@@ -246,16 +365,18 @@ const RETRIABLE_SCHEMA_INIT_MATCHERS = [
|
|
|
246
365
|
* Did this control write fail because the cluster cohort did not ANSWER — i.e. is
|
|
247
366
|
* re-presenting the SAME signed write a moment later the right response?
|
|
248
367
|
*
|
|
249
|
-
* Classifies by MESSAGE, walking the `cause` chain with the shared
|
|
250
|
-
* (Quereus' own `unwrapError` underneath) —
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
368
|
+
* Classifies the transient cases by MESSAGE, walking the `cause` chain with the shared
|
|
369
|
+
* {@link chainMessages} (Quereus' own `unwrapError` underneath) — the transactor and the
|
|
370
|
+
* coordinator both throw bare `Error`s, not recognisable by type. By the time one reaches
|
|
371
|
+
* `ControlDatabase` it is wrapped in a `QuereusError` (the real chain is `QuereusError` → `Error`
|
|
372
|
+
* → `Error`), so the match must work at any depth. Anything that is not an `Error` is never
|
|
373
|
+
* retried.
|
|
255
374
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
375
|
+
* Two vetoes run over the whole chain before any matcher: {@link reportsPossiblyStoredWrite}
|
|
376
|
+
* (by type — a spent sync budget, a non-final torn write, a partial commit) and
|
|
377
|
+
* {@link reportsIndeterminateCommit} (by text — a commit-phase batch). A chain either veto flags,
|
|
378
|
+
* at any level, is never retried on the strength of some other level looking transient. The one
|
|
379
|
+
* typed failure claimed as retriable is a final torn write ({@link isFinalTornWrite}).
|
|
259
380
|
*
|
|
260
381
|
* NOTE: this depends on engine/transactor error TEXT. The messages producible without a
|
|
261
382
|
* network are driven from the REAL engine in `control-formation-seat-budget.spec.ts`, so
|
|
@@ -266,7 +387,7 @@ const RETRIABLE_SCHEMA_INIT_MATCHERS = [
|
|
|
266
387
|
* aggregate from a real stream reset, which the retry there absorbs end to end.
|
|
267
388
|
*/
|
|
268
389
|
export function isRetriableControlWriteFailure(error) {
|
|
269
|
-
return
|
|
390
|
+
return matchesRetriableFailure(error, RETRIABLE_CONTROL_WRITE_MATCHERS);
|
|
270
391
|
}
|
|
271
392
|
/**
|
|
272
393
|
* {@link isRetriableControlWriteFailure} widened by exactly one class — the self-coordination
|
|
@@ -276,22 +397,31 @@ export function isRetriableControlWriteFailure(error) {
|
|
|
276
397
|
* default classifier untouched.
|
|
277
398
|
*/
|
|
278
399
|
export function isRetriableSchemaInitFailure(error) {
|
|
279
|
-
return
|
|
400
|
+
return matchesRetriableFailure(error, RETRIABLE_SCHEMA_INIT_MATCHERS);
|
|
280
401
|
}
|
|
281
402
|
/**
|
|
282
|
-
* Shared body of both classifiers: walk the `cause` chain, let
|
|
283
|
-
* {@link reportsIndeterminateCommit} veto it, then
|
|
284
|
-
*
|
|
285
|
-
*
|
|
403
|
+
* Shared body of both classifiers: walk the `cause` chain, let {@link reportsPossiblyStoredWrite}
|
|
404
|
+
* and then {@link reportsIndeterminateCommit} veto it, then claim a final torn write
|
|
405
|
+
* ({@link isFinalTornWrite}), then ask `matchers`. The vetoes run BEFORE any matcher for every
|
|
406
|
+
* policy — a chain reporting a possibly-stored write or a commit-phase batch is never retried,
|
|
407
|
+
* however transient some other level looks, and a final torn write carried by a partial commit is
|
|
408
|
+
* still declined because a sibling collection landed.
|
|
286
409
|
*/
|
|
287
|
-
function
|
|
410
|
+
function matchesRetriableFailure(error, matchers) {
|
|
288
411
|
if (!(error instanceof Error)) {
|
|
289
412
|
return false;
|
|
290
413
|
}
|
|
414
|
+
const links = causeChain(error);
|
|
415
|
+
if (reportsPossiblyStoredWrite(links)) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
291
418
|
const messages = chainMessages(error);
|
|
292
419
|
if (reportsIndeterminateCommit(messages)) {
|
|
293
420
|
return false;
|
|
294
421
|
}
|
|
422
|
+
if (links.some(isFinalTornWrite)) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
295
425
|
return messages.some(message => matchers.some(matches => matches(message)));
|
|
296
426
|
}
|
|
297
427
|
/**
|
|
@@ -326,6 +456,15 @@ export const SCHEMA_INIT_RETRY_POLICY = {
|
|
|
326
456
|
* On exhaustion (attempts or budget) the LAST error is rethrown unchanged — never wrapped,
|
|
327
457
|
* so the exact messages downstream code and the degraded-cohort-member scenario assert on
|
|
328
458
|
* survive. A non-retriable failure propagates from the attempt that raised it.
|
|
459
|
+
*
|
|
460
|
+
* Either way — declined or exhausted — an `options.onAbandon` observer is notified exactly
|
|
461
|
+
* once before the rethrow (a `ControlRetryAbandonment`). Only WRITES pass one: an abandoned
|
|
462
|
+
* read throws to a caller that is awaiting it, so nothing is lost silently there
|
|
463
|
+
* (`control-read-retry.ts` says so at its own seam), while an abandoned write may have no
|
|
464
|
+
* caller at all — a background self-record republish is `void`-ed with a `debug` catch, so
|
|
465
|
+
* its failure reaches nobody unless something is listening here.
|
|
466
|
+
* `ControlDatabase.setControlWriteAbandonedListener` is the only thing that passes one in
|
|
467
|
+
* production; `retryControlWrite` itself neither supplies nor requires an observer.
|
|
329
468
|
*/
|
|
330
469
|
export function retryControlWrite(attempt, options = {}) {
|
|
331
470
|
return retryControlOperation(attempt, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-write-retry.js","sourceRoot":"","sources":["../src/control-write-retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"control-write-retry.js","sourceRoot":"","sources":["../src/control-write-retry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* File-backed {@link EnrolledMachineStore}. Open via {@link open}, which loads the
|
|
4
|
+
* existing file and returns the cross-platform store over it. An absent, corrupt,
|
|
5
|
+
* wrong-party, junk-valued **or unreadable** file is a cold start (`count()` is
|
|
6
|
+
* `undefined`, the node declares no repair yardstick and runs at the base control
|
|
7
|
+
* policy) — `open` never rejects, unlike the sibling stores' file backends. That
|
|
8
|
+
* divergence and its reasoning live on `enrolled-machine-store.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FileEnrolledMachineStore: {
|
|
11
|
+
/** Load (or cold-start) the party's last recorded machine count from a file in `dir`. */
|
|
12
|
+
open(dir: string, partyId: string): Promise<EnrolledMachineStore>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only file-backed {@link EnrolledMachineStore}: one JSON file per party under
|
|
3
|
+
* a configured directory (the node's state directory — `cadre-cli` passes
|
|
4
|
+
* `ResolvedConfig.nodeStateDir`), holding the node-local, non-replicated count of
|
|
5
|
+
* machines this party had enrolled the last time this node looked.
|
|
6
|
+
*
|
|
7
|
+
* Nothing but the file itself lives here: the envelope, the cold-start-on-anything
|
|
8
|
+
* load policy (deliberately NOT the throw-on-unreadable policy its two sibling
|
|
9
|
+
* records use — see the `enrolled-machine-store.ts` module comment) and the
|
|
10
|
+
* serialised writes are cross-platform in `PersistentEnrolledMachineStore`, and the
|
|
11
|
+
* file is a `FileDurableSlot`. This module exists only so the `node:fs` edge stays
|
|
12
|
+
* out of the package's cross-platform default entry (`./index.js`) — import it from
|
|
13
|
+
* the dedicated subpath instead:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { FileEnrolledMachineStore } from '@serfab/cadre-core/enrolled-machine-store-file';
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* A React Native / browser entry graph never resolves this path, so the `node:fs`
|
|
20
|
+
* edge never reaches a bundler that cannot satisfy it (same isolation pattern as
|
|
21
|
+
* `bootstrap-peer-store-file.ts` / `trusted-owner-store-file.ts` / `key-store-file.ts`).
|
|
22
|
+
*/
|
|
23
|
+
import { FileDurableSlot } from './file-durable-slot.js';
|
|
24
|
+
import { PersistentEnrolledMachineStore } from './enrolled-machine-store.js';
|
|
25
|
+
/** Base name of the store file: `<dir>/enrolled-machines.<encoded partyId>.json`. */
|
|
26
|
+
const SLOT_NAME = 'enrolled-machines';
|
|
27
|
+
/**
|
|
28
|
+
* File-backed {@link EnrolledMachineStore}. Open via {@link open}, which loads the
|
|
29
|
+
* existing file and returns the cross-platform store over it. An absent, corrupt,
|
|
30
|
+
* wrong-party, junk-valued **or unreadable** file is a cold start (`count()` is
|
|
31
|
+
* `undefined`, the node declares no repair yardstick and runs at the base control
|
|
32
|
+
* policy) — `open` never rejects, unlike the sibling stores' file backends. That
|
|
33
|
+
* divergence and its reasoning live on `enrolled-machine-store.ts`.
|
|
34
|
+
*/
|
|
35
|
+
export const FileEnrolledMachineStore = {
|
|
36
|
+
/** Load (or cold-start) the party's last recorded machine count from a file in `dir`. */
|
|
37
|
+
async open(dir, partyId) {
|
|
38
|
+
return PersistentEnrolledMachineStore.open(new FileDurableSlot(dir, SLOT_NAME, partyId), partyId);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=enrolled-machine-store-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrolled-machine-store-file.js","sourceRoot":"","sources":["../src/enrolled-machine-store-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAC;AAG7E,qFAAqF;AACrF,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACvC,yFAAyF;IACzF,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,OAAe;QACtC,OAAO,8BAA8B,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IACnG,CAAC;CACD,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DurableSlot } from './node-local-snapshot.js';
|
|
2
|
+
export interface EnrolledMachineStore {
|
|
3
|
+
/** Party this store is scoped to. */
|
|
4
|
+
readonly partyId: string;
|
|
5
|
+
/**
|
|
6
|
+
* Machines last recorded for this party, or `undefined` when nothing usable was
|
|
7
|
+
* ever recorded — a brand-new node, or a slot that could not be read. Callers
|
|
8
|
+
* pass this straight to `controlClusterPolicy`, whose `undefined` branch returns
|
|
9
|
+
* the frozen base policy unchanged, so "this node does not know" and "this node
|
|
10
|
+
* has never run" are the same declaration: none.
|
|
11
|
+
*/
|
|
12
|
+
count(): number | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Record the current count. Reflected by {@link count} SYNCHRONOUSLY; the
|
|
15
|
+
* returned promise resolves once the persist attempt has settled.
|
|
16
|
+
*
|
|
17
|
+
* **Never rejects.** Its sole caller is
|
|
18
|
+
* `CadreNode.refreshAuthorizedControlPeers`, whose contract is never-rejects and
|
|
19
|
+
* which leaves the promise un-awaited (`void`) — so a rejection here would
|
|
20
|
+
* surface as an unhandled rejection in the embedder's process, over a hint that
|
|
21
|
+
* re-lands on the next refresh anyway. A failed persist is LOGGED, and the
|
|
22
|
+
* in-memory count stays correct for this session.
|
|
23
|
+
*
|
|
24
|
+
* A count that is not a positive integer is a caller bug: it is logged and
|
|
25
|
+
* IGNORED rather than persisted, so a degenerate value can never reach the slot
|
|
26
|
+
* and be read back as a declaration nobody chose.
|
|
27
|
+
*/
|
|
28
|
+
record(count: number): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ephemeral in-memory store — the default when no store is injected via
|
|
32
|
+
* `CadreNodeConfig.enrolledMachines`. A node using this cold-starts on every
|
|
33
|
+
* launch: `count()` is `undefined` at `start()`, so its control node declares
|
|
34
|
+
* nothing and runs at today's behaviour, exactly as before this record existed.
|
|
35
|
+
*/
|
|
36
|
+
export declare class MemoryEnrolledMachineStore implements EnrolledMachineStore {
|
|
37
|
+
readonly partyId: string;
|
|
38
|
+
private current;
|
|
39
|
+
constructor(partyId: string);
|
|
40
|
+
count(): number | undefined;
|
|
41
|
+
record(count: number): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Durable {@link EnrolledMachineStore} over an app-supplied {@link DurableSlot} —
|
|
45
|
+
* the cross-platform half of every persistent backend (the Node
|
|
46
|
+
* `FileEnrolledMachineStore` is this class over a file slot).
|
|
47
|
+
*
|
|
48
|
+
* Construct via {@link open}. Load policy is the module comment's: every failure
|
|
49
|
+
* mode cold-starts, and `open` never rejects.
|
|
50
|
+
*/
|
|
51
|
+
export declare class PersistentEnrolledMachineStore implements EnrolledMachineStore {
|
|
52
|
+
private readonly slot;
|
|
53
|
+
readonly partyId: string;
|
|
54
|
+
/** Serialises persists so the last count recorded is the last one written. */
|
|
55
|
+
private writeChain;
|
|
56
|
+
/** Last count accepted, whether or not it has reached the slot yet. */
|
|
57
|
+
private current;
|
|
58
|
+
/** Last count believed to BE in the slot; drives the unchanged-write skip. */
|
|
59
|
+
private persisted;
|
|
60
|
+
private constructor();
|
|
61
|
+
/** Load (or cold-start) the party's last recorded machine count from `slot`. */
|
|
62
|
+
static open(slot: DurableSlot, partyId: string): Promise<PersistentEnrolledMachineStore>;
|
|
63
|
+
count(): number | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Record the count: visible via {@link count} synchronously, then persisted.
|
|
66
|
+
*
|
|
67
|
+
* Writes are SKIPPED when the slot already holds this number, because the sole
|
|
68
|
+
* caller runs on every committed membership write *and* every timed reconcile
|
|
69
|
+
* pass — a party whose membership is stable would otherwise rewrite the same
|
|
70
|
+
* integer forever. The skip is keyed off {@link persisted} rather than
|
|
71
|
+
* {@link current}, so a write that FAILED is retried by the next refresh instead
|
|
72
|
+
* of being suppressed as "unchanged".
|
|
73
|
+
*/
|
|
74
|
+
record(count: number): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Write whatever {@link current} holds when this link of the chain runs — so a
|
|
77
|
+
* burst of records collapses into one write of the latest value — and never
|
|
78
|
+
* throw (see {@link EnrolledMachineStore.record}).
|
|
79
|
+
*/
|
|
80
|
+
private persistCurrent;
|
|
81
|
+
}
|