@minnowdb/core 0.10.1 → 0.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",
@@ -149,6 +149,9 @@
149
149
  "!dist/engine/storage-test-helpers.*",
150
150
  "!dist/testing/seeds.*",
151
151
  "!dist/testing/oracle.*",
152
+ "!dist/engine/client-audit-harness.*",
153
+ "!dist/storage/indexeddb-audit-helpers.*",
154
+ "!dist/storage/opfs/power-loss-model.*",
152
155
  "!dist/*.tsbuildinfo",
153
156
  "sql-feature-matrix.json",
154
157
  "postgres-feature-profile.json"
@@ -1,123 +0,0 @@
1
- import { MemoryBlockStore } from "../storage/index.js";
2
- function createBoundary() {
3
- const clientListeners = /* @__PURE__ */ new Map();
4
- const workerListeners = /* @__PURE__ */ new Map();
5
- let chain = Promise.resolve();
6
- let severed = false;
7
- const sentByClient = [];
8
- const sentByWorker = [];
9
- const deliver = (target, message, transfer) => {
10
- const data = structuredClone(message, transfer === void 0 ? void 0 : { transfer });
11
- if (severed)
12
- return;
13
- chain = chain.then(() => {
14
- if (severed)
15
- return;
16
- for (const listener of target.get("message") ?? [])
17
- listener({ data });
18
- });
19
- };
20
- const add = (map, type, listener) => {
21
- const list = map.get(type) ?? [];
22
- list.push(listener);
23
- map.set(type, list);
24
- };
25
- const remove = (map, type, listener) => {
26
- const list = map.get(type) ?? [];
27
- const index = list.indexOf(listener);
28
- if (index >= 0)
29
- list.splice(index, 1);
30
- };
31
- const clientSide = {
32
- postMessage: (message, options) => {
33
- sentByClient.push(structuredClone(message));
34
- deliver(workerListeners, message, options?.transfer);
35
- },
36
- addEventListener: (type, listener) => {
37
- add(clientListeners, type, listener);
38
- },
39
- removeEventListener: (type, listener) => {
40
- remove(clientListeners, type, listener);
41
- },
42
- terminate: () => {
43
- severed = true;
44
- }
45
- };
46
- const workerSide = {
47
- postMessage: (message, options) => {
48
- sentByWorker.push(structuredClone(message));
49
- deliver(clientListeners, message, options?.transfer);
50
- },
51
- addEventListener: (type, listener) => {
52
- add(workerListeners, type, listener);
53
- }
54
- };
55
- return {
56
- sentByClient,
57
- sentByWorker,
58
- clientSide,
59
- workerSide,
60
- sever: () => {
61
- severed = true;
62
- },
63
- injectToClient: (frame) => {
64
- for (const listener of clientListeners.get("message") ?? []) {
65
- listener({ data: frame });
66
- }
67
- },
68
- injectToWorker: (frame) => {
69
- for (const listener of workerListeners.get("message") ?? []) {
70
- listener({ data: frame });
71
- }
72
- },
73
- emitWorkerGlobal: (type, event) => {
74
- for (const listener of workerListeners.get(type) ?? [])
75
- listener(event);
76
- },
77
- emitTransport: (type, event) => {
78
- for (const listener of clientListeners.get(type) ?? [])
79
- listener(event);
80
- },
81
- flush: async () => {
82
- await chain;
83
- await new Promise((resolve) => setTimeout(resolve, 5));
84
- }
85
- };
86
- }
87
- function settled(ms = 20) {
88
- return new Promise((resolve) => setTimeout(resolve, ms));
89
- }
90
- function faultyStore(fault, options = {}) {
91
- const inner = new MemoryBlockStore();
92
- const calls = [];
93
- const hidden = new Set(options.hide ?? []);
94
- const store = new Proxy(inner, {
95
- get(target, property) {
96
- if (typeof property === "string" && hidden.has(property))
97
- return void 0;
98
- const value = target[property];
99
- if (typeof value !== "function" || typeof property !== "string")
100
- return value;
101
- const method = property;
102
- return (...args) => {
103
- const run = () => value.apply(inner, args);
104
- const isAsync = value.constructor?.name === "AsyncFunction";
105
- if (!isAsync)
106
- return run();
107
- calls.push({ method, args });
108
- return fault(method, args, run);
109
- };
110
- },
111
- has(target, property) {
112
- if (typeof property === "string" && hidden.has(property))
113
- return false;
114
- return Reflect.has(target, property);
115
- }
116
- });
117
- return { store, calls };
118
- }
119
- export {
120
- createBoundary,
121
- faultyStore,
122
- settled
123
- };
@@ -1,269 +0,0 @@
1
- import { IndexedDbBlockStore } from "./indexeddb.js";
2
- const NOW = "2026-09-12T12:00:00.000Z";
3
- async function openStore(indexedDB, name = crypto.randomUUID(), durability) {
4
- return IndexedDbBlockStore.open({ name, indexedDB, ...durability ? { durability } : {} });
5
- }
6
- const EVENTS_TABLE = {
7
- managed: false,
8
- id: "events",
9
- name: "events",
10
- columns: [{ id: "value", name: "value", type: "number", nullable: false }],
11
- revision: 0,
12
- createdAt: NOW
13
- };
14
- function activeTransaction(id, snapshotVersion) {
15
- return {
16
- id,
17
- ownerId: `owner-${id}`,
18
- expiresAt: "2026-09-12T12:30:00.000Z",
19
- snapshotVersion,
20
- pendingBlockIds: [],
21
- pendingSegmentIds: [],
22
- status: "active",
23
- revision: 0,
24
- startedAt: NOW,
25
- updatedAt: NOW,
26
- committedVersion: null
27
- };
28
- }
29
- function segment(id, transactionId, blockId, commitOrdinal = 0, rowIdStart = 1n) {
30
- return {
31
- id,
32
- tableId: "events",
33
- transactionId,
34
- rowCount: 1,
35
- rowIdStart,
36
- rowIdEndExclusive: rowIdStart + 1n,
37
- columnBlockIds: { value: [blockId] },
38
- kind: "insert",
39
- level: 0,
40
- logicalOrder: 0,
41
- commitOrdinal,
42
- rowIdSpans: [],
43
- createdAt: NOW
44
- };
45
- }
46
- async function rawOpen(indexedDB, name) {
47
- return new Promise((resolve, reject) => {
48
- const request = indexedDB.open(name);
49
- request.onsuccess = () => resolve(request.result);
50
- request.onerror = () => reject(request.error ?? new Error("raw IndexedDB open failed"));
51
- });
52
- }
53
- async function readRawValue(indexedDB, name, storeName, key) {
54
- const database = await rawOpen(indexedDB, name);
55
- try {
56
- const transaction = database.transaction(storeName, "readonly");
57
- const value = await new Promise((resolve, reject) => {
58
- const request = transaction.objectStore(storeName).get(key);
59
- request.onsuccess = () => resolve(request.result);
60
- request.onerror = () => reject(request.error ?? new Error("raw IndexedDB read failed"));
61
- });
62
- await new Promise((resolve, reject) => {
63
- transaction.oncomplete = () => resolve();
64
- transaction.onabort = () => reject(transaction.error ?? new Error("raw read aborted"));
65
- });
66
- return value;
67
- } finally {
68
- database.close();
69
- }
70
- }
71
- async function readRawKeys(indexedDB, name, storeName) {
72
- const database = await rawOpen(indexedDB, name);
73
- try {
74
- const transaction = database.transaction(storeName, "readonly");
75
- const keys = await new Promise((resolve, reject) => {
76
- const request = transaction.objectStore(storeName).getAllKeys();
77
- request.onsuccess = () => resolve(request.result);
78
- request.onerror = () => reject(request.error ?? new Error("raw IndexedDB read failed"));
79
- });
80
- await new Promise((resolve, reject) => {
81
- transaction.oncomplete = () => resolve();
82
- transaction.onabort = () => reject(transaction.error ?? new Error("raw read aborted"));
83
- });
84
- return keys;
85
- } finally {
86
- database.close();
87
- }
88
- }
89
- function instrumentFactory(indexedDB) {
90
- const state = {
91
- factory: indexedDB,
92
- transactions: [],
93
- requestCounts: /* @__PURE__ */ new Map(),
94
- setHook(hook) {
95
- currentHook = hook;
96
- },
97
- reset() {
98
- state.transactions.length = 0;
99
- state.requestCounts.clear();
100
- }
101
- };
102
- let currentHook;
103
- const wrappedScanners = /* @__PURE__ */ new WeakSet();
104
- const countScans = (target, label) => {
105
- if (wrappedScanners.has(target))
106
- return;
107
- wrappedScanners.add(target);
108
- for (const method of [
109
- "openCursor",
110
- "openKeyCursor",
111
- "getAll",
112
- "getAllKeys",
113
- "count"
114
- ]) {
115
- const original = target[method].bind(target);
116
- Object.defineProperty(target, method, {
117
- configurable: true,
118
- value: (...args) => {
119
- const key = `${label}:${method}`;
120
- state.requestCounts.set(key, (state.requestCounts.get(key) ?? 0) + 1);
121
- const request = original(...args);
122
- if (method === "openCursor" || method === "openKeyCursor") {
123
- request.addEventListener("success", () => {
124
- const stepKey = `${label}:cursor-steps`;
125
- if (request.result !== null && request.result !== void 0) {
126
- state.requestCounts.set(stepKey, (state.requestCounts.get(stepKey) ?? 0) + 1);
127
- }
128
- });
129
- }
130
- return request;
131
- }
132
- });
133
- }
134
- };
135
- const wrappedStores = /* @__PURE__ */ new WeakSet();
136
- const originalOpen = indexedDB.open.bind(indexedDB);
137
- Object.defineProperty(indexedDB, "open", {
138
- configurable: true,
139
- value: (name, version) => {
140
- const request = version === void 0 ? originalOpen(name) : originalOpen(name, version);
141
- request.addEventListener("success", () => {
142
- const database = request.result;
143
- const originalTransaction = database.transaction.bind(database);
144
- Object.defineProperty(database, "transaction", {
145
- configurable: true,
146
- value: (stores, mode, options) => {
147
- const transaction = originalTransaction(stores, mode, options);
148
- const entry = {
149
- transaction,
150
- stores: typeof stores === "string" ? [stores] : [...stores],
151
- mode: mode ?? "readonly",
152
- options,
153
- completed: false,
154
- aborted: false
155
- };
156
- transaction.addEventListener("complete", () => {
157
- entry.completed = true;
158
- });
159
- transaction.addEventListener("abort", () => {
160
- entry.aborted = true;
161
- });
162
- state.transactions.push(entry);
163
- const originalObjectStore = transaction.objectStore.bind(transaction);
164
- Object.defineProperty(transaction, "objectStore", {
165
- configurable: true,
166
- value: (storeName) => {
167
- const store = originalObjectStore(storeName);
168
- if (wrappedStores.has(store))
169
- return store;
170
- wrappedStores.add(store);
171
- countScans(store, storeName);
172
- const originalIndex = store.index.bind(store);
173
- Object.defineProperty(store, "index", {
174
- configurable: true,
175
- value: (indexName) => {
176
- const index = originalIndex(indexName);
177
- countScans(index, `${storeName}.${indexName}`);
178
- return index;
179
- }
180
- });
181
- for (const method of ["put", "add", "delete", "get", "getKey"]) {
182
- const original = store[method].bind(store);
183
- Object.defineProperty(store, method, {
184
- configurable: true,
185
- value: (...args) => {
186
- state.requestCounts.set(storeName, (state.requestCounts.get(storeName) ?? 0) + 1);
187
- const [first, second] = args;
188
- const isWrite = method === "put" || method === "add";
189
- const decision = currentHook?.({
190
- transaction,
191
- storeName,
192
- method,
193
- key: isWrite ? second : first,
194
- value: isWrite ? first : void 0
195
- });
196
- if (decision === "throw-quota") {
197
- const failing = original(...args);
198
- const queue = transaction._requests;
199
- const entry2 = queue.find((candidate) => candidate.request === failing);
200
- if (entry2 === void 0)
201
- throw new Error("request not queued");
202
- entry2.operation = () => {
203
- throw new DOMException("The quota has been exceeded.", "QuotaExceededError");
204
- };
205
- return failing;
206
- }
207
- const result = original(...args);
208
- if (decision === "abort") {
209
- try {
210
- transaction.abort();
211
- } catch {
212
- }
213
- }
214
- return result;
215
- }
216
- });
217
- }
218
- return store;
219
- }
220
- });
221
- return transaction;
222
- }
223
- });
224
- });
225
- return request;
226
- }
227
- });
228
- return state;
229
- }
230
- async function stageBlocks(store, record, ids) {
231
- let current = record;
232
- for (let start = 0; start < ids.length; start += 64) {
233
- const slice = ids.slice(start, start + 64);
234
- current = await store.stageTransactionArtifacts({
235
- transactionId: record.id,
236
- expectedRevision: current.revision,
237
- blocks: slice.map((id) => ({ id, bytes: Uint8Array.of(1) })),
238
- segments: [],
239
- updatedAt: NOW
240
- });
241
- }
242
- return current;
243
- }
244
- async function stageSegments(store, record, ids, blockId) {
245
- let current = record;
246
- for (let start = 0; start < ids.length; start += 64) {
247
- const slice = ids.slice(start, start + 64);
248
- current = await store.stageTransactionArtifacts({
249
- transactionId: record.id,
250
- expectedRevision: current.revision,
251
- blocks: [],
252
- segments: slice.map((id, offset) => segment(id, record.id, blockId, current.pendingSegmentIds.length + offset, BigInt(current.pendingSegmentIds.length + offset + 1))),
253
- updatedAt: NOW
254
- });
255
- }
256
- return current;
257
- }
258
- export {
259
- EVENTS_TABLE,
260
- NOW,
261
- activeTransaction,
262
- instrumentFactory,
263
- openStore,
264
- readRawKeys,
265
- readRawValue,
266
- segment,
267
- stageBlocks,
268
- stageSegments
269
- };
@@ -1,62 +0,0 @@
1
- class PowerLossModel {
2
- #durable = /* @__PURE__ */ new Map();
3
- #touched = /* @__PURE__ */ new Set();
4
- #shim;
5
- constructor(shim) {
6
- this.#shim = shim;
7
- shim.setWriteFault((path, phase) => {
8
- if (phase === "flush") {
9
- this.#durable.set(path, shim.readFileBytes(path) ?? new Uint8Array());
10
- } else {
11
- this.#touched.add(path);
12
- }
13
- });
14
- }
15
- durableBytes(path) {
16
- return this.#durable.get(path);
17
- }
18
- powerLoss(keepPrefix) {
19
- const reverted = [];
20
- for (const path of this.#touched) {
21
- const current = this.#shim.readFileBytes(path);
22
- if (current === void 0)
23
- continue;
24
- const durable = this.#durable.get(path) ?? new Uint8Array();
25
- let kept = durable;
26
- if (keepPrefix !== void 0 && current.byteLength > durable.byteLength && startsWith(current, durable)) {
27
- const unflushed = current.byteLength - durable.byteLength;
28
- const extra = keepPrefix(unflushed, path);
29
- kept = current.slice(0, durable.byteLength + Math.max(0, Math.min(extra, unflushed)));
30
- }
31
- if (!bytesEqual(kept, current)) {
32
- this.#shim.writeFileBytes(path, kept);
33
- reverted.push(path);
34
- }
35
- this.#durable.set(path, kept.slice());
36
- }
37
- this.#touched.clear();
38
- return reverted;
39
- }
40
- }
41
- function startsWith(bytes, prefix) {
42
- if (prefix.byteLength > bytes.byteLength)
43
- return false;
44
- for (let index = 0; index < prefix.byteLength; index += 1) {
45
- if (bytes[index] !== prefix[index])
46
- return false;
47
- }
48
- return true;
49
- }
50
- function bytesEqual(left, right) {
51
- if (left.byteLength !== right.byteLength)
52
- return false;
53
- for (let index = 0; index < left.byteLength; index += 1) {
54
- if (left[index] !== right[index])
55
- return false;
56
- }
57
- return true;
58
- }
59
- export {
60
- PowerLossModel,
61
- bytesEqual
62
- };