@minnowdb/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -18
- package/dist/engine/client.d.ts +9 -1
- package/dist/engine/client.d.ts.map +1 -1
- package/dist/engine/client.js +29 -5
- package/dist/engine/client.js.map +1 -1
- package/dist/engine/database.d.ts +51 -6
- package/dist/engine/database.d.ts.map +1 -1
- package/dist/engine/database.js +2597 -865
- package/dist/engine/database.js.map +1 -1
- package/dist/engine/live.d.ts.map +1 -1
- package/dist/engine/live.js +35 -11
- package/dist/engine/live.js.map +1 -1
- package/dist/engine/query-cache.d.ts +13 -1
- package/dist/engine/query-cache.d.ts.map +1 -1
- package/dist/engine/query-cache.js +34 -5
- package/dist/engine/query-cache.js.map +1 -1
- package/dist/engine/query.d.ts +11 -1
- package/dist/engine/query.d.ts.map +1 -1
- package/dist/engine/query.js +49 -41
- package/dist/engine/query.js.map +1 -1
- package/dist/engine/result-wire.d.ts +68 -0
- package/dist/engine/result-wire.d.ts.map +1 -0
- package/dist/engine/result-wire.js +264 -0
- package/dist/engine/result-wire.js.map +1 -0
- package/dist/engine/sort-keys.d.ts +50 -12
- package/dist/engine/sort-keys.d.ts.map +1 -1
- package/dist/engine/sort-keys.js +371 -25
- package/dist/engine/sort-keys.js.map +1 -1
- package/dist/engine/vector.d.ts +11 -0
- package/dist/engine/vector.d.ts.map +1 -1
- package/dist/engine/vector.js +99 -203
- package/dist/engine/vector.js.map +1 -1
- package/dist/engine/worker-host.d.ts +17 -1
- package/dist/engine/worker-host.d.ts.map +1 -1
- package/dist/engine/worker-host.js +83 -13
- package/dist/engine/worker-host.js.map +1 -1
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/indexeddb.d.ts +5 -1
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +484 -402
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/memory.d.ts +24 -21
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +124 -1199
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/opfs/files.d.ts +64 -0
- package/dist/storage/opfs/files.d.ts.map +1 -0
- package/dist/storage/opfs/files.js +243 -0
- package/dist/storage/opfs/files.js.map +1 -0
- package/dist/storage/opfs/index.d.ts +3 -0
- package/dist/storage/opfs/index.d.ts.map +1 -0
- package/dist/storage/opfs/index.js +3 -0
- package/dist/storage/opfs/index.js.map +1 -0
- package/dist/storage/opfs/leader.d.ts +130 -0
- package/dist/storage/opfs/leader.d.ts.map +1 -0
- package/dist/storage/opfs/leader.js +1104 -0
- package/dist/storage/opfs/leader.js.map +1 -0
- package/dist/storage/opfs/rpc.d.ts +70 -0
- package/dist/storage/opfs/rpc.d.ts.map +1 -0
- package/dist/storage/opfs/rpc.js +58 -0
- package/dist/storage/opfs/rpc.js.map +1 -0
- package/dist/storage/opfs/store.d.ts +154 -0
- package/dist/storage/opfs/store.d.ts.map +1 -0
- package/dist/storage/opfs/store.js +958 -0
- package/dist/storage/opfs/store.js.map +1 -0
- package/dist/storage/toolkit/extents.d.ts +69 -0
- package/dist/storage/toolkit/extents.d.ts.map +1 -0
- package/dist/storage/toolkit/extents.js +175 -0
- package/dist/storage/toolkit/extents.js.map +1 -0
- package/dist/storage/toolkit/index.d.ts +30 -0
- package/dist/storage/toolkit/index.d.ts.map +1 -0
- package/dist/storage/toolkit/index.js +5 -0
- package/dist/storage/toolkit/index.js.map +1 -0
- package/dist/storage/toolkit/record-core.d.ts +252 -0
- package/dist/storage/toolkit/record-core.d.ts.map +1 -0
- package/dist/storage/toolkit/record-core.js +1670 -0
- package/dist/storage/toolkit/record-core.js.map +1 -0
- package/dist/storage/toolkit/sync-file.d.ts +29 -0
- package/dist/storage/toolkit/sync-file.d.ts.map +1 -0
- package/dist/storage/toolkit/sync-file.js +2 -0
- package/dist/storage/toolkit/sync-file.js.map +1 -0
- package/dist/storage/toolkit/wal.d.ts +22 -0
- package/dist/storage/toolkit/wal.d.ts.map +1 -0
- package/dist/storage/toolkit/wal.js +79 -0
- package/dist/storage/toolkit/wal.js.map +1 -0
- package/dist/storage/toolkit/wire.d.ts +25 -0
- package/dist/storage/toolkit/wire.d.ts.map +1 -0
- package/dist/storage/toolkit/wire.js +100 -0
- package/dist/storage/toolkit/wire.js.map +1 -0
- package/dist/storage/types.d.ts +343 -59
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +112 -11
- package/dist/storage/types.js.map +1 -1
- package/dist/testing/block-store-conformance.d.ts +51 -0
- package/dist/testing/block-store-conformance.d.ts.map +1 -0
- package/dist/testing/block-store-conformance.js +745 -0
- package/dist/testing/block-store-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/opfs-shim.d.ts +35 -0
- package/dist/testing/opfs-shim.d.ts.map +1 -0
- package/dist/testing/opfs-shim.js +295 -0
- package/dist/testing/opfs-shim.js.map +1 -0
- package/dist/transactions/index.d.ts +42 -1
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +219 -13
- package/dist/transactions/index.js.map +1 -1
- package/dist/worker-protocol/index.d.ts +2 -2
- package/dist/worker-protocol/index.d.ts.map +1 -1
- package/dist/worker-protocol/index.js +2 -1
- package/dist/worker-protocol/index.js.map +1 -1
- package/package.json +10 -2
- package/dist/engine/dsl/db.d.ts +0 -137
- package/dist/engine/dsl/db.d.ts.map +0 -1
- package/dist/engine/dsl/db.js +0 -161
- package/dist/engine/dsl/db.js.map +0 -1
- package/dist/engine/dsl/expression.d.ts +0 -172
- package/dist/engine/dsl/expression.d.ts.map +0 -1
- package/dist/engine/dsl/expression.js +0 -397
- package/dist/engine/dsl/expression.js.map +0 -1
- package/dist/engine/dsl/index.d.ts +0 -8
- package/dist/engine/dsl/index.d.ts.map +0 -1
- package/dist/engine/dsl/index.js +0 -7
- package/dist/engine/dsl/index.js.map +0 -1
- package/dist/engine/dsl/live-query.d.ts +0 -45
- package/dist/engine/dsl/live-query.d.ts.map +0 -1
- package/dist/engine/dsl/live-query.js +0 -116
- package/dist/engine/dsl/live-query.js.map +0 -1
- package/dist/engine/dsl/mutations.d.ts +0 -123
- package/dist/engine/dsl/mutations.d.ts.map +0 -1
- package/dist/engine/dsl/mutations.js +0 -241
- package/dist/engine/dsl/mutations.js.map +0 -1
- package/dist/engine/dsl/select-query-builder.d.ts +0 -148
- package/dist/engine/dsl/select-query-builder.d.ts.map +0 -1
- package/dist/engine/dsl/select-query-builder.js +0 -377
- package/dist/engine/dsl/select-query-builder.js.map +0 -1
- package/dist/engine/dsl/sql-tag.d.ts +0 -46
- package/dist/engine/dsl/sql-tag.d.ts.map +0 -1
- package/dist/engine/dsl/sql-tag.js +0 -85
- package/dist/engine/dsl/sql-tag.js.map +0 -1
- package/dist/engine/dsl/types.d.ts +0 -133
- package/dist/engine/dsl/types.d.ts.map +0 -1
- package/dist/engine/dsl/types.js +0 -9
- package/dist/engine/dsl/types.js.map +0 -1
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
import { collectFtsCandidates, } from "../types.js";
|
|
2
|
+
import { RecordCore, validateBeginTransactionInput, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "../toolkit/record-core.js";
|
|
3
|
+
import { encodeSegment, decodeSegment } from "./files.js";
|
|
4
|
+
import { decodeChunk, decodeSyncCheckpoint, encodeChunk, encodeSyncCheckpoint, } from "../toolkit/wire.js";
|
|
5
|
+
import { WalWriter, replayWalFrames } from "../toolkit/wal.js";
|
|
6
|
+
import { ExtentPool } from "../toolkit/extents.js";
|
|
7
|
+
/** Checkpoint when the WAL passes either bound; both trade replay time against pause time. */
|
|
8
|
+
const CHECKPOINT_WAL_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
const CHECKPOINT_ENTRIES = 1024;
|
|
10
|
+
/** Decoded full-text base chunks kept in memory; least recently loaded goes first. */
|
|
11
|
+
const FTS_CHUNK_CACHE_SIZE = 64;
|
|
12
|
+
/**
|
|
13
|
+
* The leader: the one instance holding the database's write-ahead log handle, and with it
|
|
14
|
+
* every other handle — checkpoint slots, the tail extent, a small cache of sealed extents.
|
|
15
|
+
*
|
|
16
|
+
* Because the handles are held, every operation's storage cost is a synchronous write or read
|
|
17
|
+
* measured in microseconds, and because leadership is exclusive by the browser's own file
|
|
18
|
+
* lock, the leader's in-memory `RecordCore` is always provably current — there are no
|
|
19
|
+
* freshness probes anywhere. An operation is: validate and mutate the core (synchronously),
|
|
20
|
+
* append one checksummed WAL frame (synchronously), acknowledge. The only awaits sit before
|
|
21
|
+
* that critical section (extent appends that may seal, temp-file IO), so no observer ever
|
|
22
|
+
* sees state mid-mutation.
|
|
23
|
+
*
|
|
24
|
+
* Checkpointing is also synchronous — dump, write to the older of two fixed slots, flush,
|
|
25
|
+
* reset the WAL — which is what makes a single WAL file safe: the flush precedes the reset,
|
|
26
|
+
* and a crash mid-checkpoint corrupts only the slot being written while the other slot plus
|
|
27
|
+
* the un-reset WAL still reconstruct everything.
|
|
28
|
+
*/
|
|
29
|
+
export class OpfsLeader {
|
|
30
|
+
#tree;
|
|
31
|
+
#strict;
|
|
32
|
+
#core;
|
|
33
|
+
#blockIndex = new Map();
|
|
34
|
+
#ftsBases = new Map();
|
|
35
|
+
#ftsChunkCache = new Map();
|
|
36
|
+
#extents;
|
|
37
|
+
#wal;
|
|
38
|
+
/** The pool exists from the moment recovery succeeds; before that, nothing may touch it. */
|
|
39
|
+
get #pool() {
|
|
40
|
+
const pool = this.#extents;
|
|
41
|
+
if (pool === undefined)
|
|
42
|
+
throw new Error("The OPFS leader has not recovered yet");
|
|
43
|
+
return pool;
|
|
44
|
+
}
|
|
45
|
+
#walHandle;
|
|
46
|
+
#slots;
|
|
47
|
+
/** The slot holding the newest checkpoint; the next checkpoint writes to the other. */
|
|
48
|
+
#newestSlot = 0;
|
|
49
|
+
#seq = 0;
|
|
50
|
+
#entriesSinceCheckpoint = 0;
|
|
51
|
+
#permissivePhysical = false;
|
|
52
|
+
#poisoned = false;
|
|
53
|
+
#closed = false;
|
|
54
|
+
#chain = Promise.resolve();
|
|
55
|
+
#checkpointEntries;
|
|
56
|
+
#lastCheckpointBytes = 0;
|
|
57
|
+
#checkpointScheduled = false;
|
|
58
|
+
constructor(tree, strict, walHandle, slots, checkpointEntries) {
|
|
59
|
+
this.#checkpointEntries = checkpointEntries ?? CHECKPOINT_ENTRIES;
|
|
60
|
+
this.#tree = tree;
|
|
61
|
+
this.#strict = strict;
|
|
62
|
+
this.#walHandle = walHandle;
|
|
63
|
+
this.#slots = slots;
|
|
64
|
+
this.#wal = new WalWriter(walHandle, 0);
|
|
65
|
+
this.#core = new RecordCore({
|
|
66
|
+
hasBlock: (id) => this.#permissivePhysical || this.#blockIndex.has(id),
|
|
67
|
+
blockByteLength: (id) => this.#blockIndex.get(id)?.length ?? (this.#permissivePhysical ? 0 : undefined),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Builds a leader from held handles: newest valid checkpoint slot plus the WAL tail. */
|
|
71
|
+
static async recover(tree, strict, handles, checkpointEntries) {
|
|
72
|
+
const leader = new OpfsLeader(tree, strict, handles.wal, [handles.slotA, handles.slotB], checkpointEntries);
|
|
73
|
+
try {
|
|
74
|
+
await leader.#loadFromDisk();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// A failed recovery must release every handle it acquired, or the browser's file lock
|
|
78
|
+
// outlives the attempt and no connection can ever lead this database again.
|
|
79
|
+
leader.crash();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return leader;
|
|
83
|
+
}
|
|
84
|
+
async #loadFromDisk() {
|
|
85
|
+
const states = this.#slots.map((slot) => {
|
|
86
|
+
const size = slot.getSize();
|
|
87
|
+
const bytes = new Uint8Array(size);
|
|
88
|
+
slot.read(bytes, { at: 0 });
|
|
89
|
+
return decodeSyncCheckpoint(bytes);
|
|
90
|
+
});
|
|
91
|
+
let checkpoint;
|
|
92
|
+
this.#newestSlot = 0;
|
|
93
|
+
states.forEach((state, index) => {
|
|
94
|
+
if (state !== undefined && (checkpoint === undefined || state.lastSeq > checkpoint.lastSeq)) {
|
|
95
|
+
checkpoint = state;
|
|
96
|
+
this.#newestSlot = index;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.#blockIndex.clear();
|
|
100
|
+
this.#ftsBases.clear();
|
|
101
|
+
this.#ftsChunkCache.clear();
|
|
102
|
+
this.#extents?.close();
|
|
103
|
+
this.#extents = await ExtentPool.open(this.#tree, checkpoint?.extents);
|
|
104
|
+
if (checkpoint === undefined) {
|
|
105
|
+
this.#core.load(EMPTY_CORE_STATE());
|
|
106
|
+
this.#seq = 0;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.#core.load(checkpoint.core);
|
|
110
|
+
for (const [id, placement] of checkpoint.blockIndex)
|
|
111
|
+
this.#blockIndex.set(id, placement);
|
|
112
|
+
for (const [key, pointer] of checkpoint.ftsBases)
|
|
113
|
+
this.#ftsBases.set(key, pointer);
|
|
114
|
+
this.#seq = checkpoint.lastSeq;
|
|
115
|
+
}
|
|
116
|
+
const newest = this.#slots[this.#newestSlot];
|
|
117
|
+
this.#lastCheckpointBytes = newest === undefined ? 0 : newest.getSize();
|
|
118
|
+
const { payloads, endOffset } = replayWalFrames(this.#walHandle);
|
|
119
|
+
let applied = 0;
|
|
120
|
+
let tailExtent = -1;
|
|
121
|
+
let tailEnd = 0;
|
|
122
|
+
for (const payload of payloads) {
|
|
123
|
+
const entry = payload;
|
|
124
|
+
if (entry.seq <= this.#seq)
|
|
125
|
+
continue; // Covered by the checkpoint; reset had not run yet.
|
|
126
|
+
applied += 1;
|
|
127
|
+
this.#applyReplayed(entry);
|
|
128
|
+
for (const placement of placementsOf(entry)) {
|
|
129
|
+
this.#pool.restorePlacement(placement);
|
|
130
|
+
if (placement.extent > tailExtent ||
|
|
131
|
+
(placement.extent === tailExtent && placement.offset + placement.length > tailEnd)) {
|
|
132
|
+
tailExtent = placement.extent;
|
|
133
|
+
tailEnd = placement.offset + placement.length;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.#seq = entry.seq;
|
|
137
|
+
}
|
|
138
|
+
if (tailExtent > this.#pool.tailExtentId) {
|
|
139
|
+
await this.#pool.adoptTail(tailExtent, tailEnd);
|
|
140
|
+
}
|
|
141
|
+
this.#wal = new WalWriter(this.#walHandle, endOffset);
|
|
142
|
+
this.#entriesSinceCheckpoint = applied;
|
|
143
|
+
this.#poisoned = false;
|
|
144
|
+
// Extents fully drained before the crash but never deleted: finish the job.
|
|
145
|
+
for (const id of this.#pool.release([]))
|
|
146
|
+
await this.#pool.deleteExtent(id);
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------------------
|
|
149
|
+
// The logged-operation spine.
|
|
150
|
+
// ---------------------------------------------------------------------------------------
|
|
151
|
+
#run(work) {
|
|
152
|
+
const result = this.#chain.then(async () => {
|
|
153
|
+
if (this.#closed)
|
|
154
|
+
throw new Error("This OPFS store connection is closed");
|
|
155
|
+
if (this.#poisoned)
|
|
156
|
+
await this.#loadFromDisk();
|
|
157
|
+
return work();
|
|
158
|
+
});
|
|
159
|
+
this.#chain = result.catch(() => undefined);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Reads answer from memory without the queue — but never from a poisoned instance, whose
|
|
164
|
+
* memory ran ahead of a write the disk refused. The fast path is one boolean check; the
|
|
165
|
+
* poisoned path rides the queue, which reloads from the held handles first.
|
|
166
|
+
*/
|
|
167
|
+
async #healthy() {
|
|
168
|
+
if (!this.#poisoned)
|
|
169
|
+
return;
|
|
170
|
+
await this.#run(() => undefined);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The critical section: apply to the core and append the frame in one synchronous run.
|
|
174
|
+
* A validation throw leaves the core untouched (validate-then-mutate bodies); a WAL write
|
|
175
|
+
* failure after mutation poisons the leader, which reloads from its own handles before the
|
|
176
|
+
* next operation, and the original error — quota, most importantly — escapes unwrapped.
|
|
177
|
+
*/
|
|
178
|
+
#logged(body) {
|
|
179
|
+
const result = this.#applyBody(body);
|
|
180
|
+
this.#appendFrame(body);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
#appendFrame(body) {
|
|
184
|
+
try {
|
|
185
|
+
this.#seq += 1;
|
|
186
|
+
this.#wal.append({ seq: this.#seq, ...body }, this.#strict);
|
|
187
|
+
this.#entriesSinceCheckpoint += 1;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.#poisoned = true;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
// The checkpoint runs as its own queued step: the operation that trips the threshold is
|
|
194
|
+
// already durable and should not absorb the pause — or a checkpoint failure. A failed
|
|
195
|
+
// checkpoint (quota, most plausibly) leaves the WAL covering everything and retries on a
|
|
196
|
+
// later trigger; the operations themselves stay correct throughout.
|
|
197
|
+
if (this.#checkpointDue() && !this.#checkpointScheduled) {
|
|
198
|
+
this.#checkpointScheduled = true;
|
|
199
|
+
void this.#run(() => {
|
|
200
|
+
this.#checkpointScheduled = false;
|
|
201
|
+
if (this.#checkpointDue())
|
|
202
|
+
this.checkpointNow();
|
|
203
|
+
}).catch(() => undefined);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Due when the WAL outgrows the larger of the floor and the previous checkpoint's own size —
|
|
208
|
+
* so checkpointing amortizes to O(bytes written) instead of re-serializing the whole state
|
|
209
|
+
* every fixed interval during a bulk load — or when the frame count alone would make a
|
|
210
|
+
* replay slow.
|
|
211
|
+
*/
|
|
212
|
+
#checkpointDue() {
|
|
213
|
+
return (this.#wal.byteLength >= Math.max(CHECKPOINT_WAL_BYTES, this.#lastCheckpointBytes) ||
|
|
214
|
+
this.#entriesSinceCheckpoint >= this.#checkpointEntries);
|
|
215
|
+
}
|
|
216
|
+
#applyBody(body) {
|
|
217
|
+
switch (body.op) {
|
|
218
|
+
case "addBlocks": {
|
|
219
|
+
for (const { id, placement } of body.placements)
|
|
220
|
+
this.#blockIndex.set(id, placement);
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
case "removeBlock": {
|
|
224
|
+
const placement = this.#blockIndex.get(body.id);
|
|
225
|
+
this.#blockIndex.delete(body.id);
|
|
226
|
+
if (placement !== undefined)
|
|
227
|
+
this.#pool.release([placement]);
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
case "relocatePayloads": {
|
|
231
|
+
// Validate the complete move before changing an index: a stale relocation must leave
|
|
232
|
+
// both indexes and live-byte accounting untouched.
|
|
233
|
+
for (const move of body.blocks) {
|
|
234
|
+
if (!samePlacement(this.#blockIndex.get(move.id), move.from)) {
|
|
235
|
+
throw new Error(`OPFS block moved before relocation: ${move.id}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const move of body.ftsChunks) {
|
|
239
|
+
if (!samePlacement(this.#ftsBases.get(move.key)?.chunks[move.ordinal], move.from)) {
|
|
240
|
+
throw new Error(`OPFS full-text chunk moved before relocation: ${move.key}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const move of body.blocks)
|
|
244
|
+
this.#blockIndex.set(move.id, move.placement);
|
|
245
|
+
for (const move of body.ftsChunks) {
|
|
246
|
+
const pointer = this.#ftsBases.get(move.key);
|
|
247
|
+
if (pointer === undefined)
|
|
248
|
+
throw new Error(`OPFS full-text base is missing: ${move.key}`);
|
|
249
|
+
pointer.chunks[move.ordinal] = move.placement;
|
|
250
|
+
this.#dropFtsChunkCache(move.key);
|
|
251
|
+
}
|
|
252
|
+
this.#pool.release([
|
|
253
|
+
...body.blocks.map((move) => move.from),
|
|
254
|
+
...body.ftsChunks.map((move) => move.from),
|
|
255
|
+
]);
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
case "addTable":
|
|
259
|
+
return this.#core.addTable(body.record);
|
|
260
|
+
case "updateTable":
|
|
261
|
+
return this.#core.updateTable(body.id, body.expectedRevision, body.update);
|
|
262
|
+
case "removeTable": {
|
|
263
|
+
this.#core.removeTable(body.id, body.expectedRevision);
|
|
264
|
+
const owned = `${body.id}/`;
|
|
265
|
+
for (const [key, pointer] of [...this.#ftsBases]) {
|
|
266
|
+
if (!key.startsWith(owned))
|
|
267
|
+
continue;
|
|
268
|
+
this.#ftsBases.delete(key);
|
|
269
|
+
this.#dropFtsChunkCache(key);
|
|
270
|
+
this.#pool.release(pointer.chunks);
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
case "addSegment":
|
|
275
|
+
return this.#core.addSegment(body.record);
|
|
276
|
+
case "removeSegment":
|
|
277
|
+
return this.#core.removeSegment(body.id);
|
|
278
|
+
case "reserveRowIds":
|
|
279
|
+
return this.#core.reserveRowIds(body.tableId, body.count);
|
|
280
|
+
case "reserveAutoIncrement":
|
|
281
|
+
return this.#core.reserveAutoIncrement(body.tableId, body.columnId, body.count, body.atLeast);
|
|
282
|
+
case "publishManifest":
|
|
283
|
+
return this.#core.publishManifest(body.input);
|
|
284
|
+
case "beginTransaction":
|
|
285
|
+
return this.#core.beginTransaction(body.input);
|
|
286
|
+
case "createTransaction":
|
|
287
|
+
return this.#core.createTransaction(body.record);
|
|
288
|
+
case "updateTransaction":
|
|
289
|
+
return this.#core.updateTransaction(body.id, body.expectedRevision, body.update);
|
|
290
|
+
case "stageTransactionArtifacts": {
|
|
291
|
+
for (const { id, placement } of body.blocks)
|
|
292
|
+
this.#blockIndex.set(id, placement);
|
|
293
|
+
return this.#core.stageTransactionArtifacts({
|
|
294
|
+
transactionId: body.transactionId,
|
|
295
|
+
expectedRevision: body.expectedRevision,
|
|
296
|
+
blocks: body.blocks.map(({ id }) => ({ id, bytes: EMPTY_BYTES })),
|
|
297
|
+
segments: body.segments,
|
|
298
|
+
updatedAt: body.updatedAt,
|
|
299
|
+
}, { blocksPrevalidated: true });
|
|
300
|
+
}
|
|
301
|
+
case "commitTransaction":
|
|
302
|
+
return this.#core.commitTransaction(body.input);
|
|
303
|
+
case "writeTransaction": {
|
|
304
|
+
// The core counts this step's own blocks as present, so the index is set only once the
|
|
305
|
+
// whole write has been accepted — a refusal leaves no stray placements behind.
|
|
306
|
+
const summary = this.#core.writeTransaction({ ...body.input, blocks: body.blocks.map(({ id }) => ({ id, bytes: EMPTY_BYTES })) }, { blocksPrevalidated: true });
|
|
307
|
+
for (const { id, placement } of body.blocks)
|
|
308
|
+
this.#blockIndex.set(id, placement);
|
|
309
|
+
return summary;
|
|
310
|
+
}
|
|
311
|
+
case "createLease":
|
|
312
|
+
return this.#core.createLease(body.record);
|
|
313
|
+
case "renewLease":
|
|
314
|
+
return this.#core.renewLease(body.id, body.expectedRevision, body.expiresAt);
|
|
315
|
+
case "moveLease":
|
|
316
|
+
return this.#core.moveLease(body.id, body.expectedRevision, body.manifestVersion, body.expiresAt);
|
|
317
|
+
case "removeLeaseIfExpired":
|
|
318
|
+
return this.#core.removeLeaseIfExpired(body.id, body.expectedRevision, body.expiresAtCutoff);
|
|
319
|
+
case "removeLease":
|
|
320
|
+
return this.#core.removeLease(body.id);
|
|
321
|
+
case "createCompactionJob":
|
|
322
|
+
return this.#core.createCompactionJob(body.record);
|
|
323
|
+
case "updateCompactionJob":
|
|
324
|
+
return this.#core.updateCompactionJob(body.id, body.expectedRevision, body.update);
|
|
325
|
+
case "cancelCompactionJob":
|
|
326
|
+
return this.#core.cancelCompactionJob(body.id, body.expectedRevision, body.cancelledAt);
|
|
327
|
+
case "removeCompactionJob":
|
|
328
|
+
return this.#core.removeCompactionJob(body.id);
|
|
329
|
+
case "createGarbageCollectionJob":
|
|
330
|
+
return this.#core.createGarbageCollectionJob(body.input);
|
|
331
|
+
case "garbageCollectionStep": {
|
|
332
|
+
this.#core.applyGarbageCollectionEffect(body.effect);
|
|
333
|
+
this.#releaseReclaimedBlocks(body.effect.reclaimedBlockIds);
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
case "removeGarbageCollectionJob":
|
|
337
|
+
return this.#core.removeGarbageCollectionJob(body.id);
|
|
338
|
+
case "removePrunedManifestRecords":
|
|
339
|
+
return this.#core.removePrunedManifestRecords().length;
|
|
340
|
+
case "createTempOwner":
|
|
341
|
+
return this.#core.createTempOwner(body.record);
|
|
342
|
+
case "renewTempOwner":
|
|
343
|
+
return this.#core.renewTempOwner(body.ownerId, body.expectedRevision, body.expiresAt);
|
|
344
|
+
case "removeTempOwnerIfExpired":
|
|
345
|
+
return this.#core.removeTempOwnerIfExpired(body.ownerId, body.expiresAtCutoff);
|
|
346
|
+
case "removeTempOwner":
|
|
347
|
+
return this.#core.removeTempOwner(body.ownerId);
|
|
348
|
+
case "writeFtsBase": {
|
|
349
|
+
const key = `${body.tableId}/${body.columnId}`;
|
|
350
|
+
const previous = this.#ftsBases.get(key);
|
|
351
|
+
if (previous !== undefined) {
|
|
352
|
+
this.#pool.release(previous.chunks);
|
|
353
|
+
this.#dropFtsChunkCache(key);
|
|
354
|
+
}
|
|
355
|
+
this.#ftsBases.set(key, structuredClone(body.pointer));
|
|
356
|
+
this.#core.pruneFtsDeltas(body.tableId, body.columnId, body.pointer.coversVersion);
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
case "importSnapshot": {
|
|
360
|
+
if (this.#core.getCurrentManifestVersion() !== null || this.#core.listTables().length > 0) {
|
|
361
|
+
throw new Error("This store already holds a database");
|
|
362
|
+
}
|
|
363
|
+
for (const { id, placement } of body.blockPlacements)
|
|
364
|
+
this.#blockIndex.set(id, placement);
|
|
365
|
+
this.#core.loadSnapshotRecords({ ...body.records, blocks: undefined }, body.blockPlacements.map(({ id }) => id));
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
#applyReplayed(entry) {
|
|
371
|
+
this.#permissivePhysical = true;
|
|
372
|
+
try {
|
|
373
|
+
const { seq: _seq, ...body } = entry;
|
|
374
|
+
void _seq;
|
|
375
|
+
this.#applyBody(body);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
this.#permissivePhysical = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
#releaseReclaimedBlocks(ids) {
|
|
382
|
+
const placements = [];
|
|
383
|
+
for (const id of ids) {
|
|
384
|
+
const placement = this.#blockIndex.get(id);
|
|
385
|
+
if (placement !== undefined)
|
|
386
|
+
placements.push(placement);
|
|
387
|
+
this.#blockIndex.delete(id);
|
|
388
|
+
}
|
|
389
|
+
this.#pool.release(placements);
|
|
390
|
+
}
|
|
391
|
+
/** Fully synchronous: dump, write the older slot, flush it, then and only then reset. */
|
|
392
|
+
checkpointNow() {
|
|
393
|
+
const state = {
|
|
394
|
+
lastSeq: this.#seq,
|
|
395
|
+
core: this.#core.dump(),
|
|
396
|
+
blockIndex: [...this.#blockIndex.entries()],
|
|
397
|
+
ftsBases: [...this.#ftsBases.entries()],
|
|
398
|
+
extents: this.#pool.meta(),
|
|
399
|
+
};
|
|
400
|
+
const bytes = encodeSyncCheckpoint(state);
|
|
401
|
+
const slotIndex = this.#newestSlot === 0 ? 1 : 0;
|
|
402
|
+
const slot = this.#slots[slotIndex];
|
|
403
|
+
slot.truncate(0);
|
|
404
|
+
slot.write(bytes, { at: 0 });
|
|
405
|
+
slot.flush();
|
|
406
|
+
this.#newestSlot = slotIndex;
|
|
407
|
+
this.#wal.reset();
|
|
408
|
+
this.#entriesSinceCheckpoint = 0;
|
|
409
|
+
this.#lastCheckpointBytes = bytes.byteLength;
|
|
410
|
+
}
|
|
411
|
+
async #deleteDrainedExtents() {
|
|
412
|
+
for (const id of this.#pool.release([])) {
|
|
413
|
+
await this.#pool.deleteExtent(id);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** Re-packs every sealed extent below 50% occupancy; each source file is at most 8 MiB. */
|
|
417
|
+
async #compactFragmentedExtents() {
|
|
418
|
+
for (;;) {
|
|
419
|
+
const extent = await this.#pool.fragmentedExtentId();
|
|
420
|
+
if (extent === undefined)
|
|
421
|
+
return;
|
|
422
|
+
const blockSources = [...this.#blockIndex.entries()]
|
|
423
|
+
.filter(([, placement]) => placement.extent === extent)
|
|
424
|
+
.map(([id, from]) => ({ id, from }));
|
|
425
|
+
const ftsSources = [...this.#ftsBases.entries()].flatMap(([key, pointer]) => pointer.chunks.flatMap((from, ordinal) => from.extent === extent ? [{ key, ordinal, from }] : []));
|
|
426
|
+
if (blockSources.length === 0 && ftsSources.length === 0) {
|
|
427
|
+
throw new Error(`OPFS extent ${String(extent)} has live bytes but no indexed payload`);
|
|
428
|
+
}
|
|
429
|
+
const blocks = [];
|
|
430
|
+
const ftsChunks = [];
|
|
431
|
+
const appended = [];
|
|
432
|
+
try {
|
|
433
|
+
for (const source of blockSources) {
|
|
434
|
+
const placement = await this.#pool.append(await this.#pool.read(source.from), this.#strict);
|
|
435
|
+
appended.push(placement);
|
|
436
|
+
blocks.push({ ...source, placement });
|
|
437
|
+
}
|
|
438
|
+
for (const source of ftsSources) {
|
|
439
|
+
const placement = await this.#pool.append(await this.#pool.read(source.from), this.#strict);
|
|
440
|
+
appended.push(placement);
|
|
441
|
+
ftsChunks.push({ ...source, placement });
|
|
442
|
+
}
|
|
443
|
+
this.#logged({ op: "relocatePayloads", blocks, ftsChunks });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
this.#pool.release(appended);
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
await this.#deleteDrainedExtents();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ---------------------------------------------------------------------------------------
|
|
453
|
+
// Blocks.
|
|
454
|
+
// ---------------------------------------------------------------------------------------
|
|
455
|
+
async addBlock(id, bytes) {
|
|
456
|
+
return this.addBlocks([{ id, bytes }]);
|
|
457
|
+
}
|
|
458
|
+
async addBlocks(blocks) {
|
|
459
|
+
return this.#run(async () => {
|
|
460
|
+
const ids = new Set();
|
|
461
|
+
for (const block of blocks) {
|
|
462
|
+
validateId(block.id);
|
|
463
|
+
if (ids.has(block.id) || this.#blockIndex.has(block.id)) {
|
|
464
|
+
throw new Error(`Block already exists: ${block.id}`);
|
|
465
|
+
}
|
|
466
|
+
ids.add(block.id);
|
|
467
|
+
}
|
|
468
|
+
const placements = [];
|
|
469
|
+
try {
|
|
470
|
+
for (const block of blocks) {
|
|
471
|
+
placements.push({
|
|
472
|
+
id: block.id,
|
|
473
|
+
placement: await this.#pool.append(block.bytes, this.#strict),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
// Nothing was recorded; the written bytes are dead space. Undo the accounting.
|
|
479
|
+
this.#pool.release(placements.map(({ placement }) => placement));
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
this.#logged({ op: "addBlocks", placements });
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async getBlock(id) {
|
|
486
|
+
await this.#healthy();
|
|
487
|
+
const placement = this.#blockIndex.get(id);
|
|
488
|
+
if (placement === undefined)
|
|
489
|
+
return undefined;
|
|
490
|
+
return this.#pool.read(placement);
|
|
491
|
+
}
|
|
492
|
+
async getBlocks(ids) {
|
|
493
|
+
return Promise.all(ids.map((id) => this.getBlock(id)));
|
|
494
|
+
}
|
|
495
|
+
async removeBlock(id) {
|
|
496
|
+
await this.#run(async () => {
|
|
497
|
+
if (!this.#blockIndex.has(id))
|
|
498
|
+
return;
|
|
499
|
+
this.#logged({ op: "removeBlock", id });
|
|
500
|
+
await this.#deleteDrainedExtents();
|
|
501
|
+
await this.#compactFragmentedExtents();
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
async listBlockIds() {
|
|
505
|
+
await this.#healthy();
|
|
506
|
+
return [...this.#blockIndex.keys()].sort();
|
|
507
|
+
}
|
|
508
|
+
// ---------------------------------------------------------------------------------------
|
|
509
|
+
// Temp spill pages: instance-local files, never logged, never proxied.
|
|
510
|
+
// ---------------------------------------------------------------------------------------
|
|
511
|
+
async putTempRunPage(page) {
|
|
512
|
+
validateTempRunPage(page);
|
|
513
|
+
await this.#tree.writeFile(tempPagePath(page.ownerId, page.runId, page.pageIndex), page.bytes);
|
|
514
|
+
}
|
|
515
|
+
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
516
|
+
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
517
|
+
return this.#tree.readFile(tempPagePath(ownerId, runId, pageIndex));
|
|
518
|
+
}
|
|
519
|
+
async removeTempRun(ownerId, runId) {
|
|
520
|
+
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
521
|
+
await this.#tree.deleteTree(["temp", encodeSegment(ownerId), encodeSegment(runId)]);
|
|
522
|
+
}
|
|
523
|
+
async removeTempOwner(ownerId) {
|
|
524
|
+
validateId(ownerId);
|
|
525
|
+
await this.#run(() => this.#logged({ op: "removeTempOwner", ownerId }));
|
|
526
|
+
await this.#tree.deleteTree(["temp", encodeSegment(ownerId)]);
|
|
527
|
+
}
|
|
528
|
+
async createTempOwner(record) {
|
|
529
|
+
await this.#run(() => this.#logged({ op: "createTempOwner", record }));
|
|
530
|
+
}
|
|
531
|
+
async getTempOwner(ownerId) {
|
|
532
|
+
await this.#healthy();
|
|
533
|
+
return this.#core.getTempOwner(ownerId);
|
|
534
|
+
}
|
|
535
|
+
async renewTempOwner(ownerId, expectedRevision, expiresAt) {
|
|
536
|
+
return this.#run(() => this.#logged({
|
|
537
|
+
op: "renewTempOwner",
|
|
538
|
+
ownerId,
|
|
539
|
+
expectedRevision,
|
|
540
|
+
expiresAt,
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
|
|
544
|
+
const removed = await this.#run(() => this.#logged({ op: "removeTempOwnerIfExpired", ownerId, expiresAtCutoff }));
|
|
545
|
+
if (removed)
|
|
546
|
+
await this.#tree.deleteTree(["temp", encodeSegment(ownerId)]);
|
|
547
|
+
return removed;
|
|
548
|
+
}
|
|
549
|
+
async listTempOwnerIdsPage(afterOwnerId, limit) {
|
|
550
|
+
await this.#healthy();
|
|
551
|
+
const names = await this.#tree.listNames(["temp"]);
|
|
552
|
+
return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, names.map(decodeSegment));
|
|
553
|
+
}
|
|
554
|
+
// ---------------------------------------------------------------------------------------
|
|
555
|
+
// Record reads: the leader's memory is always current; no probes, no refreshes.
|
|
556
|
+
// ---------------------------------------------------------------------------------------
|
|
557
|
+
async getTable(id) {
|
|
558
|
+
await this.#healthy();
|
|
559
|
+
return this.#core.getTable(id);
|
|
560
|
+
}
|
|
561
|
+
async getTableByName(name) {
|
|
562
|
+
await this.#healthy();
|
|
563
|
+
return this.#core.getTableByName(name);
|
|
564
|
+
}
|
|
565
|
+
async listTables() {
|
|
566
|
+
await this.#healthy();
|
|
567
|
+
return this.#core.listTables();
|
|
568
|
+
}
|
|
569
|
+
async getSegment(id) {
|
|
570
|
+
await this.#healthy();
|
|
571
|
+
return this.#core.getSegment(id);
|
|
572
|
+
}
|
|
573
|
+
async listSegments(tableId) {
|
|
574
|
+
await this.#healthy();
|
|
575
|
+
return this.#core.listSegments(tableId);
|
|
576
|
+
}
|
|
577
|
+
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
578
|
+
await this.#healthy();
|
|
579
|
+
return this.#core.getExistingUniqueKeys(tableId, keyTokens);
|
|
580
|
+
}
|
|
581
|
+
async getCurrentManifestVersion() {
|
|
582
|
+
await this.#healthy();
|
|
583
|
+
return this.#core.getCurrentManifestVersion();
|
|
584
|
+
}
|
|
585
|
+
async getCatalogProbe() {
|
|
586
|
+
await this.#healthy();
|
|
587
|
+
return this.#core.getCatalogProbe();
|
|
588
|
+
}
|
|
589
|
+
async getQueryCatalogState(tableNames) {
|
|
590
|
+
await this.#healthy();
|
|
591
|
+
return this.#core.getQueryCatalogState(tableNames);
|
|
592
|
+
}
|
|
593
|
+
async getCurrentManifest() {
|
|
594
|
+
await this.#healthy();
|
|
595
|
+
return this.#core.getCurrentManifest();
|
|
596
|
+
}
|
|
597
|
+
async getManifest(version) {
|
|
598
|
+
await this.#healthy();
|
|
599
|
+
return this.#core.getManifest(version);
|
|
600
|
+
}
|
|
601
|
+
async listManifests() {
|
|
602
|
+
await this.#healthy();
|
|
603
|
+
return this.#core.listManifests();
|
|
604
|
+
}
|
|
605
|
+
async listManifestPage(afterVersion, limit) {
|
|
606
|
+
await this.#healthy();
|
|
607
|
+
return this.#core.listManifestPage(afterVersion, limit);
|
|
608
|
+
}
|
|
609
|
+
async getTransaction(id) {
|
|
610
|
+
await this.#healthy();
|
|
611
|
+
return this.#core.getTransaction(id);
|
|
612
|
+
}
|
|
613
|
+
async getTransactions(ids) {
|
|
614
|
+
await this.#healthy();
|
|
615
|
+
return this.#core.getTransactions(ids);
|
|
616
|
+
}
|
|
617
|
+
async listTransactions() {
|
|
618
|
+
await this.#healthy();
|
|
619
|
+
return this.#core.listTransactions();
|
|
620
|
+
}
|
|
621
|
+
async listTransactionPage(afterId, limit) {
|
|
622
|
+
await this.#healthy();
|
|
623
|
+
return this.#core.listTransactionPage(afterId, limit);
|
|
624
|
+
}
|
|
625
|
+
async getLease(id) {
|
|
626
|
+
await this.#healthy();
|
|
627
|
+
return this.#core.getLease(id);
|
|
628
|
+
}
|
|
629
|
+
async listLeases() {
|
|
630
|
+
await this.#healthy();
|
|
631
|
+
return this.#core.listLeases();
|
|
632
|
+
}
|
|
633
|
+
async getCompactionJob(id) {
|
|
634
|
+
await this.#healthy();
|
|
635
|
+
return this.#core.getCompactionJob(id);
|
|
636
|
+
}
|
|
637
|
+
async listCompactionJobs(tableId) {
|
|
638
|
+
await this.#healthy();
|
|
639
|
+
return this.#core.listCompactionJobs(tableId);
|
|
640
|
+
}
|
|
641
|
+
async listCompactionJobPage(afterId, limit) {
|
|
642
|
+
await this.#healthy();
|
|
643
|
+
return this.#core.listCompactionJobPage(afterId, limit);
|
|
644
|
+
}
|
|
645
|
+
async getGarbageCollectionJob(id) {
|
|
646
|
+
await this.#healthy();
|
|
647
|
+
return this.#core.getGarbageCollectionJob(id);
|
|
648
|
+
}
|
|
649
|
+
async listGarbageCollectionJobs() {
|
|
650
|
+
await this.#healthy();
|
|
651
|
+
return this.#core.listGarbageCollectionJobs();
|
|
652
|
+
}
|
|
653
|
+
// ---------------------------------------------------------------------------------------
|
|
654
|
+
// Logged record mutations.
|
|
655
|
+
// ---------------------------------------------------------------------------------------
|
|
656
|
+
async addTable(record) {
|
|
657
|
+
await this.#run(() => this.#logged({ op: "addTable", record }));
|
|
658
|
+
}
|
|
659
|
+
async updateTable(id, expectedRevision, update) {
|
|
660
|
+
return this.#run(() => this.#logged({ op: "updateTable", id, expectedRevision, update }));
|
|
661
|
+
}
|
|
662
|
+
async removeTable(id, expectedRevision) {
|
|
663
|
+
await this.#run(async () => {
|
|
664
|
+
this.#logged({ op: "removeTable", id, expectedRevision });
|
|
665
|
+
await this.#deleteDrainedExtents();
|
|
666
|
+
await this.#compactFragmentedExtents();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
async addSegment(record) {
|
|
670
|
+
await this.#run(() => this.#logged({ op: "addSegment", record }));
|
|
671
|
+
}
|
|
672
|
+
async removeSegment(id) {
|
|
673
|
+
await this.#run(() => this.#logged({ op: "removeSegment", id }));
|
|
674
|
+
}
|
|
675
|
+
async reserveRowIds(tableId, count) {
|
|
676
|
+
return this.#run(() => this.#logged({ op: "reserveRowIds", tableId, count }));
|
|
677
|
+
}
|
|
678
|
+
async reserveAutoIncrement(tableId, columnId, count, atLeast) {
|
|
679
|
+
return this.#run(() => this.#logged({
|
|
680
|
+
op: "reserveAutoIncrement",
|
|
681
|
+
tableId,
|
|
682
|
+
columnId,
|
|
683
|
+
count,
|
|
684
|
+
...(atLeast === undefined ? {} : { atLeast }),
|
|
685
|
+
}));
|
|
686
|
+
}
|
|
687
|
+
async publishManifest(input) {
|
|
688
|
+
// Resolve the optional timestamp before logging: replays must be deterministic.
|
|
689
|
+
const resolved = {
|
|
690
|
+
...input,
|
|
691
|
+
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
692
|
+
};
|
|
693
|
+
return this.#run(() => this.#logged({ op: "publishManifest", input: resolved }));
|
|
694
|
+
}
|
|
695
|
+
async beginTransaction(input) {
|
|
696
|
+
validateBeginTransactionInput(input);
|
|
697
|
+
return this.#run(() => this.#logged({ op: "beginTransaction", input }));
|
|
698
|
+
}
|
|
699
|
+
async createTransaction(record) {
|
|
700
|
+
await this.#run(() => this.#logged({ op: "createTransaction", record }));
|
|
701
|
+
}
|
|
702
|
+
async updateTransaction(id, expectedRevision, update) {
|
|
703
|
+
return this.#run(() => this.#logged({
|
|
704
|
+
op: "updateTransaction",
|
|
705
|
+
id,
|
|
706
|
+
expectedRevision,
|
|
707
|
+
update,
|
|
708
|
+
}));
|
|
709
|
+
}
|
|
710
|
+
async stageTransactionArtifacts(input) {
|
|
711
|
+
return this.#run(async () => {
|
|
712
|
+
const ids = new Set();
|
|
713
|
+
for (const block of input.blocks) {
|
|
714
|
+
validateId(block.id);
|
|
715
|
+
if (ids.has(block.id) || this.#blockIndex.has(block.id)) {
|
|
716
|
+
throw new Error(`Block already exists: ${block.id}`);
|
|
717
|
+
}
|
|
718
|
+
ids.add(block.id);
|
|
719
|
+
}
|
|
720
|
+
const blocks = [];
|
|
721
|
+
try {
|
|
722
|
+
for (const block of input.blocks) {
|
|
723
|
+
blocks.push({
|
|
724
|
+
id: block.id,
|
|
725
|
+
placement: await this.#pool.append(block.bytes, this.#strict),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return this.#logged({
|
|
729
|
+
op: "stageTransactionArtifacts",
|
|
730
|
+
transactionId: input.transactionId,
|
|
731
|
+
expectedRevision: input.expectedRevision,
|
|
732
|
+
blocks,
|
|
733
|
+
segments: [...input.segments],
|
|
734
|
+
updatedAt: input.updatedAt,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
this.#pool.release(blocks.map(({ placement }) => placement));
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async commitTransaction(input) {
|
|
744
|
+
return this.#run(() => this.#logged({ op: "commitTransaction", input }));
|
|
745
|
+
}
|
|
746
|
+
async writeTransaction(input) {
|
|
747
|
+
return this.#run(async () => {
|
|
748
|
+
const ids = new Set();
|
|
749
|
+
for (const block of input.blocks) {
|
|
750
|
+
validateId(block.id);
|
|
751
|
+
if (ids.has(block.id) || this.#blockIndex.has(block.id)) {
|
|
752
|
+
throw new Error(`Block already exists: ${block.id}`);
|
|
753
|
+
}
|
|
754
|
+
ids.add(block.id);
|
|
755
|
+
}
|
|
756
|
+
const { blocks: bytes, ...rest } = input;
|
|
757
|
+
const blocks = [];
|
|
758
|
+
try {
|
|
759
|
+
for (const block of bytes) {
|
|
760
|
+
blocks.push({
|
|
761
|
+
id: block.id,
|
|
762
|
+
placement: await this.#pool.append(block.bytes, this.#strict),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return this.#logged({
|
|
766
|
+
op: "writeTransaction",
|
|
767
|
+
input: { ...rest, segments: [...input.segments] },
|
|
768
|
+
blocks,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
// Nothing was recorded; the written bytes are dead space. Undo the accounting.
|
|
773
|
+
this.#pool.release(blocks.map(({ placement }) => placement));
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async createLease(record) {
|
|
779
|
+
await this.#run(() => this.#logged({ op: "createLease", record }));
|
|
780
|
+
}
|
|
781
|
+
async renewLease(id, expectedRevision, expiresAt) {
|
|
782
|
+
return this.#run(() => this.#logged({ op: "renewLease", id, expectedRevision, expiresAt }));
|
|
783
|
+
}
|
|
784
|
+
async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
|
|
785
|
+
return this.#run(() => this.#logged({
|
|
786
|
+
op: "moveLease",
|
|
787
|
+
id,
|
|
788
|
+
expectedRevision,
|
|
789
|
+
manifestVersion,
|
|
790
|
+
expiresAt,
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
794
|
+
return this.#run(() => this.#logged({
|
|
795
|
+
op: "removeLeaseIfExpired",
|
|
796
|
+
id,
|
|
797
|
+
expectedRevision,
|
|
798
|
+
expiresAtCutoff,
|
|
799
|
+
}));
|
|
800
|
+
}
|
|
801
|
+
async removeLease(id) {
|
|
802
|
+
await this.#run(() => this.#logged({ op: "removeLease", id }));
|
|
803
|
+
}
|
|
804
|
+
async createCompactionJob(record) {
|
|
805
|
+
await this.#run(() => this.#logged({ op: "createCompactionJob", record }));
|
|
806
|
+
}
|
|
807
|
+
async updateCompactionJob(id, expectedRevision, update) {
|
|
808
|
+
return this.#run(() => this.#logged({
|
|
809
|
+
op: "updateCompactionJob",
|
|
810
|
+
id,
|
|
811
|
+
expectedRevision,
|
|
812
|
+
update,
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
async cancelCompactionJob(id, expectedRevision, cancelledAt) {
|
|
816
|
+
return this.#run(() => this.#logged({
|
|
817
|
+
op: "cancelCompactionJob",
|
|
818
|
+
id,
|
|
819
|
+
expectedRevision,
|
|
820
|
+
cancelledAt,
|
|
821
|
+
}));
|
|
822
|
+
}
|
|
823
|
+
async removeCompactionJob(id) {
|
|
824
|
+
await this.#run(() => this.#logged({ op: "removeCompactionJob", id }));
|
|
825
|
+
}
|
|
826
|
+
async createGarbageCollectionJob(input) {
|
|
827
|
+
return this.#run(() => this.#logged({ op: "createGarbageCollectionJob", input }));
|
|
828
|
+
}
|
|
829
|
+
async runGarbageCollectionStep(input) {
|
|
830
|
+
return this.#run(async () => {
|
|
831
|
+
// Computed against provably-current state, logged as the resolved effect so replay
|
|
832
|
+
// needs no knowledge of physical placement.
|
|
833
|
+
const step = this.#core.runGarbageCollectionStep(input);
|
|
834
|
+
this.#releaseReclaimedBlocks(step.reclaimedBlockIds);
|
|
835
|
+
this.#appendFrame({
|
|
836
|
+
op: "garbageCollectionStep",
|
|
837
|
+
effect: {
|
|
838
|
+
job: step.job,
|
|
839
|
+
prunedManifestVersions: [...step.prunedManifestVersions],
|
|
840
|
+
reclaimedSegmentIds: [...step.reclaimedSegmentIds],
|
|
841
|
+
reclaimedBlockIds: [...step.reclaimedBlockIds],
|
|
842
|
+
reclaimedTransactionIds: [...step.reclaimedTransactionIds],
|
|
843
|
+
updatedAt: input.updatedAt,
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
await this.#deleteDrainedExtents();
|
|
847
|
+
if (step.job.state === "completed")
|
|
848
|
+
await this.#compactFragmentedExtents();
|
|
849
|
+
return step;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
async removeGarbageCollectionJob(id) {
|
|
853
|
+
await this.#run(() => this.#logged({ op: "removeGarbageCollectionJob", id }));
|
|
854
|
+
}
|
|
855
|
+
async removePrunedManifestRecords() {
|
|
856
|
+
return this.#run(() => this.#logged({ op: "removePrunedManifestRecords" }));
|
|
857
|
+
}
|
|
858
|
+
// ---------------------------------------------------------------------------------------
|
|
859
|
+
// Full-text bases: immutable chunks in extents plus a logged pointer.
|
|
860
|
+
// ---------------------------------------------------------------------------------------
|
|
861
|
+
async writeFtsBase(tableId, columnId, input) {
|
|
862
|
+
await this.#run(async () => {
|
|
863
|
+
const chunks = [];
|
|
864
|
+
try {
|
|
865
|
+
for (const chunk of input.chunks) {
|
|
866
|
+
chunks.push(await this.#pool.append(encodeFtsChunk(chunk), this.#strict));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
this.#pool.release(chunks);
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
const pointer = {
|
|
874
|
+
coversVersion: input.coversVersion,
|
|
875
|
+
totalTokens: input.totalTokens,
|
|
876
|
+
chunks,
|
|
877
|
+
chunkBounds: input.chunks.map((chunk) => ({
|
|
878
|
+
first: chunk[0]?.term ?? "",
|
|
879
|
+
last: chunk[chunk.length - 1]?.term ?? "",
|
|
880
|
+
})),
|
|
881
|
+
};
|
|
882
|
+
this.#logged({ op: "writeFtsBase", tableId, columnId, pointer });
|
|
883
|
+
await this.#deleteDrainedExtents();
|
|
884
|
+
await this.#compactFragmentedExtents();
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
888
|
+
await this.#healthy();
|
|
889
|
+
const key = `${tableId}/${columnId}`;
|
|
890
|
+
const pointer = this.#ftsBases.get(key);
|
|
891
|
+
const coversVersion = pointer?.coversVersion ?? -1;
|
|
892
|
+
const deltas = this.#core.readFtsDeltas(tableId, columnId, coversVersion, upToVersion);
|
|
893
|
+
const baseChunks = pointer === undefined
|
|
894
|
+
? []
|
|
895
|
+
: await Promise.all(selectFtsChunks(pointer.chunkBounds, terms).map((ordinal) => this.#loadFtsChunk(key, pointer, ordinal)));
|
|
896
|
+
return {
|
|
897
|
+
...collectFtsCandidates([...baseChunks, ...deltas.chunkLists], terms),
|
|
898
|
+
deltaChunkCount: deltas.deltaChunkCount,
|
|
899
|
+
totalTokens: (pointer?.totalTokens ?? 0) + deltas.deltaTokens,
|
|
900
|
+
coversVersion,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
async #loadFtsChunk(key, pointer, ordinal) {
|
|
904
|
+
const placement = pointer.chunks[ordinal];
|
|
905
|
+
if (placement === undefined)
|
|
906
|
+
return [];
|
|
907
|
+
const cacheKey = `${key}/${String(placement.extent)}/${String(placement.offset)}`;
|
|
908
|
+
const cached = this.#ftsChunkCache.get(cacheKey);
|
|
909
|
+
if (cached !== undefined)
|
|
910
|
+
return cached;
|
|
911
|
+
const chunk = decodeFtsChunk(await this.#pool.read(placement));
|
|
912
|
+
this.#ftsChunkCache.set(cacheKey, chunk);
|
|
913
|
+
if (this.#ftsChunkCache.size > FTS_CHUNK_CACHE_SIZE) {
|
|
914
|
+
const [oldest] = this.#ftsChunkCache.keys();
|
|
915
|
+
if (oldest !== undefined)
|
|
916
|
+
this.#ftsChunkCache.delete(oldest);
|
|
917
|
+
}
|
|
918
|
+
return chunk;
|
|
919
|
+
}
|
|
920
|
+
#dropFtsChunkCache(keyPrefix) {
|
|
921
|
+
for (const key of [...this.#ftsChunkCache.keys()]) {
|
|
922
|
+
if (key.startsWith(`${keyPrefix}/`))
|
|
923
|
+
this.#ftsChunkCache.delete(key);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// ---------------------------------------------------------------------------------------
|
|
927
|
+
// Snapshots, sizing, lifecycle.
|
|
928
|
+
// ---------------------------------------------------------------------------------------
|
|
929
|
+
/**
|
|
930
|
+
* Consistent by construction: the export runs as one queue operation, so no commit or
|
|
931
|
+
* garbage collection interleaves with it. Full-text bases export as rebuilds.
|
|
932
|
+
*/
|
|
933
|
+
async exportSnapshot() {
|
|
934
|
+
return this.#run(async () => {
|
|
935
|
+
const { records, blockIds } = this.#core.exportSnapshotRecords();
|
|
936
|
+
const blocks = [];
|
|
937
|
+
for (const id of blockIds) {
|
|
938
|
+
const placement = this.#blockIndex.get(id);
|
|
939
|
+
if (placement === undefined)
|
|
940
|
+
throw new Error(`Manifest references missing block: ${id}`);
|
|
941
|
+
blocks.push({ id, bytes: await this.#pool.read(placement) });
|
|
942
|
+
}
|
|
943
|
+
return { ...records, blocks };
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
async importSnapshot(snapshot, options = {}) {
|
|
947
|
+
const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
|
|
948
|
+
options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
|
|
949
|
+
await this.#run(async () => {
|
|
950
|
+
if (this.#core.getCurrentManifestVersion() !== null || this.#core.listTables().length > 0) {
|
|
951
|
+
throw new Error("This store already holds a database");
|
|
952
|
+
}
|
|
953
|
+
let writtenBytes = 0;
|
|
954
|
+
const blockPlacements = [];
|
|
955
|
+
try {
|
|
956
|
+
for (const block of snapshot.blocks) {
|
|
957
|
+
blockPlacements.push({
|
|
958
|
+
id: block.id,
|
|
959
|
+
placement: await this.#pool.append(block.bytes, this.#strict),
|
|
960
|
+
});
|
|
961
|
+
writtenBytes += block.bytes.byteLength;
|
|
962
|
+
options.onProgress?.({ phase: "blocks", writtenBytes, totalBytes });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch (error) {
|
|
966
|
+
this.#pool.release(blockPlacements.map(({ placement }) => placement));
|
|
967
|
+
throw error;
|
|
968
|
+
}
|
|
969
|
+
this.#logged({
|
|
970
|
+
op: "importSnapshot",
|
|
971
|
+
// Full-text bases restore as rebuilds; the index is an accelerator, not truth.
|
|
972
|
+
records: {
|
|
973
|
+
version: snapshot.version,
|
|
974
|
+
createdAt: snapshot.createdAt,
|
|
975
|
+
tables: snapshot.tables.map((table) => ({
|
|
976
|
+
...structuredClone(table),
|
|
977
|
+
fts: [],
|
|
978
|
+
record: invalidateFtsColumns(structuredClone(table.record)),
|
|
979
|
+
})),
|
|
980
|
+
segments: structuredClone(snapshot.segments),
|
|
981
|
+
transactions: structuredClone(snapshot.transactions),
|
|
982
|
+
},
|
|
983
|
+
blockPlacements,
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
|
|
987
|
+
}
|
|
988
|
+
/** Live payload bytes plus the control files — what this database logically occupies. */
|
|
989
|
+
async getLogicalStorageBytes() {
|
|
990
|
+
let total = this.#wal.byteLength;
|
|
991
|
+
const slot = this.#slots[this.#newestSlot];
|
|
992
|
+
if (slot !== undefined)
|
|
993
|
+
total += slot.getSize();
|
|
994
|
+
for (const [, bytes] of this.#pool.meta().liveBytes)
|
|
995
|
+
total += bytes;
|
|
996
|
+
for (const { size } of await this.#tree.walkFiles(["temp"]))
|
|
997
|
+
total += size;
|
|
998
|
+
return total;
|
|
999
|
+
}
|
|
1000
|
+
/** Graceful: flush, checkpoint (so the next leader's takeover is instant), release. */
|
|
1001
|
+
async shutdown() {
|
|
1002
|
+
await this.#run(() => {
|
|
1003
|
+
this.checkpointNow();
|
|
1004
|
+
this.#closed = true;
|
|
1005
|
+
}).catch(() => {
|
|
1006
|
+
// Shutting down a poisoned leader still releases its handles below.
|
|
1007
|
+
this.#closed = true;
|
|
1008
|
+
});
|
|
1009
|
+
this.#walHandle.close();
|
|
1010
|
+
for (const slot of this.#slots)
|
|
1011
|
+
slot.close();
|
|
1012
|
+
this.#extents?.close();
|
|
1013
|
+
}
|
|
1014
|
+
/** Test-only: what tab death does — locks release, nothing is flushed or checkpointed. */
|
|
1015
|
+
crash() {
|
|
1016
|
+
this.#closed = true;
|
|
1017
|
+
this.#walHandle.close();
|
|
1018
|
+
for (const slot of this.#slots)
|
|
1019
|
+
slot.close();
|
|
1020
|
+
this.#extents?.close();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const EMPTY_BYTES = new Uint8Array(0);
|
|
1024
|
+
function EMPTY_CORE_STATE() {
|
|
1025
|
+
return {
|
|
1026
|
+
currentVersion: null,
|
|
1027
|
+
catalogEpoch: 0,
|
|
1028
|
+
manifests: [],
|
|
1029
|
+
transactions: [],
|
|
1030
|
+
tables: [],
|
|
1031
|
+
segments: [],
|
|
1032
|
+
leases: [],
|
|
1033
|
+
compactionJobs: [],
|
|
1034
|
+
garbageCollectionJobs: [],
|
|
1035
|
+
nextRowIds: [],
|
|
1036
|
+
nextAutoIncrement: [],
|
|
1037
|
+
ftsBases: [],
|
|
1038
|
+
ftsDeltas: [],
|
|
1039
|
+
uniqueKeys: [],
|
|
1040
|
+
tempOwners: [],
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function tempPagePath(ownerId, runId, pageIndex) {
|
|
1044
|
+
return ["temp", encodeSegment(ownerId), encodeSegment(runId), String(pageIndex)];
|
|
1045
|
+
}
|
|
1046
|
+
function placementsOf(entry) {
|
|
1047
|
+
switch (entry.op) {
|
|
1048
|
+
case "addBlocks":
|
|
1049
|
+
return entry.placements.map(({ placement }) => placement);
|
|
1050
|
+
case "stageTransactionArtifacts":
|
|
1051
|
+
case "writeTransaction":
|
|
1052
|
+
return entry.blocks.map(({ placement }) => placement);
|
|
1053
|
+
case "writeFtsBase":
|
|
1054
|
+
return entry.pointer.chunks;
|
|
1055
|
+
case "importSnapshot":
|
|
1056
|
+
return entry.blockPlacements.map(({ placement }) => placement);
|
|
1057
|
+
case "relocatePayloads":
|
|
1058
|
+
return [
|
|
1059
|
+
...entry.blocks.map(({ placement }) => placement),
|
|
1060
|
+
...entry.ftsChunks.map(({ placement }) => placement),
|
|
1061
|
+
];
|
|
1062
|
+
default:
|
|
1063
|
+
return [];
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function samePlacement(left, right) {
|
|
1067
|
+
return (left?.extent === right.extent && left.offset === right.offset && left.length === right.length);
|
|
1068
|
+
}
|
|
1069
|
+
function encodeFtsChunk(chunk) {
|
|
1070
|
+
return encodeChunk(chunk);
|
|
1071
|
+
}
|
|
1072
|
+
function decodeFtsChunk(bytes) {
|
|
1073
|
+
const chunk = decodeChunk(bytes);
|
|
1074
|
+
if (chunk === undefined)
|
|
1075
|
+
throw new Error("Full-text base chunk is unreadable");
|
|
1076
|
+
return chunk;
|
|
1077
|
+
}
|
|
1078
|
+
/** The ordinals of base chunks whose term range can contain any of the query's terms. */
|
|
1079
|
+
function selectFtsChunks(chunkBounds, terms) {
|
|
1080
|
+
const ordinals = [];
|
|
1081
|
+
for (const [ordinal, bounds] of chunkBounds.entries()) {
|
|
1082
|
+
const needed = terms.some(({ term, prefix }) => {
|
|
1083
|
+
if (bounds.last < term)
|
|
1084
|
+
return false;
|
|
1085
|
+
if (prefix) {
|
|
1086
|
+
return bounds.first <= `${term}` || bounds.first.startsWith(term);
|
|
1087
|
+
}
|
|
1088
|
+
return bounds.first <= term;
|
|
1089
|
+
});
|
|
1090
|
+
if (needed)
|
|
1091
|
+
ordinals.push(ordinal);
|
|
1092
|
+
}
|
|
1093
|
+
return ordinals;
|
|
1094
|
+
}
|
|
1095
|
+
function invalidateFtsColumns(record) {
|
|
1096
|
+
if (record.ftsColumns === undefined)
|
|
1097
|
+
return record;
|
|
1098
|
+
const ftsColumns = Object.fromEntries(Object.entries(record.ftsColumns).map(([columnId, state]) => [
|
|
1099
|
+
columnId,
|
|
1100
|
+
{ ...state, state: "invalid" },
|
|
1101
|
+
]));
|
|
1102
|
+
return { ...record, ftsColumns };
|
|
1103
|
+
}
|
|
1104
|
+
//# sourceMappingURL=leader.js.map
|