@yorozu/db-sqlite 0.5.6 → 1.0.31

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.
package/README.md CHANGED
@@ -12,6 +12,7 @@ let driver = createSqliteDriver({
12
12
  filename: "app.sqlite",
13
13
  // native, // inject better-sqlite3 ctor (tests). Default: better-sqlite3
14
14
  log, // optional Logger; silent default
15
+ autoFlush: true, // opt-in; or { pendingPuts: 32, idleMs: 1000 }
15
16
  })
16
17
 
17
18
  let db = await driver.open(schema)
@@ -27,9 +28,12 @@ Logger is optional. Internally: `makeLog(opts.log ?? makeSilentLog(), "yorozu-db
27
28
  - One table per collection: `pk TEXT PRIMARY KEY`, `payload TEXT` (JSON minus blob fields), plus `"<index>__<i>"` columns (TEXT/REAL). Sidecar `"<collection>__blobs"` `(pk, field, data BLOB)`.
28
29
  - `get` rehydrates `Blob` when `Blob` exists, otherwise `Uint8Array`.
29
30
  - Reserved index `"__pk"`: primary-key walk, even if not in `CollectionDef.indexes`.
31
+ - `scan(..., { direction: "rev" })` returns the highest keys first; omit = fwd.
30
32
  - `scan(..., { keysOnly: true })` is `SELECT pk, index columns` only. Never `payload`, never join `__blobs`. Omit `ScanHit.value`.
31
33
  - Prefix TTL: `scan("by-evict", { lt: [cutoff], keysOnly: true })` matches `[storedAt, bytes]` with `storedAt < cutoff` (`[cutoff] < [cutoff, 0]`).
32
34
  - Default `put` flush is `"now"`. `"batch"` buffers until `db.flush()` or the next `"rw"` transact commit, not `"r"`. `flush()` coalesces by `(collection, pk)`. Sync put batches use better-sqlite3 `db.transaction`.
35
+ - `flush({ reason, signal })`: already-aborted `signal` rejects with `AbortError` (or `signal.reason`) before taking the lock; in-flight writes finish. Read transact `flush` stays a no-op.
36
+ - Opt-in `autoFlush`: omitted = off. `true` → `{ pendingPuts: 32, idleMs: 1000 }`. After batch puts, idle-flush via `requestIdle` with `{ reason: "idle" }`; at `pendingPuts` distinct pks, re-arm with timeout 0. Successful flush / close cancels the idle handle.
33
37
  - Async `transact`: mutex + `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`. Not `db.transaction(fn)` (that API is sync-only). Nested `transact` throws via a facade. Concurrent `transact` serializes. Prefer the callback's `db.collection()`. A collection obtained before `transact` is reentrant while the SQL tx is open and joins that tx (rolls back with it). Concurrent ops from another task still queue on the mutex.
34
38
  - Scan `limit` is applied after `inRange` + `compareIndexKey` sort (no SQL `LIMIT`). String / mixed index keys are not filtered by SQL `WHERE` (SQLite TEXT is UTF-8; `IndexKey` strings compare as UTF-16). Select covering columns, then `inRange` / `compareIndexKey`.
35
39
  - Call `await db.flush()` before `close()`. Do not leave `{ flush: "batch" }` puts outstanding if another process may take the file.
package/driver.d.ts CHANGED
@@ -5,4 +5,8 @@ export declare function createSqliteDriver(opts: {
5
5
  filename: string;
6
6
  native?: typeof Database;
7
7
  log?: Logger;
8
+ autoFlush?: boolean | {
9
+ pendingPuts?: number;
10
+ idleMs?: number;
11
+ };
8
12
  }): DbDriver;
package/index.js CHANGED
@@ -773,6 +773,9 @@ var require_lib = /* @__PURE__ */ __commonJSMin(((exports, module) => {
773
773
  module.exports.SqliteError = require_sqlite_error();
774
774
  }));
775
775
  //#endregion
776
+ //#region ../db/src/types.ts
777
+ var DEFAULT_AUTO_FLUSH_IDLE_MS = 1e3;
778
+ //#endregion
776
779
  //#region ../db/src/bounds.ts
777
780
  function rankOf(value) {
778
781
  if (typeof value === "number") {
@@ -829,6 +832,33 @@ function inRange(indexKey, bound = {}) {
829
832
  return true;
830
833
  }
831
834
  //#endregion
835
+ //#region ../utils/src/async/idle.ts
836
+ function requestIdle(fn, opts) {
837
+ let g = globalThis;
838
+ let timeout = opts?.timeout;
839
+ if (typeof g.requestIdleCallback === "function") {
840
+ let ricOpts;
841
+ if (timeout !== void 0) ricOpts = { timeout: timeout > 0 ? timeout : 1 };
842
+ let id = g.requestIdleCallback(fn, ricOpts);
843
+ return { cancel() {
844
+ g.cancelIdleCallback?.(id);
845
+ } };
846
+ }
847
+ let delay = 0;
848
+ if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) delay = timeout;
849
+ let timer = setTimeout(() => {
850
+ fn({
851
+ didTimeout: true,
852
+ timeRemaining() {
853
+ return 0;
854
+ }
855
+ });
856
+ }, delay);
857
+ return { cancel() {
858
+ clearTimeout(timer);
859
+ } };
860
+ }
861
+ //#endregion
832
862
  //#region ../../node_modules/.pnpm/halua@5.0.0/node_modules/halua/lib/index.js
833
863
  function toarray(value) {
834
864
  return Array.isArray(value) ? value : typeof value === "undefined" ? [] : [value];
@@ -1599,6 +1629,22 @@ function makeLog(src, issueKey) {
1599
1629
  //#region src/driver.ts
1600
1630
  var import___vite_browser_external = require___vite_browser_external();
1601
1631
  var import_lib = /* @__PURE__ */ __toESM(require_lib(), 1);
1632
+ function abortedReason(signal) {
1633
+ if (!signal?.aborted) return void 0;
1634
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
1635
+ }
1636
+ function resolveAutoFlush(autoFlush) {
1637
+ if (autoFlush !== true && (typeof autoFlush !== "object" || autoFlush === null)) return null;
1638
+ let src = autoFlush === true ? {} : autoFlush;
1639
+ let pendingPuts = src.pendingPuts;
1640
+ let idleMs = src.idleMs;
1641
+ if (pendingPuts === void 0 || !Number.isFinite(pendingPuts) || pendingPuts < 1) pendingPuts = 32;
1642
+ if (idleMs === void 0 || !Number.isFinite(idleMs) || idleMs < 0) idleMs = DEFAULT_AUTO_FLUSH_IDLE_MS;
1643
+ return {
1644
+ pendingPuts,
1645
+ idleMs
1646
+ };
1647
+ }
1602
1648
  var IDENT_RE = /^[A-Za-z0-9_-]+$/;
1603
1649
  var ISSUE_KEY = "yorozu-db-sqlite";
1604
1650
  function reportError(log, err) {
@@ -1664,6 +1710,11 @@ function indexKeyPath(def, name) {
1664
1710
  if (!idx) throw new Error(`unknown index: ${name}`);
1665
1711
  return idx.keyPath;
1666
1712
  }
1713
+ function compareHits(a, b, rev) {
1714
+ let c = compareIndexKey(a.indexKey, b.indexKey);
1715
+ if (c === 0) c = compareIndexKey(a.primaryKey, b.primaryKey);
1716
+ return rev ? -c : c;
1717
+ }
1667
1718
  function isBlobish(value) {
1668
1719
  if (typeof Blob !== "undefined" && value instanceof Blob) return true;
1669
1720
  if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) return true;
@@ -1836,6 +1887,10 @@ var SerialQueue = class {
1836
1887
  this._tail = run.then(() => void 0, () => void 0);
1837
1888
  return run;
1838
1889
  }
1890
+ /** Run fn without the reentrant store so timers/idle do not inherit the lock. */
1891
+ detached(fn) {
1892
+ return this._held.exit(fn);
1893
+ }
1839
1894
  };
1840
1895
  var WriteGuardCollection = class {
1841
1896
  name;
@@ -1895,8 +1950,10 @@ var NestedTxDb = class {
1895
1950
  transact(_names, _mode, _fn) {
1896
1951
  return Promise.reject(/* @__PURE__ */ new Error("nested transact is not supported"));
1897
1952
  }
1898
- flush() {
1953
+ flush(opts) {
1899
1954
  if (this._mode.value === "r") return Promise.resolve();
1955
+ let aborted = abortedReason(opts?.signal);
1956
+ if (aborted !== void 0) return Promise.reject(aborted);
1900
1957
  this._flushUnlocked();
1901
1958
  return Promise.resolve();
1902
1959
  }
@@ -1950,7 +2007,8 @@ var SqliteCollection = class {
1950
2007
  _table;
1951
2008
  _blobTable;
1952
2009
  _indexColIds;
1953
- constructor(def, handle, pending, inSqlTx) {
2010
+ _onBatchQueued;
2011
+ constructor(def, handle, pending, inSqlTx, onBatchQueued) {
1954
2012
  this.name = def.name;
1955
2013
  this._def = def;
1956
2014
  this._keyPath = def.keyPath;
@@ -1962,6 +2020,7 @@ var SqliteCollection = class {
1962
2020
  this._table = quoteIdent(def.name);
1963
2021
  this._blobTable = quoteIdent(`${def.name}__blobs`);
1964
2022
  this._indexColIds = allIndexColIds(def);
2023
+ this._onBatchQueued = onBatchQueued;
1965
2024
  }
1966
2025
  _colPending() {
1967
2026
  return this._pending.get(this.name);
@@ -2063,6 +2122,7 @@ var SqliteCollection = class {
2063
2122
  let write = await this._prepareWrite(row);
2064
2123
  if ((opts?.flush ?? "now") === "batch") {
2065
2124
  this._ensurePending().set(write.pk, write);
2125
+ this._onBatchQueued();
2066
2126
  return;
2067
2127
  }
2068
2128
  this._colPending()?.delete(write.pk);
@@ -2075,6 +2135,7 @@ var SqliteCollection = class {
2075
2135
  if ((opts?.flush ?? "now") === "batch") {
2076
2136
  let pending = this._ensurePending();
2077
2137
  for (let write of writes) pending.set(write.pk, write);
2138
+ this._onBatchQueued();
2078
2139
  return;
2079
2140
  }
2080
2141
  let live = this._colPending();
@@ -2181,11 +2242,7 @@ var SqliteCollection = class {
2181
2242
  if (!keysOnly && valueByPk.size > 0) this._attachBlobs(valueByPk, [...valueByPk.keys()]);
2182
2243
  }
2183
2244
  if (pending && pending.size > 0) hits = this._mergePending(index, bound, hits, pending);
2184
- hits.sort((a, b) => {
2185
- let c = compareIndexKey(a.indexKey, b.indexKey);
2186
- if (c !== 0) return c;
2187
- return compareIndexKey(a.primaryKey, b.primaryKey);
2188
- });
2245
+ hits.sort((a, b) => compareHits(a, b, bound.direction === "rev"));
2189
2246
  if (bound.limit !== void 0) hits = hits.slice(0, Math.max(0, bound.limit));
2190
2247
  return hits;
2191
2248
  }
@@ -2207,16 +2264,12 @@ var SqliteCollection = class {
2207
2264
  value: write.row
2208
2265
  });
2209
2266
  }
2210
- out.sort((a, b) => {
2211
- let c = compareIndexKey(a.indexKey, b.indexKey);
2212
- if (c !== 0) return c;
2213
- return compareIndexKey(a.primaryKey, b.primaryKey);
2214
- });
2215
2267
  return out;
2216
2268
  }
2217
2269
  };
2218
2270
  var SqliteDb = class {
2219
2271
  schema;
2272
+ log;
2220
2273
  _handle;
2221
2274
  _collections;
2222
2275
  _gated;
@@ -2228,10 +2281,14 @@ var SqliteDb = class {
2228
2281
  _txView;
2229
2282
  _onClose;
2230
2283
  _closed = false;
2231
- constructor(schema, handle, onClose) {
2284
+ _autoFlush;
2285
+ _idleHandle = null;
2286
+ constructor(schema, handle, onClose, autoFlush, log) {
2232
2287
  this.schema = schema;
2233
2288
  this._handle = handle;
2234
2289
  this._onClose = onClose;
2290
+ this._autoFlush = autoFlush;
2291
+ this.log = log;
2235
2292
  this._collections = /* @__PURE__ */ new Map();
2236
2293
  this._gated = /* @__PURE__ */ new Map();
2237
2294
  this._txView = new NestedTxDb(this, (name) => {
@@ -2240,7 +2297,7 @@ var SqliteDb = class {
2240
2297
  return col;
2241
2298
  }, () => this._flushPending(), this._txMode);
2242
2299
  for (let def of schema.collections) {
2243
- let col = new SqliteCollection(def, handle, this._pending, this._inSqlTx);
2300
+ let col = new SqliteCollection(def, handle, this._pending, this._inSqlTx, () => this._onBatchQueued());
2244
2301
  this._collections.set(def.name, col);
2245
2302
  this._gated.set(def.name, new GatedCollection(col, this._lock));
2246
2303
  }
@@ -2273,12 +2330,18 @@ var SqliteDb = class {
2273
2330
  }
2274
2331
  }));
2275
2332
  }
2276
- flush() {
2333
+ flush(opts) {
2334
+ let aborted = abortedReason(opts?.signal);
2335
+ if (aborted !== void 0) return Promise.reject(aborted);
2277
2336
  return this._lock.with(async () => {
2337
+ if (this._closed) return;
2338
+ let aborted2 = abortedReason(opts?.signal);
2339
+ if (aborted2 !== void 0) throw aborted2;
2278
2340
  this._flushPending();
2279
2341
  });
2280
2342
  }
2281
2343
  async close() {
2344
+ this._cancelIdle();
2282
2345
  if (this._closed) return;
2283
2346
  this._closed = true;
2284
2347
  try {
@@ -2288,13 +2351,41 @@ var SqliteDb = class {
2288
2351
  this._onClose();
2289
2352
  }
2290
2353
  }
2354
+ _pendingDistinct() {
2355
+ let n = 0;
2356
+ for (let rows of this._pending.values()) n += rows.size;
2357
+ return n;
2358
+ }
2359
+ _cancelIdle() {
2360
+ this._idleHandle?.cancel();
2361
+ this._idleHandle = null;
2362
+ }
2363
+ _armIdle(timeout) {
2364
+ this._cancelIdle();
2365
+ this._idleHandle = this._lock.detached(() => requestIdle(() => {
2366
+ this._idleHandle = null;
2367
+ if (this._closed) return;
2368
+ this.flush({ reason: "idle" }).catch((err) => reportError(this.log, err));
2369
+ }, { timeout }));
2370
+ }
2371
+ _onBatchQueued() {
2372
+ if (!this._autoFlush) return;
2373
+ if (this._pendingDistinct() >= this._autoFlush.pendingPuts) {
2374
+ this._armIdle(0);
2375
+ return;
2376
+ }
2377
+ if (!this._idleHandle) this._armIdle(this._autoFlush.idleMs);
2378
+ }
2291
2379
  _flushPending() {
2292
2380
  let snapshot = [];
2293
2381
  for (let [name, rows] of this._pending) {
2294
2382
  if (rows.size === 0) continue;
2295
2383
  snapshot.push([name, [...rows.values()]]);
2296
2384
  }
2297
- if (snapshot.length === 0) return;
2385
+ if (snapshot.length === 0) {
2386
+ this._cancelIdle();
2387
+ return;
2388
+ }
2298
2389
  let run = () => {
2299
2390
  for (let [name, writes] of snapshot) {
2300
2391
  let col = this._collections.get(name);
@@ -2309,17 +2400,20 @@ var SqliteDb = class {
2309
2400
  if (!live) continue;
2310
2401
  for (let write of writes) if (live.get(write.pk) === write) live.delete(write.pk);
2311
2402
  }
2403
+ if (this._pendingDistinct() === 0) this._cancelIdle();
2312
2404
  }
2313
2405
  };
2314
2406
  var SqliteDriver = class {
2315
2407
  log;
2316
2408
  _filename;
2317
2409
  _native;
2410
+ _autoFlush;
2318
2411
  _conns = /* @__PURE__ */ new Set();
2319
2412
  constructor(opts) {
2320
2413
  this.log = makeLog(opts.log ?? makeSilentLog(), ISSUE_KEY);
2321
2414
  this._filename = opts.filename;
2322
2415
  this._native = opts.native ?? import_lib.default;
2416
+ this._autoFlush = resolveAutoFlush(opts.autoFlush);
2323
2417
  }
2324
2418
  async open(schema) {
2325
2419
  let raw;
@@ -2334,7 +2428,7 @@ var SqliteDriver = class {
2334
2428
  applySchema(handle, schema);
2335
2429
  let db = new SqliteDb(schema, handle, () => {
2336
2430
  this._conns.delete(db);
2337
- });
2431
+ }, this._autoFlush, this.log);
2338
2432
  this._conns.add(db);
2339
2433
  return db;
2340
2434
  } catch (err) {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@yorozu/db-sqlite",
3
3
  "type": "module",
4
- "version": "0.5.6",
4
+ "version": "1.0.31",
5
5
  "description": "better-sqlite3 thin wrapper + driver for @yorozu/db",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
- "@yorozu/db": "^0.5.6",
9
- "@yorozu/log": "^0.5.6",
8
+ "@yorozu/db": "^1.0.31",
9
+ "@yorozu/log": "^1.0.31",
10
+ "@yorozu/utils": "^1.0.31",
10
11
  "better-sqlite3": "12.11.1"
11
12
  },
12
13
  "exports": {