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.
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  (() => {
3
3
  // src/wasm_bindings.ts
4
- var resultStates = ["ok", "pending", "chunk", "done", "error", "busy"];
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
- if (pointer + bytes.byteLength > module.HEAPU8.byteLength) throw new Error("allocation exceeds the wasm heap");
215
- module.HEAPU8.set(bytes, pointer);
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
- if (request.type === "shutdown" && result.response.result.state === "ok") module.PThread.terminateAllThreads();
289
- transport.postMessage(result.response, result.transfer);
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");