@optimystic/db-core 1.0.0-beta.3 → 1.0.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
package/src/transform/tracker.ts
CHANGED
|
@@ -1,270 +1,423 @@
|
|
|
1
|
-
import type { IBlock, BlockId, BlockStore as IBlockStore, BlockHeader, BlockOperation, BlockType, BlockSource as IBlockSource, ReadPurpose } from "../index.js";
|
|
2
|
-
import { applyOperation, applyOperations, applyTransform, emptyTransforms, blockIdsForTransforms, transformForBlockId } from "./helpers.js";
|
|
3
|
-
import { BasePins } from "./base-pins.js";
|
|
4
|
-
import type { PinnedBase } from "./base-pins.js";
|
|
5
|
-
import { ensured } from "../utility/ensured.js";
|
|
6
|
-
|
|
7
|
-
/** The base a source
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
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
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
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
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
const
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1
|
+
import type { IBlock, BlockId, BlockStore as IBlockStore, BlockHeader, BlockOperation, BlockType, BlockSource as IBlockSource, ReadPurpose } from "../index.js";
|
|
2
|
+
import { applyOperation, applyOperations, applyTransform, emptyTransforms, blockIdsForTransforms, transformForBlockId } from "./helpers.js";
|
|
3
|
+
import { BasePins } from "./base-pins.js";
|
|
4
|
+
import type { PinnedBase } from "./base-pins.js";
|
|
5
|
+
import { ensured } from "../utility/ensured.js";
|
|
6
|
+
|
|
7
|
+
/** The two base probes a source may offer, duck-typed because Tracker layers over test doubles as
|
|
8
|
+
* well as CacheSource. `peek` must return a clone (CacheSource's does). */
|
|
9
|
+
type BaseProbes = {
|
|
10
|
+
peek?: (id: BlockId) => IBlock | undefined;
|
|
11
|
+
getCachedRevision?: (id: BlockId) => number | undefined;
|
|
12
|
+
getGeneration?: (id: BlockId) => number;
|
|
13
|
+
retains?: (id: BlockId) => boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The base a source can answer from memory, with the committed revision of that content — the
|
|
17
|
+
* shared probe behind the unpinned {@link Tracker.peekMaterialized} path, for a source that cannot
|
|
18
|
+
* report drift and so takes no pins. Returns undefined unless BOTH probes answer. */
|
|
19
|
+
function cachedBase(source: unknown, id: BlockId): { block: IBlock; rev: number } | undefined {
|
|
20
|
+
const src = source as BaseProbes;
|
|
21
|
+
if (typeof src.peek !== 'function' || typeof src.getCachedRevision !== 'function') return undefined;
|
|
22
|
+
const block = src.peek(id); // already a clone (peek contract)
|
|
23
|
+
const rev = src.getCachedRevision(id);
|
|
24
|
+
return block === undefined || rev === undefined ? undefined : { block, rev };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A block store that collects transformations, without applying them to the underlying source.
|
|
28
|
+
* Transformations are also applied to the retrieved blocks, making it seem like the source has been modified.
|
|
29
|
+
*/
|
|
30
|
+
export class Tracker<T extends IBlock> implements IBlockStore<T> {
|
|
31
|
+
/** Per-id memo of the materialized (source block + all `updates[id]` ops) result, so a
|
|
32
|
+
* repeated read of a hot op-carrying block is O(block size) instead of O(block size + ops).
|
|
33
|
+
* Kept fresh incrementally on {@link update}, dropped on {@link insert}/{@link delete}/{@link reset},
|
|
34
|
+
* and invalidated when the source's generation for the id advances (external cache mutation).
|
|
35
|
+
* Only populated for sources that expose `getGeneration` — without a drift signal we cannot
|
|
36
|
+
* detect source changes, so those fall back to always-replay. `gen` is the source generation of
|
|
37
|
+
* the base block content the memo was built from. */
|
|
38
|
+
private materialized = new Map<BlockId, { block: T; gen: number }>();
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private readonly source: IBlockSource<T>,
|
|
42
|
+
/** The collected set of transformations to be applied. Treat as immutable */
|
|
43
|
+
public transforms = emptyTransforms(),
|
|
44
|
+
/** Committed bases pinned at the moment each update was staged, so the digest pass can
|
|
45
|
+
* describe every updated block even after the read cache evicts its base, and so the pend
|
|
46
|
+
* can name the revision each block's operations were computed against. Shared by
|
|
47
|
+
* reference across the trackers of one transaction (see {@link BasePins}); pass an
|
|
48
|
+
* existing store to join a transaction, omit for a private one. */
|
|
49
|
+
public readonly pins: BasePins = new BasePins(),
|
|
50
|
+
) {
|
|
51
|
+
// A pin's `gen` and `rev` are counters PRIVATE to one base source, so a store may only be
|
|
52
|
+
// shared between trackers that bottom out at the same one. Binding here turns the otherwise
|
|
53
|
+
// silent failure (a pin from cache A passing the freshness check against cache B, which
|
|
54
|
+
// numbers generations from 0 independently, and declaring A's content for B's block) into a
|
|
55
|
+
// throw at the moment the stores are joined.
|
|
56
|
+
pins.bindAuthority(this.baseSource());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The non-Tracker source at the bottom of the tracker stack. Single authority for everything
|
|
60
|
+
* pin-related: an Atomic layers over a Collection's tracker, which layers over the read cache,
|
|
61
|
+
* and only that cache can report a base, its committed revision, and its drift generation. */
|
|
62
|
+
private baseSource(): BaseProbes {
|
|
63
|
+
let src: unknown = this.source;
|
|
64
|
+
while (src instanceof Tracker) src = src.source;
|
|
65
|
+
return src as BaseProbes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The source's generation for an id, or undefined if the source cannot report drift. */
|
|
69
|
+
private sourceGeneration(id: BlockId): number | undefined {
|
|
70
|
+
const src = this.source as { getGeneration?: (id: BlockId) => number };
|
|
71
|
+
return typeof src.getGeneration === 'function' ? src.getGeneration(id) : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether the source will answer the next read of `id` from memory with the base it just served.
|
|
75
|
+
* A source that cannot say is taken to — the behaviour before the probe, and right for every
|
|
76
|
+
* source but a CacheSource handing an answer through unkept.
|
|
77
|
+
*
|
|
78
|
+
* The generation cannot carry this. A memo is stamped with the generation read AFTER the load
|
|
79
|
+
* (see {@link tryGet}), so however the source bumps while handing an unkept answer through, the
|
|
80
|
+
* stamp matches on the next read and the memo is served — freezing, under this tracker's staged
|
|
81
|
+
* ops, exactly the base the source declined to freeze. */
|
|
82
|
+
private sourceRetains(id: BlockId): boolean {
|
|
83
|
+
const src = this.source as { retains?: (id: BlockId) => boolean };
|
|
84
|
+
return typeof src.retains !== 'function' || src.retains(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The drift generation from the same authority {@link probeBase} pins from — the base source,
|
|
88
|
+
* so an Atomic validates its pins against the collection's read cache rather than against the
|
|
89
|
+
* drift-blind tracker in between. */
|
|
90
|
+
protected baseGeneration(id: BlockId): number | undefined {
|
|
91
|
+
const src = this.baseSource();
|
|
92
|
+
return typeof src.getGeneration === 'function' ? src.getGeneration(id) : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The base for `id` as the base source describes it now: its committed revision, its content
|
|
96
|
+
* when the source still has it, and the source's drift generation. Taken from
|
|
97
|
+
* {@link baseSource}, so an Atomic staged over a Collection's tracker pins from the collection's
|
|
98
|
+
* read cache instead of finding nothing. Returns undefined unless the source reports drift and a
|
|
99
|
+
* revision — which keeps drift-blind sources (test doubles) on exactly the pre-pin behaviour,
|
|
100
|
+
* and leaves a blind update (a block never read, or read and since cleared) unpinned. A
|
|
101
|
+
* revision without content is a REV-ONLY pin: the block was read and then evicted, so the base
|
|
102
|
+
* can be named but not materialized (see {@link PinnedBase.block}). Recency-neutral. */
|
|
103
|
+
protected probeBase(id: BlockId): PinnedBase | undefined {
|
|
104
|
+
const src = this.baseSource();
|
|
105
|
+
// Without a drift signal a pin could never be re-judged, so never take one.
|
|
106
|
+
if (typeof src.getGeneration !== 'function' || typeof src.getCachedRevision !== 'function') return undefined;
|
|
107
|
+
const rev = src.getCachedRevision(id);
|
|
108
|
+
if (rev === undefined) return undefined;
|
|
109
|
+
const block = typeof src.peek === 'function' ? src.peek(id) : undefined;
|
|
110
|
+
return { rev, gen: src.getGeneration(id), ...(block === undefined ? {} : { block }) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Re-judge `pin` against what the base source describes for `id` NOW, if the source's
|
|
114
|
+
* generation for the id has advanced since the pin was last judged. The source's content for
|
|
115
|
+
* the id changed hands; whether the BASE moved is a question of revision, not generation:
|
|
116
|
+
*
|
|
117
|
+
* - the same revision (a re-load of an evicted id, a refresh that re-read identical content, a
|
|
118
|
+
* below-floor answer served again) is the same committed content, so the pin is refreshed in
|
|
119
|
+
* place — the clone filled or replaced, the generation restamped — and stays declarable;
|
|
120
|
+
* - a different revision, or none (the id cleared, folded away, or handed through at another
|
|
121
|
+
* revision), means the staged operations were computed on content the source no longer
|
|
122
|
+
* describes. The pin is marked {@link PinnedBase.moved} and is never repaired here: re-pinning
|
|
123
|
+
* would put the new revision on operations built for the old one, which is precisely the wrong
|
|
124
|
+
* base the storage guard cannot catch. Only a re-stage (which resets this tracker) recovers.
|
|
125
|
+
*
|
|
126
|
+
* Cheap — one generation compare on the common path, two probes on drift — so every consumer of
|
|
127
|
+
* a pin runs it first. */
|
|
128
|
+
private revalidatePin(id: BlockId, pin: PinnedBase): PinnedBase {
|
|
129
|
+
const gen = this.baseGeneration(id);
|
|
130
|
+
if (pin.moved || gen === undefined || pin.gen === gen) return pin;
|
|
131
|
+
const src = this.baseSource();
|
|
132
|
+
const rev = typeof src.getCachedRevision === 'function' ? src.getCachedRevision(id) : undefined;
|
|
133
|
+
if (rev !== pin.rev) {
|
|
134
|
+
this.pins.markMoved(id);
|
|
135
|
+
return pin;
|
|
136
|
+
}
|
|
137
|
+
const block = typeof src.peek === 'function' ? src.peek(id) : undefined;
|
|
138
|
+
const refreshed: PinnedBase = { rev, gen, ...(block ?? pin.block ? { block: block ?? pin.block } : {}) };
|
|
139
|
+
this.pins.set(id, refreshed);
|
|
140
|
+
return refreshed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<T | undefined> {
|
|
144
|
+
// NOTE: precedence here is insert > delete > source+updates. In a well-formed transform an id is
|
|
145
|
+
// never in both `inserts` and `deletes` (insert/delete each clear the other), so order is moot. It
|
|
146
|
+
// only diverges from the canonical `applyTransform` (delete-last-wins, see struct.ts / helpers.ts:132)
|
|
147
|
+
// in the malformed insert+delete state reachable via the phantom-delete bug (double-delete then
|
|
148
|
+
// reinsert). Likewise the insert path intentionally skips `updates[id]` — inserted blocks bake ops
|
|
149
|
+
// in-place via update(); a stale pre-insert `updates[id]` is discarded here but would be re-applied
|
|
150
|
+
// on commit. Both are read-vs-commit inconsistencies confined to malformed states; fix the source
|
|
151
|
+
// bug (phantom delete / stale updates) rather than papering over it here.
|
|
152
|
+
if (this.transforms.inserts && Object.hasOwn(this.transforms.inserts, id)) {
|
|
153
|
+
return structuredClone(this.transforms.inserts[id]) as T;
|
|
154
|
+
}
|
|
155
|
+
if (this.transforms.deletes?.includes(id)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const gen = this.sourceGeneration(id);
|
|
159
|
+
const memo = this.materialized.get(id);
|
|
160
|
+
if (memo && (gen === undefined || memo.gen === gen)) {
|
|
161
|
+
return structuredClone(memo.block); // O(block size), no replay
|
|
162
|
+
}
|
|
163
|
+
const block = await this.source.tryGet(id, purpose);
|
|
164
|
+
if (block) {
|
|
165
|
+
const ops = this.transforms.updates?.[id] ?? [];
|
|
166
|
+
if (ops.length > 0) {
|
|
167
|
+
// A read of a block whose base has MOVED under its staged ops is served as the live
|
|
168
|
+
// content plus those ops all the same — never refused. The pend is what pays for a
|
|
169
|
+
// moved base (Collection.restageIfBasesMoved), with one replay.
|
|
170
|
+
applyOperations(block, ops);
|
|
171
|
+
// Memoize only when the source can report drift, and stamp with the generation read
|
|
172
|
+
// AFTER the load — the source may bump during tryGet (a cache miss-load), and stamping
|
|
173
|
+
// with the pre-load generation would force a needless reload on the very next read.
|
|
174
|
+
// And only over a base the source kept: one it will re-ask for must be re-asked for
|
|
175
|
+
// here too, or this memo outlives the source's own refusal to remember it.
|
|
176
|
+
const freshGen = this.sourceGeneration(id);
|
|
177
|
+
if (freshGen !== undefined && this.sourceRetains(id)) {
|
|
178
|
+
this.materialized.set(id, { block, gen: freshGen });
|
|
179
|
+
} else {
|
|
180
|
+
this.materialized.delete(id);
|
|
181
|
+
}
|
|
182
|
+
return structuredClone(block); // clone so callers can't mutate the memo
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return block; // no-ops path unchanged (source already cloned)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The block `id` materializes to under the staged transforms, computed WITHOUT loading from the
|
|
189
|
+
* source, plus the committed revision of the base used. `undefined` when not computable here —
|
|
190
|
+
* nothing staged for the id, the result is a delete, or an update's base cannot be materialized:
|
|
191
|
+
* unpinned over a drift-blind source and not locally cached (a commit must not pay a network
|
|
192
|
+
* round trip to describe itself), pinned rev-only (the content was evicted before the update was
|
|
193
|
+
* staged — the base is still named by {@link stagedBaseRevs}, only its digest is undeclared), or
|
|
194
|
+
* pinned but MOVED (the operations no longer describe any content; see {@link revalidatePin}).
|
|
195
|
+
*
|
|
196
|
+
* Materializes with the canonical {@link applyTransform} — the exact function the member side
|
|
197
|
+
* uses at commit — so client and member can never disagree on semantics (insert replaces the
|
|
198
|
+
* block, then updates apply, then delete wins). An insert makes the result base-independent, so
|
|
199
|
+
* `baseRev` is absent; updates-only returns the pinned base's revision, which is the revision the
|
|
200
|
+
* staged operations were computed against — never the live cache's, which may have moved on.
|
|
201
|
+
* Memo-neutral and recency-neutral: reads observe no change. The one thing it may record is the
|
|
202
|
+
* discovery that a pinned base has moved. */
|
|
203
|
+
peekMaterialized(id: BlockId): { block: IBlock; baseRev?: number } | undefined {
|
|
204
|
+
const transform = transformForBlockId(this.transforms, id);
|
|
205
|
+
if (transform.insert === undefined && transform.updates === undefined && transform.delete === undefined) {
|
|
206
|
+
return undefined; // nothing staged for this id
|
|
207
|
+
}
|
|
208
|
+
if (transform.delete) {
|
|
209
|
+
return undefined; // delete-last-wins: materializes to nothing
|
|
210
|
+
}
|
|
211
|
+
if (transform.insert) {
|
|
212
|
+
// No clone needed: transformForBlockId already deep-cloned `insert`, which applyTransform mutates.
|
|
213
|
+
const block = applyTransform(undefined, transform);
|
|
214
|
+
return block ? { block } : undefined;
|
|
215
|
+
}
|
|
216
|
+
const pin = this.pins.get(id);
|
|
217
|
+
if (pin) {
|
|
218
|
+
const current = this.revalidatePin(id, pin);
|
|
219
|
+
if (current.moved || current.block === undefined) return undefined;
|
|
220
|
+
// The clone on use is required, not defensive: applyTransform mutates, and syncAttempts
|
|
221
|
+
// re-runs the digest pass on every retry attempt against the same pin.
|
|
222
|
+
const block = applyTransform(structuredClone(current.block), transform);
|
|
223
|
+
return block ? { block, baseRev: current.rev } : undefined;
|
|
224
|
+
}
|
|
225
|
+
// Unpinned. Over a drift-aware base source that is a blind update (the block was never read,
|
|
226
|
+
// or was cleared before the update was staged): nothing is known about what the operations
|
|
227
|
+
// were computed against, so nothing is declared — the live cache may hold the block by now,
|
|
228
|
+
// but declaring ITS revision would name a base the operations were not built on. Only a
|
|
229
|
+
// drift-blind source, which takes no pins, keeps its pre-pin behaviour: the IMMEDIATE source's
|
|
230
|
+
// live peek, not {@link baseSource}, exactly as before pins existed.
|
|
231
|
+
if (typeof this.baseSource().getGeneration === 'function') return undefined;
|
|
232
|
+
const base = cachedBase(this.source, id);
|
|
233
|
+
if (!base) return undefined;
|
|
234
|
+
const block = applyTransform(base.block, transform); // base already a clone (peek contract)
|
|
235
|
+
return block ? { block, baseRev: base.rev } : undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Per block in `blockIds`, the committed revision its staged UPDATE operations were computed
|
|
239
|
+
* against — the pinned base, fixed when the first of them was staged and unchanged since, even
|
|
240
|
+
* if the base has moved (a moved base is still the truth about the operations; the pend
|
|
241
|
+
* carrying it is refused, which is the point). Only update-only blocks are named: an inserted
|
|
242
|
+
* block is base-independent, a deleted one materializes to nothing, and a block updated
|
|
243
|
+
* without a pin (a blind update, or a drift-blind source) has no base to name. */
|
|
244
|
+
stagedBaseRevs(blockIds: readonly BlockId[]): Record<BlockId, number> {
|
|
245
|
+
const revs: Record<BlockId, number> = {};
|
|
246
|
+
for (const id of blockIds) {
|
|
247
|
+
if (!this.isUpdateOnly(id)) continue;
|
|
248
|
+
const pin = this.pins.get(id);
|
|
249
|
+
if (pin) revs[id] = pin.rev;
|
|
250
|
+
}
|
|
251
|
+
return revs;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The staged update-only blocks whose pinned base has MOVED — the base source no longer
|
|
255
|
+
* describes the id at the revision the operations were computed against — after re-judging
|
|
256
|
+
* every such pin against the source (one generation compare each; see {@link revalidatePin}).
|
|
257
|
+
* Restricted to ids THIS tracker stages as updates: the shared store can also hold pins an
|
|
258
|
+
* abandoned per-attempt tracker took for its log blocks, which describe operations this tracker
|
|
259
|
+
* does not carry. A non-empty answer means the pending actions must be re-staged before they
|
|
260
|
+
* are pended (Collection.restageIfBasesMoved). */
|
|
261
|
+
movedBases(): BlockId[] {
|
|
262
|
+
const moved: BlockId[] = [];
|
|
263
|
+
for (const id of Object.keys(this.transforms.updates ?? {}) as BlockId[]) {
|
|
264
|
+
const pin = this.pins.get(id);
|
|
265
|
+
if (pin && this.isUpdateOnly(id) && this.revalidatePin(id, pin).moved) moved.push(id);
|
|
266
|
+
}
|
|
267
|
+
return moved;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The staged update-only blocks whose pinned base the base source describes but does not RETAIN
|
|
271
|
+
* — content handed through unkept (a below-floor answer), which the source re-asks for on every
|
|
272
|
+
* read. The pin names the revision last served; storage may have caught up since without any
|
|
273
|
+
* log movement to say so, and only a read can tell. The candidates for a pre-pend re-read. */
|
|
274
|
+
unretainedBases(): BlockId[] {
|
|
275
|
+
const src = this.baseSource();
|
|
276
|
+
if (typeof src.retains !== 'function' || typeof src.peek !== 'function') return [];
|
|
277
|
+
const ids: BlockId[] = [];
|
|
278
|
+
for (const id of Object.keys(this.transforms.updates ?? {}) as BlockId[]) {
|
|
279
|
+
if (this.isUpdateOnly(id) && this.pins.get(id) && !src.retains(id) && src.peek(id) !== undefined) ids.push(id);
|
|
280
|
+
}
|
|
281
|
+
return ids;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Whether `id` is staged as updates alone — not inserted (base-independent) and not deleted
|
|
285
|
+
* (materializes to nothing), the two shapes for which no base is ever named. */
|
|
286
|
+
private isUpdateOnly(id: BlockId): boolean {
|
|
287
|
+
return (this.transforms.updates?.[id]?.length ?? 0) > 0
|
|
288
|
+
&& !(this.transforms.inserts && Object.hasOwn(this.transforms.inserts, id))
|
|
289
|
+
&& !this.transforms.deletes?.includes(id);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Forward a leaf-value upgrade down to the source's read collector (duck-typed: only the
|
|
293
|
+
* CacheSource layer implements it). Lets the B-tree point-lookup descent, which reads through
|
|
294
|
+
* this tracker, pin its terminal leaf as a `value` read after tagging interior nodes
|
|
295
|
+
* `navigation`. No-op for sources without a collector (test doubles, log-walk caches). */
|
|
296
|
+
markReadValue(id: BlockId): void {
|
|
297
|
+
(this.source as { markReadValue?: (id: BlockId) => void }).markReadValue?.(id);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
generateId(): BlockId {
|
|
301
|
+
return this.source.generateId();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader {
|
|
305
|
+
return this.source.createBlockHeader(type, newId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
insert(block: T) {
|
|
309
|
+
const inserts = this.transforms.inserts ??= {};
|
|
310
|
+
inserts[block.header.id] = structuredClone(block);
|
|
311
|
+
// Served from `inserts` now, not source+updates — the materialized memo no longer applies.
|
|
312
|
+
this.materialized.delete(block.header.id);
|
|
313
|
+
// An insert makes the materialized result base-independent, so any pinned base is moot.
|
|
314
|
+
this.pins.delete(block.header.id);
|
|
315
|
+
const deletes = this.transforms.deletes;
|
|
316
|
+
const deleteIndex = deletes?.indexOf(block.header.id) ?? -1;
|
|
317
|
+
if (deleteIndex >= 0) {
|
|
318
|
+
deletes!.splice(deleteIndex, 1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
update(blockId: BlockId, op: BlockOperation) {
|
|
323
|
+
if (this.stageUpdate(blockId, op)) this.pinBase(blockId);
|
|
324
|
+
else this.recheckPin(blockId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Fold a child tracker's staged transform into this one — the flush behind {@link Atomic.commit}
|
|
328
|
+
* — and empty the child. The child's pins are adopted first ({@link BasePins.adopt}), and an
|
|
329
|
+
* update whose id the child pinned keeps that pin as the base of its first operation here rather
|
|
330
|
+
* than probing the base source: the child's pin IS the base those operations were computed
|
|
331
|
+
* against, while the source may have moved on since (a concurrent unlatched read that reloaded
|
|
332
|
+
* the block at a newer revision), and probing would put the newer revision on operations built
|
|
333
|
+
* for the older one — the wrong base the storage-side guard cannot catch. An adopted pin the
|
|
334
|
+
* source no longer describes at its revision is judged moved on its next use, like any pin. An
|
|
335
|
+
* update the child did NOT pin (a blind one) is pinned here exactly as a direct update would be. */
|
|
336
|
+
absorb(child: Tracker<T>): void {
|
|
337
|
+
this.pins.adopt(child.pins);
|
|
338
|
+
const adopted = new Set(child.pins.ids());
|
|
339
|
+
const transform = child.reset();
|
|
340
|
+
for (const blockId of transform.deletes ?? []) this.delete(blockId);
|
|
341
|
+
for (const block of Object.values(transform.inserts ?? {})) this.insert(block as T);
|
|
342
|
+
for (const [blockId, ops] of Object.entries(transform.updates ?? {}) as [BlockId, BlockOperation[]][]) {
|
|
343
|
+
for (const op of ops) {
|
|
344
|
+
if (this.stageUpdate(blockId, op) && !adopted.has(blockId)) this.pinBase(blockId);
|
|
345
|
+
else this.recheckPin(blockId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Stage `op` for `blockId` — folded into a staged insert if there is one, else appended to the
|
|
351
|
+
* id's update list — and keep the materialized memo current. Returns whether this was the FIRST
|
|
352
|
+
* update staged for the id here: the moment its base is fixed (see {@link pinBase}). */
|
|
353
|
+
private stageUpdate(blockId: BlockId, op: BlockOperation): boolean {
|
|
354
|
+
const inserted = this.transforms.inserts?.[blockId];
|
|
355
|
+
if (inserted) {
|
|
356
|
+
applyOperation(inserted, op);
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const updates = this.transforms.updates ??= {};
|
|
360
|
+
const ops = ensured(updates, blockId, () => []);
|
|
361
|
+
const first = ops.length === 0;
|
|
362
|
+
ops.push(structuredClone(op));
|
|
363
|
+
// The memo already equals (base source content + prior ops); applying just the new op
|
|
364
|
+
// keeps it equal to the full ops list — O(1), no full replay. Leave `gen` untouched: it
|
|
365
|
+
// still records the base-content generation, so a later external source change still
|
|
366
|
+
// forces a reload. (Refreshing gen here would mask stale base content.)
|
|
367
|
+
const memo = this.materialized.get(blockId);
|
|
368
|
+
if (memo) {
|
|
369
|
+
applyOperation(memo.block, op);
|
|
370
|
+
}
|
|
371
|
+
return first;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Fix the base of `id`'s staged operations at the moment the FIRST of them is staged in this
|
|
375
|
+
* tracker: the caller just read the block, so the base source describes what the operation was
|
|
376
|
+
* computed against. Whatever the store held for the id before is replaced (or dropped, when the
|
|
377
|
+
* source can pin nothing): no operations of THIS tracker's list were computed on it — a pin that
|
|
378
|
+
* outlived its tracker, such as an abandoned per-attempt tracker's log-block pin from before a
|
|
379
|
+
* refresh cleared the block, would otherwise speak for operations built on something else.
|
|
380
|
+
* (The one pre-existing pin that IS this list's base, an atomic's, is kept by {@link absorb},
|
|
381
|
+
* which never comes through here for it.) A later operation for the same id never re-pins: it
|
|
382
|
+
* re-judges the pin ({@link recheckPin}), and a base found to have moved stays moved. */
|
|
383
|
+
private pinBase(id: BlockId): void {
|
|
384
|
+
const pin = this.probeBase(id);
|
|
385
|
+
if (pin) this.pins.set(id, pin);
|
|
386
|
+
else this.pins.delete(id);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Re-judge `id`'s pin, if it has one, against the base source (see {@link revalidatePin}). */
|
|
390
|
+
private recheckPin(id: BlockId): void {
|
|
391
|
+
const existing = this.pins.get(id);
|
|
392
|
+
if (existing) this.revalidatePin(id, existing);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
delete(blockId: BlockId) {
|
|
396
|
+
if (this.transforms.inserts) delete this.transforms.inserts[blockId];
|
|
397
|
+
if (this.transforms.updates) delete this.transforms.updates[blockId];
|
|
398
|
+
this.materialized.delete(blockId);
|
|
399
|
+
// A delete materializes to nothing (delete-last-wins), so the pinned base is moot.
|
|
400
|
+
this.pins.delete(blockId);
|
|
401
|
+
const deletes = this.transforms.deletes ??= [];
|
|
402
|
+
deletes.push(blockId);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
reset(newTransform = emptyTransforms()) {
|
|
406
|
+
const oldTransform = this.transforms;
|
|
407
|
+
this.transforms = newTransform;
|
|
408
|
+
this.materialized.clear();
|
|
409
|
+
// The single reclamation point for pins: a plain reset clears them (empty updates), a
|
|
410
|
+
// rollback-style reset(transforms) keeps exactly the pins — moved marks included — for ids
|
|
411
|
+
// still staged as updates.
|
|
412
|
+
this.pins.retainOnly(Object.keys(newTransform.updates ?? {}));
|
|
413
|
+
return oldTransform;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
transformedBlockIds(): BlockId[] {
|
|
417
|
+
return blockIdsForTransforms(this.transforms);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
conflicts(blockIds: Set<BlockId>) {
|
|
421
|
+
return this.transformedBlockIds().filter(id => blockIds.has(id));
|
|
422
|
+
}
|
|
423
|
+
}
|