@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 +4 -0
- package/driver.d.ts +4 -0
- package/index.js +111 -17
- package/package.json +4 -3
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
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
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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.
|
|
9
|
-
"@yorozu/log": "^0.
|
|
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": {
|