@optimystic/db-core 1.0.0-beta.1 → 1.0.0-beta.3
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/dist/src/cluster/structs.d.ts +45 -15
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +29 -2
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +37 -2
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +81 -2
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +59 -0
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +39 -2
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/logger-registry.d.ts +57 -0
- package/dist/src/logger-registry.d.ts.map +1 -0
- package/dist/src/logger-registry.js +168 -0
- package/dist/src/logger-registry.js.map +1 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +3 -0
- package/dist/src/logger.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +8 -3
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/package.json +1 -1
- package/src/cluster/structs.ts +277 -247
- package/src/collection/collection.ts +37 -2
- package/src/collections/tree/struct.ts +116 -26
- package/src/collections/tree/tree.ts +36 -2
- package/src/index.ts +1 -0
- package/src/logger-registry.ts +224 -0
- package/src/logger.ts +4 -0
- package/src/transactor/network-transactor.ts +8 -3
package/src/cluster/structs.ts
CHANGED
|
@@ -1,247 +1,277 @@
|
|
|
1
|
-
import type { RepoMessage } from "../network/repo-protocol.js";
|
|
2
|
-
import type { PendResult } from "../network/struct.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* One member's vote on a cluster transaction, in either the promise or the commit map.
|
|
6
|
-
*
|
|
7
|
-
* A discriminated union rather than one shape with optional fields, so each vote kind carries
|
|
8
|
-
* exactly its own payload: a `conflict` without its `conflictWith` (or a stray `rejectReason` on an
|
|
9
|
-
* `approve`) does not typecheck. Every variant's extra field is folded into the signed payload
|
|
10
|
-
* ({@link clusterVoteSigningPayload}), so none of them can be altered in transit.
|
|
11
|
-
*/
|
|
12
|
-
export type Signature =
|
|
13
|
-
| { type: 'approve'; signature: string }
|
|
14
|
-
| { type: 'reject'; signature: string; rejectReason?: string }
|
|
15
|
-
/**
|
|
16
|
-
* This member refuses the transaction *for now*: it holds a conflicting transaction that won
|
|
17
|
-
* the deterministic race (`resolveRace`). Retryable — NOT a validity judgement, and never
|
|
18
|
-
* counted toward the permanent-rejection threshold. `conflictWith` is the winning
|
|
19
|
-
* transaction's messageHash: structured, signed, and readable without parsing prose.
|
|
20
|
-
*/
|
|
21
|
-
| { type: 'conflict'; signature: string; conflictWith: string };
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The exact bytes a vote signature covers: `<hash>:<type>[:<extra>]`, where `extra` is the variant's
|
|
25
|
-
* own payload — a reject's `rejectReason`, a conflict's `conflictWith`, nothing for an approve.
|
|
26
|
-
* Folding the extra in is what makes it integrity-protected in transit rather than free-floating
|
|
27
|
-
* prose.
|
|
28
|
-
*
|
|
29
|
-
* Producers and verifiers must both build the preimage here. It lives beside {@link Signature}
|
|
30
|
-
* rather than in either consumer because a second copy that forgets a variant does not fail loudly:
|
|
31
|
-
* it reports an honest vote as an invalid signature. (The dispute path once carried such a copy.)
|
|
32
|
-
*
|
|
33
|
-
* "Cluster" in the name distinguishes these consensus votes from the dispute subsystem's
|
|
34
|
-
* arbitration votes, which have their own unrelated preimage (`dispute/invalidation.ts`).
|
|
35
|
-
*/
|
|
36
|
-
export function clusterVoteSigningPayload(hash: string, type: Signature['type'], extra?: string): Uint8Array {
|
|
37
|
-
return new TextEncoder().encode(hash + ':' + type + (extra ? ':' + extra : ''));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Verifier-side {@link clusterVoteSigningPayload}: reads each variant's signed extra off the vote itself. */
|
|
41
|
-
export function clusterVoteVerificationPayload(hash: string, signature: Signature): Uint8Array {
|
|
42
|
-
switch (signature.type) {
|
|
43
|
-
case 'reject': return clusterVoteSigningPayload(hash, 'reject', signature.rejectReason);
|
|
44
|
-
case 'conflict': return clusterVoteSigningPayload(hash, 'conflict', signature.conflictWith);
|
|
45
|
-
default: return clusterVoteSigningPayload(hash, signature.type);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type ClusterPeers = {
|
|
50
|
-
[id: string]: {
|
|
51
|
-
multiaddrs: string[];
|
|
52
|
-
/** Base64url-encoded public key (serialization-safe) */
|
|
53
|
-
publicKey: string;
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* One member's own report of what its storage did with this transaction at consensus-apply time; see
|
|
59
|
-
* {@link ClusterRecord.applyOutcomes} for the trust rules.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*/
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
*/
|
|
247
|
-
|
|
1
|
+
import type { RepoMessage } from "../network/repo-protocol.js";
|
|
2
|
+
import type { CommitResult, PendResult } from "../network/struct.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One member's vote on a cluster transaction, in either the promise or the commit map.
|
|
6
|
+
*
|
|
7
|
+
* A discriminated union rather than one shape with optional fields, so each vote kind carries
|
|
8
|
+
* exactly its own payload: a `conflict` without its `conflictWith` (or a stray `rejectReason` on an
|
|
9
|
+
* `approve`) does not typecheck. Every variant's extra field is folded into the signed payload
|
|
10
|
+
* ({@link clusterVoteSigningPayload}), so none of them can be altered in transit.
|
|
11
|
+
*/
|
|
12
|
+
export type Signature =
|
|
13
|
+
| { type: 'approve'; signature: string }
|
|
14
|
+
| { type: 'reject'; signature: string; rejectReason?: string }
|
|
15
|
+
/**
|
|
16
|
+
* This member refuses the transaction *for now*: it holds a conflicting transaction that won
|
|
17
|
+
* the deterministic race (`resolveRace`). Retryable — NOT a validity judgement, and never
|
|
18
|
+
* counted toward the permanent-rejection threshold. `conflictWith` is the winning
|
|
19
|
+
* transaction's messageHash: structured, signed, and readable without parsing prose.
|
|
20
|
+
*/
|
|
21
|
+
| { type: 'conflict'; signature: string; conflictWith: string };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The exact bytes a vote signature covers: `<hash>:<type>[:<extra>]`, where `extra` is the variant's
|
|
25
|
+
* own payload — a reject's `rejectReason`, a conflict's `conflictWith`, nothing for an approve.
|
|
26
|
+
* Folding the extra in is what makes it integrity-protected in transit rather than free-floating
|
|
27
|
+
* prose.
|
|
28
|
+
*
|
|
29
|
+
* Producers and verifiers must both build the preimage here. It lives beside {@link Signature}
|
|
30
|
+
* rather than in either consumer because a second copy that forgets a variant does not fail loudly:
|
|
31
|
+
* it reports an honest vote as an invalid signature. (The dispute path once carried such a copy.)
|
|
32
|
+
*
|
|
33
|
+
* "Cluster" in the name distinguishes these consensus votes from the dispute subsystem's
|
|
34
|
+
* arbitration votes, which have their own unrelated preimage (`dispute/invalidation.ts`).
|
|
35
|
+
*/
|
|
36
|
+
export function clusterVoteSigningPayload(hash: string, type: Signature['type'], extra?: string): Uint8Array {
|
|
37
|
+
return new TextEncoder().encode(hash + ':' + type + (extra ? ':' + extra : ''));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Verifier-side {@link clusterVoteSigningPayload}: reads each variant's signed extra off the vote itself. */
|
|
41
|
+
export function clusterVoteVerificationPayload(hash: string, signature: Signature): Uint8Array {
|
|
42
|
+
switch (signature.type) {
|
|
43
|
+
case 'reject': return clusterVoteSigningPayload(hash, 'reject', signature.rejectReason);
|
|
44
|
+
case 'conflict': return clusterVoteSigningPayload(hash, 'conflict', signature.conflictWith);
|
|
45
|
+
default: return clusterVoteSigningPayload(hash, signature.type);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ClusterPeers = {
|
|
50
|
+
[id: string]: {
|
|
51
|
+
multiaddrs: string[];
|
|
52
|
+
/** Base64url-encoded public key (serialization-safe) */
|
|
53
|
+
publicKey: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One member's own report of what its storage did with this transaction at consensus-apply time; see
|
|
59
|
+
* {@link ClusterRecord.applyOutcomes} for the trust rules. The two arms report differently, and
|
|
60
|
+
* deliberately so:
|
|
61
|
+
*
|
|
62
|
+
* - `pend` is set ONLY for a conflict-shaped refusal. The coordinator's rule for it is "an entry
|
|
63
|
+
* means retry", so a success must not exist there.
|
|
64
|
+
* - `commit` is ALWAYS set once a commit applied — successes included — because the coordinator
|
|
65
|
+
* counts durable holders (`CoordinatorRepo.commit`'s durability gate), and a count needs the
|
|
66
|
+
* positives. Consensus votes say the cohort agreed to apply the commit; this arm says whether
|
|
67
|
+
* this member's storage actually holds the committed revision under the record's action
|
|
68
|
+
* afterwards, measured AFTER the member's own reconcile (a member that pulled the revision from a
|
|
69
|
+
* cohort peer reports success). A commit that reached consensus is authoritative for ordering,
|
|
70
|
+
* but it was never evidence of storage — which is what this arm supplies.
|
|
71
|
+
*/
|
|
72
|
+
export type MemberApplyOutcome = {
|
|
73
|
+
/** This member's storage refused the record's pend with a conflict-shaped result. */
|
|
74
|
+
pend?: PendResult;
|
|
75
|
+
/**
|
|
76
|
+
* Whether this member's storage durably holds the record's committed revision, for every block
|
|
77
|
+
* the commit named, after applying and (if needed) reconciling. `success: true` is a durable
|
|
78
|
+
* holder; a failure carries storage's refusal (or a refusal built from the missing-pend throw)
|
|
79
|
+
* for the operator's benefit — the coordinator reads only `success`.
|
|
80
|
+
*/
|
|
81
|
+
commit?: CommitResult;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ClusterRecord = {
|
|
85
|
+
messageHash: string; // Serves as a unique identifier for the clustered transaction record
|
|
86
|
+
peers: ClusterPeers;
|
|
87
|
+
/**
|
|
88
|
+
* Membership-binding version. Absent or `1` = legacy *unbound* record: the peer set is NOT covered by
|
|
89
|
+
* any hash (pre-binding history and its stored commit certs verify byte-identically to before). `2` =
|
|
90
|
+
* the sorted peer-id set (as {@link ClusterRecord.membershipDigest}) is folded into `messageHash`,
|
|
91
|
+
* `promiseHash`, and `commitHash`, so two different peer sets yield two different `messageHash`es.
|
|
92
|
+
* New coordinators always emit `2`. See `packages/db-core/src/cluster/membership.ts`.
|
|
93
|
+
*/
|
|
94
|
+
membershipVersion?: 1 | 2;
|
|
95
|
+
/** Membership digest of {@link ClusterRecord.peers}; present iff `membershipVersion === 2`. base64url. */
|
|
96
|
+
membershipDigest?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The transaction's operations and the block the coordinator selected the cohort by
|
|
99
|
+
* ({@link RepoMessage.coordinatingBlockIds}). There is deliberately NO top-level copy of the
|
|
100
|
+
* coordinating block ids on the record: `messageHash` covers `message` only, so a duplicate at
|
|
101
|
+
* this level would be outside every hash and any relaying peer could rewrite it — and the
|
|
102
|
+
* membership admission gate derives its own cohort view from exactly that id
|
|
103
|
+
* (`ClusterMember.deriveExpectedClusterView`). One source of truth, inside the hash.
|
|
104
|
+
*/
|
|
105
|
+
message: RepoMessage;
|
|
106
|
+
promises: { [peerId: string]: Signature };
|
|
107
|
+
commits: { [peerId: string]: Signature };
|
|
108
|
+
/** Sender's recommended cluster size: min(estimated network size, configured cluster size) */
|
|
109
|
+
suggestedClusterSize?: number;
|
|
110
|
+
minRequiredSize?: number;
|
|
111
|
+
/** Sender's current network size estimate */
|
|
112
|
+
networkSizeHint?: number;
|
|
113
|
+
/** Confidence in the network size estimate (0-1) */
|
|
114
|
+
networkSizeConfidence?: number;
|
|
115
|
+
/**
|
|
116
|
+
* What each member's OWN storage answered when it applied this record's operations at consensus,
|
|
117
|
+
* keyed by peer id. Advisory and **unsigned** — no hash covers it — and deliberately so: it is
|
|
118
|
+
* written by a member *after* the votes are cast, on the response it hands back, so no signed
|
|
119
|
+
* payload could carry it without a further round trip.
|
|
120
|
+
*
|
|
121
|
+
* Members set only their own entry. The pend arm is set only for a *conflict-shaped* pend
|
|
122
|
+
* refusal (one carrying `pending` or `missing`, per `isConflictFailure`) — the
|
|
123
|
+
* optimistic-concurrency verdict that a rival holds the blocks or already took the revision.
|
|
124
|
+
* Pend successes and bare-reason faults are omitted: a bare fault stays tolerated local
|
|
125
|
+
* divergence, mirroring the coordinator's own local-verdict arm. The commit arm is set for every
|
|
126
|
+
* applied commit, success or not, because the coordinator counts the successes
|
|
127
|
+
* ({@link MemberApplyOutcome}).
|
|
128
|
+
*
|
|
129
|
+
* Why unsigned is acceptable: a hostile pend entry can only *downgrade* a reported pend success
|
|
130
|
+
* into a retryable conflict, which the writer answers by rebasing and trying again. A hostile
|
|
131
|
+
* commit entry is no worse: a false *success* counts one durable holder the member already
|
|
132
|
+
* counted for with its signed approve vote (the vote is what admits it to the majority the count
|
|
133
|
+
* is measured against), and a false *refusal* is again only retry pressure. The same member
|
|
134
|
+
* could already force strictly worse outcomes with a signed reject or conflict vote, so this adds
|
|
135
|
+
* no attack surface beyond retry pressure — and failing toward retry is the correct direction for
|
|
136
|
+
* optimistic concurrency. Never treat an entry here as evidence of anything but "retry" (pend) or
|
|
137
|
+
* "one more holder in a count that a signed vote already bounded" (commit).
|
|
138
|
+
*
|
|
139
|
+
* Old peers never set it and old coordinators ignore it, so it is wire-compatible in both
|
|
140
|
+
* directions.
|
|
141
|
+
*
|
|
142
|
+
* NOTE: unlike a signed reject/conflict vote, an entry here is unattributable — nothing verifies
|
|
143
|
+
* it came from the peer it is keyed under beyond the coordinator taking each peer's entry from
|
|
144
|
+
* that peer's own response, and no reputation penalty can be pinned on a false one. Fine while the
|
|
145
|
+
* worst it buys is retry pressure a signed vote could already produce. If a member is ever
|
|
146
|
+
* observed reporting refusals it did not make — writes on a block failing their retry budget with
|
|
147
|
+
* `coordinator-repo:pend-remote-refusal` naming one peer over and over — the fix is to make the
|
|
148
|
+
* entry attributable (sign it on a follow-up round) rather than to start trusting it less.
|
|
149
|
+
*/
|
|
150
|
+
applyOutcomes?: { [peerId: string]: MemberApplyOutcome };
|
|
151
|
+
/** Transaction proceeded despite minority rejections */
|
|
152
|
+
disputed?: boolean;
|
|
153
|
+
/** Evidence of the dispute: which peers rejected and why */
|
|
154
|
+
disputeEvidence?: {
|
|
155
|
+
rejectingPeers: string[];
|
|
156
|
+
rejectReasons: { [peerId: string]: string };
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Single source of truth for the default super-majority threshold — the fraction of a cluster's peers
|
|
162
|
+
* that must promise before a transaction may proceed. Every component that falls back to a default when
|
|
163
|
+
* config is absent (cluster member, coordinator policy, node composition root) references THIS constant,
|
|
164
|
+
* so a member cannot silently default to a different threshold than the coordinator that commits. Explicit
|
|
165
|
+
* caller-supplied thresholds are unaffected; this only unifies the *absent-config* default.
|
|
166
|
+
* 0.75 = 3/4: chosen because the coordinator (which actually commits) already used it and the type documents it.
|
|
167
|
+
*/
|
|
168
|
+
export const DEFAULT_SUPER_MAJORITY_THRESHOLD = 0.75;
|
|
169
|
+
|
|
170
|
+
export interface ClusterConsensusConfig {
|
|
171
|
+
/** Super-majority threshold for promises (default {@link DEFAULT_SUPER_MAJORITY_THRESHOLD} = 0.75 = 3/4) */
|
|
172
|
+
superMajorityThreshold: number;
|
|
173
|
+
/** Simple majority threshold for commits (default 0.51 = >50%) */
|
|
174
|
+
simpleMajorityThreshold: number;
|
|
175
|
+
/** Minimum absolute cluster size (default 3) */
|
|
176
|
+
minAbsoluteClusterSize: number;
|
|
177
|
+
/** Allow cluster to operate below configured size (default false) */
|
|
178
|
+
allowClusterDownsize: boolean;
|
|
179
|
+
/** Tolerance for cluster size variance as fraction (default 0.5 = 50%) */
|
|
180
|
+
clusterSizeTolerance: number;
|
|
181
|
+
/**
|
|
182
|
+
* Configured full cluster size — the replication factor / target cohort breadth. This is what the
|
|
183
|
+
* coordinator aims for when selecting a cohort. It is NOT a statement about how many peers exist,
|
|
184
|
+
* and nothing may use it as a security yardstick; see {@link ClusterConsensusConfig.assumedClusterSize}.
|
|
185
|
+
* The coordinator supplies this as a required `clusterSize` via `ClusterConsensusConfig & { clusterSize: number }`.
|
|
186
|
+
*/
|
|
187
|
+
clusterSize?: number;
|
|
188
|
+
/**
|
|
189
|
+
* The smallest cohort the operator asserts this deployment can genuinely field — typically
|
|
190
|
+
* `min(clusterSize, number of nodes actually run)`. Read ONLY when a node cannot independently
|
|
191
|
+
* measure a cohort (no derivation capability wired, or no confident network-size estimate), where
|
|
192
|
+
* it stands in for the measured estimate in the membership admission floor.
|
|
193
|
+
*
|
|
194
|
+
* Undefined means "unknown": the admission gate then cannot tell a downsize from a legitimately
|
|
195
|
+
* small cluster and preserves legacy approve behavior. Composition roots should supply a concrete
|
|
196
|
+
* value; `libp2p-node-base` defaults it to `minAbsoluteClusterSize`.
|
|
197
|
+
*/
|
|
198
|
+
assumedClusterSize?: number;
|
|
199
|
+
/**
|
|
200
|
+
* Fraction of the member's OWN confident cluster-size estimate a declared peer set must meet to be
|
|
201
|
+
* admitted for voting (default 0.75). Below `⌈membershipAdmissionFraction · K_est⌉` a declared set is
|
|
202
|
+
* treated as an unjustified self-shrink and the member declines to approve. Distinct from
|
|
203
|
+
* {@link superMajorityThreshold} (the vote-counting threshold) — this gates *which set* may be voted on.
|
|
204
|
+
*/
|
|
205
|
+
membershipAdmissionFraction?: number;
|
|
206
|
+
/** Window for detecting partition in milliseconds (default 60000 = 1 min) */
|
|
207
|
+
partitionDetectionWindow: number;
|
|
208
|
+
/** Enable dispute escalation protocol (default false) */
|
|
209
|
+
disputeEnabled?: boolean;
|
|
210
|
+
/** Timeout for dispute arbitration in milliseconds (default 60000) */
|
|
211
|
+
disputeArbitrationTimeoutMs?: number;
|
|
212
|
+
/**
|
|
213
|
+
* Hard horizon on an invalidation cascade: maximum number of recursive re-evaluation rounds
|
|
214
|
+
* (dependency-graph depth) before the cascade stops and escalates the affected collection(s)
|
|
215
|
+
* for operator full re-sync (default 32). Bounds unbounded automatic reversal.
|
|
216
|
+
*/
|
|
217
|
+
maxCascadeDepth?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Hard horizon on an invalidation cascade: maximum number of transactions (including the root)
|
|
220
|
+
* the cascade may invalidate before it stops and escalates for operator full re-sync
|
|
221
|
+
* (default 1000). On overflow the already-applied invalidations stand; the remainder is flagged,
|
|
222
|
+
* never silently dropped.
|
|
223
|
+
*/
|
|
224
|
+
maxCascadeTransactions?: number;
|
|
225
|
+
/** Initial scheduled-retry interval for failed commit broadcasts, ms (default 250) */
|
|
226
|
+
commitBroadcastRetryInitialMs?: number;
|
|
227
|
+
/** Backoff factor for commit-broadcast scheduled retries (default 2) */
|
|
228
|
+
commitBroadcastRetryBackoffFactor?: number;
|
|
229
|
+
/** Max scheduled-retry interval, ms (default 8000) */
|
|
230
|
+
commitBroadcastRetryMaxIntervalMs?: number;
|
|
231
|
+
/** Max scheduled retry attempts before giving up (default 5) */
|
|
232
|
+
commitBroadcastRetryMaxAttempts?: number;
|
|
233
|
+
/** Immediate in-line retries per failed peer inside the broadcast (default 1) */
|
|
234
|
+
commitBroadcastImmediateRetries?: number;
|
|
235
|
+
/**
|
|
236
|
+
* Immediate in-line retries per peer while collecting promises (default 1).
|
|
237
|
+
* The promise phase rides the same libp2p stream the commit broadcast does;
|
|
238
|
+
* a circuit-relay ("limited") connection can reset that stream once a
|
|
239
|
+
* per-circuit cap is hit, surfacing to the coordinator as a StreamResetError.
|
|
240
|
+
* Unlike the commit broadcast there is no follow-up scheduled retry, so a
|
|
241
|
+
* single reset here would otherwise drop the peer and sink super-majority.
|
|
242
|
+
*/
|
|
243
|
+
promiseImmediateRetries?: number;
|
|
244
|
+
/** Read-repair behavior: 'off' (only fetch on missing — legacy), 'lazy' (fetch when local age > window), 'paranoid' (always verify against cluster on read, missing blocks included). Default 'lazy'. In 'off' and 'lazy', a missing block whose cohort is this node alone is re-consulted at most once per `readRepairWindowMs`. */
|
|
245
|
+
readRepairMode?: 'off' | 'lazy' | 'paranoid';
|
|
246
|
+
/** For 'lazy' mode: read-repair triggers when (now - localEntry.lastSeenCommitMs) > this. In 'lazy' and 'off' it is also how long a settled absence is remembered. Default 10000. */
|
|
247
|
+
readRepairWindowMs?: number;
|
|
248
|
+
/** Per-read probability of a cohort check in 'lazy' mode even within the window — of a held block or a settled absence (0..1). Default 0 (no random check). */
|
|
249
|
+
readRepairSampleRate?: number;
|
|
250
|
+
/**
|
|
251
|
+
* When FRET has no confident network-size estimate, allow an undersized cluster
|
|
252
|
+
* (peerCount < minAbsoluteClusterSize) to proceed anyway. Default false: with no
|
|
253
|
+
* confident estimate an undersized cluster is REJECTED. Turn on only for
|
|
254
|
+
* single-node / local dev where you knowingly run below the safe floor.
|
|
255
|
+
*/
|
|
256
|
+
allowUnvalidatedSmallCluster?: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* What a member WITH a transaction validator does with a pend that carries no `validation`
|
|
259
|
+
* payload. See {@link UnvalidatablePendPolicy}; default 'accept'.
|
|
260
|
+
*/
|
|
261
|
+
unvalidatablePendPolicy?: UnvalidatablePendPolicy;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* What a receiver WITH a transaction checker does with a pend that carries no
|
|
266
|
+
* {@link PendRequest.validation} payload — the single-collection (`Collection.sync`) shape, which
|
|
267
|
+
* has no transaction to re-execute.
|
|
268
|
+
*
|
|
269
|
+
* - `'accept'` (default) preserves the historical behaviour: the pend is approved unchecked.
|
|
270
|
+
* - `'reject'` is the fail-closed posture for a deployment that has decided every write must be
|
|
271
|
+
* re-checkable; it REFUSES `Collection.sync` writes, which is the point, not a bug.
|
|
272
|
+
*
|
|
273
|
+
* Irrelevant on a receiver with no checker, which never re-checks anything. Named once here and
|
|
274
|
+
* referenced by every tier that carries the knob (`ClusterConsensusConfig`, db-p2p's
|
|
275
|
+
* `ClusterPolicyOptions` and `StorageRepoOptions`) so the three cannot drift apart.
|
|
276
|
+
*/
|
|
277
|
+
export type UnvalidatablePendPolicy = 'accept' | 'reject';
|
|
@@ -646,8 +646,36 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
646
646
|
|
|
647
647
|
/** Restore the staged state captured by {@link snapshotPending}, discarding any
|
|
648
648
|
* mutations staged since. Reads through the collection then observe exactly the
|
|
649
|
-
* snapshot state again; storage is untouched because nothing was ever synced.
|
|
649
|
+
* snapshot state again; storage is untouched because nothing was ever synced.
|
|
650
|
+
*
|
|
651
|
+
* A snapshot is only restorable VERBATIM onto the committed boundary it was captured
|
|
652
|
+
* on. If this collection has since ADOPTED a newer committed revision — a rival's
|
|
653
|
+
* commit folded in by a refresh while the snapshot's transaction was in flight, e.g.
|
|
654
|
+
* the conflict replay that refused a guarded insert (TreeKeyTakenError) — the
|
|
655
|
+
* snapshot's transforms describe block state at the OLD boundary, and reinstalling
|
|
656
|
+
* them would shadow committed blocks with stale structure. The observed case: an
|
|
657
|
+
* INVENTED collection's pre-commit header/root transforms restored over the rival's
|
|
658
|
+
* now-committed collection make every later read descend an empty tree, silently
|
|
659
|
+
* hiding the committed rows. When the snapshot's pending queue is empty (the
|
|
660
|
+
* transaction-rollback shape: the capture predates the transaction's first stage),
|
|
661
|
+
* the correct restore target IS the committed state — reset the tracker empty and
|
|
662
|
+
* let reads flow through to the adopted revision.
|
|
663
|
+
*
|
|
664
|
+
* NOTE: a snapshot that carries PENDING actions across a moved boundary (a
|
|
665
|
+
* mid-transaction savepoint captured before a mid-transaction refresh adopted a
|
|
666
|
+
* rival's commit) still restores verbatim below — rebasing it would require an async
|
|
667
|
+
* replay this synchronous method cannot run. That shape predates this guard and
|
|
668
|
+
* keeps its old behaviour; if it is ever observed producing stale reads, the rebase
|
|
669
|
+
* belongs in an async caller that can replay the pending queue (see replayActions). */
|
|
650
670
|
restorePending(snapshot: CollectionSnapshot<TAction>): void {
|
|
671
|
+
const capturedRev = snapshot.context?.rev;
|
|
672
|
+
const currentRev = this.source.actionContext?.rev;
|
|
673
|
+
const boundaryMoved = currentRev !== undefined && (capturedRev === undefined || currentRev > capturedRev);
|
|
674
|
+
if (boundaryMoved && snapshot.pending.length === 0) {
|
|
675
|
+
this.tracker.reset();
|
|
676
|
+
this.pending = [];
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
651
679
|
this.tracker.reset(copyTransforms(snapshot.transforms));
|
|
652
680
|
this.pending = [...snapshot.pending];
|
|
653
681
|
}
|
|
@@ -763,7 +791,14 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
763
791
|
* process can be at different revisions at the same instant. That gap is invisible
|
|
764
792
|
* from outside the class without this accessor, which is the whole reason it
|
|
765
793
|
* exists: `docs/debugging.md` (§ "Which revision did a read descend?") explains
|
|
766
|
-
* how an operator reads the difference.
|
|
794
|
+
* how an operator reads the difference.
|
|
795
|
+
*
|
|
796
|
+
* The one exception is `undefined` itself, which is a STATE rather than a revision:
|
|
797
|
+
* "this instance invented the collection and has never adopted a committed revision".
|
|
798
|
+
* For the same reason (only this instance moves it), it stays true until this instance
|
|
799
|
+
* updates, syncs or records a commit, so a caller holding a freshly opened instance may
|
|
800
|
+
* branch on it — the Quereus adapter does, to leave an invented, never-written index tree
|
|
801
|
+
* unflushed exactly as an unwritten table tree is left. Never branch on the NUMBER. */
|
|
767
802
|
committedRevision(): number | undefined {
|
|
768
803
|
return this.source.actionContext?.rev;
|
|
769
804
|
}
|