@optimystic/db-core 0.19.0 → 0.21.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/dist/src/collection/collection.d.ts +70 -8
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +76 -15
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +20 -8
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +24 -8
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/struct.d.ts +13 -0
- package/dist/src/network/struct.d.ts.map +1 -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 +25 -4
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +7 -0
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +5 -0
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +15 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +13 -1
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +25 -1
- package/dist/src/transform/cache-source.js.map +1 -1
- package/package.json +1 -1
- package/src/collection/collection.ts +107 -16
- package/src/collections/tree/readme.md +32 -0
- package/src/collections/tree/tree.ts +312 -293
- package/src/network/struct.ts +13 -0
- package/src/testing/test-transactor.ts +22 -5
- package/src/transactor/network-transactor.ts +7 -0
- package/src/transactor/transactor-source.ts +16 -2
- package/src/transform/cache-source.ts +25 -0
package/src/network/struct.ts
CHANGED
|
@@ -162,6 +162,19 @@ export type GetBlockResult = {
|
|
|
162
162
|
block?: IBlock;
|
|
163
163
|
/** The latest and pending states of the repo that retrieved the block */
|
|
164
164
|
state: BlockActionState;
|
|
165
|
+
/** The revision the returned `block` was actually materialized at — the highest committed
|
|
166
|
+
* revision of THIS block at or below the caller's {@link BlockGets.context}`.rev`. Differs
|
|
167
|
+
* from `state.latest.rev` only for a revision-pinned read of a block that has committed
|
|
168
|
+
* further since the pin; for an unpinned read the two agree.
|
|
169
|
+
*
|
|
170
|
+
* This — not `state.latest.rev` — is the revision a read observed, so it is what a read
|
|
171
|
+
* dependency must record (recording `latest` would claim the reader saw content it never
|
|
172
|
+
* read, and the validator's stale-read check would wrongly pass). `state.latest` keeps its
|
|
173
|
+
* own meaning: the newest revision the answering repo holds for the block.
|
|
174
|
+
*
|
|
175
|
+
* Optional: a producer that does not know the materialized revision leaves it absent
|
|
176
|
+
* rather than guessing, and consumers fall back to `state.latest?.rev ?? 0`. */
|
|
177
|
+
materializedRev?: number;
|
|
165
178
|
/** Set when this repo could not determine whether the block exists — its answer is a
|
|
166
179
|
* guess, not an authoritative absent. Every producer that omits it (including
|
|
167
180
|
* TestTransactor) keeps meaning "authoritative". */
|
|
@@ -61,6 +61,11 @@ export class TestTransactor implements ITransactor {
|
|
|
61
61
|
|
|
62
62
|
// Get the appropriate materialized block based on context
|
|
63
63
|
let block: IBlock | undefined;
|
|
64
|
+
// The revision `block` was actually materialized at, reported as
|
|
65
|
+
// GetBlockResult.materializedRev. Equals `latestRev` on every unpinned path; only a
|
|
66
|
+
// revision-pinned read of a block committed further since the pin makes them differ,
|
|
67
|
+
// and there the pinned value is what the reader observed (see the field's doc).
|
|
68
|
+
let materializedRev: number | undefined;
|
|
64
69
|
if (blockGets.context?.actionId !== undefined) {
|
|
65
70
|
// If requesting a specific action, apply pending transform if it exists
|
|
66
71
|
const pendingTransform = blockState.pendingActions.get(blockGets.context.actionId);
|
|
@@ -68,6 +73,9 @@ export class TestTransactor implements ITransactor {
|
|
|
68
73
|
// Read latest committed block as base for pending transform
|
|
69
74
|
const baseBlock = blockState.materializedBlocks.get(blockState.latestRev);
|
|
70
75
|
block = applyTransformSafe(baseBlock, pendingTransform);
|
|
76
|
+
// A pending carries no revision of its own — report the committed base it was
|
|
77
|
+
// applied over. Absent when there was no base (a pending-only insert).
|
|
78
|
+
if (baseBlock) materializedRev = blockState.latestRev;
|
|
71
79
|
} else {
|
|
72
80
|
// Action not pending, maybe committed? Or maybe invalid actionId for context.
|
|
73
81
|
// For simplicity, return undefined block if specific pending action not found.
|
|
@@ -83,29 +91,37 @@ export class TestTransactor implements ITransactor {
|
|
|
83
91
|
if (pendingTransform) {
|
|
84
92
|
const baseBlock = blockState.materializedBlocks.get(blockState.latestRev);
|
|
85
93
|
block = applyTransformSafe(baseBlock, pendingTransform);
|
|
94
|
+
if (baseBlock) materializedRev = blockState.latestRev;
|
|
86
95
|
break;
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
// Fall through to standard resolution if no pending match
|
|
90
99
|
if (block === undefined) {
|
|
91
100
|
if (blockGets.context.rev !== undefined) {
|
|
92
|
-
|
|
101
|
+
const found = latestMaterializedAt(blockState, blockGets.context.rev);
|
|
102
|
+
block = structuredClone(found?.block);
|
|
103
|
+
materializedRev = found?.rev;
|
|
93
104
|
} else {
|
|
94
105
|
block = structuredClone(blockState.materializedBlocks.get(blockState.latestRev));
|
|
106
|
+
if (block) materializedRev = blockState.latestRev;
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
} else if (blockGets.context?.rev !== undefined) {
|
|
98
110
|
// Return the materialized block at the highest revision ≤ requested
|
|
99
|
-
|
|
111
|
+
const found = latestMaterializedAt(blockState, blockGets.context.rev);
|
|
112
|
+
block = structuredClone(found?.block);
|
|
113
|
+
materializedRev = found?.rev;
|
|
100
114
|
} else {
|
|
101
115
|
// Otherwise return latest materialized block
|
|
102
116
|
block = structuredClone(blockState.materializedBlocks.get(blockState.latestRev));
|
|
117
|
+
if (block) materializedRev = blockState.latestRev;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
|
|
106
121
|
const actionId = blockState.revisionActions.get(blockState.latestRev);
|
|
107
122
|
results[blockId] = {
|
|
108
123
|
block,
|
|
124
|
+
...(materializedRev !== undefined ? { materializedRev } : {}),
|
|
109
125
|
state: {
|
|
110
126
|
latest: actionId !== undefined ? {
|
|
111
127
|
rev: blockState.latestRev,
|
|
@@ -457,11 +473,12 @@ function newBlockState(): BlockState {
|
|
|
457
473
|
};
|
|
458
474
|
}
|
|
459
475
|
|
|
460
|
-
/** Returns the materialized block at the highest revision ≤ the given revision
|
|
461
|
-
|
|
476
|
+
/** Returns the materialized block at the highest revision ≤ the given revision, together with
|
|
477
|
+
* that revision — the caller reports it as {@link GetBlockResult.materializedRev}. */
|
|
478
|
+
function latestMaterializedAt(blockState: BlockState, maxRev: number): { block: IBlock, rev: number } | undefined {
|
|
462
479
|
for (let rev = maxRev; rev >= 0; rev--) {
|
|
463
480
|
const block = blockState.materializedBlocks.get(rev);
|
|
464
|
-
if (block) return block;
|
|
481
|
+
if (block) return { block, rev };
|
|
465
482
|
}
|
|
466
483
|
return undefined;
|
|
467
484
|
}
|
|
@@ -215,6 +215,13 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
215
215
|
// absent, which beats an `unavailable` guess — one peer that positively knows the
|
|
216
216
|
// block is absent outranks another that could not find out. Non-object junk ranks
|
|
217
217
|
// below everything so any real entry replaces it.
|
|
218
|
+
// NOTE: `materializedRev` is not part of the ranking, so two peers answering the same
|
|
219
|
+
// pinned get with block-carrying entries at DIFFERENT materialized revisions resolve
|
|
220
|
+
// to whichever arrived first. Not a concern today — cohort peers share the block's
|
|
221
|
+
// revision log, so they agree on the highest committed rev at or below a pin — and the
|
|
222
|
+
// failure direction is safe (a lower recorded revision spuriously stale-rejects rather
|
|
223
|
+
// than wrongly accepting). If peers are ever seen to disagree here, break the tie on
|
|
224
|
+
// the HIGHEST materializedRev among rank-2 entries.
|
|
218
225
|
const rankOf = (r: unknown): number => {
|
|
219
226
|
if (!r || typeof r !== 'object') return -1;
|
|
220
227
|
const entry = r as GetBlockResults[BlockId];
|
|
@@ -43,7 +43,7 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
43
43
|
// `result[id]` is undefined. Destructuring that would throw a TypeError.
|
|
44
44
|
const entry = result?.[id];
|
|
45
45
|
if (entry) {
|
|
46
|
-
const { block, state, unavailable } = entry;
|
|
46
|
+
const { block, state, materializedRev, unavailable } = entry;
|
|
47
47
|
// An entry flagged `unavailable` with no block is the repo saying "I could not find
|
|
48
48
|
// out whether this exists" — an answer that must not be read as absent. Throw rather
|
|
49
49
|
// than return undefined, and record no read dependency (dependencies are recorded
|
|
@@ -60,7 +60,13 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
60
60
|
// Record read dependency for optimistic concurrency control, carrying the caller's
|
|
61
61
|
// read purpose (default `value`) so a purely-structural navigation read can later be
|
|
62
62
|
// dropped from the conflict set (see ReadDependencyCollector / Theorem 5).
|
|
63
|
-
|
|
63
|
+
// Record the revision the content was MATERIALIZED at, not the newest the repo holds —
|
|
64
|
+
// see {@link GetBlockResult.materializedRev} for why `state.latest` is the wrong number
|
|
65
|
+
// and why the fallback preserves today's behaviour for repos that omit the field.
|
|
66
|
+
// Both sinks must take the SAME value: CacheSource learns it via getReadRevision on a
|
|
67
|
+
// miss-load and re-emits it on every later hit, so a split would stamp the cache
|
|
68
|
+
// differently from the collector.
|
|
69
|
+
const rev = materializedRev ?? state.latest?.rev ?? 0;
|
|
64
70
|
this.collector.record(id, rev, purpose);
|
|
65
71
|
this.readRevisions.set(id, rev);
|
|
66
72
|
}
|
|
@@ -81,6 +87,14 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
81
87
|
return this.collector.getReadDependencies();
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** The collector this source records into — the one shared with the collection's
|
|
91
|
+
* CacheSource. Exposed ONLY so a pinned read view built with `recordReads: true`
|
|
92
|
+
* can feed the same per-transaction read set (see Collection.createReadTracker);
|
|
93
|
+
* every other consumer should go through {@link getReadDependencies}. */
|
|
94
|
+
getCollector(): ReadDependencyCollector {
|
|
95
|
+
return this.collector;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
clearReadDependencies(): void {
|
|
85
99
|
this.collector.clear();
|
|
86
100
|
}
|
|
@@ -44,8 +44,20 @@ export class CacheSource<T extends IBlock> implements BlockSource<T> {
|
|
|
44
44
|
/** Shared per-transaction read-dependency accumulator (same instance the collection's
|
|
45
45
|
* TransactorSource holds). Optional: log-walk caches that never form a transaction omit it. */
|
|
46
46
|
private readonly collector?: ReadDependencyCollector,
|
|
47
|
+
/** Pre-warm entries for a pinned read view — the output of another cache's
|
|
48
|
+
* {@link snapshotEntries}. Entries are already cloned by snapshotEntries, so they are
|
|
49
|
+
* adopted as-is; per-id revisions ride along so a seeded HIT still records at the
|
|
50
|
+
* revision the block was committed at. Seeding does not bump generations (a fresh
|
|
51
|
+
* cache has no consumers with stale memos). */
|
|
52
|
+
seed?: ReadonlyArray<[BlockId, T, number]>,
|
|
47
53
|
) {
|
|
48
54
|
this.cache = new LruMap(maxSize);
|
|
55
|
+
if (seed) {
|
|
56
|
+
for (const [id, block, revision] of seed) {
|
|
57
|
+
this.cache.set(id, block);
|
|
58
|
+
this.revisions.set(id, revision);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
private bump(id: BlockId) {
|
|
@@ -121,6 +133,19 @@ export class CacheSource<T extends IBlock> implements BlockSource<T> {
|
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
/** A cloned copy of the current cache contents with each id's committed revision, in LRU
|
|
137
|
+
* order (oldest first, so replaying into another LruMap preserves eviction order). For
|
|
138
|
+
* building a pinned read view ONLY (see {@link Collection.createReadTracker}): pass the
|
|
139
|
+
* result as the `seed` of a fresh, PRIVATE CacheSource. Blocks are cloned on the way out,
|
|
140
|
+
* so the seeded cache shares no mutable state with this one. */
|
|
141
|
+
snapshotEntries(): Array<[BlockId, T, number]> {
|
|
142
|
+
const entries: Array<[BlockId, T, number]> = [];
|
|
143
|
+
for (const [id, block] of this.cache) {
|
|
144
|
+
entries.push([id, structuredClone(block), this.revisions.get(id) ?? 0]);
|
|
145
|
+
}
|
|
146
|
+
return entries;
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
/** Mutates the cache without affecting the source. `revision` is the committed revision this
|
|
125
150
|
* transform lands at; the stored per-id revision advances to it so a later read records a
|
|
126
151
|
* dependency at the NEW revision (recording the old one would spuriously fail validation). */
|