@statewalker/webrun-files-sqlite 0.10.0

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,1253 @@
1
+ //#region ../webrun-files/dist/esm/index.js
2
+ /**
3
+ * The order every `FilesApi.list()` yields: paths compared by Unicode code
4
+ * point, which is the same as comparing their UTF-8 bytes — the order of
5
+ * SQLite's `BINARY` collation and of S3 key listings.
6
+ *
7
+ * Not JavaScript's `<`, which compares UTF-16 code units and so puts a
8
+ * character above U+FFFF (a surrogate pair) before U+E000–U+FFFF.
9
+ */
10
+ function comparePaths(a, b) {
11
+ const length = Math.min(a.length, b.length);
12
+ for (let i = 0; i < length; i++) {
13
+ const x = a.charCodeAt(i);
14
+ const y = b.charCodeAt(i);
15
+ if (x === y) continue;
16
+ const xSurrogate = x >= 55296 && x <= 57343;
17
+ if (xSurrogate !== (y >= 55296 && y <= 57343)) return xSurrogate ? 1 : -1;
18
+ return x - y;
19
+ }
20
+ return a.length - b.length;
21
+ }
22
+ /**
23
+ * Path manipulation utilities for virtual filesystem paths.
24
+ * All paths use forward slashes and start with "/".
25
+ */
26
+ /**
27
+ * Normalizes a file path to a consistent format.
28
+ * - Adds leading slash if missing
29
+ * - Removes trailing slash
30
+ * - Removes `.` segments
31
+ * - Collapses multiple slashes
32
+ */
33
+ function normalizePath(filePath) {
34
+ const segments = filePath.split("/").filter((s) => !!s && s !== ".");
35
+ if (segments.length === 0) return "/";
36
+ return `/${segments.join("/")}`;
37
+ }
38
+ //#endregion
39
+ //#region src/bytes.ts
40
+ /** Blobs arrive as Uint8Array (node:sqlite), ArrayBuffer (Durable Objects) or number[] (D1). */
41
+ function toBytes(value) {
42
+ if (value instanceof Uint8Array) return value;
43
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
44
+ if (Array.isArray(value)) return Uint8Array.from(value);
45
+ if (typeof value === "string") return new TextEncoder().encode(value);
46
+ return /* @__PURE__ */ new Uint8Array(0);
47
+ }
48
+ //#endregion
49
+ //#region src/blocks/byte-reader.ts
50
+ /**
51
+ * Pull bytes from a chunk source in bounded pieces.
52
+ *
53
+ * `read(max)` hands out at most `max` bytes as a zero-copy view of the current
54
+ * source chunk, and pulls the next chunk only when the current one is spent.
55
+ * The source is therefore never read ahead of its consumer: this is the
56
+ * backpressure boundary between a caller's stream and block storage.
57
+ */
58
+ var ByteReader = class {
59
+ #iterator;
60
+ #chunk;
61
+ #done = false;
62
+ constructor(source) {
63
+ this.#iterator = Symbol.asyncIterator in source ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
64
+ }
65
+ /** At most `max` bytes, or `undefined` once the source is exhausted. */
66
+ async read(max) {
67
+ while (!this.#chunk || this.#chunk.length === 0) {
68
+ if (this.#done) return void 0;
69
+ const next = await this.#iterator.next();
70
+ if (next.done) {
71
+ this.#done = true;
72
+ this.#chunk = void 0;
73
+ return;
74
+ }
75
+ this.#chunk = next.value;
76
+ }
77
+ const piece = this.#chunk.subarray(0, max);
78
+ this.#chunk = this.#chunk.subarray(piece.length);
79
+ return piece;
80
+ }
81
+ /** Release the source early, as `break` in a `for await` would. */
82
+ async close() {
83
+ if (this.#done) return;
84
+ this.#done = true;
85
+ this.#chunk = void 0;
86
+ await this.#iterator.return?.();
87
+ }
88
+ };
89
+ /** One Uint8Array from a stream's chunks — only ever used on a single block's worth. */
90
+ async function collectBlock(chunks) {
91
+ const parts = [];
92
+ let total = 0;
93
+ for await (const chunk of chunks) {
94
+ parts.push(chunk);
95
+ total += chunk.length;
96
+ }
97
+ const out = new Uint8Array(total);
98
+ let at = 0;
99
+ for (const part of parts) {
100
+ out.set(part, at);
101
+ at += part.length;
102
+ }
103
+ return out;
104
+ }
105
+ //#endregion
106
+ //#region src/blocks/sha256.ts
107
+ /**
108
+ * Incremental SHA-256 (FIPS 180-4). WebCrypto only hashes a whole buffer, and
109
+ * content here arrives as a stream it must never be collected from.
110
+ */
111
+ const K = new Uint32Array([
112
+ 1116352408,
113
+ 1899447441,
114
+ 3049323471,
115
+ 3921009573,
116
+ 961987163,
117
+ 1508970993,
118
+ 2453635748,
119
+ 2870763221,
120
+ 3624381080,
121
+ 310598401,
122
+ 607225278,
123
+ 1426881987,
124
+ 1925078388,
125
+ 2162078206,
126
+ 2614888103,
127
+ 3248222580,
128
+ 3835390401,
129
+ 4022224774,
130
+ 264347078,
131
+ 604807628,
132
+ 770255983,
133
+ 1249150122,
134
+ 1555081692,
135
+ 1996064986,
136
+ 2554220882,
137
+ 2821834349,
138
+ 2952996808,
139
+ 3210313671,
140
+ 3336571891,
141
+ 3584528711,
142
+ 113926993,
143
+ 338241895,
144
+ 666307205,
145
+ 773529912,
146
+ 1294757372,
147
+ 1396182291,
148
+ 1695183700,
149
+ 1986661051,
150
+ 2177026350,
151
+ 2456956037,
152
+ 2730485921,
153
+ 2820302411,
154
+ 3259730800,
155
+ 3345764771,
156
+ 3516065817,
157
+ 3600352804,
158
+ 4094571909,
159
+ 275423344,
160
+ 430227734,
161
+ 506948616,
162
+ 659060556,
163
+ 883997877,
164
+ 958139571,
165
+ 1322822218,
166
+ 1537002063,
167
+ 1747873779,
168
+ 1955562222,
169
+ 2024104815,
170
+ 2227730452,
171
+ 2361852424,
172
+ 2428436474,
173
+ 2756734187,
174
+ 3204031479,
175
+ 3329325298
176
+ ]);
177
+ var Sha256 = class {
178
+ #state = new Uint32Array([
179
+ 1779033703,
180
+ 3144134277,
181
+ 1013904242,
182
+ 2773480762,
183
+ 1359893119,
184
+ 2600822924,
185
+ 528734635,
186
+ 1541459225
187
+ ]);
188
+ #buffer = /* @__PURE__ */ new Uint8Array(64);
189
+ #words = /* @__PURE__ */ new Uint32Array(80);
190
+ #buffered = 0;
191
+ #length = 0;
192
+ #finished = false;
193
+ update(bytes) {
194
+ if (this.#finished) throw new Error("Sha256: already digested");
195
+ this.#length += bytes.length;
196
+ let at = 0;
197
+ if (this.#buffered > 0) {
198
+ const take = Math.min(64 - this.#buffered, bytes.length);
199
+ this.#buffer.set(bytes.subarray(0, take), this.#buffered);
200
+ this.#buffered += take;
201
+ at = take;
202
+ if (this.#buffered < 64) return this;
203
+ this.#compress(this.#buffer, 0);
204
+ this.#buffered = 0;
205
+ }
206
+ for (; at + 64 <= bytes.length; at += 64) this.#compress(bytes, at);
207
+ if (at < bytes.length) {
208
+ this.#buffer.set(bytes.subarray(at));
209
+ this.#buffered = bytes.length - at;
210
+ }
211
+ return this;
212
+ }
213
+ /** Finalises the hash; the instance cannot be used afterwards. */
214
+ digestHex() {
215
+ if (this.#finished) throw new Error("Sha256: already digested");
216
+ const bitLength = this.#length * 8;
217
+ const padding = new Uint8Array((this.#buffered < 56 ? 56 : 120) - this.#buffered + 8);
218
+ padding[0] = 128;
219
+ const view = new DataView(padding.buffer);
220
+ view.setUint32(padding.length - 8, Math.floor(bitLength / 2 ** 32));
221
+ view.setUint32(padding.length - 4, bitLength >>> 0);
222
+ this.update(padding);
223
+ this.#finished = true;
224
+ let hex = "";
225
+ for (const word of this.#state) hex += word.toString(16).padStart(8, "0");
226
+ return hex;
227
+ }
228
+ #compress(bytes, at) {
229
+ const w = this.#words;
230
+ for (let i = 0; i < 16; i++) {
231
+ const j = at + i * 4;
232
+ w[i] = bytes[j] << 24 | bytes[j + 1] << 16 | bytes[j + 2] << 8 | bytes[j + 3];
233
+ }
234
+ for (let i = 16; i < 64; i++) {
235
+ const a = w[i - 15];
236
+ const b = w[i - 2];
237
+ const s0 = (a >>> 7 | a << 25) ^ (a >>> 18 | a << 14) ^ a >>> 3;
238
+ const s1 = (b >>> 17 | b << 15) ^ (b >>> 19 | b << 13) ^ b >>> 10;
239
+ w[i] = s1 + w[i - 7] + s0 + w[i - 16] | 0;
240
+ }
241
+ const h = this.#state;
242
+ let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], k = h[7];
243
+ for (let i = 0; i < 64; i++) {
244
+ const t1 = k + ((e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
245
+ const t2 = ((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c) | 0;
246
+ k = g;
247
+ g = f;
248
+ f = e;
249
+ e = d + t1 | 0;
250
+ d = c;
251
+ c = b;
252
+ b = a;
253
+ a = t1 + t2 | 0;
254
+ }
255
+ h[0] += a;
256
+ h[1] += b;
257
+ h[2] += c;
258
+ h[3] += d;
259
+ h[4] += e;
260
+ h[5] += f;
261
+ h[6] += g;
262
+ h[7] += k;
263
+ }
264
+ };
265
+ //#endregion
266
+ //#region src/blocks/stream-codec.ts
267
+ /**
268
+ * zlib-wrapped Deflate over Compression Streams — browsers, Node, Deno, Bun,
269
+ * Workers. The source is wrapped in a pull-only ReadableStream, so the
270
+ * transform asks for input only when it has room, and cancelling the output
271
+ * closes the source.
272
+ */
273
+ function webDeflateCodec() {
274
+ if (typeof CompressionStream === "undefined") throw new Error("webDeflateCodec: CompressionStream is not available in this runtime");
275
+ return {
276
+ name: "deflate",
277
+ compress: (input) => iterate(pullStream(input).pipeThrough(asPair(new CompressionStream("deflate")))),
278
+ decompress: (input) => iterate(pullStream(input).pipeThrough(asPair(new DecompressionStream("deflate"))))
279
+ };
280
+ }
281
+ /**
282
+ * zlib-wrapped Deflate through pako's incremental classes, for runtimes without
283
+ * Compression Streams. The module is injected, not imported.
284
+ */
285
+ function pakoDeflateCodec(pako, opts = {}) {
286
+ const options = opts.level === void 0 ? {} : { level: opts.level };
287
+ return {
288
+ name: "deflate",
289
+ compress: (input) => pakoTransform(new pako.Deflate(options), input, true),
290
+ decompress: (input) => pakoTransform(new pako.Inflate(), input, false)
291
+ };
292
+ }
293
+ /**
294
+ * Feed one input chunk at a time and hand on whatever that produced before
295
+ * pulling again. pako's `push` is synchronous, so its output per chunk is
296
+ * bounded by the chunk.
297
+ */
298
+ async function* pakoTransform(stream, input, compressing) {
299
+ let pending = [];
300
+ stream.onData = (chunk) => pending.push(chunk);
301
+ const check = () => {
302
+ if (stream.err) throw new Error(`pako: ${stream.msg || `error ${stream.err}`}`);
303
+ };
304
+ for await (const chunk of input) {
305
+ if (chunk.length === 0) continue;
306
+ stream.push(chunk, false);
307
+ check();
308
+ const out = pending;
309
+ pending = [];
310
+ yield* out;
311
+ }
312
+ if (compressing) {
313
+ stream.push(/* @__PURE__ */ new Uint8Array(0), true);
314
+ check();
315
+ } else if (!stream.ended) throw new Error("pako: truncated deflate stream");
316
+ yield* pending;
317
+ }
318
+ /** Compression streams accept any BufferSource; this pipe only ever writes Uint8Array. */
319
+ function asPair(transform) {
320
+ return transform;
321
+ }
322
+ function pullStream(source) {
323
+ const iterator = source[Symbol.asyncIterator]();
324
+ return new ReadableStream({
325
+ async pull(controller) {
326
+ const next = await iterator.next();
327
+ if (next.done) controller.close();
328
+ else controller.enqueue(next.value);
329
+ },
330
+ async cancel(reason) {
331
+ await iterator.return?.(reason);
332
+ }
333
+ }, { highWaterMark: 0 });
334
+ }
335
+ async function* iterate(stream) {
336
+ const reader = stream.getReader();
337
+ let finished = false;
338
+ try {
339
+ for (;;) {
340
+ const { done, value } = await reader.read();
341
+ if (done) {
342
+ finished = true;
343
+ return;
344
+ }
345
+ yield value;
346
+ }
347
+ } finally {
348
+ if (!finished) await reader.cancel().catch(() => {});
349
+ reader.releaseLock();
350
+ }
351
+ }
352
+ //#endregion
353
+ //#region src/blocks/sqlite-files-api.ts
354
+ const MiB = 1048576;
355
+ /** Rows on Durable Objects and D1 are capped at 2 MB; leave room for deflate's worst case. */
356
+ const MAX_BLOCK_SIZE_LIMIT = 1.5 * MiB;
357
+ const LIST_PAGE = 256;
358
+ /**
359
+ * A FilesApi over three SQLite tables: paths point at immutable contents, and
360
+ * a content is a sequence of bounded, optionally compressed blocks. Reads and
361
+ * writes stream one block at a time, so no file is ever held whole and no row
362
+ * outgrows the 2 MB limit of Durable Objects and D1.
363
+ *
364
+ * Streaming happens outside transactions; every step that changes what a path
365
+ * shows is one transaction, so an interruption leaves at most an unreferenced
366
+ * content, which {@link sweep} removes.
367
+ */
368
+ var SqliteFilesApi = class {
369
+ #sql;
370
+ #codec;
371
+ #blockSize;
372
+ #paths;
373
+ #files;
374
+ #blocks;
375
+ #prefix;
376
+ constructor(sql, opts = {}) {
377
+ const prefix = opts.tablePrefix ?? "fs_";
378
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) throw new Error(`SqliteFilesApi: tablePrefix must be a plain SQL identifier, got ${prefix}`);
379
+ this.#blockSize = opts.blockSize ?? MiB;
380
+ if (!Number.isInteger(this.#blockSize) || this.#blockSize < 1 || this.#blockSize > MAX_BLOCK_SIZE_LIMIT) throw new Error(`SqliteFilesApi: blockSize must be an integer from 1 to ${MAX_BLOCK_SIZE_LIMIT}, got ${this.#blockSize}`);
381
+ this.#sql = sql;
382
+ this.#codec = opts.compression === void 0 ? availableWebCodec() : opts.compression;
383
+ this.#prefix = prefix;
384
+ this.#paths = `${prefix}paths`;
385
+ this.#files = `${prefix}files`;
386
+ this.#blocks = `${prefix}blocks`;
387
+ }
388
+ /** Creates the tables and indexes if missing. Await it before any other call. */
389
+ async init() {
390
+ const statements = [
391
+ `CREATE TABLE IF NOT EXISTS ${this.#files}(
392
+ fid INTEGER PRIMARY KEY,
393
+ size INTEGER,
394
+ compression TEXT NOT NULL,
395
+ block_size INTEGER NOT NULL,
396
+ hash TEXT,
397
+ updated INTEGER NOT NULL
398
+ )`,
399
+ `CREATE TABLE IF NOT EXISTS ${this.#blocks}(
400
+ fid INTEGER NOT NULL,
401
+ shift INTEGER NOT NULL,
402
+ block BLOB NOT NULL,
403
+ PRIMARY KEY (fid, shift)
404
+ ) WITHOUT ROWID`,
405
+ `CREATE TABLE IF NOT EXISTS ${this.#paths}(
406
+ pid INTEGER PRIMARY KEY,
407
+ path TEXT NOT NULL UNIQUE,
408
+ fid INTEGER,
409
+ mtime INTEGER NOT NULL
410
+ )`,
411
+ `CREATE INDEX IF NOT EXISTS ${this.#prefix}paths_fid ON ${this.#paths}(fid)`,
412
+ `CREATE INDEX IF NOT EXISTS ${this.#prefix}files_hash ON ${this.#files}(hash, size)`
413
+ ];
414
+ for (const statement of statements) await this.#sql.run(statement);
415
+ }
416
+ async *read(path, options = {}) {
417
+ options.signal?.throwIfAborted();
418
+ const name = normalizePath(path);
419
+ const entry = await this.#entry(name);
420
+ if (!entry || entry.fid === null || entry.size === null) return;
421
+ const start = Math.max(0, options.start ?? 0);
422
+ const end = Math.min(entry.size, options.length === void 0 ? entry.size : start + options.length);
423
+ if (start >= end) return;
424
+ const { fid, size } = entry;
425
+ const blockSize = entry.block_size ?? this.#blockSize;
426
+ const decoder = this.#decoder(name, entry.compression ?? "none");
427
+ for (let shift = start - start % blockSize; shift < end; shift += blockSize) {
428
+ options.signal?.throwIfAborted();
429
+ const block = await this.#block(fid, shift);
430
+ if (!block) throw new Error(`sqlite-files: ${name} changed during read`);
431
+ const expected = Math.min(blockSize, size - shift);
432
+ const bytes = toBytes(block.block);
433
+ const chunks = decoder ? decoder.decompress(once(bytes)) : once(bytes);
434
+ let offset = shift;
435
+ let stoppedEarly = false;
436
+ for await (const chunk of chunks) {
437
+ if (offset + chunk.length - shift > expected) throw corruptBlock(name, shift, `at least ${offset + chunk.length - shift}`, expected);
438
+ const from = Math.max(0, start - offset);
439
+ const to = Math.min(chunk.length, end - offset);
440
+ offset += chunk.length;
441
+ if (to > from) yield chunk.subarray(from, to);
442
+ if (offset >= end) {
443
+ stoppedEarly = true;
444
+ break;
445
+ }
446
+ }
447
+ if (!stoppedEarly && offset - shift !== expected) throw corruptBlock(name, shift, offset - shift, expected);
448
+ }
449
+ }
450
+ /**
451
+ * Streams the content into a new, invisible content row, one block per
452
+ * transaction, then points the path at it in one transaction. Any failure
453
+ * removes the pending content; a crash leaves it for {@link sweep}.
454
+ */
455
+ async write(path, content) {
456
+ const name = normalizePath(path);
457
+ const compression = this.#codec?.name ?? "none";
458
+ const fid = (await this.#sql.all(`INSERT INTO ${this.#files}(size, compression, block_size, hash, updated)
459
+ VALUES (NULL, ?, ?, NULL, ?) RETURNING fid`, compression, this.#blockSize, Date.now()))[0].fid;
460
+ const limit = this.#blockSize;
461
+ const reader = new ByteReader(content);
462
+ const hash = new Sha256();
463
+ try {
464
+ let size = 0;
465
+ for (;;) {
466
+ const first = await reader.read(limit);
467
+ if (!first) break;
468
+ let raw = 0;
469
+ const pieces = async function* () {
470
+ let piece = first;
471
+ while (piece) {
472
+ hash.update(piece);
473
+ raw += piece.length;
474
+ yield piece;
475
+ if (raw === limit) return;
476
+ piece = await reader.read(limit - raw);
477
+ }
478
+ };
479
+ const block = await collectBlock(this.#codec ? this.#codec.compress(pieces()) : pieces());
480
+ const [alive] = await this.#sql.transaction([{
481
+ sql: `UPDATE ${this.#files} SET updated = ? WHERE fid = ? AND size IS NULL`,
482
+ params: [Date.now(), fid]
483
+ }, {
484
+ sql: `INSERT INTO ${this.#blocks}(fid, shift, block)
485
+ SELECT ?, ?, ? WHERE EXISTS (${this.#pending("?")})`,
486
+ params: [
487
+ fid,
488
+ size,
489
+ block,
490
+ fid
491
+ ]
492
+ }]);
493
+ if (alive === 0) throw sweptDuringWrite(name);
494
+ size += raw;
495
+ }
496
+ const previous = await this.#entry(name);
497
+ const [finished] = await this.#sql.transaction(this.#finishWrite(name, fid, size, hash.digestHex(), compression, previous?.fid ?? null));
498
+ if (finished === 0) throw sweptDuringWrite(name);
499
+ } catch (error) {
500
+ try {
501
+ await reader.close();
502
+ } catch {}
503
+ try {
504
+ await this.#sql.transaction([{
505
+ sql: `DELETE FROM ${this.#files} WHERE fid = ? AND size IS NULL`,
506
+ params: [fid]
507
+ }, ...this.#deleteBlocksOfMissing(fid)]);
508
+ } catch {}
509
+ throw error;
510
+ }
511
+ }
512
+ /**
513
+ * One transaction: finish the content, create the parents, point the path at
514
+ * an identical content if one exists or else at this one, then collect this
515
+ * content if it ended up unused and the path's previous content. Everything
516
+ * after the first statement is conditioned on the content still existing, so
517
+ * a swept write changes nothing.
518
+ */
519
+ #finishWrite(name, fid, size, digest, compression, previous) {
520
+ const now = Date.now();
521
+ const exists = `SELECT 1 FROM ${this.#files} WHERE fid = ?`;
522
+ const statements = [
523
+ {
524
+ sql: `UPDATE ${this.#files} SET size = ?, hash = ?, updated = ? WHERE fid = ? AND size IS NULL`,
525
+ params: [
526
+ size,
527
+ digest,
528
+ now,
529
+ fid
530
+ ]
531
+ },
532
+ ...ancestorsAndSelf$1(parentOf(name)).map((dir) => ({
533
+ sql: `INSERT INTO ${this.#paths}(path, fid, mtime) SELECT ?, NULL, ? WHERE EXISTS (${exists})
534
+ ON CONFLICT(path) DO NOTHING`,
535
+ params: [
536
+ dir,
537
+ now,
538
+ fid
539
+ ]
540
+ })),
541
+ {
542
+ sql: `INSERT INTO ${this.#paths}(path, fid, mtime)
543
+ SELECT ?, COALESCE((
544
+ SELECT f.fid FROM ${this.#files} f
545
+ WHERE f.hash = ? AND f.size = ? AND f.compression = ? AND f.block_size = ? AND f.fid <> ?
546
+ ORDER BY f.fid LIMIT 1
547
+ ), ?), ?
548
+ WHERE EXISTS (${exists})
549
+ ON CONFLICT(path) DO UPDATE SET fid = excluded.fid, mtime = excluded.mtime`,
550
+ params: [
551
+ name,
552
+ digest,
553
+ size,
554
+ compression,
555
+ this.#blockSize,
556
+ fid,
557
+ fid,
558
+ now,
559
+ fid
560
+ ]
561
+ },
562
+ ...this.#collect(fid)
563
+ ];
564
+ if (previous !== null && previous !== fid) statements.push(...this.#collect(previous));
565
+ return statements;
566
+ }
567
+ async mkdir(path) {
568
+ const now = Date.now();
569
+ for (const dir of ancestorsAndSelf$1(normalizePath(path))) await this.#sql.run(`INSERT INTO ${this.#paths}(path, fid, mtime) VALUES (?, NULL, ?) ON CONFLICT(path) DO NOTHING`, dir, now);
570
+ }
571
+ async stats(path) {
572
+ const name = normalizePath(path);
573
+ if (name === "/") return { kind: "directory" };
574
+ const entry = await this.#entry(name);
575
+ return entry && toStats$1(entry);
576
+ }
577
+ async exists(path) {
578
+ return await this.stats(path) !== void 0;
579
+ }
580
+ async *list(path, options = {}) {
581
+ const base = normalizePath(path);
582
+ if ((await this.stats(base))?.kind !== "directory") return;
583
+ const range = descendants$1(base);
584
+ const prefixLength = base === "/" ? 1 : base.length + 1;
585
+ const directOnly = options.recursive ? "" : "AND instr(substr(p.path, ?), '/') = 0";
586
+ let cursor = options.after ?? "";
587
+ for (;;) {
588
+ const params = [...range.params, cursor];
589
+ if (!options.recursive) params.push(prefixLength + 1);
590
+ const rows = await this.#sql.all(`SELECT p.path, p.fid, p.mtime, f.size, f.compression, f.block_size
591
+ FROM ${this.#paths} p LEFT JOIN ${this.#files} f ON f.fid = p.fid
592
+ WHERE ${range.where} AND p.path > ? ${directOnly}
593
+ ORDER BY p.path LIMIT ${LIST_PAGE}`, ...params);
594
+ for (const row of rows) {
595
+ const stats = toStats$1(row);
596
+ if (stats) yield {
597
+ ...stats,
598
+ name: row.path.slice(row.path.lastIndexOf("/") + 1),
599
+ path: row.path
600
+ };
601
+ }
602
+ if (rows.length < LIST_PAGE) return;
603
+ cursor = rows[rows.length - 1].path;
604
+ }
605
+ }
606
+ async remove(path) {
607
+ const counts = await this.#sql.transaction(this.#removeTree(normalizePath(path)));
608
+ return counts[counts.length - 1] > 0;
609
+ }
610
+ /**
611
+ * Deletes available contents and blocks referenced only from inside the tree, then the tree's
612
+ * path rows. Set-based, so it needs no ids read beforehand. `guard` conditions every statement.
613
+ */
614
+ #removeTree(path, guard = NO_GUARD) {
615
+ const inP = selfAndDescendants$1(path, "p");
616
+ const inQ = selfAndDescendants$1(path, "q");
617
+ const referenced = `SELECT p.fid FROM ${this.#paths} p WHERE ${inP.where}`;
618
+ return [
619
+ {
620
+ sql: `DELETE FROM ${this.#files} WHERE size IS NOT NULL
621
+ AND fid IN (${referenced})
622
+ AND NOT EXISTS (
623
+ SELECT 1 FROM ${this.#paths} q WHERE q.fid = ${this.#files}.fid AND NOT ${inQ.where}
624
+ ) ${guard.sql}`,
625
+ params: [
626
+ ...inP.params,
627
+ ...inQ.params,
628
+ ...guard.params
629
+ ]
630
+ },
631
+ {
632
+ sql: `DELETE FROM ${this.#blocks} WHERE fid IN (${referenced})
633
+ AND NOT EXISTS (SELECT 1 FROM ${this.#files} f WHERE f.fid = ${this.#blocks}.fid)
634
+ ${guard.sql}`,
635
+ params: [...inP.params, ...guard.params]
636
+ },
637
+ {
638
+ sql: `DELETE FROM ${this.#paths} AS p WHERE ${inP.where} ${guard.sql}`,
639
+ params: [...inP.params, ...guard.params]
640
+ }
641
+ ];
642
+ }
643
+ async copy(source, target) {
644
+ return this.#transfer(source, target, "copy");
645
+ }
646
+ async move(source, target) {
647
+ return this.#transfer(source, target, "move");
648
+ }
649
+ /**
650
+ * One transaction: replace the target's subtree, create its parents, then copy
651
+ * or rename. Everything before the last statement is conditioned on the source
652
+ * existing, so a source that vanished leaves the target untouched.
653
+ */
654
+ async #transfer(source, target, mode) {
655
+ const from = normalizePath(source);
656
+ const to = normalizePath(target);
657
+ if (from === to) return this.exists(from);
658
+ if (isInside(to, from) || isInside(from, to)) throw new Error(`sqlite-files: cannot ${mode} ${from} to ${to}: one contains the other`);
659
+ const range = selfAndDescendants$1(from, "p");
660
+ const inS = selfAndDescendants$1(from, "s");
661
+ const guard = {
662
+ sql: `AND EXISTS (SELECT 1 FROM ${this.#paths} s WHERE ${inS.where})`,
663
+ params: inS.params
664
+ };
665
+ const now = Date.now();
666
+ const statements = [
667
+ ...this.#removeTree(to, guard),
668
+ ...ancestorsAndSelf$1(parentOf(to)).map((dir) => ({
669
+ sql: `INSERT INTO ${this.#paths}(path, fid, mtime) SELECT ?, NULL, ? WHERE 1 ${guard.sql}
670
+ ON CONFLICT(path) DO NOTHING`,
671
+ params: [
672
+ dir,
673
+ now,
674
+ ...guard.params
675
+ ]
676
+ })),
677
+ mode === "copy" ? {
678
+ sql: `INSERT INTO ${this.#paths}(path, fid, mtime)
679
+ SELECT ? || substr(p.path, ?), p.fid, ? FROM ${this.#paths} p WHERE ${range.where}`,
680
+ params: [
681
+ to,
682
+ from.length + 1,
683
+ now,
684
+ ...range.params
685
+ ]
686
+ } : {
687
+ sql: `UPDATE ${this.#paths} AS p SET path = ? || substr(p.path, ?), mtime = ?
688
+ WHERE ${range.where}`,
689
+ params: [
690
+ to,
691
+ from.length + 1,
692
+ now,
693
+ ...range.params
694
+ ]
695
+ }
696
+ ];
697
+ const counts = await this.#sql.transaction(statements);
698
+ return counts[counts.length - 1] > 0;
699
+ }
700
+ /**
701
+ * Deletes, in one transaction, every content not updated for `olderThan`
702
+ * milliseconds that is still pending or that no path references, with its
703
+ * blocks. Returns how many contents were deleted.
704
+ *
705
+ * A write refreshes its pending content with every block, so only a write
706
+ * stalled longer than `olderThan` can be swept — and it then fails instead of
707
+ * pointing a path at missing content. Never runs on its own.
708
+ */
709
+ async sweep(options) {
710
+ const { olderThan } = options;
711
+ if (!(Number.isFinite(olderThan) && olderThan >= 0)) throw new Error(`SqliteFilesApi.sweep: olderThan must be a finite number >= 0, got ${olderThan}`);
712
+ const cutoff = Date.now() - olderThan;
713
+ const eligible = `f.updated < ? AND NOT EXISTS (
714
+ SELECT 1 FROM ${this.#paths} p WHERE p.fid = f.fid
715
+ )`;
716
+ const [, contents] = await this.#sql.transaction([{
717
+ sql: `DELETE FROM ${this.#blocks} WHERE fid IN (SELECT f.fid FROM ${this.#files} f WHERE ${eligible})`,
718
+ params: [cutoff]
719
+ }, {
720
+ sql: `DELETE FROM ${this.#files} AS f WHERE ${eligible}`,
721
+ params: [cutoff]
722
+ }]);
723
+ return contents;
724
+ }
725
+ async #entry(name) {
726
+ return (await this.#sql.all(`SELECT p.fid, p.mtime, f.size, f.compression, f.block_size
727
+ FROM ${this.#paths} p LEFT JOIN ${this.#files} f ON f.fid = p.fid
728
+ WHERE p.path = ?`, name))[0];
729
+ }
730
+ #decoder(name, compression) {
731
+ if (compression === "none") return null;
732
+ if (this.#codec?.name === compression) return this.#codec;
733
+ if (compression === "deflate") {
734
+ const web = availableWebCodec();
735
+ if (web) return web;
736
+ }
737
+ throw new Error(`sqlite-files: ${name} is compressed with ${compression}, and no codec reads it`);
738
+ }
739
+ async #block(fid, shift) {
740
+ return (await this.#sql.all(`SELECT shift, block FROM ${this.#blocks} WHERE fid = ? AND shift = ?`, fid, shift))[0];
741
+ }
742
+ /** A subquery selecting the content `fid` while it is still pending. */
743
+ #pending(fid) {
744
+ return `SELECT 1 FROM ${this.#files} WHERE fid = ${fid} AND size IS NULL`;
745
+ }
746
+ /**
747
+ * Delete a finished content no path references, then its blocks. Inside a
748
+ * transaction the reference check and the delete cannot be separated by a
749
+ * write that shares the content.
750
+ */
751
+ #collect(fid) {
752
+ return [{
753
+ sql: `DELETE FROM ${this.#files} WHERE fid = ? AND size IS NOT NULL
754
+ AND NOT EXISTS (SELECT 1 FROM ${this.#paths} WHERE fid = ?)`,
755
+ params: [fid, fid]
756
+ }, ...this.#deleteBlocksOfMissing(fid)];
757
+ }
758
+ #deleteBlocksOfMissing(fid) {
759
+ return [{
760
+ sql: `DELETE FROM ${this.#blocks} WHERE fid = ?
761
+ AND NOT EXISTS (SELECT 1 FROM ${this.#files} WHERE fid = ?)`,
762
+ params: [fid, fid]
763
+ }];
764
+ }
765
+ };
766
+ const NO_GUARD = {
767
+ sql: "",
768
+ params: []
769
+ };
770
+ function sweptDuringWrite(name) {
771
+ return /* @__PURE__ */ new Error(`sqlite-files: ${name} content was swept during write`);
772
+ }
773
+ function corruptBlock(name, shift, holds, expected) {
774
+ return /* @__PURE__ */ new Error(`sqlite-files: ${name} block at ${shift} holds ${holds} bytes, expected ${expected}`);
775
+ }
776
+ function availableWebCodec() {
777
+ return typeof CompressionStream === "undefined" ? null : webDeflateCodec();
778
+ }
779
+ function toStats$1(row) {
780
+ if (row.fid === null) return { kind: "directory" };
781
+ if (row.size === null) return void 0;
782
+ return {
783
+ kind: "file",
784
+ size: row.size,
785
+ lastModified: row.mtime
786
+ };
787
+ }
788
+ async function* once(bytes) {
789
+ yield bytes;
790
+ }
791
+ /** Rows strictly under `path` as a range on the unique index (case-sensitive, unlike LIKE). */
792
+ function descendants$1(path, alias = "p") {
793
+ if (path === "/") return {
794
+ where: "1",
795
+ params: []
796
+ };
797
+ return {
798
+ where: `(${alias}.path > ? AND ${alias}.path < ?)`,
799
+ params: [`${path}/`, `${path}0`]
800
+ };
801
+ }
802
+ function selfAndDescendants$1(path, alias = "p") {
803
+ if (path === "/") return descendants$1(path, alias);
804
+ const range = descendants$1(path, alias);
805
+ return {
806
+ where: `(${alias}.path = ? OR ${range.where})`,
807
+ params: [path, ...range.params]
808
+ };
809
+ }
810
+ function isInside(path, ancestor) {
811
+ return ancestor === "/" ? path !== "/" : path.startsWith(`${ancestor}/`);
812
+ }
813
+ function parentOf(path) {
814
+ const i = path.lastIndexOf("/");
815
+ return i <= 0 ? "/" : path.slice(0, i);
816
+ }
817
+ /** "/a/b" → ["/a", "/a/b"]; the root yields nothing. */
818
+ function ancestorsAndSelf$1(path) {
819
+ const parts = path.split("/").filter(Boolean);
820
+ return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
821
+ }
822
+ //#endregion
823
+ //#region src/codec.ts
824
+ const MIN_SIZE = 64;
825
+ /** Compression Streams — browsers, Node, Workers. No dependency. */
826
+ function webCodec(opts = {}) {
827
+ if (typeof CompressionStream === "undefined") throw new Error("webCodec: CompressionStream is not available in this runtime");
828
+ return {
829
+ name: "web",
830
+ minSize: opts.minSize ?? MIN_SIZE,
831
+ deflate: (bytes) => pipe(bytes, new CompressionStream("deflate")),
832
+ inflate: (data) => pipe(data, new DecompressionStream("deflate"))
833
+ };
834
+ }
835
+ /**
836
+ * pako — synchronous, for runtimes without a usable CompressionStream. The
837
+ * module is injected so this package does not depend on it:
838
+ *
839
+ * import * as pako from "pako";
840
+ * new SqlarFilesApi(driver, { codec: pakoCodec(pako) });
841
+ */
842
+ function pakoCodec(pako, opts = {}) {
843
+ const deflateOptions = opts.level === void 0 ? {} : { level: opts.level };
844
+ return {
845
+ name: "pako",
846
+ minSize: opts.minSize ?? MIN_SIZE,
847
+ deflate: (bytes) => pako.deflate(bytes, deflateOptions),
848
+ inflate: (data) => pako.inflate(data)
849
+ };
850
+ }
851
+ /**
852
+ * No compression — still a valid archive. Reading a compressed row through it
853
+ * throws rather than returning deflate bytes as file content.
854
+ */
855
+ function rawCodec() {
856
+ return {
857
+ name: "raw",
858
+ minSize: Number.POSITIVE_INFINITY,
859
+ deflate: (bytes) => bytes,
860
+ inflate() {
861
+ throw new Error("rawCodec cannot inflate: this archive holds compressed rows");
862
+ }
863
+ };
864
+ }
865
+ /** webCodec where CompressionStream exists, rawCodec elsewhere. */
866
+ function defaultCodec() {
867
+ return typeof CompressionStream === "undefined" ? rawCodec() : webCodec();
868
+ }
869
+ async function pipe(bytes, transform) {
870
+ const stream = new Blob([bytes]).stream().pipeThrough(transform);
871
+ return new Uint8Array(await new Response(stream).arrayBuffer());
872
+ }
873
+ //#endregion
874
+ //#region src/drivers.ts
875
+ /** node:sqlite — local development, tests and any Node deployment. */
876
+ var NodeSqlDriver = class {
877
+ #db;
878
+ constructor(db) {
879
+ this.#db = db;
880
+ }
881
+ all(sql, ...params) {
882
+ return this.#db.prepare(sql).all(...params);
883
+ }
884
+ run(sql, ...params) {
885
+ this.#db.prepare(sql).run(...params);
886
+ }
887
+ /** Synchronous from BEGIN to COMMIT, so no other call can interleave on the connection. */
888
+ transaction(statements) {
889
+ this.#db.exec("BEGIN IMMEDIATE");
890
+ try {
891
+ const counts = statements.map(({ sql, params = [] }) => Number(this.#db.prepare(sql).run(...params).changes));
892
+ this.#db.exec("COMMIT");
893
+ return counts;
894
+ } catch (error) {
895
+ this.#db.exec("ROLLBACK");
896
+ throw error;
897
+ }
898
+ }
899
+ };
900
+ /**
901
+ * Durable Object SQLite storage: pass `ctx.storage`. Synchronous; BLOBs come
902
+ * back as ArrayBuffer. Transactions use `transactionSync`, since `sql.exec`
903
+ * rejects `BEGIN`.
904
+ */
905
+ var DoSqlDriver = class {
906
+ #storage;
907
+ constructor(storage) {
908
+ this.#storage = storage;
909
+ }
910
+ all(sql, ...params) {
911
+ return this.#storage.sql.exec(sql, ...params).toArray();
912
+ }
913
+ run(sql, ...params) {
914
+ this.#storage.sql.exec(sql, ...params).toArray();
915
+ }
916
+ /**
917
+ * Counts come from SQLite's `changes()` on the same connection, not from the
918
+ * cursor's `rowsWritten`, which also counts every index row a write touches.
919
+ */
920
+ transaction(statements) {
921
+ return this.#storage.transactionSync(() => statements.map(({ sql, params = [] }) => {
922
+ this.#storage.sql.exec(sql, ...params).toArray();
923
+ const [row] = this.#storage.sql.exec("SELECT changes() AS n").toArray();
924
+ return row.n;
925
+ }));
926
+ }
927
+ };
928
+ /** Cloudflare D1. Asynchronous; BLOBs come back as number[]; transactions are `batch()`. */
929
+ var D1SqlDriver = class {
930
+ #db;
931
+ constructor(db) {
932
+ this.#db = db;
933
+ }
934
+ async all(sql, ...params) {
935
+ const { results } = await this.#db.prepare(sql).bind(...params).all();
936
+ return results;
937
+ }
938
+ async run(sql, ...params) {
939
+ await this.#db.prepare(sql).bind(...params).run();
940
+ }
941
+ async transaction(statements) {
942
+ if (statements.length === 0) return [];
943
+ return (await this.#db.batch(statements.map(({ sql, params = [] }) => this.#db.prepare(sql).bind(...params)))).map((result) => result.meta.changes);
944
+ }
945
+ };
946
+ //#endregion
947
+ //#region src/sqlar-files-api.ts
948
+ /**
949
+ * The table, verbatim from the SQLite Archive specification. Five columns,
950
+ * `name` as the primary key and nothing more: any extra column, index or table
951
+ * and other SQLAR tools may not recognise the archive.
952
+ */
953
+ const SCHEMA = `CREATE TABLE IF NOT EXISTS sqlar(
954
+ name TEXT PRIMARY KEY, -- name of the file
955
+ mode INT, -- access permissions
956
+ mtime INT, -- last modification time
957
+ sz INT, -- original file size
958
+ data BLOB -- compressed content
959
+ )`;
960
+ const FILE_MODE = 33188;
961
+ const DIR_MODE = 16877;
962
+ const LINK_MODE = 41471;
963
+ /** Links followed before a path counts as unresolvable (POSIX ELOOP territory). */
964
+ const MAX_SYMLINK_HOPS = 8;
965
+ /**
966
+ * A FilesApi whose whole state is one SQLite Archive table.
967
+ *
968
+ * SQLAR can also hold symlinks, which `FileKind` cannot express. The FilesApi
969
+ * view follows them the way POSIX `stat()` does — an unresolvable link is a
970
+ * missing path — and {@link symlink} / {@link readlink} expose the link itself.
971
+ */
972
+ var SqlarFilesApi = class {
973
+ #sql;
974
+ #codec;
975
+ constructor(sql, opts = {}) {
976
+ this.#sql = sql;
977
+ this.#codec = opts.codec ?? defaultCodec();
978
+ }
979
+ /** Creates the table if it is missing. Await it before any other call. */
980
+ async init() {
981
+ await this.#sql.run(SCHEMA);
982
+ }
983
+ /** A missing path or a directory yields nothing; a file yields one chunk. */
984
+ async *read(path, options = {}) {
985
+ options.signal?.throwIfAborted();
986
+ const resolved = await this.#resolve(toName(path));
987
+ if (resolved?.stats.kind !== "file") return;
988
+ const rows = await this.#sql.all("SELECT data FROM sqlar WHERE name = ?", resolved.name);
989
+ const content = await this.#content(resolved.name, resolved.stats.size, toBytes(rows[0]?.data));
990
+ options.signal?.throwIfAborted();
991
+ const start = options.start ?? 0;
992
+ const end = options.length === void 0 ? content.byteLength : start + options.length;
993
+ const chunk = content.subarray(start, Math.min(end, content.byteLength));
994
+ if (chunk.byteLength > 0) yield chunk;
995
+ }
996
+ async write(path, content) {
997
+ const name = toName(path);
998
+ const bytes = await concat(content);
999
+ await this.mkdir(parentName(name));
1000
+ let data = bytes;
1001
+ if (bytes.byteLength >= this.#codec.minSize) {
1002
+ const deflated = await this.#codec.deflate(bytes);
1003
+ if (deflated.byteLength < bytes.byteLength) data = deflated;
1004
+ }
1005
+ await this.#put(name, FILE_MODE, bytes.byteLength, data);
1006
+ }
1007
+ /** `sz = 0` with `data IS NULL` is the format's only marker for a directory. */
1008
+ async mkdir(path) {
1009
+ const name = toName(path);
1010
+ for (const dir of ancestorsAndSelf(name)) if (!await this.#row(dir)) await this.#put(dir, DIR_MODE, 0, null);
1011
+ }
1012
+ /** Writes a symlink row: `sz = -1`, the target verbatim as TEXT. */
1013
+ async symlink(path, target) {
1014
+ const name = toName(path);
1015
+ await this.mkdir(parentName(name));
1016
+ await this.#put(name, LINK_MODE, -1, target);
1017
+ }
1018
+ /** The stored target of a symlink, or `undefined` for anything else. */
1019
+ async readlink(path) {
1020
+ const row = await this.#row(toName(path));
1021
+ return row && isSymlink(row) ? linkTarget(row) : void 0;
1022
+ }
1023
+ async stats(path) {
1024
+ return (await this.#resolve(toName(path)))?.stats;
1025
+ }
1026
+ async exists(path) {
1027
+ return await this.#resolve(toName(path)) !== void 0;
1028
+ }
1029
+ /**
1030
+ * SQLAR is a flat keyspace, so the tree is derived from names: a path that
1031
+ * only exists as a prefix of other names is yielded as a directory, once,
1032
+ * before anything under it. Link entries report their target's stats; a
1033
+ * recursive listing never descends through one, which rules out cycles.
1034
+ */
1035
+ /**
1036
+ * Rows come back in name order, but implicit directories and resolved links
1037
+ * do not sit where `comparePaths` puts them, so the entries are collected,
1038
+ * sorted and filtered by `after`. This backend already loads every row of
1039
+ * the directory; sorting them costs no extra reads.
1040
+ */
1041
+ async *list(path, options = {}) {
1042
+ const entries = [];
1043
+ for await (const entry of this.#listUnordered(path, options)) entries.push(entry);
1044
+ entries.sort((a, b) => comparePaths(a.path, b.path));
1045
+ for (const entry of entries) if (options.after === void 0 || comparePaths(entry.path, options.after) > 0) yield entry;
1046
+ }
1047
+ async *#listUnordered(path, options) {
1048
+ const requested = toName(path);
1049
+ const resolved = await this.#resolve(requested);
1050
+ if (resolved?.stats.kind !== "directory") return;
1051
+ const base = resolved.name;
1052
+ const range = descendants(base);
1053
+ const rows = await this.#sql.all(`SELECT ${ROW_COLUMNS} FROM sqlar WHERE ${range.where} ORDER BY name`, ...range.params);
1054
+ const prefixLength = base === "" ? 0 : base.length + 1;
1055
+ const seen = /* @__PURE__ */ new Set();
1056
+ for (const row of rows) {
1057
+ const relative = row.name.slice(prefixLength);
1058
+ const segments = relative.split("/");
1059
+ const depth = options.recursive ? segments.length : 1;
1060
+ for (let i = 1; i < Math.min(depth + 1, segments.length); i++) {
1061
+ const dir = segments.slice(0, i).join("/");
1062
+ if (seen.has(dir)) continue;
1063
+ seen.add(dir);
1064
+ yield {
1065
+ kind: "directory",
1066
+ name: segments[i - 1],
1067
+ path: `/${joinName(requested, dir)}`
1068
+ };
1069
+ }
1070
+ if (segments.length > depth || seen.has(relative)) continue;
1071
+ seen.add(relative);
1072
+ const stats = isSymlink(row) ? (await this.#resolve(row.name))?.stats : toStats(row);
1073
+ if (!stats) continue;
1074
+ yield {
1075
+ ...stats,
1076
+ name: segments[segments.length - 1],
1077
+ path: `/${joinName(requested, relative)}`
1078
+ };
1079
+ }
1080
+ }
1081
+ async remove(path) {
1082
+ const range = selfAndDescendants(toName(path));
1083
+ if ((await this.#sql.all(`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`, ...range.params)).length === 0) return false;
1084
+ await this.#sql.run(`DELETE FROM sqlar WHERE ${range.where}`, ...range.params);
1085
+ return true;
1086
+ }
1087
+ /**
1088
+ * Rows are copied verbatim — mode, sz and the stored data — so a compressed
1089
+ * row is never inflated and deflated again. Only mtime is fresh.
1090
+ */
1091
+ async copy(source, target) {
1092
+ const from = toName(source);
1093
+ const to = toName(target);
1094
+ const range = selfAndDescendants(from);
1095
+ const rows = await this.#sql.all(`SELECT name, mode, sz, typeof(data) AS dtype, data FROM sqlar WHERE ${range.where}`, ...range.params);
1096
+ if (rows.length === 0) return false;
1097
+ await this.mkdir(parentName(to));
1098
+ for (const row of rows) {
1099
+ const name = joinName(to, row.name.slice(from.length).replace(/^\//, ""));
1100
+ await this.#put(name, row.mode, row.sz, storedData(row.dtype, row.data));
1101
+ }
1102
+ return true;
1103
+ }
1104
+ async move(source, target) {
1105
+ if (!await this.copy(source, target)) return false;
1106
+ await this.remove(source);
1107
+ return true;
1108
+ }
1109
+ /** `length(data) = sz` is plaintext, `< sz` is compressed, `> sz` is corrupt. */
1110
+ async #content(name, sz, data) {
1111
+ if (data.byteLength === sz) return data;
1112
+ if (data.byteLength > sz) throw new Error(`sqlar: ${name} has length(data)=${data.byteLength} > sz=${sz}`);
1113
+ let inflated;
1114
+ try {
1115
+ inflated = await this.#codec.inflate(data);
1116
+ } catch (cause) {
1117
+ const reason = cause instanceof Error && cause.message ? cause.message : String(cause);
1118
+ throw new Error(`sqlar: ${name} cannot be inflated: ${reason}`, { cause });
1119
+ }
1120
+ if (inflated.byteLength !== sz) throw new Error(`sqlar: ${name} inflated ${inflated.byteLength} bytes, sz ${sz}`);
1121
+ return inflated;
1122
+ }
1123
+ /**
1124
+ * The entry a stored name leads to, following symlinks in the final
1125
+ * component. `undefined` for a missing path, a dangling link or a chain
1126
+ * longer than {@link MAX_SYMLINK_HOPS}.
1127
+ */
1128
+ async #resolve(start) {
1129
+ let name = start;
1130
+ for (let hops = 0;; hops++) {
1131
+ if (name === "") return {
1132
+ name,
1133
+ stats: { kind: "directory" }
1134
+ };
1135
+ const row = await this.#row(name);
1136
+ if (!row) return await this.#hasDescendants(name) ? {
1137
+ name,
1138
+ stats: { kind: "directory" }
1139
+ } : void 0;
1140
+ if (!isSymlink(row)) return {
1141
+ name,
1142
+ stats: toStats(row)
1143
+ };
1144
+ if (hops === MAX_SYMLINK_HOPS) return void 0;
1145
+ name = resolveTarget(row.name, linkTarget(row));
1146
+ }
1147
+ }
1148
+ async #row(name) {
1149
+ return (await this.#sql.all(`SELECT ${ROW_COLUMNS} FROM sqlar WHERE name = ?`, name))[0];
1150
+ }
1151
+ async #hasDescendants(name) {
1152
+ const range = descendants(name);
1153
+ return (await this.#sql.all(`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`, ...range.params)).length > 0;
1154
+ }
1155
+ async #put(name, mode, sz, data) {
1156
+ await this.#sql.run(`INSERT INTO sqlar (name, mode, mtime, sz, data) VALUES (?, ?, ?, ?, ?)
1157
+ ON CONFLICT(name) DO UPDATE SET
1158
+ mode = excluded.mode, mtime = excluded.mtime, sz = excluded.sz, data = excluded.data`, name, mode, Math.floor(Date.now() / 1e3), sz, data);
1159
+ }
1160
+ };
1161
+ const ROW_COLUMNS = "name, mode, mtime, sz, typeof(data) AS dtype, CASE WHEN sz = -1 THEN data END AS target";
1162
+ /** Classification follows sz and data only: other writers may not set mode's type bits. */
1163
+ function isSymlink(row) {
1164
+ return row.sz === -1;
1165
+ }
1166
+ function toStats(row) {
1167
+ if (row.sz === 0 && row.dtype === "null") return { kind: "directory" };
1168
+ return {
1169
+ kind: "file",
1170
+ size: row.sz,
1171
+ lastModified: row.mtime * 1e3
1172
+ };
1173
+ }
1174
+ /**
1175
+ * Every name strictly under `name`, as a range on the primary key. `LIKE`
1176
+ * would be shorter and wrong: it is case-insensitive for ASCII, so "a/%" also
1177
+ * matches "A/x", and it cannot use the index. "0" is the character after "/".
1178
+ */
1179
+ function descendants(name) {
1180
+ if (name === "") return {
1181
+ where: "1",
1182
+ params: []
1183
+ };
1184
+ return {
1185
+ where: "(name > ? AND name < ?)",
1186
+ params: [`${name}/`, `${name}0`]
1187
+ };
1188
+ }
1189
+ function selfAndDescendants(name) {
1190
+ if (name === "") return descendants(name);
1191
+ const range = descendants(name);
1192
+ return {
1193
+ where: `(name = ? OR ${range.where})`,
1194
+ params: [name, ...range.params]
1195
+ };
1196
+ }
1197
+ /** A FilesApi path as a stored name: normalised, without the leading slash. The root is "". */
1198
+ function toName(path) {
1199
+ return normalizePath(path).slice(1);
1200
+ }
1201
+ function joinName(base, rest) {
1202
+ if (base === "") return rest;
1203
+ return rest === "" ? base : `${base}/${rest}`;
1204
+ }
1205
+ function parentName(name) {
1206
+ const i = name.lastIndexOf("/");
1207
+ return i === -1 ? "" : name.slice(0, i);
1208
+ }
1209
+ function linkTarget(row) {
1210
+ return typeof row.target === "string" ? row.target : new TextDecoder().decode(toBytes(row.target));
1211
+ }
1212
+ /**
1213
+ * A link target as a stored name: absolute from the root, relative from the
1214
+ * link's parent, with "." and ".." collapsed.
1215
+ */
1216
+ function resolveTarget(linkName, target) {
1217
+ const out = target.startsWith("/") ? [] : parentName(linkName).split("/").filter(Boolean);
1218
+ for (const segment of target.split("/")) {
1219
+ if (segment === "" || segment === ".") continue;
1220
+ if (segment === "..") out.pop();
1221
+ else out.push(segment);
1222
+ }
1223
+ return out.join("/");
1224
+ }
1225
+ /** A selected `data` value, ready to bind again: NULL, TEXT or a byte blob. */
1226
+ function storedData(dtype, data) {
1227
+ if (dtype === "null") return null;
1228
+ if (dtype === "text") return typeof data === "string" ? data : new TextDecoder().decode(toBytes(data));
1229
+ return toBytes(data);
1230
+ }
1231
+ /** "a/b/c" → ["a", "a/b", "a/b/c"]; the root yields nothing. */
1232
+ function ancestorsAndSelf(name) {
1233
+ if (name === "") return [];
1234
+ const parts = name.split("/");
1235
+ return parts.map((_, i) => parts.slice(0, i + 1).join("/"));
1236
+ }
1237
+ async function concat(content) {
1238
+ const chunks = [];
1239
+ let total = 0;
1240
+ for await (const chunk of content) {
1241
+ chunks.push(chunk);
1242
+ total += chunk.byteLength;
1243
+ }
1244
+ const out = new Uint8Array(total);
1245
+ let at = 0;
1246
+ for (const chunk of chunks) {
1247
+ out.set(chunk, at);
1248
+ at += chunk.byteLength;
1249
+ }
1250
+ return out;
1251
+ }
1252
+ //#endregion
1253
+ export { D1SqlDriver, DoSqlDriver, NodeSqlDriver, SqlarFilesApi, SqliteFilesApi, defaultCodec, pakoCodec, pakoDeflateCodec, rawCodec, webCodec, webDeflateCodec };