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/browser_worker.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
3
|
// src/wasm_bindings.ts
|
|
4
|
-
|
|
4
|
+
function completionFor(cleanups) {
|
|
5
|
+
const pending = cleanups.filter((cleanup) => cleanup !== void 0);
|
|
6
|
+
return pending.length === 0 ? void 0 : Promise.all(pending).then(() => void 0);
|
|
7
|
+
}
|
|
8
|
+
var resultStates = ["ok", "pending", "chunk", "done", "error", "busy", "structuredError"];
|
|
9
|
+
var structuredErrorMagic = new Uint8Array([72, 68, 66, 69]);
|
|
5
10
|
function safeInteger(value, name) {
|
|
6
11
|
const number = typeof value === "bigint" && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
|
7
12
|
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
@@ -11,6 +16,33 @@
|
|
|
11
16
|
if (!value) throw new Error(`${name} must not be empty`);
|
|
12
17
|
if (value.includes("\0")) throw new Error(`${name} must not contain NUL`);
|
|
13
18
|
}
|
|
19
|
+
function decodeStructuredError(payload) {
|
|
20
|
+
if (!structuredErrorMagic.every((byte, index) => payload[index] === byte)) throw new Error("structured error has invalid magic");
|
|
21
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
22
|
+
if (payload.byteLength < 8 || view.getUint32(4, true) !== 1) throw new Error("unsupported structured error version");
|
|
23
|
+
let offset = 8;
|
|
24
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
const readField = () => {
|
|
26
|
+
if (offset + 4 > payload.byteLength) throw new Error("truncated structured error field");
|
|
27
|
+
const length = view.getUint32(offset, true);
|
|
28
|
+
offset += 4;
|
|
29
|
+
if (length > payload.byteLength - offset) throw new Error("truncated structured error field");
|
|
30
|
+
const value = decoder.decode(payload.subarray(offset, offset + length));
|
|
31
|
+
offset += length;
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
const sqlState = readField();
|
|
35
|
+
const errorSource = readField();
|
|
36
|
+
const message = readField();
|
|
37
|
+
const detail = readField();
|
|
38
|
+
const hint = readField();
|
|
39
|
+
if (offset !== payload.byteLength) throw new Error("structured error has trailing bytes");
|
|
40
|
+
if (!/^[0-9A-Z]{5}$/.test(sqlState)) throw new Error("structured error has invalid SQLSTATE");
|
|
41
|
+
if (errorSource !== "User" && errorSource !== "System" && errorSource !== "OverloadRejection") {
|
|
42
|
+
throw new Error("structured error has invalid source");
|
|
43
|
+
}
|
|
44
|
+
return { sqlState, errorSource, message, detail, hint };
|
|
45
|
+
}
|
|
14
46
|
var HyperDBBindings = class {
|
|
15
47
|
module;
|
|
16
48
|
initialize;
|
|
@@ -22,13 +54,18 @@
|
|
|
22
54
|
checkpointDatabase;
|
|
23
55
|
dropDatabase;
|
|
24
56
|
shutdown;
|
|
57
|
+
registerExternalFile;
|
|
58
|
+
registerExternalDirectory;
|
|
59
|
+
removeExternalRegistration;
|
|
25
60
|
attachDatabase;
|
|
26
61
|
detachDatabase;
|
|
27
62
|
startQuery;
|
|
63
|
+
startQueryWithInfo;
|
|
28
64
|
insertArrowIPCFromPath;
|
|
29
65
|
pollQuery;
|
|
30
66
|
cancelQuery;
|
|
31
67
|
releaseQuery;
|
|
68
|
+
releaseQueryWithInfo;
|
|
32
69
|
resultState;
|
|
33
70
|
resultPayloadLength;
|
|
34
71
|
free;
|
|
@@ -44,13 +81,18 @@
|
|
|
44
81
|
this.checkpointDatabase = module.cwrap("hyperdb_wasm_checkpoint_database", "pointer", ["string"]);
|
|
45
82
|
this.dropDatabase = module.cwrap("hyperdb_wasm_drop_database", "pointer", ["string"]);
|
|
46
83
|
this.shutdown = module.cwrap("hyperdb_wasm_shutdown", "pointer", []);
|
|
84
|
+
this.registerExternalFile = module.cwrap("hyperdb_wasm_register_external_file", "pointer", ["string", "string", "number"]);
|
|
85
|
+
this.registerExternalDirectory = module.cwrap("hyperdb_wasm_register_external_directory", "pointer", ["string", "string", "number"]);
|
|
86
|
+
this.removeExternalRegistration = module.cwrap("hyperdb_wasm_remove_external_registration", "pointer", ["string"]);
|
|
47
87
|
this.attachDatabase = module.cwrap("hyperdb_wasm_attach_database", "pointer", ["number", "string", "string"]);
|
|
48
88
|
this.detachDatabase = module.cwrap("hyperdb_wasm_detach_database", "pointer", ["number", "string"]);
|
|
49
|
-
this.startQuery = module.cwrap("hyperdb_wasm_query_start", "pointer", ["number", "string"]);
|
|
89
|
+
this.startQuery = module.cwrap("hyperdb_wasm_query_start", "pointer", ["number", "string", "number"]);
|
|
90
|
+
this.startQueryWithInfo = module.cwrap("hyperdb_wasm_query_start_with_info", "pointer", ["number", "string"]);
|
|
50
91
|
this.insertArrowIPCFromPath = module.cwrap("hyperdb_wasm_arrow_insert_from_path", "pointer", ["number", "string", "string", "string", "number"]);
|
|
51
92
|
this.pollQuery = module.cwrap("hyperdb_wasm_query_poll", "pointer", ["number"]);
|
|
52
93
|
this.cancelQuery = module.cwrap("hyperdb_wasm_query_cancel", "pointer", ["number"]);
|
|
53
94
|
this.releaseQuery = module.cwrap("hyperdb_wasm_query_release", "pointer", ["number"]);
|
|
95
|
+
this.releaseQueryWithInfo = module.cwrap("hyperdb_wasm_query_release_with_info", "pointer", ["number"]);
|
|
54
96
|
this.resultState = module.cwrap("hyperdb_wasm_result_state", "number", ["number"]);
|
|
55
97
|
this.resultPayloadLength = module.cwrap("hyperdb_wasm_result_payload_len", "number", ["number"]);
|
|
56
98
|
this.free = module.cwrap("hyperdb_wasm_free", null, ["pointer"]);
|
|
@@ -66,8 +108,92 @@
|
|
|
66
108
|
this.module.FS.unlink(request.path);
|
|
67
109
|
return { response: { id: request.id, type: "response", result: { state: "ok", payload: new Uint8Array() } }, transfer: [] };
|
|
68
110
|
}
|
|
111
|
+
if (request.type === "registerExternalFile" && "path" in request) {
|
|
112
|
+
if (!this.module.externalFileBackend) {
|
|
113
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "Host external files are supported only by the Node client" } }, transfer: [] };
|
|
114
|
+
}
|
|
115
|
+
const registration = this.module.externalFileBackend.register(request.path);
|
|
116
|
+
try {
|
|
117
|
+
const pointer2 = safeInteger(this.registerExternalFile(registration.name, registration.locator, registration.size), "result pointer");
|
|
118
|
+
const result2 = this.decodeResult(pointer2);
|
|
119
|
+
if (result2.state === "ok") {
|
|
120
|
+
const path = new TextDecoder("utf-8", { fatal: true }).decode(result2.payload);
|
|
121
|
+
try {
|
|
122
|
+
this.module.externalFileBackend.registered(path, registration.locator);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const cleanupPointer = safeInteger(this.removeExternalRegistration(path), "result pointer");
|
|
125
|
+
this.decodeResult(cleanupPointer);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
this.module.externalFileBackend.removed(registration.locator);
|
|
130
|
+
}
|
|
131
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.module.externalFileBackend.removed(registration.locator);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (request.type === "registerExternalFile" && !this.module.externalFileBackendAvailable) {
|
|
138
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "Blob external files are supported only by the browser client" } }, transfer: [] };
|
|
139
|
+
}
|
|
140
|
+
if (request.type === "registerExternalDirectory") {
|
|
141
|
+
const backend = this.module.externalDirectoryBackend;
|
|
142
|
+
if (!backend) {
|
|
143
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "External directories are not supported by this client" } }, transfer: [] };
|
|
144
|
+
}
|
|
145
|
+
const registration = backend.register("handle" in request ? request.handle : request.path, request.mode);
|
|
146
|
+
try {
|
|
147
|
+
const pointer2 = safeInteger(this.registerExternalDirectory(registration.name, registration.locator, Number(request.mode === "readwrite")), "result pointer");
|
|
148
|
+
const result2 = this.decodeResult(pointer2);
|
|
149
|
+
if (result2.state === "ok") {
|
|
150
|
+
const path = new TextDecoder("utf-8", { fatal: true }).decode(result2.payload);
|
|
151
|
+
try {
|
|
152
|
+
backend.registered(path, registration.locator);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const cleanupPointer = safeInteger(this.removeExternalRegistration(path), "result pointer");
|
|
155
|
+
this.decodeResult(cleanupPointer);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
const completion = completionFor([backend.removed(registration.locator)]);
|
|
160
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [], ...completion && { completion } };
|
|
161
|
+
}
|
|
162
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
void Promise.resolve(backend.removed(registration.locator)).catch(() => void 0);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (request.type === "removeExternalRegistration" && (this.module.externalFileBackend || this.module.externalDirectoryBackend)) {
|
|
169
|
+
validateCString(request.path, "external registration path");
|
|
170
|
+
const pointer2 = safeInteger(this.removeExternalRegistration(request.path), "result pointer");
|
|
171
|
+
const result2 = this.decodeResult(pointer2);
|
|
172
|
+
if (result2.state === "ok") {
|
|
173
|
+
const completion = completionFor([
|
|
174
|
+
this.module.externalFileBackend?.removed(request.path),
|
|
175
|
+
this.module.externalDirectoryBackend?.removed(request.path)
|
|
176
|
+
]);
|
|
177
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [], ...completion && { completion } };
|
|
178
|
+
}
|
|
179
|
+
return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
|
|
180
|
+
}
|
|
181
|
+
if (request.type === "removeExternalRegistration" && !this.module.externalFileBackendAvailable) {
|
|
182
|
+
return { response: { id: request.id, type: "response", result: { state: "error", error: "External files are not supported by this client" } }, transfer: [] };
|
|
183
|
+
}
|
|
69
184
|
const pointer = safeInteger(this.invoke(request), "result pointer");
|
|
70
185
|
const result = this.decodeResult(pointer);
|
|
186
|
+
if (request.type === "shutdown" && result.state === "ok") {
|
|
187
|
+
const completion = completionFor([
|
|
188
|
+
this.module.externalFileBackend?.shutdown(),
|
|
189
|
+
this.module.externalDirectoryBackend?.shutdown()
|
|
190
|
+
]);
|
|
191
|
+
return {
|
|
192
|
+
response: { id: request.id, type: "response", result },
|
|
193
|
+
transfer: [],
|
|
194
|
+
...completion && { completion }
|
|
195
|
+
};
|
|
196
|
+
}
|
|
71
197
|
const response = { id: request.id, type: "response", result };
|
|
72
198
|
return { response, transfer: result.state === "chunk" ? [result.payload.buffer] : [] };
|
|
73
199
|
}
|
|
@@ -103,8 +229,20 @@
|
|
|
103
229
|
return this.detachDatabase(request.connection, request.alias);
|
|
104
230
|
case "shutdown":
|
|
105
231
|
return this.shutdown();
|
|
232
|
+
case "registerExternalFile":
|
|
233
|
+
if ("path" in request) throw new Error("host external file request has no backend");
|
|
234
|
+
validateCString(request.name, "external file name");
|
|
235
|
+
validateCString(request.url, "external file URL");
|
|
236
|
+
return this.registerExternalFile(request.name, request.url, safeInteger(request.size, "external file size"));
|
|
237
|
+
case "registerExternalDirectory":
|
|
238
|
+
throw new Error("external directory request has no backend");
|
|
239
|
+
case "removeExternalRegistration":
|
|
240
|
+
validateCString(request.path, "external registration path");
|
|
241
|
+
return this.removeExternalRegistration(request.path);
|
|
106
242
|
case "startQuery":
|
|
107
|
-
return this.startQuery(request.connection, request.sql);
|
|
243
|
+
return this.startQuery(request.connection, request.sql, Number(request.sqlTestMode ?? false));
|
|
244
|
+
case "startQueryWithInfo":
|
|
245
|
+
return this.startQueryWithInfo(request.connection, request.sql);
|
|
108
246
|
case "insertArrowIPCFromPath": {
|
|
109
247
|
const path = request.internal ? request.path : this.module.resolveHyperDBPath?.(request.path) ?? request.path;
|
|
110
248
|
return this.insertArrowIPCFromPath(request.connection, path, request.name, request.schema ?? "", Number(request.create));
|
|
@@ -115,6 +253,8 @@
|
|
|
115
253
|
return this.cancelQuery(request.query);
|
|
116
254
|
case "releaseQuery":
|
|
117
255
|
return this.releaseQuery(request.query);
|
|
256
|
+
case "releaseQueryWithInfo":
|
|
257
|
+
return this.releaseQueryWithInfo(request.query);
|
|
118
258
|
}
|
|
119
259
|
}
|
|
120
260
|
decodeResult(pointer) {
|
|
@@ -144,6 +284,8 @@
|
|
|
144
284
|
return { state, error: new TextDecoder().decode(payload) };
|
|
145
285
|
case "busy":
|
|
146
286
|
return { state, error: new TextDecoder().decode(payload) };
|
|
287
|
+
case "structuredError":
|
|
288
|
+
return { state: "error", error: decodeStructuredError(payload) };
|
|
147
289
|
}
|
|
148
290
|
} finally {
|
|
149
291
|
this.free(pointer);
|
|
@@ -183,6 +325,8 @@
|
|
|
183
325
|
case "detachDatabase":
|
|
184
326
|
return hasKeys(value, ["id", "type", "connection", "alias"]) && Number.isSafeInteger(value.connection) && typeof value.alias === "string";
|
|
185
327
|
case "startQuery":
|
|
328
|
+
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");
|
|
329
|
+
case "startQueryWithInfo":
|
|
186
330
|
return hasKeys(value, ["id", "type", "connection", "sql"]) && Number.isSafeInteger(value.connection) && typeof value.sql === "string";
|
|
187
331
|
case "insertArrowIPCFromPath":
|
|
188
332
|
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";
|
|
@@ -190,9 +334,16 @@
|
|
|
190
334
|
return hasKeys(value, ["id", "type", "bytes"]) && value.bytes instanceof Uint8Array;
|
|
191
335
|
case "removeFile":
|
|
192
336
|
return hasKeys(value, ["id", "type", "path"]) && typeof value.path === "string";
|
|
337
|
+
case "registerExternalFile":
|
|
338
|
+
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";
|
|
339
|
+
case "registerExternalDirectory":
|
|
340
|
+
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"));
|
|
341
|
+
case "removeExternalRegistration":
|
|
342
|
+
return hasKeys(value, ["id", "type", "path"]) && typeof value.path === "string";
|
|
193
343
|
case "pollQuery":
|
|
194
344
|
case "cancelQuery":
|
|
195
345
|
case "releaseQuery":
|
|
346
|
+
case "releaseQueryWithInfo":
|
|
196
347
|
return hasKeys(value, ["id", "type", "query"]) && Number.isSafeInteger(value.query);
|
|
197
348
|
default:
|
|
198
349
|
return false;
|
|
@@ -203,6 +354,10 @@
|
|
|
203
354
|
function errorText(error) {
|
|
204
355
|
return error instanceof Error ? error.message : String(error);
|
|
205
356
|
}
|
|
357
|
+
function isNotFoundError(error) {
|
|
358
|
+
if (typeof error !== "object" || error === null) return false;
|
|
359
|
+
return "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR") || "name" in error && error.name === "NotFoundError";
|
|
360
|
+
}
|
|
206
361
|
function safePointer(value, name) {
|
|
207
362
|
const pointer = typeof value === "bigint" && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
|
208
363
|
if (typeof pointer !== "number" || !Number.isSafeInteger(pointer) || pointer < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
@@ -211,22 +366,48 @@
|
|
|
211
366
|
function allocate(module, bytes) {
|
|
212
367
|
const pointer = safePointer(module._malloc(bytes.byteLength || 1), "allocation pointer");
|
|
213
368
|
if (pointer === 0) throw new Error("wasm allocation failed");
|
|
214
|
-
|
|
215
|
-
module.HEAPU8
|
|
369
|
+
module.getValue(pointer, "i8");
|
|
370
|
+
const heap = module.HEAPU8;
|
|
371
|
+
if (pointer + bytes.byteLength > heap.byteLength) throw new Error("allocation exceeds the wasm heap");
|
|
372
|
+
heap.set(bytes, pointer);
|
|
216
373
|
return pointer;
|
|
217
374
|
}
|
|
375
|
+
function externalDirectoryError(module, error) {
|
|
376
|
+
return safePointer(module.stringToNewUTF8(errorText(error)), "external directory error pointer");
|
|
377
|
+
}
|
|
378
|
+
function completeExternalDirectoryOperation(module, id, complete, operation) {
|
|
379
|
+
void Promise.resolve().then(operation).then(
|
|
380
|
+
() => complete(id, 0),
|
|
381
|
+
(error) => complete(id, externalDirectoryError(module, error))
|
|
382
|
+
);
|
|
383
|
+
}
|
|
218
384
|
function installHttpRuntime(module, transport) {
|
|
219
|
-
module.hyperdbWasmFetchStart = (id, url, offset, length) => {
|
|
385
|
+
module.hyperdbWasmFetchStart = (id, url, offset, length, expectedSize) => {
|
|
386
|
+
if (url.startsWith("hyperdb-node-file:") || url.startsWith("hyperdb-node-host-file:") || url.startsWith("hyperdb-node-directory-file:") || url.startsWith("hyperdb-browser-file:")) {
|
|
387
|
+
const read = module.externalFileBackend?.read(url, offset, length, expectedSize) ?? module.externalDirectoryBackend?.read(url, offset, length, expectedSize);
|
|
388
|
+
if (!read) {
|
|
389
|
+
module._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
void read.then((bytes) => {
|
|
393
|
+
module._hyperdb_wasm_fetch_complete(id, allocate(module, bytes), bytes.byteLength);
|
|
394
|
+
}).catch(() => {
|
|
395
|
+
module._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
220
399
|
const headers = new Headers();
|
|
221
400
|
if (length > 0) headers.set("Range", `bytes=${offset}-${offset + length - 1}`);
|
|
222
401
|
void fetch(url, { headers, redirect: "manual" }).then(async (response) => {
|
|
223
402
|
if (!response.ok || response.type === "opaqueredirect" || (length === 0 ? response.status !== 200 : response.status !== 206)) {
|
|
403
|
+
void response.body?.cancel().catch(() => void 0);
|
|
224
404
|
module._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
225
405
|
return;
|
|
226
406
|
}
|
|
227
407
|
if (length > 0) {
|
|
228
408
|
const contentRange = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(response.headers.get("content-range") ?? "");
|
|
229
|
-
if (!contentRange || contentRange[1] !== String(offset) || contentRange[2] !== String(offset + length - 1)) {
|
|
409
|
+
if (!contentRange || contentRange[1] !== String(offset) || contentRange[2] !== String(offset + length - 1) || expectedSize >= 0 && contentRange[3] !== String(expectedSize)) {
|
|
410
|
+
void response.body?.cancel().catch(() => void 0);
|
|
230
411
|
module._hyperdb_wasm_fetch_complete(id, 0, -1);
|
|
231
412
|
return;
|
|
232
413
|
}
|
|
@@ -275,6 +456,97 @@
|
|
|
275
456
|
});
|
|
276
457
|
};
|
|
277
458
|
}
|
|
459
|
+
function installExternalDirectoryRuntime(module) {
|
|
460
|
+
const backend = module.externalDirectoryBackend;
|
|
461
|
+
if (!backend) return;
|
|
462
|
+
const listComplete = module._hyperdb_wasm_external_directory_list_complete;
|
|
463
|
+
const openComplete = module._hyperdb_wasm_external_directory_open_complete;
|
|
464
|
+
if (listComplete) {
|
|
465
|
+
module.hyperdbWasmExternalDirectoryListStart = (id, locator) => {
|
|
466
|
+
void backend.list(locator).then((paths) => {
|
|
467
|
+
listComplete(id, safePointer(module.stringToNewUTF8(JSON.stringify(paths)), "external directory list pointer"), 0);
|
|
468
|
+
}).catch((error) => {
|
|
469
|
+
listComplete(id, 0, externalDirectoryError(module, error));
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (openComplete) {
|
|
474
|
+
module.hyperdbWasmExternalDirectoryOpenStart = (id, locator, path) => {
|
|
475
|
+
void backend.open(locator, path).then((file) => {
|
|
476
|
+
openComplete(id, safePointer(module.stringToNewUTF8(file.locator), "external file locator pointer"), file.size);
|
|
477
|
+
}).catch((error) => openComplete(id, 0, isNotFoundError(error) ? -2 : -1));
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const createDirectoriesComplete = module._hyperdb_wasm_external_directory_create_directories_complete;
|
|
481
|
+
if (createDirectoriesComplete) {
|
|
482
|
+
module.hyperdbWasmExternalDirectoryCreateDirectoriesStart = (id, locator, path) => {
|
|
483
|
+
completeExternalDirectoryOperation(module, id, createDirectoriesComplete, () => backend.createDirectories(locator, path));
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const writerWriteComplete = module._hyperdb_wasm_external_directory_writer_write_complete;
|
|
487
|
+
if (writerWriteComplete) {
|
|
488
|
+
module.hyperdbWasmExternalDirectoryWriterWriteStart = (writer, id, locator, path, offset, pointerValue, length) => {
|
|
489
|
+
try {
|
|
490
|
+
const pointer = safePointer(pointerValue, "external directory write pointer");
|
|
491
|
+
const safeLength = safePointer(length, "external directory write length");
|
|
492
|
+
const end = safePointer(pointer + safeLength, "external directory write end");
|
|
493
|
+
module.getValue(pointer, "i8");
|
|
494
|
+
if (end > module.HEAPU8.byteLength) throw new Error("external directory write exceeds the wasm heap");
|
|
495
|
+
const bytes = module.HEAPU8.slice(pointer, end);
|
|
496
|
+
completeExternalDirectoryOperation(module, id, writerWriteComplete, async () => {
|
|
497
|
+
await backend.openWriter(writer, locator, path);
|
|
498
|
+
await backend.write(writer, offset, bytes);
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
writerWriteComplete(id, externalDirectoryError(module, error));
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const writerFinishComplete = module._hyperdb_wasm_external_directory_writer_finish_complete;
|
|
506
|
+
if (writerFinishComplete) {
|
|
507
|
+
module.hyperdbWasmExternalDirectoryWriterFinishStart = (writer, id, locator, path, size) => {
|
|
508
|
+
completeExternalDirectoryOperation(module, id, writerFinishComplete, async () => {
|
|
509
|
+
await backend.openWriter(writer, locator, path);
|
|
510
|
+
await backend.finish(writer, size);
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const writerAbortComplete = module._hyperdb_wasm_external_directory_writer_abort_complete;
|
|
515
|
+
if (writerAbortComplete) {
|
|
516
|
+
module.hyperdbWasmExternalDirectoryWriterAbortStart = (writer, id) => {
|
|
517
|
+
completeExternalDirectoryOperation(module, id, writerAbortComplete, () => backend.abort(writer));
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function installHostFileRuntime(module) {
|
|
522
|
+
const complete = module._hyperdb_wasm_host_file_open_complete;
|
|
523
|
+
if (!complete) return;
|
|
524
|
+
module.hyperdbWasmHostFileOpenStart = (id, path) => {
|
|
525
|
+
try {
|
|
526
|
+
const file = module.externalFileBackend?.openHostFile(path);
|
|
527
|
+
if (!file) throw new Error("Node host-file backend is unavailable");
|
|
528
|
+
complete(id, file.id, file.size);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
complete(id, 0, isNotFoundError(error) ? -2 : -1);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
module.hyperdbWasmHostFileClose = (id) => module.externalFileBackend?.closeHostFile(id);
|
|
534
|
+
const listComplete = module._hyperdb_wasm_external_directory_list_complete;
|
|
535
|
+
if (listComplete) {
|
|
536
|
+
module.hyperdbWasmHostDirectoryListStart = (id, path) => {
|
|
537
|
+
const list = module.externalFileBackend?.listHostDirectory(path);
|
|
538
|
+
if (!list) {
|
|
539
|
+
listComplete(id, 0, externalDirectoryError(module, new Error("Node host-directory backend is unavailable")));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
void list.then((paths) => {
|
|
543
|
+
listComplete(id, safePointer(module.stringToNewUTF8(JSON.stringify(paths)), "host directory list pointer"), 0);
|
|
544
|
+
}).catch((error) => {
|
|
545
|
+
listComplete(id, 0, externalDirectoryError(module, error));
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
278
550
|
function configureWorker(module, transport, configureFilesystem = () => void 0) {
|
|
279
551
|
const queuedRequests = [];
|
|
280
552
|
let bindings;
|
|
@@ -285,8 +557,14 @@
|
|
|
285
557
|
}
|
|
286
558
|
try {
|
|
287
559
|
const result = bindings.handle(request);
|
|
288
|
-
|
|
289
|
-
|
|
560
|
+
const complete = () => {
|
|
561
|
+
if (request.type === "shutdown" && result.response.result.state === "ok") module.PThread.terminateAllThreads();
|
|
562
|
+
transport.postMessage(result.response, result.transfer);
|
|
563
|
+
};
|
|
564
|
+
if (result.completion) void result.completion.then(complete, (error) => {
|
|
565
|
+
transport.postMessage({ id: request.id, type: "response", result: { state: "error", error: errorText(error) } });
|
|
566
|
+
});
|
|
567
|
+
else complete();
|
|
290
568
|
} catch (error) {
|
|
291
569
|
transport.postMessage({ id: request.id, type: "response", result: { state: "error", error: errorText(error) } });
|
|
292
570
|
}
|
|
@@ -300,6 +578,8 @@
|
|
|
300
578
|
try {
|
|
301
579
|
configureFilesystem(module);
|
|
302
580
|
installHttpRuntime(module, transport);
|
|
581
|
+
installHostFileRuntime(module);
|
|
582
|
+
installExternalDirectoryRuntime(module);
|
|
303
583
|
bindings = new HyperDBBindings(module);
|
|
304
584
|
transport.postMessage({ type: "ready" });
|
|
305
585
|
for (const request of queuedRequests.splice(0)) handle(request);
|
|
@@ -310,6 +590,256 @@
|
|
|
310
590
|
module.onAbort = (reason) => transport.postMessage({ type: "fatal", error: `engine aborted: ${String(reason)}` });
|
|
311
591
|
}
|
|
312
592
|
|
|
593
|
+
// src/browser_external_directory.ts
|
|
594
|
+
var directoryLocatorPrefix = "hyperdb-browser-directory:";
|
|
595
|
+
var fileLocatorPrefix = "hyperdb-browser-file:";
|
|
596
|
+
function opaqueId() {
|
|
597
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
598
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
599
|
+
}
|
|
600
|
+
function validateSegment(segment) {
|
|
601
|
+
if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\") || segment.includes("\0")) {
|
|
602
|
+
throw new Error("External directory entries must have safe relative path segments");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function relativeSegments(path) {
|
|
606
|
+
if (!path || path.startsWith("/") || path.endsWith("/") || path.includes("\\") || path.includes("\0")) {
|
|
607
|
+
throw new Error("External directory file path must be relative");
|
|
608
|
+
}
|
|
609
|
+
const segments = path.split("/");
|
|
610
|
+
for (const segment of segments) validateSegment(segment);
|
|
611
|
+
return segments;
|
|
612
|
+
}
|
|
613
|
+
function directorySegments(path) {
|
|
614
|
+
return path ? relativeSegments(path) : [];
|
|
615
|
+
}
|
|
616
|
+
function nonnegativeSafeInteger(value, name) {
|
|
617
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a nonnegative safe integer`);
|
|
618
|
+
}
|
|
619
|
+
function asDirectoryHandle(handle) {
|
|
620
|
+
if (typeof handle !== "object" || handle === null || !("kind" in handle) || handle.kind !== "directory" || !("values" in handle) || typeof handle.values !== "function" || !("getDirectoryHandle" in handle) || typeof handle.getDirectoryHandle !== "function" || !("getFileHandle" in handle) || typeof handle.getFileHandle !== "function") {
|
|
621
|
+
throw new Error("External directory handle is invalid");
|
|
622
|
+
}
|
|
623
|
+
return handle;
|
|
624
|
+
}
|
|
625
|
+
var BrowserExternalDirectoryBackend = class {
|
|
626
|
+
directories = /* @__PURE__ */ new Map();
|
|
627
|
+
paths = /* @__PURE__ */ new Map();
|
|
628
|
+
files = /* @__PURE__ */ new Map();
|
|
629
|
+
writers = /* @__PURE__ */ new Map();
|
|
630
|
+
register(handle, mode) {
|
|
631
|
+
const directoryHandle = asDirectoryHandle(handle);
|
|
632
|
+
validateSegment(directoryHandle.name);
|
|
633
|
+
const locator = `${directoryLocatorPrefix}${opaqueId()}`;
|
|
634
|
+
this.directories.set(locator, { handle: directoryHandle, mode, files: /* @__PURE__ */ new Map(), locators: /* @__PURE__ */ new Set() });
|
|
635
|
+
return { name: directoryHandle.name, locator };
|
|
636
|
+
}
|
|
637
|
+
registered(path, locator) {
|
|
638
|
+
if (!this.directories.has(locator)) throw new Error("External directory registration is unavailable");
|
|
639
|
+
this.paths.set(path, locator);
|
|
640
|
+
}
|
|
641
|
+
removed(pathOrLocator) {
|
|
642
|
+
const locator = this.paths.get(pathOrLocator) ?? pathOrLocator;
|
|
643
|
+
const directory = this.directories.get(locator);
|
|
644
|
+
if (!directory) return;
|
|
645
|
+
for (const fileLocator of directory.locators) this.files.delete(fileLocator);
|
|
646
|
+
for (const [writer, state] of this.writers) {
|
|
647
|
+
if (state.locator === locator) void this.abort(writer).catch(() => void 0);
|
|
648
|
+
}
|
|
649
|
+
this.directories.delete(locator);
|
|
650
|
+
this.paths.delete(pathOrLocator);
|
|
651
|
+
}
|
|
652
|
+
async list(locator) {
|
|
653
|
+
const directory = this.getDirectory(locator);
|
|
654
|
+
const files = [];
|
|
655
|
+
const visit = async (handle, prefix) => {
|
|
656
|
+
for await (const child of handle.values()) {
|
|
657
|
+
validateSegment(child.name);
|
|
658
|
+
const path = prefix ? `${prefix}/${child.name}` : child.name;
|
|
659
|
+
if (child.kind === "file") files.push(path);
|
|
660
|
+
else if (child.kind === "directory") await visit(child, path);
|
|
661
|
+
else throw new Error("External directory contains an unsupported entry");
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
await visit(directory.handle, "");
|
|
665
|
+
files.sort();
|
|
666
|
+
return files;
|
|
667
|
+
}
|
|
668
|
+
async open(locator, path) {
|
|
669
|
+
const directory = this.getDirectory(locator);
|
|
670
|
+
const cached = directory.files.get(path);
|
|
671
|
+
if (cached) {
|
|
672
|
+
const file = await cached;
|
|
673
|
+
return { locator: file.locator, size: file.file.size };
|
|
674
|
+
}
|
|
675
|
+
const segments = relativeSegments(path);
|
|
676
|
+
const snapshot = (async () => {
|
|
677
|
+
let parent = directory.handle;
|
|
678
|
+
for (const segment of segments.slice(0, -1)) {
|
|
679
|
+
const child = await parent.getDirectoryHandle(segment);
|
|
680
|
+
if (child.kind !== "directory" || child.name !== segment) throw new Error("External directory returned an unexpected child handle");
|
|
681
|
+
parent = child;
|
|
682
|
+
}
|
|
683
|
+
const name = segments.at(-1);
|
|
684
|
+
if (!name) throw new Error("External directory file path must not be empty");
|
|
685
|
+
const fileHandle = await parent.getFileHandle(name);
|
|
686
|
+
if (fileHandle.kind !== "file" || fileHandle.name !== name) throw new Error("External directory returned an unexpected file handle");
|
|
687
|
+
const file = await fileHandle.getFile();
|
|
688
|
+
if (!Number.isSafeInteger(file.size) || file.size < 0) throw new Error("External file size must be a nonnegative safe integer");
|
|
689
|
+
const fileLocator = `${fileLocatorPrefix}${opaqueId()}`;
|
|
690
|
+
this.files.set(fileLocator, file);
|
|
691
|
+
directory.locators.add(fileLocator);
|
|
692
|
+
return { locator: fileLocator, file };
|
|
693
|
+
})();
|
|
694
|
+
directory.files.set(path, snapshot);
|
|
695
|
+
try {
|
|
696
|
+
const file = await snapshot;
|
|
697
|
+
return { locator: file.locator, size: file.file.size };
|
|
698
|
+
} catch (error) {
|
|
699
|
+
directory.files.delete(path);
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
read(locator, offset, length, expectedSize) {
|
|
704
|
+
if (!locator.startsWith(fileLocatorPrefix)) return void 0;
|
|
705
|
+
const file = this.files.get(locator);
|
|
706
|
+
if (!file) return Promise.reject(new Error("External directory file registration does not exist"));
|
|
707
|
+
const end = offset + length;
|
|
708
|
+
if (expectedSize !== file.size || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || !Number.isSafeInteger(end) || end > file.size) {
|
|
709
|
+
return Promise.reject(new Error("External file byte range is invalid"));
|
|
710
|
+
}
|
|
711
|
+
return file.slice(offset, end).arrayBuffer().then((buffer) => {
|
|
712
|
+
const bytes = new Uint8Array(buffer);
|
|
713
|
+
if (bytes.byteLength !== length) throw new Error("External file ended before the registered size");
|
|
714
|
+
return bytes;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
async createDirectories(locator, path) {
|
|
718
|
+
const directory = this.getWritableDirectory(locator);
|
|
719
|
+
let parent = directory.handle;
|
|
720
|
+
for (const segment of directorySegments(path)) {
|
|
721
|
+
const child = await parent.getDirectoryHandle(segment, { create: true });
|
|
722
|
+
if (child.kind !== "directory" || child.name !== segment) throw new Error("External directory returned an unexpected child handle");
|
|
723
|
+
parent = child;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
openWriter(writer, locator, path) {
|
|
727
|
+
nonnegativeSafeInteger(writer, "External directory writer");
|
|
728
|
+
const directory = this.getWritableDirectory(locator);
|
|
729
|
+
const segments = relativeSegments(path);
|
|
730
|
+
const name = segments.at(-1);
|
|
731
|
+
if (!name) throw new Error("External directory file path must not be empty");
|
|
732
|
+
const existing = this.writers.get(writer);
|
|
733
|
+
if (existing) {
|
|
734
|
+
if (existing.locator !== locator || existing.path !== path) throw new Error("External directory writer was reused for another path");
|
|
735
|
+
return existing.chain;
|
|
736
|
+
}
|
|
737
|
+
const state = { locator, path, name, chain: Promise.resolve(), terminal: false };
|
|
738
|
+
this.writers.set(writer, state);
|
|
739
|
+
const opened = this.enqueue(state, async () => {
|
|
740
|
+
let parent = directory.handle;
|
|
741
|
+
for (const segment of segments.slice(0, -1)) {
|
|
742
|
+
const child = await parent.getDirectoryHandle(segment, { create: true });
|
|
743
|
+
if (child.kind !== "directory" || child.name !== segment) throw new Error("External directory returned an unexpected child handle");
|
|
744
|
+
parent = child;
|
|
745
|
+
}
|
|
746
|
+
let file;
|
|
747
|
+
try {
|
|
748
|
+
file = await parent.getFileHandle(name);
|
|
749
|
+
state.createdTarget = false;
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (!(error instanceof DOMException) || error.name !== "NotFoundError") throw error;
|
|
752
|
+
file = await parent.getFileHandle(name, { create: true });
|
|
753
|
+
state.createdTarget = true;
|
|
754
|
+
}
|
|
755
|
+
if (file.kind !== "file" || file.name !== name || typeof file.createWritable !== "function") {
|
|
756
|
+
throw new Error("External directory returned an unexpected file handle");
|
|
757
|
+
}
|
|
758
|
+
state.parent = parent;
|
|
759
|
+
state.stream = await file.createWritable();
|
|
760
|
+
});
|
|
761
|
+
return opened;
|
|
762
|
+
}
|
|
763
|
+
write(writer, offset, bytes) {
|
|
764
|
+
nonnegativeSafeInteger(offset, "External directory write offset");
|
|
765
|
+
nonnegativeSafeInteger(offset + bytes.byteLength, "External directory write end");
|
|
766
|
+
const state = this.getOpenWriter(writer);
|
|
767
|
+
return this.enqueue(state, () => {
|
|
768
|
+
if (!state.stream) throw new Error("External directory writer failed to open");
|
|
769
|
+
return state.stream.write({ type: "write", position: offset, data: bytes });
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
finish(writer, size) {
|
|
773
|
+
nonnegativeSafeInteger(size, "External directory file size");
|
|
774
|
+
const state = this.getOpenWriter(writer);
|
|
775
|
+
state.terminal = true;
|
|
776
|
+
const operation = this.enqueue(state, async () => {
|
|
777
|
+
let failure = state.failure;
|
|
778
|
+
if (failure === void 0 && !state.stream) failure = new Error("External directory writer failed to open");
|
|
779
|
+
if (failure === void 0) {
|
|
780
|
+
try {
|
|
781
|
+
await state.stream?.truncate(size);
|
|
782
|
+
await state.stream?.close();
|
|
783
|
+
} catch (error) {
|
|
784
|
+
failure = error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (failure !== void 0) {
|
|
788
|
+
await this.cleanupWriter(state).catch(() => void 0);
|
|
789
|
+
throw failure;
|
|
790
|
+
}
|
|
791
|
+
this.getDirectory(state.locator).files.delete(state.path);
|
|
792
|
+
}, true);
|
|
793
|
+
return operation.finally(() => this.writers.delete(writer));
|
|
794
|
+
}
|
|
795
|
+
abort(writer) {
|
|
796
|
+
const state = this.getOpenWriter(writer);
|
|
797
|
+
state.terminal = true;
|
|
798
|
+
const operation = this.enqueue(state, () => this.cleanupWriter(state), true);
|
|
799
|
+
return operation.finally(() => this.writers.delete(writer));
|
|
800
|
+
}
|
|
801
|
+
shutdown() {
|
|
802
|
+
for (const writer of this.writers.keys()) void this.abort(writer).catch(() => void 0);
|
|
803
|
+
this.files.clear();
|
|
804
|
+
this.paths.clear();
|
|
805
|
+
this.directories.clear();
|
|
806
|
+
}
|
|
807
|
+
getDirectory(locator) {
|
|
808
|
+
const directory = this.directories.get(locator);
|
|
809
|
+
if (!directory) throw new Error("External directory registration does not exist");
|
|
810
|
+
return directory;
|
|
811
|
+
}
|
|
812
|
+
getWritableDirectory(locator) {
|
|
813
|
+
const directory = this.getDirectory(locator);
|
|
814
|
+
if (directory.mode !== "readwrite") throw new Error("External directory registration is not writable");
|
|
815
|
+
return directory;
|
|
816
|
+
}
|
|
817
|
+
getOpenWriter(writer) {
|
|
818
|
+
nonnegativeSafeInteger(writer, "External directory writer");
|
|
819
|
+
const state = this.writers.get(writer);
|
|
820
|
+
if (!state || state.terminal) throw new Error("External directory writer is not open");
|
|
821
|
+
return state;
|
|
822
|
+
}
|
|
823
|
+
async cleanupWriter(state) {
|
|
824
|
+
this.directories.get(state.locator)?.files.delete(state.path);
|
|
825
|
+
try {
|
|
826
|
+
await state.stream?.abort();
|
|
827
|
+
} finally {
|
|
828
|
+
if (state.createdTarget) await state.parent?.removeEntry?.(state.name).catch(() => void 0);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
enqueue(state, operation, afterFailure = false) {
|
|
832
|
+
const result = state.chain.then(async () => {
|
|
833
|
+
if (!afterFailure && state.failure !== void 0) throw state.failure;
|
|
834
|
+
await operation();
|
|
835
|
+
});
|
|
836
|
+
state.chain = result.catch((error) => {
|
|
837
|
+
state.failure ??= error;
|
|
838
|
+
});
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
313
843
|
// src/browser_worker.ts
|
|
314
844
|
var configuredEngineUrl = new URL(self.location.href).searchParams.get("engineUrl");
|
|
315
845
|
var engineUrl = self.HYPERDB_WASM_ENGINE_URL ?? configuredEngineUrl ?? "hyperdb-wasm.js";
|
|
@@ -356,6 +886,8 @@
|
|
|
356
886
|
};
|
|
357
887
|
self.Module = module;
|
|
358
888
|
configureWorker(module, transport, (runtime) => {
|
|
889
|
+
runtime.externalFileBackendAvailable = true;
|
|
890
|
+
runtime.externalDirectoryBackend = new BrowserExternalDirectoryBackend();
|
|
359
891
|
if (!runtime.OPFS) throw new Error("OPFS backend is unavailable");
|
|
360
892
|
runtime.FS.mkdirTree("/opfs");
|
|
361
893
|
runtime.FS.mount(runtime.OPFS, {}, "/opfs");
|