@statewalker/webrun-files-sqlite 0.10.2 → 0.11.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.
@@ -1,4 +1,19 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region ../../node_modules/.pnpm/@statewalker+db-sqlite@0.1.0/node_modules/@statewalker/db-sqlite/dist/index.mjs
3
+ /** Key under which a SQLite Db carries its transaction hook. Shared across bundles via Symbol.for. */
4
+ const SQLITE_TRANSACTION = Symbol.for("@statewalker/db-sqlite:transaction");
5
+ const NO_HOOK = "inTransaction: this Db has no SQLite transaction hook. It is not a @statewalker/db-sqlite* Db, or it wraps one (a wrapper object loses the hook - pass the driver's Db itself), or it is the handle of a running transaction (nesting is not supported).";
6
+ /**
7
+ * Run `cb` in one SQLite transaction: every statement issued through `tx` commits together, or
8
+ * none does if `cb` throws. Other calls on `db` wait until the transaction ends. `cb` must not do
9
+ * external I/O and must be idempotent: a Durable Object may run it more than once.
10
+ */
11
+ function inTransaction(db, cb) {
12
+ const hook = db[SQLITE_TRANSACTION];
13
+ if (typeof hook !== "function") return Promise.reject(/* @__PURE__ */ new Error(NO_HOOK));
14
+ return hook(cb);
15
+ }
16
+ //#endregion
2
17
  //#region ../webrun-files/dist/esm/index.js
3
18
  /**
4
19
  * The order every `FilesApi.list()` yields: paths compared by Unicode code
@@ -38,15 +53,31 @@ function normalizePath(filePath) {
38
53
  }
39
54
  //#endregion
40
55
  //#region src/bytes.ts
41
- /** Blobs arrive as Uint8Array (node:sqlite), ArrayBuffer (Durable Objects) or number[] (D1). */
56
+ /** BLOBs arrive as Uint8Array from every @statewalker/db-sqlite driver; SQLAR may hold TEXT. */
42
57
  function toBytes(value) {
43
58
  if (value instanceof Uint8Array) return value;
44
- if (value instanceof ArrayBuffer) return new Uint8Array(value);
45
- if (Array.isArray(value)) return Uint8Array.from(value);
46
59
  if (typeof value === "string") return new TextEncoder().encode(value);
47
60
  return /* @__PURE__ */ new Uint8Array(0);
48
61
  }
49
62
  //#endregion
63
+ //#region src/run-statements.ts
64
+ /**
65
+ * Runs the statements in order on a transaction's Db and returns, per
66
+ * statement, the rows it changed — zero exactly when it changed nothing. The
67
+ * count is SQLite's `changes()` on the same connection, so index writes are
68
+ * not counted. Call it only inside `inTransaction`; it does nothing but SQL,
69
+ * so a retried transaction callback is safe.
70
+ */
71
+ async function runStatements(tx, statements) {
72
+ const counts = [];
73
+ for (const { sql, params = [] } of statements) {
74
+ await tx.query(sql, params);
75
+ const [row] = await tx.query("SELECT changes() AS n");
76
+ counts.push(Number(row.n));
77
+ }
78
+ return counts;
79
+ }
80
+ //#endregion
50
81
  //#region src/blocks/byte-reader.ts
51
82
  /**
52
83
  * Pull bytes from a chunk source in bounded pieces.
@@ -353,33 +384,33 @@ async function* iterate(stream) {
353
384
  //#endregion
354
385
  //#region src/blocks/sqlite-files-api.ts
355
386
  const MiB = 1048576;
356
- /** Rows on Durable Objects and D1 are capped at 2 MB; leave room for deflate's worst case. */
387
+ /** Rows on Durable Objects are capped at 2 MB; leave room for deflate's worst case. */
357
388
  const MAX_BLOCK_SIZE_LIMIT = 1.5 * MiB;
358
389
  const LIST_PAGE = 256;
359
390
  /**
360
391
  * A FilesApi over three SQLite tables: paths point at immutable contents, and
361
392
  * a content is a sequence of bounded, optionally compressed blocks. Reads and
362
393
  * writes stream one block at a time, so no file is ever held whole and no row
363
- * outgrows the 2 MB limit of Durable Objects and D1.
394
+ * outgrows the 2 MB row limit of Durable Objects.
364
395
  *
365
396
  * Streaming happens outside transactions; every step that changes what a path
366
397
  * shows is one transaction, so an interruption leaves at most an unreferenced
367
398
  * content, which {@link sweep} removes.
368
399
  */
369
400
  var SqliteFilesApi = class {
370
- #sql;
401
+ #db;
371
402
  #codec;
372
403
  #blockSize;
373
404
  #paths;
374
405
  #files;
375
406
  #blocks;
376
407
  #prefix;
377
- constructor(sql, opts = {}) {
408
+ constructor(db, opts = {}) {
378
409
  const prefix = opts.tablePrefix ?? "fs_";
379
410
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) throw new Error(`SqliteFilesApi: tablePrefix must be a plain SQL identifier, got ${prefix}`);
380
411
  this.#blockSize = opts.blockSize ?? MiB;
381
412
  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}`);
382
- this.#sql = sql;
413
+ this.#db = db;
383
414
  this.#codec = opts.compression === void 0 ? availableWebCodec() : opts.compression;
384
415
  this.#prefix = prefix;
385
416
  this.#paths = `${prefix}paths`;
@@ -412,7 +443,7 @@ var SqliteFilesApi = class {
412
443
  `CREATE INDEX IF NOT EXISTS ${this.#prefix}paths_fid ON ${this.#paths}(fid)`,
413
444
  `CREATE INDEX IF NOT EXISTS ${this.#prefix}files_hash ON ${this.#files}(hash, size)`
414
445
  ];
415
- for (const statement of statements) await this.#sql.run(statement);
446
+ for (const statement of statements) await this.#db.query(statement);
416
447
  }
417
448
  async *read(path, options = {}) {
418
449
  options.signal?.throwIfAborted();
@@ -456,8 +487,12 @@ var SqliteFilesApi = class {
456
487
  async write(path, content) {
457
488
  const name = normalizePath(path);
458
489
  const compression = this.#codec?.name ?? "none";
459
- const fid = (await this.#sql.all(`INSERT INTO ${this.#files}(size, compression, block_size, hash, updated)
460
- VALUES (NULL, ?, ?, NULL, ?) RETURNING fid`, compression, this.#blockSize, Date.now()))[0].fid;
490
+ const fid = await inTransaction(this.#db, async (tx) => (await tx.query(`INSERT INTO ${this.#files}(size, compression, block_size, hash, updated)
491
+ VALUES (NULL, ?, ?, NULL, ?) RETURNING fid`, [
492
+ compression,
493
+ this.#blockSize,
494
+ Date.now()
495
+ ]))[0].fid);
461
496
  const limit = this.#blockSize;
462
497
  const reader = new ByteReader(content);
463
498
  const hash = new Sha256();
@@ -478,7 +513,7 @@ var SqliteFilesApi = class {
478
513
  }
479
514
  };
480
515
  const block = await collectBlock(this.#codec ? this.#codec.compress(pieces()) : pieces());
481
- const [alive] = await this.#sql.transaction([{
516
+ const [alive] = await this.#transaction([{
482
517
  sql: `UPDATE ${this.#files} SET updated = ? WHERE fid = ? AND size IS NULL`,
483
518
  params: [Date.now(), fid]
484
519
  }, {
@@ -495,14 +530,14 @@ var SqliteFilesApi = class {
495
530
  size += raw;
496
531
  }
497
532
  const previous = await this.#entry(name);
498
- const [finished] = await this.#sql.transaction(this.#finishWrite(name, fid, size, hash.digestHex(), compression, previous?.fid ?? null));
533
+ const [finished] = await this.#transaction(this.#finishWrite(name, fid, size, hash.digestHex(), compression, previous?.fid ?? null));
499
534
  if (finished === 0) throw sweptDuringWrite(name);
500
535
  } catch (error) {
501
536
  try {
502
537
  await reader.close();
503
538
  } catch {}
504
539
  try {
505
- await this.#sql.transaction([{
540
+ await this.#transaction([{
506
541
  sql: `DELETE FROM ${this.#files} WHERE fid = ? AND size IS NULL`,
507
542
  params: [fid]
508
543
  }, ...this.#deleteBlocksOfMissing(fid)]);
@@ -567,7 +602,7 @@ var SqliteFilesApi = class {
567
602
  }
568
603
  async mkdir(path) {
569
604
  const now = Date.now();
570
- 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);
605
+ for (const dir of ancestorsAndSelf$1(normalizePath(path))) await this.#db.query(`INSERT INTO ${this.#paths}(path, fid, mtime) VALUES (?, NULL, ?) ON CONFLICT(path) DO NOTHING`, [dir, now]);
571
606
  }
572
607
  async stats(path) {
573
608
  const name = normalizePath(path);
@@ -588,10 +623,10 @@ var SqliteFilesApi = class {
588
623
  for (;;) {
589
624
  const params = [...range.params, cursor];
590
625
  if (!options.recursive) params.push(prefixLength + 1);
591
- const rows = await this.#sql.all(`SELECT p.path, p.fid, p.mtime, f.size, f.compression, f.block_size
626
+ const rows = await this.#db.query(`SELECT p.path, p.fid, p.mtime, f.size, f.compression, f.block_size
592
627
  FROM ${this.#paths} p LEFT JOIN ${this.#files} f ON f.fid = p.fid
593
628
  WHERE ${range.where} AND p.path > ? ${directOnly}
594
- ORDER BY p.path LIMIT ${LIST_PAGE}`, ...params);
629
+ ORDER BY p.path LIMIT ${LIST_PAGE}`, params);
595
630
  for (const row of rows) {
596
631
  const stats = toStats$1(row);
597
632
  if (stats) yield {
@@ -605,7 +640,7 @@ var SqliteFilesApi = class {
605
640
  }
606
641
  }
607
642
  async remove(path) {
608
- const counts = await this.#sql.transaction(this.#removeTree(normalizePath(path)));
643
+ const counts = await this.#transaction(this.#removeTree(normalizePath(path)));
609
644
  return counts[counts.length - 1] > 0;
610
645
  }
611
646
  /**
@@ -695,7 +730,7 @@ var SqliteFilesApi = class {
695
730
  ]
696
731
  }
697
732
  ];
698
- const counts = await this.#sql.transaction(statements);
733
+ const counts = await this.#transaction(statements);
699
734
  return counts[counts.length - 1] > 0;
700
735
  }
701
736
  /**
@@ -714,7 +749,7 @@ var SqliteFilesApi = class {
714
749
  const eligible = `f.updated < ? AND NOT EXISTS (
715
750
  SELECT 1 FROM ${this.#paths} p WHERE p.fid = f.fid
716
751
  )`;
717
- const [, contents] = await this.#sql.transaction([{
752
+ const [, contents] = await this.#transaction([{
718
753
  sql: `DELETE FROM ${this.#blocks} WHERE fid IN (SELECT f.fid FROM ${this.#files} f WHERE ${eligible})`,
719
754
  params: [cutoff]
720
755
  }, {
@@ -723,10 +758,14 @@ var SqliteFilesApi = class {
723
758
  }]);
724
759
  return contents;
725
760
  }
761
+ /** The statements as one transaction; per statement, the rows it changed. */
762
+ #transaction(statements) {
763
+ return inTransaction(this.#db, (tx) => runStatements(tx, statements));
764
+ }
726
765
  async #entry(name) {
727
- return (await this.#sql.all(`SELECT p.fid, p.mtime, f.size, f.compression, f.block_size
766
+ return (await this.#db.query(`SELECT p.fid, p.mtime, f.size, f.compression, f.block_size
728
767
  FROM ${this.#paths} p LEFT JOIN ${this.#files} f ON f.fid = p.fid
729
- WHERE p.path = ?`, name))[0];
768
+ WHERE p.path = ?`, [name]))[0];
730
769
  }
731
770
  #decoder(name, compression) {
732
771
  if (compression === "none") return null;
@@ -738,7 +777,7 @@ var SqliteFilesApi = class {
738
777
  throw new Error(`sqlite-files: ${name} is compressed with ${compression}, and no codec reads it`);
739
778
  }
740
779
  async #block(fid, shift) {
741
- return (await this.#sql.all(`SELECT shift, block FROM ${this.#blocks} WHERE fid = ? AND shift = ?`, fid, shift))[0];
780
+ return (await this.#db.query(`SELECT shift, block FROM ${this.#blocks} WHERE fid = ? AND shift = ?`, [fid, shift]))[0];
742
781
  }
743
782
  /** A subquery selecting the content `fid` while it is still pending. */
744
783
  #pending(fid) {
@@ -872,79 +911,6 @@ async function pipe(bytes, transform) {
872
911
  return new Uint8Array(await new Response(stream).arrayBuffer());
873
912
  }
874
913
  //#endregion
875
- //#region src/drivers.ts
876
- /** node:sqlite — local development, tests and any Node deployment. */
877
- var NodeSqlDriver = class {
878
- #db;
879
- constructor(db) {
880
- this.#db = db;
881
- }
882
- all(sql, ...params) {
883
- return this.#db.prepare(sql).all(...params);
884
- }
885
- run(sql, ...params) {
886
- this.#db.prepare(sql).run(...params);
887
- }
888
- /** Synchronous from BEGIN to COMMIT, so no other call can interleave on the connection. */
889
- transaction(statements) {
890
- this.#db.exec("BEGIN IMMEDIATE");
891
- try {
892
- const counts = statements.map(({ sql, params = [] }) => Number(this.#db.prepare(sql).run(...params).changes));
893
- this.#db.exec("COMMIT");
894
- return counts;
895
- } catch (error) {
896
- this.#db.exec("ROLLBACK");
897
- throw error;
898
- }
899
- }
900
- };
901
- /**
902
- * Durable Object SQLite storage: pass `ctx.storage`. Synchronous; BLOBs come
903
- * back as ArrayBuffer. Transactions use `transactionSync`, since `sql.exec`
904
- * rejects `BEGIN`.
905
- */
906
- var DoSqlDriver = class {
907
- #storage;
908
- constructor(storage) {
909
- this.#storage = storage;
910
- }
911
- all(sql, ...params) {
912
- return this.#storage.sql.exec(sql, ...params).toArray();
913
- }
914
- run(sql, ...params) {
915
- this.#storage.sql.exec(sql, ...params).toArray();
916
- }
917
- /**
918
- * Counts come from SQLite's `changes()` on the same connection, not from the
919
- * cursor's `rowsWritten`, which also counts every index row a write touches.
920
- */
921
- transaction(statements) {
922
- return this.#storage.transactionSync(() => statements.map(({ sql, params = [] }) => {
923
- this.#storage.sql.exec(sql, ...params).toArray();
924
- const [row] = this.#storage.sql.exec("SELECT changes() AS n").toArray();
925
- return row.n;
926
- }));
927
- }
928
- };
929
- /** Cloudflare D1. Asynchronous; BLOBs come back as number[]; transactions are `batch()`. */
930
- var D1SqlDriver = class {
931
- #db;
932
- constructor(db) {
933
- this.#db = db;
934
- }
935
- async all(sql, ...params) {
936
- const { results } = await this.#db.prepare(sql).bind(...params).all();
937
- return results;
938
- }
939
- async run(sql, ...params) {
940
- await this.#db.prepare(sql).bind(...params).run();
941
- }
942
- async transaction(statements) {
943
- if (statements.length === 0) return [];
944
- return (await this.#db.batch(statements.map(({ sql, params = [] }) => this.#db.prepare(sql).bind(...params)))).map((result) => result.meta.changes);
945
- }
946
- };
947
- //#endregion
948
914
  //#region src/sqlar-files-api.ts
949
915
  /**
950
916
  * The table, verbatim from the SQLite Archive specification. Five columns,
@@ -971,22 +937,22 @@ const MAX_SYMLINK_HOPS = 8;
971
937
  * missing path — and {@link symlink} / {@link readlink} expose the link itself.
972
938
  */
973
939
  var SqlarFilesApi = class {
974
- #sql;
940
+ #db;
975
941
  #codec;
976
- constructor(sql, opts = {}) {
977
- this.#sql = sql;
942
+ constructor(db, opts = {}) {
943
+ this.#db = db;
978
944
  this.#codec = opts.codec ?? defaultCodec();
979
945
  }
980
946
  /** Creates the table if it is missing. Await it before any other call. */
981
947
  async init() {
982
- await this.#sql.run(SCHEMA);
948
+ await this.#db.query(SCHEMA);
983
949
  }
984
950
  /** A missing path or a directory yields nothing; a file yields one chunk. */
985
951
  async *read(path, options = {}) {
986
952
  options.signal?.throwIfAborted();
987
953
  const resolved = await this.#resolve(toName(path));
988
954
  if (resolved?.stats.kind !== "file") return;
989
- const rows = await this.#sql.all("SELECT data FROM sqlar WHERE name = ?", resolved.name);
955
+ const rows = await this.#db.query("SELECT data FROM sqlar WHERE name = ?", [resolved.name]);
990
956
  const content = await this.#content(resolved.name, resolved.stats.size, toBytes(rows[0]?.data));
991
957
  options.signal?.throwIfAborted();
992
958
  const start = options.start ?? 0;
@@ -1051,7 +1017,7 @@ var SqlarFilesApi = class {
1051
1017
  if (resolved?.stats.kind !== "directory") return;
1052
1018
  const base = resolved.name;
1053
1019
  const range = descendants(base);
1054
- const rows = await this.#sql.all(`SELECT ${ROW_COLUMNS} FROM sqlar WHERE ${range.where} ORDER BY name`, ...range.params);
1020
+ const rows = await this.#db.query(`SELECT ${ROW_COLUMNS} FROM sqlar WHERE ${range.where} ORDER BY name`, range.params);
1055
1021
  const prefixLength = base === "" ? 0 : base.length + 1;
1056
1022
  const seen = /* @__PURE__ */ new Set();
1057
1023
  for (const row of rows) {
@@ -1081,8 +1047,8 @@ var SqlarFilesApi = class {
1081
1047
  }
1082
1048
  async remove(path) {
1083
1049
  const range = selfAndDescendants(toName(path));
1084
- if ((await this.#sql.all(`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`, ...range.params)).length === 0) return false;
1085
- await this.#sql.run(`DELETE FROM sqlar WHERE ${range.where}`, ...range.params);
1050
+ if ((await this.#db.query(`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`, range.params)).length === 0) return false;
1051
+ await this.#db.query(`DELETE FROM sqlar WHERE ${range.where}`, range.params);
1086
1052
  return true;
1087
1053
  }
1088
1054
  /**
@@ -1093,7 +1059,7 @@ var SqlarFilesApi = class {
1093
1059
  const from = toName(source);
1094
1060
  const to = toName(target);
1095
1061
  const range = selfAndDescendants(from);
1096
- const rows = await this.#sql.all(`SELECT name, mode, sz, typeof(data) AS dtype, data FROM sqlar WHERE ${range.where}`, ...range.params);
1062
+ const rows = await this.#db.query(`SELECT name, mode, sz, typeof(data) AS dtype, data FROM sqlar WHERE ${range.where}`, range.params);
1097
1063
  if (rows.length === 0) return false;
1098
1064
  await this.mkdir(parentName(to));
1099
1065
  for (const row of rows) {
@@ -1147,16 +1113,22 @@ var SqlarFilesApi = class {
1147
1113
  }
1148
1114
  }
1149
1115
  async #row(name) {
1150
- return (await this.#sql.all(`SELECT ${ROW_COLUMNS} FROM sqlar WHERE name = ?`, name))[0];
1116
+ return (await this.#db.query(`SELECT ${ROW_COLUMNS} FROM sqlar WHERE name = ?`, [name]))[0];
1151
1117
  }
1152
1118
  async #hasDescendants(name) {
1153
1119
  const range = descendants(name);
1154
- return (await this.#sql.all(`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`, ...range.params)).length > 0;
1120
+ return (await this.#db.query(`SELECT 1 FROM sqlar WHERE ${range.where} LIMIT 1`, range.params)).length > 0;
1155
1121
  }
1156
1122
  async #put(name, mode, sz, data) {
1157
- await this.#sql.run(`INSERT INTO sqlar (name, mode, mtime, sz, data) VALUES (?, ?, ?, ?, ?)
1123
+ await this.#db.query(`INSERT INTO sqlar (name, mode, mtime, sz, data) VALUES (?, ?, ?, ?, ?)
1158
1124
  ON CONFLICT(name) DO UPDATE SET
1159
- mode = excluded.mode, mtime = excluded.mtime, sz = excluded.sz, data = excluded.data`, name, mode, Math.floor(Date.now() / 1e3), sz, data);
1125
+ mode = excluded.mode, mtime = excluded.mtime, sz = excluded.sz, data = excluded.data`, [
1126
+ name,
1127
+ mode,
1128
+ Math.floor(Date.now() / 1e3),
1129
+ sz,
1130
+ data
1131
+ ]);
1160
1132
  }
1161
1133
  };
1162
1134
  const ROW_COLUMNS = "name, mode, mtime, sz, typeof(data) AS dtype, CASE WHEN sz = -1 THEN data END AS target";
@@ -1251,9 +1223,6 @@ async function concat(content) {
1251
1223
  return out;
1252
1224
  }
1253
1225
  //#endregion
1254
- exports.D1SqlDriver = D1SqlDriver;
1255
- exports.DoSqlDriver = DoSqlDriver;
1256
- exports.NodeSqlDriver = NodeSqlDriver;
1257
1226
  exports.SqlarFilesApi = SqlarFilesApi;
1258
1227
  exports.SqliteFilesApi = SqliteFilesApi;
1259
1228
  exports.defaultCodec = defaultCodec;