@minnowdb/core 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +27 -1
- package/dist/block-format/block.d.ts +46 -0
- package/dist/block-format/block.d.ts.map +1 -0
- package/dist/block-format/block.js +244 -0
- package/dist/block-format/block.js.map +1 -0
- package/dist/block-format/checksum.d.ts +7 -0
- package/dist/block-format/checksum.d.ts.map +1 -0
- package/dist/block-format/checksum.js +56 -0
- package/dist/block-format/checksum.js.map +1 -0
- package/dist/block-format/codecs.d.ts +24 -0
- package/dist/block-format/codecs.d.ts.map +1 -0
- package/dist/block-format/codecs.js +102 -0
- package/dist/block-format/codecs.js.map +1 -0
- package/dist/block-format/column.d.ts +8 -0
- package/dist/block-format/column.d.ts.map +1 -0
- package/dist/block-format/column.js +166 -0
- package/dist/block-format/column.js.map +1 -0
- package/dist/block-format/index.d.ts +7 -0
- package/dist/block-format/index.d.ts.map +1 -0
- package/dist/block-format/index.js +7 -0
- package/dist/block-format/index.js.map +1 -0
- package/dist/block-format/physical.d.ts +20 -0
- package/dist/block-format/physical.d.ts.map +1 -0
- package/dist/block-format/physical.js +439 -0
- package/dist/block-format/physical.js.map +1 -0
- package/dist/block-format/types.d.ts +93 -0
- package/dist/block-format/types.d.ts.map +1 -0
- package/dist/block-format/types.js +2 -0
- package/dist/block-format/types.js.map +1 -0
- package/dist/engine/artifact-cache.d.ts +22 -0
- package/dist/engine/artifact-cache.d.ts.map +1 -0
- package/dist/engine/artifact-cache.js +68 -0
- package/dist/engine/artifact-cache.js.map +1 -0
- package/dist/engine/batch.d.ts +28 -0
- package/dist/engine/batch.d.ts.map +1 -0
- package/dist/engine/batch.js +38 -0
- package/dist/engine/batch.js.map +1 -0
- package/dist/engine/buffered-writer.d.ts +43 -0
- package/dist/engine/buffered-writer.d.ts.map +1 -0
- package/dist/engine/buffered-writer.js +158 -0
- package/dist/engine/buffered-writer.js.map +1 -0
- package/dist/engine/catalog.d.ts +82 -0
- package/dist/engine/catalog.d.ts.map +1 -0
- package/dist/engine/catalog.js +47 -0
- package/dist/engine/catalog.js.map +1 -0
- package/dist/engine/client.d.ts +211 -0
- package/dist/engine/client.d.ts.map +1 -0
- package/dist/engine/client.js +538 -0
- package/dist/engine/client.js.map +1 -0
- package/dist/engine/coordinator.d.ts +17 -0
- package/dist/engine/coordinator.d.ts.map +1 -0
- package/dist/engine/coordinator.js +60 -0
- package/dist/engine/coordinator.js.map +1 -0
- package/dist/engine/database.d.ts +661 -0
- package/dist/engine/database.d.ts.map +1 -0
- package/dist/engine/database.js +10313 -0
- package/dist/engine/database.js.map +1 -0
- package/dist/engine/defaults.d.ts +33 -0
- package/dist/engine/defaults.d.ts.map +1 -0
- package/dist/engine/defaults.js +87 -0
- package/dist/engine/defaults.js.map +1 -0
- package/dist/engine/dsl/db.d.ts +137 -0
- package/dist/engine/dsl/db.d.ts.map +1 -0
- package/dist/engine/dsl/db.js +161 -0
- package/dist/engine/dsl/db.js.map +1 -0
- package/dist/engine/dsl/expression.d.ts +172 -0
- package/dist/engine/dsl/expression.d.ts.map +1 -0
- package/dist/engine/dsl/expression.js +397 -0
- package/dist/engine/dsl/expression.js.map +1 -0
- package/dist/engine/dsl/index.d.ts +8 -0
- package/dist/engine/dsl/index.d.ts.map +1 -0
- package/dist/engine/dsl/index.js +7 -0
- package/dist/engine/dsl/index.js.map +1 -0
- package/dist/engine/dsl/live-query.d.ts +45 -0
- package/dist/engine/dsl/live-query.d.ts.map +1 -0
- package/dist/engine/dsl/live-query.js +116 -0
- package/dist/engine/dsl/live-query.js.map +1 -0
- package/dist/engine/dsl/mutations.d.ts +123 -0
- package/dist/engine/dsl/mutations.d.ts.map +1 -0
- package/dist/engine/dsl/mutations.js +241 -0
- package/dist/engine/dsl/mutations.js.map +1 -0
- package/dist/engine/dsl/select-query-builder.d.ts +148 -0
- package/dist/engine/dsl/select-query-builder.d.ts.map +1 -0
- package/dist/engine/dsl/select-query-builder.js +377 -0
- package/dist/engine/dsl/select-query-builder.js.map +1 -0
- package/dist/engine/dsl/sql-tag.d.ts +46 -0
- package/dist/engine/dsl/sql-tag.d.ts.map +1 -0
- package/dist/engine/dsl/sql-tag.js +85 -0
- package/dist/engine/dsl/sql-tag.js.map +1 -0
- package/dist/engine/dsl/types.d.ts +133 -0
- package/dist/engine/dsl/types.d.ts.map +1 -0
- package/dist/engine/dsl/types.js +9 -0
- package/dist/engine/dsl/types.js.map +1 -0
- package/dist/engine/errors.d.ts +55 -0
- package/dist/engine/errors.d.ts.map +1 -0
- package/dist/engine/errors.js +81 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/fts.d.ts +93 -0
- package/dist/engine/fts.d.ts.map +1 -0
- package/dist/engine/fts.js +273 -0
- package/dist/engine/fts.js.map +1 -0
- package/dist/engine/group-index.d.ts +32 -0
- package/dist/engine/group-index.d.ts.map +1 -0
- package/dist/engine/group-index.js +366 -0
- package/dist/engine/group-index.js.map +1 -0
- package/dist/engine/index.d.ts +13 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +12 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/join-index.d.ts +12 -0
- package/dist/engine/join-index.d.ts.map +1 -0
- package/dist/engine/join-index.js +217 -0
- package/dist/engine/join-index.js.map +1 -0
- package/dist/engine/live.d.ts +86 -0
- package/dist/engine/live.d.ts.map +1 -0
- package/dist/engine/live.js +289 -0
- package/dist/engine/live.js.map +1 -0
- package/dist/engine/memory.d.ts +37 -0
- package/dist/engine/memory.d.ts.map +1 -0
- package/dist/engine/memory.js +119 -0
- package/dist/engine/memory.js.map +1 -0
- package/dist/engine/optimizer.d.ts +20 -0
- package/dist/engine/optimizer.d.ts.map +1 -0
- package/dist/engine/optimizer.js +1170 -0
- package/dist/engine/optimizer.js.map +1 -0
- package/dist/engine/query-cache.d.ts +10 -0
- package/dist/engine/query-cache.d.ts.map +1 -0
- package/dist/engine/query-cache.js +58 -0
- package/dist/engine/query-cache.js.map +1 -0
- package/dist/engine/query.d.ts +844 -0
- package/dist/engine/query.d.ts.map +1 -0
- package/dist/engine/query.js +7450 -0
- package/dist/engine/query.js.map +1 -0
- package/dist/engine/schema-wire.d.ts +92 -0
- package/dist/engine/schema-wire.d.ts.map +1 -0
- package/dist/engine/schema-wire.js +115 -0
- package/dist/engine/schema-wire.js.map +1 -0
- package/dist/engine/schema.d.ts +394 -0
- package/dist/engine/schema.d.ts.map +1 -0
- package/dist/engine/schema.js +709 -0
- package/dist/engine/schema.js.map +1 -0
- package/dist/engine/sort-keys.d.ts +36 -0
- package/dist/engine/sort-keys.d.ts.map +1 -0
- package/dist/engine/sort-keys.js +60 -0
- package/dist/engine/sort-keys.js.map +1 -0
- package/dist/engine/sql-json.d.ts +30 -0
- package/dist/engine/sql-json.d.ts.map +1 -0
- package/dist/engine/sql-json.js +120 -0
- package/dist/engine/sql-json.js.map +1 -0
- package/dist/engine/sql-semantics.d.ts +31 -0
- package/dist/engine/sql-semantics.d.ts.map +1 -0
- package/dist/engine/sql-semantics.js +121 -0
- package/dist/engine/sql-semantics.js.map +1 -0
- package/dist/engine/vector.d.ts +87 -0
- package/dist/engine/vector.d.ts.map +1 -0
- package/dist/engine/vector.js +4227 -0
- package/dist/engine/vector.js.map +1 -0
- package/dist/engine/worker-host.d.ts +48 -0
- package/dist/engine/worker-host.d.ts.map +1 -0
- package/dist/engine/worker-host.js +501 -0
- package/dist/engine/worker-host.js.map +1 -0
- package/dist/engine/worker.d.ts +2 -0
- package/dist/engine/worker.d.ts.map +1 -0
- package/dist/engine/worker.js +14 -0
- package/dist/engine/worker.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/plan/index.d.ts +17 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +17 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/storage/fixture-shape.d.ts +42 -0
- package/dist/storage/fixture-shape.d.ts.map +1 -0
- package/dist/storage/fixture-shape.js +146 -0
- package/dist/storage/fixture-shape.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +139 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +3161 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/memory.d.ts +121 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +1455 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/snapshot.d.ts +108 -0
- package/dist/storage/snapshot.d.ts.map +1 -0
- package/dist/storage/snapshot.js +276 -0
- package/dist/storage/snapshot.js.map +1 -0
- package/dist/storage/types.d.ts +895 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +1549 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/testing/index.d.ts +76 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +233 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/seeds.d.ts +11 -0
- package/dist/testing/seeds.d.ts.map +1 -0
- package/dist/testing/seeds.js +50 -0
- package/dist/testing/seeds.js.map +1 -0
- package/dist/transactions/index.d.ts +148 -0
- package/dist/transactions/index.d.ts.map +1 -0
- package/dist/transactions/index.js +662 -0
- package/dist/transactions/index.js.map +1 -0
- package/dist/worker-protocol/index.d.ts +84 -0
- package/dist/worker-protocol/index.d.ts.map +1 -0
- package/dist/worker-protocol/index.js +117 -0
- package/dist/worker-protocol/index.js.map +1 -0
- package/package.json +66 -16
- package/sql-feature-matrix.json +1275 -0
- package/index.d.ts +0 -1
- package/index.js +0 -1
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
import { CompactionJobConflictError, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, invalidateUncoveredFtsColumns, GarbageCollectionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, normalizeCompactionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, WriteConflictError, } from "./types.js";
|
|
2
|
+
import { selectLiveRecords, } from "./snapshot.js";
|
|
3
|
+
export class MemoryBlockStore {
|
|
4
|
+
#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
|
+
#tempRunPages = new Map();
|
|
19
|
+
#tempOwners = new Map();
|
|
20
|
+
#currentVersion = null;
|
|
21
|
+
/** Advances on every catalog mutation; see `CatalogProbe` for the freshness contract. */
|
|
22
|
+
#catalogEpoch = 0;
|
|
23
|
+
#commitQueue = Promise.resolve();
|
|
24
|
+
async addBlock(id, bytes) {
|
|
25
|
+
return this.addBlocks([{ id, bytes }]);
|
|
26
|
+
}
|
|
27
|
+
async addBlocks(blocks) {
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
for (const block of blocks) {
|
|
30
|
+
validateId(block.id);
|
|
31
|
+
if (ids.has(block.id) || this.#blocks.has(block.id)) {
|
|
32
|
+
throw new Error(`Block already exists: ${block.id}`);
|
|
33
|
+
}
|
|
34
|
+
ids.add(block.id);
|
|
35
|
+
}
|
|
36
|
+
for (const block of blocks)
|
|
37
|
+
this.#blocks.set(block.id, new Uint8Array(block.bytes));
|
|
38
|
+
}
|
|
39
|
+
async getBlock(id) {
|
|
40
|
+
const bytes = this.#blocks.get(id);
|
|
41
|
+
return bytes === undefined ? undefined : new Uint8Array(bytes);
|
|
42
|
+
}
|
|
43
|
+
async getBlocks(ids) {
|
|
44
|
+
return ids.map((id) => {
|
|
45
|
+
const bytes = this.#blocks.get(id);
|
|
46
|
+
return bytes === undefined ? undefined : new Uint8Array(bytes);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async removeBlock(id) {
|
|
50
|
+
this.#blocks.delete(id);
|
|
51
|
+
}
|
|
52
|
+
async listBlockIds() {
|
|
53
|
+
return [...this.#blocks.keys()].sort();
|
|
54
|
+
}
|
|
55
|
+
async putTempRunPage(page) {
|
|
56
|
+
validateTempRunPage(page);
|
|
57
|
+
this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
|
|
58
|
+
}
|
|
59
|
+
async getTempRunPage(ownerId, runId, pageIndex) {
|
|
60
|
+
validateTempRunPageIdentity(ownerId, runId, pageIndex);
|
|
61
|
+
return this.#tempRunPages.get(tempRunPageKey(ownerId, runId, pageIndex))?.slice();
|
|
62
|
+
}
|
|
63
|
+
async removeTempRun(ownerId, runId) {
|
|
64
|
+
validateTempRunPageIdentity(ownerId, runId, 0);
|
|
65
|
+
const prefix = tempRunPagePrefix(ownerId, runId);
|
|
66
|
+
for (const key of this.#tempRunPages.keys())
|
|
67
|
+
if (key.startsWith(prefix))
|
|
68
|
+
this.#tempRunPages.delete(key);
|
|
69
|
+
}
|
|
70
|
+
async removeTempOwner(ownerId) {
|
|
71
|
+
validateId(ownerId);
|
|
72
|
+
const prefix = `${String(ownerId.length)}:${ownerId}:`;
|
|
73
|
+
for (const key of this.#tempRunPages.keys())
|
|
74
|
+
if (key.startsWith(prefix))
|
|
75
|
+
this.#tempRunPages.delete(key);
|
|
76
|
+
this.#tempOwners.delete(ownerId);
|
|
77
|
+
}
|
|
78
|
+
async createTempOwner(record) {
|
|
79
|
+
validateTempOwnerRecord(record);
|
|
80
|
+
return this.#runAtomic(() => {
|
|
81
|
+
if (this.#tempOwners.has(record.ownerId)) {
|
|
82
|
+
throw new Error(`Temp owner already exists: ${record.ownerId}`);
|
|
83
|
+
}
|
|
84
|
+
this.#tempOwners.set(record.ownerId, structuredClone(record));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async getTempOwner(ownerId) {
|
|
88
|
+
validateId(ownerId);
|
|
89
|
+
const record = this.#tempOwners.get(ownerId);
|
|
90
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
91
|
+
}
|
|
92
|
+
async renewTempOwner(ownerId, expectedRevision, expiresAt) {
|
|
93
|
+
validateLeaseExpiration(expiresAt);
|
|
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
|
+
});
|
|
103
|
+
}
|
|
104
|
+
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
|
+
return this.#runAtomic(() => {
|
|
110
|
+
const record = this.#tempOwners.get(ownerId);
|
|
111
|
+
if (record !== undefined) {
|
|
112
|
+
const expiresAt = Date.parse(record.expiresAt);
|
|
113
|
+
if (Number.isFinite(expiresAt) && expiresAt > cutoff)
|
|
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;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async listTempOwnerIdsPage(afterOwnerId, limit) {
|
|
125
|
+
validatePageLimit(limit);
|
|
126
|
+
const ownerIds = new Set(this.#tempOwners.keys());
|
|
127
|
+
for (const key of this.#tempRunPages.keys()) {
|
|
128
|
+
const separator = key.indexOf(":");
|
|
129
|
+
const length = Number(key.slice(0, separator));
|
|
130
|
+
ownerIds.add(key.slice(separator + 1, separator + 1 + length));
|
|
131
|
+
}
|
|
132
|
+
const sorted = [...ownerIds]
|
|
133
|
+
.filter((ownerId) => afterOwnerId === null || ownerId > afterOwnerId)
|
|
134
|
+
.sort();
|
|
135
|
+
const records = sorted.slice(0, limit);
|
|
136
|
+
return {
|
|
137
|
+
records,
|
|
138
|
+
nextCursor: sorted.length > limit ? (records[records.length - 1] ?? null) : null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async addTable(record) {
|
|
142
|
+
if (this.#tables.has(record.id))
|
|
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;
|
|
149
|
+
}
|
|
150
|
+
async getTable(id) {
|
|
151
|
+
const record = this.#tables.get(id);
|
|
152
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
153
|
+
}
|
|
154
|
+
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
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async removeTable(id, expectedRevision) {
|
|
183
|
+
return this.#runAtomic(() => {
|
|
184
|
+
const record = this.#tables.get(id);
|
|
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;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async writeFtsBase(tableId, columnId, input) {
|
|
214
|
+
return this.#runAtomic(() => {
|
|
215
|
+
const key = `${tableId}/${columnId}`;
|
|
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
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
227
|
+
const key = `${tableId}/${columnId}`;
|
|
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
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async getTableByName(name) {
|
|
281
|
+
const id = this.#tableIdsByName.get(name);
|
|
282
|
+
return id === undefined ? undefined : this.getTable(id);
|
|
283
|
+
}
|
|
284
|
+
async listTables() {
|
|
285
|
+
return [...this.#tables.values()]
|
|
286
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
287
|
+
.map((record) => structuredClone(record));
|
|
288
|
+
}
|
|
289
|
+
async addSegment(record) {
|
|
290
|
+
const normalized = normalizeSegmentRecord(record);
|
|
291
|
+
if (this.#segments.has(normalized.id)) {
|
|
292
|
+
throw new Error(`Segment already exists: ${normalized.id}`);
|
|
293
|
+
}
|
|
294
|
+
this.#segments.set(normalized.id, normalized);
|
|
295
|
+
}
|
|
296
|
+
async getSegment(id) {
|
|
297
|
+
const record = this.#segments.get(id);
|
|
298
|
+
return record === undefined ? undefined : normalizeSegmentRecord(record);
|
|
299
|
+
}
|
|
300
|
+
async listSegments(tableId) {
|
|
301
|
+
return [...this.#segments.values()]
|
|
302
|
+
.filter((record) => tableId === undefined || record.tableId === tableId)
|
|
303
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
304
|
+
.map((record) => normalizeSegmentRecord(record));
|
|
305
|
+
}
|
|
306
|
+
async removeSegment(id) {
|
|
307
|
+
this.#segments.delete(id);
|
|
308
|
+
}
|
|
309
|
+
async reserveRowIds(tableId, count) {
|
|
310
|
+
validateCount(count);
|
|
311
|
+
const start = this.#nextRowIds.get(tableId) ?? 1n;
|
|
312
|
+
const endExclusive = start + BigInt(count);
|
|
313
|
+
this.#nextRowIds.set(tableId, endExclusive);
|
|
314
|
+
return { start, endExclusive };
|
|
315
|
+
}
|
|
316
|
+
async reserveAutoIncrement(tableId, columnId, count, atLeast) {
|
|
317
|
+
validateAutoIncrementReservation(count, atLeast);
|
|
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 };
|
|
328
|
+
}
|
|
329
|
+
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
330
|
+
const existing = this.#uniqueKeys.get(tableId);
|
|
331
|
+
if (existing === undefined)
|
|
332
|
+
return [];
|
|
333
|
+
return [...new Set(keyTokens)].filter((token) => existing.has(token)).sort();
|
|
334
|
+
}
|
|
335
|
+
async getCurrentManifestVersion() {
|
|
336
|
+
return this.#currentVersion;
|
|
337
|
+
}
|
|
338
|
+
async getCatalogProbe() {
|
|
339
|
+
return { manifestVersion: this.#currentVersion, catalogEpoch: this.#catalogEpoch };
|
|
340
|
+
}
|
|
341
|
+
async getQueryCatalogState(tableNames) {
|
|
342
|
+
const tables = await Promise.all(tableNames.map((name) => this.getTableByName(name)));
|
|
343
|
+
const foundTableIds = new Set(tables.filter((table) => table !== undefined).map((table) => table.id));
|
|
344
|
+
const segments = (await this.listSegments()).filter((record) => foundTableIds.has(record.tableId));
|
|
345
|
+
const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
|
|
346
|
+
const transactions = (await this.getTransactions(transactionIds)).filter((record) => record !== undefined);
|
|
347
|
+
return {
|
|
348
|
+
manifestVersion: this.#currentVersion,
|
|
349
|
+
tables,
|
|
350
|
+
segments,
|
|
351
|
+
transactions,
|
|
352
|
+
catalogEpoch: this.#catalogEpoch,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async getCurrentManifest() {
|
|
356
|
+
const manifest = this.#currentVersion === null ? undefined : this.#manifests.get(this.#currentVersion);
|
|
357
|
+
return manifest === undefined ? undefined : structuredClone(manifest);
|
|
358
|
+
}
|
|
359
|
+
async getManifest(version) {
|
|
360
|
+
const manifest = this.#manifests.get(version);
|
|
361
|
+
return manifest === undefined ? undefined : structuredClone(manifest);
|
|
362
|
+
}
|
|
363
|
+
async listManifests() {
|
|
364
|
+
return [...this.#manifests.values()]
|
|
365
|
+
.sort((left, right) => left.version - right.version)
|
|
366
|
+
.map((manifest) => structuredClone(manifest));
|
|
367
|
+
}
|
|
368
|
+
async listManifestPage(afterVersion, limit) {
|
|
369
|
+
validatePageLimit(limit);
|
|
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
|
+
};
|
|
379
|
+
}
|
|
380
|
+
async publishManifest(input) {
|
|
381
|
+
let resolveResult;
|
|
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;
|
|
407
|
+
}
|
|
408
|
+
async beginTransaction(input) {
|
|
409
|
+
if (input.record.pendingBlockIds.length > 0 || input.record.pendingSegmentIds.length > 0) {
|
|
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
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async createTransaction(record) {
|
|
444
|
+
return this.#runAtomic(() => {
|
|
445
|
+
if (this.#transactions.has(record.id)) {
|
|
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));
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async getTransaction(id) {
|
|
454
|
+
const record = this.#transactions.get(id);
|
|
455
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
456
|
+
}
|
|
457
|
+
async getTransactions(ids) {
|
|
458
|
+
return ids.map((id) => {
|
|
459
|
+
const record = this.#transactions.get(id);
|
|
460
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
async listTransactions() {
|
|
464
|
+
return [...this.#transactions.values()]
|
|
465
|
+
.sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
|
|
466
|
+
.map((record) => structuredClone(record));
|
|
467
|
+
}
|
|
468
|
+
async listTransactionPage(afterId, limit) {
|
|
469
|
+
validatePageLimit(limit);
|
|
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 };
|
|
476
|
+
}
|
|
477
|
+
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
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async stageTransactionArtifacts(input) {
|
|
494
|
+
return this.#runAtomic(() => {
|
|
495
|
+
const ids = new Set();
|
|
496
|
+
for (const block of input.blocks) {
|
|
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);
|
|
525
|
+
for (const block of input.blocks)
|
|
526
|
+
this.#blocks.set(block.id, new Uint8Array(block.bytes));
|
|
527
|
+
for (const segment of input.segments) {
|
|
528
|
+
this.#segments.set(segment.id, normalizeSegmentRecord(segment));
|
|
529
|
+
}
|
|
530
|
+
this.#transactions.set(input.transactionId, updated);
|
|
531
|
+
return structuredClone(updated);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
async commitTransaction(input) {
|
|
535
|
+
let resolveResult;
|
|
536
|
+
let rejectResult;
|
|
537
|
+
const result = new Promise((resolve, reject) => {
|
|
538
|
+
resolveResult = resolve;
|
|
539
|
+
rejectResult = reject;
|
|
540
|
+
});
|
|
541
|
+
this.#commitQueue = this.#commitQueue.then(() => {
|
|
542
|
+
try {
|
|
543
|
+
const transaction = this.#transactions.get(input.transactionId);
|
|
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
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
async createLease(record) {
|
|
661
|
+
validateLeaseExpiration(record.expiresAt);
|
|
662
|
+
return this.#runAtomic(() => {
|
|
663
|
+
if (this.#leases.has(record.id))
|
|
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));
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
async getLease(id) {
|
|
670
|
+
const record = this.#leases.get(id);
|
|
671
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
672
|
+
}
|
|
673
|
+
async listLeases() {
|
|
674
|
+
return [...this.#leases.values()]
|
|
675
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
676
|
+
.map((record) => structuredClone(record));
|
|
677
|
+
}
|
|
678
|
+
async renewLease(id, expectedRevision, expiresAt) {
|
|
679
|
+
validateLeaseExpiration(expiresAt);
|
|
680
|
+
return this.#runAtomic(() => {
|
|
681
|
+
const record = this.#leases.get(id);
|
|
682
|
+
if (record?.revision !== expectedRevision) {
|
|
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
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
|
|
692
|
+
const cutoff = Date.parse(expiresAtCutoff);
|
|
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
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async removeLease(id) {
|
|
708
|
+
return this.#runAtomic(() => {
|
|
709
|
+
this.#leases.delete(id);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async createCompactionJob(record) {
|
|
713
|
+
const normalized = normalizeCompactionJobRecord(record);
|
|
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);
|
|
718
|
+
}
|
|
719
|
+
async getCompactionJob(id) {
|
|
720
|
+
const record = this.#compactionJobs.get(id);
|
|
721
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
722
|
+
}
|
|
723
|
+
async listCompactionJobs(tableId) {
|
|
724
|
+
return [...this.#compactionJobs.values()]
|
|
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));
|
|
728
|
+
}
|
|
729
|
+
async listCompactionJobPage(afterId, limit) {
|
|
730
|
+
validatePageLimit(limit);
|
|
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 };
|
|
737
|
+
}
|
|
738
|
+
async updateCompactionJob(id, expectedRevision, update) {
|
|
739
|
+
if (update.state === "cancelled") {
|
|
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);
|
|
749
|
+
}
|
|
750
|
+
async cancelCompactionJob(id, expectedRevision, cancelledAt) {
|
|
751
|
+
let resolveResult;
|
|
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;
|
|
807
|
+
}
|
|
808
|
+
async removeCompactionJob(id) {
|
|
809
|
+
this.#compactionJobs.delete(id);
|
|
810
|
+
}
|
|
811
|
+
async createGarbageCollectionJob(input) {
|
|
812
|
+
const record = createGarbageCollectionJobRecord(input);
|
|
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
|
+
});
|
|
821
|
+
}
|
|
822
|
+
async getGarbageCollectionJob(id) {
|
|
823
|
+
const record = this.#garbageCollectionJobs.get(id);
|
|
824
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
825
|
+
}
|
|
826
|
+
async listGarbageCollectionJobs() {
|
|
827
|
+
return [...this.#garbageCollectionJobs.values()]
|
|
828
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
|
829
|
+
.map((record) => structuredClone(record));
|
|
830
|
+
}
|
|
831
|
+
async runGarbageCollectionStep(input) {
|
|
832
|
+
validateGarbageCollectionStepInput(input);
|
|
833
|
+
return this.#runAtomic(() => {
|
|
834
|
+
const current = this.#garbageCollectionJobs.get(input.jobId);
|
|
835
|
+
if (current?.revision !== input.expectedRevision) {
|
|
836
|
+
throw new GarbageCollectionJobConflictError(input.jobId, input.expectedRevision, current?.revision ?? null);
|
|
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
|
+
};
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
async removeGarbageCollectionJob(id) {
|
|
963
|
+
return this.#runAtomic(() => {
|
|
964
|
+
this.#garbageCollectionJobs.delete(id);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* 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.
|
|
976
|
+
*/
|
|
977
|
+
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
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Loads a snapshot into this store, which must be empty — the same contract as the IndexedDB
|
|
1036
|
+
* store, so a caller holding a `BlockStore` can restore into either without asking which it
|
|
1037
|
+
* has. The load runs on the commit queue, so it cannot interleave with a commit.
|
|
1038
|
+
*
|
|
1039
|
+
* Progress is reported for parity with the IndexedDB store rather than because it is needed:
|
|
1040
|
+
* an in-memory load is a handful of map writes with no transaction to break it into, so it
|
|
1041
|
+
* reports the start and then the end.
|
|
1042
|
+
*/
|
|
1043
|
+
async importSnapshot(snapshot, options = {}) {
|
|
1044
|
+
const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
|
|
1045
|
+
options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
|
|
1046
|
+
await this.#runAtomic(() => {
|
|
1047
|
+
if (this.#currentVersion !== null)
|
|
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);
|
|
1052
|
+
});
|
|
1053
|
+
options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
|
|
1054
|
+
}
|
|
1055
|
+
/** Builds a store holding exactly what the snapshot captured. */
|
|
1056
|
+
static fromSnapshot(snapshot) {
|
|
1057
|
+
const store = new MemoryBlockStore();
|
|
1058
|
+
store.#loadSnapshot(snapshot);
|
|
1059
|
+
return store;
|
|
1060
|
+
}
|
|
1061
|
+
#loadSnapshot(snapshot) {
|
|
1062
|
+
for (const block of snapshot.blocks)
|
|
1063
|
+
this.#blocks.set(block.id, block.bytes.slice());
|
|
1064
|
+
for (const segment of snapshot.segments) {
|
|
1065
|
+
this.#segments.set(segment.id, normalizeSegmentRecord(segment));
|
|
1066
|
+
}
|
|
1067
|
+
for (const record of snapshot.transactions) {
|
|
1068
|
+
this.#transactions.set(record.id, structuredClone(record));
|
|
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;
|
|
1100
|
+
}
|
|
1101
|
+
close() {
|
|
1102
|
+
// The in-memory implementation owns no external resources.
|
|
1103
|
+
}
|
|
1104
|
+
#runAtomic(operation) {
|
|
1105
|
+
let resolveResult;
|
|
1106
|
+
let rejectResult;
|
|
1107
|
+
const result = new Promise((resolve, reject) => {
|
|
1108
|
+
resolveResult = resolve;
|
|
1109
|
+
rejectResult = reject;
|
|
1110
|
+
});
|
|
1111
|
+
this.#commitQueue = this.#commitQueue.then(() => {
|
|
1112
|
+
try {
|
|
1113
|
+
resolveResult(operation());
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
rejectResult(error);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
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
|
+
function tempRunPageKey(ownerId, runId, pageIndex) {
|
|
1422
|
+
return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:${String(pageIndex)}`;
|
|
1423
|
+
}
|
|
1424
|
+
function tempRunPagePrefix(ownerId, runId) {
|
|
1425
|
+
return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
|
|
1426
|
+
}
|
|
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
|
+
//# sourceMappingURL=memory.js.map
|