@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,662 @@
|
|
|
1
|
+
import { GarbageCollectionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TransactionRecordConflictError, } from "../storage/index.js";
|
|
2
|
+
export class TransactionClosedError extends Error {
|
|
3
|
+
transactionId;
|
|
4
|
+
name = "TransactionClosedError";
|
|
5
|
+
constructor(transactionId) {
|
|
6
|
+
super(`Transaction is no longer active: ${transactionId}`);
|
|
7
|
+
this.transactionId = transactionId;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class Snapshot {
|
|
11
|
+
store;
|
|
12
|
+
version;
|
|
13
|
+
#blockIds;
|
|
14
|
+
constructor(store, version, blockIds) {
|
|
15
|
+
this.store = store;
|
|
16
|
+
this.version = version;
|
|
17
|
+
this.#blockIds = new Set(blockIds);
|
|
18
|
+
}
|
|
19
|
+
listBlockIds() {
|
|
20
|
+
return [...this.#blockIds].sort();
|
|
21
|
+
}
|
|
22
|
+
hasBlock(id) {
|
|
23
|
+
return this.#blockIds.has(id);
|
|
24
|
+
}
|
|
25
|
+
async getBlock(id) {
|
|
26
|
+
if (!this.#blockIds.has(id))
|
|
27
|
+
return undefined;
|
|
28
|
+
return this.store.getBlock(id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class LeasedSnapshot extends Snapshot {
|
|
32
|
+
now;
|
|
33
|
+
#record;
|
|
34
|
+
#released = false;
|
|
35
|
+
constructor(store, version, blockIds, record, now) {
|
|
36
|
+
super(store, version, blockIds);
|
|
37
|
+
this.now = now;
|
|
38
|
+
this.#record = structuredClone(record);
|
|
39
|
+
this.#store = store;
|
|
40
|
+
}
|
|
41
|
+
#store;
|
|
42
|
+
get leaseId() {
|
|
43
|
+
return this.#record.id;
|
|
44
|
+
}
|
|
45
|
+
get expiresAt() {
|
|
46
|
+
return new Date(this.#record.expiresAt);
|
|
47
|
+
}
|
|
48
|
+
async getBlock(id) {
|
|
49
|
+
this.#assertOpen();
|
|
50
|
+
return super.getBlock(id);
|
|
51
|
+
}
|
|
52
|
+
async renew(ttlMs) {
|
|
53
|
+
this.#assertOpen();
|
|
54
|
+
validateTtl(ttlMs);
|
|
55
|
+
const expiresAt = new Date(this.now().getTime() + ttlMs).toISOString();
|
|
56
|
+
this.#record = await this.#store.renewLease(this.#record.id, this.#record.revision, expiresAt);
|
|
57
|
+
return new Date(this.#record.expiresAt);
|
|
58
|
+
}
|
|
59
|
+
async release() {
|
|
60
|
+
if (this.#released)
|
|
61
|
+
return;
|
|
62
|
+
await this.#store.removeLease(this.#record.id);
|
|
63
|
+
this.#released = true;
|
|
64
|
+
}
|
|
65
|
+
#assertOpen() {
|
|
66
|
+
if (this.#released)
|
|
67
|
+
throw new Error(`Snapshot lease has been released: ${this.#record.id}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class DatabaseTransaction {
|
|
71
|
+
store;
|
|
72
|
+
now;
|
|
73
|
+
#record;
|
|
74
|
+
#uniqueKeyChanges = [];
|
|
75
|
+
#ftsChanges = [];
|
|
76
|
+
#supersededBlockIds = new Set();
|
|
77
|
+
#changedTableIds = new Set();
|
|
78
|
+
#logicallyUnchanged = false;
|
|
79
|
+
constructor(store, record, now) {
|
|
80
|
+
this.store = store;
|
|
81
|
+
this.now = now;
|
|
82
|
+
this.#record = structuredClone(record);
|
|
83
|
+
}
|
|
84
|
+
get id() {
|
|
85
|
+
return this.#record.id;
|
|
86
|
+
}
|
|
87
|
+
get status() {
|
|
88
|
+
return this.#record.status;
|
|
89
|
+
}
|
|
90
|
+
get snapshotVersion() {
|
|
91
|
+
return this.#record.snapshotVersion;
|
|
92
|
+
}
|
|
93
|
+
get pendingBlockIds() {
|
|
94
|
+
return [...this.#record.pendingBlockIds];
|
|
95
|
+
}
|
|
96
|
+
get pendingSegmentIds() {
|
|
97
|
+
return [...this.#record.pendingSegmentIds];
|
|
98
|
+
}
|
|
99
|
+
get supersededBlockIds() {
|
|
100
|
+
return [...this.#supersededBlockIds].sort();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Counts every registration this transaction carries: staged blocks and segments, unique-key
|
|
104
|
+
* entries, full-text entries, and supersessions. A caller that fails part-way through a
|
|
105
|
+
* multi-step stage compares this before and after to tell "nothing was registered" (the
|
|
106
|
+
* statement is cleanly undone) from "partial work landed" (the transaction can only abort),
|
|
107
|
+
* since there is no statement-level rollback inside a transaction.
|
|
108
|
+
*/
|
|
109
|
+
get stagedWorkCount() {
|
|
110
|
+
return (this.#record.pendingBlockIds.length +
|
|
111
|
+
this.#record.pendingSegmentIds.length +
|
|
112
|
+
this.#uniqueKeyChanges.length +
|
|
113
|
+
this.#ftsChanges.length +
|
|
114
|
+
this.#supersededBlockIds.size);
|
|
115
|
+
}
|
|
116
|
+
async snapshot() {
|
|
117
|
+
return loadSnapshot(this.store, this.#record.snapshotVersion);
|
|
118
|
+
}
|
|
119
|
+
async getBlock(id) {
|
|
120
|
+
this.#assertActive();
|
|
121
|
+
if (this.#record.pendingBlockIds.includes(id))
|
|
122
|
+
return this.store.getBlock(id);
|
|
123
|
+
return (await this.snapshot()).getBlock(id);
|
|
124
|
+
}
|
|
125
|
+
async stageBlock(id, bytes) {
|
|
126
|
+
return this.stageBlocks([{ id, bytes }]);
|
|
127
|
+
}
|
|
128
|
+
async stageBlocks(blocks) {
|
|
129
|
+
this.#assertActive();
|
|
130
|
+
if (blocks.length === 0)
|
|
131
|
+
return;
|
|
132
|
+
await this.store.addBlocks(blocks);
|
|
133
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
134
|
+
pendingBlockIds: [...this.#record.pendingBlockIds, ...blocks.map((block) => block.id)],
|
|
135
|
+
updatedAt: this.now().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Stages blocks and segments in one journal step. When the store implements the atomic
|
|
140
|
+
* combined operation this is a single storage transaction instead of the four the sequential
|
|
141
|
+
* shape costs (block writes, journal update, segment writes, journal update); either way the
|
|
142
|
+
* observable journal invariant is identical — a journaled artifact always exists.
|
|
143
|
+
*/
|
|
144
|
+
async stageArtifacts(blocks, segments) {
|
|
145
|
+
this.#assertActive();
|
|
146
|
+
for (const segment of segments) {
|
|
147
|
+
if (segment.transactionId !== this.id) {
|
|
148
|
+
throw new Error(`Segment ${segment.id} belongs to another transaction`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// The journal stores pendingSegmentIds deduped and sorted, so staging position is the
|
|
152
|
+
// only record of operation order within this transaction. Stamp it on the segments:
|
|
153
|
+
// reads use it to fold same-commit segments in the order they were staged.
|
|
154
|
+
const ordinalBase = this.#record.pendingSegmentIds.length;
|
|
155
|
+
const ordered = segments.map((segment, index) => ({
|
|
156
|
+
...segment,
|
|
157
|
+
commitOrdinal: segment.commitOrdinal ?? ordinalBase + index,
|
|
158
|
+
}));
|
|
159
|
+
const batched = this.store.stageTransactionArtifacts?.bind(this.store);
|
|
160
|
+
if (batched === undefined) {
|
|
161
|
+
await this.stageBlocks(blocks);
|
|
162
|
+
for (const segment of ordered)
|
|
163
|
+
await this.stageSegment(segment);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (blocks.length === 0 && ordered.length === 0)
|
|
167
|
+
return;
|
|
168
|
+
this.#record = await batched({
|
|
169
|
+
transactionId: this.id,
|
|
170
|
+
expectedRevision: this.#record.revision,
|
|
171
|
+
blocks,
|
|
172
|
+
segments: ordered,
|
|
173
|
+
updatedAt: this.now().toISOString(),
|
|
174
|
+
});
|
|
175
|
+
for (const segment of ordered)
|
|
176
|
+
this.#changedTableIds.add(segment.tableId);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Adds already-persisted immutable blocks to this transaction's journal.
|
|
180
|
+
*
|
|
181
|
+
* Resumable background jobs use this to reconcile the narrow crash window
|
|
182
|
+
* between an immutable block write and the transaction-record update. The
|
|
183
|
+
* bytes must already exist, and already-journaled IDs are ignored.
|
|
184
|
+
*/
|
|
185
|
+
async stageExistingBlocks(blockIds) {
|
|
186
|
+
this.#assertActive();
|
|
187
|
+
const pending = new Set(this.#record.pendingBlockIds);
|
|
188
|
+
const additions = [...new Set(blockIds)].filter((id) => !pending.has(id));
|
|
189
|
+
if (additions.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
if (additions.some((id) => id.length === 0)) {
|
|
192
|
+
throw new TypeError("Block ID cannot be empty");
|
|
193
|
+
}
|
|
194
|
+
for (const id of additions) {
|
|
195
|
+
if ((await this.store.getBlock(id)) === undefined) {
|
|
196
|
+
throw new Error(`Cannot stage a missing existing block: ${id}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
200
|
+
pendingBlockIds: [...this.#record.pendingBlockIds, ...additions],
|
|
201
|
+
updatedAt: this.now().toISOString(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async stageSegment(record) {
|
|
205
|
+
this.#assertActive();
|
|
206
|
+
if (record.transactionId !== this.id) {
|
|
207
|
+
throw new Error(`Segment ${record.id} belongs to another transaction`);
|
|
208
|
+
}
|
|
209
|
+
if (record.commitOrdinal === undefined) {
|
|
210
|
+
record = { ...record, commitOrdinal: this.#record.pendingSegmentIds.length };
|
|
211
|
+
}
|
|
212
|
+
this.#changedTableIds.add(record.tableId);
|
|
213
|
+
await this.store.addSegment(record);
|
|
214
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
215
|
+
pendingSegmentIds: [...this.#record.pendingSegmentIds, record.id],
|
|
216
|
+
updatedAt: this.now().toISOString(),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** Reconciles a persisted segment whose journal update was interrupted. */
|
|
220
|
+
async stageExistingSegment(segmentId) {
|
|
221
|
+
this.#assertActive();
|
|
222
|
+
if (this.#record.pendingSegmentIds.includes(segmentId))
|
|
223
|
+
return;
|
|
224
|
+
const record = await this.store.getSegment(segmentId);
|
|
225
|
+
if (record === undefined)
|
|
226
|
+
throw new Error(`Cannot stage a missing existing segment: ${segmentId}`);
|
|
227
|
+
if (record.transactionId !== this.id) {
|
|
228
|
+
throw new Error(`Segment ${segmentId} belongs to another transaction`);
|
|
229
|
+
}
|
|
230
|
+
this.#changedTableIds.add(record.tableId);
|
|
231
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
232
|
+
pendingSegmentIds: [...this.#record.pendingSegmentIds, segmentId],
|
|
233
|
+
updatedAt: this.now().toISOString(),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Records that this commit changes a table's logical content without staging a segment for
|
|
238
|
+
* it — dropping the table, for instance, whose only publish is the retirement of its blocks.
|
|
239
|
+
* Change-driven readers such as live queries need to see it move.
|
|
240
|
+
*/
|
|
241
|
+
markTableChanged(tableId) {
|
|
242
|
+
this.#assertActive();
|
|
243
|
+
this.#changedTableIds.add(tableId);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Marks this commit as logically content-preserving (for example a compaction rewrite), so
|
|
247
|
+
* change-driven readers such as live queries skip it even though it stages segments.
|
|
248
|
+
*/
|
|
249
|
+
markLogicallyUnchanged() {
|
|
250
|
+
this.#assertActive();
|
|
251
|
+
this.#logicallyUnchanged = true;
|
|
252
|
+
}
|
|
253
|
+
/** Accumulated key changes in operation order, for in-scope membership overlays. */
|
|
254
|
+
get accumulatedUniqueKeyChanges() {
|
|
255
|
+
return this.#uniqueKeyChanges;
|
|
256
|
+
}
|
|
257
|
+
/** Appends one operation's key changes; entries commit in operation order. */
|
|
258
|
+
setUniqueKeyChanges(changes) {
|
|
259
|
+
this.#assertActive();
|
|
260
|
+
this.#uniqueKeyChanges.push({
|
|
261
|
+
tableId: changes.tableId,
|
|
262
|
+
keyTokens: [...new Set(changes.keyTokens)].sort(),
|
|
263
|
+
requireAbsent: changes.requireAbsent,
|
|
264
|
+
...(changes.remove === undefined ? {} : { remove: changes.remove }),
|
|
265
|
+
...(changes.storageMode === undefined ? {} : { storageMode: changes.storageMode }),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Attaches one batch's full-text deltas; applied atomically with the publish. A second
|
|
270
|
+
* batch for the same table merges per column — postings re-sorted by term (each batch's
|
|
271
|
+
* reserved row ids are strictly above the last, so rowIds stay ascending within a term)
|
|
272
|
+
* and token totals summed — so a scope can insert into one FTS table any number of times.
|
|
273
|
+
*/
|
|
274
|
+
setFtsChanges(changes) {
|
|
275
|
+
this.#assertActive();
|
|
276
|
+
const existing = this.#ftsChanges.find((entry) => entry.tableId === changes.tableId);
|
|
277
|
+
if (existing === undefined) {
|
|
278
|
+
this.#ftsChanges.push(changes);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const merged = new Map(existing.columns.map((column) => [column.columnId, column]));
|
|
282
|
+
for (const column of changes.columns) {
|
|
283
|
+
const present = merged.get(column.columnId);
|
|
284
|
+
if (present === undefined) {
|
|
285
|
+
merged.set(column.columnId, column);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const byTerm = new Map(present.postings.map((posting) => [posting.term, posting]));
|
|
289
|
+
for (const posting of column.postings) {
|
|
290
|
+
const held = byTerm.get(posting.term);
|
|
291
|
+
if (held === undefined) {
|
|
292
|
+
byTerm.set(posting.term, posting);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
byTerm.set(posting.term, {
|
|
296
|
+
term: posting.term,
|
|
297
|
+
rowIds: [...held.rowIds, ...posting.rowIds],
|
|
298
|
+
tf: [...held.tf, ...posting.tf],
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
merged.set(column.columnId, {
|
|
303
|
+
columnId: column.columnId,
|
|
304
|
+
postings: [...byTerm.values()].sort((left, right) => left.term < right.term ? -1 : left.term > right.term ? 1 : 0),
|
|
305
|
+
totalTokens: present.totalTokens + column.totalTokens,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
this.#ftsChanges[this.#ftsChanges.indexOf(existing)] = {
|
|
309
|
+
tableId: changes.tableId,
|
|
310
|
+
columns: [...merged.values()],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
supersedeBlocks(blockIds) {
|
|
314
|
+
this.#assertActive();
|
|
315
|
+
for (const id of blockIds) {
|
|
316
|
+
if (id.length === 0)
|
|
317
|
+
throw new TypeError("Superseded block ID cannot be empty");
|
|
318
|
+
if (this.#record.pendingBlockIds.includes(id)) {
|
|
319
|
+
throw new Error(`A transaction cannot supersede its own pending block: ${id}`);
|
|
320
|
+
}
|
|
321
|
+
this.#supersededBlockIds.add(id);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async commit() {
|
|
325
|
+
this.#assertActive();
|
|
326
|
+
// The store derives the published manifest from its stored base plus this delta, so the
|
|
327
|
+
// commit neither loads nor rebuilds the full block list.
|
|
328
|
+
const committedAt = this.now().toISOString();
|
|
329
|
+
try {
|
|
330
|
+
const manifest = await this.store.commitTransaction({
|
|
331
|
+
transactionId: this.id,
|
|
332
|
+
expectedTransactionRevision: this.#record.revision,
|
|
333
|
+
expectedManifestVersion: this.#record.snapshotVersion,
|
|
334
|
+
changedTableIds: this.#logicallyUnchanged ? [] : [...this.#changedTableIds],
|
|
335
|
+
...(this.#supersededBlockIds.size === 0
|
|
336
|
+
? {}
|
|
337
|
+
: { removedBlockIds: [...this.#supersededBlockIds] }),
|
|
338
|
+
...(this.#uniqueKeyChanges.length === 0
|
|
339
|
+
? {}
|
|
340
|
+
: { uniqueKeyChanges: this.#uniqueKeyChanges }),
|
|
341
|
+
...(this.#ftsChanges.length === 0 ? {} : { ftsChanges: this.#ftsChanges }),
|
|
342
|
+
committedAt,
|
|
343
|
+
});
|
|
344
|
+
this.#record = {
|
|
345
|
+
...this.#record,
|
|
346
|
+
status: "committed",
|
|
347
|
+
committedVersion: manifest.version,
|
|
348
|
+
revision: this.#record.revision + 1,
|
|
349
|
+
updatedAt: committedAt,
|
|
350
|
+
};
|
|
351
|
+
return manifest;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const persisted = await this.store.getTransaction(this.id);
|
|
355
|
+
if (persisted?.status === "committed" && persisted.committedVersion !== null) {
|
|
356
|
+
const manifest = await this.store.getManifest(persisted.committedVersion);
|
|
357
|
+
if (manifest !== undefined) {
|
|
358
|
+
this.#record = persisted;
|
|
359
|
+
return manifest;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async rebase() {
|
|
366
|
+
this.#assertActive();
|
|
367
|
+
for (;;) {
|
|
368
|
+
const current = await this.store.getCurrentManifest();
|
|
369
|
+
try {
|
|
370
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
371
|
+
snapshotVersion: current?.version ?? null,
|
|
372
|
+
updatedAt: this.now().toISOString(),
|
|
373
|
+
});
|
|
374
|
+
return new Snapshot(this.store, current?.version ?? null, current?.blockIds ?? []);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (error instanceof SnapshotManifestMissingError) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async abort() {
|
|
385
|
+
this.#assertActive();
|
|
386
|
+
this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
|
|
387
|
+
status: "aborted",
|
|
388
|
+
updatedAt: this.now().toISOString(),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
#assertActive() {
|
|
392
|
+
if (this.#record.status !== "active")
|
|
393
|
+
throw new TransactionClosedError(this.id);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
export class TransactionManager {
|
|
397
|
+
store;
|
|
398
|
+
#now;
|
|
399
|
+
#createId;
|
|
400
|
+
constructor(store, options = {}) {
|
|
401
|
+
this.store = store;
|
|
402
|
+
this.#now = options.now ?? (() => new Date());
|
|
403
|
+
this.#createId = options.createId ?? (() => crypto.randomUUID());
|
|
404
|
+
}
|
|
405
|
+
async begin() {
|
|
406
|
+
return (await this.beginWithReservation()).transaction;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Begins a transaction at the current manifest version and optionally reserves row ids and
|
|
410
|
+
* auto-increment values in the same step. Stores implementing the atomic `beginTransaction`
|
|
411
|
+
* pay one storage round trip instead of the version-read/create/reserve sequence; the
|
|
412
|
+
* read-then-create fallback retries when the version it read is pruned before the record lands.
|
|
413
|
+
*/
|
|
414
|
+
async beginWithReservation(reserveRowIds, reserveAutoIncrement) {
|
|
415
|
+
const id = this.#createId();
|
|
416
|
+
const batched = this.store.beginTransaction?.bind(this.store);
|
|
417
|
+
if (batched !== undefined) {
|
|
418
|
+
const timestamp = this.#now().toISOString();
|
|
419
|
+
const begun = await batched({
|
|
420
|
+
record: {
|
|
421
|
+
id,
|
|
422
|
+
pendingBlockIds: [],
|
|
423
|
+
pendingSegmentIds: [],
|
|
424
|
+
status: "active",
|
|
425
|
+
revision: 0,
|
|
426
|
+
startedAt: timestamp,
|
|
427
|
+
updatedAt: timestamp,
|
|
428
|
+
committedVersion: null,
|
|
429
|
+
},
|
|
430
|
+
...(reserveRowIds === undefined ? {} : { reserveRowIds }),
|
|
431
|
+
...(reserveAutoIncrement === undefined ? {} : { reserveAutoIncrement }),
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
transaction: new DatabaseTransaction(this.store, begun.record, this.#now),
|
|
435
|
+
...(begun.rowIds === undefined ? {} : { rowIds: begun.rowIds }),
|
|
436
|
+
...(begun.autoIncrementValues === undefined
|
|
437
|
+
? {}
|
|
438
|
+
: { autoIncrementValues: begun.autoIncrementValues }),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
for (;;) {
|
|
442
|
+
const currentVersion = await this.store.getCurrentManifestVersion();
|
|
443
|
+
const timestamp = this.#now().toISOString();
|
|
444
|
+
const record = {
|
|
445
|
+
id,
|
|
446
|
+
snapshotVersion: currentVersion,
|
|
447
|
+
pendingBlockIds: [],
|
|
448
|
+
pendingSegmentIds: [],
|
|
449
|
+
status: "active",
|
|
450
|
+
revision: 0,
|
|
451
|
+
startedAt: timestamp,
|
|
452
|
+
updatedAt: timestamp,
|
|
453
|
+
committedVersion: null,
|
|
454
|
+
};
|
|
455
|
+
try {
|
|
456
|
+
await this.store.createTransaction(record);
|
|
457
|
+
const transaction = new DatabaseTransaction(this.store, record, this.#now);
|
|
458
|
+
const autoIncrementValues = reserveAutoIncrement === undefined
|
|
459
|
+
? undefined
|
|
460
|
+
: await this.store.reserveAutoIncrement(reserveAutoIncrement.tableId, reserveAutoIncrement.columnId, reserveAutoIncrement.count, reserveAutoIncrement.atLeast);
|
|
461
|
+
const rowIds = reserveRowIds === undefined
|
|
462
|
+
? undefined
|
|
463
|
+
: await this.store.reserveRowIds(reserveRowIds.tableId, reserveRowIds.count);
|
|
464
|
+
return {
|
|
465
|
+
transaction,
|
|
466
|
+
...(rowIds === undefined ? {} : { rowIds }),
|
|
467
|
+
...(autoIncrementValues === undefined ? {} : { autoIncrementValues }),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (error instanceof SnapshotManifestMissingError) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async resume(transactionId) {
|
|
479
|
+
const record = await this.store.getTransaction(transactionId);
|
|
480
|
+
if (record === undefined)
|
|
481
|
+
throw new Error(`Transaction not found: ${transactionId}`);
|
|
482
|
+
if (record.status !== "active")
|
|
483
|
+
throw new TransactionClosedError(transactionId);
|
|
484
|
+
return new DatabaseTransaction(this.store, record, this.#now);
|
|
485
|
+
}
|
|
486
|
+
async openSnapshot(version) {
|
|
487
|
+
if (version === null)
|
|
488
|
+
return new Snapshot(this.store, null, []);
|
|
489
|
+
if (version !== undefined)
|
|
490
|
+
return loadSnapshot(this.store, version);
|
|
491
|
+
const current = await this.store.getCurrentManifest();
|
|
492
|
+
return new Snapshot(this.store, current?.version ?? null, current?.blockIds ?? []);
|
|
493
|
+
}
|
|
494
|
+
async openLeasedSnapshot(options) {
|
|
495
|
+
validateTtl(options.ttlMs);
|
|
496
|
+
if (options.ownerId.trim().length === 0)
|
|
497
|
+
throw new TypeError("Lease owner cannot be empty");
|
|
498
|
+
if (options.id?.trim().length === 0) {
|
|
499
|
+
throw new TypeError("Lease ID cannot be empty");
|
|
500
|
+
}
|
|
501
|
+
const id = options.id ?? this.#createId();
|
|
502
|
+
for (;;) {
|
|
503
|
+
const snapshot = await this.openSnapshot(options.version);
|
|
504
|
+
const record = {
|
|
505
|
+
id,
|
|
506
|
+
kind: options.kind ?? "reader",
|
|
507
|
+
manifestVersion: snapshot.version,
|
|
508
|
+
ownerId: options.ownerId,
|
|
509
|
+
expiresAt: new Date(this.#now().getTime() + options.ttlMs).toISOString(),
|
|
510
|
+
revision: 0,
|
|
511
|
+
};
|
|
512
|
+
try {
|
|
513
|
+
await this.store.createLease(record);
|
|
514
|
+
return new LeasedSnapshot(this.store, snapshot.version, snapshot.listBlockIds(), record, this.#now);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (options.version === undefined && error instanceof SnapshotManifestMissingError) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async removeExpiredLeases(at = this.#now()) {
|
|
525
|
+
const timestamp = at.getTime();
|
|
526
|
+
if (!Number.isFinite(timestamp))
|
|
527
|
+
throw new TypeError("Lease cutoff must be a valid date");
|
|
528
|
+
const removed = [];
|
|
529
|
+
for (const lease of await this.store.listLeases()) {
|
|
530
|
+
if (Date.parse(lease.expiresAt) > timestamp)
|
|
531
|
+
continue;
|
|
532
|
+
try {
|
|
533
|
+
if (await this.store.removeLeaseIfExpired(lease.id, lease.revision, at.toISOString())) {
|
|
534
|
+
removed.push(lease.id);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error instanceof LeaseConflictError)
|
|
539
|
+
continue;
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return removed.sort();
|
|
544
|
+
}
|
|
545
|
+
async recover(options) {
|
|
546
|
+
const staleBefore = options.staleBefore.getTime();
|
|
547
|
+
if (!Number.isFinite(staleBefore))
|
|
548
|
+
throw new TypeError("Recovery cutoff must be a valid date");
|
|
549
|
+
const abortedTransactionIds = [];
|
|
550
|
+
const skippedTransactionIds = [];
|
|
551
|
+
const candidates = new Set();
|
|
552
|
+
const segmentCandidates = new Set();
|
|
553
|
+
let transactionCursor = null;
|
|
554
|
+
do {
|
|
555
|
+
const page = await this.store.listTransactionPage(transactionCursor, 64);
|
|
556
|
+
for (const record of page.records) {
|
|
557
|
+
if (record.status !== "active" || Date.parse(record.updatedAt) >= staleBefore)
|
|
558
|
+
continue;
|
|
559
|
+
try {
|
|
560
|
+
await this.store.updateTransaction(record.id, record.revision, {
|
|
561
|
+
status: "aborted",
|
|
562
|
+
updatedAt: this.#now().toISOString(),
|
|
563
|
+
});
|
|
564
|
+
abortedTransactionIds.push(record.id);
|
|
565
|
+
record.pendingBlockIds.forEach((id) => candidates.add(id));
|
|
566
|
+
record.pendingSegmentIds.forEach((id) => segmentCandidates.add(id));
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
if (error instanceof TransactionRecordConflictError) {
|
|
570
|
+
skippedTransactionIds.push(record.id);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
transactionCursor = page.nextCursor;
|
|
577
|
+
} while (transactionCursor !== null);
|
|
578
|
+
const removedBlockIds = [];
|
|
579
|
+
const retainedBlockIds = [];
|
|
580
|
+
const removedSegmentIds = [];
|
|
581
|
+
const retainedSegmentIds = [];
|
|
582
|
+
if (options.removePendingBlocks === false) {
|
|
583
|
+
retainedBlockIds.push(...[...candidates].sort());
|
|
584
|
+
retainedSegmentIds.push(...[...segmentCandidates].sort());
|
|
585
|
+
}
|
|
586
|
+
else if (candidates.size > 0 || segmentCandidates.size > 0) {
|
|
587
|
+
const timestamp = this.#now().toISOString();
|
|
588
|
+
const baseId = `recovery/${timestamp}/${this.#createId()}`;
|
|
589
|
+
let suffix = 0;
|
|
590
|
+
let job;
|
|
591
|
+
for (;;) {
|
|
592
|
+
const id = suffix === 0 ? baseId : `${baseId}/${String(suffix)}`;
|
|
593
|
+
if ((await this.store.getGarbageCollectionJob(id)) !== undefined) {
|
|
594
|
+
suffix += 1;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
job = await this.store.createGarbageCollectionJob({
|
|
599
|
+
id,
|
|
600
|
+
candidateManifestVersions: [],
|
|
601
|
+
candidateSegmentIds: [...segmentCandidates],
|
|
602
|
+
candidateBlockIds: [...candidates],
|
|
603
|
+
leaseCutoff: timestamp,
|
|
604
|
+
createdAt: timestamp,
|
|
605
|
+
});
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
if ((await this.store.getGarbageCollectionJob(id)) === undefined)
|
|
610
|
+
throw error;
|
|
611
|
+
suffix += 1;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
while (job.state !== "completed") {
|
|
615
|
+
try {
|
|
616
|
+
const step = await this.store.runGarbageCollectionStep({
|
|
617
|
+
jobId: job.id,
|
|
618
|
+
expectedRevision: job.revision,
|
|
619
|
+
maxItems: 128,
|
|
620
|
+
updatedAt: this.#now().toISOString(),
|
|
621
|
+
});
|
|
622
|
+
removedBlockIds.push(...step.reclaimedBlockIds);
|
|
623
|
+
retainedBlockIds.push(...step.retainedBlockIds);
|
|
624
|
+
removedSegmentIds.push(...step.reclaimedSegmentIds);
|
|
625
|
+
retainedSegmentIds.push(...step.retainedSegmentIds);
|
|
626
|
+
job = step.job;
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
if (!(error instanceof GarbageCollectionJobConflictError))
|
|
630
|
+
throw error;
|
|
631
|
+
const latest = await this.store.getGarbageCollectionJob(job.id);
|
|
632
|
+
if (latest === undefined)
|
|
633
|
+
throw new Error(`Garbage collection job not found: ${job.id}`, { cause: error });
|
|
634
|
+
job = latest;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
abortedTransactionIds: abortedTransactionIds.sort(),
|
|
640
|
+
skippedTransactionIds: skippedTransactionIds.sort(),
|
|
641
|
+
removedBlockIds,
|
|
642
|
+
retainedBlockIds,
|
|
643
|
+
removedSegmentIds,
|
|
644
|
+
retainedSegmentIds,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function validateTtl(ttlMs) {
|
|
649
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
650
|
+
throw new RangeError("Lease lifetime must be a positive number of milliseconds");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async function loadSnapshot(store, version) {
|
|
654
|
+
if (version === null)
|
|
655
|
+
return new Snapshot(store, null, []);
|
|
656
|
+
const manifest = await store.getManifest(version);
|
|
657
|
+
if (manifest === undefined || manifest.prunedAt !== undefined) {
|
|
658
|
+
throw new SnapshotManifestMissingError(version);
|
|
659
|
+
}
|
|
660
|
+
return new Snapshot(store, manifest.version, manifest.blockIds);
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=index.js.map
|