@optimystic/db-core 0.17.0 → 0.18.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 +336 -336
- package/dist/src/btree/btree.d.ts +1 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +5 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +14 -3
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/collection/collection-type-registry.d.ts +3 -2
- package/dist/src/collection/collection-type-registry.d.ts.map +1 -1
- package/dist/src/collection/collection-type-registry.js.map +1 -1
- package/dist/src/collection/collection.d.ts +34 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +86 -23
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts +6 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +26 -19
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +15 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +37 -12
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/index.d.ts +1 -0
- package/dist/src/network/index.d.ts.map +1 -1
- package/dist/src/network/index.js +1 -0
- package/dist/src/network/index.js.map +1 -1
- package/dist/src/network/stale-failure.d.ts +11 -0
- package/dist/src/network/stale-failure.d.ts.map +1 -0
- package/dist/src/network/stale-failure.js +12 -0
- package/dist/src/network/stale-failure.js.map +1 -0
- package/dist/src/network/struct.d.ts +31 -0
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +16 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +6 -2
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +16 -10
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +64 -18
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +9 -1
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +4 -1
- package/src/cluster/structs.ts +14 -3
- package/src/collection/collection-type-registry.ts +3 -2
- package/src/collection/collection.ts +102 -24
- package/src/collections/diary/diary.ts +67 -59
- package/src/collections/tree/tree.ts +63 -12
- package/src/network/index.ts +1 -0
- package/src/network/stale-failure.ts +13 -0
- package/src/network/struct.ts +34 -0
- package/src/testing/test-transactor.ts +6 -2
- package/src/transaction/coordinator.ts +16 -10
- package/src/transactor/network-transactor.ts +64 -18
- package/src/transactor/transactor-source.ts +9 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { peerIdFromString } from "../network/types.js";
|
|
2
2
|
import type { PeerId } from "../network/types.js";
|
|
3
|
+
import { isConflictFailure } from "../network/stale-failure.js";
|
|
4
|
+
import { BlockUnavailableError } from "../network/struct.js";
|
|
3
5
|
import type { ActionTransforms, ActionBlocks, BlockActionStatus, ITransactor, PendSuccess, StaleFailure, IKeyNetwork, BlockId, GetBlockResults, PendResult, CommitResult, PendRequest, IRepo, BlockGets, Transforms, CommitRequest, ActionId, RepoCommitRequest, ClusterNomineesResult, CollectionId, IBlock } from "../index.js";
|
|
4
6
|
import type { IBlockChangeNotifier, CollectionChangeListener } from "./change-notifier.js";
|
|
5
7
|
import { transformForBlockId, groupBy, concatTransforms, concatTransform, transformsFromTransform, blockIdsForTransforms, Log, Tracker, CacheSource, TransactorSource } from "../index.js";
|
|
@@ -130,28 +132,33 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
// Second-chance retry: ONLY for a genuine no-response — a batch with no valid
|
|
133
|
-
// response,
|
|
134
|
-
// authoritative "absent" answer (a valid response that
|
|
135
|
-
// every requested block id, even one whose entry has only
|
|
136
|
-
// materialized `block`) is FINAL and must not retry. A block that
|
|
137
|
-
// does not exist yet surfaces as `{ state: {} }` (an entry that is
|
|
138
|
-
// retrying it doubles the round-trips on the common
|
|
139
|
-
// block exist?" probe. Cross-member reconciliation for a
|
|
140
|
-
// already happened one layer down: CoordinatorRepo.get detects
|
|
141
|
-
// consults cluster peers before it responds
|
|
142
|
-
//
|
|
143
|
-
//
|
|
135
|
+
// response, a response missing an entry for a requested block id, or an entry
|
|
136
|
+
// flagged `unavailable`. An authoritative "absent" answer (a valid response that
|
|
137
|
+
// carries an entry for every requested block id, even one whose entry has only
|
|
138
|
+
// `state` and no materialized `block`) is FINAL and must not retry. A block that
|
|
139
|
+
// genuinely does not exist yet surfaces as `{ state: {} }` (an entry that is
|
|
140
|
+
// present and unflagged) — retrying it doubles the round-trips on the common
|
|
141
|
+
// createOrOpen "does this block exist?" probe. Cross-member reconciliation for a
|
|
142
|
+
// missing block has already happened one layer down: CoordinatorRepo.get detects
|
|
143
|
+
// `isMissing` and consults cluster peers before it responds — and when that
|
|
144
|
+
// consult FAILS, the entry now says so via `unavailable` instead of posing as an
|
|
145
|
+
// authoritative absent. So by the time an unflagged absent reaches here there is
|
|
146
|
+
// nothing left for a transactor-level retry to discover, while a flagged entry
|
|
147
|
+
// earns the retry against a different peer that an absent deliberately does not.
|
|
148
|
+
// See tickets txn-perf-authoritative-notfound and repo-reports-unavailable-vs-absent.
|
|
144
149
|
const hasValidResponse = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
145
150
|
return b.request?.isResponse === true && b.request.response != null;
|
|
146
151
|
};
|
|
147
152
|
|
|
148
153
|
// A batch is answered when its response carries an entry for EVERY requested
|
|
149
|
-
// block id
|
|
150
|
-
// "absent", which counts as
|
|
154
|
+
// block id and none of those entries is flagged `unavailable`. An entry present
|
|
155
|
+
// with only `state` (no `block`) is an authoritative "absent", which counts as
|
|
156
|
+
// answered — not a gap. An `unavailable` entry is the peer saying its own answer
|
|
157
|
+
// is a guess, so it does NOT count as answered.
|
|
151
158
|
const isAuthoritative = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
152
159
|
if (!hasValidResponse(b)) return false;
|
|
153
160
|
const resp = b.request!.response! as GetBlockResults;
|
|
154
|
-
return b.payload.every(bid => resp[bid] !== undefined);
|
|
161
|
+
return b.payload.every(bid => resp[bid] !== undefined && resp[bid]!.unavailable === undefined);
|
|
155
162
|
};
|
|
156
163
|
|
|
157
164
|
// Retry only genuine no-response / partial-response batches. An authoritative
|
|
@@ -199,16 +206,24 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
199
206
|
// Cache the completed batches that had actual responses (not just coordinator not found)
|
|
200
207
|
const completedBatches = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && !isRecordEmpty(b.request!.response!)));
|
|
201
208
|
|
|
209
|
+
// Three-way ranking per block id: a materialized block beats an authoritative
|
|
210
|
+
// absent, which beats an `unavailable` guess — one peer that positively knows the
|
|
211
|
+
// block is absent outranks another that could not find out. Non-object junk ranks
|
|
212
|
+
// below everything so any real entry replaces it.
|
|
213
|
+
const rankOf = (r: unknown): number => {
|
|
214
|
+
if (!r || typeof r !== 'object') return -1;
|
|
215
|
+
const entry = r as GetBlockResults[BlockId];
|
|
216
|
+
if (entry.block != null) return 2;
|
|
217
|
+
return entry.unavailable === undefined ? 1 : 0;
|
|
218
|
+
};
|
|
219
|
+
|
|
202
220
|
// Create a lookup map from successful responses only
|
|
203
221
|
const resultEntries = new Map<string, any>();
|
|
204
222
|
for (const batch of completedBatches) {
|
|
205
223
|
const resp = batch.request!.response! as any;
|
|
206
224
|
for (const [bid, res] of Object.entries(resp)) {
|
|
207
225
|
const existing = resultEntries.get(bid);
|
|
208
|
-
|
|
209
|
-
const resHasBlock = res && typeof res === 'object' && 'block' in (res as any) && (res as any).block != null;
|
|
210
|
-
const existingHasBlock = existing && typeof existing === 'object' && 'block' in (existing as any) && (existing as any).block != null;
|
|
211
|
-
if (!existing || (resHasBlock && !existingHasBlock)) {
|
|
226
|
+
if (!existing || rankOf(res) > rankOf(existing)) {
|
|
212
227
|
resultEntries.set(bid, res);
|
|
213
228
|
}
|
|
214
229
|
}
|
|
@@ -245,6 +260,17 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
245
260
|
// Get block states from repos
|
|
246
261
|
const blockStates = await this.get({ blockIds: allBlockIds });
|
|
247
262
|
|
|
263
|
+
// A block whose repo could not determine whether it exists carries no status either:
|
|
264
|
+
// its empty `state` would read below as `aborted`, turning "I could not find out" into
|
|
265
|
+
// a definite verdict on someone's action. Fail loudly instead, like every other read of
|
|
266
|
+
// an unavailable block (see BlockUnavailableError).
|
|
267
|
+
for (const blockId of allBlockIds) {
|
|
268
|
+
const entry = blockStates[blockId];
|
|
269
|
+
if (entry?.unavailable !== undefined && entry.block == null) {
|
|
270
|
+
throw new BlockUnavailableError(blockId, entry.unavailable);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
248
274
|
// Determine status for each action ref
|
|
249
275
|
const results: BlockActionStatus[] = blockActions.map(ref => ({
|
|
250
276
|
...ref,
|
|
@@ -511,8 +537,28 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
511
537
|
const stale = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && !b.request!.response!.success));
|
|
512
538
|
if (stale.length > 0) { // Any active stale failures should preempt reporting connection or other potential transient errors (we have information)
|
|
513
539
|
log('pend:stale actionId=%s staleCount=%d', blockAction.actionId, stale.length);
|
|
540
|
+
// Carry the first available reject reason through: `SyncRetryExhaustedError.lastReason`
|
|
541
|
+
// and the multi-collection writer's failure message both read it, and it is the only
|
|
542
|
+
// diagnostic that survives an exhausted retry budget.
|
|
543
|
+
const reason = stale.map(b => (b.request!.response! as StaleFailure).reason).find(r => r !== undefined);
|
|
544
|
+
// This response is REBUILT from the per-batch ones rather than forwarded, so
|
|
545
|
+
// retryability has to be carried explicitly or it is lost: a batch whose failure was
|
|
546
|
+
// a confirmed lost race can arrive with neither `missing` nor `pending` (see
|
|
547
|
+
// CoordinatorRepo.classifyStaleRejection), and the aggregate would then look like a
|
|
548
|
+
// hard rejection to `isConflictFailure`. Any conflicting batch makes the aggregate a
|
|
549
|
+
// conflict — the pend failed as a whole, and a re-read/rebase can clear it.
|
|
550
|
+
// NOTE: `some`, not `every`, so a pend whose batches mix a lost race with a genuine hard
|
|
551
|
+
// rejection is reported retryable and burns its (bounded, backed-off) retry budget before
|
|
552
|
+
// failing. Deliberate: an unclassified reason-only response from an older peer is
|
|
553
|
+
// indistinguishable from a hard rejection here, and `every` would refuse to retry a real
|
|
554
|
+
// race whenever one batch came from such a peer. Revisit if every producer sets `conflict`
|
|
555
|
+
// (then `every` is both safe and tighter), or if mixed-outcome pends show up as wasted
|
|
556
|
+
// retry latency in practice.
|
|
557
|
+
const conflict = stale.some(b => isConflictFailure(b.request!.response! as StaleFailure));
|
|
514
558
|
return {
|
|
515
559
|
success: false,
|
|
560
|
+
conflict,
|
|
561
|
+
...(reason === undefined ? {} : { reason }),
|
|
516
562
|
missing: distinctBlockActionTransforms(stale.flatMap(b => (b.request!.response! as StaleFailure).missing).filter((x): x is ActionTransforms => x !== undefined)),
|
|
517
563
|
};
|
|
518
564
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from '@noble/hashes/utils.js'
|
|
2
2
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
3
|
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId, StaleFailure, ActionContext, BlockType, BlockSource, ReadPurpose, Transforms } from "../index.js";
|
|
4
|
+
import { BlockUnavailableError } from "../network/struct.js";
|
|
4
5
|
import type { ReadDependency } from "../transaction/transaction.js";
|
|
5
6
|
import { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
6
7
|
|
|
@@ -42,7 +43,14 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
42
43
|
// `result[id]` is undefined. Destructuring that would throw a TypeError.
|
|
43
44
|
const entry = result?.[id];
|
|
44
45
|
if (entry) {
|
|
45
|
-
const { block, state } = entry;
|
|
46
|
+
const { block, state, unavailable } = entry;
|
|
47
|
+
// An entry flagged `unavailable` with no block is the repo saying "I could not find
|
|
48
|
+
// out whether this exists" — an answer that must not be read as absent. Throw rather
|
|
49
|
+
// than return undefined, and record no read dependency (dependencies are recorded
|
|
50
|
+
// only for blocks that actually exist). A repo that omits the flag stays authoritative.
|
|
51
|
+
if (!block && unavailable) {
|
|
52
|
+
throw new BlockUnavailableError(id, unavailable);
|
|
53
|
+
}
|
|
46
54
|
// Record a read dependency only for a block that actually exists. A transactor may return a
|
|
47
55
|
// populated entry with `block: undefined` for a genuinely-missing block (TestTransactor does;
|
|
48
56
|
// the Network transactor always populates the key); recording there would add a phantom
|