@syncular/client 0.15.46 → 0.15.48

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,23 +1,9 @@
1
1
  /**
2
- * `ClientDatabase` on better-sqlite3 the Electron-main / plain-Node
3
- * backend. Semantics mirror `./bun-database`
4
- * exactly (synchronous exec/query/transaction with the shared savepoint
5
- * helper, and the same §5.3 sqlite-image ATTACH path), so the core behaves
6
- * identically whether it runs on bun:sqlite (tests), sqlite-wasm (browser)
7
- * or better-sqlite3 (Node/Electron-main).
8
- *
9
- * better-sqlite3 is an OPTIONAL peer dependency, not a hard one: the package
10
- * installs cleanly without it and this module errors helpfully only when a
11
- * host actually calls `openNodeDatabase()` without having installed the peer.
12
- * Not exported from the package root, so browser/bun entries never resolve
13
- * the native module. Subpath export: `@syncular/client/node`.
14
- *
15
- * bun CANNOT dlopen better-sqlite3 (ERR_DLOPEN_FAILED, oven-sh/bun#4290), so
16
- * this adapter is verified under real Node — see the README "Electron-main /
17
- * plain-Node" section for the one-command recipe and `test/node-database`.
2
+ * `ClientDatabase` on Node's built-in `node:sqlite`. Semantics mirror the Bun
3
+ * adapter: synchronous queries, nested transactions, and SQLite image attach.
18
4
  */
5
+ import { DatabaseSync } from 'node:sqlite';
19
6
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
20
- import { createRequire } from 'node:module';
21
7
  import { tmpdir } from 'node:os';
22
8
  import { join } from 'node:path';
23
9
  import { assertImageAlias, runTransaction, } from './database.js';
@@ -28,19 +14,12 @@ function coerceParams(params) {
28
14
  return value;
29
15
  });
30
16
  }
31
- /**
32
- * better-sqlite3 returns BLOB columns as Node `Buffer`s. A Buffer IS a
33
- * Uint8Array subclass, but it can be a view onto a shared pool buffer, so we
34
- * normalize to a standalone Uint8Array — matching what bun:sqlite hands back
35
- * and keeping the buffer-ownership assumptions elsewhere (worker transfer,
36
- * structured clone) honest.
37
- */
38
17
  function normalizeRow(row) {
39
18
  const out = {};
40
19
  for (const key in row) {
41
20
  const value = row[key];
42
- if (Buffer.isBuffer(value)) {
43
- out[key] = new Uint8Array(value); // copies out of the pool
21
+ if (value instanceof Uint8Array) {
22
+ out[key] = new Uint8Array(value);
44
23
  }
45
24
  else {
46
25
  out[key] = value;
@@ -48,47 +27,11 @@ function normalizeRow(row) {
48
27
  }
49
28
  return out;
50
29
  }
51
- /**
52
- * Load the optional peer AND open the database in one guarded step, so BOTH
53
- * failure modes are turned into a clear, actionable error rather than a raw
54
- * one:
55
- *
56
- * - `require('better-sqlite3')` throwing MODULE_NOT_FOUND — the peer is not
57
- * installed (the common browser-only-host case), and
58
- * - `new Database()` throwing ERR_DLOPEN_FAILED — the module resolves but the
59
- * native addon cannot load, which is exactly what bun does for
60
- * better-sqlite3 (oven-sh/bun#4290); the addon only dlopens at construction.
61
- */
62
- function openBetterSqlite(path) {
63
- const require = createRequire(import.meta.url);
64
- try {
65
- const mod = require('better-sqlite3');
66
- const Database = mod.default ??
67
- mod;
68
- return new Database(path);
69
- }
70
- catch (error) {
71
- const code = error?.code;
72
- if (code === 'ERR_DLOPEN_FAILED') {
73
- throw new Error("openNodeDatabase() requires the 'better-sqlite3' native module, but " +
74
- 'it failed to load. This most commonly means you are running under ' +
75
- 'bun, which cannot dlopen better-sqlite3 (oven-sh/bun#4290) — use ' +
76
- "the bun:sqlite backend ('@syncular/client/bun') under bun, " +
77
- "and reserve '@syncular/client/node' for Node/Electron-main. " +
78
- `Underlying error: ${String(error)}`);
79
- }
80
- throw new Error('openNodeDatabase() requires the optional peer dependency ' +
81
- "'better-sqlite3', which is not installed. Add it to your app " +
82
- '(`npm install better-sqlite3` / `bun add better-sqlite3`) — it is ' +
83
- 'kept optional so @syncular/client installs without a native ' +
84
- `build for browser-only hosts. Underlying error: ${String(error)}`);
85
- }
86
- }
87
30
  export class NodeClientDatabase {
88
31
  db;
89
32
  #tx = { depth: 0 };
90
33
  constructor(path = ':memory:') {
91
- this.db = openBetterSqlite(path);
34
+ this.db = new DatabaseSync(path);
92
35
  }
93
36
  exec(sql, params = []) {
94
37
  this.db.prepare(sql).run(...coerceParams(params));
@@ -100,12 +43,7 @@ export class NodeClientDatabase {
100
43
  transaction(fn) {
101
44
  return runTransaction(this.#tx, (sql) => this.db.exec(sql), fn);
102
45
  }
103
- /**
104
- * §5.3 image import: better-sqlite3 (like bun:sqlite) attaches files, not
105
- * buffers, so the image lands in a private temp file for the duration of
106
- * the ATTACH. Must be called outside any open transaction (SQLite cannot
107
- * ATTACH inside one).
108
- */
46
+ /** §5.3 image import through a private file attached for one callback. */
109
47
  withSqliteImage(bytes, alias, fn) {
110
48
  assertImageAlias(alias);
111
49
  const dir = mkdtempSync(join(tmpdir(), 'syncular-image-'));
@@ -10,8 +10,8 @@
10
10
  * bypasses the outbox (SPEC §7.1) and silently diverges from the
11
11
  * server — writes MUST go through `client.mutate([...])`.
12
12
  * 2. ONE STATEMENT. `sqlite-wasm`'s `exec` runs every statement in a
13
- * multi-statement string (`SELECT 1; DROP TABLE t`), while bun:sqlite /
14
- * better-sqlite3 prepare only the first. We unify on the strict
13
+ * multi-statement string (`SELECT 1; DROP TABLE t`), while the native
14
+ * SQLite adapters prepare only the first. We unify on the strict
15
15
  * behaviour: exactly one statement per `query()`.
16
16
  *
17
17
  * The guard only fronts the PUBLIC `client.query()` — engine-internal reads
@@ -10,8 +10,8 @@
10
10
  * bypasses the outbox (SPEC §7.1) and silently diverges from the
11
11
  * server — writes MUST go through `client.mutate([...])`.
12
12
  * 2. ONE STATEMENT. `sqlite-wasm`'s `exec` runs every statement in a
13
- * multi-statement string (`SELECT 1; DROP TABLE t`), while bun:sqlite /
14
- * better-sqlite3 prepare only the first. We unify on the strict
13
+ * multi-statement string (`SELECT 1; DROP TABLE t`), while the native
14
+ * SQLite adapters prepare only the first. We unify on the strict
15
15
  * behaviour: exactly one statement per `query()`.
16
16
  *
17
17
  * The guard only fronts the PUBLIC `client.query()` — engine-internal reads
package/dist/remote.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface SyncRemoteClientConfig {
18
18
  readonly operations?: RemoteOperationTransport;
19
19
  readonly operationRealtime?: RemoteOperationRealtimeConnector;
20
20
  readonly encryption?: EncryptionConfig;
21
+ /** Acquired partition log epoch for restore-safe ordinary commits (§2.1). */
22
+ readonly logEpoch?: string;
21
23
  }
22
24
  export interface RemoteCommitInput {
23
25
  /** Stable caller-owned idempotency identity for this logical commit. */
package/dist/remote.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Database-less SSP2 producer (§6.10). It prepares and sends ordinary commits
3
3
  * through the existing push path without creating a local replica or outbox.
4
4
  */
5
- import { decodeMessage, decodeRemoteOperationResponse, decodeRemoteOperationRealtimeMessage, encodeMessage, encodeRemoteOperationRequest, encodeRemoteOperationRealtimeMessage, encodeRow, PROTOCOL_WIRE_VERSION, } from '@syncular/core';
5
+ import { decodeMessage, decodeRemoteOperationResponse, decodeRemoteOperationRealtimeMessage, encodeMessage, encodeRemoteOperationRequest, encodeRemoteOperationRealtimeMessage, encodeRow, } from '@syncular/core';
6
6
  import { encryptRowValues } from './encryption.js';
7
7
  import { ClientSyncError } from './errors.js';
8
8
  import { compileClientSchema, recordToRowValues, } from './schema.js';
@@ -71,10 +71,14 @@ export class SyncRemoteClient {
71
71
  #operationSocketGeneration = 0;
72
72
  #watches = new Map();
73
73
  #encryption;
74
+ #logEpoch;
74
75
  constructor(config) {
75
76
  if (config.clientId.length === 0) {
76
77
  throw invalid('SyncRemoteClient clientId must be non-empty');
77
78
  }
79
+ if (config.logEpoch !== undefined && config.logEpoch.length === 0) {
80
+ throw invalid('SyncRemoteClient logEpoch must be non-empty');
81
+ }
78
82
  this.#schema =
79
83
  config.schema === undefined
80
84
  ? undefined
@@ -84,6 +88,7 @@ export class SyncRemoteClient {
84
88
  this.#operations = config.operations;
85
89
  this.#operationRealtime = config.operationRealtime;
86
90
  this.#encryption = config.encryption;
91
+ this.#logEpoch = config.logEpoch;
87
92
  }
88
93
  async prepareCommit(input) {
89
94
  const schema = this.#schema;
@@ -137,13 +142,16 @@ export class SyncRemoteClient {
137
142
  return {
138
143
  requestId: input.requestId,
139
144
  bytes: encodeMessage({
140
- wireVersion: PROTOCOL_WIRE_VERSION,
145
+ wireVersion: this.#logEpoch === undefined ? 1 : 2,
141
146
  msgKind: 'request',
142
147
  frames: [
143
148
  {
144
149
  type: 'REQ_HEADER',
145
150
  clientId: this.#clientId,
146
151
  schemaVersion: schema.version,
152
+ ...(this.#logEpoch !== undefined
153
+ ? { logEpoch: this.#logEpoch }
154
+ : {}),
147
155
  },
148
156
  {
149
157
  type: 'PUSH_COMMIT',
@@ -0,0 +1,2 @@
1
+ import type { ClientDatabase } from './database.js';
2
+ export declare function openSqliteDatabase(path?: string): ClientDatabase;
@@ -0,0 +1,4 @@
1
+ import { openBunDatabase } from './bun-database.js';
2
+ export function openSqliteDatabase(path = ':memory:') {
3
+ return openBunDatabase(path);
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { ClientDatabase } from './database.js';
2
+ export declare function openSqliteDatabase(path?: string): ClientDatabase;
@@ -0,0 +1,4 @@
1
+ import { openNodeDatabase } from './node-database.js';
2
+ export function openSqliteDatabase(path = ':memory:') {
3
+ return openNodeDatabase(path);
4
+ }
@@ -0,0 +1,23 @@
1
+ import type { WakeReason } from '@syncular/core';
2
+ import type { SecurityLifecycle } from './client.js';
3
+ import type { SyncIntent } from './invalidation.js';
4
+ export interface SyncSchedulerClient {
5
+ readonly syncNeeded: boolean;
6
+ readonly securityLifecycle: SecurityLifecycle;
7
+ syncUntilIdle(maxRounds?: number): Promise<unknown>;
8
+ onSyncNeeded(listener: (reason: 'startup' | 'hello' | WakeReason) => void): () => void;
9
+ onSyncIntent(listener: (intent: SyncIntent) => void): () => void;
10
+ }
11
+ export interface SyncSchedulerOptions {
12
+ readonly maxRounds?: number;
13
+ readonly onError?: (error: unknown) => void;
14
+ readonly now?: () => number;
15
+ readonly queueMicrotask?: (callback: () => void) => void;
16
+ readonly schedule?: (callback: () => void, delayMs: number) => () => void;
17
+ }
18
+ export interface SyncScheduler {
19
+ readonly stopped: boolean;
20
+ stop(): void;
21
+ }
22
+ /** Install the event-driven single-flight host loop for a direct client. */
23
+ export declare function installSyncScheduler(client: SyncSchedulerClient, options?: SyncSchedulerOptions): SyncScheduler;
@@ -0,0 +1,122 @@
1
+ /** Install the event-driven single-flight host loop for a direct client. */
2
+ export function installSyncScheduler(client, options = {}) {
3
+ const now = options.now ?? Date.now;
4
+ const enqueue = options.queueMicrotask ?? globalThis.queueMicrotask;
5
+ const schedule = options.schedule ??
6
+ ((callback, delayMs) => {
7
+ const timer = globalThis.setTimeout(callback, delayMs);
8
+ return () => globalThis.clearTimeout(timer);
9
+ });
10
+ let stopped = false;
11
+ let running = false;
12
+ let immediatePending = false;
13
+ let immediateQueued = false;
14
+ let backgroundReady = false;
15
+ let backgroundDue = Number.POSITIVE_INFINITY;
16
+ let cancelBackground;
17
+ const report = (error) => {
18
+ if (options.onError !== undefined) {
19
+ options.onError(error);
20
+ return;
21
+ }
22
+ const root = globalThis;
23
+ if (root.reportError !== undefined)
24
+ root.reportError(error);
25
+ else
26
+ console.error(error);
27
+ };
28
+ const clearBackground = () => {
29
+ cancelBackground?.();
30
+ cancelBackground = undefined;
31
+ backgroundReady = false;
32
+ backgroundDue = Number.POSITIVE_INFINITY;
33
+ };
34
+ const queueImmediate = () => {
35
+ immediatePending = true;
36
+ if (stopped || running || immediateQueued)
37
+ return;
38
+ immediateQueued = true;
39
+ enqueue(() => {
40
+ immediateQueued = false;
41
+ if (stopped || !immediatePending)
42
+ return;
43
+ immediatePending = false;
44
+ run();
45
+ });
46
+ };
47
+ const run = () => {
48
+ if (stopped || running)
49
+ return;
50
+ if (client.securityLifecycle === 'preflight') {
51
+ immediatePending = false;
52
+ return;
53
+ }
54
+ running = true;
55
+ backgroundReady = false;
56
+ void client
57
+ .syncUntilIdle(options.maxRounds)
58
+ .catch((error) => {
59
+ if (!stopped)
60
+ report(error);
61
+ })
62
+ .finally(() => {
63
+ running = false;
64
+ if (stopped)
65
+ return;
66
+ if (immediatePending || backgroundReady) {
67
+ queueImmediate();
68
+ return;
69
+ }
70
+ if (cancelBackground !== undefined && backgroundDue <= now()) {
71
+ clearBackground();
72
+ queueImmediate();
73
+ }
74
+ });
75
+ };
76
+ const consume = (intent) => {
77
+ if (stopped)
78
+ return;
79
+ if (intent.kind === 'none') {
80
+ clearBackground();
81
+ immediatePending = false;
82
+ return;
83
+ }
84
+ if (intent.kind === 'interactive') {
85
+ clearBackground();
86
+ queueImmediate();
87
+ return;
88
+ }
89
+ if (immediatePending || immediateQueued)
90
+ return;
91
+ clearBackground();
92
+ backgroundDue = now() + Math.max(0, intent.delayMs);
93
+ cancelBackground = schedule(() => {
94
+ cancelBackground = undefined;
95
+ backgroundDue = Number.POSITIVE_INFINITY;
96
+ backgroundReady = true;
97
+ if (!running)
98
+ queueImmediate();
99
+ }, Math.max(0, intent.delayMs));
100
+ };
101
+ const unsubscribeNeeded = client.onSyncNeeded(() => {
102
+ consume({ kind: 'interactive' });
103
+ });
104
+ const unsubscribeIntent = client.onSyncIntent(consume);
105
+ if (client.syncNeeded && client.securityLifecycle === 'active') {
106
+ consume({ kind: 'interactive' });
107
+ }
108
+ return {
109
+ get stopped() {
110
+ return stopped;
111
+ },
112
+ stop() {
113
+ if (stopped)
114
+ return;
115
+ stopped = true;
116
+ clearBackground();
117
+ immediatePending = false;
118
+ unsubscribeNeeded();
119
+ unsubscribeIntent();
120
+ },
121
+ };
122
+ }
package/dist/window.d.ts CHANGED
@@ -14,6 +14,11 @@
14
14
  */
15
15
  import { type ScopeMap } from '@syncular/core';
16
16
  import type { ClientDatabase } from './database.js';
17
+ export type TimeBucketUnit = 'month';
18
+ /** Derive the immutable UTC scope value stored when a row is created. */
19
+ export declare function creationTimeBucket(createdAtMs: number, unit: TimeBucketUnit): string;
20
+ /** Return a rolling UTC month window ordered from oldest to newest. */
21
+ export declare function last(count: number, unit: TimeBucketUnit, nowMs?: number): string[];
17
22
  /**
18
23
  * A window base: one table, one variable whose values are the window
19
24
  * units, and any FIXED scopes every unit shares (other variables pinned
package/dist/window.js CHANGED
@@ -13,6 +13,45 @@
13
13
  * transaction and the invalidation choke point.
14
14
  */
15
15
  import { canonicalScopeJson } from '@syncular/core';
16
+ import { ClientSyncError } from './errors.js';
17
+ const MAX_TIME_BUCKET_MS = 253_402_300_799_999;
18
+ function monthBucket(year, month) {
19
+ return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}`;
20
+ }
21
+ /** Derive the immutable UTC scope value stored when a row is created. */
22
+ export function creationTimeBucket(createdAtMs, unit) {
23
+ if (unit !== 'month' ||
24
+ !Number.isSafeInteger(createdAtMs) ||
25
+ createdAtMs < 0 ||
26
+ createdAtMs > MAX_TIME_BUCKET_MS) {
27
+ throw new ClientSyncError('sync.invalid_request', 'creationTimeBucket requires a supported unit and a UTC timestamp from 1970 through 9999');
28
+ }
29
+ const date = new Date(createdAtMs);
30
+ return monthBucket(date.getUTCFullYear(), date.getUTCMonth() + 1);
31
+ }
32
+ /** Return a rolling UTC month window ordered from oldest to newest. */
33
+ export function last(count, unit, nowMs = Date.now()) {
34
+ if (unit !== 'month' ||
35
+ !Number.isSafeInteger(count) ||
36
+ count < 1 ||
37
+ count > 1_200 ||
38
+ !Number.isSafeInteger(nowMs) ||
39
+ nowMs < 0 ||
40
+ nowMs > MAX_TIME_BUCKET_MS) {
41
+ throw new ClientSyncError('sync.invalid_request', 'last requires a supported unit, a count from 1 through 1200, and a UTC timestamp from 1970 through 9999');
42
+ }
43
+ const date = new Date(nowMs);
44
+ const current = date.getUTCFullYear() * 12 + date.getUTCMonth();
45
+ if (current - (count - 1) < 1970 * 12) {
46
+ throw new ClientSyncError('sync.invalid_request', 'last requires every returned UTC month to fall from 1970 through 9999');
47
+ }
48
+ const units = [];
49
+ for (let offset = count - 1; offset >= 0; offset -= 1) {
50
+ const value = current - offset;
51
+ units.push(monthBucket(Math.floor(value / 12), (value % 12) + 1));
52
+ }
53
+ return units;
54
+ }
16
55
  /**
17
56
  * A stable, server-opaque key for a window base — table + variable +
18
57
  * canonical fixed scopes. Two `setWindow` calls with the same base
@@ -147,9 +147,6 @@ export function startSyncWorker(overrides = {}) {
147
147
  autoSyncScheduled = true;
148
148
  queueMicrotask(runAutoSync);
149
149
  }
150
- function consumeEffects(effects) {
151
- consumeSyncIntent(effects.sync);
152
- }
153
150
  function requireClient() {
154
151
  if (client === undefined) {
155
152
  throw new ClientSyncError(WORKER_FAILED_CODE, 'the worker received a call before init completed');
@@ -304,20 +301,16 @@ export function startSyncWorker(overrides = {}) {
304
301
  subscribe: (input) => requireClient().subscribe(input),
305
302
  unsubscribe: (id) => requireClient().unsubscribe(id),
306
303
  setWindow: async (base, units) => {
307
- const result = await requireClient().setWindowCommand(base, units);
308
- consumeEffects(result.effects);
304
+ await requireClient().setWindowCommand(base, units);
309
305
  },
310
306
  windowState: (base) => requireClient().windowState(base),
311
307
  mutate: (mutations) => {
312
- const result = requireClient().mutateCommand(mutations);
313
- consumeEffects(result.effects);
314
- return result.value;
308
+ return requireClient().mutateCommand(mutations).value;
315
309
  },
316
310
  patch: (table, rowId, partial, options) => {
317
311
  // Same §8.4 rule as `mutate`: a local write must push without the app
318
312
  // orchestrating sync, so consume the core's immediate intent.
319
313
  const result = requireClient().patchCommand(table, rowId, partial, options);
320
- consumeEffects(result.effects);
321
314
  return result.value;
322
315
  },
323
316
  purgeLocalData: (input) => requireClient().purgeLocalData(input),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.15.46",
3
+ "version": "0.15.48",
4
4
  "description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -58,6 +58,14 @@
58
58
  "default": "./dist/node-database.js"
59
59
  }
60
60
  },
61
+ "./sqlite": {
62
+ "bun": "./src/sqlite-bun.ts",
63
+ "node": {
64
+ "types": "./dist/sqlite-node.d.ts",
65
+ "default": "./dist/sqlite-node.js"
66
+ },
67
+ "types": "./dist/sqlite-node.d.ts"
68
+ },
61
69
  "./wasm": {
62
70
  "bun": "./src/wasm-database.ts",
63
71
  "browser": "./dist/wasm-database.js",
@@ -85,23 +93,13 @@
85
93
  "!dist/**/*.test.d.ts"
86
94
  ],
87
95
  "scripts": {
88
- "verify:node": "bun build ./test/node-database/verify-node.mjs --target=node --external better-sqlite3 --outfile=./.verify-node.built.mjs && node ./.verify-node.built.mjs"
96
+ "verify:node": "bun build ./test/node-database/verify-node.mjs --target=node --outfile=./.verify-node.built.mjs && node ./.verify-node.built.mjs"
89
97
  },
90
98
  "dependencies": {
91
99
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
92
- "@syncular/core": "0.15.46"
93
- },
94
- "peerDependencies": {
95
- "better-sqlite3": ">=11"
96
- },
97
- "peerDependenciesMeta": {
98
- "better-sqlite3": {
99
- "optional": true
100
- }
100
+ "@syncular/core": "0.15.48"
101
101
  },
102
102
  "devDependencies": {
103
- "@syncular/server": "0.15.46",
104
- "@types/better-sqlite3": "^7.6.13",
105
- "better-sqlite3": "^12.11.1"
103
+ "@syncular/server": "0.15.48"
106
104
  }
107
105
  }
@@ -15,6 +15,12 @@ import {
15
15
  type SqlValue,
16
16
  } from './database';
17
17
 
18
+ declare module 'bun:sqlite' {
19
+ interface Database {
20
+ clearQueryCache(): void;
21
+ }
22
+ }
23
+
18
24
  type BunParam = string | number | bigint | Uint8Array | null;
19
25
 
20
26
  function coerceParams(params: readonly SqlValue[]): BunParam[] {
@@ -34,6 +40,11 @@ export class BunClientDatabase implements ClientDatabase {
34
40
 
35
41
  exec(sql: string, params: readonly SqlValue[] = []): void {
36
42
  this.db.query(sql).run(...coerceParams(params));
43
+ // `Database.query()` caches prepared statements. Clear that cache after
44
+ // schema DDL so a reset does not reprepare every later row upsert.
45
+ if (/^\s*(?:CREATE|DROP|ALTER)\b/i.test(sql)) {
46
+ this.db.clearQueryCache();
47
+ }
37
48
  }
38
49
 
39
50
  query(sql: string, params: readonly SqlValue[] = []): SqlRow[] {