@optimystic/db-core 1.0.0-beta.3 → 1.1.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/cluster/structs.d.ts +30 -6
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +5 -3
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +2 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +10 -3
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +8 -2
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/collection/collection.d.ts +439 -34
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +843 -148
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +109 -4
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +88 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts +6 -2
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +5 -2
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +124 -16
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +86 -11
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +13 -3
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +106 -40
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +8 -2
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/durability.d.ts +45 -0
- package/dist/src/network/durability.d.ts.map +1 -0
- package/dist/src/network/durability.js +114 -0
- package/dist/src/network/durability.js.map +1 -0
- package/dist/src/network/i-key-network.d.ts +14 -6
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/index.d.ts +3 -0
- package/dist/src/network/index.d.ts.map +1 -1
- package/dist/src/network/index.js +3 -0
- package/dist/src/network/index.js.map +1 -1
- package/dist/src/network/lineage.d.ts +39 -0
- package/dist/src/network/lineage.d.ts.map +1 -0
- package/dist/src/network/lineage.js +61 -0
- package/dist/src/network/lineage.js.map +1 -0
- package/dist/src/network/routing-key.d.ts +24 -0
- package/dist/src/network/routing-key.d.ts.map +1 -0
- package/dist/src/network/routing-key.js +9 -0
- package/dist/src/network/routing-key.js.map +1 -0
- package/dist/src/network/stale-failure.d.ts +5 -1
- package/dist/src/network/stale-failure.d.ts.map +1 -1
- package/dist/src/network/stale-failure.js +5 -1
- package/dist/src/network/stale-failure.js.map +1 -1
- package/dist/src/network/struct.d.ts +151 -8
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/reactivity/wire.d.ts.map +1 -1
- package/dist/src/reactivity/wire.js +8 -2
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +64 -7
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +150 -23
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +141 -21
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +408 -122
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +39 -10
- package/dist/src/transaction/errors.d.ts.map +1 -1
- package/dist/src/transaction/errors.js +34 -9
- package/dist/src/transaction/errors.js.map +1 -1
- package/dist/src/transaction/index.d.ts +1 -0
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transactor/block-floors.d.ts +78 -0
- package/dist/src/transactor/block-floors.d.ts.map +1 -0
- package/dist/src/transactor/block-floors.js +62 -0
- package/dist/src/transactor/block-floors.js.map +1 -0
- package/dist/src/transactor/change-notifier.d.ts +43 -0
- package/dist/src/transactor/change-notifier.d.ts.map +1 -1
- package/dist/src/transactor/change-notifier.js +3 -0
- package/dist/src/transactor/change-notifier.js.map +1 -1
- package/dist/src/transactor/index.d.ts +1 -0
- package/dist/src/transactor/index.d.ts.map +1 -1
- package/dist/src/transactor/index.js +1 -0
- package/dist/src/transactor/index.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +43 -6
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +279 -86
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +94 -4
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +120 -42
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transactor/transactor.d.ts +14 -1
- package/dist/src/transactor/transactor.d.ts.map +1 -1
- package/dist/src/transform/atomic.d.ts.map +1 -1
- package/dist/src/transform/atomic.js +12 -9
- package/dist/src/transform/atomic.js.map +1 -1
- package/dist/src/transform/base-pins.d.ts +50 -17
- package/dist/src/transform/base-pins.d.ts.map +1 -1
- package/dist/src/transform/base-pins.js +58 -11
- package/dist/src/transform/base-pins.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +75 -11
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +162 -36
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/digest.d.ts +8 -1
- package/dist/src/transform/digest.d.ts.map +1 -1
- package/dist/src/transform/digest.js +19 -9
- package/dist/src/transform/digest.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -2
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -3
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +94 -14
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +222 -68
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/structural-equals.d.ts +38 -0
- package/dist/src/utility/structural-equals.d.ts.map +1 -0
- package/dist/src/utility/structural-equals.js +77 -0
- package/dist/src/utility/structural-equals.js.map +1 -0
- package/package.json +2 -2
- package/src/cluster/structs.ts +28 -7
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +260 -253
- package/src/cohort-topic/wire/codec.ts +222 -216
- package/src/collection/collection.ts +960 -157
- package/src/collection/struct.ts +99 -4
- package/src/collections/diary/diary.ts +71 -68
- package/src/collections/tree/struct.ts +155 -23
- package/src/collections/tree/tree.ts +127 -40
- package/src/index.ts +1 -1
- package/src/matchmaking/wire.ts +8 -2
- package/src/network/durability.ts +123 -0
- package/src/network/i-key-network.ts +60 -52
- package/src/network/index.ts +10 -7
- package/src/network/lineage.ts +80 -0
- package/src/network/routing-key.ts +27 -0
- package/src/network/stale-failure.ts +5 -1
- package/src/network/struct.ts +156 -8
- package/src/reactivity/wire.ts +230 -224
- package/src/testing/test-transactor.ts +152 -24
- package/src/transaction/coordinator.ts +470 -131
- package/src/transaction/errors.ts +34 -9
- package/src/transaction/index.ts +1 -0
- package/src/transactor/block-floors.ts +105 -0
- package/src/transactor/change-notifier.ts +129 -80
- package/src/transactor/index.ts +1 -0
- package/src/transactor/network-transactor.ts +297 -89
- package/src/transactor/transactor-source.ts +131 -42
- package/src/transactor/transactor.ts +13 -1
- package/src/transform/atomic.ts +25 -22
- package/src/transform/base-pins.ts +76 -18
- package/src/transform/cache-source.ts +327 -194
- package/src/transform/digest.ts +21 -10
- package/src/transform/helpers.ts +8 -3
- package/src/transform/tracker.ts +423 -270
- package/src/utility/structural-equals.ts +72 -0
- package/dist/src/utility/block-id-to-bytes.d.ts +0 -3
- package/dist/src/utility/block-id-to-bytes.d.ts.map +0 -1
- package/dist/src/utility/block-id-to-bytes.js +0 -7
- package/dist/src/utility/block-id-to-bytes.js.map +0 -1
- package/src/utility/block-id-to-bytes.ts +0 -8
|
@@ -1,194 +1,327 @@
|
|
|
1
|
-
import type { IBlock, BlockHeader, BlockId, BlockSource, BlockType, ReadPurpose, Transforms } from "../index.js";
|
|
2
|
-
import { applyOperation } from "./helpers.js";
|
|
3
|
-
import { LruMap } from "../utility/lru-map.js";
|
|
4
|
-
import { createLogger } from "../logger.js";
|
|
5
|
-
import type { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
6
|
-
|
|
7
|
-
const log = createLogger('cache');
|
|
8
|
-
|
|
9
|
-
const DefaultMaxSize = 128;
|
|
10
|
-
|
|
11
|
-
/** The revision a source reports for an id, or undefined if the source can't report one.
|
|
12
|
-
* Duck-typed exactly like {@link Tracker}'s getGeneration probe — CacheSource layers over
|
|
13
|
-
* arbitrary BlockSources (including test doubles) that need not implement it. */
|
|
14
|
-
function sourceReadRevision(source: unknown, id: BlockId): number | undefined {
|
|
15
|
-
const src = source as { getReadRevision?: (id: BlockId) => number | undefined };
|
|
16
|
-
return typeof src.getReadRevision === 'function' ? src.getReadRevision(id) : undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
this.
|
|
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
|
-
|
|
1
|
+
import type { IBlock, BlockHeader, BlockId, BlockSource, BlockType, ReadPurpose, Transforms } from "../index.js";
|
|
2
|
+
import { applyOperation } from "./helpers.js";
|
|
3
|
+
import { LruMap } from "../utility/lru-map.js";
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
|
+
import type { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
6
|
+
|
|
7
|
+
const log = createLogger('cache');
|
|
8
|
+
|
|
9
|
+
const DefaultMaxSize = 128;
|
|
10
|
+
|
|
11
|
+
/** The revision a source reports for an id, or undefined if the source can't report one.
|
|
12
|
+
* Duck-typed exactly like {@link Tracker}'s getGeneration probe — CacheSource layers over
|
|
13
|
+
* arbitrary BlockSources (including test doubles) that need not implement it. */
|
|
14
|
+
function sourceReadRevision(source: unknown, id: BlockId): number | undefined {
|
|
15
|
+
const src = source as { getReadRevision?: (id: BlockId) => number | undefined };
|
|
16
|
+
return typeof src.getReadRevision === 'function' ? src.getReadRevision(id) : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What the source says about `block`, the object it just returned for `id`: the revision the
|
|
20
|
+
* content is, and whether it may be kept and served again without re-asking.
|
|
21
|
+
*
|
|
22
|
+
* Prefers the source's per-OBJECT answer (`describeServed`, which TransactorSource provides) over
|
|
23
|
+
* the by-id {@link sourceReadRevision}. This runs after an `await`, so with two reads of one id in
|
|
24
|
+
* flight a by-id record describes whichever answer the source processed last — not necessarily
|
|
25
|
+
* this one — and would stamp one answer's content with the other's revision and verdict. A source
|
|
26
|
+
* that offers neither allows keeping at revision 0, the behaviour before either probe existed. */
|
|
27
|
+
function sourceServed(source: unknown, id: BlockId, block: IBlock): { rev: number; mayRetain: boolean } {
|
|
28
|
+
const src = source as { describeServed?: (block: IBlock) => { rev: number; mayRetain: boolean } | undefined };
|
|
29
|
+
const described = typeof src.describeServed === 'function' ? src.describeServed(block) : undefined;
|
|
30
|
+
return described ?? { rev: sourceReadRevision(source, id) ?? 0, mayRetain: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class CacheSource<T extends IBlock> implements BlockSource<T> {
|
|
34
|
+
protected cache: LruMap<BlockId, T>;
|
|
35
|
+
/** Per-id monotonic counter, bumped whenever the cached content for an id changes.
|
|
36
|
+
* Consumers (e.g. {@link Tracker}'s materialized-block memo) read it via
|
|
37
|
+
* {@link getGeneration} to detect that a cached "source + ops" result has gone stale.
|
|
38
|
+
* Over-bumping is safe (it only forces a re-materialize); under-bumping is a correctness
|
|
39
|
+
* bug, so every content-changing site bumps. A benign LRU evict + reload also bumps. */
|
|
40
|
+
// NOTE: generations is never pruned — it retains one small (id → number) entry per distinct id
|
|
41
|
+
// ever touched, even after LRU eviction from `cache`. Bounded by the number of distinct blocks a
|
|
42
|
+
// collection sees over its lifetime; if that ever grows large enough to matter, evict alongside
|
|
43
|
+
// the LRU (dropping a generation is safe — a reload re-bumps from 0/absent, forcing re-materialize).
|
|
44
|
+
private generations = new Map<BlockId, number>();
|
|
45
|
+
/** Per-id committed revision of the content currently cached for that id. Learned from the
|
|
46
|
+
* source on a miss-load, advanced by {@link transformCache} when a commit folds new content
|
|
47
|
+
* in, and dropped alongside the cached block on delete/clear. Re-emitted on every cache HIT
|
|
48
|
+
* so a hit records a read dependency at the right revision — the whole point of this map, since
|
|
49
|
+
* the underlying source is never consulted on a hit. */
|
|
50
|
+
// NOTE: an LRU-evicted id keeps its `revisions` entry (eviction drops `cache` but not this map).
|
|
51
|
+
// That lingering entry is LOAD-BEARING, not a leak: every path that serves or forgets content for
|
|
52
|
+
// an id writes this map in the same step (keep, handThrough, clear, transformCache), so the entry
|
|
53
|
+
// is always the revision of the content LAST SERVED for the id — which is exactly what a write
|
|
54
|
+
// staged over a since-evicted read needs to declare as its base (a rev-only pin, see
|
|
55
|
+
// `PinnedBase.block` in base-pins.ts). Only `peek` pairs a revision with content, and it answers
|
|
56
|
+
// nothing for an evicted id, so the revision never pairs with a block it does not describe.
|
|
57
|
+
private revisions = new Map<BlockId, number>();
|
|
58
|
+
/** The most recent answer per id that this cache returned to a reader but did not keep — a
|
|
59
|
+
* below-floor answer, content older than a log entry the collection already walked says the
|
|
60
|
+
* block is (see {@link sourceServed}), or an answer overtaken while in flight over an id nothing
|
|
61
|
+
* is held for (see {@link admit}). NEVER served to a read: {@link tryGet} re-asks the source
|
|
62
|
+
* every time such an id is read, which is the whole point. It exists for the base probes alone
|
|
63
|
+
* ({@link peek} / {@link getCachedRevision}): a write staged over this content must declare the
|
|
64
|
+
* base it was really built on, at the revision it was really served at, so that the storage-side
|
|
65
|
+
* guard which refuses a transform whose declared base is not the one the member holds can fire
|
|
66
|
+
* instead of abstaining and applying edits computed against the old content over newer content.
|
|
67
|
+
* An id is never in both this map and `cache`/`revisions`. It leaves on its next keepable answer,
|
|
68
|
+
* on {@link clear}, and on any {@link transformCache} that touches it, so the map is bounded by
|
|
69
|
+
* the source's unmet floors plus the ids caught mid-flight by a clear or a fold. */
|
|
70
|
+
private unkept = new Map<BlockId, { block: T; rev: number }>();
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
protected readonly source: BlockSource<T>,
|
|
74
|
+
maxSize = DefaultMaxSize,
|
|
75
|
+
/** Shared per-transaction read-dependency accumulator (same instance the collection's
|
|
76
|
+
* TransactorSource holds). Optional: log-walk caches that never form a transaction omit it. */
|
|
77
|
+
private readonly collector?: ReadDependencyCollector,
|
|
78
|
+
/** Pre-warm entries for a pinned read view — the output of another cache's
|
|
79
|
+
* {@link snapshotEntries}. Entries are already cloned by snapshotEntries, so they are
|
|
80
|
+
* adopted as-is; per-id revisions ride along so a seeded HIT still records at the
|
|
81
|
+
* revision the block was committed at. Seeding does not bump generations (a fresh
|
|
82
|
+
* cache has no consumers with stale memos). */
|
|
83
|
+
seed?: ReadonlyArray<[BlockId, T, number]>,
|
|
84
|
+
) {
|
|
85
|
+
this.cache = new LruMap(maxSize);
|
|
86
|
+
if (seed) {
|
|
87
|
+
for (const [id, block, revision] of seed) {
|
|
88
|
+
this.cache.set(id, block);
|
|
89
|
+
this.revisions.set(id, revision);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private bump(id: BlockId) {
|
|
95
|
+
this.generations.set(id, (this.generations.get(id) ?? 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The current generation for an id — advances every time this cache's content for
|
|
99
|
+
* the id changes (miss-load, clear, or transformCache). Stable across pure cache hits. */
|
|
100
|
+
getGeneration(id: BlockId): number {
|
|
101
|
+
return this.generations.get(id) ?? 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** INVARIANT: what a read returns for an id is what this cache then describes for it — the
|
|
105
|
+
* block {@link peek} answers and the revision {@link getCachedRevision} answers, immediately
|
|
106
|
+
* after, are the content the reader holds, on every path (hit, kept, handed through, evicted
|
|
107
|
+
* since). A write staged over the returned content pins that revision as its base, so a read
|
|
108
|
+
* that returned one thing while the cache described another would put a wrong base on the
|
|
109
|
+
* commit — the one direction the storage-side guard cannot catch. {@link admit} is where the
|
|
110
|
+
* two are decided together. */
|
|
111
|
+
async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<T | undefined> {
|
|
112
|
+
const hit = this.cache.get(id);
|
|
113
|
+
if (hit) {
|
|
114
|
+
// Cache hit: the source is never consulted, so re-emit the revision we learned when this
|
|
115
|
+
// id was first loaded/folded. Without this a block served from cache records NO read
|
|
116
|
+
// dependency (the original bug), so its stale-read check could never fire. Carry the
|
|
117
|
+
// caller's purpose so a navigation-only cache hit stays droppable from the conflict set.
|
|
118
|
+
const rev = this.revisions.get(id);
|
|
119
|
+
if (rev !== undefined) this.collector?.record(id, rev, purpose);
|
|
120
|
+
log('hit id=%s', id);
|
|
121
|
+
return structuredClone(hit);
|
|
122
|
+
}
|
|
123
|
+
const generationAtMiss = this.getGeneration(id);
|
|
124
|
+
const answer = await this.source.tryGet(id, purpose);
|
|
125
|
+
if (!answer) {
|
|
126
|
+
// Absent block: record nothing (matches TransactorSource, which skips missing blocks).
|
|
127
|
+
log('miss:absent id=%s', id);
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const { rev, mayRetain } = sourceServed(this.source, id, answer);
|
|
131
|
+
const served = this.admit(id, answer, rev, mayRetain, generationAtMiss);
|
|
132
|
+
// Record the dependency at the revision RETURNED. On a miss the underlying TransactorSource
|
|
133
|
+
// already recorded the answer's own id@rev/purpose into the shared collector; max-wins
|
|
134
|
+
// (revision) + value-wins (purpose) collapse the two to one entry, at the higher revision.
|
|
135
|
+
this.collector?.record(id, served.rev, purpose);
|
|
136
|
+
return structuredClone(served.block);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Decide, for an answer the source just gave for `id`, what the reader gets and what this cache
|
|
140
|
+
* describes for the id from now on — one decision, so the two cannot come apart.
|
|
141
|
+
*
|
|
142
|
+
* A keepable answer that is {@link stillWanted} is kept and returned. Otherwise the answer is
|
|
143
|
+
* either below its floor or was overtaken while in flight, and of two answers to one id the
|
|
144
|
+
* higher revision is the truer — so if content is HELD for the id (a concurrent read answered
|
|
145
|
+
* well, a folded commit, or content that met a floor this answer does not), the held content is
|
|
146
|
+
* what the reader gets, exactly as a hit a moment later would get it; the answer leaves no trace.
|
|
147
|
+
* Held content is never older than a below-floor answer: raising a floor forgets the id in the
|
|
148
|
+
* same step, so anything held for it since met the floor. With nothing held, the answer is
|
|
149
|
+
* handed through (see {@link handThrough}): returned, described, not kept. */
|
|
150
|
+
private admit(id: BlockId, answer: T, rev: number, mayRetain: boolean, generationAtMiss: number): { block: T; rev: number } {
|
|
151
|
+
if (mayRetain && this.stillWanted(id, rev, generationAtMiss)) {
|
|
152
|
+
this.keep(id, answer, rev);
|
|
153
|
+
return { block: answer, rev };
|
|
154
|
+
}
|
|
155
|
+
const held = this.cache.get(id);
|
|
156
|
+
const heldRev = held === undefined ? undefined : this.revisions.get(id);
|
|
157
|
+
if (held !== undefined && heldRev !== undefined) {
|
|
158
|
+
log('miss:superseded id=%s rev=%d heldRev=%d unkeepable=%s', id, rev, heldRev, !mayRetain);
|
|
159
|
+
return { block: held, rev: heldRev };
|
|
160
|
+
}
|
|
161
|
+
this.handThrough(id, answer, rev);
|
|
162
|
+
return { block: answer, rev };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Whether an answer at `rev`, asked for when this id's generation was `generationAtMiss`, is
|
|
166
|
+
* still one to keep now that it has arrived.
|
|
167
|
+
*
|
|
168
|
+
* A miss is decided before the `await` and acted on after it. If the generation has not moved,
|
|
169
|
+
* nothing happened to the id meanwhile and the answer is kept. If it has — a second read of the
|
|
170
|
+
* id landed first, a refresh cleared it, a commit folded in ({@link transformCache}) — this answer
|
|
171
|
+
* was asked for in a world that has since changed, and keeping it blindly is how old content gets
|
|
172
|
+
* remembered for good: nothing clears a block but a log entry naming it, and that entry may be
|
|
173
|
+
* the very thing that moved the generation. So it is then kept only to REPLACE strictly older
|
|
174
|
+
* content (every answer to one source is for one view, so of two the higher revision is the
|
|
175
|
+
* truer, whichever arrives last); what an overtaken answer's reader gets is {@link admit}'s call.
|
|
176
|
+
* Only a CACHED id is compared: `revisions` outlives an LRU-evicted block (see its NOTE). */
|
|
177
|
+
private stillWanted(id: BlockId, rev: number, generationAtMiss: number): boolean {
|
|
178
|
+
if (this.getGeneration(id) === generationAtMiss) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const held = this.cache.has(id) ? this.revisions.get(id) : undefined;
|
|
182
|
+
const replacesOlder = held !== undefined && held < rev;
|
|
183
|
+
if (!replacesOlder) {
|
|
184
|
+
log('miss:overtaken id=%s rev=%d heldRev=%s', id, rev, held ?? 'none');
|
|
185
|
+
}
|
|
186
|
+
return replacesOlder;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A keepable answer: cached, and served to every later read of `id` until something clears it. */
|
|
190
|
+
private keep(id: BlockId, block: T, rev: number) {
|
|
191
|
+
this.cache.set(id, block);
|
|
192
|
+
this.revisions.set(id, rev);
|
|
193
|
+
this.unkept.delete(id);
|
|
194
|
+
this.bump(id);
|
|
195
|
+
log('miss:loaded id=%s cacheSize=%d', id, this.cache.size);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** An answer this cache will not keep, over an id it holds nothing for: returned to this one
|
|
199
|
+
* reader and described to the base probes (see {@link unkept}), but the next read of `id` asks
|
|
200
|
+
* the source again. Only {@link admit} calls this, after establishing that nothing is held —
|
|
201
|
+
* a cached id is never also in {@link unkept}.
|
|
202
|
+
*
|
|
203
|
+
* Bumped like a load: the answer changes what this cache can say about the id, and the bump is
|
|
204
|
+
* what sends a base pin or a materialized memo built on the previous answer back to be
|
|
205
|
+
* re-judged. */
|
|
206
|
+
private handThrough(id: BlockId, block: T, rev: number) {
|
|
207
|
+
this.revisions.delete(id);
|
|
208
|
+
this.unkept.set(id, { block, rev });
|
|
209
|
+
this.bump(id);
|
|
210
|
+
log('miss:unkept id=%s rev=%d', id, rev);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The base this cache can describe for `id`, without consulting the source: the cached block,
|
|
214
|
+
* or the content last handed through unkept (see {@link unkept}) — either way the content the
|
|
215
|
+
* most recent {@link tryGet} of the id returned, unless it has been evicted since. Cloned
|
|
216
|
+
* (callers apply ops to it) and recency-neutral ({@link LruMap.peek}) — an observation pass must
|
|
217
|
+
* neither pay a network read nor reshape eviction order. Records no read dependency: the caller
|
|
218
|
+
* that peeks already read the block through {@link tryGet} (that is how this cache learned it),
|
|
219
|
+
* so the dependency exists; a digest pass merely re-describes it. */
|
|
220
|
+
peek(id: BlockId): T | undefined {
|
|
221
|
+
const block = this.cache.peek(id) ?? this.unkept.get(id)?.block;
|
|
222
|
+
return block === undefined ? undefined : structuredClone(block);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Whether a later {@link tryGet} of `id` is answered from memory with the content this cache
|
|
226
|
+
* holds now. `false` for content handed through unkept, which {@link peek} still describes — so
|
|
227
|
+
* a consumer that would FREEZE a read result ({@link Tracker}'s materialized memo) asks this, not
|
|
228
|
+
* {@link peek}. Recency-neutral. */
|
|
229
|
+
retains(id: BlockId): boolean {
|
|
230
|
+
return this.cache.has(id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The committed revision of the content this cache last served for `id` — the source-reported
|
|
234
|
+
* materialized revision learned on miss-load (see {@link revisions}) or handed through
|
|
235
|
+
* ({@link unkept}), NOT the block's own `state.latest.rev`. Answers for an LRU-evicted id too
|
|
236
|
+
* (see the NOTE on {@link revisions}): that is the revision a write staged over the evicted
|
|
237
|
+
* read was computed against, and what a rev-only base pin carries. {@link peek} returns
|
|
238
|
+
* `undefined` for the evicted id, so the revision never pairs with content it does not describe.
|
|
239
|
+
* `undefined` once the id has been cleared or folded away, or if it was never served. */
|
|
240
|
+
getCachedRevision(id: BlockId): number | undefined {
|
|
241
|
+
return this.revisions.get(id) ?? this.unkept.get(id)?.rev;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Upgrade an already-captured read of `id` to a `value` read in the shared collector,
|
|
245
|
+
* retaining it in the conflict set. The B-tree point-lookup descent calls this (through the
|
|
246
|
+
* Tracker, which forwards) to pin the terminal leaf after recording the interior nodes as
|
|
247
|
+
* `navigation`. No-op when no collector is wired (log-walk caches) or the id was never
|
|
248
|
+
* recorded. Duck-typed by the Tracker; keep the name in sync with Tracker.markReadValue. */
|
|
249
|
+
markReadValue(id: BlockId): void {
|
|
250
|
+
this.collector?.markValue(id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
generateId(): BlockId {
|
|
254
|
+
return this.source.generateId();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader {
|
|
258
|
+
return this.source.createBlockHeader(type, newId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clear(blockIds: BlockId[] | undefined = undefined) {
|
|
262
|
+
if (blockIds) {
|
|
263
|
+
for (const id of blockIds) {
|
|
264
|
+
this.cache.delete(id);
|
|
265
|
+
this.revisions.delete(id);
|
|
266
|
+
this.unkept.delete(id);
|
|
267
|
+
this.bump(id);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
for (const [id] of this.cache) {
|
|
271
|
+
this.bump(id);
|
|
272
|
+
}
|
|
273
|
+
for (const id of this.unkept.keys()) {
|
|
274
|
+
this.bump(id);
|
|
275
|
+
}
|
|
276
|
+
this.cache.clear();
|
|
277
|
+
this.revisions.clear();
|
|
278
|
+
this.unkept.clear();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A cloned copy of the current cache contents with each id's committed revision, in LRU
|
|
283
|
+
* order (oldest first, so replaying into another LruMap preserves eviction order). For
|
|
284
|
+
* building a pinned read view ONLY (see {@link Collection.createReadTracker}): pass the
|
|
285
|
+
* result as the `seed` of a fresh, PRIVATE CacheSource. Blocks are cloned on the way out,
|
|
286
|
+
* so the seeded cache shares no mutable state with this one. */
|
|
287
|
+
snapshotEntries(): Array<[BlockId, T, number]> {
|
|
288
|
+
const entries: Array<[BlockId, T, number]> = [];
|
|
289
|
+
for (const [id, block] of this.cache) {
|
|
290
|
+
entries.push([id, structuredClone(block), this.revisions.get(id) ?? 0]);
|
|
291
|
+
}
|
|
292
|
+
return entries;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Mutates the cache without affecting the source. `revision` is the committed revision this
|
|
296
|
+
* transform lands at; the stored per-id revision advances to it so a later read records a
|
|
297
|
+
* dependency at the NEW revision (recording the old one would spuriously fail validation). */
|
|
298
|
+
transformCache(transform: Transforms, revision: number) {
|
|
299
|
+
for (const blockId of transform.deletes ?? []) {
|
|
300
|
+
this.cache.delete(blockId);
|
|
301
|
+
this.revisions.delete(blockId);
|
|
302
|
+
this.unkept.delete(blockId);
|
|
303
|
+
this.bump(blockId);
|
|
304
|
+
}
|
|
305
|
+
for (const [, block] of Object.entries(transform.inserts ?? {})) {
|
|
306
|
+
this.cache.set(block.header.id, structuredClone(block) as T);
|
|
307
|
+
this.revisions.set(block.header.id, revision);
|
|
308
|
+
this.unkept.delete(block.header.id);
|
|
309
|
+
this.bump(block.header.id);
|
|
310
|
+
}
|
|
311
|
+
for (const [blockId, operations] of Object.entries(transform.updates ?? {})) {
|
|
312
|
+
const block = this.cache.get(blockId);
|
|
313
|
+
if (block) {
|
|
314
|
+
for (const op of operations) {
|
|
315
|
+
applyOperation(block, op);
|
|
316
|
+
this.bump(blockId);
|
|
317
|
+
}
|
|
318
|
+
this.revisions.set(blockId, revision);
|
|
319
|
+
} else if (this.unkept.delete(blockId)) {
|
|
320
|
+
// The commit superseded the unkept base, and the ops are NOT folded into it: it was
|
|
321
|
+
// never trusted as this id's content, so the result would not be either. The id stays
|
|
322
|
+
// out of memory and the next read asks the source, which now holds `revision`.
|
|
323
|
+
this.bump(blockId);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
package/src/transform/digest.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlockId, IBlock } from "../index.js";
|
|
2
|
-
import type { BlockContentDigests } from "../network/struct.js";
|
|
2
|
+
import type { BlockBaseRevs, BlockContentDigests } from "../network/struct.js";
|
|
3
3
|
import { canonicalBlockHash } from "../blocks/helpers.js";
|
|
4
4
|
import { createLogger } from "../logger.js";
|
|
5
5
|
import { isRecordEmpty } from "../utility/is-record-empty.js";
|
|
@@ -16,15 +16,18 @@ const log = createLogger('digest');
|
|
|
16
16
|
* block will contain at the committing revision; `baseRev` rides along except for base-independent
|
|
17
17
|
* (inserted) blocks. */
|
|
18
18
|
// NOTE: declarability follows what the transaction read and staged, not read-cache residency. Each
|
|
19
|
-
// updated block's committed base is pinned at the moment its update is staged (Tracker.update
|
|
20
|
-
// BasePins) and held until the transaction boundary, so a commit of any size
|
|
21
|
-
// blocks whose bases it read — verified through the production path
|
|
22
|
-
// `test/digest-cache-coverage.spec.ts` at 2x and 4x the cache capacity.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// read to
|
|
26
|
-
//
|
|
27
|
-
//
|
|
19
|
+
// updated block's committed base is pinned at the moment its FIRST update is staged (Tracker.update
|
|
20
|
+
// -> BasePins), fixed there, and held until the transaction boundary, so a commit of any size
|
|
21
|
+
// declares 100% of the blocks whose bases it read — verified through the production path
|
|
22
|
+
// (`Collection.act`/`sync`) in `test/digest-cache-coverage.spec.ts` at 2x and 4x the cache capacity.
|
|
23
|
+
// `baseRev` is always the pin's revision: the one the operations were computed against, never the
|
|
24
|
+
// live cache's. The legitimate omissions: a delete (materializes to nothing); a blind update to a
|
|
25
|
+
// block this node never read (nothing to declare, and a commit must never pay a network read to
|
|
26
|
+
// describe itself); read-far-then-update — a block read, then evicted by 128+ other reads, and only
|
|
27
|
+
// then updated — which pins the revision but not the content, so the base is still named on the
|
|
28
|
+
// pend (`Tracker.stagedBaseRevs`) and only its digest is undeclared; and a base that MOVED under its
|
|
29
|
+
// staged operations, which is never declared at any revision (`Tracker.revalidatePin`) and is
|
|
30
|
+
// re-staged before it is pended (`Collection.restageIfBasesMoved`).
|
|
28
31
|
export async function computeBlockContentDigests<T extends IBlock>(
|
|
29
32
|
tracker: Tracker<T>,
|
|
30
33
|
blockIds: BlockId[]
|
|
@@ -49,6 +52,14 @@ export function blockDigestsField(digests: BlockContentDigests | undefined): { b
|
|
|
49
52
|
return digests && !isRecordEmpty(digests) ? { blockDigests: digests } : {};
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
/** The pend-side sibling of {@link blockDigestsField}: wraps `PendRequest.baseRevs` so it spreads
|
|
56
|
+
* onto a request only when some block names a base. Same reason — the pend is hashed verbatim into
|
|
57
|
+
* every cohort signature preimage, so a pend that names no base must serialize exactly as it did
|
|
58
|
+
* before the field existed. Every producer of the field goes through here. */
|
|
59
|
+
export function baseRevsField(baseRevs: BlockBaseRevs | undefined): { baseRevs?: BlockBaseRevs } {
|
|
60
|
+
return baseRevs && !isRecordEmpty(baseRevs) ? { baseRevs } : {};
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
/** {@link Tracker.peekMaterialized}, degraded to "undeclared" when materializing throws.
|
|
53
64
|
*
|
|
54
65
|
* Declaring content must never break committing it. Materializing replays the staged ops against the
|
package/src/transform/helpers.ts
CHANGED
|
@@ -99,13 +99,18 @@ export function concatTransforms(...transforms: Transforms[]): Transforms {
|
|
|
99
99
|
/**
|
|
100
100
|
* Extracts the transform for a specific block from a Transforms object.
|
|
101
101
|
*
|
|
102
|
-
* @pitfall
|
|
103
|
-
* the array reference, causing mutations
|
|
102
|
+
* @pitfall Both `insert` and `updates` MUST be deep cloned - extracting without cloning shares
|
|
103
|
+
* the object/array reference, causing mutations (e.g. `applyTransform` mutating `insert` in place
|
|
104
|
+
* when `updates` ride along) to affect the original Transforms.
|
|
104
105
|
* @see docs/internals.md "Shallow Copy of Transforms" pitfall
|
|
106
|
+
*
|
|
107
|
+
* NOTE: clones unconditionally, even for read-only callers (e.g. StorageRepo.pend's classify pass);
|
|
108
|
+
* if large inserts ever show up in a pend/commit profile, add a non-cloning variant for those callers.
|
|
105
109
|
*/
|
|
106
110
|
export function transformForBlockId(transform: Transforms, blockId: BlockId): Transform {
|
|
107
111
|
return {
|
|
108
|
-
|
|
112
|
+
// Clone insert to prevent applyTransform's in-place mutation from reaching the original
|
|
113
|
+
...(transform.inserts && blockId in transform.inserts ? { insert: structuredClone(transform.inserts[blockId]) } : {}),
|
|
109
114
|
// Clone updates array to prevent shared references
|
|
110
115
|
...(transform.updates && blockId in transform.updates ? { updates: structuredClone(transform.updates[blockId]) } : {}),
|
|
111
116
|
...(transform.deletes?.includes(blockId) ? { delete: true } : {})
|