hyperdb-wasm 0.0.27356-dev.wasm.ga5fd9868 → 0.0.27367-dev.wasm.g50457039
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/dist/api.d.ts +2 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +64 -30
- package/dist/browser_database_locks.d.ts +11 -0
- package/dist/browser_worker.js +155 -17
- package/dist/client.d.ts +5 -1
- package/dist/database_locks.d.ts +15 -0
- package/dist/hyperdb-wasm.cjs +1 -1
- package/dist/hyperdb-wasm.js +1 -1
- package/dist/hyperdb-wasm.wasm.br +0 -0
- package/dist/node.d.ts +3 -1
- package/dist/node.js +70 -31
- package/dist/node_worker.js +107 -18
- package/dist/protocol.d.ts +1 -0
- package/dist/raw.browser.js +45 -15
- package/dist/raw.node.js +49 -15
- package/dist/wasm_bindings.d.ts +3 -0
- package/dist/worker_setup.d.ts +3 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { BrowserClientOptions } from './browser.js';
|
|
|
5
5
|
import type { MountSource, WasmResult } from './protocol.js';
|
|
6
6
|
import type { HyperDBErrorSource } from './protocol.js';
|
|
7
7
|
export interface HyperDBClient {
|
|
8
|
+
readonly failure?: Error | undefined;
|
|
8
9
|
ready(): Promise<void>;
|
|
9
10
|
initialize(settings: HyperDBSettings): Promise<WasmResult>;
|
|
10
11
|
connect(settings?: ConnectionSettings): Promise<WasmResult>;
|
|
@@ -125,6 +126,7 @@ export declare class HyperDBConnection {
|
|
|
125
126
|
execute(sql: string, options?: QueryOptions): QueryPromise<bigint | null>;
|
|
126
127
|
private ensureAvailable;
|
|
127
128
|
closeForTermination(): Promise<void>;
|
|
129
|
+
releaseAfterShutdown(): void;
|
|
128
130
|
}
|
|
129
131
|
export type { HyperDBEngineClient };
|
|
130
132
|
export { initializeHyperDB as initializeHyperDBInternal };
|
package/dist/browser.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface BrowserClientOptions {
|
|
|
14
14
|
/** OPFS-relative directory mounted as the durable filesystem root. */
|
|
15
15
|
readonly storagePath?: string;
|
|
16
16
|
readonly onSetupProgress?: (progress: SetupProgress) => void;
|
|
17
|
+
/** Receives engine output; omitted callbacks discard output, and callback exceptions are ignored */
|
|
18
|
+
readonly onLog?: (stream: 'stdout' | 'stderr', text: string) => void;
|
|
17
19
|
}
|
|
18
20
|
type BrowserEnvironment = {
|
|
19
21
|
readonly WebAssembly?: typeof WebAssembly;
|
package/dist/browser.js
CHANGED
|
@@ -52,7 +52,7 @@ function isWorkerEvent(message) {
|
|
|
52
52
|
case "fatal":
|
|
53
53
|
return hasKeys(message, ["type", "error"]) && typeof message.error === "string";
|
|
54
54
|
case "log":
|
|
55
|
-
return hasKeys(message, ["type", "text"]) && typeof message.text === "string";
|
|
55
|
+
return hasKeys(message, ["type", "stream", "text"]) && (message.stream === "stdout" || message.stream === "stderr") && typeof message.text === "string";
|
|
56
56
|
case "response":
|
|
57
57
|
return hasKeys(message, ["id", "type", "result"]) && Number.isSafeInteger(message.id) && isWasmResult(message.result);
|
|
58
58
|
default:
|
|
@@ -72,8 +72,9 @@ var init_client = __esm({
|
|
|
72
72
|
"use strict";
|
|
73
73
|
init_protocol();
|
|
74
74
|
HyperDBEngineClient = class {
|
|
75
|
-
constructor(transport, onSetupProgress) {
|
|
75
|
+
constructor(transport, onSetupProgress, onLog) {
|
|
76
76
|
this.onSetupProgress = onSetupProgress;
|
|
77
|
+
this.onLog = onLog;
|
|
77
78
|
this.transport = transport;
|
|
78
79
|
this.readyPromise = new Promise((resolve, reject) => {
|
|
79
80
|
this.resolveReady = resolve;
|
|
@@ -81,11 +82,12 @@ var init_client = __esm({
|
|
|
81
82
|
});
|
|
82
83
|
this.unsubscribe = [
|
|
83
84
|
transport.onMessage((message) => this.onMessage(message)),
|
|
84
|
-
transport.onError((error) => this.
|
|
85
|
+
transport.onError((error) => this.failAndTerminate(error)),
|
|
85
86
|
transport.onClose((error) => this.fail(error))
|
|
86
87
|
];
|
|
87
88
|
}
|
|
88
89
|
onSetupProgress;
|
|
90
|
+
onLog;
|
|
89
91
|
transport;
|
|
90
92
|
pending = /* @__PURE__ */ new Map();
|
|
91
93
|
readyPromise;
|
|
@@ -93,10 +95,14 @@ var init_client = __esm({
|
|
|
93
95
|
rejectReady;
|
|
94
96
|
nextId = 1;
|
|
95
97
|
failed;
|
|
98
|
+
termination;
|
|
96
99
|
unsubscribe;
|
|
97
100
|
ready() {
|
|
98
101
|
return this.readyPromise;
|
|
99
102
|
}
|
|
103
|
+
get failure() {
|
|
104
|
+
return this.failed;
|
|
105
|
+
}
|
|
100
106
|
initialize(settings) {
|
|
101
107
|
return this.request({ id: this.nextId++, type: "initialize", settings });
|
|
102
108
|
}
|
|
@@ -131,8 +137,15 @@ var init_client = __esm({
|
|
|
131
137
|
return this.request({ id: this.nextId++, type: "finishQuery", query });
|
|
132
138
|
}
|
|
133
139
|
async terminate() {
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
if (!this.termination) {
|
|
141
|
+
this.termination = Promise.resolve().then(() => this.transport.terminate()).then(() => {
|
|
142
|
+
this.fail(new Error("engine worker terminated"));
|
|
143
|
+
}, (error) => {
|
|
144
|
+
this.termination = void 0;
|
|
145
|
+
throw error;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
await this.termination;
|
|
136
149
|
}
|
|
137
150
|
request(request, transfer = []) {
|
|
138
151
|
if (this.failed) return Promise.reject(this.failed);
|
|
@@ -156,8 +169,15 @@ var init_client = __esm({
|
|
|
156
169
|
this.onSetupProgress?.({ bytesLoaded: message.bytesLoaded, bytesTotal: message.bytesTotal });
|
|
157
170
|
return;
|
|
158
171
|
}
|
|
172
|
+
if (message.type === "log") {
|
|
173
|
+
try {
|
|
174
|
+
this.onLog?.(message.stream, message.text);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
159
179
|
if (message.type === "fatal") {
|
|
160
|
-
this.
|
|
180
|
+
this.failAndTerminate(new Error(message.error || "engine aborted"));
|
|
161
181
|
return;
|
|
162
182
|
}
|
|
163
183
|
if (message.type !== "response") return;
|
|
@@ -166,6 +186,10 @@ var init_client = __esm({
|
|
|
166
186
|
this.pending.delete(message.id);
|
|
167
187
|
pending.resolve(message.result);
|
|
168
188
|
}
|
|
189
|
+
failAndTerminate(error) {
|
|
190
|
+
this.fail(error);
|
|
191
|
+
void this.terminate().catch(() => void 0);
|
|
192
|
+
}
|
|
169
193
|
fail(error) {
|
|
170
194
|
if (this.failed) return;
|
|
171
195
|
this.failed = error;
|
|
@@ -234,7 +258,8 @@ function createBrowserClient(options = {}) {
|
|
|
234
258
|
const engineUrl = options.engineUrl ?? new URL("./hyperdb-wasm.js", import.meta.url);
|
|
235
259
|
return new HyperDBEngineClient(
|
|
236
260
|
new BrowserWorker(new Worker(resolveBrowserWorkerUrl(workerUrl, engineUrl, options.wasmUrl, options.storagePath), { type: "classic" })),
|
|
237
|
-
options.onSetupProgress
|
|
261
|
+
options.onSetupProgress,
|
|
262
|
+
options.onLog
|
|
238
263
|
);
|
|
239
264
|
}
|
|
240
265
|
var memory64Probe, threadsProbe, exceptionsProbe, tailCallsProbe, BrowserWorker;
|
|
@@ -591,25 +616,23 @@ var HyperDB = class _HyperDB {
|
|
|
591
616
|
}
|
|
592
617
|
async terminate() {
|
|
593
618
|
if (this.terminated) return;
|
|
594
|
-
if (this.activeDatabaseOperations !== 0) throw new Error("database has an active operation");
|
|
619
|
+
if (this.activeDatabaseOperations !== 0 && !this.client.failure) throw new Error("database has an active operation");
|
|
595
620
|
if (!this.termination) {
|
|
596
621
|
this.termination = (async () => {
|
|
597
622
|
try {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
this.termination = void 0;
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
609
|
-
for (const resource of this.mountedResources) resource.releaseAfterShutdown();
|
|
610
|
-
this.mountedResources.clear();
|
|
611
|
-
try {
|
|
623
|
+
if (!this.client.failure) {
|
|
624
|
+
try {
|
|
625
|
+
await Promise.all([...this.connections].map(async (connection) => connection.closeForTermination()));
|
|
626
|
+
expectOk(await this.client.shutdown(), "shutdown");
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (!this.client.failure) throw error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
612
631
|
await this.client.terminate();
|
|
632
|
+
for (const connection of this.connections) connection.releaseAfterShutdown();
|
|
633
|
+
this.connections.clear();
|
|
634
|
+
for (const resource of this.mountedResources) resource.releaseAfterShutdown();
|
|
635
|
+
this.mountedResources.clear();
|
|
613
636
|
this.terminated = true;
|
|
614
637
|
} catch (error) {
|
|
615
638
|
this.termination = void 0;
|
|
@@ -742,12 +765,17 @@ var HyperDBConnection = class {
|
|
|
742
765
|
const result = {
|
|
743
766
|
[Symbol.asyncIterator]() {
|
|
744
767
|
return {
|
|
745
|
-
next() {
|
|
746
|
-
|
|
747
|
-
abortDelivered
|
|
748
|
-
|
|
768
|
+
async next() {
|
|
769
|
+
try {
|
|
770
|
+
if (operation.aborted && !abortDelivered) {
|
|
771
|
+
abortDelivered = true;
|
|
772
|
+
throw operation.abortReason;
|
|
773
|
+
}
|
|
774
|
+
return await operation.interrupt(generator.next());
|
|
775
|
+
} catch (error) {
|
|
776
|
+
await generator.return(void 0);
|
|
777
|
+
throw error;
|
|
749
778
|
}
|
|
750
|
-
return operation.interrupt(generator.next());
|
|
751
779
|
},
|
|
752
780
|
return(value) {
|
|
753
781
|
return generator.return(value);
|
|
@@ -791,12 +819,14 @@ var HyperDBConnection = class {
|
|
|
791
819
|
return queryPromise(result, stream.info);
|
|
792
820
|
}
|
|
793
821
|
query(sql, options = {}) {
|
|
794
|
-
const stream = this.
|
|
822
|
+
const stream = this.queryStreamIPC(sql, options);
|
|
795
823
|
const result = (async () => {
|
|
796
|
-
const reader = await stream;
|
|
824
|
+
const reader = await RecordBatchReader.from(stream);
|
|
825
|
+
await reader.open();
|
|
826
|
+
const schema = reader.schema;
|
|
797
827
|
const batches = [];
|
|
798
828
|
for await (const batch of reader) batches.push(batch);
|
|
799
|
-
return new Table(
|
|
829
|
+
return schema ? new Table(schema, batches) : new Table();
|
|
800
830
|
})();
|
|
801
831
|
return queryPromise(result, stream.info);
|
|
802
832
|
}
|
|
@@ -821,6 +851,10 @@ var HyperDBConnection = class {
|
|
|
821
851
|
if (this.active) throw new Error("cannot terminate while a connection has an active operation");
|
|
822
852
|
await this.close();
|
|
823
853
|
}
|
|
854
|
+
releaseAfterShutdown() {
|
|
855
|
+
this.closed = true;
|
|
856
|
+
this.onClose();
|
|
857
|
+
}
|
|
824
858
|
};
|
|
825
859
|
|
|
826
860
|
// src/index.ts
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatabaseLockBackend } from './database_locks.js';
|
|
2
|
+
/** Paths must include the full OPFS path below /mnt/fs, including the configured storage directory */
|
|
3
|
+
export declare class BrowserDatabaseLockBackend implements DatabaseLockBackend {
|
|
4
|
+
private readonly locks;
|
|
5
|
+
private readonly requests;
|
|
6
|
+
private nextToken;
|
|
7
|
+
private closed;
|
|
8
|
+
acquire(path: string, shared: boolean): Promise<number>;
|
|
9
|
+
release(token: number): Promise<void>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
package/dist/browser_worker.js
CHANGED
|
@@ -145,20 +145,25 @@
|
|
|
145
145
|
const pointer = safeInteger(this.invoke(request), "result pointer");
|
|
146
146
|
const result = this.decodeResult(pointer);
|
|
147
147
|
if (request.type === "shutdown" && result.state === "ok") {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
const completion2 = (async () => {
|
|
149
|
+
await this.module.hyperdbWasmDatabaseLocksDrain?.();
|
|
150
|
+
await this.module.databaseLockBackend?.close();
|
|
151
|
+
await Promise.all([
|
|
152
|
+
this.module.externalFileBackend?.shutdown(),
|
|
153
|
+
this.module.externalDirectoryBackend?.shutdown()
|
|
154
|
+
]);
|
|
155
|
+
this.mountedPaths.clear();
|
|
156
|
+
this.mountedDirectoryPaths.clear();
|
|
157
|
+
})();
|
|
154
158
|
return {
|
|
155
159
|
response: { id: request.id, type: "response", result },
|
|
156
160
|
transfer: [],
|
|
157
|
-
|
|
161
|
+
completion: completion2
|
|
158
162
|
};
|
|
159
163
|
}
|
|
160
164
|
const response = { id: request.id, type: "response", result };
|
|
161
|
-
|
|
165
|
+
const completion = request.type === "disconnect" || request.type === "finishQuery" || request.type === "shutdown" ? this.module.hyperdbWasmDatabaseLocksDrain?.() : void 0;
|
|
166
|
+
return { response, transfer: result.state === "chunk" ? [result.payload.buffer] : [], ...completion && { completion } };
|
|
162
167
|
}
|
|
163
168
|
invoke(request) {
|
|
164
169
|
switch (request.type) {
|
|
@@ -378,6 +383,29 @@
|
|
|
378
383
|
}
|
|
379
384
|
}
|
|
380
385
|
|
|
386
|
+
// src/database_locks.ts
|
|
387
|
+
var DatabaseLockConflictError = class extends Error {
|
|
388
|
+
constructor(path, options) {
|
|
389
|
+
super(`Database lock is already held: ${path}`, options);
|
|
390
|
+
this.name = "DatabaseLockConflictError";
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
function normalizeDatabaseLockPath(path) {
|
|
394
|
+
if (!path.startsWith("/") || path.endsWith("/") || path.includes("\0") || path.includes("\\")) {
|
|
395
|
+
throw new Error("Database lock path must be an absolute file path below /mnt/fs");
|
|
396
|
+
}
|
|
397
|
+
const segments = [];
|
|
398
|
+
for (const segment of path.split("/")) {
|
|
399
|
+
if (!segment || segment === ".") continue;
|
|
400
|
+
if (segment === "..") segments.pop();
|
|
401
|
+
else segments.push(segment);
|
|
402
|
+
}
|
|
403
|
+
if (segments[0] !== "mnt" || segments[1] !== "fs" || segments.length < 3) {
|
|
404
|
+
throw new Error("Database lock path must be an absolute file path below /mnt/fs");
|
|
405
|
+
}
|
|
406
|
+
return `/${segments.join("/")}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
381
409
|
// src/worker_setup.ts
|
|
382
410
|
function errorText(error) {
|
|
383
411
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -455,11 +483,11 @@
|
|
|
455
483
|
try {
|
|
456
484
|
completeFetch(id, allocate(module, bytes), bytes.byteLength);
|
|
457
485
|
} catch (error) {
|
|
458
|
-
transport.postMessage({ type: "log", text: `fetch failed: ${errorText(error)}` });
|
|
486
|
+
transport.postMessage({ type: "log", stream: "stderr", text: `fetch failed: ${errorText(error)}` });
|
|
459
487
|
completeFetch(id, 0, -1);
|
|
460
488
|
}
|
|
461
489
|
}).catch(() => {
|
|
462
|
-
transport.postMessage({ type: "log", text: "fetch failed" });
|
|
490
|
+
transport.postMessage({ type: "log", stream: "stderr", text: "fetch failed" });
|
|
463
491
|
completeFetch(id, 0, -1);
|
|
464
492
|
});
|
|
465
493
|
};
|
|
@@ -470,7 +498,7 @@
|
|
|
470
498
|
if (!backend) return;
|
|
471
499
|
module.hyperdbWasmExternalDirectoryReleaseGeneration = (locator, generation) => {
|
|
472
500
|
void Promise.resolve().then(() => backend.releaseGeneration(locator, generation)).catch(() => {
|
|
473
|
-
transport.postMessage({ type: "log", text: "External directory generation cleanup failed; mount removal will retry" });
|
|
501
|
+
transport.postMessage({ type: "log", stream: "stderr", text: "External directory generation cleanup failed; mount removal will retry" });
|
|
474
502
|
});
|
|
475
503
|
};
|
|
476
504
|
const listComplete = module._hyperdb_wasm_external_directory_list_complete;
|
|
@@ -603,10 +631,50 @@
|
|
|
603
631
|
}).catch(() => complete(id, 0, -1));
|
|
604
632
|
};
|
|
605
633
|
}
|
|
634
|
+
function installDatabaseLockRuntime(module, fatal) {
|
|
635
|
+
const releases = /* @__PURE__ */ new Set();
|
|
636
|
+
const complete = (id, token, error) => {
|
|
637
|
+
if (!module._hyperdb_wasm_database_lock_complete) throw new Error("Database lock completion export is unavailable");
|
|
638
|
+
const status = error === void 0 ? 0 : error instanceof DatabaseLockConflictError ? 1 : 2;
|
|
639
|
+
const pointer = error === void 0 ? 0 : safePointer(module.stringToNewUTF8(errorText(error)), "database lock error pointer");
|
|
640
|
+
if (error !== void 0 && pointer === 0) throw new Error("Database lock error allocation failed");
|
|
641
|
+
module._hyperdb_wasm_database_lock_complete(id, token, status, pointer);
|
|
642
|
+
};
|
|
643
|
+
module.hyperdbWasmDatabaseLockAcquireStart = (id, path, shared) => {
|
|
644
|
+
void Promise.resolve().then(() => {
|
|
645
|
+
if (!module.databaseLockBackend) throw new Error("Host database locking is unavailable");
|
|
646
|
+
return module.databaseLockBackend.acquire(path, shared);
|
|
647
|
+
}).then((token) => complete(id, token), (error) => complete(id, 0, error)).catch((error) => fatal(`Database lock completion failed: ${errorText(error)}`));
|
|
648
|
+
};
|
|
649
|
+
module.hyperdbWasmDatabaseLockReleaseStart = (id, token) => {
|
|
650
|
+
const release = Promise.resolve().then(() => {
|
|
651
|
+
if (!module.databaseLockBackend) throw new Error("Host database locking is unavailable");
|
|
652
|
+
return module.databaseLockBackend.release(token);
|
|
653
|
+
}).then(() => {
|
|
654
|
+
if (id !== 0) complete(id, 0);
|
|
655
|
+
});
|
|
656
|
+
releases.add(release);
|
|
657
|
+
void release.then(() => releases.delete(release), (error) => fatal(`Database lock release failed: ${errorText(error)}`));
|
|
658
|
+
};
|
|
659
|
+
module.hyperdbWasmDatabaseLocksDrain = async () => {
|
|
660
|
+
while (releases.size) await Promise.all([...releases]);
|
|
661
|
+
};
|
|
662
|
+
}
|
|
606
663
|
function configureWorker(module, transport, configureFilesystem = () => void 0) {
|
|
607
664
|
const queuedRequests = [];
|
|
608
665
|
let bindings;
|
|
666
|
+
let failed = false;
|
|
667
|
+
const fatal = (error) => {
|
|
668
|
+
if (failed) return;
|
|
669
|
+
failed = true;
|
|
670
|
+
try {
|
|
671
|
+
module.PThread?.terminateAllThreads();
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
transport.postMessage({ type: "fatal", error });
|
|
675
|
+
};
|
|
609
676
|
const handle = (request) => {
|
|
677
|
+
if (failed) return;
|
|
610
678
|
if (!bindings) {
|
|
611
679
|
queuedRequests.push(request);
|
|
612
680
|
return;
|
|
@@ -614,37 +682,48 @@
|
|
|
614
682
|
try {
|
|
615
683
|
const result = bindings.handle(request);
|
|
616
684
|
const complete = () => {
|
|
685
|
+
if (failed) return;
|
|
617
686
|
if (request.type === "shutdown" && result.response.result.state === "ok") module.PThread.terminateAllThreads();
|
|
618
687
|
transport.postMessage(result.response, result.transfer);
|
|
619
688
|
};
|
|
620
|
-
if (result.completion) void result.completion.then(complete
|
|
621
|
-
|
|
689
|
+
if (result.completion) void result.completion.then(complete).catch((error) => {
|
|
690
|
+
if (failed) return;
|
|
691
|
+
if (request.type === "shutdown" || request.type === "disconnect" || request.type === "finishQuery") {
|
|
692
|
+
fatal(`engine cleanup failed: ${errorText(error)}`);
|
|
693
|
+
} else {
|
|
694
|
+
transport.postMessage({ id: request.id, type: "response", result: { state: "error", error: errorText(error) } });
|
|
695
|
+
}
|
|
622
696
|
});
|
|
623
697
|
else complete();
|
|
624
698
|
} catch (error) {
|
|
699
|
+
if (failed) return;
|
|
625
700
|
transport.postMessage({ id: request.id, type: "response", result: { state: "error", error: errorText(error) } });
|
|
626
701
|
}
|
|
627
702
|
};
|
|
628
703
|
transport.onMessage((message) => {
|
|
629
704
|
if (isEngineRequest(message)) handle(message);
|
|
630
705
|
});
|
|
631
|
-
module.print = (text) => transport.postMessage({ type: "log", text });
|
|
632
|
-
module.printErr = (text) => transport.postMessage({ type: "log", text });
|
|
706
|
+
module.print = (text) => transport.postMessage({ type: "log", stream: "stdout", text });
|
|
707
|
+
module.printErr = (text) => transport.postMessage({ type: "log", stream: "stderr", text });
|
|
633
708
|
module.onRuntimeInitialized = () => {
|
|
709
|
+
if (failed) return;
|
|
634
710
|
try {
|
|
635
711
|
configureFilesystem(module);
|
|
636
712
|
installHttpRuntime(module, transport);
|
|
637
713
|
installHostFileRuntime(module);
|
|
638
714
|
installExternalDirectoryRuntime(module, transport);
|
|
639
715
|
installFilesystemProjectionRuntime(module);
|
|
716
|
+
installDatabaseLockRuntime(module, fatal);
|
|
640
717
|
bindings = new HyperDBBindings(module);
|
|
641
718
|
transport.postMessage({ type: "ready" });
|
|
642
719
|
for (const request of queuedRequests.splice(0)) handle(request);
|
|
643
720
|
} catch (error) {
|
|
644
|
-
|
|
721
|
+
fatal(`engine initialization failed: ${errorText(error)}`);
|
|
645
722
|
}
|
|
646
723
|
};
|
|
647
|
-
module.onAbort = (reason) =>
|
|
724
|
+
module.onAbort = (reason) => {
|
|
725
|
+
fatal(`engine aborted: ${String(reason)}`);
|
|
726
|
+
};
|
|
648
727
|
}
|
|
649
728
|
|
|
650
729
|
// src/browser_external_directory.ts
|
|
@@ -960,6 +1039,64 @@
|
|
|
960
1039
|
}
|
|
961
1040
|
};
|
|
962
1041
|
|
|
1042
|
+
// src/browser_database_locks.ts
|
|
1043
|
+
var BrowserDatabaseLockBackend = class {
|
|
1044
|
+
locks = /* @__PURE__ */ new Map();
|
|
1045
|
+
requests = /* @__PURE__ */ new Set();
|
|
1046
|
+
nextToken = 1;
|
|
1047
|
+
closed = false;
|
|
1048
|
+
async acquire(path, shared) {
|
|
1049
|
+
if (this.closed) throw new Error("Database lock backend is closed");
|
|
1050
|
+
const normalized = normalizeDatabaseLockPath(path);
|
|
1051
|
+
const manager = globalThis.navigator?.locks;
|
|
1052
|
+
if (!manager || typeof manager.request !== "function") throw new Error("Database locking requires Web Locks");
|
|
1053
|
+
if (this.nextToken > Number.MAX_SAFE_INTEGER) throw new Error("Database lock tokens exhausted");
|
|
1054
|
+
const token = this.nextToken++;
|
|
1055
|
+
return new Promise((resolve, reject) => {
|
|
1056
|
+
let unlock;
|
|
1057
|
+
const held = new Promise((resolveHeld) => {
|
|
1058
|
+
unlock = resolveHeld;
|
|
1059
|
+
});
|
|
1060
|
+
const completion = manager.request(`hyperdb-wasm-database:${normalized}`, {
|
|
1061
|
+
mode: shared ? "shared" : "exclusive",
|
|
1062
|
+
ifAvailable: true
|
|
1063
|
+
}, async (lock) => {
|
|
1064
|
+
if (!lock) throw new DatabaseLockConflictError(normalized);
|
|
1065
|
+
if (this.closed) throw new Error("Database lock backend is closed");
|
|
1066
|
+
this.locks.set(token, { unlock, completion, releasing: false });
|
|
1067
|
+
resolve(token);
|
|
1068
|
+
await held;
|
|
1069
|
+
});
|
|
1070
|
+
this.requests.add(completion);
|
|
1071
|
+
void completion.then(() => {
|
|
1072
|
+
this.requests.delete(completion);
|
|
1073
|
+
}, (error) => {
|
|
1074
|
+
this.requests.delete(completion);
|
|
1075
|
+
reject(error);
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
async release(token) {
|
|
1080
|
+
const lock = this.locks.get(token);
|
|
1081
|
+
if (!lock || lock.releasing) throw new Error("Unknown database lock token");
|
|
1082
|
+
lock.releasing = true;
|
|
1083
|
+
lock.unlock();
|
|
1084
|
+
try {
|
|
1085
|
+
await lock.completion;
|
|
1086
|
+
} finally {
|
|
1087
|
+
this.locks.delete(token);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async close() {
|
|
1091
|
+
this.closed = true;
|
|
1092
|
+
const releases = [...this.locks].map(([token, lock]) => lock.releasing ? lock.completion : this.release(token));
|
|
1093
|
+
const results = await Promise.allSettled(releases);
|
|
1094
|
+
await Promise.allSettled([...this.requests]);
|
|
1095
|
+
const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1096
|
+
if (failures.length) throw new AggregateError(failures, "Database lock cleanup failed");
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
963
1100
|
// src/browser_worker.ts
|
|
964
1101
|
var configuredEngineUrl = new URL(self.location.href).searchParams.get("engineUrl");
|
|
965
1102
|
var engineUrl = self.HYPERDB_WASM_ENGINE_URL ?? configuredEngineUrl ?? "hyperdb-wasm.js";
|
|
@@ -1020,6 +1157,7 @@
|
|
|
1020
1157
|
};
|
|
1021
1158
|
self.Module = module;
|
|
1022
1159
|
configureWorker(module, transport, (runtime) => {
|
|
1160
|
+
runtime.databaseLockBackend = new BrowserDatabaseLockBackend();
|
|
1023
1161
|
runtime.externalDirectoryBackend = new BrowserExternalDirectoryBackend();
|
|
1024
1162
|
if (!runtime.OPFS) throw new Error("OPFS backend is unavailable");
|
|
1025
1163
|
runtime.FS.mkdirTree("/mnt/fs");
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ConnectionSettings, HyperDBSettings, MountAccessMode } from './api
|
|
|
2
2
|
import type { MountSource, SetupProgress, WasmResult, WorkerTransport } from './protocol.js';
|
|
3
3
|
export declare class HyperDBEngineClient {
|
|
4
4
|
private readonly onSetupProgress?;
|
|
5
|
+
private readonly onLog?;
|
|
5
6
|
private readonly transport;
|
|
6
7
|
private readonly pending;
|
|
7
8
|
private readonly readyPromise;
|
|
@@ -9,9 +10,11 @@ export declare class HyperDBEngineClient {
|
|
|
9
10
|
private rejectReady;
|
|
10
11
|
private nextId;
|
|
11
12
|
private failed;
|
|
13
|
+
private termination;
|
|
12
14
|
private readonly unsubscribe;
|
|
13
|
-
constructor(transport: WorkerTransport, onSetupProgress?: ((progress: SetupProgress) => void) | undefined);
|
|
15
|
+
constructor(transport: WorkerTransport, onSetupProgress?: ((progress: SetupProgress) => void) | undefined, onLog?: ((stream: "stdout" | "stderr", text: string) => void) | undefined);
|
|
14
16
|
ready(): Promise<void>;
|
|
17
|
+
get failure(): Error | undefined;
|
|
15
18
|
initialize(settings: HyperDBSettings): Promise<WasmResult>;
|
|
16
19
|
connect(settings?: ConnectionSettings): Promise<WasmResult>;
|
|
17
20
|
disconnect(connection: number): Promise<WasmResult>;
|
|
@@ -26,5 +29,6 @@ export declare class HyperDBEngineClient {
|
|
|
26
29
|
terminate(): Promise<void>;
|
|
27
30
|
private request;
|
|
28
31
|
private onMessage;
|
|
32
|
+
private failAndTerminate;
|
|
29
33
|
private fail;
|
|
30
34
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Host ownership interface; browser workers coordinate, while Node callers provide exclusive access */
|
|
2
|
+
export interface DatabaseLockBackend {
|
|
3
|
+
/** Returns a never-reused positive safe-integer token or rejects with DatabaseLockConflictError on contention */
|
|
4
|
+
acquire(path: string, shared: boolean): Promise<number>;
|
|
5
|
+
/** Releases a positive, backend-local token and waits for host ownership to end */
|
|
6
|
+
release(token: number): Promise<void>;
|
|
7
|
+
/** Stops acquisitions and releases all held locks; repeated closes are allowed */
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Identifies contention separately from host or filesystem failures */
|
|
11
|
+
export declare class DatabaseLockConflictError extends Error {
|
|
12
|
+
constructor(path: string, options?: ErrorOptions);
|
|
13
|
+
}
|
|
14
|
+
/** Normalizes an absolute durable file path */
|
|
15
|
+
export declare function normalizeDatabaseLockPath(path: string): string;
|