@minnowdb/core 0.6.9 → 0.7.1
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/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +71 -41
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15952 -18465
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +9 -1
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2952 -3228
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -1,1764 +1,1516 @@
|
|
|
1
1
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
2
|
-
import { assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_LEASE_TTL_MS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_TRANSACTION_STAGE_SEGMENTS, MAX_TRANSACTION_COMMIT_DELTA_BYTES, MAX_TRANSACTION_COMMIT_DELTA_ENTRIES, transactionCommitDeltaRetainedBytes, SnapshotManifestMissingError, TransactionRecordConflictError, WriteConflictError
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
2
|
+
import { assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_LEASE_TTL_MS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_TRANSACTION_STAGE_SEGMENTS, MAX_TRANSACTION_COMMIT_DELTA_BYTES, MAX_TRANSACTION_COMMIT_DELTA_ENTRIES, transactionCommitDeltaRetainedBytes, SnapshotManifestMissingError, TransactionRecordConflictError, WriteConflictError } from "../storage/types.js";
|
|
3
|
+
const DEFAULT_TRANSACTION_TTL_MS = 3e4;
|
|
4
|
+
class TransactionClosedError extends Error {
|
|
5
|
+
transactionId;
|
|
6
|
+
name = "TransactionClosedError";
|
|
7
|
+
constructor(transactionId) {
|
|
8
|
+
super(`Transaction is no longer active: ${transactionId}`);
|
|
9
|
+
this.transactionId = transactionId;
|
|
10
|
+
}
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
class Snapshot {
|
|
13
|
+
store;
|
|
14
|
+
version;
|
|
15
|
+
constructor(store, version) {
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.version = version;
|
|
18
|
+
}
|
|
19
|
+
async hasBlock(id) {
|
|
20
|
+
return (await this.store.hasManifestBlocks(this.version, [id]))[0] === true;
|
|
21
|
+
}
|
|
22
|
+
async hasBlocks(ids) {
|
|
23
|
+
const present = [];
|
|
24
|
+
for (let start = 0; start < ids.length; start += MAX_MANIFEST_BLOCK_PRESENCE_IDS) {
|
|
25
|
+
present.push(...await this.store.hasManifestBlocks(this.version, ids.slice(start, start + MAX_MANIFEST_BLOCK_PRESENCE_IDS)));
|
|
26
|
+
}
|
|
27
|
+
return present;
|
|
28
|
+
}
|
|
29
|
+
async getBlock(id) {
|
|
30
|
+
return this.store.readManifestBlock(this.version, id);
|
|
31
|
+
}
|
|
33
32
|
}
|
|
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
|
-
/** Closes this snapshot without touching the store — for a lease the store no longer holds. */
|
|
106
|
-
discard() {
|
|
107
|
-
this.#released = true;
|
|
108
|
-
}
|
|
109
|
-
#assertOpen() {
|
|
110
|
-
if (this.#released)
|
|
111
|
-
throw new Error(`Snapshot lease has been released: ${this.#record.id}`);
|
|
112
|
-
}
|
|
33
|
+
class LeasedSnapshot extends Snapshot {
|
|
34
|
+
now;
|
|
35
|
+
#record;
|
|
36
|
+
#released = false;
|
|
37
|
+
#expiresAtIso;
|
|
38
|
+
#expiresAtMs = Number.NaN;
|
|
39
|
+
constructor(store, version, record, now) {
|
|
40
|
+
super(store, version);
|
|
41
|
+
this.now = now;
|
|
42
|
+
this.#record = structuredClone(record);
|
|
43
|
+
this.#store = store;
|
|
44
|
+
}
|
|
45
|
+
#store;
|
|
46
|
+
get leaseId() {
|
|
47
|
+
return this.#record.id;
|
|
48
|
+
}
|
|
49
|
+
get expiresAt() {
|
|
50
|
+
const iso = this.#record.expiresAt;
|
|
51
|
+
if (iso !== this.#expiresAtIso) {
|
|
52
|
+
this.#expiresAtIso = iso;
|
|
53
|
+
this.#expiresAtMs = Date.parse(iso);
|
|
54
|
+
}
|
|
55
|
+
return new Date(this.#expiresAtMs);
|
|
56
|
+
}
|
|
57
|
+
async getBlock(id) {
|
|
58
|
+
this.#assertOpen();
|
|
59
|
+
return super.getBlock(id);
|
|
60
|
+
}
|
|
61
|
+
async renew(ttlMs) {
|
|
62
|
+
this.#assertOpen();
|
|
63
|
+
const now = this.now();
|
|
64
|
+
const expiresAt = expiryAfter(now, ttlMs);
|
|
65
|
+
this.#record = await this.#store.renewLease({
|
|
66
|
+
id: this.#record.id,
|
|
67
|
+
expectedRevision: this.#record.revision,
|
|
68
|
+
expiresAtCutoff: dateIsoString(now),
|
|
69
|
+
expiresAt
|
|
70
|
+
});
|
|
71
|
+
return new Date(this.#record.expiresAt);
|
|
72
|
+
}
|
|
73
|
+
async moveTo(version, ttlMs) {
|
|
74
|
+
this.#assertOpen();
|
|
75
|
+
const move = this.#store.moveLease?.bind(this.#store);
|
|
76
|
+
if (move === void 0)
|
|
77
|
+
return void 0;
|
|
78
|
+
const now = this.now();
|
|
79
|
+
const expiresAt = expiryAfter(now, ttlMs);
|
|
80
|
+
const record = await move({
|
|
81
|
+
id: this.#record.id,
|
|
82
|
+
expectedRevision: this.#record.revision,
|
|
83
|
+
manifestVersion: version,
|
|
84
|
+
expiresAtCutoff: dateIsoString(now),
|
|
85
|
+
expiresAt
|
|
86
|
+
});
|
|
87
|
+
this.#released = true;
|
|
88
|
+
return new LeasedSnapshot(this.#store, version, record, this.now);
|
|
89
|
+
}
|
|
90
|
+
async release() {
|
|
91
|
+
if (this.#released)
|
|
92
|
+
return;
|
|
93
|
+
await this.#store.removeLease({ id: this.#record.id, ownerId: this.#record.ownerId });
|
|
94
|
+
this.#released = true;
|
|
95
|
+
}
|
|
96
|
+
discard() {
|
|
97
|
+
this.#released = true;
|
|
98
|
+
}
|
|
99
|
+
#assertOpen() {
|
|
100
|
+
if (this.#released)
|
|
101
|
+
throw new Error(`Snapshot lease has been released: ${this.#record.id}`);
|
|
102
|
+
}
|
|
113
103
|
}
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
this.#
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
104
|
+
class DatabaseTransaction {
|
|
105
|
+
store;
|
|
106
|
+
now;
|
|
107
|
+
ttlMs;
|
|
108
|
+
#record;
|
|
109
|
+
#uniqueKeyChanges = [];
|
|
110
|
+
#ftsChanges = /* @__PURE__ */ new Map();
|
|
111
|
+
#commitDeltaBytes = 0;
|
|
112
|
+
#commitDeltaEntries = 0;
|
|
113
|
+
#compactionJobId = null;
|
|
114
|
+
#compactionSourceBlockIds = /* @__PURE__ */ new Set();
|
|
115
|
+
#changedTableIds = /* @__PURE__ */ new Set();
|
|
116
|
+
#levelZeroSegmentLimits = /* @__PURE__ */ new Map();
|
|
117
|
+
#logicallyUnchanged = false;
|
|
118
|
+
#deferredBlocks = [];
|
|
119
|
+
#deferredSegments = [];
|
|
120
|
+
#knownSegments = /* @__PURE__ */ new Map();
|
|
121
|
+
#persisted;
|
|
122
|
+
#snapshotLease;
|
|
123
|
+
#snapshotLeaseOperation = Promise.resolve();
|
|
124
|
+
#snapshotRenewal;
|
|
125
|
+
#heartbeatTimer;
|
|
126
|
+
#renewal;
|
|
127
|
+
#initialCatalogProbe;
|
|
128
|
+
constructor(store, record, now, ttlMs, createWeakRef, options = {}) {
|
|
129
|
+
this.store = store;
|
|
130
|
+
this.now = now;
|
|
131
|
+
this.ttlMs = ttlMs;
|
|
132
|
+
this.#record = structuredClone(record);
|
|
133
|
+
this.#persisted = options.persisted ?? true;
|
|
134
|
+
this.#initialCatalogProbe = options.initialCatalogProbe;
|
|
135
|
+
this.#snapshotLease = options.snapshotLease;
|
|
136
|
+
const reference = createWeakRef(this);
|
|
137
|
+
const timer = setInterval(() => {
|
|
138
|
+
const transaction = reference.deref();
|
|
139
|
+
if (transaction === void 0) {
|
|
140
|
+
clearInterval(timer);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
void transaction.#renewOwnership(true).catch(() => void 0);
|
|
144
|
+
}, Math.max(1, Math.floor(ttlMs / 3)));
|
|
145
|
+
timer.unref?.();
|
|
146
|
+
this.#heartbeatTimer = timer;
|
|
147
|
+
}
|
|
148
|
+
get id() {
|
|
149
|
+
return this.#record.id;
|
|
150
|
+
}
|
|
151
|
+
get status() {
|
|
152
|
+
return this.#record.status;
|
|
153
|
+
}
|
|
154
|
+
get snapshotVersion() {
|
|
155
|
+
return this.#record.snapshotVersion;
|
|
156
|
+
}
|
|
157
|
+
get pendingBlockIds() {
|
|
158
|
+
return [...this.#record.pendingBlockIds, ...this.#deferredBlocks.map((block) => block.id)];
|
|
159
|
+
}
|
|
160
|
+
get pendingSegmentIds() {
|
|
161
|
+
return [
|
|
162
|
+
...this.#record.pendingSegmentIds,
|
|
163
|
+
...this.#deferredSegments.map((segment) => segment.id)
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
get deferredSegments() {
|
|
167
|
+
return structuredClone(this.#deferredSegments);
|
|
168
|
+
}
|
|
169
|
+
get pendingTableNextRowId() {
|
|
170
|
+
return this.#record.pendingTableNextRowId;
|
|
171
|
+
}
|
|
172
|
+
get initialCatalogProbe() {
|
|
173
|
+
return this.#initialCatalogProbe === void 0 ? void 0 : { ...this.#initialCatalogProbe };
|
|
174
|
+
}
|
|
175
|
+
get committedCatalogContribution() {
|
|
176
|
+
if (this.#record.status !== "committed")
|
|
177
|
+
return void 0;
|
|
178
|
+
return {
|
|
179
|
+
transaction: structuredClone(this.#record),
|
|
180
|
+
segments: structuredClone([...this.#knownSegments.values()]),
|
|
181
|
+
...this.#initialCatalogProbe === void 0 ? {} : { initialCatalogProbe: { ...this.#initialCatalogProbe } }
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
get compactionSourceBlockIds() {
|
|
185
|
+
return [...this.#compactionSourceBlockIds].sort();
|
|
186
|
+
}
|
|
187
|
+
async renew() {
|
|
188
|
+
this.#assertActive();
|
|
189
|
+
await this.#renewOwnership(true);
|
|
190
|
+
}
|
|
191
|
+
get stagedWorkCount() {
|
|
192
|
+
return this.pendingBlockIds.length + this.pendingSegmentIds.length + this.#uniqueKeyChanges.length + this.#ftsChanges.size + this.#compactionSourceBlockIds.size;
|
|
193
|
+
}
|
|
194
|
+
checkpoint() {
|
|
195
|
+
this.#assertActive();
|
|
196
|
+
return {
|
|
197
|
+
transactionId: this.id,
|
|
198
|
+
pendingBlockIds: this.pendingBlockIds,
|
|
199
|
+
pendingSegmentIds: this.pendingSegmentIds,
|
|
200
|
+
uniqueKeyChanges: structuredClone(this.#uniqueKeyChanges),
|
|
201
|
+
ftsChanges: structuredClone(this.#materializedFtsChanges()),
|
|
202
|
+
compactionJobId: this.#compactionJobId,
|
|
203
|
+
compactionSourceBlockIds: [...this.#compactionSourceBlockIds],
|
|
204
|
+
levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
|
|
205
|
+
tableId,
|
|
206
|
+
limit
|
|
207
|
+
})),
|
|
208
|
+
changedTableIds: [...this.#changedTableIds],
|
|
209
|
+
logicallyUnchanged: this.#logicallyUnchanged
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
checkpointRetainedBytes() {
|
|
213
|
+
this.#assertActive();
|
|
214
|
+
let bytes = 256;
|
|
215
|
+
const addString = (value) => {
|
|
216
|
+
bytes = checkpointByteSum(bytes, 16 + value.length * 2);
|
|
217
|
+
};
|
|
218
|
+
addString(this.id);
|
|
219
|
+
for (const id of this.pendingBlockIds)
|
|
220
|
+
addString(id);
|
|
221
|
+
for (const id of this.pendingSegmentIds)
|
|
222
|
+
addString(id);
|
|
223
|
+
for (const change of this.#uniqueKeyChanges) {
|
|
224
|
+
addString(change.tableId);
|
|
225
|
+
for (const token of change.keyTokens)
|
|
226
|
+
addString(token);
|
|
227
|
+
bytes = checkpointByteSum(bytes, 32);
|
|
228
|
+
}
|
|
229
|
+
for (const change of this.#materializedFtsChanges()) {
|
|
230
|
+
addString(change.tableId);
|
|
231
|
+
for (const column of change.columns) {
|
|
232
|
+
addString(column.columnId);
|
|
233
|
+
bytes = checkpointByteSum(bytes, 48);
|
|
234
|
+
for (const posting of column.postings) {
|
|
235
|
+
addString(posting.term);
|
|
236
|
+
bytes = checkpointByteSum(bytes, posting.rowIds.length * 8 + posting.tf.length * 8 + 32);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (this.#compactionJobId !== null)
|
|
241
|
+
addString(this.#compactionJobId);
|
|
242
|
+
for (const id of this.#compactionSourceBlockIds)
|
|
243
|
+
addString(id);
|
|
244
|
+
for (const [tableId] of this.#levelZeroSegmentLimits)
|
|
245
|
+
addString(tableId);
|
|
246
|
+
for (const id of this.#changedTableIds)
|
|
247
|
+
addString(id);
|
|
248
|
+
return bytes;
|
|
249
|
+
}
|
|
250
|
+
async rollbackTo(checkpoint) {
|
|
251
|
+
this.#assertActive();
|
|
252
|
+
if (checkpoint.transactionId !== this.id) {
|
|
253
|
+
throw new TypeError("A transaction checkpoint belongs to another transaction");
|
|
254
|
+
}
|
|
255
|
+
const blockPrefix = checkpoint.pendingBlockIds;
|
|
256
|
+
const segmentPrefix = checkpoint.pendingSegmentIds;
|
|
257
|
+
const retainedBlocks = new Set(blockPrefix);
|
|
258
|
+
const retainedSegments = new Set(segmentPrefix);
|
|
259
|
+
const currentBlockIds = this.pendingBlockIds;
|
|
260
|
+
const currentSegmentIds = this.pendingSegmentIds;
|
|
261
|
+
if (blockPrefix.some((id) => !currentBlockIds.includes(id)) || segmentPrefix.some((id) => !currentSegmentIds.includes(id))) {
|
|
262
|
+
throw new TypeError("A transaction checkpoint is no longer reachable");
|
|
263
|
+
}
|
|
264
|
+
const removedBlockIds = currentBlockIds.filter((id) => !retainedBlocks.has(id));
|
|
265
|
+
const removedSegmentIds = currentSegmentIds.filter((id) => !retainedSegments.has(id));
|
|
266
|
+
if (this.#persisted && (removedBlockIds.length > 0 || removedSegmentIds.length > 0)) {
|
|
267
|
+
await this.#renewOwnership();
|
|
268
|
+
this.#record = await this.store.rollbackTransactionArtifacts({
|
|
269
|
+
transactionId: this.id,
|
|
270
|
+
expectedRevision: this.#record.revision,
|
|
271
|
+
pendingBlockIds: blockPrefix,
|
|
272
|
+
pendingSegmentIds: segmentPrefix,
|
|
273
|
+
removeBlockIds: removedBlockIds,
|
|
274
|
+
removeSegmentIds: removedSegmentIds,
|
|
275
|
+
updatedAt: dateIsoString(this.now())
|
|
276
|
+
});
|
|
277
|
+
} else if (!this.#persisted) {
|
|
278
|
+
for (let index = this.#deferredBlocks.length - 1; index >= 0; index -= 1) {
|
|
279
|
+
if (!retainedBlocks.has(this.#deferredBlocks[index]?.id ?? "")) {
|
|
280
|
+
this.#deferredBlocks.splice(index, 1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (let index = this.#deferredSegments.length - 1; index >= 0; index -= 1) {
|
|
284
|
+
if (!retainedSegments.has(this.#deferredSegments[index]?.id ?? "")) {
|
|
285
|
+
this.#deferredSegments.splice(index, 1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const id of removedSegmentIds)
|
|
290
|
+
this.#knownSegments.delete(id);
|
|
291
|
+
this.#uniqueKeyChanges.splice(0);
|
|
292
|
+
this.#ftsChanges.clear();
|
|
293
|
+
this.#commitDeltaBytes = 0;
|
|
294
|
+
this.#commitDeltaEntries = 0;
|
|
295
|
+
for (const changes of checkpoint.uniqueKeyChanges)
|
|
296
|
+
this.setUniqueKeyChanges(changes);
|
|
297
|
+
for (const changes of checkpoint.ftsChanges)
|
|
298
|
+
this.setFtsChanges(changes);
|
|
299
|
+
this.#compactionJobId = checkpoint.compactionJobId;
|
|
300
|
+
this.#compactionSourceBlockIds.clear();
|
|
301
|
+
for (const id of checkpoint.compactionSourceBlockIds)
|
|
302
|
+
this.#compactionSourceBlockIds.add(id);
|
|
303
|
+
this.#levelZeroSegmentLimits.clear();
|
|
304
|
+
for (const { tableId, limit } of checkpoint.levelZeroSegmentLimits) {
|
|
305
|
+
this.#levelZeroSegmentLimits.set(tableId, limit);
|
|
306
|
+
}
|
|
307
|
+
this.#changedTableIds.clear();
|
|
308
|
+
for (const id of checkpoint.changedTableIds)
|
|
309
|
+
this.#changedTableIds.add(id);
|
|
310
|
+
this.#logicallyUnchanged = checkpoint.logicallyUnchanged;
|
|
311
|
+
}
|
|
312
|
+
async snapshot() {
|
|
313
|
+
return loadSnapshot(this.store, this.#record.snapshotVersion);
|
|
314
|
+
}
|
|
315
|
+
async getBlock(id) {
|
|
316
|
+
this.#assertActive();
|
|
317
|
+
const deferred = this.#deferredBlocks.find((block) => block.id === id);
|
|
318
|
+
if (deferred !== void 0)
|
|
319
|
+
return new Uint8Array(deferred.bytes);
|
|
320
|
+
if (this.#record.pendingBlockIds.includes(id))
|
|
321
|
+
return this.store.getBlock(id);
|
|
322
|
+
return (await this.snapshot()).getBlock(id);
|
|
323
|
+
}
|
|
324
|
+
#assertProspectiveArtifactJournal(blocks, segments) {
|
|
325
|
+
const blockIds = new Set(this.pendingBlockIds);
|
|
326
|
+
const segmentIds = new Set(this.pendingSegmentIds);
|
|
327
|
+
for (const block of blocks) {
|
|
328
|
+
if (block.id.length === 0)
|
|
329
|
+
throw new TypeError("Block ID cannot be empty");
|
|
330
|
+
if (blockIds.has(block.id))
|
|
331
|
+
throw new Error(`Block already exists: ${block.id}`);
|
|
332
|
+
blockIds.add(block.id);
|
|
333
|
+
}
|
|
334
|
+
for (const segment of segments) {
|
|
335
|
+
if (segment.id.length === 0)
|
|
336
|
+
throw new TypeError("Segment ID cannot be empty");
|
|
337
|
+
if (segmentIds.has(segment.id))
|
|
338
|
+
throw new Error(`Segment already exists: ${segment.id}`);
|
|
339
|
+
segmentIds.add(segment.id);
|
|
340
|
+
}
|
|
341
|
+
assertTransactionArtifactJournalLimits([...blockIds], [...segmentIds]);
|
|
342
|
+
}
|
|
343
|
+
async #stageArtifactBatch(blocks, segments) {
|
|
344
|
+
if (blocks.length === 0 && segments.length === 0)
|
|
345
|
+
return;
|
|
346
|
+
assertTransactionArtifactBatchLimits(blocks, segments);
|
|
347
|
+
await this.#ensurePersisted();
|
|
348
|
+
await this.#renewOwnership();
|
|
349
|
+
try {
|
|
350
|
+
this.#record = await this.store.stageTransactionArtifacts({
|
|
351
|
+
transactionId: this.id,
|
|
352
|
+
expectedRevision: this.#record.revision,
|
|
353
|
+
blocks,
|
|
354
|
+
segments,
|
|
355
|
+
updatedAt: dateIsoString(this.now())
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
await this.#recoverStagedAcknowledgement(error, blocks, segments);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async stageBlock(id, bytes) {
|
|
362
|
+
return this.stageBlocks([{ id, bytes }]);
|
|
363
|
+
}
|
|
364
|
+
async stageBlocks(blocks) {
|
|
365
|
+
this.#assertActive();
|
|
366
|
+
if (blocks.length === 0)
|
|
367
|
+
return;
|
|
368
|
+
await this.stageArtifacts(blocks, []);
|
|
369
|
+
}
|
|
370
|
+
async stageArtifacts(blocks, segments) {
|
|
371
|
+
this.#assertActive();
|
|
372
|
+
for (const segment of segments) {
|
|
373
|
+
if (segment.transactionId !== this.id) {
|
|
374
|
+
throw new Error(`Segment ${segment.id} belongs to another transaction`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const ordinalBase = this.pendingSegmentIds.length;
|
|
378
|
+
const ordered = segments.map((segment, index) => ({
|
|
379
|
+
...segment,
|
|
380
|
+
commitOrdinal: ordinalBase + index
|
|
381
|
+
}));
|
|
382
|
+
this.#assertProspectiveArtifactJournal(blocks, ordered);
|
|
383
|
+
this.#registerLevelZeroSegments(ordered);
|
|
384
|
+
if (blocks.length === 0 && ordered.length === 0)
|
|
385
|
+
return;
|
|
386
|
+
const batches = transactionArtifactBatches(blocks, ordered);
|
|
387
|
+
if (!this.#persisted && this.#deferredBlocks.length === 0 && this.#deferredSegments.length === 0 && batches.length === 1) {
|
|
388
|
+
assertTransactionArtifactBatchLimits(blocks, ordered);
|
|
389
|
+
this.#deferredBlocks.push(...blocks.map((block) => ({ id: block.id, bytes: new Uint8Array(block.bytes) })));
|
|
390
|
+
this.#deferredSegments.push(...structuredClone(ordered));
|
|
391
|
+
for (const segment of ordered)
|
|
392
|
+
this.#knownSegments.set(segment.id, structuredClone(segment));
|
|
393
|
+
for (const segment of ordered)
|
|
394
|
+
this.#changedTableIds.add(segment.tableId);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
await this.#persistDeferredArtifacts();
|
|
398
|
+
for (const batch of batches) {
|
|
399
|
+
await this.#stageArtifactBatch(batch.blocks, batch.segments);
|
|
400
|
+
}
|
|
401
|
+
for (const segment of ordered)
|
|
402
|
+
this.#knownSegments.set(segment.id, structuredClone(segment));
|
|
403
|
+
for (const segment of ordered)
|
|
404
|
+
this.#changedTableIds.add(segment.tableId);
|
|
405
|
+
}
|
|
406
|
+
async stageArtifactsAndCommit(blocks, segments) {
|
|
407
|
+
this.#assertActive();
|
|
408
|
+
for (const segment of segments) {
|
|
409
|
+
if (segment.transactionId !== this.id) {
|
|
410
|
+
throw new Error(`Segment ${segment.id} belongs to another transaction`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const pendingBlockIds = new Set(this.#record.pendingBlockIds);
|
|
414
|
+
const pendingSegmentIds = new Set(this.#record.pendingSegmentIds);
|
|
415
|
+
await this.#assertRetainedArtifactsUnchanged(blocks.filter((block) => pendingBlockIds.has(block.id)), segments.filter((segment) => pendingSegmentIds.has(segment.id)));
|
|
416
|
+
const unstagedBlocks = blocks.filter((block) => !pendingBlockIds.has(block.id));
|
|
417
|
+
const unstagedSegments = segments.filter((segment) => !pendingSegmentIds.has(segment.id));
|
|
418
|
+
if (blocks.length + segments.length > 0 && unstagedBlocks.length === 0 && unstagedSegments.length === 0) {
|
|
419
|
+
return this.commit();
|
|
420
|
+
}
|
|
421
|
+
this.#assertProspectiveArtifactJournal(unstagedBlocks, unstagedSegments);
|
|
422
|
+
const single = this.store.writeTransaction?.bind(this.store);
|
|
423
|
+
const batches = transactionArtifactBatches(unstagedBlocks, unstagedSegments);
|
|
424
|
+
if (single === void 0 || batches.length !== 1) {
|
|
425
|
+
await this.stageArtifacts(unstagedBlocks, unstagedSegments);
|
|
426
|
+
return this.commit();
|
|
427
|
+
}
|
|
428
|
+
await this.#renewOwnership();
|
|
429
|
+
const ordinalBase = this.#record.pendingSegmentIds.length;
|
|
430
|
+
const ordered = unstagedSegments.map((segment, index) => ({
|
|
431
|
+
...segment,
|
|
432
|
+
commitOrdinal: ordinalBase + index
|
|
433
|
+
}));
|
|
434
|
+
assertTransactionArtifactBatchLimits(unstagedBlocks, ordered);
|
|
435
|
+
this.#registerLevelZeroSegments(ordered);
|
|
436
|
+
const changedTableIds = new Set(this.#changedTableIds);
|
|
437
|
+
for (const segment of ordered)
|
|
438
|
+
changedTableIds.add(segment.tableId);
|
|
439
|
+
const { snapshotVersion: _pinned, ...fresh } = this.#record;
|
|
440
|
+
void _pinned;
|
|
441
|
+
const committedAt = dateIsoString(this.now());
|
|
442
|
+
const committedRevision = advanceRevision(this.#record.revision, 2);
|
|
443
|
+
const compactionJobId = this.#compactionJobId;
|
|
444
|
+
const ftsChanges = this.#materializedFtsChanges();
|
|
445
|
+
for (const segment of ordered)
|
|
446
|
+
this.#knownSegments.set(segment.id, structuredClone(segment));
|
|
447
|
+
for (const segment of ordered)
|
|
448
|
+
this.#changedTableIds.add(segment.tableId);
|
|
449
|
+
try {
|
|
450
|
+
const manifest = await single({
|
|
451
|
+
transaction: this.#persisted ? { id: this.id, expectedRevision: this.#record.revision } : { record: fresh },
|
|
452
|
+
blocks: unstagedBlocks,
|
|
453
|
+
segments: ordered,
|
|
454
|
+
expectedManifestVersion: this.#record.snapshotVersion,
|
|
455
|
+
changedTableIds: this.#logicallyUnchanged ? [] : [...changedTableIds],
|
|
456
|
+
...this.#compactionSourceBlockIds.size === 0 ? {} : {
|
|
457
|
+
compactionJobId: requiredCompactionJobId(compactionJobId),
|
|
458
|
+
removedBlockIds: [...this.#compactionSourceBlockIds]
|
|
459
|
+
},
|
|
460
|
+
...this.#uniqueKeyChanges.length === 0 ? {} : { uniqueKeyChanges: this.#uniqueKeyChanges },
|
|
461
|
+
...ftsChanges.length === 0 ? {} : { ftsChanges },
|
|
462
|
+
...this.#levelZeroSegmentLimits.size === 0 ? {} : {
|
|
463
|
+
levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
|
|
464
|
+
tableId,
|
|
465
|
+
limit
|
|
466
|
+
}))
|
|
467
|
+
},
|
|
468
|
+
committedAt
|
|
469
|
+
});
|
|
470
|
+
this.#record = {
|
|
471
|
+
...this.#record,
|
|
472
|
+
pendingBlockIds: [...pendingBlockIds, ...unstagedBlocks.map((block) => block.id)],
|
|
473
|
+
pendingSegmentIds: [...pendingSegmentIds, ...ordered.map((segment) => segment.id)],
|
|
474
|
+
status: "committed",
|
|
475
|
+
committedVersion: manifest.version,
|
|
476
|
+
revision: committedRevision,
|
|
477
|
+
updatedAt: committedAt
|
|
478
|
+
};
|
|
479
|
+
this.#persisted = true;
|
|
480
|
+
this.#stopHeartbeat();
|
|
481
|
+
await this.#releaseSnapshotLease().catch(() => void 0);
|
|
482
|
+
return manifest;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return this.#recoverCommitted(error);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async stageExistingBlocks(blockIds) {
|
|
488
|
+
this.#assertActive();
|
|
489
|
+
await this.#persistDeferredArtifacts();
|
|
490
|
+
const pending = new Set(this.#record.pendingBlockIds);
|
|
491
|
+
const additions = [...new Set(blockIds)].filter((id) => !pending.has(id));
|
|
492
|
+
if (additions.length === 0)
|
|
493
|
+
return;
|
|
494
|
+
if (additions.some((id) => id.length === 0)) {
|
|
495
|
+
throw new TypeError("Block ID cannot be empty");
|
|
496
|
+
}
|
|
497
|
+
assertTransactionArtifactJournalLimits([...this.#record.pendingBlockIds, ...additions], this.#record.pendingSegmentIds);
|
|
498
|
+
for (const id of additions) {
|
|
499
|
+
if (await this.store.getBlock(id) === void 0) {
|
|
500
|
+
throw new Error(`Cannot stage a missing existing block: ${id}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
await this.#ensurePersisted();
|
|
504
|
+
await this.#renewOwnership();
|
|
505
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
506
|
+
pendingBlockIds: [...this.#record.pendingBlockIds, ...additions],
|
|
507
|
+
updatedAt: dateIsoString(this.now())
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async stageSegment(record) {
|
|
511
|
+
this.#assertActive();
|
|
512
|
+
if (record.transactionId !== this.id) {
|
|
513
|
+
throw new Error(`Segment ${record.id} belongs to another transaction`);
|
|
514
|
+
}
|
|
515
|
+
await this.stageArtifacts([], [record]);
|
|
516
|
+
}
|
|
517
|
+
async stageExistingSegment(segmentId) {
|
|
518
|
+
this.#assertActive();
|
|
519
|
+
await this.#persistDeferredArtifacts();
|
|
520
|
+
if (this.#record.pendingSegmentIds.includes(segmentId))
|
|
521
|
+
return;
|
|
522
|
+
const record = await this.store.getSegment(segmentId);
|
|
523
|
+
if (record === void 0)
|
|
524
|
+
throw new Error(`Cannot stage a missing existing segment: ${segmentId}`);
|
|
525
|
+
if (record.transactionId !== this.id) {
|
|
526
|
+
throw new Error(`Segment ${segmentId} belongs to another transaction`);
|
|
527
|
+
}
|
|
528
|
+
if (record.commitOrdinal !== this.#record.pendingSegmentIds.length) {
|
|
529
|
+
throw new Error(`Existing segment ${segmentId} is not the next journal ordinal`);
|
|
530
|
+
}
|
|
531
|
+
assertTransactionArtifactJournalLimits(this.#record.pendingBlockIds, [
|
|
532
|
+
...this.#record.pendingSegmentIds,
|
|
533
|
+
segmentId
|
|
534
|
+
]);
|
|
535
|
+
this.#registerLevelZeroSegments([record]);
|
|
536
|
+
this.#changedTableIds.add(record.tableId);
|
|
537
|
+
await this.#ensurePersisted();
|
|
538
|
+
await this.#renewOwnership();
|
|
539
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
540
|
+
pendingSegmentIds: [...this.#record.pendingSegmentIds, segmentId],
|
|
541
|
+
updatedAt: dateIsoString(this.now())
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
async adoptAbortedCompactionSegment(record, abortedOwner) {
|
|
545
|
+
this.#assertActive();
|
|
546
|
+
await this.#persistDeferredArtifacts();
|
|
547
|
+
if (record.transactionId !== this.id) {
|
|
548
|
+
throw new Error(`Segment ${record.id} belongs to another transaction`);
|
|
549
|
+
}
|
|
550
|
+
if (abortedOwner.status !== "aborted") {
|
|
551
|
+
throw new Error(`Segment ${record.id} owner is not aborted`);
|
|
552
|
+
}
|
|
553
|
+
const compactionJobId = requiredCompactionJobId(this.#compactionJobId);
|
|
554
|
+
this.#assertProspectiveArtifactJournal([], [record]);
|
|
555
|
+
this.#registerLevelZeroSegments([record]);
|
|
556
|
+
this.#changedTableIds.add(record.tableId);
|
|
557
|
+
await this.#ensurePersisted();
|
|
558
|
+
await this.#renewOwnership();
|
|
559
|
+
try {
|
|
560
|
+
this.#record = await this.store.adoptAbortedSegment({
|
|
561
|
+
segment: record,
|
|
562
|
+
expectedAbortedTransactionId: abortedOwner.id,
|
|
563
|
+
expectedAbortedTransactionRevision: abortedOwner.revision,
|
|
564
|
+
replacementTransactionId: this.id,
|
|
565
|
+
expectedReplacementTransactionRevision: this.#record.revision,
|
|
566
|
+
compactionJobId,
|
|
567
|
+
updatedAt: dateIsoString(this.now())
|
|
568
|
+
});
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const [current, segment] = await Promise.all([
|
|
571
|
+
this.store.getTransaction(this.id),
|
|
572
|
+
this.store.getSegment(record.id)
|
|
573
|
+
]);
|
|
574
|
+
if (current?.status !== "active" || !current.pendingSegmentIds.includes(record.id) || segment?.transactionId !== this.id || !sameSegmentArtifacts(segment, record)) {
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
this.#record = current;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
markTableChanged(tableId) {
|
|
581
|
+
this.#assertActive();
|
|
582
|
+
this.#changedTableIds.add(tableId);
|
|
583
|
+
}
|
|
584
|
+
markLogicallyUnchanged() {
|
|
585
|
+
this.#assertActive();
|
|
586
|
+
this.#logicallyUnchanged = true;
|
|
587
|
+
}
|
|
588
|
+
get accumulatedUniqueKeyChanges() {
|
|
589
|
+
return this.#uniqueKeyChanges;
|
|
590
|
+
}
|
|
591
|
+
setUniqueKeyChanges(changes) {
|
|
592
|
+
this.#assertActive();
|
|
593
|
+
if (changes.keyTokens.length > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
|
|
594
|
+
throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
|
|
595
|
+
}
|
|
596
|
+
transactionCommitDeltaRetainedBytes([changes], []);
|
|
597
|
+
const normalized = {
|
|
598
|
+
tableId: changes.tableId,
|
|
599
|
+
keyTokens: [...new Set(changes.keyTokens)],
|
|
600
|
+
requireAbsent: changes.requireAbsent,
|
|
601
|
+
...changes.remove === void 0 ? {} : { remove: changes.remove }
|
|
602
|
+
};
|
|
603
|
+
this.#reserveCommitDelta([normalized], []);
|
|
604
|
+
this.#uniqueKeyChanges.push(normalized);
|
|
605
|
+
}
|
|
606
|
+
setFtsChanges(changes) {
|
|
607
|
+
this.#assertActive();
|
|
608
|
+
const existingColumns = this.#ftsChanges.get(changes.tableId);
|
|
609
|
+
const tokenTotals = /* @__PURE__ */ new Map();
|
|
610
|
+
for (const column of changes.columns) {
|
|
611
|
+
const prior = tokenTotals.get(column.columnId) ?? existingColumns?.get(column.columnId)?.totalTokens ?? 0;
|
|
612
|
+
tokenTotals.set(column.columnId, safeWholeNumberSum([prior, column.totalTokens], "Full-text transaction token count"));
|
|
613
|
+
}
|
|
614
|
+
this.#reserveCommitDelta([], [changes]);
|
|
615
|
+
let columns = this.#ftsChanges.get(changes.tableId);
|
|
616
|
+
if (columns === void 0) {
|
|
617
|
+
columns = /* @__PURE__ */ new Map();
|
|
618
|
+
this.#ftsChanges.set(changes.tableId, columns);
|
|
619
|
+
}
|
|
620
|
+
for (const column of changes.columns) {
|
|
621
|
+
const present = columns.get(column.columnId);
|
|
622
|
+
if (present === void 0) {
|
|
623
|
+
columns.set(column.columnId, {
|
|
624
|
+
postings: new Map(column.postings.map((posting) => [
|
|
625
|
+
posting.term,
|
|
626
|
+
{
|
|
627
|
+
term: posting.term,
|
|
628
|
+
rowIds: [...posting.rowIds],
|
|
629
|
+
tf: [...posting.tf]
|
|
592
630
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
await this.#renewOwnership();
|
|
596
|
-
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
597
|
-
pendingBlockIds: [...this.#record.pendingBlockIds, ...additions],
|
|
598
|
-
updatedAt: dateIsoString(this.now()),
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
async stageSegment(record) {
|
|
602
|
-
this.#assertActive();
|
|
603
|
-
if (record.transactionId !== this.id) {
|
|
604
|
-
throw new Error(`Segment ${record.id} belongs to another transaction`);
|
|
605
|
-
}
|
|
606
|
-
await this.stageArtifacts([], [record]);
|
|
607
|
-
}
|
|
608
|
-
/** Reconciles a persisted segment whose journal update was interrupted. */
|
|
609
|
-
async stageExistingSegment(segmentId) {
|
|
610
|
-
this.#assertActive();
|
|
611
|
-
await this.#persistDeferredArtifacts();
|
|
612
|
-
if (this.#record.pendingSegmentIds.includes(segmentId))
|
|
613
|
-
return;
|
|
614
|
-
const record = await this.store.getSegment(segmentId);
|
|
615
|
-
if (record === undefined)
|
|
616
|
-
throw new Error(`Cannot stage a missing existing segment: ${segmentId}`);
|
|
617
|
-
if (record.transactionId !== this.id) {
|
|
618
|
-
throw new Error(`Segment ${segmentId} belongs to another transaction`);
|
|
619
|
-
}
|
|
620
|
-
if (record.commitOrdinal !== this.#record.pendingSegmentIds.length) {
|
|
621
|
-
throw new Error(`Existing segment ${segmentId} is not the next journal ordinal`);
|
|
622
|
-
}
|
|
623
|
-
assertTransactionArtifactJournalLimits(this.#record.pendingBlockIds, [
|
|
624
|
-
...this.#record.pendingSegmentIds,
|
|
625
|
-
segmentId,
|
|
626
|
-
]);
|
|
627
|
-
this.#registerLevelZeroSegments([record]);
|
|
628
|
-
this.#changedTableIds.add(record.tableId);
|
|
629
|
-
await this.#ensurePersisted();
|
|
630
|
-
await this.#renewOwnership();
|
|
631
|
-
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
632
|
-
pendingSegmentIds: [...this.#record.pendingSegmentIds, segmentId],
|
|
633
|
-
updatedAt: dateIsoString(this.now()),
|
|
631
|
+
])),
|
|
632
|
+
totalTokens: column.totalTokens
|
|
634
633
|
});
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
let duplicateTokens = 0;
|
|
637
|
+
for (const posting of column.postings) {
|
|
638
|
+
const held = present.postings.get(posting.term);
|
|
639
|
+
if (held === void 0) {
|
|
640
|
+
present.postings.set(posting.term, {
|
|
641
|
+
term: posting.term,
|
|
642
|
+
rowIds: [...posting.rowIds],
|
|
643
|
+
tf: [...posting.tf]
|
|
644
|
+
});
|
|
645
|
+
} else {
|
|
646
|
+
const merged = mergeFtsPostingRows(held, posting);
|
|
647
|
+
duplicateTokens = safeWholeNumberSum([
|
|
648
|
+
duplicateTokens,
|
|
649
|
+
postingTokenCount(held) + postingTokenCount(posting) - postingTokenCount(merged)
|
|
650
|
+
], "Duplicate posting token count");
|
|
651
|
+
present.postings.set(posting.term, merged);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
|
|
655
|
+
if (duplicateTokens > totalTokens)
|
|
656
|
+
throw new Error("Posting token merge is inconsistent");
|
|
657
|
+
present.totalTokens = totalTokens - duplicateTokens;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
#materializedFtsChanges() {
|
|
661
|
+
return [...this.#ftsChanges].map(([tableId, columns]) => ({
|
|
662
|
+
tableId,
|
|
663
|
+
columns: [...columns].map(([columnId, change]) => ({
|
|
664
|
+
columnId,
|
|
665
|
+
postings: [...change.postings.values()].sort((left, right) => left.term < right.term ? -1 : left.term > right.term ? 1 : 0),
|
|
666
|
+
totalTokens: change.totalTokens
|
|
667
|
+
}))
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
#reserveCommitDelta(uniqueKeyChanges, ftsChanges) {
|
|
671
|
+
const added = transactionCommitDeltaRetainedBytes(uniqueKeyChanges, ftsChanges);
|
|
672
|
+
const bytes = this.#commitDeltaBytes + added.bytes;
|
|
673
|
+
const entries = this.#commitDeltaEntries + added.entries;
|
|
674
|
+
if (!Number.isSafeInteger(bytes) || bytes > MAX_TRANSACTION_COMMIT_DELTA_BYTES) {
|
|
675
|
+
throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_BYTES)} retained bytes`);
|
|
676
|
+
}
|
|
677
|
+
if (!Number.isSafeInteger(entries) || entries > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
|
|
678
|
+
throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
|
|
679
|
+
}
|
|
680
|
+
this.#commitDeltaBytes = bytes;
|
|
681
|
+
this.#commitDeltaEntries = entries;
|
|
682
|
+
}
|
|
683
|
+
setCompactionIntent(compactionJobId, sourceBlockIds) {
|
|
684
|
+
this.#assertActive();
|
|
685
|
+
if (compactionJobId.length === 0)
|
|
686
|
+
throw new TypeError("Compaction job ID cannot be empty");
|
|
687
|
+
if (sourceBlockIds.length === 0) {
|
|
688
|
+
throw new TypeError("A compaction retirement must name at least one source block");
|
|
689
|
+
}
|
|
690
|
+
const next = new Set(sourceBlockIds);
|
|
691
|
+
if (next.size !== sourceBlockIds.length) {
|
|
692
|
+
throw new TypeError("Compaction source block IDs must be unique");
|
|
693
|
+
}
|
|
694
|
+
if (this.#compactionJobId !== null) {
|
|
695
|
+
if (this.#compactionJobId !== compactionJobId || next.size !== this.#compactionSourceBlockIds.size || [...next].some((id) => !this.#compactionSourceBlockIds.has(id))) {
|
|
696
|
+
throw new Error("A transaction cannot change its compaction retirement intent");
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
for (const id of next) {
|
|
701
|
+
if (id.length === 0)
|
|
702
|
+
throw new TypeError("Compaction source block ID cannot be empty");
|
|
703
|
+
if (this.#record.pendingBlockIds.includes(id)) {
|
|
704
|
+
throw new Error(`A compaction cannot retire its own pending block: ${id}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
this.#compactionJobId = compactionJobId;
|
|
708
|
+
for (const id of next)
|
|
709
|
+
this.#compactionSourceBlockIds.add(id);
|
|
710
|
+
}
|
|
711
|
+
limitLevelZeroSegments(tableId, limit) {
|
|
712
|
+
this.#assertActive();
|
|
713
|
+
if (tableId.length === 0)
|
|
714
|
+
throw new TypeError("Table ID cannot be empty");
|
|
715
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
716
|
+
throw new RangeError("Level-zero segment limit must be a positive safe integer");
|
|
717
|
+
}
|
|
718
|
+
const existing = this.#levelZeroSegmentLimits.get(tableId);
|
|
719
|
+
this.#levelZeroSegmentLimits.set(tableId, existing === void 0 ? limit : Math.min(existing, limit));
|
|
720
|
+
}
|
|
721
|
+
#registerLevelZeroSegments(segments) {
|
|
722
|
+
for (const segment of segments) {
|
|
723
|
+
if (segment.level !== 0)
|
|
724
|
+
continue;
|
|
725
|
+
const existing = this.#levelZeroSegmentLimits.get(segment.tableId);
|
|
726
|
+
if (existing === void 0) {
|
|
727
|
+
this.#levelZeroSegmentLimits.set(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async commit() {
|
|
732
|
+
this.#assertActive();
|
|
733
|
+
if (this.#deferredBlocks.length > 0 || this.#deferredSegments.length > 0) {
|
|
734
|
+
if (this.store.writeTransaction === void 0) {
|
|
639
735
|
await this.#persistDeferredArtifacts();
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (abortedOwner.status !== "aborted") {
|
|
644
|
-
throw new Error(`Segment ${record.id} owner is not aborted`);
|
|
645
|
-
}
|
|
646
|
-
const compactionJobId = requiredCompactionJobId(this.#compactionJobId);
|
|
647
|
-
this.#assertProspectiveArtifactJournal([], [record]);
|
|
648
|
-
this.#registerLevelZeroSegments([record]);
|
|
649
|
-
this.#changedTableIds.add(record.tableId);
|
|
650
|
-
await this.#ensurePersisted();
|
|
651
|
-
await this.#renewOwnership();
|
|
736
|
+
} else {
|
|
737
|
+
const blocks = this.#deferredBlocks.splice(0);
|
|
738
|
+
const segments = this.#deferredSegments.splice(0);
|
|
652
739
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
* it — dropping the table, for instance, whose only publish is the retirement of its blocks.
|
|
682
|
-
* Change-driven readers such as live queries need to see it move.
|
|
683
|
-
*/
|
|
684
|
-
markTableChanged(tableId) {
|
|
685
|
-
this.#assertActive();
|
|
686
|
-
this.#changedTableIds.add(tableId);
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Marks this commit as logically content-preserving (for example a compaction rewrite), so
|
|
690
|
-
* change-driven readers such as live queries skip it even though it stages segments.
|
|
691
|
-
*/
|
|
692
|
-
markLogicallyUnchanged() {
|
|
693
|
-
this.#assertActive();
|
|
694
|
-
this.#logicallyUnchanged = true;
|
|
695
|
-
}
|
|
696
|
-
/** Accumulated key changes in operation order, for in-scope membership overlays. */
|
|
697
|
-
get accumulatedUniqueKeyChanges() {
|
|
698
|
-
return this.#uniqueKeyChanges;
|
|
699
|
-
}
|
|
700
|
-
/** Appends one operation's key changes; entries commit in operation order. */
|
|
701
|
-
setUniqueKeyChanges(changes) {
|
|
702
|
-
this.#assertActive();
|
|
703
|
-
if (changes.keyTokens.length > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
|
|
704
|
-
throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
|
|
705
|
-
}
|
|
706
|
-
// Walk the caller-owned array before allocating a Set or clone. The aggregate tracker below
|
|
707
|
-
// charges the deduplicated retained form, while this preflight prevents one hostile call from
|
|
708
|
-
// creating a database-sized temporary allocation first.
|
|
709
|
-
transactionCommitDeltaRetainedBytes([changes], []);
|
|
710
|
-
const normalized = {
|
|
711
|
-
tableId: changes.tableId,
|
|
712
|
-
// Deduplicated, not sorted: membership is all a store reads from these, and sorting fifty
|
|
713
|
-
// thousand tokens cost a bulk delete a fifth of its time.
|
|
714
|
-
keyTokens: [...new Set(changes.keyTokens)],
|
|
715
|
-
requireAbsent: changes.requireAbsent,
|
|
716
|
-
...(changes.remove === undefined ? {} : { remove: changes.remove }),
|
|
717
|
-
};
|
|
718
|
-
this.#reserveCommitDelta([normalized], []);
|
|
719
|
-
this.#uniqueKeyChanges.push(normalized);
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Attaches one batch's full-text deltas; applied atomically with the publish. A second
|
|
723
|
-
* batch for the same table merges per column — postings stay sorted by term and row ID,
|
|
724
|
-
* duplicate row locators collapse to their greatest term frequency, and token totals are
|
|
725
|
-
* summed — so a scope can mutate one indexed table any number of times. Secondary indexes
|
|
726
|
-
* use stable hashed key locators rather than reserved row IDs, so operation order does not
|
|
727
|
-
* imply locator order.
|
|
728
|
-
*/
|
|
729
|
-
setFtsChanges(changes) {
|
|
730
|
-
this.#assertActive();
|
|
731
|
-
const existingColumns = this.#ftsChanges.get(changes.tableId);
|
|
732
|
-
const tokenTotals = new Map();
|
|
733
|
-
for (const column of changes.columns) {
|
|
734
|
-
const prior = tokenTotals.get(column.columnId) ?? existingColumns?.get(column.columnId)?.totalTokens ?? 0;
|
|
735
|
-
tokenTotals.set(column.columnId, safeWholeNumberSum([prior, column.totalTokens], "Full-text transaction token count"));
|
|
736
|
-
}
|
|
737
|
-
this.#reserveCommitDelta([], [changes]);
|
|
738
|
-
let columns = this.#ftsChanges.get(changes.tableId);
|
|
739
|
-
if (columns === undefined) {
|
|
740
|
-
columns = new Map();
|
|
741
|
-
this.#ftsChanges.set(changes.tableId, columns);
|
|
742
|
-
}
|
|
743
|
-
for (const column of changes.columns) {
|
|
744
|
-
const present = columns.get(column.columnId);
|
|
745
|
-
if (present === undefined) {
|
|
746
|
-
columns.set(column.columnId, {
|
|
747
|
-
postings: new Map(column.postings.map((posting) => [
|
|
748
|
-
posting.term,
|
|
749
|
-
{
|
|
750
|
-
term: posting.term,
|
|
751
|
-
rowIds: [...posting.rowIds],
|
|
752
|
-
tf: [...posting.tf],
|
|
753
|
-
},
|
|
754
|
-
])),
|
|
755
|
-
totalTokens: column.totalTokens,
|
|
756
|
-
});
|
|
757
|
-
continue;
|
|
758
|
-
}
|
|
759
|
-
let duplicateTokens = 0;
|
|
760
|
-
for (const posting of column.postings) {
|
|
761
|
-
const held = present.postings.get(posting.term);
|
|
762
|
-
if (held === undefined) {
|
|
763
|
-
present.postings.set(posting.term, {
|
|
764
|
-
term: posting.term,
|
|
765
|
-
rowIds: [...posting.rowIds],
|
|
766
|
-
tf: [...posting.tf],
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
const merged = mergeFtsPostingRows(held, posting);
|
|
771
|
-
duplicateTokens = safeWholeNumberSum([
|
|
772
|
-
duplicateTokens,
|
|
773
|
-
postingTokenCount(held) + postingTokenCount(posting) - postingTokenCount(merged),
|
|
774
|
-
], "Duplicate posting token count");
|
|
775
|
-
present.postings.set(posting.term, merged);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
const totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
|
|
779
|
-
if (duplicateTokens > totalTokens)
|
|
780
|
-
throw new Error("Posting token merge is inconsistent");
|
|
781
|
-
present.totalTokens = totalTokens - duplicateTokens;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
#materializedFtsChanges() {
|
|
785
|
-
return [...this.#ftsChanges].map(([tableId, columns]) => ({
|
|
740
|
+
return await this.stageArtifactsAndCommit(blocks, segments);
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.#deferredBlocks.push(...blocks);
|
|
743
|
+
this.#deferredSegments.push(...segments);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
await this.#ensurePersisted();
|
|
749
|
+
await this.#renewOwnership();
|
|
750
|
+
const committedAt = dateIsoString(this.now());
|
|
751
|
+
const committedRevision = advanceRevision(this.#record.revision, 1);
|
|
752
|
+
const compactionJobId = this.#compactionJobId;
|
|
753
|
+
const ftsChanges = this.#materializedFtsChanges();
|
|
754
|
+
try {
|
|
755
|
+
const manifest = await this.store.commitTransaction({
|
|
756
|
+
transactionId: this.id,
|
|
757
|
+
expectedTransactionRevision: this.#record.revision,
|
|
758
|
+
expectedManifestVersion: this.#record.snapshotVersion,
|
|
759
|
+
changedTableIds: this.#logicallyUnchanged ? [] : [...this.#changedTableIds],
|
|
760
|
+
...this.#compactionSourceBlockIds.size === 0 ? {} : {
|
|
761
|
+
compactionJobId: requiredCompactionJobId(compactionJobId),
|
|
762
|
+
removedBlockIds: [...this.#compactionSourceBlockIds]
|
|
763
|
+
},
|
|
764
|
+
...this.#uniqueKeyChanges.length === 0 ? {} : { uniqueKeyChanges: this.#uniqueKeyChanges },
|
|
765
|
+
...ftsChanges.length === 0 ? {} : { ftsChanges },
|
|
766
|
+
...this.#levelZeroSegmentLimits.size === 0 ? {} : {
|
|
767
|
+
levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
|
|
786
768
|
tableId,
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (next.size !== sourceBlockIds.length) {
|
|
821
|
-
throw new TypeError("Compaction source block IDs must be unique");
|
|
822
|
-
}
|
|
823
|
-
if (this.#compactionJobId !== null) {
|
|
824
|
-
if (this.#compactionJobId !== compactionJobId ||
|
|
825
|
-
next.size !== this.#compactionSourceBlockIds.size ||
|
|
826
|
-
[...next].some((id) => !this.#compactionSourceBlockIds.has(id))) {
|
|
827
|
-
throw new Error("A transaction cannot change its compaction retirement intent");
|
|
828
|
-
}
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
for (const id of next) {
|
|
832
|
-
if (id.length === 0)
|
|
833
|
-
throw new TypeError("Compaction source block ID cannot be empty");
|
|
834
|
-
if (this.#record.pendingBlockIds.includes(id)) {
|
|
835
|
-
throw new Error(`A compaction cannot retire its own pending block: ${id}`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
this.#compactionJobId = compactionJobId;
|
|
839
|
-
for (const id of next)
|
|
840
|
-
this.#compactionSourceBlockIds.add(id);
|
|
841
|
-
}
|
|
842
|
-
/** Adds an adapter-enforced post-commit level-zero ceiling for one table. */
|
|
843
|
-
limitLevelZeroSegments(tableId, limit) {
|
|
844
|
-
this.#assertActive();
|
|
845
|
-
if (tableId.length === 0)
|
|
846
|
-
throw new TypeError("Table ID cannot be empty");
|
|
847
|
-
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
848
|
-
throw new RangeError("Level-zero segment limit must be a positive safe integer");
|
|
849
|
-
}
|
|
850
|
-
const existing = this.#levelZeroSegmentLimits.get(tableId);
|
|
851
|
-
this.#levelZeroSegmentLimits.set(tableId, existing === undefined ? limit : Math.min(existing, limit));
|
|
852
|
-
}
|
|
853
|
-
#registerLevelZeroSegments(segments) {
|
|
854
|
-
for (const segment of segments) {
|
|
855
|
-
if (segment.level !== 0)
|
|
856
|
-
continue;
|
|
857
|
-
const existing = this.#levelZeroSegmentLimits.get(segment.tableId);
|
|
858
|
-
if (existing === undefined) {
|
|
859
|
-
this.#levelZeroSegmentLimits.set(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
async commit() {
|
|
864
|
-
this.#assertActive();
|
|
865
|
-
if (this.#deferredBlocks.length > 0 || this.#deferredSegments.length > 0) {
|
|
866
|
-
if (this.store.writeTransaction === undefined) {
|
|
867
|
-
await this.#persistDeferredArtifacts();
|
|
868
|
-
}
|
|
869
|
-
else {
|
|
870
|
-
const blocks = this.#deferredBlocks.splice(0);
|
|
871
|
-
const segments = this.#deferredSegments.splice(0);
|
|
872
|
-
try {
|
|
873
|
-
return await this.stageArtifactsAndCommit(blocks, segments);
|
|
874
|
-
}
|
|
875
|
-
catch (error) {
|
|
876
|
-
// A single-shot refusal mutates nothing. Keep the exact immutable artifacts available
|
|
877
|
-
// for the caller's ordinary rebase-and-retry path.
|
|
878
|
-
this.#deferredBlocks.push(...blocks);
|
|
879
|
-
this.#deferredSegments.push(...segments);
|
|
880
|
-
throw error;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
await this.#ensurePersisted();
|
|
885
|
-
await this.#renewOwnership();
|
|
886
|
-
// The store derives the published manifest from its stored base plus this delta, so the
|
|
887
|
-
// commit neither loads nor rebuilds the full block list.
|
|
888
|
-
const committedAt = dateIsoString(this.now());
|
|
889
|
-
const committedRevision = advanceRevision(this.#record.revision, 1);
|
|
890
|
-
const compactionJobId = this.#compactionJobId;
|
|
891
|
-
const ftsChanges = this.#materializedFtsChanges();
|
|
892
|
-
try {
|
|
893
|
-
const manifest = await this.store.commitTransaction({
|
|
894
|
-
transactionId: this.id,
|
|
895
|
-
expectedTransactionRevision: this.#record.revision,
|
|
896
|
-
expectedManifestVersion: this.#record.snapshotVersion,
|
|
897
|
-
changedTableIds: this.#logicallyUnchanged ? [] : [...this.#changedTableIds],
|
|
898
|
-
...(this.#compactionSourceBlockIds.size === 0
|
|
899
|
-
? {}
|
|
900
|
-
: {
|
|
901
|
-
compactionJobId: requiredCompactionJobId(compactionJobId),
|
|
902
|
-
removedBlockIds: [...this.#compactionSourceBlockIds],
|
|
903
|
-
}),
|
|
904
|
-
...(this.#uniqueKeyChanges.length === 0
|
|
905
|
-
? {}
|
|
906
|
-
: { uniqueKeyChanges: this.#uniqueKeyChanges }),
|
|
907
|
-
...(ftsChanges.length === 0 ? {} : { ftsChanges }),
|
|
908
|
-
...(this.#levelZeroSegmentLimits.size === 0
|
|
909
|
-
? {}
|
|
910
|
-
: {
|
|
911
|
-
levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
|
|
912
|
-
tableId,
|
|
913
|
-
limit,
|
|
914
|
-
})),
|
|
915
|
-
}),
|
|
916
|
-
committedAt,
|
|
917
|
-
});
|
|
918
|
-
this.#record = {
|
|
919
|
-
...this.#record,
|
|
920
|
-
status: "committed",
|
|
921
|
-
committedVersion: manifest.version,
|
|
922
|
-
revision: committedRevision,
|
|
923
|
-
updatedAt: committedAt,
|
|
924
|
-
};
|
|
925
|
-
this.#stopHeartbeat();
|
|
926
|
-
await this.#releaseSnapshotLease().catch(() => undefined);
|
|
927
|
-
return manifest;
|
|
928
|
-
}
|
|
929
|
-
catch (error) {
|
|
930
|
-
return this.#recoverCommitted(error);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
async rebase() {
|
|
934
|
-
this.#assertActive();
|
|
935
|
-
for (;;) {
|
|
936
|
-
const currentVersion = await this.store.getCurrentManifestVersion();
|
|
937
|
-
if (!this.#persisted) {
|
|
938
|
-
// Nothing is in the store yet, so move the temporary durable pin before changing the
|
|
939
|
-
// local snapshot. The old version remains protected until the replacement is admitted.
|
|
940
|
-
await this.#moveSnapshotLease(currentVersion);
|
|
941
|
-
this.#record = {
|
|
942
|
-
...this.#record,
|
|
943
|
-
snapshotVersion: currentVersion,
|
|
944
|
-
updatedAt: dateIsoString(this.now()),
|
|
945
|
-
};
|
|
946
|
-
return new Snapshot(this.store, currentVersion);
|
|
947
|
-
}
|
|
948
|
-
try {
|
|
949
|
-
await this.#renewOwnership();
|
|
950
|
-
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
951
|
-
snapshotVersion: currentVersion,
|
|
952
|
-
updatedAt: dateIsoString(this.now()),
|
|
953
|
-
});
|
|
954
|
-
return new Snapshot(this.store, currentVersion);
|
|
955
|
-
}
|
|
956
|
-
catch (error) {
|
|
957
|
-
if (error instanceof SnapshotManifestMissingError) {
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
throw error;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
async abort() {
|
|
965
|
-
this.#assertActive();
|
|
966
|
-
if (!this.#persisted) {
|
|
967
|
-
// Never written, so there is nothing to mark: the transaction simply ends here.
|
|
968
|
-
this.#deferredBlocks.length = 0;
|
|
969
|
-
this.#deferredSegments.length = 0;
|
|
970
|
-
this.#knownSegments.clear();
|
|
971
|
-
this.#record = { ...this.#record, status: "aborted", updatedAt: dateIsoString(this.now()) };
|
|
972
|
-
this.#stopHeartbeat();
|
|
973
|
-
await this.#releaseSnapshotLease().catch(() => undefined);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
769
|
+
limit
|
|
770
|
+
}))
|
|
771
|
+
},
|
|
772
|
+
committedAt
|
|
773
|
+
});
|
|
774
|
+
this.#record = {
|
|
775
|
+
...this.#record,
|
|
776
|
+
status: "committed",
|
|
777
|
+
committedVersion: manifest.version,
|
|
778
|
+
revision: committedRevision,
|
|
779
|
+
updatedAt: committedAt
|
|
780
|
+
};
|
|
781
|
+
this.#stopHeartbeat();
|
|
782
|
+
await this.#releaseSnapshotLease().catch(() => void 0);
|
|
783
|
+
return manifest;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return this.#recoverCommitted(error);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async rebase() {
|
|
789
|
+
this.#assertActive();
|
|
790
|
+
for (; ; ) {
|
|
791
|
+
const currentVersion = await this.store.getCurrentManifestVersion();
|
|
792
|
+
if (!this.#persisted) {
|
|
793
|
+
await this.#moveSnapshotLease(currentVersion);
|
|
794
|
+
this.#record = {
|
|
795
|
+
...this.#record,
|
|
796
|
+
snapshotVersion: currentVersion,
|
|
797
|
+
updatedAt: dateIsoString(this.now())
|
|
798
|
+
};
|
|
799
|
+
return new Snapshot(this.store, currentVersion);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
976
802
|
await this.#renewOwnership();
|
|
977
803
|
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
978
|
-
|
|
979
|
-
|
|
804
|
+
snapshotVersion: currentVersion,
|
|
805
|
+
updatedAt: dateIsoString(this.now())
|
|
980
806
|
});
|
|
981
|
-
this
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (this.#record.status !== "active")
|
|
986
|
-
throw new TransactionClosedError(this.id);
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Writes a deferred transaction's record before the first two-step staging call. The pinned
|
|
990
|
-
* version can only be unavailable if a commit moved the manifest on and collection pruned it
|
|
991
|
-
* meanwhile, so that surfaces as the `WriteConflictError` the rebase loops already handle.
|
|
992
|
-
*/
|
|
993
|
-
async #ensurePersisted() {
|
|
994
|
-
if (this.#persisted)
|
|
995
|
-
return;
|
|
996
|
-
this.#record = {
|
|
997
|
-
...this.#record,
|
|
998
|
-
expiresAt: this.#nextExpiry(),
|
|
999
|
-
updatedAt: dateIsoString(this.now()),
|
|
1000
|
-
};
|
|
1001
|
-
try {
|
|
1002
|
-
await this.store.createTransaction(this.#record);
|
|
1003
|
-
}
|
|
1004
|
-
catch (error) {
|
|
1005
|
-
if (error instanceof SnapshotManifestMissingError) {
|
|
1006
|
-
throw new WriteConflictError(this.#record.snapshotVersion, await this.store.getCurrentManifestVersion());
|
|
1007
|
-
}
|
|
1008
|
-
throw error;
|
|
1009
|
-
}
|
|
1010
|
-
this.#persisted = true;
|
|
1011
|
-
// The transaction record now owns the same durable pin and renews on this heartbeat. Keep
|
|
1012
|
-
// only one record rooted; a failed cleanup remains bounded by the lease expiry.
|
|
1013
|
-
void this.#releaseSnapshotLease().catch(() => undefined);
|
|
1014
|
-
}
|
|
1015
|
-
/** Flushes the bounded process-local batch before a second durable staging operation. */
|
|
1016
|
-
async #persistDeferredArtifacts() {
|
|
1017
|
-
if (this.#deferredBlocks.length === 0 && this.#deferredSegments.length === 0)
|
|
1018
|
-
return;
|
|
1019
|
-
const blocks = [...this.#deferredBlocks];
|
|
1020
|
-
const segments = [...this.#deferredSegments];
|
|
1021
|
-
await this.#stageArtifactBatch(blocks, segments);
|
|
1022
|
-
this.#deferredBlocks.length = 0;
|
|
1023
|
-
this.#deferredSegments.length = 0;
|
|
1024
|
-
}
|
|
1025
|
-
/** A commit whose acknowledgement was lost still committed: answer from the persisted record. */
|
|
1026
|
-
async #recoverCommitted(error) {
|
|
1027
|
-
const persisted = await this.store.getTransaction(this.id);
|
|
1028
|
-
if (persisted?.status === "committed" && persisted.committedVersion !== null) {
|
|
1029
|
-
const manifest = await this.store.getManifest(persisted.committedVersion);
|
|
1030
|
-
if (manifest !== undefined) {
|
|
1031
|
-
this.#record = persisted;
|
|
1032
|
-
this.#persisted = true;
|
|
1033
|
-
this.#stopHeartbeat();
|
|
1034
|
-
await this.#releaseSnapshotLease().catch(() => undefined);
|
|
1035
|
-
return manifest;
|
|
1036
|
-
}
|
|
807
|
+
return new Snapshot(this.store, currentVersion);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
if (error instanceof SnapshotManifestMissingError) {
|
|
810
|
+
continue;
|
|
1037
811
|
}
|
|
1038
812
|
throw error;
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async abort() {
|
|
817
|
+
this.#assertActive();
|
|
818
|
+
if (!this.#persisted) {
|
|
819
|
+
this.#deferredBlocks.length = 0;
|
|
820
|
+
this.#deferredSegments.length = 0;
|
|
821
|
+
this.#knownSegments.clear();
|
|
822
|
+
this.#record = { ...this.#record, status: "aborted", updatedAt: dateIsoString(this.now()) };
|
|
823
|
+
this.#stopHeartbeat();
|
|
824
|
+
await this.#releaseSnapshotLease().catch(() => void 0);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await this.#renewOwnership();
|
|
828
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
829
|
+
status: "aborted",
|
|
830
|
+
updatedAt: dateIsoString(this.now())
|
|
831
|
+
});
|
|
832
|
+
this.#stopHeartbeat();
|
|
833
|
+
await this.#releaseSnapshotLease().catch(() => void 0);
|
|
834
|
+
}
|
|
835
|
+
#assertActive() {
|
|
836
|
+
if (this.#record.status !== "active")
|
|
837
|
+
throw new TransactionClosedError(this.id);
|
|
838
|
+
}
|
|
839
|
+
async #ensurePersisted() {
|
|
840
|
+
if (this.#persisted)
|
|
841
|
+
return;
|
|
842
|
+
this.#record = {
|
|
843
|
+
...this.#record,
|
|
844
|
+
expiresAt: this.#nextExpiry(),
|
|
845
|
+
updatedAt: dateIsoString(this.now())
|
|
846
|
+
};
|
|
847
|
+
try {
|
|
848
|
+
await this.store.createTransaction(this.#record);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (error instanceof SnapshotManifestMissingError) {
|
|
851
|
+
throw new WriteConflictError(this.#record.snapshotVersion, await this.store.getCurrentManifestVersion());
|
|
852
|
+
}
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
this.#persisted = true;
|
|
856
|
+
void this.#releaseSnapshotLease().catch(() => void 0);
|
|
857
|
+
}
|
|
858
|
+
async #persistDeferredArtifacts() {
|
|
859
|
+
if (this.#deferredBlocks.length === 0 && this.#deferredSegments.length === 0)
|
|
860
|
+
return;
|
|
861
|
+
const blocks = [...this.#deferredBlocks];
|
|
862
|
+
const segments = [...this.#deferredSegments];
|
|
863
|
+
await this.#stageArtifactBatch(blocks, segments);
|
|
864
|
+
this.#deferredBlocks.length = 0;
|
|
865
|
+
this.#deferredSegments.length = 0;
|
|
866
|
+
}
|
|
867
|
+
async #recoverCommitted(error) {
|
|
868
|
+
const persisted = await this.store.getTransaction(this.id);
|
|
869
|
+
if (persisted?.status === "committed" && persisted.committedVersion !== null) {
|
|
870
|
+
const manifest = await this.store.getManifest(persisted.committedVersion);
|
|
871
|
+
if (manifest !== void 0) {
|
|
872
|
+
this.#record = persisted;
|
|
873
|
+
this.#persisted = true;
|
|
1073
874
|
this.#stopHeartbeat();
|
|
1074
|
-
await this.#releaseSnapshotLease().catch(() =>
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
875
|
+
await this.#releaseSnapshotLease().catch(() => void 0);
|
|
876
|
+
return manifest;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
881
|
+
async #renewOwnership(force = false) {
|
|
882
|
+
await this.#renewSnapshotLease(force);
|
|
883
|
+
if (!this.#persisted || this.#record.status !== "active")
|
|
884
|
+
return;
|
|
885
|
+
const now = dateMilliseconds(this.now());
|
|
886
|
+
if (!force && Date.parse(this.#record.expiresAt) - now > this.ttlMs / 3)
|
|
887
|
+
return;
|
|
888
|
+
const expiresAt = expiryAfter(new Date(now), this.ttlMs);
|
|
889
|
+
const renewal = this.#renewal ?? this.store.renewTransaction({
|
|
890
|
+
transactionId: this.id,
|
|
891
|
+
ownerId: this.#record.ownerId,
|
|
892
|
+
expiresAtCutoff: dateIsoString(new Date(now)),
|
|
893
|
+
expiresAt
|
|
894
|
+
}).then((owned) => ({ owned, expiresAt }));
|
|
895
|
+
this.#renewal = renewal;
|
|
896
|
+
let result;
|
|
897
|
+
try {
|
|
898
|
+
result = await renewal;
|
|
899
|
+
} finally {
|
|
900
|
+
if (this.#renewal === renewal)
|
|
901
|
+
this.#renewal = void 0;
|
|
902
|
+
}
|
|
903
|
+
if (result.owned) {
|
|
904
|
+
this.#record = { ...this.#record, expiresAt: result.expiresAt };
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const persisted = await this.store.getTransaction(this.id);
|
|
908
|
+
if (persisted !== void 0)
|
|
909
|
+
this.#record = persisted;
|
|
910
|
+
this.#stopHeartbeat();
|
|
911
|
+
await this.#releaseSnapshotLease().catch(() => void 0);
|
|
912
|
+
throw new TransactionClosedError(this.id);
|
|
913
|
+
}
|
|
914
|
+
async #renewSnapshotLease(force) {
|
|
915
|
+
const lease = this.#snapshotLease;
|
|
916
|
+
if (lease === void 0 || this.#record.status !== "active")
|
|
917
|
+
return;
|
|
918
|
+
if (!force && dateMilliseconds(lease.expiresAt) - dateMilliseconds(this.now()) > this.ttlMs / 3) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const renewal = this.#snapshotRenewal ?? this.#serializeSnapshotLeaseOperation(async () => {
|
|
922
|
+
const current = this.#snapshotLease;
|
|
923
|
+
if (current === void 0 || this.#record.status !== "active")
|
|
924
|
+
return;
|
|
925
|
+
if (!force && dateMilliseconds(current.expiresAt) - dateMilliseconds(this.now()) > this.ttlMs / 3) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
await current.renew(this.ttlMs);
|
|
929
|
+
});
|
|
930
|
+
this.#snapshotRenewal = renewal;
|
|
931
|
+
try {
|
|
932
|
+
await renewal;
|
|
933
|
+
} finally {
|
|
934
|
+
if (this.#snapshotRenewal === renewal)
|
|
935
|
+
this.#snapshotRenewal = void 0;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async #moveSnapshotLease(version) {
|
|
939
|
+
await this.#serializeSnapshotLeaseOperation(async () => {
|
|
940
|
+
const current = this.#snapshotLease;
|
|
941
|
+
if (current === void 0 || current.version === version)
|
|
942
|
+
return;
|
|
943
|
+
const moved = await current.moveTo(version, this.ttlMs);
|
|
944
|
+
if (moved !== void 0) {
|
|
945
|
+
if (this.#snapshotLease === current) {
|
|
946
|
+
this.#snapshotLease = moved;
|
|
947
|
+
} else {
|
|
948
|
+
await moved.release().catch(() => void 0);
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const createdAt = dateIsoString(this.now());
|
|
953
|
+
const record = {
|
|
954
|
+
id: crypto.randomUUID(),
|
|
955
|
+
kind: "reader",
|
|
956
|
+
manifestVersion: version,
|
|
957
|
+
ownerId: this.#record.ownerId,
|
|
958
|
+
createdAt,
|
|
959
|
+
expiresAt: expiryAfter(new Date(createdAt), this.ttlMs),
|
|
960
|
+
revision: 0
|
|
961
|
+
};
|
|
962
|
+
await this.store.createLease(record);
|
|
963
|
+
const replacement = new LeasedSnapshot(this.store, version, record, this.now);
|
|
964
|
+
if (this.#snapshotLease === current) {
|
|
965
|
+
this.#snapshotLease = replacement;
|
|
966
|
+
} else {
|
|
967
|
+
await replacement.release().catch(() => void 0);
|
|
968
|
+
}
|
|
969
|
+
await current.release().catch(() => void 0);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async #releaseSnapshotLease() {
|
|
973
|
+
const lease = this.#snapshotLease;
|
|
974
|
+
if (lease === void 0)
|
|
975
|
+
return;
|
|
976
|
+
this.#snapshotLease = void 0;
|
|
977
|
+
await this.#serializeSnapshotLeaseOperation(() => lease.release());
|
|
978
|
+
}
|
|
979
|
+
#serializeSnapshotLeaseOperation(operation) {
|
|
980
|
+
const run = this.#snapshotLeaseOperation.then(operation);
|
|
981
|
+
this.#snapshotLeaseOperation = run.then(() => void 0, () => void 0);
|
|
982
|
+
return run;
|
|
983
|
+
}
|
|
984
|
+
#nextExpiry() {
|
|
985
|
+
return expiryAfter(this.now(), this.ttlMs);
|
|
986
|
+
}
|
|
987
|
+
#stopHeartbeat() {
|
|
988
|
+
if (this.#heartbeatTimer !== void 0)
|
|
989
|
+
clearInterval(this.#heartbeatTimer);
|
|
990
|
+
this.#heartbeatTimer = void 0;
|
|
991
|
+
}
|
|
992
|
+
async #recoverStagedAcknowledgement(error, blocks, segments) {
|
|
993
|
+
try {
|
|
994
|
+
const persisted = await this.store.getTransaction(this.id);
|
|
995
|
+
const expectedBlocks = [...this.#record.pendingBlockIds, ...blocks.map((block) => block.id)];
|
|
996
|
+
const expectedSegments = [
|
|
997
|
+
...this.#record.pendingSegmentIds,
|
|
998
|
+
...segments.map((segment) => segment.id)
|
|
999
|
+
];
|
|
1000
|
+
if (persisted?.status === "active" && persisted.revision === advanceRevision(this.#record.revision, 1) && sameStrings(persisted.pendingBlockIds, expectedBlocks) && sameStrings(persisted.pendingSegmentIds, expectedSegments)) {
|
|
1001
|
+
const [storedBlocks, storedSegments] = await Promise.all([
|
|
1002
|
+
Promise.all(blocks.map((block) => this.store.getBlock(block.id))),
|
|
1003
|
+
Promise.all(segments.map((segment) => this.store.getSegment(segment.id)))
|
|
1004
|
+
]);
|
|
1005
|
+
if (blocks.every((block, index) => sameBytes(block.bytes, storedBlocks[index])) && segments.every((segment, index) => {
|
|
1006
|
+
const stored = storedSegments[index];
|
|
1007
|
+
return stored !== void 0 && sameSegmentArtifacts(segment, stored);
|
|
1008
|
+
})) {
|
|
1009
|
+
this.#record = persisted;
|
|
1010
|
+
this.#persisted = true;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
async #assertRetainedArtifactsUnchanged(blocks, segments) {
|
|
1018
|
+
for (const block of blocks) {
|
|
1019
|
+
if (!sameBytes(block.bytes, await this.store.getBlock(block.id))) {
|
|
1020
|
+
throw new Error(`Retried block differs from the staged artifact: ${block.id}`);
|
|
1021
|
+
}
|
|
1144
1022
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
// owner; a slow lease removal must not let the next ownership check renew the retiring pin
|
|
1151
|
-
// concurrently and turn a healthy handoff into a LeaseConflictError.
|
|
1152
|
-
this.#snapshotLease = undefined;
|
|
1153
|
-
await this.#serializeSnapshotLeaseOperation(() => lease.release());
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Lease revisions are compare-and-swap tokens, so renew, move, and release must observe one
|
|
1157
|
-
* another's returned record before issuing the next adapter operation. Keep the tail fulfilled
|
|
1158
|
-
* after a refusal: the caller still receives its error, while a terminal cleanup can proceed.
|
|
1159
|
-
*/
|
|
1160
|
-
#serializeSnapshotLeaseOperation(operation) {
|
|
1161
|
-
const run = this.#snapshotLeaseOperation.then(operation);
|
|
1162
|
-
this.#snapshotLeaseOperation = run.then(() => undefined, () => undefined);
|
|
1163
|
-
return run;
|
|
1164
|
-
}
|
|
1165
|
-
#nextExpiry() {
|
|
1166
|
-
return expiryAfter(this.now(), this.ttlMs);
|
|
1167
|
-
}
|
|
1168
|
-
#stopHeartbeat() {
|
|
1169
|
-
if (this.#heartbeatTimer !== undefined)
|
|
1170
|
-
clearInterval(this.#heartbeatTimer);
|
|
1171
|
-
this.#heartbeatTimer = undefined;
|
|
1172
|
-
}
|
|
1173
|
-
/**
|
|
1174
|
-
* An adapter can durably stage the artifacts and lose the acknowledgement afterwards. Refresh
|
|
1175
|
-
* the local revision only when the persisted journal is exactly the one this call would have
|
|
1176
|
-
* produced, so the caller's abort/rollback can clean it up without masking the original error.
|
|
1177
|
-
*/
|
|
1178
|
-
async #recoverStagedAcknowledgement(error, blocks, segments) {
|
|
1179
|
-
try {
|
|
1180
|
-
const persisted = await this.store.getTransaction(this.id);
|
|
1181
|
-
const expectedBlocks = [...this.#record.pendingBlockIds, ...blocks.map((block) => block.id)];
|
|
1182
|
-
const expectedSegments = [
|
|
1183
|
-
...this.#record.pendingSegmentIds,
|
|
1184
|
-
...segments.map((segment) => segment.id),
|
|
1185
|
-
];
|
|
1186
|
-
if (persisted?.status === "active" &&
|
|
1187
|
-
persisted.revision === advanceRevision(this.#record.revision, 1) &&
|
|
1188
|
-
sameStrings(persisted.pendingBlockIds, expectedBlocks) &&
|
|
1189
|
-
sameStrings(persisted.pendingSegmentIds, expectedSegments)) {
|
|
1190
|
-
const [storedBlocks, storedSegments] = await Promise.all([
|
|
1191
|
-
Promise.all(blocks.map((block) => this.store.getBlock(block.id))),
|
|
1192
|
-
Promise.all(segments.map((segment) => this.store.getSegment(segment.id))),
|
|
1193
|
-
]);
|
|
1194
|
-
if (blocks.every((block, index) => sameBytes(block.bytes, storedBlocks[index])) &&
|
|
1195
|
-
segments.every((segment, index) => {
|
|
1196
|
-
const stored = storedSegments[index];
|
|
1197
|
-
return stored !== undefined && sameSegmentArtifacts(segment, stored);
|
|
1198
|
-
})) {
|
|
1199
|
-
this.#record = persisted;
|
|
1200
|
-
this.#persisted = true;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
catch {
|
|
1205
|
-
// Preserve the operation error. A failed diagnostic read cannot prove a different outcome.
|
|
1206
|
-
}
|
|
1207
|
-
throw error;
|
|
1208
|
-
}
|
|
1209
|
-
async #assertRetainedArtifactsUnchanged(blocks, segments) {
|
|
1210
|
-
for (const block of blocks) {
|
|
1211
|
-
if (!sameBytes(block.bytes, await this.store.getBlock(block.id))) {
|
|
1212
|
-
throw new Error(`Retried block differs from the staged artifact: ${block.id}`);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
for (const segment of segments) {
|
|
1216
|
-
const stored = await this.store.getSegment(segment.id);
|
|
1217
|
-
if (stored === undefined ||
|
|
1218
|
-
!sameSegmentArtifacts(stored, { ...segment, commitOrdinal: stored.commitOrdinal })) {
|
|
1219
|
-
throw new Error(`Retried segment differs from the staged artifact: ${segment.id}`);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1023
|
+
for (const segment of segments) {
|
|
1024
|
+
const stored = await this.store.getSegment(segment.id);
|
|
1025
|
+
if (stored === void 0 || !sameSegmentArtifacts(stored, { ...segment, commitOrdinal: stored.commitOrdinal })) {
|
|
1026
|
+
throw new Error(`Retried segment differs from the staged artifact: ${segment.id}`);
|
|
1027
|
+
}
|
|
1222
1028
|
}
|
|
1029
|
+
}
|
|
1223
1030
|
}
|
|
1224
1031
|
function sameStrings(left, right) {
|
|
1225
|
-
|
|
1032
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1226
1033
|
}
|
|
1227
1034
|
function sameBytes(left, right) {
|
|
1228
|
-
|
|
1035
|
+
return left.byteLength === right?.byteLength && left.every((byte, index) => byte === right[index]);
|
|
1229
1036
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
expiresAt: this.#transactionExpiry(),
|
|
1351
|
-
pendingBlockIds: [],
|
|
1352
|
-
pendingSegmentIds: [],
|
|
1353
|
-
status: "active",
|
|
1354
|
-
revision: 0,
|
|
1355
|
-
startedAt: timestamp,
|
|
1356
|
-
updatedAt: timestamp,
|
|
1357
|
-
committedVersion: null,
|
|
1358
|
-
},
|
|
1359
|
-
...(reserveRowIds === undefined ? {} : { reserveRowIds }),
|
|
1360
|
-
...(reserveAutoIncrement === undefined ? {} : { reserveAutoIncrement }),
|
|
1361
|
-
});
|
|
1362
|
-
break;
|
|
1363
|
-
}
|
|
1364
|
-
catch (error) {
|
|
1365
|
-
// The store's atomic begin aborted in full, including any counter reservation.
|
|
1366
|
-
if (error instanceof SnapshotManifestMissingError)
|
|
1367
|
-
continue;
|
|
1368
|
-
throw error;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
return {
|
|
1372
|
-
transaction: new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef),
|
|
1373
|
-
...(begun.rowIds === undefined ? {} : { rowIds: begun.rowIds }),
|
|
1374
|
-
...(begun.autoIncrementValues === undefined
|
|
1375
|
-
? {}
|
|
1376
|
-
: { autoIncrementValues: begun.autoIncrementValues }),
|
|
1377
|
-
};
|
|
1037
|
+
class TransactionManager {
|
|
1038
|
+
store;
|
|
1039
|
+
#now;
|
|
1040
|
+
#createId;
|
|
1041
|
+
#transactionTtlMs;
|
|
1042
|
+
#leaseCleanupCursor = null;
|
|
1043
|
+
#createWeakRef;
|
|
1044
|
+
constructor(store, options = {}) {
|
|
1045
|
+
this.store = store;
|
|
1046
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1047
|
+
this.#createId = options.createId ?? (() => crypto.randomUUID());
|
|
1048
|
+
this.#transactionTtlMs = options.transactionTtlMs ?? DEFAULT_TRANSACTION_TTL_MS;
|
|
1049
|
+
validateTtl(this.#transactionTtlMs);
|
|
1050
|
+
this.#createWeakRef = options.createWeakRef ?? ((transaction) => new WeakRef(transaction));
|
|
1051
|
+
}
|
|
1052
|
+
async begin() {
|
|
1053
|
+
return (await this.beginWithReservation()).transaction;
|
|
1054
|
+
}
|
|
1055
|
+
async beginWithPendingTable(record, expectedCatalogEpoch) {
|
|
1056
|
+
const id = this.#createId();
|
|
1057
|
+
const ownerId = `${id}/owner`;
|
|
1058
|
+
const timestamp = dateIsoString(this.#now());
|
|
1059
|
+
let begun;
|
|
1060
|
+
for (; ; ) {
|
|
1061
|
+
try {
|
|
1062
|
+
begun = await this.store.beginTransaction({
|
|
1063
|
+
record: {
|
|
1064
|
+
id,
|
|
1065
|
+
ownerId,
|
|
1066
|
+
expiresAt: this.#transactionExpiry(),
|
|
1067
|
+
pendingBlockIds: [],
|
|
1068
|
+
pendingSegmentIds: [],
|
|
1069
|
+
status: "active",
|
|
1070
|
+
revision: 0,
|
|
1071
|
+
startedAt: timestamp,
|
|
1072
|
+
updatedAt: timestamp,
|
|
1073
|
+
committedVersion: null
|
|
1074
|
+
},
|
|
1075
|
+
pendingTable: { record, nextRowId: 1n, expectedCatalogEpoch }
|
|
1076
|
+
});
|
|
1077
|
+
break;
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
if (error instanceof SnapshotManifestMissingError)
|
|
1080
|
+
continue;
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef);
|
|
1085
|
+
}
|
|
1086
|
+
async beginDeferred(options = {}) {
|
|
1087
|
+
const durableSnapshot = options.durableSnapshot ?? true;
|
|
1088
|
+
for (; ; ) {
|
|
1089
|
+
const probe = await this.store.getCatalogProbe();
|
|
1090
|
+
const timestamp = dateIsoString(this.#now());
|
|
1091
|
+
const id = this.#createId();
|
|
1092
|
+
const ownerId = `${id}/owner`;
|
|
1093
|
+
let snapshotLease;
|
|
1094
|
+
if (durableSnapshot) {
|
|
1095
|
+
try {
|
|
1096
|
+
snapshotLease = await this.openLeasedSnapshot({
|
|
1097
|
+
id: `${id}/snapshot`,
|
|
1098
|
+
ownerId,
|
|
1099
|
+
ttlMs: this.#transactionTtlMs,
|
|
1100
|
+
version: probe.manifestVersion
|
|
1101
|
+
});
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
if (error instanceof SnapshotManifestMissingError)
|
|
1104
|
+
continue;
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return new DatabaseTransaction(this.store, {
|
|
1109
|
+
id,
|
|
1110
|
+
ownerId,
|
|
1111
|
+
expiresAt: this.#transactionExpiry(),
|
|
1112
|
+
snapshotVersion: probe.manifestVersion,
|
|
1113
|
+
schemaEpochGuard: probe.schemaEpoch,
|
|
1114
|
+
pendingBlockIds: [],
|
|
1115
|
+
pendingSegmentIds: [],
|
|
1116
|
+
status: "active",
|
|
1117
|
+
revision: 0,
|
|
1118
|
+
startedAt: timestamp,
|
|
1119
|
+
updatedAt: timestamp,
|
|
1120
|
+
committedVersion: null
|
|
1121
|
+
}, this.#now, this.#transactionTtlMs, this.#createWeakRef, {
|
|
1122
|
+
persisted: false,
|
|
1123
|
+
initialCatalogProbe: probe,
|
|
1124
|
+
...snapshotLease === void 0 ? {} : { snapshotLease }
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async beginWithReservation(reserveRowIds, reserveAutoIncrement) {
|
|
1129
|
+
const id = this.#createId();
|
|
1130
|
+
const ownerId = `${id}/owner`;
|
|
1131
|
+
const timestamp = dateIsoString(this.#now());
|
|
1132
|
+
let begun;
|
|
1133
|
+
for (; ; ) {
|
|
1134
|
+
try {
|
|
1135
|
+
begun = await this.store.beginTransaction({
|
|
1136
|
+
record: {
|
|
1137
|
+
id,
|
|
1138
|
+
ownerId,
|
|
1139
|
+
expiresAt: this.#transactionExpiry(),
|
|
1140
|
+
pendingBlockIds: [],
|
|
1141
|
+
pendingSegmentIds: [],
|
|
1142
|
+
status: "active",
|
|
1143
|
+
revision: 0,
|
|
1144
|
+
startedAt: timestamp,
|
|
1145
|
+
updatedAt: timestamp,
|
|
1146
|
+
committedVersion: null
|
|
1147
|
+
},
|
|
1148
|
+
...reserveRowIds === void 0 ? {} : { reserveRowIds },
|
|
1149
|
+
...reserveAutoIncrement === void 0 ? {} : { reserveAutoIncrement }
|
|
1150
|
+
});
|
|
1151
|
+
break;
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
if (error instanceof SnapshotManifestMissingError)
|
|
1154
|
+
continue;
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1378
1157
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1158
|
+
return {
|
|
1159
|
+
transaction: new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef),
|
|
1160
|
+
...begun.rowIds === void 0 ? {} : { rowIds: begun.rowIds },
|
|
1161
|
+
...begun.autoIncrementValues === void 0 ? {} : { autoIncrementValues: begun.autoIncrementValues }
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
async resume(transactionId) {
|
|
1165
|
+
const record = await this.store.getTransaction(transactionId);
|
|
1166
|
+
if (record === void 0)
|
|
1167
|
+
throw new Error(`Transaction not found: ${transactionId}`);
|
|
1168
|
+
if (record.status !== "active")
|
|
1169
|
+
throw new TransactionClosedError(transactionId);
|
|
1170
|
+
const transaction = new DatabaseTransaction(this.store, record, this.#now, this.#transactionTtlMs, this.#createWeakRef);
|
|
1171
|
+
for (const segmentId of record.pendingSegmentIds) {
|
|
1172
|
+
const segment = await this.store.getSegment(segmentId);
|
|
1173
|
+
if (segment?.level === 0) {
|
|
1174
|
+
transaction.limitLevelZeroSegments(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
await transaction.renew();
|
|
1178
|
+
return transaction;
|
|
1179
|
+
}
|
|
1180
|
+
#transactionExpiry() {
|
|
1181
|
+
return expiryAfter(this.#now(), this.#transactionTtlMs);
|
|
1182
|
+
}
|
|
1183
|
+
async openSnapshot(version) {
|
|
1184
|
+
if (version === null)
|
|
1185
|
+
return new Snapshot(this.store, null);
|
|
1186
|
+
if (version !== void 0)
|
|
1187
|
+
return loadSnapshot(this.store, version);
|
|
1188
|
+
return new Snapshot(this.store, await this.store.getCurrentManifestVersion());
|
|
1189
|
+
}
|
|
1190
|
+
async openLeasedSnapshot(options) {
|
|
1191
|
+
validateTtl(options.ttlMs);
|
|
1192
|
+
if (options.ownerId.trim().length === 0)
|
|
1193
|
+
throw new TypeError("Lease owner cannot be empty");
|
|
1194
|
+
if (options.id?.trim().length === 0) {
|
|
1195
|
+
throw new TypeError("Lease ID cannot be empty");
|
|
1196
|
+
}
|
|
1197
|
+
const id = options.id ?? this.#createId();
|
|
1198
|
+
for (; ; ) {
|
|
1199
|
+
const snapshot = new Snapshot(this.store, options.version === void 0 ? await this.store.getCurrentManifestVersion() : options.version);
|
|
1200
|
+
const createdAt = dateIsoString(this.#now());
|
|
1201
|
+
const record = {
|
|
1202
|
+
id,
|
|
1203
|
+
kind: options.kind ?? "reader",
|
|
1204
|
+
manifestVersion: snapshot.version,
|
|
1205
|
+
ownerId: options.ownerId,
|
|
1206
|
+
createdAt,
|
|
1207
|
+
expiresAt: expiryAfter(new Date(createdAt), options.ttlMs),
|
|
1208
|
+
revision: 0
|
|
1209
|
+
};
|
|
1210
|
+
try {
|
|
1211
|
+
await this.store.createLease(record);
|
|
1212
|
+
return new LeasedSnapshot(this.store, snapshot.version, record, this.#now);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
if (options.version === void 0 && error instanceof SnapshotManifestMissingError) {
|
|
1215
|
+
continue;
|
|
1437
1216
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async moveLeasedSnapshot(snapshot, options) {
|
|
1222
|
+
validateTtl(options.ttlMs);
|
|
1223
|
+
try {
|
|
1224
|
+
const moved = await snapshot.moveTo(options.version, options.ttlMs);
|
|
1225
|
+
if (moved !== void 0)
|
|
1226
|
+
return moved;
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
if (!(error instanceof LeaseConflictError) && !(error instanceof LeaseExpiredError)) {
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
const replacement2 = await this.openLeasedSnapshot(options);
|
|
1232
|
+
snapshot.discard();
|
|
1233
|
+
return replacement2;
|
|
1234
|
+
}
|
|
1235
|
+
const replacement = await this.openLeasedSnapshot(options);
|
|
1236
|
+
await snapshot.release();
|
|
1237
|
+
return replacement;
|
|
1238
|
+
}
|
|
1239
|
+
async removeExpiredLeases(at = this.#now()) {
|
|
1240
|
+
const timestamp = dateMilliseconds(at);
|
|
1241
|
+
if (!Number.isFinite(timestamp))
|
|
1242
|
+
throw new TypeError("Lease cutoff must be a valid date");
|
|
1243
|
+
const removed = [];
|
|
1244
|
+
const cutoff = dateIsoString(at);
|
|
1245
|
+
const page = await this.store.listExpiredLeasePage(cutoff, this.#leaseCleanupCursor, 64);
|
|
1246
|
+
this.#leaseCleanupCursor = page.nextCursor;
|
|
1247
|
+
for (const lease of page.records) {
|
|
1248
|
+
try {
|
|
1249
|
+
if (await this.store.removeLeaseIfExpired(lease.id, lease.revision, cutoff)) {
|
|
1250
|
+
removed.push(lease.id);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (error instanceof LeaseConflictError)
|
|
1254
|
+
continue;
|
|
1255
|
+
throw error;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return removed;
|
|
1259
|
+
}
|
|
1260
|
+
async recover(options) {
|
|
1261
|
+
const staleBefore = dateMilliseconds(options.staleBefore);
|
|
1262
|
+
if (!Number.isFinite(staleBefore))
|
|
1263
|
+
throw new TypeError("Recovery cutoff must be a valid date");
|
|
1264
|
+
const abortedTransactionIds = [];
|
|
1265
|
+
const skippedTransactionIds = [];
|
|
1266
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1267
|
+
const segmentCandidates = /* @__PURE__ */ new Set();
|
|
1268
|
+
let transactionCursor = null;
|
|
1269
|
+
do {
|
|
1270
|
+
const page = await this.store.listTransactionPage(transactionCursor, 64);
|
|
1271
|
+
for (const record of page.records) {
|
|
1272
|
+
if (record.status !== "active" || Date.parse(record.expiresAt) > staleBefore)
|
|
1273
|
+
continue;
|
|
1449
1274
|
try {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1275
|
+
const aborted = await this.store.abortTransactionIfExpired({
|
|
1276
|
+
transactionId: record.id,
|
|
1277
|
+
expectedOwnerId: record.ownerId,
|
|
1278
|
+
expiresAtCutoff: dateIsoString(options.staleBefore),
|
|
1279
|
+
updatedAt: dateIsoString(this.#now())
|
|
1280
|
+
});
|
|
1281
|
+
if (aborted === void 0) {
|
|
1282
|
+
skippedTransactionIds.push(record.id);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
abortedTransactionIds.push(record.id);
|
|
1286
|
+
aborted.pendingBlockIds.forEach((id) => candidates.add(id));
|
|
1287
|
+
aborted.pendingSegmentIds.forEach((id) => segmentCandidates.add(id));
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (error instanceof TransactionRecordConflictError) {
|
|
1290
|
+
skippedTransactionIds.push(record.id);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
throw error;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
transactionCursor = page.nextCursor;
|
|
1297
|
+
} while (transactionCursor !== null);
|
|
1298
|
+
const removedBlockIds = [];
|
|
1299
|
+
const retainedBlockIds = [];
|
|
1300
|
+
const removedSegmentIds = [];
|
|
1301
|
+
const retainedSegmentIds = [];
|
|
1302
|
+
if (options.removePendingBlocks === false) {
|
|
1303
|
+
retainedBlockIds.push(...[...candidates].sort());
|
|
1304
|
+
retainedSegmentIds.push(...[...segmentCandidates].sort());
|
|
1305
|
+
} else if (candidates.size > 0 || segmentCandidates.size > 0) {
|
|
1306
|
+
const timestamp = dateIsoString(this.#now());
|
|
1307
|
+
const baseId = `recovery/${timestamp}/${this.#createId()}`;
|
|
1308
|
+
let suffix = 0;
|
|
1309
|
+
let job;
|
|
1310
|
+
for (; ; ) {
|
|
1311
|
+
const id = suffix === 0 ? baseId : `${baseId}/${String(suffix)}`;
|
|
1312
|
+
if (await this.store.getGarbageCollectionJob(id) !== void 0) {
|
|
1313
|
+
suffix += 1;
|
|
1314
|
+
continue;
|
|
1487
1315
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
if (record.status !== "active" || Date.parse(record.expiresAt) > staleBefore)
|
|
1503
|
-
continue;
|
|
1504
|
-
try {
|
|
1505
|
-
const aborted = await this.store.abortTransactionIfExpired({
|
|
1506
|
-
transactionId: record.id,
|
|
1507
|
-
expectedOwnerId: record.ownerId,
|
|
1508
|
-
expiresAtCutoff: dateIsoString(options.staleBefore),
|
|
1509
|
-
updatedAt: dateIsoString(this.#now()),
|
|
1510
|
-
});
|
|
1511
|
-
if (aborted === undefined) {
|
|
1512
|
-
skippedTransactionIds.push(record.id);
|
|
1513
|
-
continue;
|
|
1514
|
-
}
|
|
1515
|
-
abortedTransactionIds.push(record.id);
|
|
1516
|
-
aborted.pendingBlockIds.forEach((id) => candidates.add(id));
|
|
1517
|
-
aborted.pendingSegmentIds.forEach((id) => segmentCandidates.add(id));
|
|
1518
|
-
}
|
|
1519
|
-
catch (error) {
|
|
1520
|
-
if (error instanceof TransactionRecordConflictError) {
|
|
1521
|
-
skippedTransactionIds.push(record.id);
|
|
1522
|
-
continue;
|
|
1523
|
-
}
|
|
1524
|
-
throw error;
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
transactionCursor = page.nextCursor;
|
|
1528
|
-
} while (transactionCursor !== null);
|
|
1529
|
-
const removedBlockIds = [];
|
|
1530
|
-
const retainedBlockIds = [];
|
|
1531
|
-
const removedSegmentIds = [];
|
|
1532
|
-
const retainedSegmentIds = [];
|
|
1533
|
-
if (options.removePendingBlocks === false) {
|
|
1534
|
-
retainedBlockIds.push(...[...candidates].sort());
|
|
1535
|
-
retainedSegmentIds.push(...[...segmentCandidates].sort());
|
|
1536
|
-
}
|
|
1537
|
-
else if (candidates.size > 0 || segmentCandidates.size > 0) {
|
|
1538
|
-
const timestamp = dateIsoString(this.#now());
|
|
1539
|
-
const baseId = `recovery/${timestamp}/${this.#createId()}`;
|
|
1540
|
-
let suffix = 0;
|
|
1541
|
-
let job;
|
|
1542
|
-
for (;;) {
|
|
1543
|
-
const id = suffix === 0 ? baseId : `${baseId}/${String(suffix)}`;
|
|
1544
|
-
if ((await this.store.getGarbageCollectionJob(id)) !== undefined) {
|
|
1545
|
-
suffix += 1;
|
|
1546
|
-
continue;
|
|
1547
|
-
}
|
|
1548
|
-
try {
|
|
1549
|
-
job = await this.store.createGarbageCollectionJob({
|
|
1550
|
-
id,
|
|
1551
|
-
candidateManifestVersions: [],
|
|
1552
|
-
candidateSegmentIds: [...segmentCandidates],
|
|
1553
|
-
candidateBlockIds: [...candidates],
|
|
1554
|
-
leaseCutoff: timestamp,
|
|
1555
|
-
createdAt: timestamp,
|
|
1556
|
-
});
|
|
1557
|
-
break;
|
|
1558
|
-
}
|
|
1559
|
-
catch (error) {
|
|
1560
|
-
if ((await this.store.getGarbageCollectionJob(id)) === undefined)
|
|
1561
|
-
throw error;
|
|
1562
|
-
suffix += 1;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
while (job.state !== "completed") {
|
|
1566
|
-
try {
|
|
1567
|
-
const step = await this.store.runGarbageCollectionStep({
|
|
1568
|
-
jobId: job.id,
|
|
1569
|
-
expectedRevision: job.revision,
|
|
1570
|
-
maxItems: 128,
|
|
1571
|
-
updatedAt: dateIsoString(this.#now()),
|
|
1572
|
-
});
|
|
1573
|
-
removedBlockIds.push(...step.reclaimedBlockIds);
|
|
1574
|
-
retainedBlockIds.push(...step.retainedBlockIds);
|
|
1575
|
-
removedSegmentIds.push(...step.reclaimedSegmentIds);
|
|
1576
|
-
retainedSegmentIds.push(...step.retainedSegmentIds);
|
|
1577
|
-
job = step.job;
|
|
1578
|
-
}
|
|
1579
|
-
catch (error) {
|
|
1580
|
-
if (!(error instanceof GarbageCollectionJobConflictError))
|
|
1581
|
-
throw error;
|
|
1582
|
-
const latest = await this.store.getGarbageCollectionJob(job.id);
|
|
1583
|
-
if (latest === undefined)
|
|
1584
|
-
throw new Error(`Garbage collection job not found: ${job.id}`, { cause: error });
|
|
1585
|
-
job = latest;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1316
|
+
try {
|
|
1317
|
+
job = await this.store.createGarbageCollectionJob({
|
|
1318
|
+
id,
|
|
1319
|
+
candidateManifestVersions: [],
|
|
1320
|
+
candidateSegmentIds: [...segmentCandidates],
|
|
1321
|
+
candidateBlockIds: [...candidates],
|
|
1322
|
+
leaseCutoff: timestamp,
|
|
1323
|
+
createdAt: timestamp
|
|
1324
|
+
});
|
|
1325
|
+
break;
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
if (await this.store.getGarbageCollectionJob(id) === void 0)
|
|
1328
|
+
throw error;
|
|
1329
|
+
suffix += 1;
|
|
1588
1330
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1331
|
+
}
|
|
1332
|
+
while (job.state !== "completed") {
|
|
1333
|
+
try {
|
|
1334
|
+
const step = await this.store.runGarbageCollectionStep({
|
|
1335
|
+
jobId: job.id,
|
|
1336
|
+
expectedRevision: job.revision,
|
|
1337
|
+
maxItems: 128,
|
|
1338
|
+
updatedAt: dateIsoString(this.#now())
|
|
1339
|
+
});
|
|
1340
|
+
removedBlockIds.push(...step.reclaimedBlockIds);
|
|
1341
|
+
retainedBlockIds.push(...step.retainedBlockIds);
|
|
1342
|
+
removedSegmentIds.push(...step.reclaimedSegmentIds);
|
|
1343
|
+
retainedSegmentIds.push(...step.retainedSegmentIds);
|
|
1344
|
+
job = step.job;
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
if (!(error instanceof GarbageCollectionJobConflictError))
|
|
1347
|
+
throw error;
|
|
1348
|
+
const latest = await this.store.getGarbageCollectionJob(job.id);
|
|
1349
|
+
if (latest === void 0)
|
|
1350
|
+
throw new Error(`Garbage collection job not found: ${job.id}`, { cause: error });
|
|
1351
|
+
job = latest;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
abortedTransactionIds: abortedTransactionIds.sort(),
|
|
1357
|
+
skippedTransactionIds: skippedTransactionIds.sort(),
|
|
1358
|
+
removedBlockIds,
|
|
1359
|
+
retainedBlockIds,
|
|
1360
|
+
removedSegmentIds,
|
|
1361
|
+
retainedSegmentIds
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1598
1364
|
}
|
|
1599
1365
|
function validateTtl(ttlMs) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1366
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_LEASE_TTL_MS) {
|
|
1367
|
+
throw new RangeError(`Lease lifetime must be a positive number no greater than ${String(MAX_LEASE_TTL_MS)} milliseconds`);
|
|
1368
|
+
}
|
|
1603
1369
|
}
|
|
1604
1370
|
function expiryAfter(now, ttlMs) {
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1371
|
+
validateTtl(ttlMs);
|
|
1372
|
+
const expiresAt = dateMilliseconds(now) + ttlMs;
|
|
1373
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt > 864e13) {
|
|
1374
|
+
throw new RangeError("Lease expiry is outside the supported Date range");
|
|
1375
|
+
}
|
|
1376
|
+
return dateIsoString(new Date(expiresAt));
|
|
1611
1377
|
}
|
|
1612
1378
|
function advanceRevision(revision, amount) {
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
}
|
|
1618
|
-
return revision + amount;
|
|
1379
|
+
if (!Number.isSafeInteger(revision) || revision < 0 || revision > Number.MAX_SAFE_INTEGER - amount) {
|
|
1380
|
+
throw new RangeError("Transaction revision is outside the safe integer range");
|
|
1381
|
+
}
|
|
1382
|
+
return revision + amount;
|
|
1619
1383
|
}
|
|
1620
1384
|
function checkpointByteSum(total, addition) {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1385
|
+
const next = total + addition;
|
|
1386
|
+
if (!Number.isSafeInteger(next)) {
|
|
1387
|
+
throw new RangeError("Transaction checkpoint size exceeds the safe integer range");
|
|
1388
|
+
}
|
|
1389
|
+
return next;
|
|
1626
1390
|
}
|
|
1627
1391
|
function safeWholeNumberSum(values, label) {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1392
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
1393
|
+
if (!Number.isSafeInteger(total) || total < 0) {
|
|
1394
|
+
throw new RangeError(`${label} exceeds the safe integer range`);
|
|
1395
|
+
}
|
|
1396
|
+
return total;
|
|
1633
1397
|
}
|
|
1634
|
-
/** Linear merge of two canonical postings for one term. */
|
|
1635
1398
|
function mergeFtsPostingRows(left, right) {
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1399
|
+
if (left.term !== right.term)
|
|
1400
|
+
throw new TypeError("Posting terms differ during transaction merge");
|
|
1401
|
+
const rowIds = [];
|
|
1402
|
+
const tf = [];
|
|
1403
|
+
let leftIndex = 0;
|
|
1404
|
+
let rightIndex = 0;
|
|
1405
|
+
const push = (rowId, frequency) => {
|
|
1406
|
+
const previous = rowIds[rowIds.length - 1];
|
|
1407
|
+
if (previous === rowId) {
|
|
1408
|
+
tf[tf.length - 1] = Math.max(tf[tf.length - 1] ?? 1, frequency);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
rowIds.push(rowId);
|
|
1412
|
+
tf.push(frequency);
|
|
1413
|
+
};
|
|
1414
|
+
while (leftIndex < left.rowIds.length && rightIndex < right.rowIds.length) {
|
|
1415
|
+
const leftRowId = left.rowIds[leftIndex] ?? 0n;
|
|
1416
|
+
const rightRowId = right.rowIds[rightIndex] ?? 0n;
|
|
1417
|
+
if (leftRowId <= rightRowId) {
|
|
1418
|
+
push(leftRowId, left.tf[leftIndex] ?? 1);
|
|
1419
|
+
leftIndex += 1;
|
|
1420
|
+
}
|
|
1421
|
+
if (rightRowId <= leftRowId) {
|
|
1422
|
+
push(rightRowId, right.tf[rightIndex] ?? 1);
|
|
1423
|
+
rightIndex += 1;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
while (leftIndex < left.rowIds.length) {
|
|
1427
|
+
push(left.rowIds[leftIndex] ?? 0n, left.tf[leftIndex] ?? 1);
|
|
1428
|
+
leftIndex += 1;
|
|
1429
|
+
}
|
|
1430
|
+
while (rightIndex < right.rowIds.length) {
|
|
1431
|
+
push(right.rowIds[rightIndex] ?? 0n, right.tf[rightIndex] ?? 1);
|
|
1432
|
+
rightIndex += 1;
|
|
1433
|
+
}
|
|
1434
|
+
return { term: left.term, rowIds, tf };
|
|
1672
1435
|
}
|
|
1673
1436
|
function postingTokenCount(posting) {
|
|
1674
|
-
|
|
1437
|
+
return safeWholeNumberSum(posting.tf, "Posting token count");
|
|
1675
1438
|
}
|
|
1676
1439
|
function requiredCompactionJobId(id) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1440
|
+
if (id === null)
|
|
1441
|
+
throw new Error("Compaction source blocks require a compaction job");
|
|
1442
|
+
return id;
|
|
1680
1443
|
}
|
|
1681
1444
|
function sameSegmentArtifacts(left, right) {
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
left.partitionOrdinal === right.partitionOrdinal &&
|
|
1697
|
-
left.createdAt === right.createdAt &&
|
|
1698
|
-
leftColumns.length === rightColumns.length &&
|
|
1699
|
-
leftColumns.every((columnId, index) => {
|
|
1700
|
-
if (columnId !== rightColumns[index])
|
|
1701
|
-
return false;
|
|
1702
|
-
const leftIds = left.columnBlockIds[columnId] ?? [];
|
|
1703
|
-
const rightIds = right.columnBlockIds[columnId] ?? [];
|
|
1704
|
-
return (leftIds.length === rightIds.length && leftIds.every((id, item) => id === rightIds[item]));
|
|
1705
|
-
}) &&
|
|
1706
|
-
leftSpans.length === rightSpans.length &&
|
|
1707
|
-
leftSpans.every((span, index) => {
|
|
1708
|
-
const other = rightSpans[index];
|
|
1709
|
-
return (span.rowStart === other?.rowStart &&
|
|
1710
|
-
span.rowCount === other.rowCount &&
|
|
1711
|
-
span.rowIdStart === other.rowIdStart);
|
|
1712
|
-
}));
|
|
1445
|
+
const leftColumns = Object.keys(left.columnBlockIds).sort();
|
|
1446
|
+
const rightColumns = Object.keys(right.columnBlockIds).sort();
|
|
1447
|
+
const leftSpans = left.rowIdSpans;
|
|
1448
|
+
const rightSpans = right.rowIdSpans;
|
|
1449
|
+
return left.id === right.id && left.tableId === right.tableId && left.rowCount === right.rowCount && left.rowIdStart === right.rowIdStart && left.rowIdEndExclusive === right.rowIdEndExclusive && left.kind === right.kind && left.keyColumnId === right.keyColumnId && left.level === right.level && left.logicalOrder === right.logicalOrder && left.commitOrdinal === right.commitOrdinal && left.partitionOrdinal === right.partitionOrdinal && left.createdAt === right.createdAt && leftColumns.length === rightColumns.length && leftColumns.every((columnId, index) => {
|
|
1450
|
+
if (columnId !== rightColumns[index])
|
|
1451
|
+
return false;
|
|
1452
|
+
const leftIds = left.columnBlockIds[columnId] ?? [];
|
|
1453
|
+
const rightIds = right.columnBlockIds[columnId] ?? [];
|
|
1454
|
+
return leftIds.length === rightIds.length && leftIds.every((id, item) => id === rightIds[item]);
|
|
1455
|
+
}) && leftSpans.length === rightSpans.length && leftSpans.every((span, index) => {
|
|
1456
|
+
const other = rightSpans[index];
|
|
1457
|
+
return span.rowStart === other?.rowStart && span.rowCount === other.rowCount && span.rowIdStart === other.rowIdStart;
|
|
1458
|
+
});
|
|
1713
1459
|
}
|
|
1714
1460
|
function transactionBlockBatches(blocks) {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
return batches;
|
|
1461
|
+
const batches = [];
|
|
1462
|
+
let batch = [];
|
|
1463
|
+
let batchBytes = 0;
|
|
1464
|
+
for (const block of blocks) {
|
|
1465
|
+
assertTransactionArtifactBatchLimits([block], []);
|
|
1466
|
+
if (batch.length === MAX_TRANSACTION_STAGE_BLOCKS || batchBytes + block.bytes.byteLength > MAX_TRANSACTION_STAGE_BYTES) {
|
|
1467
|
+
batches.push(batch);
|
|
1468
|
+
batch = [];
|
|
1469
|
+
batchBytes = 0;
|
|
1470
|
+
}
|
|
1471
|
+
batch.push(block);
|
|
1472
|
+
batchBytes += block.bytes.byteLength;
|
|
1473
|
+
}
|
|
1474
|
+
if (batch.length > 0)
|
|
1475
|
+
batches.push(batch);
|
|
1476
|
+
return batches;
|
|
1732
1477
|
}
|
|
1733
1478
|
function transactionArtifactBatches(blocks, segments) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1754
|
-
return batches;
|
|
1479
|
+
let blockBytes = 0;
|
|
1480
|
+
for (const block of blocks) {
|
|
1481
|
+
assertTransactionArtifactBatchLimits([block], []);
|
|
1482
|
+
blockBytes += block.bytes.byteLength;
|
|
1483
|
+
}
|
|
1484
|
+
if (blocks.length <= MAX_TRANSACTION_STAGE_BLOCKS && segments.length <= MAX_TRANSACTION_STAGE_SEGMENTS && blockBytes <= MAX_TRANSACTION_STAGE_BYTES) {
|
|
1485
|
+
return [{ blocks, segments }];
|
|
1486
|
+
}
|
|
1487
|
+
const batches = transactionBlockBatches(blocks).map((batch) => ({
|
|
1488
|
+
blocks: batch,
|
|
1489
|
+
segments: []
|
|
1490
|
+
}));
|
|
1491
|
+
for (let start = 0; start < segments.length; start += MAX_TRANSACTION_STAGE_SEGMENTS) {
|
|
1492
|
+
batches.push({
|
|
1493
|
+
blocks: [],
|
|
1494
|
+
segments: segments.slice(start, start + MAX_TRANSACTION_STAGE_SEGMENTS)
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return batches;
|
|
1755
1498
|
}
|
|
1756
1499
|
async function loadSnapshot(store, version) {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1500
|
+
if (version === null)
|
|
1501
|
+
return new Snapshot(store, null);
|
|
1502
|
+
const manifest = await store.getManifest(version);
|
|
1503
|
+
if (manifest === void 0 || manifest.prunedAt !== void 0) {
|
|
1504
|
+
throw new SnapshotManifestMissingError(version);
|
|
1505
|
+
}
|
|
1506
|
+
return new Snapshot(store, manifest.version);
|
|
1764
1507
|
}
|
|
1508
|
+
export {
|
|
1509
|
+
DEFAULT_TRANSACTION_TTL_MS,
|
|
1510
|
+
DatabaseTransaction,
|
|
1511
|
+
LeasedSnapshot,
|
|
1512
|
+
MAX_LEASE_TTL_MS,
|
|
1513
|
+
Snapshot,
|
|
1514
|
+
TransactionClosedError,
|
|
1515
|
+
TransactionManager
|
|
1516
|
+
};
|