flatsql 1.3.0 → 1.4.5

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,618 @@
1
+ /**
2
+ * FlatSQL host I/O backends — the JavaScript half of the seven-import contract
3
+ * (cpp/include/flatsql/flatsql_io.h).
4
+ *
5
+ * ONE backend interface, used by BOTH JavaScript loaders:
6
+ * - the emscripten browser bundle, via cpp/js/flatsql_io_library.js
7
+ * - the standalone/WASI shim, via wasm/standalone.js
8
+ * ...and matched call-for-call by the Go host and by cpp/src/flatsql_io_native.cpp.
9
+ *
10
+ * A backend is SYNCHRONOUS, because a wasm import is. Its methods are:
11
+ *
12
+ * open(path: string, flags: number): number // handle, or negative status
13
+ * read(handle, dst: Uint8Array, offset: number): number
14
+ * write(handle, src: Uint8Array, offset: number): number
15
+ * truncate(handle, size: number): number
16
+ * sync(handle): number
17
+ * size(handle): number
18
+ * close(handle): number
19
+ *
20
+ * The pointer->subarray translation is done by the loader; a backend never sees
21
+ * wasm memory. That is the entire reason two very different hosts can satisfy
22
+ * the same seven calls without one line of runtime detection inside the module.
23
+ *
24
+ * ASYNC STORES (the one honest asymmetry, and it is in the shim where it
25
+ * belongs): IndexedDB and an HTTP desktop store cannot answer synchronously.
26
+ * createChunkedStoreBackend() therefore keeps a synchronous page cache and
27
+ * makes durability an explicit, awaited JavaScript step:
28
+ *
29
+ * await backend.hydrate(path); // BEFORE opening the database
30
+ * ... engine runs entirely synchronously ...
31
+ * await backend.flush(); // AFTER flatsql_flush_index()
32
+ *
33
+ * Results are identical to the native lane in every case; only the moment at
34
+ * which bytes become durable differs, and it is observable and awaited rather
35
+ * than assumed. Nothing about the wasm-side contract changes.
36
+ */
37
+
38
+ export const FLATSQL_IO_READ = 0x0001;
39
+ export const FLATSQL_IO_WRITE = 0x0002;
40
+ export const FLATSQL_IO_CREATE = 0x0004;
41
+ export const FLATSQL_IO_EXCL = 0x0008;
42
+ export const FLATSQL_IO_TRUNC = 0x0010;
43
+ export const FLATSQL_IO_DELETE_ON_CLOSE = 0x0020;
44
+ export const FLATSQL_IO_PROBE = 0x0040;
45
+ export const FLATSQL_IO_UNLINK = 0x0080;
46
+
47
+ export const FLATSQL_IO_ERR_GENERIC = -1;
48
+ export const FLATSQL_IO_ERR_NOENT = -2;
49
+ export const FLATSQL_IO_ERR_ACCESS = -3;
50
+ export const FLATSQL_IO_ERR_IO = -4;
51
+ export const FLATSQL_IO_ERR_NOSPACE = -5;
52
+ export const FLATSQL_IO_ERR_BADHANDLE = -6;
53
+
54
+ /** Default page-group size. One IndexedDB value per group. */
55
+ export const DEFAULT_CHUNK_BYTES = 64 * 1024;
56
+
57
+ /**
58
+ * A file living in RAM as a set of fixed-size chunks. Shared by every backend
59
+ * so that chunk boundaries, growth and zero-fill behave identically no matter
60
+ * what the bytes are eventually persisted into.
61
+ */
62
+ class ChunkedFile {
63
+ constructor(path, chunkBytes) {
64
+ this.path = path;
65
+ this.chunkBytes = chunkBytes;
66
+ this.chunks = new Map(); // index -> Uint8Array(chunkBytes)
67
+ this.dirty = new Set();
68
+ this.length = 0;
69
+ this.metaDirty = false;
70
+ }
71
+
72
+ chunk(index, create) {
73
+ let buf = this.chunks.get(index);
74
+ if (!buf && create) {
75
+ buf = new Uint8Array(this.chunkBytes);
76
+ this.chunks.set(index, buf);
77
+ }
78
+ return buf || null;
79
+ }
80
+
81
+ read(dst, offset) {
82
+ // A read past EOF is a SHORT read, never an error — the C side zero-fills
83
+ // the remainder and reports SQLITE_IOERR_SHORT_READ, exactly as POSIX
84
+ // pread does. Getting this wrong silently corrupts pager reads.
85
+ const available = Math.max(0, this.length - offset);
86
+ const want = Math.min(dst.length, available);
87
+ let done = 0;
88
+ while (done < want) {
89
+ const at = offset + done;
90
+ const index = Math.floor(at / this.chunkBytes);
91
+ const within = at % this.chunkBytes;
92
+ const n = Math.min(this.chunkBytes - within, want - done);
93
+ const buf = this.chunk(index, false);
94
+ if (buf) {
95
+ dst.set(buf.subarray(within, within + n), done);
96
+ } else {
97
+ dst.fill(0, done, done + n); // sparse hole
98
+ }
99
+ done += n;
100
+ }
101
+ return want;
102
+ }
103
+
104
+ write(src, offset) {
105
+ let done = 0;
106
+ while (done < src.length) {
107
+ const at = offset + done;
108
+ const index = Math.floor(at / this.chunkBytes);
109
+ const within = at % this.chunkBytes;
110
+ const n = Math.min(this.chunkBytes - within, src.length - done);
111
+ const buf = this.chunk(index, true);
112
+ buf.set(src.subarray(done, done + n), within);
113
+ this.dirty.add(index);
114
+ done += n;
115
+ }
116
+ if (offset + src.length > this.length) {
117
+ this.length = offset + src.length;
118
+ this.metaDirty = true;
119
+ }
120
+ return src.length;
121
+ }
122
+
123
+ truncate(size) {
124
+ if (size < this.length) {
125
+ const lastIndex = Math.floor(size / this.chunkBytes);
126
+ for (const index of Array.from(this.chunks.keys())) {
127
+ if (index > lastIndex) {
128
+ this.chunks.delete(index);
129
+ this.dirty.add(index); // removal must be persisted too
130
+ }
131
+ }
132
+ const tail = this.chunk(lastIndex, false);
133
+ if (tail) {
134
+ tail.fill(0, size % this.chunkBytes);
135
+ this.dirty.add(lastIndex);
136
+ }
137
+ }
138
+ this.length = size;
139
+ this.metaDirty = true;
140
+ return 0;
141
+ }
142
+ }
143
+
144
+ /** Handle table shared by every backend: small, dense, reused after close. */
145
+ class HandleTable {
146
+ constructor() {
147
+ this.slots = [];
148
+ }
149
+
150
+ add(entry) {
151
+ for (let i = 0; i < this.slots.length; i++) {
152
+ if (!this.slots[i]) {
153
+ this.slots[i] = entry;
154
+ return i;
155
+ }
156
+ }
157
+ this.slots.push(entry);
158
+ return this.slots.length - 1;
159
+ }
160
+
161
+ get(handle) {
162
+ return this.slots[handle] || null;
163
+ }
164
+
165
+ remove(handle) {
166
+ const entry = this.slots[handle];
167
+ this.slots[handle] = null;
168
+ return entry;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Pure in-memory backend. Durable for exactly as long as the process lives —
174
+ * which is the DOCUMENTED behaviour asserted by the Memory-store lane of the
175
+ * test matrix, not a skipped case.
176
+ */
177
+ export function createMemoryBackend(options = {}) {
178
+ const chunkBytes = options.chunkBytes || DEFAULT_CHUNK_BYTES;
179
+ const files = new Map();
180
+ const handles = new HandleTable();
181
+
182
+ return {
183
+ kind: 'memory',
184
+ files,
185
+
186
+ open(path, flags) {
187
+ if (flags & FLATSQL_IO_PROBE) {
188
+ return files.has(path) ? 0 : FLATSQL_IO_ERR_NOENT;
189
+ }
190
+ if (flags & FLATSQL_IO_UNLINK) {
191
+ return files.delete(path) ? 0 : FLATSQL_IO_ERR_NOENT;
192
+ }
193
+ let file = files.get(path);
194
+ if (!file) {
195
+ if (!(flags & FLATSQL_IO_CREATE)) return FLATSQL_IO_ERR_NOENT;
196
+ file = new ChunkedFile(path, chunkBytes);
197
+ files.set(path, file);
198
+ } else if (flags & FLATSQL_IO_EXCL) {
199
+ return FLATSQL_IO_ERR_ACCESS;
200
+ }
201
+ if (flags & FLATSQL_IO_TRUNC) file.truncate(0);
202
+ return handles.add({ file, deleteOnClose: !!(flags & FLATSQL_IO_DELETE_ON_CLOSE) });
203
+ },
204
+
205
+ read(handle, dst, offset) {
206
+ const entry = handles.get(handle);
207
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
208
+ return entry.file.read(dst, offset);
209
+ },
210
+
211
+ write(handle, src, offset) {
212
+ const entry = handles.get(handle);
213
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
214
+ return entry.file.write(src, offset);
215
+ },
216
+
217
+ truncate(handle, size) {
218
+ const entry = handles.get(handle);
219
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
220
+ return entry.file.truncate(size);
221
+ },
222
+
223
+ sync() {
224
+ return 0; // RAM is already as durable as it will ever be
225
+ },
226
+
227
+ size(handle) {
228
+ const entry = handles.get(handle);
229
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
230
+ return entry.file.length;
231
+ },
232
+
233
+ close(handle) {
234
+ const entry = handles.remove(handle);
235
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
236
+ if (entry.deleteOnClose) files.delete(entry.file.path);
237
+ return 0;
238
+ },
239
+
240
+ async hydrate() {},
241
+ async flush() {},
242
+ async drop(path) {
243
+ files.delete(path);
244
+ },
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Node/desktop backend over real files. Fully synchronous, so sync() really is
250
+ * durable at return — this lane has no asymmetry at all.
251
+ *
252
+ * `fs` is injected rather than imported so the module stays loadable in a
253
+ * browser bundle that will never call this.
254
+ */
255
+ export function createNodeFsBackend(fs, options = {}) {
256
+ const root = options.root || '';
257
+ const handles = new HandleTable();
258
+ const resolve = (p) => (root ? `${root}/${p.replace(/^\/+/, '')}` : p);
259
+
260
+ const statusFor = (err) => {
261
+ if (!err || !err.code) return FLATSQL_IO_ERR_GENERIC;
262
+ if (err.code === 'ENOENT') return FLATSQL_IO_ERR_NOENT;
263
+ if (err.code === 'EACCES' || err.code === 'EPERM') return FLATSQL_IO_ERR_ACCESS;
264
+ if (err.code === 'ENOSPC') return FLATSQL_IO_ERR_NOSPACE;
265
+ return FLATSQL_IO_ERR_IO;
266
+ };
267
+
268
+ return {
269
+ kind: 'node-fs',
270
+
271
+ open(path, flags) {
272
+ const full = resolve(path);
273
+ try {
274
+ if (flags & FLATSQL_IO_PROBE) {
275
+ fs.statSync(full);
276
+ return 0;
277
+ }
278
+ if (flags & FLATSQL_IO_UNLINK) {
279
+ fs.unlinkSync(full);
280
+ return 0;
281
+ }
282
+ let mode = 'r';
283
+ if (flags & FLATSQL_IO_WRITE) {
284
+ mode = flags & FLATSQL_IO_EXCL ? 'wx+' : flags & FLATSQL_IO_CREATE ? 'a+' : 'r+';
285
+ }
286
+ const fd = fs.openSync(full, mode);
287
+ return handles.add({
288
+ fd,
289
+ path: full,
290
+ deleteOnClose: !!(flags & FLATSQL_IO_DELETE_ON_CLOSE),
291
+ });
292
+ } catch (err) {
293
+ return statusFor(err);
294
+ }
295
+ },
296
+
297
+ read(handle, dst, offset) {
298
+ const entry = handles.get(handle);
299
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
300
+ try {
301
+ return fs.readSync(entry.fd, dst, 0, dst.length, offset);
302
+ } catch (err) {
303
+ return statusFor(err);
304
+ }
305
+ },
306
+
307
+ write(handle, src, offset) {
308
+ const entry = handles.get(handle);
309
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
310
+ try {
311
+ return fs.writeSync(entry.fd, src, 0, src.length, offset);
312
+ } catch (err) {
313
+ return statusFor(err);
314
+ }
315
+ },
316
+
317
+ truncate(handle, size) {
318
+ const entry = handles.get(handle);
319
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
320
+ try {
321
+ fs.ftruncateSync(entry.fd, size);
322
+ return 0;
323
+ } catch (err) {
324
+ return statusFor(err);
325
+ }
326
+ },
327
+
328
+ sync(handle) {
329
+ const entry = handles.get(handle);
330
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
331
+ try {
332
+ fs.fsyncSync(entry.fd);
333
+ return 0;
334
+ } catch (err) {
335
+ return statusFor(err);
336
+ }
337
+ },
338
+
339
+ size(handle) {
340
+ const entry = handles.get(handle);
341
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
342
+ try {
343
+ return fs.fstatSync(entry.fd).size;
344
+ } catch (err) {
345
+ return statusFor(err);
346
+ }
347
+ },
348
+
349
+ close(handle) {
350
+ const entry = handles.remove(handle);
351
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
352
+ try {
353
+ fs.closeSync(entry.fd);
354
+ if (entry.deleteOnClose) {
355
+ try {
356
+ fs.unlinkSync(entry.path);
357
+ } catch (_) {
358
+ /* already gone */
359
+ }
360
+ }
361
+ return 0;
362
+ } catch (err) {
363
+ return statusFor(err);
364
+ }
365
+ },
366
+
367
+ async hydrate() {},
368
+ async flush() {},
369
+ async drop(path) {
370
+ try {
371
+ fs.unlinkSync(resolve(path));
372
+ } catch (_) {
373
+ /* already gone */
374
+ }
375
+ },
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Backend over ANY flat key->bytes store — the shape every sdn-js persistence
381
+ * store already has (readBytes/writeBytes/deleteKey). This is what gives a
382
+ * store with no seek and no partial write real pread/pwrite semantics:
383
+ *
384
+ * <prefix><path>#<pageGroup> fixed-size chunk of file bytes
385
+ * <prefix><path>#meta JSON { length }
386
+ *
387
+ * Because addressing is by chunk index, a flush costs O(dirty chunks) rather
388
+ * than O(file), and the browser lane reads and writes the same byte ranges the
389
+ * POSIX lane does. It is not "the browser rewrites everything" — the two lanes
390
+ * are the same algorithm over a different transport.
391
+ *
392
+ * `store` must provide:
393
+ * readBytes(key): Promise<Uint8Array|null>
394
+ * writeBytes(key, bytes): Promise<void>
395
+ * deleteKey(key): Promise<void>
396
+ */
397
+ export function createChunkedStoreBackend(store, options = {}) {
398
+ const chunkBytes = options.chunkBytes || DEFAULT_CHUNK_BYTES;
399
+ const prefix = options.prefix || 'flatsql-io/';
400
+ const files = new Map();
401
+ const handles = new HandleTable();
402
+ const known = new Set(options.knownPaths || []);
403
+
404
+ const metaKey = (path) => `${prefix}${path}#meta`;
405
+ const chunkKey = (path, index) => `${prefix}${path}#${index}`;
406
+
407
+ /**
408
+ * Pull a file's chunks into the synchronous cache. MUST be awaited before
409
+ * the engine opens the database: after that point every call is synchronous
410
+ * because a wasm import cannot await.
411
+ */
412
+ async function hydrate(path) {
413
+ if (files.has(path)) return files.get(path);
414
+ const file = new ChunkedFile(path, chunkBytes);
415
+ let meta = null;
416
+ const raw = await store.readBytes(metaKey(path));
417
+ if (raw) {
418
+ try {
419
+ meta = JSON.parse(new TextDecoder().decode(raw));
420
+ } catch (_) {
421
+ meta = null;
422
+ }
423
+ }
424
+ if (meta && Number.isFinite(meta.length)) {
425
+ file.length = meta.length;
426
+ if (meta.chunkBytes && meta.chunkBytes !== chunkBytes) {
427
+ // A different page-group size means the persisted keys do not line up
428
+ // with the ones we would read. Treat it as absent: derived state is
429
+ // always rebuildable, so this costs a re-derivation and never a wrong
430
+ // answer.
431
+ file.length = 0;
432
+ } else {
433
+ const count = Math.ceil(file.length / chunkBytes);
434
+ for (let i = 0; i < count; i++) {
435
+ const bytes = await store.readBytes(chunkKey(path, i));
436
+ if (bytes) {
437
+ const buf = new Uint8Array(chunkBytes);
438
+ buf.set(bytes.subarray(0, Math.min(bytes.length, chunkBytes)));
439
+ file.chunks.set(i, buf);
440
+ }
441
+ }
442
+ }
443
+ known.add(path);
444
+ }
445
+ files.set(path, file);
446
+ return file;
447
+ }
448
+
449
+ /** Persist every dirty chunk plus the length. Await this after a flush. */
450
+ async function flush() {
451
+ for (const [path, file] of files) {
452
+ if (!file.dirty.size && !file.metaDirty) continue;
453
+ for (const index of file.dirty) {
454
+ const buf = file.chunks.get(index);
455
+ if (buf) {
456
+ const start = index * chunkBytes;
457
+ const end = Math.min(chunkBytes, Math.max(0, file.length - start));
458
+ await store.writeBytes(chunkKey(path, index), buf.subarray(0, end));
459
+ } else {
460
+ await store.deleteKey(chunkKey(path, index));
461
+ }
462
+ }
463
+ file.dirty.clear();
464
+ if (file.metaDirty || file.length >= 0) {
465
+ await store.writeBytes(
466
+ metaKey(path),
467
+ new TextEncoder().encode(JSON.stringify({ length: file.length, chunkBytes })),
468
+ );
469
+ file.metaDirty = false;
470
+ }
471
+ known.add(path);
472
+ }
473
+ }
474
+
475
+ async function drop(path) {
476
+ const file = files.get(path);
477
+ const count = file
478
+ ? Math.max(Math.ceil(file.length / chunkBytes), ...[...file.chunks.keys()].map((i) => i + 1), 0)
479
+ : 0;
480
+ for (let i = 0; i < count; i++) await store.deleteKey(chunkKey(path, i));
481
+ await store.deleteKey(metaKey(path));
482
+ files.delete(path);
483
+ known.delete(path);
484
+ }
485
+
486
+ return {
487
+ kind: 'chunked-store',
488
+ chunkBytes,
489
+ hydrate,
490
+ flush,
491
+ drop,
492
+ /** Paths this backend believes exist — used to hydrate before an open. */
493
+ knownPaths: () => Array.from(known),
494
+
495
+ open(path, flags) {
496
+ if (flags & FLATSQL_IO_PROBE) {
497
+ const file = files.get(path);
498
+ if (file) return file.length > 0 || file.chunks.size > 0 ? 0 : FLATSQL_IO_ERR_NOENT;
499
+ return known.has(path) ? 0 : FLATSQL_IO_ERR_NOENT;
500
+ }
501
+ if (flags & FLATSQL_IO_UNLINK) {
502
+ const file = files.get(path);
503
+ if (file) file.truncate(0);
504
+ known.delete(path);
505
+ return 0;
506
+ }
507
+ let file = files.get(path);
508
+ if (!file) {
509
+ // Not hydrated. Creating is fine (it is genuinely new); opening an
510
+ // existing file that was never hydrated is a caller error we refuse
511
+ // rather than answer with an empty file — an empty answer here is
512
+ // indistinguishable from data loss.
513
+ if (!(flags & FLATSQL_IO_CREATE)) return FLATSQL_IO_ERR_NOENT;
514
+ file = new ChunkedFile(path, chunkBytes);
515
+ file.metaDirty = true;
516
+ files.set(path, file);
517
+ } else if (flags & FLATSQL_IO_EXCL && file.length > 0) {
518
+ return FLATSQL_IO_ERR_ACCESS;
519
+ }
520
+ if (flags & FLATSQL_IO_TRUNC) file.truncate(0);
521
+ return handles.add({
522
+ file,
523
+ deleteOnClose: !!(flags & FLATSQL_IO_DELETE_ON_CLOSE),
524
+ });
525
+ },
526
+
527
+ read(handle, dst, offset) {
528
+ const entry = handles.get(handle);
529
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
530
+ return entry.file.read(dst, offset);
531
+ },
532
+
533
+ write(handle, src, offset) {
534
+ const entry = handles.get(handle);
535
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
536
+ return entry.file.write(src, offset);
537
+ },
538
+
539
+ truncate(handle, size) {
540
+ const entry = handles.get(handle);
541
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
542
+ return entry.file.truncate(size);
543
+ },
544
+
545
+ /**
546
+ * A durability BARRIER, not a durability guarantee — the underlying store
547
+ * is asynchronous and a wasm import cannot await. Bytes become durable when
548
+ * the caller awaits flush(). Stated plainly because the alternative is a
549
+ * shim that claims fsync semantics it cannot deliver, which is precisely
550
+ * the class of lie this whole task exists to remove.
551
+ */
552
+ sync(handle) {
553
+ const entry = handles.get(handle);
554
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
555
+ return 0;
556
+ },
557
+
558
+ size(handle) {
559
+ const entry = handles.get(handle);
560
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
561
+ return entry.file.length;
562
+ },
563
+
564
+ close(handle) {
565
+ const entry = handles.remove(handle);
566
+ if (!entry) return FLATSQL_IO_ERR_BADHANDLE;
567
+ if (entry.deleteOnClose) {
568
+ entry.file.truncate(0);
569
+ known.delete(entry.file.path);
570
+ }
571
+ return 0;
572
+ },
573
+ };
574
+ }
575
+
576
+ /**
577
+ * Build the `env` import object for a directly-instantiated (WASI) module.
578
+ * The pointer->subarray translation here is the mirror image of the one in
579
+ * cpp/js/flatsql_io_library.js; keeping them in one file each, both feeding the
580
+ * same backend, is what stops the lanes drifting apart.
581
+ */
582
+ export function createFlatSqlIoImports(backend, memoryRef) {
583
+ const heap = () => new Uint8Array(memoryRef().buffer);
584
+ const guard = (fn) => {
585
+ try {
586
+ return fn();
587
+ } catch (_) {
588
+ return FLATSQL_IO_ERR_GENERIC;
589
+ }
590
+ };
591
+
592
+ return {
593
+ flatsql_io_open(pathPtr, pathLen, flags) {
594
+ return guard(() => {
595
+ const bytes = heap().subarray(pathPtr, pathPtr + pathLen);
596
+ return backend.open(new TextDecoder().decode(bytes), flags) | 0;
597
+ });
598
+ },
599
+ flatsql_io_read(handle, dstPtr, len, offset) {
600
+ return guard(() => backend.read(handle, heap().subarray(dstPtr, dstPtr + len), offset) | 0);
601
+ },
602
+ flatsql_io_write(handle, srcPtr, len, offset) {
603
+ return guard(() => backend.write(handle, heap().subarray(srcPtr, srcPtr + len), offset) | 0);
604
+ },
605
+ flatsql_io_truncate(handle, size) {
606
+ return guard(() => backend.truncate(handle, size) | 0);
607
+ },
608
+ flatsql_io_sync(handle) {
609
+ return guard(() => backend.sync(handle) | 0);
610
+ },
611
+ flatsql_io_size(handle) {
612
+ return guard(() => backend.size(handle));
613
+ },
614
+ flatsql_io_close(handle) {
615
+ return guard(() => backend.close(handle) | 0);
616
+ },
617
+ };
618
+ }
Binary file
Binary file