@optimystic/db-core 0.16.3 → 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.
Files changed (60) hide show
  1. package/README.md +336 -336
  2. package/dist/src/btree/btree.d.ts +1 -1
  3. package/dist/src/btree/btree.d.ts.map +1 -1
  4. package/dist/src/btree/btree.js +5 -1
  5. package/dist/src/btree/btree.js.map +1 -1
  6. package/dist/src/cluster/structs.d.ts +14 -3
  7. package/dist/src/cluster/structs.d.ts.map +1 -1
  8. package/dist/src/collection/collection-type-registry.d.ts +3 -2
  9. package/dist/src/collection/collection-type-registry.d.ts.map +1 -1
  10. package/dist/src/collection/collection-type-registry.js.map +1 -1
  11. package/dist/src/collection/collection.d.ts +34 -0
  12. package/dist/src/collection/collection.d.ts.map +1 -1
  13. package/dist/src/collection/collection.js +86 -23
  14. package/dist/src/collection/collection.js.map +1 -1
  15. package/dist/src/collections/diary/diary.d.ts +6 -1
  16. package/dist/src/collections/diary/diary.d.ts.map +1 -1
  17. package/dist/src/collections/diary/diary.js +26 -19
  18. package/dist/src/collections/diary/diary.js.map +1 -1
  19. package/dist/src/collections/tree/tree.d.ts +15 -0
  20. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  21. package/dist/src/collections/tree/tree.js +37 -12
  22. package/dist/src/collections/tree/tree.js.map +1 -1
  23. package/dist/src/network/index.d.ts +1 -0
  24. package/dist/src/network/index.d.ts.map +1 -1
  25. package/dist/src/network/index.js +1 -0
  26. package/dist/src/network/index.js.map +1 -1
  27. package/dist/src/network/stale-failure.d.ts +11 -0
  28. package/dist/src/network/stale-failure.d.ts.map +1 -0
  29. package/dist/src/network/stale-failure.js +12 -0
  30. package/dist/src/network/stale-failure.js.map +1 -0
  31. package/dist/src/network/struct.d.ts +31 -0
  32. package/dist/src/network/struct.d.ts.map +1 -1
  33. package/dist/src/network/struct.js +16 -1
  34. package/dist/src/network/struct.js.map +1 -1
  35. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  36. package/dist/src/testing/test-transactor.js +6 -2
  37. package/dist/src/testing/test-transactor.js.map +1 -1
  38. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  39. package/dist/src/transaction/coordinator.js +16 -10
  40. package/dist/src/transaction/coordinator.js.map +1 -1
  41. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  42. package/dist/src/transactor/network-transactor.js +64 -18
  43. package/dist/src/transactor/network-transactor.js.map +1 -1
  44. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  45. package/dist/src/transactor/transactor-source.js +9 -1
  46. package/dist/src/transactor/transactor-source.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/btree/btree.ts +4 -1
  49. package/src/cluster/structs.ts +14 -3
  50. package/src/collection/collection-type-registry.ts +3 -2
  51. package/src/collection/collection.ts +102 -24
  52. package/src/collections/diary/diary.ts +67 -59
  53. package/src/collections/tree/tree.ts +63 -12
  54. package/src/network/index.ts +1 -0
  55. package/src/network/stale-failure.ts +13 -0
  56. package/src/network/struct.ts +34 -0
  57. package/src/testing/test-transactor.ts +6 -2
  58. package/src/transaction/coordinator.ts +16 -10
  59. package/src/transactor/network-transactor.ts +64 -18
  60. 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, or a response missing an entry for a requested block id. An
134
- // authoritative "absent" answer (a valid response that carries an entry for
135
- // every requested block id, even one whose entry has only `state` and no
136
- // materialized `block`) is FINAL and must not retry. A block that genuinely
137
- // does not exist yet surfaces as `{ state: {} }` (an entry that is present) —
138
- // retrying it doubles the round-trips on the common createOrOpen "does this
139
- // block exist?" probe. Cross-member reconciliation for a missing block has
140
- // already happened one layer down: CoordinatorRepo.get detects `isMissing` and
141
- // consults cluster peers before it responds, so by the time an authoritative
142
- // absent reaches here there is nothing left for a transactor-level retry to
143
- // discover. See ticket txn-perf-authoritative-notfound.
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. An entry present with only `state` (no `block`) is an authoritative
150
- // "absent", which counts as answered — not a gap.
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
- // Prefer responses that include a materialized block
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