@minnowdb/core 0.1.1 → 0.2.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/README.md +12 -9
- package/dist/engine/client.d.ts +10 -1
- package/dist/engine/client.d.ts.map +1 -1
- package/dist/engine/client.js +30 -5
- package/dist/engine/client.js.map +1 -1
- package/dist/engine/database.d.ts +55 -6
- package/dist/engine/database.d.ts.map +1 -1
- package/dist/engine/database.js +2689 -898
- package/dist/engine/database.js.map +1 -1
- package/dist/engine/live.d.ts +6 -2
- 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 +13 -1
- package/dist/engine/query.d.ts.map +1 -1
- package/dist/engine/query.js +73 -46
- 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 +87 -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/sql-feature-matrix.json +8 -0
package/dist/storage/memory.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "./toolkit/record-core.js";
|
|
2
|
+
/**
|
|
3
|
+
* The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
|
|
4
|
+
* block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
|
|
5
|
+
* record operation on a promise-chain queue — each queued body is synchronous, so no operation
|
|
6
|
+
* ever observes another mid-mutation.
|
|
7
|
+
*/
|
|
3
8
|
export class MemoryBlockStore {
|
|
4
9
|
#blocks = new Map();
|
|
5
|
-
#manifests = new Map();
|
|
6
|
-
#transactions = new Map();
|
|
7
|
-
#tables = new Map();
|
|
8
|
-
#tableIdsByName = new Map();
|
|
9
|
-
#segments = new Map();
|
|
10
|
-
#leases = new Map();
|
|
11
|
-
#compactionJobs = new Map();
|
|
12
|
-
#garbageCollectionJobs = new Map();
|
|
13
|
-
#nextRowIds = new Map();
|
|
14
|
-
#nextAutoIncrement = new Map();
|
|
15
|
-
#ftsBases = new Map();
|
|
16
|
-
#ftsDeltas = new Map();
|
|
17
|
-
#uniqueKeys = new Map();
|
|
18
10
|
#tempRunPages = new Map();
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
#core = new RecordCore({
|
|
12
|
+
hasBlock: (id) => this.#blocks.has(id),
|
|
13
|
+
blockByteLength: (id) => this.#blocks.get(id)?.byteLength,
|
|
14
|
+
});
|
|
23
15
|
#commitQueue = Promise.resolve();
|
|
24
16
|
async addBlock(id, bytes) {
|
|
25
17
|
return this.addBlocks([{ id, bytes }]);
|
|
@@ -56,6 +48,13 @@ export class MemoryBlockStore {
|
|
|
56
48
|
validateTempRunPage(page);
|
|
57
49
|
this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
|
|
58
50
|
}
|
|
51
|
+
async putTempRunPages(pages) {
|
|
52
|
+
for (const page of pages)
|
|
53
|
+
validateTempRunPage(page);
|
|
54
|
+
for (const page of pages) {
|
|
55
|
+
this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
59
58
|
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
60
59
|
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
61
60
|
return this.#tempRunPages.get(tempRunPageKey(ownerId, runId, pageIndex))?.slice();
|
|
@@ -69,967 +68,255 @@ export class MemoryBlockStore {
|
|
|
69
68
|
}
|
|
70
69
|
async removeTempOwner(ownerId) {
|
|
71
70
|
validateId(ownerId);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (key.startsWith(prefix))
|
|
75
|
-
this.#tempRunPages.delete(key);
|
|
76
|
-
this.#tempOwners.delete(ownerId);
|
|
71
|
+
this.#removeTempOwnerPages(ownerId);
|
|
72
|
+
this.#core.removeTempOwner(ownerId);
|
|
77
73
|
}
|
|
78
74
|
async createTempOwner(record) {
|
|
79
|
-
validateTempOwnerRecord(record);
|
|
80
75
|
return this.#runAtomic(() => {
|
|
81
|
-
|
|
82
|
-
throw new Error(`Temp owner already exists: ${record.ownerId}`);
|
|
83
|
-
}
|
|
84
|
-
this.#tempOwners.set(record.ownerId, structuredClone(record));
|
|
76
|
+
this.#core.createTempOwner(record);
|
|
85
77
|
});
|
|
86
78
|
}
|
|
87
79
|
async getTempOwner(ownerId) {
|
|
88
|
-
|
|
89
|
-
const record = this.#tempOwners.get(ownerId);
|
|
90
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
80
|
+
return this.#core.getTempOwner(ownerId);
|
|
91
81
|
}
|
|
92
82
|
async renewTempOwner(ownerId, expectedRevision, expiresAt) {
|
|
93
|
-
|
|
94
|
-
return this.#runAtomic(() => {
|
|
95
|
-
const record = this.#tempOwners.get(ownerId);
|
|
96
|
-
if (record?.revision !== expectedRevision) {
|
|
97
|
-
throw new TempOwnerConflictError(ownerId, expectedRevision, record?.revision ?? null);
|
|
98
|
-
}
|
|
99
|
-
const renewed = { ...record, expiresAt, revision: record.revision + 1 };
|
|
100
|
-
this.#tempOwners.set(ownerId, renewed);
|
|
101
|
-
return structuredClone(renewed);
|
|
102
|
-
});
|
|
83
|
+
return this.#runAtomic(() => this.#core.renewTempOwner(ownerId, expectedRevision, expiresAt));
|
|
103
84
|
}
|
|
104
85
|
async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
|
|
105
|
-
validateId(ownerId);
|
|
106
|
-
const cutoff = Date.parse(expiresAtCutoff);
|
|
107
|
-
if (!Number.isFinite(cutoff))
|
|
108
|
-
throw new TypeError("Temp owner expiry cutoff must be valid");
|
|
109
86
|
return this.#runAtomic(() => {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
const prefix = `${String(ownerId.length)}:${ownerId}:`;
|
|
117
|
-
for (const key of this.#tempRunPages.keys())
|
|
118
|
-
if (key.startsWith(prefix))
|
|
119
|
-
this.#tempRunPages.delete(key);
|
|
120
|
-
this.#tempOwners.delete(ownerId);
|
|
121
|
-
return true;
|
|
87
|
+
const removed = this.#core.removeTempOwnerIfExpired(ownerId, expiresAtCutoff);
|
|
88
|
+
if (removed)
|
|
89
|
+
this.#removeTempOwnerPages(ownerId);
|
|
90
|
+
return removed;
|
|
122
91
|
});
|
|
123
92
|
}
|
|
124
93
|
async listTempOwnerIdsPage(afterOwnerId, limit) {
|
|
125
|
-
|
|
126
|
-
const ownerIds = new Set(this.#tempOwners.keys());
|
|
94
|
+
const pageOwnerIds = [];
|
|
127
95
|
for (const key of this.#tempRunPages.keys()) {
|
|
128
96
|
const separator = key.indexOf(":");
|
|
129
97
|
const length = Number(key.slice(0, separator));
|
|
130
|
-
|
|
98
|
+
pageOwnerIds.push(key.slice(separator + 1, separator + 1 + length));
|
|
131
99
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
100
|
+
return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, pageOwnerIds);
|
|
101
|
+
}
|
|
102
|
+
#removeTempOwnerPages(ownerId) {
|
|
103
|
+
const prefix = `${String(ownerId.length)}:${ownerId}:`;
|
|
104
|
+
for (const key of this.#tempRunPages.keys())
|
|
105
|
+
if (key.startsWith(prefix))
|
|
106
|
+
this.#tempRunPages.delete(key);
|
|
140
107
|
}
|
|
141
108
|
async addTable(record) {
|
|
142
|
-
|
|
143
|
-
throw new Error(`Table already exists: ${record.id}`);
|
|
144
|
-
if (this.#tableIdsByName.has(record.name))
|
|
145
|
-
throw new Error(`Table name already exists: ${record.name}`);
|
|
146
|
-
this.#tables.set(record.id, structuredClone(record));
|
|
147
|
-
this.#tableIdsByName.set(record.name, record.id);
|
|
148
|
-
this.#catalogEpoch += 1;
|
|
109
|
+
this.#core.addTable(record);
|
|
149
110
|
}
|
|
150
111
|
async getTable(id) {
|
|
151
|
-
|
|
152
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
112
|
+
return this.#core.getTable(id);
|
|
153
113
|
}
|
|
154
114
|
async updateTable(id, expectedRevision, update) {
|
|
155
|
-
return this.#runAtomic(() =>
|
|
156
|
-
const record = this.#tables.get(id);
|
|
157
|
-
const actualRevision = record === undefined ? null : (record.revision ?? 0);
|
|
158
|
-
if (record === undefined || actualRevision !== expectedRevision) {
|
|
159
|
-
throw new TableRecordConflictError(id, expectedRevision, actualRevision);
|
|
160
|
-
}
|
|
161
|
-
if (update.columns !== undefined)
|
|
162
|
-
validateTableColumns(update.columns);
|
|
163
|
-
const { ftsColumns: previousFts, triggers: previousTriggers, ...base } = record;
|
|
164
|
-
const nextFts = update.ftsColumns === undefined ? previousFts : update.ftsColumns;
|
|
165
|
-
const nextTriggers = update.triggers === undefined ? previousTriggers : update.triggers;
|
|
166
|
-
const updated = {
|
|
167
|
-
...base,
|
|
168
|
-
columns: update.columns === undefined ? record.columns : structuredClone(update.columns),
|
|
169
|
-
...(nextFts === null || nextFts === undefined
|
|
170
|
-
? {}
|
|
171
|
-
: { ftsColumns: structuredClone(nextFts) }),
|
|
172
|
-
...(nextTriggers === null || nextTriggers === undefined
|
|
173
|
-
? {}
|
|
174
|
-
: { triggers: structuredClone(nextTriggers) }),
|
|
175
|
-
revision: expectedRevision + 1,
|
|
176
|
-
};
|
|
177
|
-
this.#tables.set(id, updated);
|
|
178
|
-
this.#catalogEpoch += 1;
|
|
179
|
-
return structuredClone(updated);
|
|
180
|
-
});
|
|
115
|
+
return this.#runAtomic(() => this.#core.updateTable(id, expectedRevision, update));
|
|
181
116
|
}
|
|
182
117
|
async removeTable(id, expectedRevision) {
|
|
183
118
|
return this.#runAtomic(() => {
|
|
184
|
-
|
|
185
|
-
const actualRevision = record === undefined ? null : (record.revision ?? 0);
|
|
186
|
-
if (record === undefined || actualRevision !== expectedRevision) {
|
|
187
|
-
throw new TableRecordConflictError(id, expectedRevision, actualRevision);
|
|
188
|
-
}
|
|
189
|
-
for (const [segmentId, segment] of this.#segments) {
|
|
190
|
-
if (segment.tableId === id)
|
|
191
|
-
this.#segments.delete(segmentId);
|
|
192
|
-
}
|
|
193
|
-
const owned = `${id}/`;
|
|
194
|
-
for (const key of [...this.#ftsBases.keys()]) {
|
|
195
|
-
if (key.startsWith(owned))
|
|
196
|
-
this.#ftsBases.delete(key);
|
|
197
|
-
}
|
|
198
|
-
for (const key of [...this.#ftsDeltas.keys()]) {
|
|
199
|
-
if (key.startsWith(owned))
|
|
200
|
-
this.#ftsDeltas.delete(key);
|
|
201
|
-
}
|
|
202
|
-
for (const key of [...this.#nextAutoIncrement.keys()]) {
|
|
203
|
-
if (key.startsWith(owned))
|
|
204
|
-
this.#nextAutoIncrement.delete(key);
|
|
205
|
-
}
|
|
206
|
-
this.#uniqueKeys.delete(id);
|
|
207
|
-
this.#nextRowIds.delete(id);
|
|
208
|
-
this.#tableIdsByName.delete(record.name);
|
|
209
|
-
this.#tables.delete(id);
|
|
210
|
-
this.#catalogEpoch += 1;
|
|
119
|
+
this.#core.removeTable(id, expectedRevision);
|
|
211
120
|
});
|
|
212
121
|
}
|
|
213
122
|
async writeFtsBase(tableId, columnId, input) {
|
|
214
123
|
return this.#runAtomic(() => {
|
|
215
|
-
|
|
216
|
-
this.#ftsBases.set(key, structuredClone(input));
|
|
217
|
-
const deltas = this.#ftsDeltas.get(key);
|
|
218
|
-
if (deltas !== undefined) {
|
|
219
|
-
for (const version of [...deltas.keys()]) {
|
|
220
|
-
if (version <= input.coversVersion)
|
|
221
|
-
deltas.delete(version);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
124
|
+
this.#core.writeFtsBase(tableId, columnId, input);
|
|
224
125
|
});
|
|
225
126
|
}
|
|
226
127
|
async readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
227
|
-
|
|
228
|
-
const base = this.#ftsBases.get(key);
|
|
229
|
-
const deltas = this.#ftsDeltas.get(key) ??
|
|
230
|
-
new Map();
|
|
231
|
-
const chunkLists = [...(base?.chunks ?? [])];
|
|
232
|
-
let deltaChunkCount = 0;
|
|
233
|
-
let totalTokens = base?.totalTokens ?? 0;
|
|
234
|
-
for (const [version, delta] of deltas) {
|
|
235
|
-
if (version <= (base?.coversVersion ?? -1) || version > upToVersion)
|
|
236
|
-
continue;
|
|
237
|
-
deltaChunkCount += 1;
|
|
238
|
-
totalTokens += delta.totalTokens;
|
|
239
|
-
chunkLists.push(delta.postings);
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
...collectFtsCandidates(chunkLists, terms),
|
|
243
|
-
deltaChunkCount,
|
|
244
|
-
totalTokens,
|
|
245
|
-
coversVersion: base?.coversVersion ?? -1,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Applies a commit's full-text deltas, and closes the stale-writer race: a commit adding
|
|
250
|
-
* segments to a table with an active index but no delta for one of its columns flips that
|
|
251
|
-
* column to "invalid" so a rebuild self-heals instead of the data commit failing.
|
|
252
|
-
*/
|
|
253
|
-
#applyFtsChanges(pendingSegments, changeList, version) {
|
|
254
|
-
const changedTableIds = new Set(pendingSegments.map((segment) => segment.tableId));
|
|
255
|
-
for (const tableId of changedTableIds) {
|
|
256
|
-
const record = this.#tables.get(tableId);
|
|
257
|
-
if (record === undefined)
|
|
258
|
-
continue;
|
|
259
|
-
const forTable = (changeList ?? []).find((entry) => entry.tableId === tableId);
|
|
260
|
-
const covered = new Set(forTable?.columns.map((column) => column.columnId) ?? []);
|
|
261
|
-
const invalidated = invalidateUncoveredFtsColumns(record, covered);
|
|
262
|
-
if (invalidated !== undefined)
|
|
263
|
-
this.#tables.set(record.id, invalidated);
|
|
264
|
-
}
|
|
265
|
-
for (const changes of changeList ?? [])
|
|
266
|
-
this.#applyFtsEntry(changes, version);
|
|
267
|
-
}
|
|
268
|
-
#applyFtsEntry(changes, version) {
|
|
269
|
-
for (const column of changes.columns) {
|
|
270
|
-
const key = `${changes.tableId}/${column.columnId}`;
|
|
271
|
-
const deltas = this.#ftsDeltas.get(key) ??
|
|
272
|
-
new Map();
|
|
273
|
-
deltas.set(version, {
|
|
274
|
-
postings: structuredClone(column.postings),
|
|
275
|
-
totalTokens: column.totalTokens,
|
|
276
|
-
});
|
|
277
|
-
this.#ftsDeltas.set(key, deltas);
|
|
278
|
-
}
|
|
128
|
+
return this.#core.readFtsCandidates(tableId, columnId, terms, upToVersion);
|
|
279
129
|
}
|
|
280
130
|
async getTableByName(name) {
|
|
281
|
-
const id = this.#
|
|
131
|
+
const id = this.#core.getTableIdByName(name);
|
|
282
132
|
return id === undefined ? undefined : this.getTable(id);
|
|
283
133
|
}
|
|
284
134
|
async listTables() {
|
|
285
|
-
return
|
|
286
|
-
.sort((left, right) => left.name.localeCompare(right.name))
|
|
287
|
-
.map((record) => structuredClone(record));
|
|
135
|
+
return this.#core.listTables();
|
|
288
136
|
}
|
|
289
137
|
async addSegment(record) {
|
|
290
|
-
|
|
291
|
-
if (this.#segments.has(normalized.id)) {
|
|
292
|
-
throw new Error(`Segment already exists: ${normalized.id}`);
|
|
293
|
-
}
|
|
294
|
-
this.#segments.set(normalized.id, normalized);
|
|
138
|
+
this.#core.addSegment(record);
|
|
295
139
|
}
|
|
296
140
|
async getSegment(id) {
|
|
297
|
-
|
|
298
|
-
return record === undefined ? undefined : normalizeSegmentRecord(record);
|
|
141
|
+
return this.#core.getSegment(id);
|
|
299
142
|
}
|
|
300
143
|
async listSegments(tableId) {
|
|
301
|
-
return
|
|
302
|
-
.filter((record) => tableId === undefined || record.tableId === tableId)
|
|
303
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
304
|
-
.map((record) => normalizeSegmentRecord(record));
|
|
144
|
+
return this.#core.listSegments(tableId);
|
|
305
145
|
}
|
|
306
146
|
async removeSegment(id) {
|
|
307
|
-
this.#
|
|
147
|
+
this.#core.removeSegment(id);
|
|
308
148
|
}
|
|
309
149
|
async reserveRowIds(tableId, count) {
|
|
310
|
-
|
|
311
|
-
const start = this.#nextRowIds.get(tableId) ?? 1n;
|
|
312
|
-
const endExclusive = start + BigInt(count);
|
|
313
|
-
this.#nextRowIds.set(tableId, endExclusive);
|
|
314
|
-
return { start, endExclusive };
|
|
150
|
+
return this.#core.reserveRowIds(tableId, count);
|
|
315
151
|
}
|
|
316
152
|
async reserveAutoIncrement(tableId, columnId, count, atLeast) {
|
|
317
|
-
|
|
318
|
-
return this.#runAtomic(() => this.#reserveAutoIncrement(tableId, columnId, count, atLeast));
|
|
319
|
-
}
|
|
320
|
-
#reserveAutoIncrement(tableId, columnId, count, atLeast) {
|
|
321
|
-
const key = `${tableId}/${columnId}`;
|
|
322
|
-
const current = this.#nextAutoIncrement.get(key) ?? 1n;
|
|
323
|
-
const floor = atLeast ?? 1n;
|
|
324
|
-
const start = current > floor ? current : floor;
|
|
325
|
-
const endExclusive = start + BigInt(count);
|
|
326
|
-
this.#nextAutoIncrement.set(key, endExclusive);
|
|
327
|
-
return { start, endExclusive };
|
|
153
|
+
return this.#runAtomic(() => this.#core.reserveAutoIncrement(tableId, columnId, count, atLeast));
|
|
328
154
|
}
|
|
329
155
|
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
330
|
-
|
|
331
|
-
if (existing === undefined)
|
|
332
|
-
return [];
|
|
333
|
-
return [...new Set(keyTokens)].filter((token) => existing.has(token)).sort();
|
|
156
|
+
return this.#core.getExistingUniqueKeys(tableId, keyTokens);
|
|
334
157
|
}
|
|
335
158
|
async getCurrentManifestVersion() {
|
|
336
|
-
return this.#
|
|
159
|
+
return this.#core.getCurrentManifestVersion();
|
|
337
160
|
}
|
|
338
161
|
async getCatalogProbe() {
|
|
339
|
-
return
|
|
162
|
+
return this.#core.getCatalogProbe();
|
|
340
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Composed over this store's own methods rather than delegated to the core, so a subclass
|
|
166
|
+
* that instruments `getTransactions` or `listSegments` observes these internal reads too —
|
|
167
|
+
* the engine's read-count tests rely on that.
|
|
168
|
+
*/
|
|
341
169
|
async getQueryCatalogState(tableNames) {
|
|
342
170
|
const tables = await Promise.all(tableNames.map((name) => this.getTableByName(name)));
|
|
343
171
|
const foundTableIds = new Set(tables.filter((table) => table !== undefined).map((table) => table.id));
|
|
344
172
|
const segments = (await this.listSegments()).filter((record) => foundTableIds.has(record.tableId));
|
|
345
173
|
const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
|
|
346
174
|
const transactions = (await this.getTransactions(transactionIds)).filter((record) => record !== undefined);
|
|
175
|
+
const probe = this.#core.getCatalogProbe();
|
|
347
176
|
return {
|
|
348
|
-
manifestVersion:
|
|
177
|
+
manifestVersion: probe.manifestVersion,
|
|
349
178
|
tables,
|
|
350
179
|
segments,
|
|
351
180
|
transactions,
|
|
352
|
-
catalogEpoch:
|
|
181
|
+
catalogEpoch: probe.catalogEpoch,
|
|
353
182
|
};
|
|
354
183
|
}
|
|
355
184
|
async getCurrentManifest() {
|
|
356
|
-
|
|
357
|
-
return manifest === undefined ? undefined : structuredClone(manifest);
|
|
185
|
+
return this.#core.getCurrentManifest();
|
|
358
186
|
}
|
|
359
187
|
async getManifest(version) {
|
|
360
|
-
|
|
361
|
-
return manifest === undefined ? undefined : structuredClone(manifest);
|
|
188
|
+
return this.#core.getManifest(version);
|
|
362
189
|
}
|
|
363
190
|
async listManifests() {
|
|
364
|
-
return
|
|
365
|
-
.sort((left, right) => left.version - right.version)
|
|
366
|
-
.map((manifest) => structuredClone(manifest));
|
|
191
|
+
return this.#core.listManifests();
|
|
367
192
|
}
|
|
368
193
|
async listManifestPage(afterVersion, limit) {
|
|
369
|
-
|
|
370
|
-
const records = [...this.#manifests.values()]
|
|
371
|
-
.filter((manifest) => afterVersion === null || manifest.version > afterVersion)
|
|
372
|
-
.sort((left, right) => left.version - right.version)
|
|
373
|
-
.slice(0, limit)
|
|
374
|
-
.map((manifest) => structuredClone(manifest));
|
|
375
|
-
return {
|
|
376
|
-
records,
|
|
377
|
-
nextCursor: records.length === limit ? (records.at(-1)?.version ?? null) : null,
|
|
378
|
-
};
|
|
194
|
+
return this.#core.listManifestPage(afterVersion, limit);
|
|
379
195
|
}
|
|
380
196
|
async publishManifest(input) {
|
|
381
|
-
|
|
382
|
-
let rejectResult;
|
|
383
|
-
const result = new Promise((resolve, reject) => {
|
|
384
|
-
resolveResult = resolve;
|
|
385
|
-
rejectResult = reject;
|
|
386
|
-
});
|
|
387
|
-
this.#commitQueue = this.#commitQueue.then(() => {
|
|
388
|
-
try {
|
|
389
|
-
if (this.#currentVersion !== input.expectedVersion) {
|
|
390
|
-
throw new WriteConflictError(input.expectedVersion, this.#currentVersion);
|
|
391
|
-
}
|
|
392
|
-
for (const id of input.blockIds) {
|
|
393
|
-
if (!this.#blocks.has(id))
|
|
394
|
-
throw new Error(`Manifest references missing block: ${id}`);
|
|
395
|
-
}
|
|
396
|
-
const manifest = createManifest(input);
|
|
397
|
-
this.#manifests.set(manifest.version, manifest);
|
|
398
|
-
this.#currentVersion = manifest.version;
|
|
399
|
-
this.#catalogEpoch += 1;
|
|
400
|
-
resolveResult(structuredClone(manifest));
|
|
401
|
-
}
|
|
402
|
-
catch (error) {
|
|
403
|
-
rejectResult(error);
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
return result;
|
|
197
|
+
return this.#runAtomic(() => this.#core.publishManifest(input));
|
|
407
198
|
}
|
|
408
199
|
async beginTransaction(input) {
|
|
409
|
-
|
|
410
|
-
throw new TypeError("A fresh transaction cannot begin with pending artifacts");
|
|
411
|
-
}
|
|
412
|
-
return this.#runAtomic(() => {
|
|
413
|
-
const record = {
|
|
414
|
-
...structuredClone(input.record),
|
|
415
|
-
snapshotVersion: this.#currentVersion,
|
|
416
|
-
};
|
|
417
|
-
assertSnapshotAvailable(record.snapshotVersion, this.#manifests, this.#blocks);
|
|
418
|
-
if (this.#transactions.has(record.id)) {
|
|
419
|
-
throw new Error(`Transaction already exists: ${record.id}`);
|
|
420
|
-
}
|
|
421
|
-
this.#transactions.set(record.id, record);
|
|
422
|
-
let rowIds;
|
|
423
|
-
if (input.reserveRowIds !== undefined) {
|
|
424
|
-
validateCount(input.reserveRowIds.count);
|
|
425
|
-
const current = this.#nextRowIds.get(input.reserveRowIds.tableId) ?? 1n;
|
|
426
|
-
const endExclusive = current + BigInt(input.reserveRowIds.count);
|
|
427
|
-
this.#nextRowIds.set(input.reserveRowIds.tableId, endExclusive);
|
|
428
|
-
rowIds = { start: current, endExclusive };
|
|
429
|
-
}
|
|
430
|
-
let autoIncrementValues;
|
|
431
|
-
if (input.reserveAutoIncrement !== undefined) {
|
|
432
|
-
const { tableId, columnId, count, atLeast } = input.reserveAutoIncrement;
|
|
433
|
-
validateAutoIncrementReservation(count, atLeast);
|
|
434
|
-
autoIncrementValues = this.#reserveAutoIncrement(tableId, columnId, count, atLeast);
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
record: structuredClone(record),
|
|
438
|
-
...(rowIds === undefined ? {} : { rowIds }),
|
|
439
|
-
...(autoIncrementValues === undefined ? {} : { autoIncrementValues }),
|
|
440
|
-
};
|
|
441
|
-
});
|
|
200
|
+
return this.#runAtomic(() => this.#core.beginTransaction(input));
|
|
442
201
|
}
|
|
443
202
|
async createTransaction(record) {
|
|
444
203
|
return this.#runAtomic(() => {
|
|
445
|
-
|
|
446
|
-
throw new Error(`Transaction already exists: ${record.id}`);
|
|
447
|
-
}
|
|
448
|
-
assertSnapshotAvailable(record.snapshotVersion, this.#manifests, this.#blocks);
|
|
449
|
-
assertPendingArtifactsAvailable(record, this.#blocks, this.#segments);
|
|
450
|
-
this.#transactions.set(record.id, structuredClone(record));
|
|
204
|
+
this.#core.createTransaction(record);
|
|
451
205
|
});
|
|
452
206
|
}
|
|
453
207
|
async getTransaction(id) {
|
|
454
|
-
|
|
455
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
208
|
+
return this.#core.getTransaction(id);
|
|
456
209
|
}
|
|
457
210
|
async getTransactions(ids) {
|
|
458
|
-
return
|
|
459
|
-
const record = this.#transactions.get(id);
|
|
460
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
461
|
-
});
|
|
211
|
+
return this.#core.getTransactions(ids);
|
|
462
212
|
}
|
|
463
213
|
async listTransactions() {
|
|
464
|
-
return
|
|
465
|
-
.sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
|
|
466
|
-
.map((record) => structuredClone(record));
|
|
214
|
+
return this.#core.listTransactions();
|
|
467
215
|
}
|
|
468
216
|
async listTransactionPage(afterId, limit) {
|
|
469
|
-
|
|
470
|
-
const records = [...this.#transactions.values()]
|
|
471
|
-
.filter((record) => afterId === null || record.id > afterId)
|
|
472
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
473
|
-
.slice(0, limit)
|
|
474
|
-
.map((record) => structuredClone(record));
|
|
475
|
-
return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
|
|
217
|
+
return this.#core.listTransactionPage(afterId, limit);
|
|
476
218
|
}
|
|
477
219
|
async updateTransaction(id, expectedRevision, update) {
|
|
478
|
-
return this.#runAtomic(() =>
|
|
479
|
-
const current = this.#transactions.get(id);
|
|
480
|
-
if (current?.revision !== expectedRevision) {
|
|
481
|
-
throw new TransactionRecordConflictError(id, expectedRevision, current?.revision ?? null);
|
|
482
|
-
}
|
|
483
|
-
assertGenericTransactionUpdateAllowed(current, update);
|
|
484
|
-
const updated = updateTransactionRecord(current, update);
|
|
485
|
-
if (update.snapshotVersion !== undefined) {
|
|
486
|
-
assertSnapshotAvailable(updated.snapshotVersion, this.#manifests, this.#blocks);
|
|
487
|
-
}
|
|
488
|
-
assertPendingArtifactsAvailable(updated, this.#blocks, this.#segments, update.pendingBlockIds !== undefined, update.pendingSegmentIds !== undefined);
|
|
489
|
-
this.#transactions.set(id, updated);
|
|
490
|
-
return structuredClone(updated);
|
|
491
|
-
});
|
|
220
|
+
return this.#runAtomic(() => this.#core.updateTransaction(id, expectedRevision, update));
|
|
492
221
|
}
|
|
493
222
|
async stageTransactionArtifacts(input) {
|
|
494
223
|
return this.#runAtomic(() => {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
validateId(block.id);
|
|
498
|
-
if (ids.has(block.id) || this.#blocks.has(block.id)) {
|
|
499
|
-
throw new Error(`Block already exists: ${block.id}`);
|
|
500
|
-
}
|
|
501
|
-
ids.add(block.id);
|
|
502
|
-
}
|
|
503
|
-
for (const segment of input.segments) {
|
|
504
|
-
if (this.#segments.has(segment.id)) {
|
|
505
|
-
throw new Error(`Segment already exists: ${segment.id}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
const current = this.#transactions.get(input.transactionId);
|
|
509
|
-
if (current?.revision !== input.expectedRevision) {
|
|
510
|
-
throw new TransactionRecordConflictError(input.transactionId, input.expectedRevision, current?.revision ?? null);
|
|
511
|
-
}
|
|
512
|
-
const update = {
|
|
513
|
-
pendingBlockIds: [...current.pendingBlockIds, ...input.blocks.map((block) => block.id)],
|
|
514
|
-
pendingSegmentIds: [
|
|
515
|
-
...current.pendingSegmentIds,
|
|
516
|
-
...input.segments.map((segment) => segment.id),
|
|
517
|
-
],
|
|
518
|
-
updatedAt: input.updatedAt,
|
|
519
|
-
};
|
|
520
|
-
assertGenericTransactionUpdateAllowed(current, update);
|
|
521
|
-
const updated = updateTransactionRecord(current, update);
|
|
522
|
-
// Only previously journaled artifacts need existence checks; the new ones land with the
|
|
523
|
-
// journal update in this same atomic step.
|
|
524
|
-
assertPendingArtifactsAvailable(current, this.#blocks, this.#segments, true, true);
|
|
224
|
+
const updated = this.#core.stageTransactionArtifacts(input);
|
|
225
|
+
// The record half validated everything; the bytes land in this same atomic step.
|
|
525
226
|
for (const block of input.blocks)
|
|
526
227
|
this.#blocks.set(block.id, new Uint8Array(block.bytes));
|
|
527
|
-
|
|
528
|
-
this.#segments.set(segment.id, normalizeSegmentRecord(segment));
|
|
529
|
-
}
|
|
530
|
-
this.#transactions.set(input.transactionId, updated);
|
|
531
|
-
return structuredClone(updated);
|
|
228
|
+
return updated;
|
|
532
229
|
});
|
|
533
230
|
}
|
|
534
231
|
async commitTransaction(input) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (transaction?.revision !== input.expectedTransactionRevision ||
|
|
545
|
-
transaction.status !== "active") {
|
|
546
|
-
throw new TransactionRecordConflictError(input.transactionId, input.expectedTransactionRevision, transaction?.revision ?? null);
|
|
547
|
-
}
|
|
548
|
-
if (this.#currentVersion !== input.expectedManifestVersion) {
|
|
549
|
-
throw new WriteConflictError(input.expectedManifestVersion, this.#currentVersion);
|
|
550
|
-
}
|
|
551
|
-
if (transaction.snapshotVersion !== input.expectedManifestVersion) {
|
|
552
|
-
throw new Error("Transaction snapshot does not match the expected manifest");
|
|
553
|
-
}
|
|
554
|
-
const baseManifest = input.expectedManifestVersion === null
|
|
555
|
-
? undefined
|
|
556
|
-
: this.#manifests.get(input.expectedManifestVersion);
|
|
557
|
-
if (input.expectedManifestVersion !== null && baseManifest === undefined) {
|
|
558
|
-
throw new Error(`Snapshot manifest is missing: ${String(input.expectedManifestVersion)}`);
|
|
559
|
-
}
|
|
560
|
-
const baseBlockIds = baseManifest?.blockIds ?? [];
|
|
561
|
-
const removedBlockIds = [...new Set(input.removedBlockIds ?? [])];
|
|
562
|
-
const baseBlockIdSet = new Set(baseBlockIds);
|
|
563
|
-
for (const id of removedBlockIds) {
|
|
564
|
-
if (!baseBlockIdSet.has(id)) {
|
|
565
|
-
throw new Error(`Cannot supersede a block outside the transaction snapshot: ${id}`);
|
|
566
|
-
}
|
|
567
|
-
if (transaction.pendingBlockIds.includes(id)) {
|
|
568
|
-
throw new Error(`Cannot supersede a pending block: ${id}`);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
const removedBlockIdSet = new Set(removedBlockIds);
|
|
572
|
-
// The published list derives from the stored base plus this commit's delta; the memory
|
|
573
|
-
// store keeps full manifests internally since cloning in memory is cheap.
|
|
574
|
-
const nextBlockIds = [
|
|
575
|
-
...baseBlockIds.filter((id) => !removedBlockIdSet.has(id)),
|
|
576
|
-
...transaction.pendingBlockIds,
|
|
577
|
-
];
|
|
578
|
-
for (const id of transaction.pendingBlockIds) {
|
|
579
|
-
if (!this.#blocks.has(id))
|
|
580
|
-
throw new Error(`Manifest references missing block: ${id}`);
|
|
581
|
-
}
|
|
582
|
-
const pendingSegments = transaction.pendingSegmentIds.map((id) => {
|
|
583
|
-
const segment = this.#segments.get(id);
|
|
584
|
-
if (segment === undefined)
|
|
585
|
-
throw new Error(`Transaction references missing segment: ${id}`);
|
|
586
|
-
if (segment.transactionId !== transaction.id) {
|
|
587
|
-
throw new Error(`Segment ${id} belongs to another transaction`);
|
|
588
|
-
}
|
|
589
|
-
return segment;
|
|
590
|
-
});
|
|
591
|
-
const uniqueKeyEntries = input.uniqueKeyChanges ?? [];
|
|
592
|
-
{
|
|
593
|
-
// Entries apply in operation order over per-table working sets, so a scope that
|
|
594
|
-
// inserts the same key twice conflicts exactly like two separate commits would.
|
|
595
|
-
const working = new Map();
|
|
596
|
-
for (const entry of uniqueKeyEntries) {
|
|
597
|
-
let tokens = working.get(entry.tableId);
|
|
598
|
-
if (tokens === undefined) {
|
|
599
|
-
tokens = new Set(this.#uniqueKeys.get(entry.tableId) ?? []);
|
|
600
|
-
working.set(entry.tableId, tokens);
|
|
601
|
-
}
|
|
602
|
-
for (const token of entry.keyTokens) {
|
|
603
|
-
if (entry.remove === true) {
|
|
604
|
-
tokens.delete(token);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
if (entry.requireAbsent && tokens.has(token)) {
|
|
608
|
-
throw new UniqueKeyConflictError(entry.tableId, token);
|
|
609
|
-
}
|
|
610
|
-
tokens.add(token);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
const manifest = createManifest({
|
|
615
|
-
expectedVersion: input.expectedManifestVersion,
|
|
616
|
-
blockIds: nextBlockIds,
|
|
617
|
-
createdAt: input.committedAt,
|
|
618
|
-
...(input.changedTableIds === undefined
|
|
619
|
-
? {}
|
|
620
|
-
: { changedTableIds: input.changedTableIds }),
|
|
621
|
-
});
|
|
622
|
-
const committed = updateTransactionRecord(transaction, {
|
|
623
|
-
status: "committed",
|
|
624
|
-
committedVersion: manifest.version,
|
|
625
|
-
updatedAt: input.committedAt,
|
|
626
|
-
});
|
|
627
|
-
this.#manifests.set(manifest.version, manifest);
|
|
628
|
-
this.#currentVersion = manifest.version;
|
|
629
|
-
this.#transactions.set(transaction.id, committed);
|
|
630
|
-
for (const segment of pendingSegments) {
|
|
631
|
-
this.#segments.set(segment.id, {
|
|
632
|
-
...segment,
|
|
633
|
-
level: segment.level ?? 0,
|
|
634
|
-
logicalOrder: segment.logicalOrder ?? manifest.version,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
for (const entry of uniqueKeyEntries) {
|
|
638
|
-
const existing = this.#uniqueKeys.get(entry.tableId) ?? new Set();
|
|
639
|
-
entry.keyTokens.forEach((token) => {
|
|
640
|
-
if (entry.remove === true)
|
|
641
|
-
existing.delete(token);
|
|
642
|
-
else
|
|
643
|
-
existing.add(token);
|
|
644
|
-
});
|
|
645
|
-
this.#uniqueKeys.set(entry.tableId, existing);
|
|
646
|
-
}
|
|
647
|
-
this.#applyFtsChanges(pendingSegments, input.ftsChanges, manifest.version);
|
|
648
|
-
this.#catalogEpoch += 1;
|
|
649
|
-
// Match the IndexedDB store's observable commit shape: the summary without blockIds.
|
|
650
|
-
const { blockIds: _resolved, ...summary } = manifest;
|
|
651
|
-
void _resolved;
|
|
652
|
-
resolveResult(structuredClone(summary));
|
|
653
|
-
}
|
|
654
|
-
catch (error) {
|
|
655
|
-
rejectResult(error);
|
|
656
|
-
}
|
|
232
|
+
return this.#runAtomic(() => this.#core.commitTransaction(input));
|
|
233
|
+
}
|
|
234
|
+
async writeTransaction(input) {
|
|
235
|
+
return this.#runAtomic(() => {
|
|
236
|
+
const summary = this.#core.writeTransaction(input);
|
|
237
|
+
// The record half validated everything; the bytes land in this same atomic step.
|
|
238
|
+
for (const block of input.blocks)
|
|
239
|
+
this.#blocks.set(block.id, new Uint8Array(block.bytes));
|
|
240
|
+
return summary;
|
|
657
241
|
});
|
|
658
|
-
return result;
|
|
659
242
|
}
|
|
660
243
|
async createLease(record) {
|
|
661
|
-
validateLeaseExpiration(record.expiresAt);
|
|
662
244
|
return this.#runAtomic(() => {
|
|
663
|
-
|
|
664
|
-
throw new Error(`Lease already exists: ${record.id}`);
|
|
665
|
-
assertSnapshotAvailable(record.manifestVersion, this.#manifests, this.#blocks);
|
|
666
|
-
this.#leases.set(record.id, structuredClone(record));
|
|
245
|
+
this.#core.createLease(record);
|
|
667
246
|
});
|
|
668
247
|
}
|
|
669
248
|
async getLease(id) {
|
|
670
|
-
|
|
671
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
249
|
+
return this.#core.getLease(id);
|
|
672
250
|
}
|
|
673
251
|
async listLeases() {
|
|
674
|
-
return
|
|
675
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
676
|
-
.map((record) => structuredClone(record));
|
|
252
|
+
return this.#core.listLeases();
|
|
677
253
|
}
|
|
678
254
|
async renewLease(id, expectedRevision, expiresAt) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
throw new LeaseConflictError(id, expectedRevision, record?.revision ?? null);
|
|
684
|
-
}
|
|
685
|
-
assertSnapshotAvailable(record.manifestVersion, this.#manifests, this.#blocks);
|
|
686
|
-
const renewed = { ...record, expiresAt, revision: record.revision + 1 };
|
|
687
|
-
this.#leases.set(id, renewed);
|
|
688
|
-
return structuredClone(renewed);
|
|
689
|
-
});
|
|
255
|
+
return this.#runAtomic(() => this.#core.renewLease(id, expectedRevision, expiresAt));
|
|
256
|
+
}
|
|
257
|
+
async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
|
|
258
|
+
return this.#runAtomic(() => this.#core.moveLease(id, expectedRevision, manifestVersion, expiresAt));
|
|
690
259
|
}
|
|
691
260
|
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
692
|
-
|
|
693
|
-
if (!Number.isFinite(cutoff))
|
|
694
|
-
throw new TypeError("Lease expiry cutoff must be valid");
|
|
695
|
-
return this.#runAtomic(() => {
|
|
696
|
-
const record = this.#leases.get(id);
|
|
697
|
-
if (record?.revision !== expectedRevision) {
|
|
698
|
-
throw new LeaseConflictError(id, expectedRevision, record?.revision ?? null);
|
|
699
|
-
}
|
|
700
|
-
const expiresAt = Date.parse(record.expiresAt);
|
|
701
|
-
if (!Number.isFinite(expiresAt) || expiresAt > cutoff)
|
|
702
|
-
return false;
|
|
703
|
-
this.#leases.delete(id);
|
|
704
|
-
return true;
|
|
705
|
-
});
|
|
261
|
+
return this.#runAtomic(() => this.#core.removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff));
|
|
706
262
|
}
|
|
707
263
|
async removeLease(id) {
|
|
708
264
|
return this.#runAtomic(() => {
|
|
709
|
-
this.#
|
|
265
|
+
this.#core.removeLease(id);
|
|
710
266
|
});
|
|
711
267
|
}
|
|
712
268
|
async createCompactionJob(record) {
|
|
713
|
-
|
|
714
|
-
if (this.#compactionJobs.has(normalized.id)) {
|
|
715
|
-
throw new Error(`Compaction job already exists: ${normalized.id}`);
|
|
716
|
-
}
|
|
717
|
-
this.#compactionJobs.set(normalized.id, normalized);
|
|
269
|
+
this.#core.createCompactionJob(record);
|
|
718
270
|
}
|
|
719
271
|
async getCompactionJob(id) {
|
|
720
|
-
|
|
721
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
272
|
+
return this.#core.getCompactionJob(id);
|
|
722
273
|
}
|
|
723
274
|
async listCompactionJobs(tableId) {
|
|
724
|
-
return
|
|
725
|
-
.filter((record) => tableId === undefined || record.tableId === tableId)
|
|
726
|
-
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
|
727
|
-
.map((record) => structuredClone(record));
|
|
275
|
+
return this.#core.listCompactionJobs(tableId);
|
|
728
276
|
}
|
|
729
277
|
async listCompactionJobPage(afterId, limit) {
|
|
730
|
-
|
|
731
|
-
const records = [...this.#compactionJobs.values()]
|
|
732
|
-
.filter((record) => afterId === null || record.id > afterId)
|
|
733
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
734
|
-
.slice(0, limit)
|
|
735
|
-
.map((record) => structuredClone(record));
|
|
736
|
-
return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
|
|
278
|
+
return this.#core.listCompactionJobPage(afterId, limit);
|
|
737
279
|
}
|
|
738
280
|
async updateCompactionJob(id, expectedRevision, update) {
|
|
739
|
-
|
|
740
|
-
throw new TypeError("Use cancelCompactionJob to cancel a compaction job");
|
|
741
|
-
}
|
|
742
|
-
const current = this.#compactionJobs.get(id);
|
|
743
|
-
if (current?.revision !== expectedRevision) {
|
|
744
|
-
throw new CompactionJobConflictError(id, expectedRevision, current?.revision ?? null);
|
|
745
|
-
}
|
|
746
|
-
const updated = updateCompactionJobRecord(current, update);
|
|
747
|
-
this.#compactionJobs.set(id, updated);
|
|
748
|
-
return structuredClone(updated);
|
|
281
|
+
return this.#core.updateCompactionJob(id, expectedRevision, update);
|
|
749
282
|
}
|
|
750
283
|
async cancelCompactionJob(id, expectedRevision, cancelledAt) {
|
|
751
|
-
|
|
752
|
-
let rejectResult;
|
|
753
|
-
const result = new Promise((resolve, reject) => {
|
|
754
|
-
resolveResult = resolve;
|
|
755
|
-
rejectResult = reject;
|
|
756
|
-
});
|
|
757
|
-
this.#commitQueue = this.#commitQueue.then(() => {
|
|
758
|
-
try {
|
|
759
|
-
const current = this.#compactionJobs.get(id);
|
|
760
|
-
if (current?.revision !== expectedRevision) {
|
|
761
|
-
throw new CompactionJobConflictError(id, expectedRevision, current?.revision ?? null);
|
|
762
|
-
}
|
|
763
|
-
if (isTerminalCompactionJob(current)) {
|
|
764
|
-
resolveResult(structuredClone(current));
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
const transaction = current.transactionId === null
|
|
768
|
-
? undefined
|
|
769
|
-
: this.#transactions.get(current.transactionId);
|
|
770
|
-
if (transaction?.status === "committed") {
|
|
771
|
-
if (transaction.committedVersion === null) {
|
|
772
|
-
throw new Error(`Committed transaction has no manifest version: ${transaction.id}`);
|
|
773
|
-
}
|
|
774
|
-
const published = updateCompactionJobRecord(current, {
|
|
775
|
-
state: "published",
|
|
776
|
-
publishedVersion: transaction.committedVersion,
|
|
777
|
-
updatedAt: cancelledAt,
|
|
778
|
-
error: null,
|
|
779
|
-
});
|
|
780
|
-
this.#compactionJobs.set(id, published);
|
|
781
|
-
resolveResult(structuredClone(published));
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
const cancelled = updateCompactionJobRecord(current, {
|
|
785
|
-
state: "cancelled",
|
|
786
|
-
updatedAt: cancelledAt,
|
|
787
|
-
error: null,
|
|
788
|
-
});
|
|
789
|
-
const abortedTransaction = transaction?.status === "active"
|
|
790
|
-
? updateTransactionRecord(transaction, {
|
|
791
|
-
status: "aborted",
|
|
792
|
-
updatedAt: cancelledAt,
|
|
793
|
-
committedVersion: null,
|
|
794
|
-
})
|
|
795
|
-
: undefined;
|
|
796
|
-
if (abortedTransaction !== undefined) {
|
|
797
|
-
this.#transactions.set(abortedTransaction.id, abortedTransaction);
|
|
798
|
-
}
|
|
799
|
-
this.#compactionJobs.set(id, cancelled);
|
|
800
|
-
resolveResult(structuredClone(cancelled));
|
|
801
|
-
}
|
|
802
|
-
catch (error) {
|
|
803
|
-
rejectResult(error);
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
return result;
|
|
284
|
+
return this.#runAtomic(() => this.#core.cancelCompactionJob(id, expectedRevision, cancelledAt));
|
|
807
285
|
}
|
|
808
286
|
async removeCompactionJob(id) {
|
|
809
|
-
this.#
|
|
287
|
+
this.#core.removeCompactionJob(id);
|
|
810
288
|
}
|
|
811
289
|
async createGarbageCollectionJob(input) {
|
|
812
|
-
|
|
813
|
-
return this.#runAtomic(() => {
|
|
814
|
-
if (this.#garbageCollectionJobs.has(record.id)) {
|
|
815
|
-
throw new Error(`Garbage collection job already exists: ${record.id}`);
|
|
816
|
-
}
|
|
817
|
-
assertGarbageCollectionCandidateProvenance(record, this.#manifests, this.#segments, this.#transactions, this.#compactionJobs);
|
|
818
|
-
this.#garbageCollectionJobs.set(record.id, record);
|
|
819
|
-
return structuredClone(record);
|
|
820
|
-
});
|
|
290
|
+
return this.#runAtomic(() => this.#core.createGarbageCollectionJob(input));
|
|
821
291
|
}
|
|
822
292
|
async getGarbageCollectionJob(id) {
|
|
823
|
-
|
|
824
|
-
return record === undefined ? undefined : structuredClone(record);
|
|
293
|
+
return this.#core.getGarbageCollectionJob(id);
|
|
825
294
|
}
|
|
826
295
|
async listGarbageCollectionJobs() {
|
|
827
|
-
return
|
|
828
|
-
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
|
829
|
-
.map((record) => structuredClone(record));
|
|
296
|
+
return this.#core.listGarbageCollectionJobs();
|
|
830
297
|
}
|
|
831
298
|
async runGarbageCollectionStep(input) {
|
|
832
|
-
validateGarbageCollectionStepInput(input);
|
|
833
299
|
return this.#runAtomic(() => {
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
if (current.state === "completed")
|
|
839
|
-
return emptyGarbageCollectionStep(current);
|
|
840
|
-
const prunedManifestVersions = [];
|
|
841
|
-
const alreadyPrunedManifestVersions = [];
|
|
842
|
-
const retainedManifestVersions = [];
|
|
843
|
-
const missingManifestVersions = [];
|
|
844
|
-
const reclaimedSegmentIds = [];
|
|
845
|
-
const retainedSegmentIds = [];
|
|
846
|
-
const missingSegmentIds = [];
|
|
847
|
-
const reclaimedBlockIds = [];
|
|
848
|
-
const retainedBlockIds = [];
|
|
849
|
-
const missingBlockIds = [];
|
|
850
|
-
let reclaimedBlockBytes = 0;
|
|
851
|
-
let remaining = input.maxItems;
|
|
852
|
-
const leaseCutoff = Date.parse(current.leaseCutoff);
|
|
853
|
-
assertGarbageCollectionPinsAvailable(this.#currentVersion, this.#transactions, this.#leases.values(), this.#compactionJobs.values(), leaseCutoff, this.#manifests, this.#blocks);
|
|
854
|
-
let manifestIndex = current.cursor.manifestIndex;
|
|
855
|
-
while (remaining > 0 && manifestIndex < current.candidateManifestVersions.length) {
|
|
856
|
-
const version = current.candidateManifestVersions[manifestIndex];
|
|
857
|
-
if (version === undefined)
|
|
858
|
-
throw new Error("Garbage collection manifest cursor is invalid");
|
|
859
|
-
const manifest = this.#manifests.get(version);
|
|
860
|
-
if (manifest === undefined)
|
|
861
|
-
missingManifestVersions.push(version);
|
|
862
|
-
else if (manifest.prunedAt !== undefined)
|
|
863
|
-
alreadyPrunedManifestVersions.push(version);
|
|
864
|
-
else if (isManifestVersionPinned(version, this.#currentVersion, this.#transactions, this.#leases.values(), this.#compactionJobs.values(), leaseCutoff))
|
|
865
|
-
retainedManifestVersions.push(version);
|
|
866
|
-
else
|
|
867
|
-
prunedManifestVersions.push(version);
|
|
868
|
-
manifestIndex += 1;
|
|
869
|
-
remaining -= 1;
|
|
870
|
-
}
|
|
871
|
-
const prunedManifestVersionSet = new Set(prunedManifestVersions);
|
|
872
|
-
assertRemainingManifestBlocksAvailable(this.#manifests.values(), prunedManifestVersionSet, this.#blocks);
|
|
873
|
-
let segmentIndex = current.cursor.segmentIndex;
|
|
874
|
-
const segmentIdsToExamine = manifestIndex === current.candidateManifestVersions.length
|
|
875
|
-
? current.candidateSegmentIds.slice(segmentIndex, segmentIndex + remaining)
|
|
876
|
-
: [];
|
|
877
|
-
const blockCapacity = Math.max(0, remaining - segmentIdsToExamine.length);
|
|
878
|
-
const blockIdsToExamine = manifestIndex === current.candidateManifestVersions.length &&
|
|
879
|
-
segmentIndex + segmentIdsToExamine.length === current.candidateSegmentIds.length
|
|
880
|
-
? current.candidateBlockIds.slice(current.cursor.blockIndex, current.cursor.blockIndex + blockCapacity)
|
|
881
|
-
: [];
|
|
882
|
-
const roots = collectBoundedPhysicalRoots(segmentIdsToExamine, blockIdsToExamine, this.#manifests.values(), prunedManifestVersionSet, this.#segments, this.#transactions, this.#compactionJobs.values());
|
|
883
|
-
while (remaining > 0 &&
|
|
884
|
-
manifestIndex === current.candidateManifestVersions.length &&
|
|
885
|
-
segmentIndex < current.candidateSegmentIds.length) {
|
|
886
|
-
const id = current.candidateSegmentIds[segmentIndex];
|
|
887
|
-
if (id === undefined)
|
|
888
|
-
throw new Error("Garbage collection segment cursor is invalid");
|
|
889
|
-
if (!this.#segments.has(id))
|
|
890
|
-
missingSegmentIds.push(id);
|
|
891
|
-
else if (roots.segmentIds.has(id))
|
|
892
|
-
retainedSegmentIds.push(id);
|
|
893
|
-
else
|
|
894
|
-
reclaimedSegmentIds.push(id);
|
|
895
|
-
segmentIndex += 1;
|
|
896
|
-
remaining -= 1;
|
|
897
|
-
}
|
|
898
|
-
let blockIndex = current.cursor.blockIndex;
|
|
899
|
-
while (remaining > 0 &&
|
|
900
|
-
manifestIndex === current.candidateManifestVersions.length &&
|
|
901
|
-
segmentIndex === current.candidateSegmentIds.length &&
|
|
902
|
-
blockIndex < current.candidateBlockIds.length) {
|
|
903
|
-
const id = current.candidateBlockIds[blockIndex];
|
|
904
|
-
if (id === undefined)
|
|
905
|
-
throw new Error("Garbage collection block cursor is invalid");
|
|
906
|
-
const bytes = this.#blocks.get(id);
|
|
907
|
-
if (bytes === undefined)
|
|
908
|
-
missingBlockIds.push(id);
|
|
909
|
-
else if (roots.blockIds.has(id))
|
|
910
|
-
retainedBlockIds.push(id);
|
|
911
|
-
else {
|
|
912
|
-
reclaimedBlockIds.push(id);
|
|
913
|
-
reclaimedBlockBytes = safeStorageSum(reclaimedBlockBytes, bytes.byteLength);
|
|
914
|
-
}
|
|
915
|
-
blockIndex += 1;
|
|
916
|
-
remaining -= 1;
|
|
917
|
-
}
|
|
918
|
-
const updated = advanceGarbageCollectionJobRecord(current, {
|
|
919
|
-
examinedManifestCount: prunedManifestVersions.length +
|
|
920
|
-
alreadyPrunedManifestVersions.length +
|
|
921
|
-
retainedManifestVersions.length +
|
|
922
|
-
missingManifestVersions.length,
|
|
923
|
-
prunedManifestCount: prunedManifestVersions.length,
|
|
924
|
-
alreadyPrunedManifestCount: alreadyPrunedManifestVersions.length,
|
|
925
|
-
retainedManifestCount: retainedManifestVersions.length,
|
|
926
|
-
missingManifestCount: missingManifestVersions.length,
|
|
927
|
-
examinedSegmentCount: reclaimedSegmentIds.length + retainedSegmentIds.length + missingSegmentIds.length,
|
|
928
|
-
reclaimedSegmentCount: reclaimedSegmentIds.length,
|
|
929
|
-
retainedSegmentCount: retainedSegmentIds.length,
|
|
930
|
-
missingSegmentCount: missingSegmentIds.length,
|
|
931
|
-
examinedBlockCount: reclaimedBlockIds.length + retainedBlockIds.length + missingBlockIds.length,
|
|
932
|
-
reclaimedBlockCount: reclaimedBlockIds.length,
|
|
933
|
-
retainedBlockCount: retainedBlockIds.length,
|
|
934
|
-
missingBlockCount: missingBlockIds.length,
|
|
935
|
-
reclaimedBlockBytes,
|
|
936
|
-
updatedAt: input.updatedAt,
|
|
937
|
-
});
|
|
938
|
-
prunedManifestVersions.forEach((version) => {
|
|
939
|
-
const manifest = this.#manifests.get(version);
|
|
940
|
-
if (manifest !== undefined)
|
|
941
|
-
this.#manifests.set(version, { ...manifest, prunedAt: input.updatedAt });
|
|
942
|
-
});
|
|
943
|
-
reclaimedSegmentIds.forEach((id) => this.#segments.delete(id));
|
|
944
|
-
reclaimedBlockIds.forEach((id) => this.#blocks.delete(id));
|
|
945
|
-
this.#garbageCollectionJobs.set(updated.id, updated);
|
|
946
|
-
return {
|
|
947
|
-
job: structuredClone(updated),
|
|
948
|
-
prunedManifestVersions,
|
|
949
|
-
alreadyPrunedManifestVersions,
|
|
950
|
-
retainedManifestVersions,
|
|
951
|
-
missingManifestVersions,
|
|
952
|
-
reclaimedSegmentIds,
|
|
953
|
-
retainedSegmentIds,
|
|
954
|
-
missingSegmentIds,
|
|
955
|
-
reclaimedBlockIds,
|
|
956
|
-
retainedBlockIds,
|
|
957
|
-
missingBlockIds,
|
|
958
|
-
reclaimedBlockBytes,
|
|
959
|
-
};
|
|
300
|
+
const result = this.#core.runGarbageCollectionStep(input);
|
|
301
|
+
result.reclaimedBlockIds.forEach((id) => this.#blocks.delete(id));
|
|
302
|
+
return result;
|
|
960
303
|
});
|
|
961
304
|
}
|
|
305
|
+
async removePrunedManifestRecords() {
|
|
306
|
+
return this.#runAtomic(() => this.#core.removePrunedManifestRecords().length);
|
|
307
|
+
}
|
|
962
308
|
async removeGarbageCollectionJob(id) {
|
|
963
309
|
return this.#runAtomic(() => {
|
|
964
|
-
this.#
|
|
310
|
+
this.#core.removeGarbageCollectionJob(id);
|
|
965
311
|
});
|
|
966
312
|
}
|
|
967
313
|
/**
|
|
968
314
|
* Copies the current version out as a portable snapshot. Runs on the commit queue, so it sees
|
|
969
|
-
* one consistent version rather than a commit in progress.
|
|
970
|
-
*
|
|
971
|
-
* Only blocks the current manifest still points at are copied, which drops every superseded
|
|
972
|
-
* block a compaction left behind, and only full-text bases that already cover this exact
|
|
973
|
-
* version are carried; any other indexed column is exported as `invalid` so the loaded
|
|
974
|
-
* database rebuilds it. That costs a rebuild, never a wrong answer — the index is a pruning
|
|
975
|
-
* accelerator and the scan re-verifies every candidate.
|
|
315
|
+
* one consistent version rather than a commit in progress. What the snapshot carries and
|
|
316
|
+
* drops is `RecordCore.exportSnapshot`'s contract.
|
|
976
317
|
*/
|
|
977
318
|
exportSnapshot() {
|
|
978
|
-
return this.#runAtomic(() =>
|
|
979
|
-
const version = this.#currentVersion;
|
|
980
|
-
if (version === null)
|
|
981
|
-
throw new Error("There is no committed version to snapshot");
|
|
982
|
-
const manifest = this.#manifests.get(version);
|
|
983
|
-
if (manifest === undefined)
|
|
984
|
-
throw new SnapshotManifestMissingError(version);
|
|
985
|
-
const liveBlockIds = new Set(manifest.blockIds);
|
|
986
|
-
const blocks = manifest.blockIds.map((id) => {
|
|
987
|
-
const bytes = this.#blocks.get(id);
|
|
988
|
-
if (bytes === undefined)
|
|
989
|
-
throw new Error(`Manifest references missing block: ${id}`);
|
|
990
|
-
return { id, bytes: bytes.slice() };
|
|
991
|
-
});
|
|
992
|
-
const { segments, transactions } = selectLiveRecords({
|
|
993
|
-
liveBlockIds,
|
|
994
|
-
segments: [...this.#segments.values()].map((record) => normalizeSegmentRecord(record)),
|
|
995
|
-
transactions: [...this.#transactions.values()],
|
|
996
|
-
version,
|
|
997
|
-
});
|
|
998
|
-
const tables = [...this.#tables.values()].map((table) => {
|
|
999
|
-
const fts = [];
|
|
1000
|
-
const ftsColumns = { ...(table.ftsColumns ?? {}) };
|
|
1001
|
-
for (const [columnId, state] of Object.entries(ftsColumns)) {
|
|
1002
|
-
const base = this.#ftsBases.get(`${table.id}/${columnId}`);
|
|
1003
|
-
if (state.state === "ready" && base?.coversVersion === version) {
|
|
1004
|
-
fts.push({ columnId, ...structuredClone(base) });
|
|
1005
|
-
}
|
|
1006
|
-
else {
|
|
1007
|
-
ftsColumns[columnId] = { ...state, state: "invalid" };
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return {
|
|
1011
|
-
record: {
|
|
1012
|
-
...structuredClone(table),
|
|
1013
|
-
...(Object.keys(ftsColumns).length === 0 ? {} : { ftsColumns }),
|
|
1014
|
-
},
|
|
1015
|
-
nextRowId: this.#nextRowIds.get(table.id) ?? 1n,
|
|
1016
|
-
autoIncrement: table.columns.flatMap((column) => {
|
|
1017
|
-
const next = this.#nextAutoIncrement.get(`${table.id}/${column.id}`);
|
|
1018
|
-
return next === undefined ? [] : [{ columnId: column.id, next }];
|
|
1019
|
-
}),
|
|
1020
|
-
uniqueKeyTokens: [...(this.#uniqueKeys.get(table.id) ?? [])],
|
|
1021
|
-
fts,
|
|
1022
|
-
};
|
|
1023
|
-
});
|
|
1024
|
-
return {
|
|
1025
|
-
version,
|
|
1026
|
-
createdAt: new Date().toISOString(),
|
|
1027
|
-
tables,
|
|
1028
|
-
segments,
|
|
1029
|
-
transactions,
|
|
1030
|
-
blocks,
|
|
1031
|
-
};
|
|
1032
|
-
});
|
|
319
|
+
return this.#runAtomic(() => this.#core.exportSnapshot((id) => this.#blocks.get(id)));
|
|
1033
320
|
}
|
|
1034
321
|
/**
|
|
1035
322
|
* Loads a snapshot into this store, which must be empty — the same contract as the IndexedDB
|
|
@@ -1044,59 +331,24 @@ export class MemoryBlockStore {
|
|
|
1044
331
|
const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
|
|
1045
332
|
options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
|
|
1046
333
|
await this.#runAtomic(() => {
|
|
1047
|
-
|
|
1048
|
-
throw new Error("This store already holds a database");
|
|
1049
|
-
if (this.#tables.size > 0)
|
|
1050
|
-
throw new Error("This store already holds a catalog");
|
|
1051
|
-
this.#loadSnapshot(snapshot);
|
|
334
|
+
this.#core.importSnapshot(snapshot, (id, bytes) => this.#blocks.set(id, bytes.slice()));
|
|
1052
335
|
});
|
|
1053
336
|
options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
|
|
1054
337
|
}
|
|
1055
338
|
/** Builds a store holding exactly what the snapshot captured. */
|
|
1056
339
|
static fromSnapshot(snapshot) {
|
|
1057
340
|
const store = new MemoryBlockStore();
|
|
1058
|
-
store.#loadSnapshot(snapshot);
|
|
341
|
+
store.#core.loadSnapshot(snapshot, (id, bytes) => store.#blocks.set(id, bytes.slice()));
|
|
1059
342
|
return store;
|
|
1060
343
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
for (const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
for (const table of snapshot.tables) {
|
|
1071
|
-
const record = structuredClone(table.record);
|
|
1072
|
-
this.#tables.set(record.id, record);
|
|
1073
|
-
this.#tableIdsByName.set(record.name, record.id);
|
|
1074
|
-
this.#nextRowIds.set(record.id, table.nextRowId);
|
|
1075
|
-
for (const entry of table.autoIncrement) {
|
|
1076
|
-
this.#nextAutoIncrement.set(`${record.id}/${entry.columnId}`, entry.next);
|
|
1077
|
-
}
|
|
1078
|
-
if (table.uniqueKeyTokens.length > 0) {
|
|
1079
|
-
this.#uniqueKeys.set(record.id, new Set(table.uniqueKeyTokens));
|
|
1080
|
-
}
|
|
1081
|
-
for (const index of table.fts) {
|
|
1082
|
-
this.#ftsBases.set(`${record.id}/${index.columnId}`, {
|
|
1083
|
-
coversVersion: index.coversVersion,
|
|
1084
|
-
chunks: structuredClone(index.chunks),
|
|
1085
|
-
totalTokens: index.totalTokens,
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
// One checkpoint, not the captured history: a snapshot restores as a single readable
|
|
1090
|
-
// version, and the version number itself is preserved so committed transactions still
|
|
1091
|
-
// sort and compare against it.
|
|
1092
|
-
this.#manifests.set(snapshot.version, {
|
|
1093
|
-
version: snapshot.version,
|
|
1094
|
-
previousVersion: null,
|
|
1095
|
-
blockIds: snapshot.blocks.map((block) => block.id),
|
|
1096
|
-
createdAt: snapshot.createdAt,
|
|
1097
|
-
});
|
|
1098
|
-
this.#currentVersion = snapshot.version;
|
|
1099
|
-
this.#catalogEpoch += 1;
|
|
344
|
+
/** Block and spill bytes held; the record maps are negligible beside them. */
|
|
345
|
+
async getLogicalStorageBytes() {
|
|
346
|
+
let total = 0;
|
|
347
|
+
for (const bytes of this.#blocks.values())
|
|
348
|
+
total += bytes.byteLength;
|
|
349
|
+
for (const bytes of this.#tempRunPages.values())
|
|
350
|
+
total += bytes.byteLength;
|
|
351
|
+
return total;
|
|
1100
352
|
}
|
|
1101
353
|
close() {
|
|
1102
354
|
// The in-memory implementation owns no external resources.
|
|
@@ -1119,337 +371,10 @@ export class MemoryBlockStore {
|
|
|
1119
371
|
return result;
|
|
1120
372
|
}
|
|
1121
373
|
}
|
|
1122
|
-
function validateId(id) {
|
|
1123
|
-
if (id.length === 0)
|
|
1124
|
-
throw new TypeError("Block ID cannot be empty");
|
|
1125
|
-
}
|
|
1126
|
-
function validateCount(count) {
|
|
1127
|
-
if (!Number.isSafeInteger(count) || count <= 0) {
|
|
1128
|
-
throw new RangeError("Row ID reservation count must be a positive whole number");
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
function validateAutoIncrementReservation(count, atLeast) {
|
|
1132
|
-
if (!Number.isSafeInteger(count) || count < 0) {
|
|
1133
|
-
throw new RangeError("Auto-increment reservation count must be a non-negative whole number");
|
|
1134
|
-
}
|
|
1135
|
-
if (atLeast !== undefined && atLeast < 1n) {
|
|
1136
|
-
throw new RangeError("Auto-increment bump target must be at least 1");
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
function isTerminalCompactionJob(record) {
|
|
1140
|
-
return record.state === "published" || record.state === "cancelled" || record.state === "aborted";
|
|
1141
|
-
}
|
|
1142
|
-
function assertGenericTransactionUpdateAllowed(record, update) {
|
|
1143
|
-
if (record.status !== "active") {
|
|
1144
|
-
throw new TypeError(`Only active transactions can be updated; found ${record.status}`);
|
|
1145
|
-
}
|
|
1146
|
-
if (update.status === "committed") {
|
|
1147
|
-
throw new TypeError("Use commitTransaction to commit a transaction");
|
|
1148
|
-
}
|
|
1149
|
-
if (Reflect.has(update, "committedVersion")) {
|
|
1150
|
-
throw new TypeError("Only commitTransaction can set a committed transaction version");
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
function assertSnapshotAvailable(version, manifests, blocks) {
|
|
1154
|
-
if (version === null)
|
|
1155
|
-
return;
|
|
1156
|
-
const manifest = manifests.get(version);
|
|
1157
|
-
if (manifest === undefined ||
|
|
1158
|
-
manifest.prunedAt !== undefined ||
|
|
1159
|
-
manifest.blockIds.some((id) => !blocks.has(id))) {
|
|
1160
|
-
throw new SnapshotManifestMissingError(version);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
function assertGarbageCollectionPinsAvailable(currentVersion, transactions, leases, compactionJobs, leaseCutoff, manifests, blocks) {
|
|
1164
|
-
if (currentVersion !== null)
|
|
1165
|
-
assertSnapshotAvailable(currentVersion, manifests, blocks);
|
|
1166
|
-
for (const transaction of transactions.values()) {
|
|
1167
|
-
if (transaction.status === "active") {
|
|
1168
|
-
assertSnapshotAvailable(transaction.snapshotVersion, manifests, blocks);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
for (const lease of leases) {
|
|
1172
|
-
const expiresAt = Date.parse(lease.expiresAt);
|
|
1173
|
-
if (!Number.isFinite(expiresAt) || expiresAt > leaseCutoff) {
|
|
1174
|
-
assertSnapshotAvailable(lease.manifestVersion, manifests, blocks);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
for (const job of compactionJobs) {
|
|
1178
|
-
if (isTerminalCompactionJob(job))
|
|
1179
|
-
continue;
|
|
1180
|
-
assertSnapshotAvailable(job.sourceManifestVersion, manifests, blocks);
|
|
1181
|
-
const linkedTransaction = job.transactionId === null ? undefined : transactions.get(job.transactionId);
|
|
1182
|
-
if (linkedTransaction?.status === "committed") {
|
|
1183
|
-
if (linkedTransaction.committedVersion === null) {
|
|
1184
|
-
throw new Error(`Committed transaction has no manifest version: ${linkedTransaction.id}`);
|
|
1185
|
-
}
|
|
1186
|
-
assertSnapshotAvailable(linkedTransaction.committedVersion, manifests, blocks);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
function assertPendingArtifactsAvailable(transaction, blocks, segments, validateBlocks = true, validateSegments = true) {
|
|
1191
|
-
const missingBlockId = validateBlocks
|
|
1192
|
-
? transaction.pendingBlockIds.find((id) => !blocks.has(id))
|
|
1193
|
-
: undefined;
|
|
1194
|
-
if (missingBlockId !== undefined) {
|
|
1195
|
-
throw new Error(`Transaction references missing pending block: ${missingBlockId}`);
|
|
1196
|
-
}
|
|
1197
|
-
const missingSegmentId = validateSegments
|
|
1198
|
-
? transaction.pendingSegmentIds.find((id) => !segments.has(id))
|
|
1199
|
-
: undefined;
|
|
1200
|
-
if (missingSegmentId !== undefined) {
|
|
1201
|
-
throw new Error(`Transaction references missing pending segment: ${missingSegmentId}`);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
function assertRemainingManifestBlocksAvailable(manifests, newlyPrunedVersions, blocks) {
|
|
1205
|
-
for (const manifest of manifests) {
|
|
1206
|
-
if (manifest.prunedAt !== undefined || newlyPrunedVersions.has(manifest.version))
|
|
1207
|
-
continue;
|
|
1208
|
-
if (manifest.blockIds.some((id) => !blocks.has(id))) {
|
|
1209
|
-
throw new SnapshotManifestMissingError(manifest.version);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
function assertGarbageCollectionCandidateProvenance(job, manifests, segments, transactions, compactionJobs) {
|
|
1214
|
-
for (const version of job.candidateManifestVersions) {
|
|
1215
|
-
if (!manifests.has(version)) {
|
|
1216
|
-
throw new Error(`Garbage collection candidate manifest is missing: ${String(version)}`);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
const blockHasProvenance = (id) => {
|
|
1220
|
-
for (const version of job.candidateManifestVersions) {
|
|
1221
|
-
if (manifests.get(version)?.blockIds.includes(id))
|
|
1222
|
-
return true;
|
|
1223
|
-
}
|
|
1224
|
-
for (const transaction of transactions.values()) {
|
|
1225
|
-
if (transaction.status === "aborted" && transaction.pendingBlockIds.includes(id))
|
|
1226
|
-
return true;
|
|
1227
|
-
}
|
|
1228
|
-
for (const compaction of compactionJobs.values()) {
|
|
1229
|
-
if (isTerminalCompactionJob(compaction) &&
|
|
1230
|
-
(compaction.sourceBlockIds.includes(id) || compaction.outputBlockIds.includes(id))) {
|
|
1231
|
-
return true;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
return false;
|
|
1235
|
-
};
|
|
1236
|
-
const unprovenBlockId = job.candidateBlockIds.find((id) => !blockHasProvenance(id));
|
|
1237
|
-
if (unprovenBlockId !== undefined) {
|
|
1238
|
-
throw new Error(`Garbage collection block candidate has no persisted provenance: ${unprovenBlockId}`);
|
|
1239
|
-
}
|
|
1240
|
-
const unprovenSegmentId = job.candidateSegmentIds.find((id) => {
|
|
1241
|
-
for (const transaction of transactions.values()) {
|
|
1242
|
-
if (transaction.status === "aborted" && transaction.pendingSegmentIds.includes(id)) {
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
for (const compaction of compactionJobs.values()) {
|
|
1247
|
-
if (isTerminalCompactionJob(compaction) &&
|
|
1248
|
-
(compaction.sourceSegmentIds.includes(id) || compaction.outputSegmentId === id)) {
|
|
1249
|
-
return false;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
const segment = segments.get(id);
|
|
1253
|
-
const blockIds = segment === undefined ? [] : segmentBlockIds(segment);
|
|
1254
|
-
return blockIds.length === 0 || !blockIds.every(blockHasProvenance);
|
|
1255
|
-
});
|
|
1256
|
-
if (unprovenSegmentId !== undefined) {
|
|
1257
|
-
throw new Error(`Garbage collection segment candidate has no persisted provenance: ${unprovenSegmentId}`);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
function isManifestVersionPinned(version, currentVersion, transactions, leases, compactionJobs, leaseCutoff) {
|
|
1261
|
-
if (currentVersion === version)
|
|
1262
|
-
return true;
|
|
1263
|
-
for (const transaction of transactions.values()) {
|
|
1264
|
-
if (transaction.status === "active" && transaction.snapshotVersion === version)
|
|
1265
|
-
return true;
|
|
1266
|
-
}
|
|
1267
|
-
for (const lease of leases) {
|
|
1268
|
-
const expiresAt = Date.parse(lease.expiresAt);
|
|
1269
|
-
if (lease.manifestVersion === version &&
|
|
1270
|
-
(!Number.isFinite(expiresAt) || expiresAt > leaseCutoff)) {
|
|
1271
|
-
return true;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
for (const job of compactionJobs) {
|
|
1275
|
-
if (isTerminalCompactionJob(job))
|
|
1276
|
-
continue;
|
|
1277
|
-
if (job.sourceManifestVersion === version)
|
|
1278
|
-
return true;
|
|
1279
|
-
const linkedTransaction = job.transactionId === null ? undefined : transactions.get(job.transactionId);
|
|
1280
|
-
if (linkedTransaction?.status === "committed") {
|
|
1281
|
-
if (linkedTransaction.committedVersion === null) {
|
|
1282
|
-
throw new Error(`Committed transaction has no manifest version: ${linkedTransaction.id}`);
|
|
1283
|
-
}
|
|
1284
|
-
if (linkedTransaction.committedVersion === version)
|
|
1285
|
-
return true;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return false;
|
|
1289
|
-
}
|
|
1290
|
-
const MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES = 4_096;
|
|
1291
|
-
function collectBoundedPhysicalRoots(candidateSegmentIds, candidateBlockIds, manifests, newlyPrunedVersions, segments, transactions, compactionJobs) {
|
|
1292
|
-
const candidateBlocks = new Set(candidateBlockIds);
|
|
1293
|
-
const probeBlockIds = new Set(candidateBlockIds);
|
|
1294
|
-
const probeSegmentIds = new Set(candidateSegmentIds);
|
|
1295
|
-
const relatedSegments = new Map();
|
|
1296
|
-
let dependencyOverflow = false;
|
|
1297
|
-
for (const segment of segments.values()) {
|
|
1298
|
-
const ids = segmentBlockIds(segment);
|
|
1299
|
-
if (!candidateSegmentIds.includes(segment.id) && !ids.some((id) => candidateBlocks.has(id))) {
|
|
1300
|
-
continue;
|
|
1301
|
-
}
|
|
1302
|
-
const newBlockIds = new Set(ids.filter((id) => !probeBlockIds.has(id)));
|
|
1303
|
-
if (relatedSegments.size >= MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES ||
|
|
1304
|
-
probeBlockIds.size + newBlockIds.size > MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES) {
|
|
1305
|
-
dependencyOverflow = true;
|
|
1306
|
-
break;
|
|
1307
|
-
}
|
|
1308
|
-
relatedSegments.set(segment.id, segment);
|
|
1309
|
-
probeSegmentIds.add(segment.id);
|
|
1310
|
-
ids.forEach((id) => probeBlockIds.add(id));
|
|
1311
|
-
}
|
|
1312
|
-
if (dependencyOverflow) {
|
|
1313
|
-
return { blockIds: new Set(candidateBlockIds), segmentIds: new Set(candidateSegmentIds) };
|
|
1314
|
-
}
|
|
1315
|
-
const directBlockRoots = new Set();
|
|
1316
|
-
const directSegmentRoots = new Set();
|
|
1317
|
-
for (const manifest of manifests) {
|
|
1318
|
-
if (manifest.prunedAt !== undefined || newlyPrunedVersions.has(manifest.version))
|
|
1319
|
-
continue;
|
|
1320
|
-
for (const id of manifest.blockIds)
|
|
1321
|
-
if (probeBlockIds.has(id))
|
|
1322
|
-
directBlockRoots.add(id);
|
|
1323
|
-
}
|
|
1324
|
-
for (const transaction of transactions.values()) {
|
|
1325
|
-
if (transaction.status !== "active")
|
|
1326
|
-
continue;
|
|
1327
|
-
for (const id of transaction.pendingBlockIds)
|
|
1328
|
-
if (probeBlockIds.has(id))
|
|
1329
|
-
directBlockRoots.add(id);
|
|
1330
|
-
for (const id of transaction.pendingSegmentIds)
|
|
1331
|
-
if (probeSegmentIds.has(id))
|
|
1332
|
-
directSegmentRoots.add(id);
|
|
1333
|
-
}
|
|
1334
|
-
for (const job of compactionJobs) {
|
|
1335
|
-
if (isTerminalCompactionJob(job))
|
|
1336
|
-
continue;
|
|
1337
|
-
for (const id of job.sourceBlockIds)
|
|
1338
|
-
if (probeBlockIds.has(id))
|
|
1339
|
-
directBlockRoots.add(id);
|
|
1340
|
-
for (const id of job.outputBlockIds)
|
|
1341
|
-
if (probeBlockIds.has(id))
|
|
1342
|
-
directBlockRoots.add(id);
|
|
1343
|
-
for (const id of job.sourceSegmentIds)
|
|
1344
|
-
if (probeSegmentIds.has(id))
|
|
1345
|
-
directSegmentRoots.add(id);
|
|
1346
|
-
if (job.outputSegmentId !== null && probeSegmentIds.has(job.outputSegmentId)) {
|
|
1347
|
-
directSegmentRoots.add(job.outputSegmentId);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
const rootedBlockIds = new Set();
|
|
1351
|
-
const rootedSegmentIds = new Set();
|
|
1352
|
-
for (const segment of relatedSegments.values()) {
|
|
1353
|
-
const ids = segmentBlockIds(segment);
|
|
1354
|
-
if (directSegmentRoots.has(segment.id) ||
|
|
1355
|
-
transactions.get(segment.transactionId)?.status === "active" ||
|
|
1356
|
-
(ids.length > 0 && ids.every((id) => directBlockRoots.has(id)))) {
|
|
1357
|
-
if (candidateSegmentIds.includes(segment.id))
|
|
1358
|
-
rootedSegmentIds.add(segment.id);
|
|
1359
|
-
for (const id of ids)
|
|
1360
|
-
if (candidateBlocks.has(id))
|
|
1361
|
-
rootedBlockIds.add(id);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
for (const id of candidateBlockIds)
|
|
1365
|
-
if (directBlockRoots.has(id))
|
|
1366
|
-
rootedBlockIds.add(id);
|
|
1367
|
-
for (const id of candidateSegmentIds)
|
|
1368
|
-
if (directSegmentRoots.has(id))
|
|
1369
|
-
rootedSegmentIds.add(id);
|
|
1370
|
-
return { blockIds: rootedBlockIds, segmentIds: rootedSegmentIds };
|
|
1371
|
-
}
|
|
1372
|
-
function segmentBlockIds(segment) {
|
|
1373
|
-
return [...new Set(Object.values(segment.columnBlockIds).flat())];
|
|
1374
|
-
}
|
|
1375
|
-
function validateGarbageCollectionStepInput(input) {
|
|
1376
|
-
if (input.jobId.length === 0)
|
|
1377
|
-
throw new TypeError("Garbage collection job ID cannot be empty");
|
|
1378
|
-
if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) {
|
|
1379
|
-
throw new RangeError("Garbage collection expected revision must be a non-negative whole number");
|
|
1380
|
-
}
|
|
1381
|
-
if (!Number.isSafeInteger(input.maxItems) || input.maxItems <= 0) {
|
|
1382
|
-
throw new RangeError("Garbage collection item limit must be a positive whole number");
|
|
1383
|
-
}
|
|
1384
|
-
if (input.updatedAt.length === 0 || !Number.isFinite(Date.parse(input.updatedAt))) {
|
|
1385
|
-
throw new TypeError("Garbage collection update timestamp must be valid");
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
function validatePageLimit(limit) {
|
|
1389
|
-
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
1390
|
-
throw new RangeError("Storage page limit must be a positive whole number");
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
function validateTempRunPage(page) {
|
|
1394
|
-
validateTempRunPageIdentity(page.ownerId, page.runId, page.pageIndex);
|
|
1395
|
-
if (!(page.bytes instanceof Uint8Array))
|
|
1396
|
-
throw new TypeError("Temp run page bytes are invalid");
|
|
1397
|
-
}
|
|
1398
|
-
function validateTempRunPageIdentity(ownerId, runId, pageIndex) {
|
|
1399
|
-
validateId(ownerId);
|
|
1400
|
-
validateId(runId);
|
|
1401
|
-
if (!Number.isSafeInteger(pageIndex) || pageIndex < 0) {
|
|
1402
|
-
throw new RangeError("Temp run page index must be a non-negative whole number");
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
function validateTableColumns(columns) {
|
|
1406
|
-
if (columns.length === 0)
|
|
1407
|
-
throw new TypeError("A table needs at least one column");
|
|
1408
|
-
const ids = new Set(columns.map(({ id }) => id));
|
|
1409
|
-
const names = new Set(columns.map(({ name }) => name));
|
|
1410
|
-
if (ids.size !== columns.length || names.size !== columns.length) {
|
|
1411
|
-
throw new TypeError("Table columns must have unique IDs and names");
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
function validateTempOwnerRecord(record) {
|
|
1415
|
-
validateId(record.ownerId);
|
|
1416
|
-
validateLeaseExpiration(record.expiresAt);
|
|
1417
|
-
if (record.revision !== 0) {
|
|
1418
|
-
throw new RangeError("Temp owner record must be created at revision zero");
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
374
|
function tempRunPageKey(ownerId, runId, pageIndex) {
|
|
1422
375
|
return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:${String(pageIndex)}`;
|
|
1423
376
|
}
|
|
1424
377
|
function tempRunPagePrefix(ownerId, runId) {
|
|
1425
378
|
return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
|
|
1426
379
|
}
|
|
1427
|
-
function validateLeaseExpiration(expiresAt) {
|
|
1428
|
-
if (expiresAt.length === 0 || !Number.isFinite(Date.parse(expiresAt))) {
|
|
1429
|
-
throw new TypeError("Lease expiration must be valid");
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
function emptyGarbageCollectionStep(job) {
|
|
1433
|
-
return {
|
|
1434
|
-
job: structuredClone(job),
|
|
1435
|
-
prunedManifestVersions: [],
|
|
1436
|
-
alreadyPrunedManifestVersions: [],
|
|
1437
|
-
retainedManifestVersions: [],
|
|
1438
|
-
missingManifestVersions: [],
|
|
1439
|
-
reclaimedSegmentIds: [],
|
|
1440
|
-
retainedSegmentIds: [],
|
|
1441
|
-
missingSegmentIds: [],
|
|
1442
|
-
reclaimedBlockIds: [],
|
|
1443
|
-
retainedBlockIds: [],
|
|
1444
|
-
missingBlockIds: [],
|
|
1445
|
-
reclaimedBlockBytes: 0,
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
function safeStorageSum(left, right) {
|
|
1449
|
-
const total = left + right;
|
|
1450
|
-
if (!Number.isSafeInteger(total)) {
|
|
1451
|
-
throw new RangeError("Garbage collection reclaimed block bytes exceed the safe range");
|
|
1452
|
-
}
|
|
1453
|
-
return total;
|
|
1454
|
-
}
|
|
1455
380
|
//# sourceMappingURL=memory.js.map
|