hyperdb-wasm 0.0.26792-dev.wasm.g72cc44c1 → 0.0.26862-dev.wasm.g42f1012b
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 +71 -9
- package/dist/browser.js +293 -80
- package/dist/browser_external_directory.d.ts +30 -0
- package/dist/browser_worker.js +541 -9
- package/dist/client.d.ts +7 -1
- package/dist/hyperdb-wasm.cjs +1 -1
- package/dist/hyperdb-wasm.js +1 -1
- package/dist/hyperdb-wasm.wasm.br +0 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.node.d.ts +6 -4
- package/dist/node.d.ts +1 -1
- package/dist/node.js +12389 -191
- package/dist/node_external_directory.d.ts +30 -0
- package/dist/node_worker.js +716 -20
- package/dist/protocol.d.ts +44 -1
- package/dist/raw.browser.js +176 -5
- package/dist/raw.d.ts +1 -1
- package/dist/raw.node.js +167 -6
- package/dist/wasm_bindings.d.ts +46 -0
- package/dist/worker_setup.d.ts +19 -1
- package/examples/browser-attached-databases.html +5 -5
- package/examples/browser.html +1 -1
- package/examples/node-attached-databases.mjs +8 -8
- package/examples/node.mjs +11 -9
- package/package.json +1 -1
package/dist/node_worker.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
// src/node_worker.ts
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
2
|
+
import { closeSync, createReadStream, fstatSync, openSync, read } from "node:fs";
|
|
3
|
+
import { opendir as opendir2, stat } from "node:fs/promises";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
|
-
import { dirname, isAbsolute, join, posix,
|
|
5
|
+
import { basename as basename2, dirname, isAbsolute as isAbsolute2, join as join2, posix, resolve as resolve2 } from "node:path";
|
|
6
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5
7
|
import { Readable } from "node:stream";
|
|
6
8
|
import { parentPort, workerData } from "node:worker_threads";
|
|
7
9
|
import { fileURLToPath } from "node:url";
|
|
8
10
|
import { createBrotliDecompress } from "node:zlib";
|
|
9
11
|
|
|
10
12
|
// src/wasm_bindings.ts
|
|
11
|
-
|
|
13
|
+
function completionFor(cleanups) {
|
|
14
|
+
const pending = cleanups.filter((cleanup) => cleanup !== void 0);
|
|
15
|
+
return pending.length === 0 ? void 0 : Promise.all(pending).then(() => void 0);
|
|
16
|
+
}
|
|
17
|
+
var resultStates = ["ok", "pending", "chunk", "done", "error", "busy", "structuredError"];
|
|
18
|
+
var structuredErrorMagic = new Uint8Array([72, 68, 66, 69]);
|
|
12
19
|
function safeInteger(value, name) {
|
|
13
20
|
const number = typeof value === "bigint" && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
|
14
21
|
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
@@ -18,6 +25,33 @@ function validateCString(value, name) {
|
|
|
18
25
|
if (!value) throw new Error(`${name} must not be empty`);
|
|
19
26
|
if (value.includes("\0")) throw new Error(`${name} must not contain NUL`);
|
|
20
27
|
}
|
|
28
|
+
function decodeStructuredError(payload) {
|
|
29
|
+
if (!structuredErrorMagic.every((byte, index) => payload[index] === byte)) throw new Error("structured error has invalid magic");
|
|
30
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
31
|
+
if (payload.byteLength < 8 || view.getUint32(4, true) !== 1) throw new Error("unsupported structured error version");
|
|
32
|
+
let offset = 8;
|
|
33
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
34
|
+
const readField = () => {
|
|
35
|
+
if (offset + 4 > payload.byteLength) throw new Error("truncated structured error field");
|
|
36
|
+
const length = view.getUint32(offset, true);
|
|
37
|
+
offset += 4;
|
|
38
|
+
if (length > payload.byteLength - offset) throw new Error("truncated structured error field");
|
|
39
|
+
const value = decoder.decode(payload.subarray(offset, offset + length));
|
|
40
|
+
offset += length;
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const sqlState = readField();
|
|
44
|
+
const errorSource = readField();
|
|
45
|
+
const message = readField();
|
|
46
|
+
const detail = readField();
|
|
47
|
+
const hint = readField();
|
|
48
|
+
if (offset !== payload.byteLength) throw new Error("structured error has trailing bytes");
|
|
49
|
+
if (!/^[0-9A-Z]{5}$/.test(sqlState)) throw new Error("structured error has invalid SQLSTATE");
|
|
50
|
+
if (errorSource !== "User" && errorSource !== "System" && errorSource !== "OverloadRejection") {
|
|
51
|
+
throw new Error("structured error has invalid source");
|
|
52
|
+
}
|
|
53
|
+
return { sqlState, errorSource, message, detail, hint };
|
|
54
|
+
}
|
|
21
55
|
var HyperDBBindings = class {
|
|
22
56
|
module;
|
|
23
57
|
initialize;
|
|
@@ -29,13 +63,18 @@ var HyperDBBindings = class {
|
|
|
29
63
|
checkpointDatabase;
|
|
30
64
|
dropDatabase;
|
|
31
65
|
shutdown;
|
|
66
|
+
registerExternalFile;
|
|
67
|
+
registerExternalDirectory;
|
|
68
|
+
removeExternalRegistration;
|
|
32
69
|
attachDatabase;
|
|
33
70
|
detachDatabase;
|
|
34
71
|
startQuery;
|
|
72
|
+
startQueryWithInfo;
|
|
35
73
|
insertArrowIPCFromPath;
|
|
36
74
|
pollQuery;
|
|
37
75
|
cancelQuery;
|
|
38
76
|
releaseQuery;
|
|
77
|
+
releaseQueryWithInfo;
|
|
39
78
|
resultState;
|
|
40
79
|
resultPayloadLength;
|
|
41
80
|
free;
|
|
@@ -51,13 +90,18 @@ var HyperDBBindings = class {
|
|
|
51
90
|
this.checkpointDatabase = module2.cwrap("hyperdb_wasm_checkpoint_database", "pointer", ["string"]);
|
|
52
91
|
this.dropDatabase = module2.cwrap("hyperdb_wasm_drop_database", "pointer", ["string"]);
|
|
53
92
|
this.shutdown = module2.cwrap("hyperdb_wasm_shutdown", "pointer", []);
|
|
93
|
+
this.registerExternalFile = module2.cwrap("hyperdb_wasm_register_external_file", "pointer", ["string", "string", "number"]);
|
|
94
|
+
this.registerExternalDirectory = module2.cwrap("hyperdb_wasm_register_external_directory", "pointer", ["string", "string", "number"]);
|
|
95
|
+
this.removeExternalRegistration = module2.cwrap("hyperdb_wasm_remove_external_registration", "pointer", ["string"]);
|
|
54
96
|
this.attachDatabase = module2.cwrap("hyperdb_wasm_attach_database", "pointer", ["number", "string", "string"]);
|
|
55
97
|
this.detachDatabase = module2.cwrap("hyperdb_wasm_detach_database", "pointer", ["number", "string"]);
|
|
56
|
-
this.startQuery = module2.cwrap("hyperdb_wasm_query_start", "pointer", ["number", "string"]);
|
|
98
|
+
this.startQuery = module2.cwrap("hyperdb_wasm_query_start", "pointer", ["number", "string", "number"]);
|
|
99
|
+
this.startQueryWithInfo = module2.cwrap("hyperdb_wasm_query_start_with_info", "pointer", ["number", "string"]);
|
|
57
100
|
this.insertArrowIPCFromPath = module2.cwrap("hyperdb_wasm_arrow_insert_from_path", "pointer", ["number", "string", "string", "string", "number"]);
|
|
58
101
|
this.pollQuery = module2.cwrap("hyperdb_wasm_query_poll", "pointer", ["number"]);
|
|
59
102
|
this.cancelQuery = module2.cwrap("hyperdb_wasm_query_cancel", "pointer", ["number"]);
|
|
60
103
|
this.releaseQuery = module2.cwrap("hyperdb_wasm_query_release", "pointer", ["number"]);
|
|
104
|
+
this.releaseQueryWithInfo = module2.cwrap("hyperdb_wasm_query_release_with_info", "pointer", ["number"]);
|
|
61
105
|
this.resultState = module2.cwrap("hyperdb_wasm_result_state", "number", ["number"]);
|
|
62
106
|
this.resultPayloadLength = module2.cwrap("hyperdb_wasm_result_payload_len", "number", ["number"]);
|
|
63
107
|
this.free = module2.cwrap("hyperdb_wasm_free", null, ["pointer"]);
|
|
@@ -73,8 +117,92 @@ var HyperDBBindings = class {
|
|
|
73
117
|
this.module.FS.unlink(request.path);
|
|
74
118
|
return { response: { id: request.id, type: "response", result: { state: "ok", payload: new Uint8Array() } }, transfer: [] };
|
|
75
119
|
}
|
|
120
|
+
if (request.type === "registerExternalFile" && "path" in request) {
|
|
121
|
+
if (!this.module.externalFileBackend) {
|
|
122
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "Host external files are supported only by the Node client" } }, transfer: [] };
|
|
123
|
+
}
|
|
124
|
+
const registration = this.module.externalFileBackend.register(request.path);
|
|
125
|
+
try {
|
|
126
|
+
const pointer2 = safeInteger(this.registerExternalFile(registration.name, registration.locator, registration.size), "result pointer");
|
|
127
|
+
const result2 = this.decodeResult(pointer2);
|
|
128
|
+
if (result2.state === "ok") {
|
|
129
|
+
const path = new TextDecoder("utf-8", { fatal: true }).decode(result2.payload);
|
|
130
|
+
try {
|
|
131
|
+
this.module.externalFileBackend.registered(path, registration.locator);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const cleanupPointer = safeInteger(this.removeExternalRegistration(path), "result pointer");
|
|
134
|
+
this.decodeResult(cleanupPointer);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
this.module.externalFileBackend.removed(registration.locator);
|
|
139
|
+
}
|
|
140
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.module.externalFileBackend.removed(registration.locator);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (request.type === "registerExternalFile" && !this.module.externalFileBackendAvailable) {
|
|
147
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "Blob external files are supported only by the browser client" } }, transfer: [] };
|
|
148
|
+
}
|
|
149
|
+
if (request.type === "registerExternalDirectory") {
|
|
150
|
+
const backend = this.module.externalDirectoryBackend;
|
|
151
|
+
if (!backend) {
|
|
152
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "External directories are not supported by this client" } }, transfer: [] };
|
|
153
|
+
}
|
|
154
|
+
const registration = backend.register("handle" in request ? request.handle : request.path, request.mode);
|
|
155
|
+
try {
|
|
156
|
+
const pointer2 = safeInteger(this.registerExternalDirectory(registration.name, registration.locator, Number(request.mode === "readwrite")), "result pointer");
|
|
157
|
+
const result2 = this.decodeResult(pointer2);
|
|
158
|
+
if (result2.state === "ok") {
|
|
159
|
+
const path = new TextDecoder("utf-8", { fatal: true }).decode(result2.payload);
|
|
160
|
+
try {
|
|
161
|
+
backend.registered(path, registration.locator);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const cleanupPointer = safeInteger(this.removeExternalRegistration(path), "result pointer");
|
|
164
|
+
this.decodeResult(cleanupPointer);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
const completion = completionFor([backend.removed(registration.locator)]);
|
|
169
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [], ...completion && { completion } };
|
|
170
|
+
}
|
|
171
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
void Promise.resolve(backend.removed(registration.locator)).catch(() => void 0);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (request.type === "removeExternalRegistration" && (this.module.externalFileBackend || this.module.externalDirectoryBackend)) {
|
|
178
|
+
validateCString(request.path, "external registration path");
|
|
179
|
+
const pointer2 = safeInteger(this.removeExternalRegistration(request.path), "result pointer");
|
|
180
|
+
const result2 = this.decodeResult(pointer2);
|
|
181
|
+
if (result2.state === "ok") {
|
|
182
|
+
const completion = completionFor([
|
|
183
|
+
this.module.externalFileBackend?.removed(request.path),
|
|
184
|
+
this.module.externalDirectoryBackend?.removed(request.path)
|
|
185
|
+
]);
|
|
186
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [], ...completion && { completion } };
|
|
187
|
+
}
|
|
188
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
|
|
189
|
+
}
|
|
190
|
+
if (request.type === "removeExternalRegistration" && !this.module.externalFileBackendAvailable) {
|
|
191
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "External files are not supported by this client" } }, transfer: [] };
|
|
192
|
+
}
|
|
76
193
|
const pointer = safeInteger(this.invoke(request), "result pointer");
|
|
77
194
|
const result = this.decodeResult(pointer);
|
|
195
|
+
if (request.type === "shutdown" && result.state === "ok") {
|
|
196
|
+
const completion = completionFor([
|
|
197
|
+
this.module.externalFileBackend?.shutdown(),
|
|
198
|
+
this.module.externalDirectoryBackend?.shutdown()
|
|
199
|
+
]);
|
|
200
|
+
return {
|
|
201
|
+
response: { id: request.id, type: "response", result },
|
|
202
|
+
transfer: [],
|
|
203
|
+
...completion && { completion }
|
|
204
|
+
};
|
|
205
|
+
}
|
|
78
206
|
const response = { id: request.id, type: "response", result };
|
|
79
207
|
return { response, transfer: result.state === "chunk" ? [result.payload.buffer] : [] };
|
|
80
208
|
}
|
|
@@ -110,8 +238,20 @@ var HyperDBBindings = class {
|
|
|
110
238
|
return this.detachDatabase(request.connection, request.alias);
|
|
111
239
|
case "shutdown":
|
|
112
240
|
return this.shutdown();
|
|
241
|
+
case "registerExternalFile":
|
|
242
|
+
if ("path" in request) throw new Error("host external file request has no backend");
|
|
243
|
+
validateCString(request.name, "external file name");
|
|
244
|
+
validateCString(request.url, "external file URL");
|
|
245
|
+
return this.registerExternalFile(request.name, request.url, safeInteger(request.size, "external file size"));
|
|
246
|
+
case "registerExternalDirectory":
|
|
247
|
+
throw new Error("external directory request has no backend");
|
|
248
|
+
case "removeExternalRegistration":
|
|
249
|
+
validateCString(request.path, "external registration path");
|
|
250
|
+
return this.removeExternalRegistration(request.path);
|
|
113
251
|
case "startQuery":
|
|
114
|
-
return this.startQuery(request.connection, request.sql);
|
|
252
|
+
return this.startQuery(request.connection, request.sql, Number(request.sqlTestMode ?? false));
|
|
253
|
+
case "startQueryWithInfo":
|
|
254
|
+
return this.startQueryWithInfo(request.connection, request.sql);
|
|
115
255
|
case "insertArrowIPCFromPath": {
|
|
116
256
|
const path = request.internal ? request.path : this.module.resolveHyperDBPath?.(request.path) ?? request.path;
|
|
117
257
|
return this.insertArrowIPCFromPath(request.connection, path, request.name, request.schema ?? "", Number(request.create));
|
|
@@ -122,6 +262,8 @@ var HyperDBBindings = class {
|
|
|
122
262
|
return this.cancelQuery(request.query);
|
|
123
263
|
case "releaseQuery":
|
|
124
264
|
return this.releaseQuery(request.query);
|
|
265
|
+
case "releaseQueryWithInfo":
|
|
266
|
+
return this.releaseQueryWithInfo(request.query);
|
|
125
267
|
}
|
|
126
268
|
}
|
|
127
269
|
decodeResult(pointer) {
|
|
@@ -151,6 +293,8 @@ var HyperDBBindings = class {
|
|
|
151
293
|
return { state, error: new TextDecoder().decode(payload) };
|
|
152
294
|
case "busy":
|
|
153
295
|
return { state, error: new TextDecoder().decode(payload) };
|
|
296
|
+
case "structuredError":
|
|
297
|
+
return { state: "error", error: decodeStructuredError(payload) };
|
|
154
298
|
}
|
|
155
299
|
} finally {
|
|
156
300
|
this.free(pointer);
|
|
@@ -190,6 +334,8 @@ function isEngineRequest(value) {
|
|
|
190
334
|
case "detachDatabase":
|
|
191
335
|
return hasKeys(value, ["id", "type", "connection", "alias"]) && Number.isSafeInteger(value.connection) && typeof value.alias === "string";
|
|
192
336
|
case "startQuery":
|
|
337
|
+
return (hasKeys(value, ["id", "type", "connection", "sql"]) || hasKeys(value, ["id", "type", "connection", "sql", "sqlTestMode"])) && Number.isSafeInteger(value.connection) && typeof value.sql === "string" && (value.sqlTestMode === void 0 || typeof value.sqlTestMode === "boolean");
|
|
338
|
+
case "startQueryWithInfo":
|
|
193
339
|
return hasKeys(value, ["id", "type", "connection", "sql"]) && Number.isSafeInteger(value.connection) && typeof value.sql === "string";
|
|
194
340
|
case "insertArrowIPCFromPath":
|
|
195
341
|
return hasKeys(value, ["id", "type", "connection", "path", "name", "schema", "create", "internal"]) && Number.isSafeInteger(value.connection) && typeof value.path === "string" && typeof value.name === "string" && (typeof value.schema === "string" || value.schema === null) && typeof value.create === "boolean" && typeof value.internal === "boolean";
|
|
@@ -197,9 +343,16 @@ function isEngineRequest(value) {
|
|
|
197
343
|
return hasKeys(value, ["id", "type", "bytes"]) && value.bytes instanceof Uint8Array;
|
|
198
344
|
case "removeFile":
|
|
199
345
|
return hasKeys(value, ["id", "type", "path"]) && typeof value.path === "string";
|
|
346
|
+
case "registerExternalFile":
|
|
347
|
+
return hasKeys(value, ["id", "type", "name", "url", "size"]) && typeof value.name === "string" && typeof value.url === "string" && typeof value.size === "number" && Number.isSafeInteger(value.size) && value.size >= 0 || hasKeys(value, ["id", "type", "path"]) && typeof value.path === "string";
|
|
348
|
+
case "registerExternalDirectory":
|
|
349
|
+
return (value.mode === "read" || value.mode === "readwrite") && (hasKeys(value, ["id", "type", "handle", "mode"]) && isRecord(value.handle) && value.handle.kind === "directory" || hasKeys(value, ["id", "type", "path", "mode"]) && typeof value.path === "string" && !!value.path && !value.path.includes("\0"));
|
|
350
|
+
case "removeExternalRegistration":
|
|
351
|
+
return hasKeys(value, ["id", "type", "path"]) && typeof value.path === "string";
|
|
200
352
|
case "pollQuery":
|
|
201
353
|
case "cancelQuery":
|
|
202
354
|
case "releaseQuery":
|
|
355
|
+
case "releaseQueryWithInfo":
|
|
203
356
|
return hasKeys(value, ["id", "type", "query"]) && Number.isSafeInteger(value.query);
|
|
204
357
|
default:
|
|
205
358
|
return false;
|
|
@@ -210,6 +363,10 @@ function isEngineRequest(value) {
|
|
|
210
363
|
function errorText(error) {
|
|
211
364
|
return error instanceof Error ? error.message : String(error);
|
|
212
365
|
}
|
|
366
|
+
function isNotFoundError(error) {
|
|
367
|
+
if (typeof error !== "object" || error === null) return false;
|
|
368
|
+
return "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR") || "name" in error && error.name === "NotFoundError";
|
|
369
|
+
}
|
|
213
370
|
function safePointer(value, name) {
|
|
214
371
|
const pointer = typeof value === "bigint" && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
|
215
372
|
if (typeof pointer !== "number" || !Number.isSafeInteger(pointer) || pointer < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
@@ -218,22 +375,48 @@ function safePointer(value, name) {
|
|
|
218
375
|
function allocate(module2, bytes) {
|
|
219
376
|
const pointer = safePointer(module2._malloc(bytes.byteLength || 1), "allocation pointer");
|
|
220
377
|
if (pointer === 0) throw new Error("wasm allocation failed");
|
|
221
|
-
|
|
222
|
-
module2.HEAPU8
|
|
378
|
+
module2.getValue(pointer, "i8");
|
|
379
|
+
const heap = module2.HEAPU8;
|
|
380
|
+
if (pointer + bytes.byteLength > heap.byteLength) throw new Error("allocation exceeds the wasm heap");
|
|
381
|
+
heap.set(bytes, pointer);
|
|
223
382
|
return pointer;
|
|
224
383
|
}
|
|
384
|
+
function externalDirectoryError(module2, error) {
|
|
385
|
+
return safePointer(module2.stringToNewUTF8(errorText(error)), "external directory error pointer");
|
|
386
|
+
}
|
|
387
|
+
function completeExternalDirectoryOperation(module2, id, complete, operation) {
|
|
388
|
+
void Promise.resolve().then(operation).then(
|
|
389
|
+
() => complete(id, 0),
|
|
390
|
+
(error) => complete(id, externalDirectoryError(module2, error))
|
|
391
|
+
);
|
|
392
|
+
}
|
|
225
393
|
function installHttpRuntime(module2, transport2) {
|
|
226
|
-
module2.hyperdbWasmFetchStart = (id, url, offset, length) => {
|
|
394
|
+
module2.hyperdbWasmFetchStart = (id, url, offset, length, expectedSize) => {
|
|
395
|
+
if (url.startsWith("hyperdb-node-file:") || url.startsWith("hyperdb-node-host-file:") || url.startsWith("hyperdb-node-directory-file:") || url.startsWith("hyperdb-browser-file:")) {
|
|
396
|
+
const read2 = module2.externalFileBackend?.read(url, offset, length, expectedSize) ?? module2.externalDirectoryBackend?.read(url, offset, length, expectedSize);
|
|
397
|
+
if (!read2) {
|
|
398
|
+
module2._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
void read2.then((bytes) => {
|
|
402
|
+
module2._hyperdb_wasm_fetch_complete(id, allocate(module2, bytes), bytes.byteLength);
|
|
403
|
+
}).catch(() => {
|
|
404
|
+
module2._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
227
408
|
const headers = new Headers();
|
|
228
409
|
if (length > 0) headers.set("Range", `bytes=${offset}-${offset + length - 1}`);
|
|
229
410
|
void fetch(url, { headers, redirect: "manual" }).then(async (response) => {
|
|
230
411
|
if (!response.ok || response.type === "opaqueredirect" || (length === 0 ? response.status !== 200 : response.status !== 206)) {
|
|
412
|
+
void response.body?.cancel().catch(() => void 0);
|
|
231
413
|
module2._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
232
414
|
return;
|
|
233
415
|
}
|
|
234
416
|
if (length > 0) {
|
|
235
417
|
const contentRange = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(response.headers.get("content-range") ?? "");
|
|
236
|
-
if (!contentRange || contentRange[1] !== String(offset) || contentRange[2] !== String(offset + length - 1)) {
|
|
418
|
+
if (!contentRange || contentRange[1] !== String(offset) || contentRange[2] !== String(offset + length - 1) || expectedSize >= 0 && contentRange[3] !== String(expectedSize)) {
|
|
419
|
+
void response.body?.cancel().catch(() => void 0);
|
|
237
420
|
module2._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
238
421
|
return;
|
|
239
422
|
}
|
|
@@ -282,6 +465,97 @@ function installHttpRuntime(module2, transport2) {
|
|
|
282
465
|
});
|
|
283
466
|
};
|
|
284
467
|
}
|
|
468
|
+
function installExternalDirectoryRuntime(module2) {
|
|
469
|
+
const backend = module2.externalDirectoryBackend;
|
|
470
|
+
if (!backend) return;
|
|
471
|
+
const listComplete = module2._hyperdb_wasm_external_directory_list_complete;
|
|
472
|
+
const openComplete = module2._hyperdb_wasm_external_directory_open_complete;
|
|
473
|
+
if (listComplete) {
|
|
474
|
+
module2.hyperdbWasmExternalDirectoryListStart = (id, locator) => {
|
|
475
|
+
void backend.list(locator).then((paths) => {
|
|
476
|
+
listComplete(id, safePointer(module2.stringToNewUTF8(JSON.stringify(paths)), "external directory list pointer"), 0);
|
|
477
|
+
}).catch((error) => {
|
|
478
|
+
listComplete(id, 0, externalDirectoryError(module2, error));
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (openComplete) {
|
|
483
|
+
module2.hyperdbWasmExternalDirectoryOpenStart = (id, locator, path) => {
|
|
484
|
+
void backend.open(locator, path).then((file) => {
|
|
485
|
+
openComplete(id, safePointer(module2.stringToNewUTF8(file.locator), "external file locator pointer"), file.size);
|
|
486
|
+
}).catch((error) => openComplete(id, 0, isNotFoundError(error) ? -2 : -1));
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const createDirectoriesComplete = module2._hyperdb_wasm_external_directory_create_directories_complete;
|
|
490
|
+
if (createDirectoriesComplete) {
|
|
491
|
+
module2.hyperdbWasmExternalDirectoryCreateDirectoriesStart = (id, locator, path) => {
|
|
492
|
+
completeExternalDirectoryOperation(module2, id, createDirectoriesComplete, () => backend.createDirectories(locator, path));
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const writerWriteComplete = module2._hyperdb_wasm_external_directory_writer_write_complete;
|
|
496
|
+
if (writerWriteComplete) {
|
|
497
|
+
module2.hyperdbWasmExternalDirectoryWriterWriteStart = (writer, id, locator, path, offset, pointerValue, length) => {
|
|
498
|
+
try {
|
|
499
|
+
const pointer = safePointer(pointerValue, "external directory write pointer");
|
|
500
|
+
const safeLength = safePointer(length, "external directory write length");
|
|
501
|
+
const end = safePointer(pointer + safeLength, "external directory write end");
|
|
502
|
+
module2.getValue(pointer, "i8");
|
|
503
|
+
if (end > module2.HEAPU8.byteLength) throw new Error("external directory write exceeds the wasm heap");
|
|
504
|
+
const bytes = module2.HEAPU8.slice(pointer, end);
|
|
505
|
+
completeExternalDirectoryOperation(module2, id, writerWriteComplete, async () => {
|
|
506
|
+
await backend.openWriter(writer, locator, path);
|
|
507
|
+
await backend.write(writer, offset, bytes);
|
|
508
|
+
});
|
|
509
|
+
} catch (error) {
|
|
510
|
+
writerWriteComplete(id, externalDirectoryError(module2, error));
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const writerFinishComplete = module2._hyperdb_wasm_external_directory_writer_finish_complete;
|
|
515
|
+
if (writerFinishComplete) {
|
|
516
|
+
module2.hyperdbWasmExternalDirectoryWriterFinishStart = (writer, id, locator, path, size) => {
|
|
517
|
+
completeExternalDirectoryOperation(module2, id, writerFinishComplete, async () => {
|
|
518
|
+
await backend.openWriter(writer, locator, path);
|
|
519
|
+
await backend.finish(writer, size);
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const writerAbortComplete = module2._hyperdb_wasm_external_directory_writer_abort_complete;
|
|
524
|
+
if (writerAbortComplete) {
|
|
525
|
+
module2.hyperdbWasmExternalDirectoryWriterAbortStart = (writer, id) => {
|
|
526
|
+
completeExternalDirectoryOperation(module2, id, writerAbortComplete, () => backend.abort(writer));
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function installHostFileRuntime(module2) {
|
|
531
|
+
const complete = module2._hyperdb_wasm_host_file_open_complete;
|
|
532
|
+
if (!complete) return;
|
|
533
|
+
module2.hyperdbWasmHostFileOpenStart = (id, path) => {
|
|
534
|
+
try {
|
|
535
|
+
const file = module2.externalFileBackend?.openHostFile(path);
|
|
536
|
+
if (!file) throw new Error("Node host-file backend is unavailable");
|
|
537
|
+
complete(id, file.id, file.size);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
complete(id, 0, isNotFoundError(error) ? -2 : -1);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
module2.hyperdbWasmHostFileClose = (id) => module2.externalFileBackend?.closeHostFile(id);
|
|
543
|
+
const listComplete = module2._hyperdb_wasm_external_directory_list_complete;
|
|
544
|
+
if (listComplete) {
|
|
545
|
+
module2.hyperdbWasmHostDirectoryListStart = (id, path) => {
|
|
546
|
+
const list = module2.externalFileBackend?.listHostDirectory(path);
|
|
547
|
+
if (!list) {
|
|
548
|
+
listComplete(id, 0, externalDirectoryError(module2, new Error("Node host-directory backend is unavailable")));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
void list.then((paths) => {
|
|
552
|
+
listComplete(id, safePointer(module2.stringToNewUTF8(JSON.stringify(paths)), "host directory list pointer"), 0);
|
|
553
|
+
}).catch((error) => {
|
|
554
|
+
listComplete(id, 0, externalDirectoryError(module2, error));
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
285
559
|
function configureWorker(module2, transport2, configureFilesystem = () => void 0) {
|
|
286
560
|
const queuedRequests = [];
|
|
287
561
|
let bindings;
|
|
@@ -292,8 +566,14 @@ function configureWorker(module2, transport2, configureFilesystem = () => void 0
|
|
|
292
566
|
}
|
|
293
567
|
try {
|
|
294
568
|
const result = bindings.handle(request);
|
|
295
|
-
|
|
296
|
-
|
|
569
|
+
const complete = () => {
|
|
570
|
+
if (request.type === "shutdown" && result.response.result.state === "ok") module2.PThread.terminateAllThreads();
|
|
571
|
+
transport2.postMessage(result.response, result.transfer);
|
|
572
|
+
};
|
|
573
|
+
if (result.completion) void result.completion.then(complete, (error) => {
|
|
574
|
+
transport2.postMessage({ id: request.id, type: "response", result: { state: "error", error: errorText(error) } });
|
|
575
|
+
});
|
|
576
|
+
else complete();
|
|
297
577
|
} catch (error) {
|
|
298
578
|
transport2.postMessage({ id: request.id, type: "response", result: { state: "error", error: errorText(error) } });
|
|
299
579
|
}
|
|
@@ -307,6 +587,8 @@ function configureWorker(module2, transport2, configureFilesystem = () => void 0
|
|
|
307
587
|
try {
|
|
308
588
|
configureFilesystem(module2);
|
|
309
589
|
installHttpRuntime(module2, transport2);
|
|
590
|
+
installHostFileRuntime(module2);
|
|
591
|
+
installExternalDirectoryRuntime(module2);
|
|
310
592
|
bindings = new HyperDBBindings(module2);
|
|
311
593
|
transport2.postMessage({ type: "ready" });
|
|
312
594
|
for (const request of queuedRequests.splice(0)) handle(request);
|
|
@@ -317,17 +599,323 @@ function configureWorker(module2, transport2, configureFilesystem = () => void 0
|
|
|
317
599
|
module2.onAbort = (reason) => transport2.postMessage({ type: "fatal", error: `engine aborted: ${String(reason)}` });
|
|
318
600
|
}
|
|
319
601
|
|
|
602
|
+
// src/node_external_directory.ts
|
|
603
|
+
import { constants, lstatSync, realpathSync } from "node:fs";
|
|
604
|
+
import { lstat, mkdir, open, opendir, rename, unlink } from "node:fs/promises";
|
|
605
|
+
import { randomUUID } from "node:crypto";
|
|
606
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
607
|
+
var directoryLocatorPrefix = "hyperdb-node-directory:";
|
|
608
|
+
var fileLocatorPrefix = "hyperdb-node-directory-file:";
|
|
609
|
+
function validateSegment(segment) {
|
|
610
|
+
if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\") || segment.includes("\0")) {
|
|
611
|
+
throw new Error("External directory entries must have safe relative path segments");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function relativeSegments(path) {
|
|
615
|
+
if (!path || isAbsolute(path) || path.endsWith("/") || path.includes("\\") || path.includes("\0")) {
|
|
616
|
+
throw new Error("External directory file path must be relative");
|
|
617
|
+
}
|
|
618
|
+
const segments = path.split("/");
|
|
619
|
+
for (const segment of segments) validateSegment(segment);
|
|
620
|
+
return segments;
|
|
621
|
+
}
|
|
622
|
+
function directorySegments(path) {
|
|
623
|
+
return path ? relativeSegments(path) : [];
|
|
624
|
+
}
|
|
625
|
+
function nonnegativeSafeInteger(value, name) {
|
|
626
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
627
|
+
}
|
|
628
|
+
function isNotFound(error) {
|
|
629
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
630
|
+
}
|
|
631
|
+
function isAlreadyExists(error) {
|
|
632
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
633
|
+
}
|
|
634
|
+
function confinedPath(root, segments) {
|
|
635
|
+
const path = join(root, ...segments);
|
|
636
|
+
const fromRoot = relative(root, path);
|
|
637
|
+
if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
638
|
+
throw new Error("External directory path escapes its registration");
|
|
639
|
+
}
|
|
640
|
+
return path;
|
|
641
|
+
}
|
|
642
|
+
async function ensureDirectory(root, segments, create) {
|
|
643
|
+
let current = root;
|
|
644
|
+
for (const segment of segments) {
|
|
645
|
+
current = join(current, segment);
|
|
646
|
+
const fromRoot = relative(root, current);
|
|
647
|
+
if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
648
|
+
throw new Error("External directory path escapes its registration");
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const info = await lstat(current);
|
|
652
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("External directory path contains a non-directory entry");
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (!create || !isNotFound(error)) throw error;
|
|
655
|
+
await mkdir(current).catch((mkdirError) => {
|
|
656
|
+
if (!isAlreadyExists(mkdirError)) throw mkdirError;
|
|
657
|
+
});
|
|
658
|
+
const info = await lstat(current);
|
|
659
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("External directory path contains a non-directory entry");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return current;
|
|
663
|
+
}
|
|
664
|
+
var NodeExternalDirectoryBackend = class {
|
|
665
|
+
directories = /* @__PURE__ */ new Map();
|
|
666
|
+
paths = /* @__PURE__ */ new Map();
|
|
667
|
+
files = /* @__PURE__ */ new Map();
|
|
668
|
+
writers = /* @__PURE__ */ new Map();
|
|
669
|
+
register(path, mode) {
|
|
670
|
+
if (typeof path !== "string" || !path || path.includes("\0")) throw new Error("External directory path is invalid");
|
|
671
|
+
const root = realpathSync(resolve(path));
|
|
672
|
+
const info = lstatSync(root);
|
|
673
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("External directory path must identify a directory");
|
|
674
|
+
const locator = `${directoryLocatorPrefix}${randomUUID()}`;
|
|
675
|
+
this.directories.set(locator, { root, mode, files: /* @__PURE__ */ new Map(), locators: /* @__PURE__ */ new Set(), temporaryPaths: /* @__PURE__ */ new Set() });
|
|
676
|
+
return { name: basename(root) || "root", locator };
|
|
677
|
+
}
|
|
678
|
+
registered(path, locator) {
|
|
679
|
+
if (!this.directories.has(locator)) throw new Error("External directory registration is unavailable");
|
|
680
|
+
this.paths.set(path, locator);
|
|
681
|
+
}
|
|
682
|
+
async removed(pathOrLocator) {
|
|
683
|
+
const locator = this.paths.get(pathOrLocator) ?? pathOrLocator;
|
|
684
|
+
const directory = this.directories.get(locator);
|
|
685
|
+
if (!directory) return;
|
|
686
|
+
const writerCleanup = [];
|
|
687
|
+
for (const [writer, state] of this.writers) {
|
|
688
|
+
if (state.locator !== locator) continue;
|
|
689
|
+
if (!state.terminal) writerCleanup.push(this.abort(writer));
|
|
690
|
+
else writerCleanup.push(state.chain.then(() => this.cleanupWriter(state)).finally(() => this.writers.delete(writer)));
|
|
691
|
+
}
|
|
692
|
+
await Promise.all(writerCleanup);
|
|
693
|
+
const fileCleanup = [];
|
|
694
|
+
for (const fileLocator of directory.locators) {
|
|
695
|
+
const file = this.files.get(fileLocator);
|
|
696
|
+
if (file) fileCleanup.push(file.descriptor.close());
|
|
697
|
+
this.files.delete(fileLocator);
|
|
698
|
+
}
|
|
699
|
+
await Promise.all(fileCleanup);
|
|
700
|
+
this.directories.delete(locator);
|
|
701
|
+
for (const [path, pathLocator] of this.paths) {
|
|
702
|
+
if (pathLocator === locator) this.paths.delete(path);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async list(locator) {
|
|
706
|
+
const directory = this.getDirectory(locator);
|
|
707
|
+
const files = [];
|
|
708
|
+
const visit = async (path, prefix) => {
|
|
709
|
+
const entries = await opendir(path);
|
|
710
|
+
for await (const entry of entries) {
|
|
711
|
+
validateSegment(entry.name);
|
|
712
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
713
|
+
const child = confinedPath(directory.root, relativeSegments(relativePath));
|
|
714
|
+
if (directory.temporaryPaths.has(child)) continue;
|
|
715
|
+
const info = await lstat(child);
|
|
716
|
+
if (info.isSymbolicLink()) throw new Error("External directory must not contain symbolic links");
|
|
717
|
+
if (info.isDirectory()) await visit(child, relativePath);
|
|
718
|
+
else if (info.isFile()) files.push(relativePath);
|
|
719
|
+
else throw new Error("External directory contains an unsupported entry");
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
await visit(directory.root, "");
|
|
723
|
+
files.sort();
|
|
724
|
+
return files;
|
|
725
|
+
}
|
|
726
|
+
async open(locator, path) {
|
|
727
|
+
const directory = this.getDirectory(locator);
|
|
728
|
+
const cached = directory.files.get(path);
|
|
729
|
+
if (cached) {
|
|
730
|
+
const file = await cached;
|
|
731
|
+
return { locator: file.locator, size: file.size };
|
|
732
|
+
}
|
|
733
|
+
const segments = relativeSegments(path);
|
|
734
|
+
const snapshot = (async () => {
|
|
735
|
+
await ensureDirectory(directory.root, segments.slice(0, -1), false);
|
|
736
|
+
const target = confinedPath(directory.root, segments);
|
|
737
|
+
const info = await lstat(target);
|
|
738
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("External directory entry is not a regular file");
|
|
739
|
+
const descriptor = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
740
|
+
try {
|
|
741
|
+
const openedInfo = await descriptor.stat({ bigint: true });
|
|
742
|
+
if (!openedInfo.isFile()) throw new Error("External directory entry is not a regular file");
|
|
743
|
+
if (openedInfo.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("External file size must be a nonnegative safe integer");
|
|
744
|
+
const file = { locator: `${fileLocatorPrefix}${randomUUID()}`, descriptor, size: Number(openedInfo.size) };
|
|
745
|
+
this.files.set(file.locator, file);
|
|
746
|
+
directory.locators.add(file.locator);
|
|
747
|
+
return file;
|
|
748
|
+
} catch (error) {
|
|
749
|
+
await descriptor.close();
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
})();
|
|
753
|
+
directory.files.set(path, snapshot);
|
|
754
|
+
try {
|
|
755
|
+
const file = await snapshot;
|
|
756
|
+
return { locator: file.locator, size: file.size };
|
|
757
|
+
} catch (error) {
|
|
758
|
+
directory.files.delete(path);
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
read(locator, offset, length, expectedSize) {
|
|
763
|
+
if (!locator.startsWith(fileLocatorPrefix)) return void 0;
|
|
764
|
+
const file = this.files.get(locator);
|
|
765
|
+
if (!file) return Promise.reject(new Error("External directory file registration does not exist"));
|
|
766
|
+
const end = offset + length;
|
|
767
|
+
if (expectedSize !== file.size || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || !Number.isSafeInteger(end) || end > file.size) {
|
|
768
|
+
return Promise.reject(new Error("External file byte range is invalid"));
|
|
769
|
+
}
|
|
770
|
+
return (async () => {
|
|
771
|
+
const result = new Uint8Array(length);
|
|
772
|
+
let bytesRead = 0;
|
|
773
|
+
while (bytesRead < length) {
|
|
774
|
+
const readResult = await file.descriptor.read(result, bytesRead, length - bytesRead, offset + bytesRead);
|
|
775
|
+
if (readResult.bytesRead === 0) throw new Error("External file ended before the registered size");
|
|
776
|
+
bytesRead += readResult.bytesRead;
|
|
777
|
+
}
|
|
778
|
+
return result;
|
|
779
|
+
})();
|
|
780
|
+
}
|
|
781
|
+
async createDirectories(locator, path) {
|
|
782
|
+
const directory = this.getWritableDirectory(locator);
|
|
783
|
+
await ensureDirectory(directory.root, directorySegments(path), true);
|
|
784
|
+
}
|
|
785
|
+
openWriter(writer, locator, path) {
|
|
786
|
+
nonnegativeSafeInteger(writer, "External directory writer");
|
|
787
|
+
const directory = this.getWritableDirectory(locator);
|
|
788
|
+
const segments = relativeSegments(path);
|
|
789
|
+
const target = confinedPath(directory.root, segments);
|
|
790
|
+
const existing = this.writers.get(writer);
|
|
791
|
+
if (existing) {
|
|
792
|
+
if (existing.locator !== locator || existing.path !== path) throw new Error("External directory writer was reused for another path");
|
|
793
|
+
return existing.chain;
|
|
794
|
+
}
|
|
795
|
+
const state = { locator, path, target, temp: void 0, descriptor: void 0, chain: Promise.resolve(), terminal: false };
|
|
796
|
+
this.writers.set(writer, state);
|
|
797
|
+
return this.enqueue(state, async () => {
|
|
798
|
+
const parent = await ensureDirectory(directory.root, segments.slice(0, -1), true);
|
|
799
|
+
try {
|
|
800
|
+
const info = await lstat(target);
|
|
801
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error("External directory write target is not a regular file");
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (!isNotFound(error)) throw error;
|
|
804
|
+
}
|
|
805
|
+
state.temp = join(parent, `.hyperdb-${randomUUID()}.tmp`);
|
|
806
|
+
directory.temporaryPaths.add(state.temp);
|
|
807
|
+
try {
|
|
808
|
+
state.descriptor = await open(state.temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
state.temp = void 0;
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
write(writer, offset, bytes) {
|
|
816
|
+
nonnegativeSafeInteger(offset, "External directory write offset");
|
|
817
|
+
nonnegativeSafeInteger(offset + bytes.byteLength, "External directory write end");
|
|
818
|
+
const state = this.getOpenWriter(writer);
|
|
819
|
+
return this.enqueue(state, async () => {
|
|
820
|
+
if (!state.descriptor) throw new Error("External directory writer failed to open");
|
|
821
|
+
let written = 0;
|
|
822
|
+
while (written < bytes.byteLength) {
|
|
823
|
+
const result = await state.descriptor.write(bytes, written, bytes.byteLength - written, offset + written);
|
|
824
|
+
if (result.bytesWritten === 0) throw new Error("External directory write made no progress");
|
|
825
|
+
written += result.bytesWritten;
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
finish(writer, size) {
|
|
830
|
+
nonnegativeSafeInteger(size, "External directory file size");
|
|
831
|
+
const state = this.getOpenWriter(writer);
|
|
832
|
+
state.terminal = true;
|
|
833
|
+
const operation = this.enqueue(state, async () => {
|
|
834
|
+
let failure = state.failure;
|
|
835
|
+
if (failure === void 0 && (!state.descriptor || !state.temp)) failure = new Error("External directory writer failed to open");
|
|
836
|
+
if (failure === void 0) {
|
|
837
|
+
try {
|
|
838
|
+
await state.descriptor?.truncate(size);
|
|
839
|
+
await state.descriptor?.close();
|
|
840
|
+
state.descriptor = void 0;
|
|
841
|
+
await rename(state.temp, state.target);
|
|
842
|
+
state.temp = void 0;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
failure = error;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (failure !== void 0) {
|
|
848
|
+
await this.cleanupWriter(state);
|
|
849
|
+
throw failure;
|
|
850
|
+
}
|
|
851
|
+
this.getDirectory(state.locator).files.delete(state.path);
|
|
852
|
+
}, true);
|
|
853
|
+
return operation.finally(() => this.writers.delete(writer));
|
|
854
|
+
}
|
|
855
|
+
abort(writer) {
|
|
856
|
+
const state = this.getOpenWriter(writer);
|
|
857
|
+
state.terminal = true;
|
|
858
|
+
const operation = this.enqueue(state, () => this.cleanupWriter(state), true);
|
|
859
|
+
return operation.finally(() => this.writers.delete(writer));
|
|
860
|
+
}
|
|
861
|
+
async shutdown() {
|
|
862
|
+
await Promise.all([...this.directories.keys()].map((locator) => this.removed(locator)));
|
|
863
|
+
await Promise.all([...this.files.values()].map((file) => file.descriptor.close()));
|
|
864
|
+
this.files.clear();
|
|
865
|
+
this.paths.clear();
|
|
866
|
+
this.directories.clear();
|
|
867
|
+
}
|
|
868
|
+
getDirectory(locator) {
|
|
869
|
+
const directory = this.directories.get(locator);
|
|
870
|
+
if (!directory) throw new Error("External directory registration does not exist");
|
|
871
|
+
return directory;
|
|
872
|
+
}
|
|
873
|
+
getWritableDirectory(locator) {
|
|
874
|
+
const directory = this.getDirectory(locator);
|
|
875
|
+
if (directory.mode !== "readwrite") throw new Error("External directory registration is not writable");
|
|
876
|
+
return directory;
|
|
877
|
+
}
|
|
878
|
+
getOpenWriter(writer) {
|
|
879
|
+
nonnegativeSafeInteger(writer, "External directory writer");
|
|
880
|
+
const state = this.writers.get(writer);
|
|
881
|
+
if (!state || state.terminal) throw new Error("External directory writer is not open");
|
|
882
|
+
return state;
|
|
883
|
+
}
|
|
884
|
+
async cleanupWriter(state) {
|
|
885
|
+
this.directories.get(state.locator)?.files.delete(state.path);
|
|
886
|
+
try {
|
|
887
|
+
await state.descriptor?.close();
|
|
888
|
+
} finally {
|
|
889
|
+
state.descriptor = void 0;
|
|
890
|
+
if (state.temp) {
|
|
891
|
+
await unlink(state.temp).catch(() => void 0);
|
|
892
|
+
}
|
|
893
|
+
state.temp = void 0;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
enqueue(state, operation, afterFailure = false) {
|
|
897
|
+
const result = state.chain.then(async () => {
|
|
898
|
+
if (!afterFailure && state.failure !== void 0) throw state.failure;
|
|
899
|
+
await operation();
|
|
900
|
+
});
|
|
901
|
+
state.chain = result.catch((error) => {
|
|
902
|
+
state.failure ??= error;
|
|
903
|
+
});
|
|
904
|
+
return result;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
320
908
|
// src/node_worker.ts
|
|
321
909
|
if (!parentPort) throw new Error("Node engine worker requires a parent port");
|
|
322
910
|
var enginePort = parentPort;
|
|
323
911
|
var options = workerData;
|
|
324
912
|
var engineUrl = options?.engineUrl ?? fileURLToPath(new URL("./hyperdb-wasm.cjs", import.meta.url));
|
|
325
|
-
var
|
|
913
|
+
var storagePath = resolve2(options?.storagePath ?? process.cwd());
|
|
326
914
|
var transport = {
|
|
327
915
|
postMessage: (message, transfer = []) => enginePort.postMessage(message, [...transfer]),
|
|
328
916
|
onMessage: (listener) => enginePort.on("message", listener)
|
|
329
917
|
};
|
|
330
|
-
var compressedWasm =
|
|
918
|
+
var compressedWasm = join2(dirname(engineUrl), "hyperdb-wasm.wasm.br");
|
|
331
919
|
var decompressedWasm = Readable.toWeb(createReadStream(compressedWasm).pipe(createBrotliDecompress()));
|
|
332
920
|
var compiledWasm = await WebAssembly.compileStreaming(new Response(decompressedWasm, {
|
|
333
921
|
headers: { "Content-Type": "application/wasm" }
|
|
@@ -344,18 +932,126 @@ var module = createRequire(import.meta.url)(engineUrl);
|
|
|
344
932
|
delete configuredGlobal.HYPERDB_WASM_MODULE;
|
|
345
933
|
configureWorker(module, transport, (runtime) => {
|
|
346
934
|
if (!runtime.NODEFS) throw new Error("Node filesystem backend is unavailable");
|
|
935
|
+
const nodeFS = runtime.NODEFS;
|
|
347
936
|
runtime.FS.mkdirTree("/host");
|
|
348
|
-
runtime.FS.mount(runtime.NODEFS, { root: hostPath }, "/host");
|
|
349
937
|
runtime.FS.mkdirTree("/hyperdb-persistent");
|
|
350
|
-
runtime.FS.mount(
|
|
938
|
+
runtime.FS.mount(nodeFS, { root: storagePath }, "/hyperdb-persistent");
|
|
351
939
|
runtime.FS.mkdirTree("/hyperdb-persistent/hyperdb");
|
|
352
940
|
runtime.FS.mkdirTree("/hyperdb-persistent/logs");
|
|
941
|
+
const nodeRuntimeFS = runtime.FS;
|
|
942
|
+
const shouldAccessHostPath = (path) => isAbsolute2(path) && !nodeRuntimeFS.analyzePath(path).exists;
|
|
943
|
+
runtime.hyperdbWasmShouldOpenHostFile = shouldAccessHostPath;
|
|
944
|
+
runtime.hyperdbWasmShouldListHostDirectory = shouldAccessHostPath;
|
|
945
|
+
runtime.externalDirectoryBackend = new NodeExternalDirectoryBackend();
|
|
946
|
+
const externalFiles = /* @__PURE__ */ new Map();
|
|
947
|
+
const externalPaths = /* @__PURE__ */ new Map();
|
|
948
|
+
let nextHostFileId = 1;
|
|
949
|
+
const openExternalFile = (path, locator) => {
|
|
950
|
+
const descriptor = openSync(path, "r");
|
|
951
|
+
try {
|
|
952
|
+
const stat2 = fstatSync(descriptor, { bigint: true });
|
|
953
|
+
if (!stat2.isFile()) throw new Error(`External file is not a regular file: ${path}`);
|
|
954
|
+
if (stat2.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("External file size must be a nonnegative safe integer");
|
|
955
|
+
const file = { descriptor, size: Number(stat2.size) };
|
|
956
|
+
externalFiles.set(locator, file);
|
|
957
|
+
return file;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
closeSync(descriptor);
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
runtime.externalFileBackend = {
|
|
964
|
+
register: (path) => {
|
|
965
|
+
const absolute = resolve2(path);
|
|
966
|
+
const locator = `hyperdb-node-file:${randomUUID2()}`;
|
|
967
|
+
const { size } = openExternalFile(absolute, locator);
|
|
968
|
+
return { name: basename2(absolute), locator, size };
|
|
969
|
+
},
|
|
970
|
+
registered: (path, locator) => {
|
|
971
|
+
if (!externalFiles.has(locator)) throw new Error("External file descriptor is unavailable");
|
|
972
|
+
externalPaths.set(path, locator);
|
|
973
|
+
},
|
|
974
|
+
removed: (path) => {
|
|
975
|
+
const locator = externalPaths.get(path) ?? path;
|
|
976
|
+
const file = externalFiles.get(locator);
|
|
977
|
+
if (file === void 0) return;
|
|
978
|
+
closeSync(file.descriptor);
|
|
979
|
+
externalFiles.delete(locator);
|
|
980
|
+
externalPaths.delete(path);
|
|
981
|
+
},
|
|
982
|
+
openHostFile: (path) => {
|
|
983
|
+
if (!isAbsolute2(path)) throw new Error("Host file path must be absolute");
|
|
984
|
+
const id = nextHostFileId++;
|
|
985
|
+
if (!Number.isSafeInteger(id)) throw new Error("Host file identifier limit reached");
|
|
986
|
+
const { size } = openExternalFile(path, `hyperdb-node-host-file:${id}`);
|
|
987
|
+
return { id, size };
|
|
988
|
+
},
|
|
989
|
+
closeHostFile: (id) => {
|
|
990
|
+
const locator = `hyperdb-node-host-file:${id}`;
|
|
991
|
+
const file = externalFiles.get(locator);
|
|
992
|
+
if (!file) return;
|
|
993
|
+
closeSync(file.descriptor);
|
|
994
|
+
externalFiles.delete(locator);
|
|
995
|
+
},
|
|
996
|
+
listHostDirectory: async (path) => {
|
|
997
|
+
if (!isAbsolute2(path)) throw new Error("Host directory path must be absolute");
|
|
998
|
+
const files = [];
|
|
999
|
+
const visit = async (directory, prefix) => {
|
|
1000
|
+
const entries = await opendir2(directory);
|
|
1001
|
+
for await (const entry of entries) {
|
|
1002
|
+
if (!entry.name || entry.name === "." || entry.name === ".." || entry.name.includes("/") || entry.name.includes("\\") || entry.name.includes("\0")) {
|
|
1003
|
+
throw new Error("Host directory contains an invalid entry name");
|
|
1004
|
+
}
|
|
1005
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1006
|
+
if (entry.isDirectory()) await visit(join2(directory, entry.name), relativePath);
|
|
1007
|
+
else if (entry.isFile()) files.push(relativePath);
|
|
1008
|
+
else if (entry.isSymbolicLink() && (await stat(join2(directory, entry.name))).isFile()) files.push(relativePath);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
await visit(path, "");
|
|
1012
|
+
return files;
|
|
1013
|
+
},
|
|
1014
|
+
read: (locator, offset, length, expectedSize) => {
|
|
1015
|
+
if (!locator.startsWith("hyperdb-node-file:") && !locator.startsWith("hyperdb-node-host-file:")) return void 0;
|
|
1016
|
+
return (async () => {
|
|
1017
|
+
const file = externalFiles.get(locator);
|
|
1018
|
+
if (file === void 0) throw new Error("External file registration does not exist");
|
|
1019
|
+
const end = offset + length;
|
|
1020
|
+
if (expectedSize !== file.size || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || !Number.isSafeInteger(end) || end > file.size) {
|
|
1021
|
+
throw new Error("External file byte range is invalid");
|
|
1022
|
+
}
|
|
1023
|
+
const result = new Uint8Array(length);
|
|
1024
|
+
let bytesRead = 0;
|
|
1025
|
+
while (bytesRead < length) {
|
|
1026
|
+
const count = await new Promise((resolveRead, rejectRead) => {
|
|
1027
|
+
read(file.descriptor, result, bytesRead, length - bytesRead, offset + bytesRead, (error, readCount) => {
|
|
1028
|
+
if (error) rejectRead(error);
|
|
1029
|
+
else resolveRead(readCount);
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
if (count === 0) throw new Error("External file ended before the registered size");
|
|
1033
|
+
bytesRead += count;
|
|
1034
|
+
}
|
|
1035
|
+
return result;
|
|
1036
|
+
})();
|
|
1037
|
+
},
|
|
1038
|
+
shutdown: () => {
|
|
1039
|
+
for (const { descriptor } of externalFiles.values()) closeSync(descriptor);
|
|
1040
|
+
externalFiles.clear();
|
|
1041
|
+
externalPaths.clear();
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
const hostDirectories = /* @__PURE__ */ new Map();
|
|
353
1045
|
runtime.resolveHyperDBPath = (path) => {
|
|
354
|
-
const absolute =
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
1046
|
+
const absolute = resolve2(path);
|
|
1047
|
+
const directory = dirname(absolute);
|
|
1048
|
+
let mount = hostDirectories.get(directory);
|
|
1049
|
+
if (mount === void 0) {
|
|
1050
|
+
mount = `/host/${hostDirectories.size}`;
|
|
1051
|
+
runtime.FS.mkdirTree(mount);
|
|
1052
|
+
runtime.FS.mount(nodeFS, { root: directory }, mount);
|
|
1053
|
+
hostDirectories.set(directory, mount);
|
|
358
1054
|
}
|
|
359
|
-
return posix.join(
|
|
1055
|
+
return posix.join(mount, basename2(absolute));
|
|
360
1056
|
};
|
|
361
1057
|
});
|