space-data-module-sdk 0.8.21 → 0.8.23

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.
@@ -0,0 +1,349 @@
1
+ // One `flatsql_io` conformance script for every host (docs/architecture/
2
+ // flatsql-partition-store.md 22.3a-6: "Run one flatsql_io conformance script on
3
+ // all three hosts").
4
+ //
5
+ // It drives a provider through the contract's observable behaviour: statuses,
6
+ // short reads, sparse writes, truncation both ways, flag semantics (EXCL,
7
+ // TRUNC, PROBE, UNLINK, UNLINK_IF_UNUSED, CREATE_PARENTS, DELETE_ON_CLOSE,
8
+ // OPEN_DEFERRED), access modes, confinement and multi-handle visibility. The
9
+ // same cases run against the Node sync-fs provider, the SAB channel over the
10
+ // memory backend, and the SAB channel over OPFS in real browsers; a host that
11
+ // differs fails by name.
12
+ //
13
+ // A provider for this script exposes (sync or async; every call is awaited):
14
+ // open(path: string | Uint8Array, flags) -> handle | status
15
+ // readInto(handle, view: Uint8Array, offset) -> count | status
16
+ // writeFrom(handle, view: Uint8Array, offset) -> count | status
17
+ // truncate(handle, size) / sync(handle) / close(handle) -> status
18
+ // size(handle) -> number | status
19
+
20
+ import {
21
+ FLATSQL_IO_CREATE,
22
+ FLATSQL_IO_CREATE_PARENTS,
23
+ FLATSQL_IO_DELETE_ON_CLOSE,
24
+ FLATSQL_IO_ERR_ACCESS,
25
+ FLATSQL_IO_ERR_BADHANDLE,
26
+ FLATSQL_IO_ERR_BUSY,
27
+ FLATSQL_IO_ERR_GENERIC,
28
+ FLATSQL_IO_ERR_IO,
29
+ FLATSQL_IO_ERR_NOENT,
30
+ FLATSQL_IO_EXCL,
31
+ FLATSQL_IO_OPEN_DEFERRED,
32
+ FLATSQL_IO_PROBE,
33
+ FLATSQL_IO_READ,
34
+ FLATSQL_IO_TRUNC,
35
+ FLATSQL_IO_UNLINK,
36
+ FLATSQL_IO_UNLINK_IF_UNUSED,
37
+ FLATSQL_IO_WRITE,
38
+ } from "./flatsqlIoContract.js";
39
+
40
+ const RW = FLATSQL_IO_READ | FLATSQL_IO_WRITE;
41
+ const RWC = RW | FLATSQL_IO_CREATE;
42
+ const encoder = new TextEncoder();
43
+ const decoder = new TextDecoder();
44
+
45
+ function pattern(length, seed) {
46
+ const bytes = new Uint8Array(length);
47
+ let x = seed >>> 0 || 1;
48
+ for (let i = 0; i < length; i += 1) {
49
+ x ^= x << 13;
50
+ x ^= x >>> 17;
51
+ x ^= x << 5;
52
+ bytes[i] = x & 0xff;
53
+ }
54
+ return bytes;
55
+ }
56
+
57
+ function equalBytes(a, b) {
58
+ if (a.length !== b.length) return false;
59
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
60
+ return true;
61
+ }
62
+
63
+ class ConformanceFailure extends Error {}
64
+
65
+ function expect(condition, message) {
66
+ if (!condition) throw new ConformanceFailure(message);
67
+ }
68
+
69
+ function expectStatus(actual, expected, what) {
70
+ expect(actual === expected, `${what}: expected ${expected}, got ${actual}`);
71
+ }
72
+
73
+ function expectHandle(actual, what) {
74
+ expect(Number.isInteger(actual) && actual >= 0, `${what}: expected a handle, got ${actual}`);
75
+ }
76
+
77
+ /**
78
+ * The cases, in order. Each gets `(io, dir)` where `dir` is a fresh directory
79
+ * prefix for the case.
80
+ */
81
+ export const FLATSQL_IO_CONFORMANCE_CASES = Object.freeze([
82
+ {
83
+ name: "open without CREATE on a missing file is NOENT",
84
+ async run(io, dir) {
85
+ expectStatus(await io.open(`${dir}/missing.bin`, RW), FLATSQL_IO_ERR_NOENT, "open");
86
+ },
87
+ },
88
+ {
89
+ name: "create, write, size, short read at EOF, read past EOF",
90
+ async run(io, dir) {
91
+ const h = await io.open(`${dir}/a.bin`, RWC);
92
+ expectHandle(h, "open");
93
+ expectStatus(await io.writeFrom(h, encoder.encode("hello"), 0), 5, "write");
94
+ expectStatus(await io.size(h), 5, "size");
95
+ const buf = new Uint8Array(10);
96
+ expectStatus(await io.readInto(h, buf, 0), 5, "short read");
97
+ expect(decoder.decode(buf.subarray(0, 5)) === "hello", "short read bytes");
98
+ expectStatus(await io.readInto(h, new Uint8Array(4), 5), 0, "read at EOF");
99
+ expectStatus(await io.readInto(h, new Uint8Array(4), 100), 0, "read past EOF");
100
+ expectStatus(await io.close(h), 0, "close");
101
+ },
102
+ },
103
+ {
104
+ name: "a write past EOF extends the file and the gap reads as zeros",
105
+ async run(io, dir) {
106
+ const h = await io.open(`${dir}/sparse.bin`, RWC);
107
+ expectHandle(h, "open");
108
+ expectStatus(await io.writeFrom(h, encoder.encode("ab"), 0), 2, "write head");
109
+ expectStatus(await io.writeFrom(h, encoder.encode("z"), 100), 1, "write past EOF");
110
+ expectStatus(await io.size(h), 101, "size");
111
+ const gap = new Uint8Array(98);
112
+ gap.fill(0xee);
113
+ expectStatus(await io.readInto(h, gap, 2), 98, "read gap");
114
+ expect(gap.every((b) => b === 0), "gap is zeros");
115
+ expectStatus(await io.close(h), 0, "close");
116
+ },
117
+ },
118
+ {
119
+ name: "truncate shrinks and grows; grown bytes read as zeros",
120
+ async run(io, dir) {
121
+ const h = await io.open(`${dir}/t.bin`, RWC);
122
+ expectHandle(h, "open");
123
+ await io.writeFrom(h, encoder.encode("abcdefgh"), 0);
124
+ expectStatus(await io.truncate(h, 3), 0, "truncate down");
125
+ expectStatus(await io.size(h), 3, "size after shrink");
126
+ expectStatus(await io.truncate(h, 10), 0, "truncate up");
127
+ expectStatus(await io.size(h), 10, "size after grow");
128
+ const buf = new Uint8Array(10);
129
+ expectStatus(await io.readInto(h, buf, 0), 10, "read");
130
+ expect(decoder.decode(buf.subarray(0, 3)) === "abc", "kept prefix");
131
+ expect(buf.subarray(3).every((b) => b === 0), "grown tail is zeros");
132
+ expectStatus(await io.sync(h), 0, "sync");
133
+ expectStatus(await io.close(h), 0, "close");
134
+ },
135
+ },
136
+ {
137
+ name: "closed and bogus handles are BADHANDLE",
138
+ async run(io, dir) {
139
+ const h = await io.open(`${dir}/c.bin`, RWC);
140
+ expectHandle(h, "open");
141
+ expectStatus(await io.close(h), 0, "close");
142
+ expectStatus(await io.close(h), FLATSQL_IO_ERR_BADHANDLE, "close again");
143
+ expectStatus(await io.readInto(h, new Uint8Array(1), 0), FLATSQL_IO_ERR_BADHANDLE, "read");
144
+ expectStatus(await io.size(0x7fff00), FLATSQL_IO_ERR_BADHANDLE, "bogus size");
145
+ },
146
+ },
147
+ {
148
+ name: "PROBE reports existence without a handle",
149
+ async run(io, dir) {
150
+ const h = await io.open(`${dir}/p.bin`, RWC);
151
+ expectHandle(h, "open");
152
+ await io.close(h);
153
+ expectStatus(await io.open(`${dir}/p.bin`, FLATSQL_IO_PROBE), 0, "probe existing");
154
+ expectStatus(await io.open(`${dir}/nope.bin`, FLATSQL_IO_PROBE), FLATSQL_IO_ERR_NOENT, "probe missing");
155
+ },
156
+ },
157
+ {
158
+ name: "EXCL on an existing file is GENERIC; TRUNC empties it",
159
+ async run(io, dir) {
160
+ const h = await io.open(`${dir}/x.bin`, RWC);
161
+ expectHandle(h, "open");
162
+ await io.writeFrom(h, encoder.encode("data"), 0);
163
+ await io.close(h);
164
+ expectStatus(
165
+ await io.open(`${dir}/x.bin`, RWC | FLATSQL_IO_EXCL),
166
+ FLATSQL_IO_ERR_GENERIC,
167
+ "EXCL",
168
+ );
169
+ const t = await io.open(`${dir}/x.bin`, RW | FLATSQL_IO_TRUNC);
170
+ expectHandle(t, "TRUNC open");
171
+ expectStatus(await io.size(t), 0, "size after TRUNC");
172
+ await io.close(t);
173
+ },
174
+ },
175
+ {
176
+ name: "CREATE under a missing directory is NOENT; CREATE_PARENTS makes it",
177
+ async run(io, dir) {
178
+ expectStatus(
179
+ await io.open(`${dir}/p/00000001/h.fsh`, RWC),
180
+ FLATSQL_IO_ERR_NOENT,
181
+ "no parents",
182
+ );
183
+ const h = await io.open(`${dir}/p/00000001/h.fsh`, RWC | FLATSQL_IO_CREATE_PARENTS);
184
+ expectHandle(h, "CREATE_PARENTS");
185
+ expectStatus(await io.writeFrom(h, encoder.encode("head"), 0), 4, "write");
186
+ await io.close(h);
187
+ expectStatus(await io.open(`${dir}/p/00000001/h.fsh`, FLATSQL_IO_PROBE), 0, "probe");
188
+ },
189
+ },
190
+ {
191
+ name: "UNLINK_IF_UNUSED is BUSY while a handle is open; UNLINK removes",
192
+ async run(io, dir) {
193
+ const h = await io.open(`${dir}/u.bin`, RWC);
194
+ expectHandle(h, "open");
195
+ expectStatus(
196
+ await io.open(`${dir}/u.bin`, FLATSQL_IO_UNLINK_IF_UNUSED),
197
+ FLATSQL_IO_ERR_BUSY,
198
+ "unlink while open",
199
+ );
200
+ await io.close(h);
201
+ expectStatus(await io.open(`${dir}/u.bin`, FLATSQL_IO_UNLINK_IF_UNUSED), 0, "unlink unused");
202
+ expectStatus(await io.open(`${dir}/u.bin`, FLATSQL_IO_PROBE), FLATSQL_IO_ERR_NOENT, "gone");
203
+ expectStatus(await io.open(`${dir}/u.bin`, FLATSQL_IO_UNLINK), FLATSQL_IO_ERR_NOENT, "unlink missing");
204
+ const again = await io.open(`${dir}/v.bin`, RWC);
205
+ await io.close(again);
206
+ expectStatus(await io.open(`${dir}/v.bin`, FLATSQL_IO_UNLINK), 0, "plain unlink");
207
+ },
208
+ },
209
+ {
210
+ name: "DELETE_ON_CLOSE drops the file with its last handle",
211
+ async run(io, dir) {
212
+ const h = await io.open(`${dir}/tmp.bin`, RWC | FLATSQL_IO_DELETE_ON_CLOSE);
213
+ expectHandle(h, "open");
214
+ await io.writeFrom(h, encoder.encode("scratch"), 0);
215
+ expectStatus(await io.close(h), 0, "close");
216
+ // Hosts that delete asynchronously finish before the next namespace op.
217
+ expectStatus(await io.open(`${dir}/tmp.bin`, FLATSQL_IO_PROBE), FLATSQL_IO_ERR_NOENT, "probe");
218
+ },
219
+ },
220
+ {
221
+ name: "paths are confined below the root",
222
+ async run(io, dir) {
223
+ expectStatus(await io.open(`${dir}/../../escape.bin`, RWC), FLATSQL_IO_ERR_ACCESS, "..");
224
+ expectStatus(await io.open("", RWC), FLATSQL_IO_ERR_GENERIC, "empty path");
225
+ },
226
+ },
227
+ {
228
+ name: "access modes: read-only handles refuse writes, write-only refuse reads",
229
+ async run(io, dir) {
230
+ const h = await io.open(`${dir}/m.bin`, RWC);
231
+ await io.writeFrom(h, encoder.encode("mode"), 0);
232
+ await io.close(h);
233
+ const ro = await io.open(`${dir}/m.bin`, FLATSQL_IO_READ);
234
+ expectHandle(ro, "read-only open");
235
+ expectStatus(await io.writeFrom(ro, encoder.encode("x"), 0), FLATSQL_IO_ERR_IO, "write on RO");
236
+ expectStatus(await io.truncate(ro, 0), FLATSQL_IO_ERR_IO, "truncate on RO");
237
+ await io.close(ro);
238
+ const wo = await io.open(`${dir}/m.bin`, FLATSQL_IO_WRITE);
239
+ expectHandle(wo, "write-only open");
240
+ expectStatus(await io.readInto(wo, new Uint8Array(2), 0), FLATSQL_IO_ERR_IO, "read on WO");
241
+ await io.close(wo);
242
+ },
243
+ },
244
+ {
245
+ name: "OPEN_DEFERRED: a failed open surfaces at open or at first use",
246
+ async run(io, dir) {
247
+ const h = await io.open(`${dir}/deferred-missing.bin`, RW | FLATSQL_IO_OPEN_DEFERRED);
248
+ if (h < 0) {
249
+ expectStatus(h, FLATSQL_IO_ERR_NOENT, "sync host");
250
+ } else {
251
+ expectStatus(await io.readInto(h, new Uint8Array(1), 0), FLATSQL_IO_ERR_NOENT, "first use");
252
+ expectStatus(await io.close(h), 0, "close failed deferred");
253
+ }
254
+ const ok = await io.open(`${dir}/deferred.bin`, RWC | FLATSQL_IO_OPEN_DEFERRED);
255
+ expectHandle(ok, "deferred create");
256
+ expectStatus(await io.writeFrom(ok, encoder.encode("later"), 0), 5, "write after deferred");
257
+ expectStatus(await io.size(ok), 5, "size");
258
+ await io.close(ok);
259
+ },
260
+ },
261
+ {
262
+ name: "two handles on one path see each other's writes",
263
+ async run(io, dir) {
264
+ const a = await io.open(`${dir}/shared.bin`, RWC);
265
+ const b = await io.open(`${dir}/shared.bin`, RW);
266
+ expectHandle(a, "a");
267
+ expectHandle(b, "b");
268
+ await io.writeFrom(a, encoder.encode("from-a"), 0);
269
+ const buf = new Uint8Array(6);
270
+ expectStatus(await io.readInto(b, buf, 0), 6, "read via b");
271
+ expect(decoder.decode(buf) === "from-a", "b sees a");
272
+ await io.writeFrom(b, encoder.encode("B"), 0);
273
+ const one = new Uint8Array(1);
274
+ await io.readInto(a, one, 0);
275
+ expect(one[0] === 0x42, "a sees b");
276
+ await io.close(a);
277
+ await io.close(b);
278
+ },
279
+ },
280
+ {
281
+ name: "1 MiB round trip across chunk boundaries at an unaligned offset",
282
+ async run(io, dir) {
283
+ const h = await io.open(`${dir}/big.bin`, RWC);
284
+ expectHandle(h, "open");
285
+ const data = pattern(1024 * 1024 + 17, 0x9e3779b9);
286
+ expectStatus(await io.writeFrom(h, data, 4093), data.length, "write");
287
+ expectStatus(await io.size(h), 4093 + data.length, "size");
288
+ const back = new Uint8Array(data.length);
289
+ expectStatus(await io.readInto(h, back, 4093), data.length, "read");
290
+ expect(equalBytes(back, data), "bytes round trip");
291
+ await io.close(h);
292
+ },
293
+ },
294
+ ]);
295
+
296
+ /**
297
+ * Run the conformance cases against `io`.
298
+ *
299
+ * @param {object} io provider (see the file header)
300
+ * @param {object} [options]
301
+ * @param {string} [options.prefix="conformance"] directory for the cases
302
+ * @param {string[]} [options.skip] case names to skip, each with a reason in
303
+ * `options.skipReasons`
304
+ * @returns {Promise<{ passed: string[], failed: { name: string, error: string }[], skipped: string[] }>}
305
+ */
306
+ export async function runFlatsqlIoConformance(io, options = {}) {
307
+ const prefix = options.prefix ?? "conformance";
308
+ const skip = new Set(options.skip ?? []);
309
+ const passed = [];
310
+ const failed = [];
311
+ const skipped = [];
312
+ let index = 0;
313
+ for (const testCase of FLATSQL_IO_CONFORMANCE_CASES) {
314
+ index += 1;
315
+ if (skip.has(testCase.name)) {
316
+ skipped.push(testCase.name);
317
+ continue;
318
+ }
319
+ const dir = `${prefix}/case-${String(index).padStart(2, "0")}`;
320
+ // Every case starts from its own existing directory.
321
+ const seed = await io.open(`${dir}/.keep`, RWC | FLATSQL_IO_CREATE_PARENTS);
322
+ if (seed >= 0) await io.close(seed);
323
+ try {
324
+ await testCase.run(io, dir);
325
+ passed.push(testCase.name);
326
+ } catch (error) {
327
+ failed.push({ name: testCase.name, error: String(error?.message ?? error) });
328
+ }
329
+ }
330
+ return { passed, failed, skipped };
331
+ }
332
+
333
+ /** Adapt the async channel client (bytes-returning read) to the script's shape. */
334
+ export function conformanceAdapterForAsyncClient(client) {
335
+ return {
336
+ open: (path, flags) => client.open(path, flags),
337
+ async readInto(handle, view, offset) {
338
+ const result = await client.read(handle, view.length, offset);
339
+ if (typeof result === "number") return result;
340
+ view.set(result, 0);
341
+ return result.length;
342
+ },
343
+ writeFrom: (handle, view, offset) => client.write(handle, view, offset),
344
+ truncate: (handle, size) => client.truncate(handle, size),
345
+ sync: (handle) => client.sync(handle),
346
+ size: (handle) => client.size(handle),
347
+ close: (handle) => client.close(handle),
348
+ };
349
+ }
@@ -0,0 +1,240 @@
1
+ // The FlatSQL host I/O contract, as the SDK's JavaScript hosts implement it.
2
+ //
3
+ // FlatSQL's engine reaches files only through seven `env.flatsql_io_*` imports
4
+ // (flatsql cpp/include/flatsql/flatsql_io.h). Every host satisfies the same
5
+ // seven names with the same signatures: the SDN node's C host module, the Go
6
+ // HostIO, the Node sync-fs provider (nodeSyncFsIo.js) and the browser I/O
7
+ // workers (opfsIoWorker.mjs behind sabIoChannel.js). This file holds the
8
+ // constants those hosts share, so the flag and status values are defined once.
9
+ //
10
+ // SIGNATURE LAW: i32 and f64 only. Offsets and sizes cross as f64.
11
+ // ERROR LAW: errors are negative return values, never throws. A throw out of an
12
+ // import is a trap, and a trap poisons the calling instance.
13
+ //
14
+ // The partition store (docs/architecture/flatsql-partition-store.md §5.4, A12,
15
+ // A38) adds three open flags and one status. They are flags, not imports, so the
16
+ // import set stays at seven.
17
+
18
+ /** Open flags. Values must stay identical to flatsql_io.h. */
19
+ export const FLATSQL_IO_READ = 0x0001;
20
+ export const FLATSQL_IO_WRITE = 0x0002;
21
+ export const FLATSQL_IO_CREATE = 0x0004;
22
+ export const FLATSQL_IO_EXCL = 0x0008;
23
+ export const FLATSQL_IO_TRUNC = 0x0010;
24
+ /** Advisory: the host drops the file when its last handle closes. */
25
+ export const FLATSQL_IO_DELETE_ON_CLOSE = 0x0020;
26
+ /** xAccess: returns 0 when the path exists, NOENT otherwise. No handle. */
27
+ export const FLATSQL_IO_PROBE = 0x0040;
28
+ /** xDelete: removes the path. No handle. */
29
+ export const FLATSQL_IO_UNLINK = 0x0080;
30
+ /**
31
+ * mkdir -p the parent directories. Each created directory's parent is synced,
32
+ * and the parent directory is synced when the file itself is newly created.
33
+ * (OPFS directory durability is best effort; see §7 and 22.3a-4.)
34
+ */
35
+ export const FLATSQL_IO_CREATE_PARENTS = 0x0100;
36
+ /** Unlink, but return BUSY while any handle on the path is open. */
37
+ export const FLATSQL_IO_UNLINK_IF_UNUSED = 0x0200;
38
+ /**
39
+ * Return a handle before the open completes (A38). The first read or write on
40
+ * the handle blocks only if the open is still pending, and fails with the
41
+ * open's status if it failed. Hosts whose opens are synchronous (WasmEdge,
42
+ * Node) treat the flag as a plain open.
43
+ */
44
+ export const FLATSQL_IO_OPEN_DEFERRED = 0x0400;
45
+
46
+ /** Status codes. Every one is negative. */
47
+ export const FLATSQL_IO_OK = 0;
48
+ export const FLATSQL_IO_ERR_GENERIC = -1;
49
+ export const FLATSQL_IO_ERR_NOENT = -2;
50
+ export const FLATSQL_IO_ERR_ACCESS = -3;
51
+ export const FLATSQL_IO_ERR_IO = -4;
52
+ export const FLATSQL_IO_ERR_NOSPACE = -5;
53
+ export const FLATSQL_IO_ERR_BADHANDLE = -6;
54
+ /** A12: the path is in use (UNLINK_IF_UNUSED), or its lock is held elsewhere. */
55
+ export const FLATSQL_IO_ERR_BUSY = -7;
56
+
57
+ export const FLATSQL_IO_FLAGS = Object.freeze({
58
+ READ: FLATSQL_IO_READ,
59
+ WRITE: FLATSQL_IO_WRITE,
60
+ CREATE: FLATSQL_IO_CREATE,
61
+ EXCL: FLATSQL_IO_EXCL,
62
+ TRUNC: FLATSQL_IO_TRUNC,
63
+ DELETE_ON_CLOSE: FLATSQL_IO_DELETE_ON_CLOSE,
64
+ PROBE: FLATSQL_IO_PROBE,
65
+ UNLINK: FLATSQL_IO_UNLINK,
66
+ CREATE_PARENTS: FLATSQL_IO_CREATE_PARENTS,
67
+ UNLINK_IF_UNUSED: FLATSQL_IO_UNLINK_IF_UNUSED,
68
+ OPEN_DEFERRED: FLATSQL_IO_OPEN_DEFERRED,
69
+ });
70
+
71
+ export const FLATSQL_IO_STATUS = Object.freeze({
72
+ OK: FLATSQL_IO_OK,
73
+ GENERIC: FLATSQL_IO_ERR_GENERIC,
74
+ NOENT: FLATSQL_IO_ERR_NOENT,
75
+ ACCESS: FLATSQL_IO_ERR_ACCESS,
76
+ IO: FLATSQL_IO_ERR_IO,
77
+ NOSPACE: FLATSQL_IO_ERR_NOSPACE,
78
+ BADHANDLE: FLATSQL_IO_ERR_BADHANDLE,
79
+ BUSY: FLATSQL_IO_ERR_BUSY,
80
+ });
81
+
82
+ /** The seven import names, module "env". */
83
+ export const FLATSQL_IO_IMPORT_MODULE = "env";
84
+ export const FLATSQL_IO_IMPORT_NAMES = Object.freeze([
85
+ "flatsql_io_open",
86
+ "flatsql_io_read",
87
+ "flatsql_io_write",
88
+ "flatsql_io_truncate",
89
+ "flatsql_io_sync",
90
+ "flatsql_io_size",
91
+ "flatsql_io_close",
92
+ ]);
93
+
94
+ /** Longest path any SDK host accepts, in UTF-8 bytes (the Go host's limit). */
95
+ export const FLATSQL_IO_MAX_PATH_BYTES = 4096;
96
+
97
+ /**
98
+ * Map a thrown host error to a status. Covers DOMExceptions from OPFS
99
+ * (22.3a-6: NotFound -> NOENT, NoModificationAllowed -> BUSY, QuotaExceeded ->
100
+ * NOSPACE) and Node system errors.
101
+ *
102
+ * WebKit refuses a second sync access handle with InvalidStateError rather
103
+ * than NoModificationAllowedError (measured, WebKit 26.6), so an
104
+ * InvalidStateError raised while OPENING maps to BUSY; pass
105
+ * `{ during: "open" }` for that case.
106
+ */
107
+ export function flatsqlIoStatusForError(error, { during } = {}) {
108
+ if (!error) {
109
+ return FLATSQL_IO_ERR_GENERIC;
110
+ }
111
+ const code = typeof error.code === "string" ? error.code : null;
112
+ if (code) {
113
+ switch (code) {
114
+ case "ENOENT":
115
+ case "ENOTDIR":
116
+ return FLATSQL_IO_ERR_NOENT;
117
+ case "EACCES":
118
+ case "EPERM":
119
+ case "EROFS":
120
+ return FLATSQL_IO_ERR_ACCESS;
121
+ case "ENOSPC":
122
+ case "EDQUOT":
123
+ return FLATSQL_IO_ERR_NOSPACE;
124
+ case "EBADF":
125
+ return FLATSQL_IO_ERR_IO;
126
+ case "EEXIST":
127
+ // The Go host maps os.ErrExist to GENERIC; every SDK host matches it.
128
+ return FLATSQL_IO_ERR_GENERIC;
129
+ case "EBUSY":
130
+ case "ETXTBSY":
131
+ return FLATSQL_IO_ERR_BUSY;
132
+ case "ENAMETOOLONG":
133
+ case "EINVAL":
134
+ return FLATSQL_IO_ERR_GENERIC;
135
+ default:
136
+ return FLATSQL_IO_ERR_IO;
137
+ }
138
+ }
139
+ switch (error.name) {
140
+ case "NotFoundError":
141
+ return FLATSQL_IO_ERR_NOENT;
142
+ case "NoModificationAllowedError":
143
+ return FLATSQL_IO_ERR_BUSY;
144
+ case "InvalidStateError":
145
+ return during === "open" ? FLATSQL_IO_ERR_BUSY : FLATSQL_IO_ERR_BADHANDLE;
146
+ case "QuotaExceededError":
147
+ return FLATSQL_IO_ERR_NOSPACE;
148
+ case "TypeMismatchError":
149
+ case "SecurityError":
150
+ case "NotAllowedError":
151
+ return FLATSQL_IO_ERR_ACCESS;
152
+ case "InvalidModificationError":
153
+ return FLATSQL_IO_ERR_BUSY;
154
+ case "TypeError":
155
+ case "RangeError":
156
+ return FLATSQL_IO_ERR_GENERIC;
157
+ default:
158
+ return FLATSQL_IO_ERR_IO;
159
+ }
160
+ }
161
+
162
+ /** An Error carrying a flatsql_io status, thrown inside hosts and caught at the edge. */
163
+ export class FlatsqlIoStatusError extends Error {
164
+ constructor(status, message) {
165
+ super(message ?? `flatsql_io status ${status}`);
166
+ this.name = "FlatsqlIoStatusError";
167
+ this.status = status;
168
+ }
169
+ }
170
+
171
+ /** Status for any error thrown inside a host, honouring FlatsqlIoStatusError. */
172
+ export function flatsqlIoStatusOf(error, options) {
173
+ if (error && typeof error.status === "number" && error.status < 0) {
174
+ return error.status;
175
+ }
176
+ return flatsqlIoStatusForError(error, options);
177
+ }
178
+
179
+ /**
180
+ * Split a guest path into components below a host root. Paths are opaque
181
+ * host-namespace strings to the engine; confinement is the host's job and it
182
+ * fails closed: "", ".", ".." and NUL are refused with ACCESS (".." would
183
+ * escape) or GENERIC (malformed). A leading "/" is ignored, so "/a/b" and "a/b"
184
+ * name the same file below the root. Returns an array of components, or a
185
+ * negative status.
186
+ */
187
+ export function splitFlatsqlIoPath(path) {
188
+ if (typeof path !== "string" || path.length === 0) {
189
+ return FLATSQL_IO_ERR_GENERIC;
190
+ }
191
+ if (path.includes("\0")) {
192
+ return FLATSQL_IO_ERR_GENERIC;
193
+ }
194
+ const components = [];
195
+ for (const part of path.split("/")) {
196
+ if (part === "" || part === ".") {
197
+ continue;
198
+ }
199
+ if (part === "..") {
200
+ return FLATSQL_IO_ERR_ACCESS;
201
+ }
202
+ components.push(part);
203
+ }
204
+ if (components.length === 0) {
205
+ return FLATSQL_IO_ERR_GENERIC;
206
+ }
207
+ return components;
208
+ }
209
+
210
+ /** Decode path bytes that may live in a SharedArrayBuffer (copied first). */
211
+ const PATH_DECODER =
212
+ typeof TextDecoder === "function" ? new TextDecoder("utf-8", { fatal: true }) : null;
213
+
214
+ export function decodeFlatsqlIoPath(bytes) {
215
+ if (!PATH_DECODER) {
216
+ return FLATSQL_IO_ERR_GENERIC;
217
+ }
218
+ if (bytes.length === 0 || bytes.length > FLATSQL_IO_MAX_PATH_BYTES) {
219
+ return FLATSQL_IO_ERR_GENERIC;
220
+ }
221
+ // TextDecoder rejects views over a SharedArrayBuffer in some engines, and a
222
+ // shared view can change under the decoder. Decode a private copy.
223
+ const copy =
224
+ bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
225
+ try {
226
+ return PATH_DECODER.decode(copy);
227
+ } catch {
228
+ return FLATSQL_IO_ERR_GENERIC;
229
+ }
230
+ }
231
+
232
+ /** Word-folded-free FNV-1a 32 over path bytes; used to shard paths across workers. */
233
+ export function hashFlatsqlIoPath(bytes) {
234
+ let hash = 0x811c9dc5;
235
+ for (let i = 0; i < bytes.length; i += 1) {
236
+ hash ^= bytes[i];
237
+ hash = Math.imul(hash, 0x01000193);
238
+ }
239
+ return hash >>> 0;
240
+ }