tina4-nodejs 3.13.23 → 3.13.25
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/CLAUDE.md +30 -18
- package/package.json +3 -2
- package/packages/core/src/cache.ts +1036 -153
- package/packages/core/src/index.ts +2 -2
- package/packages/core/src/mcp.ts +14 -2
- package/packages/core/src/middleware.ts +15 -6
- package/packages/core/src/server.ts +2 -2
- package/packages/orm/src/cachedDatabase.ts +175 -23
- package/packages/orm/src/database.ts +1 -1
|
@@ -67,8 +67,8 @@ export {
|
|
|
67
67
|
export type { WebSocketClient } from "./websocket.js";
|
|
68
68
|
export { ServiceRunner, Tina4Service, matchCronField, matchesCron } from "./service.js";
|
|
69
69
|
export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
|
|
70
|
-
export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, _resetBackend } from "./cache.js";
|
|
71
|
-
export type { ResponseCacheConfig } from "./cache.js";
|
|
70
|
+
export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, createBackend, _resetBackend } from "./cache.js";
|
|
71
|
+
export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
|
|
72
72
|
export { Api } from "./api.js";
|
|
73
73
|
export type { ApiResult } from "./api.js";
|
|
74
74
|
export { Events } from "./events.js";
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -774,6 +774,9 @@ function verifyNodeSyntax(absPath: string, relPath: string): string | null {
|
|
|
774
774
|
return stripPath(lines[0]);
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
+
/** Latest resolved KV cache stats snapshot (the async API resolves into this). */
|
|
778
|
+
let _lastCacheStats: Record<string, unknown> | null = null;
|
|
779
|
+
|
|
777
780
|
/**
|
|
778
781
|
* Register all 24 built-in dev tools on the given McpServer.
|
|
779
782
|
*/
|
|
@@ -1158,9 +1161,18 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1158
1161
|
server.registerTool(
|
|
1159
1162
|
"cache_stats",
|
|
1160
1163
|
(_args) => {
|
|
1164
|
+
// The KV cache API is async on Node (cacheStats() returns a Promise) and
|
|
1165
|
+
// the MCP dispatch is synchronous, so we resolve the stats and return the
|
|
1166
|
+
// latest snapshot once available. The very first call may report the
|
|
1167
|
+
// pending placeholder; subsequent calls return live figures.
|
|
1161
1168
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1169
|
+
const mod = require("@tina4/core");
|
|
1170
|
+
const stats = mod.cacheStats?.();
|
|
1171
|
+
if (stats && typeof stats.then === "function") {
|
|
1172
|
+
stats.then((s: unknown) => { _lastCacheStats = s as Record<string, unknown>; }).catch(() => {});
|
|
1173
|
+
return _lastCacheStats ?? { hits: 0, misses: 0, size: 0, backend: "pending" };
|
|
1174
|
+
}
|
|
1175
|
+
return stats ?? {};
|
|
1164
1176
|
} catch (e) {
|
|
1165
1177
|
return { error: (e as Error).message };
|
|
1166
1178
|
}
|
|
@@ -95,18 +95,23 @@ export class MiddlewareRunner {
|
|
|
95
95
|
* boolean indicating whether the route handler should still run.
|
|
96
96
|
*
|
|
97
97
|
* Short-circuits when a before method sets a status >= 400.
|
|
98
|
+
*
|
|
99
|
+
* ASYNC — each hook is awaited so middleware can perform async work (e.g.
|
|
100
|
+
* the distributed responseCache before-hook awaiting `backend.get`). Awaiting
|
|
101
|
+
* a synchronous hook that returns an array is harmless (the array resolves
|
|
102
|
+
* immediately), so existing sync hooks keep working unchanged.
|
|
98
103
|
*/
|
|
99
|
-
static runBefore(
|
|
104
|
+
static async runBefore(
|
|
100
105
|
classes: any[],
|
|
101
106
|
req: Tina4Request,
|
|
102
107
|
res: Tina4Response,
|
|
103
|
-
): [Tina4Request, Tina4Response, boolean] {
|
|
108
|
+
): Promise<[Tina4Request, Tina4Response, boolean]> {
|
|
104
109
|
for (const cls of classes) {
|
|
105
110
|
const methods = Object.getOwnPropertyNames(cls).filter(
|
|
106
111
|
(name) => typeof cls[name] === "function" && name.startsWith("before"),
|
|
107
112
|
);
|
|
108
113
|
for (const method of methods) {
|
|
109
|
-
const result = cls[method](req, res);
|
|
114
|
+
const result = await cls[method](req, res);
|
|
110
115
|
if (Array.isArray(result)) {
|
|
111
116
|
[req, res] = result as [Tina4Request, Tina4Response];
|
|
112
117
|
}
|
|
@@ -122,18 +127,22 @@ export class MiddlewareRunner {
|
|
|
122
127
|
/**
|
|
123
128
|
* Execute every afterX static method found on the supplied classes,
|
|
124
129
|
* in order. Returns the (possibly mutated) request and response pair.
|
|
130
|
+
*
|
|
131
|
+
* ASYNC — each hook is awaited (e.g. the responseCache after-hook awaiting
|
|
132
|
+
* `backend.set`). Awaiting a synchronous hook is harmless, so existing sync
|
|
133
|
+
* after-hooks keep working unchanged.
|
|
125
134
|
*/
|
|
126
|
-
static runAfter(
|
|
135
|
+
static async runAfter(
|
|
127
136
|
classes: any[],
|
|
128
137
|
req: Tina4Request,
|
|
129
138
|
res: Tina4Response,
|
|
130
|
-
): [Tina4Request, Tina4Response] {
|
|
139
|
+
): Promise<[Tina4Request, Tina4Response]> {
|
|
131
140
|
for (const cls of classes) {
|
|
132
141
|
const methods = Object.getOwnPropertyNames(cls).filter(
|
|
133
142
|
(name) => typeof cls[name] === "function" && name.startsWith("after"),
|
|
134
143
|
);
|
|
135
144
|
for (const method of methods) {
|
|
136
|
-
const result = cls[method](req, res);
|
|
145
|
+
const result = await cls[method](req, res);
|
|
137
146
|
if (Array.isArray(result)) {
|
|
138
147
|
[req, res] = result as [Tina4Request, Tina4Response];
|
|
139
148
|
}
|
|
@@ -1142,7 +1142,7 @@ ${reset}
|
|
|
1142
1142
|
...new Set([...Router.getClassMiddlewares(), ...MiddlewareRunner.getGlobal()]),
|
|
1143
1143
|
];
|
|
1144
1144
|
if (globalMiddleware.length > 0) {
|
|
1145
|
-
const [, , proceed] = MiddlewareRunner.runBefore(globalMiddleware, req, res);
|
|
1145
|
+
const [, , proceed] = await MiddlewareRunner.runBefore(globalMiddleware, req, res);
|
|
1146
1146
|
if (!proceed || res.raw.writableEnded) return;
|
|
1147
1147
|
}
|
|
1148
1148
|
|
|
@@ -1240,7 +1240,7 @@ ${reset}
|
|
|
1240
1240
|
// Header mutations here are no-ops once the response is flushed (Node
|
|
1241
1241
|
// sends headers with the body) — set response headers in beforeX.
|
|
1242
1242
|
if (globalMiddleware.length > 0) {
|
|
1243
|
-
MiddlewareRunner.runAfter(globalMiddleware, req, res);
|
|
1243
|
+
await MiddlewareRunner.runAfter(globalMiddleware, req, res);
|
|
1244
1244
|
}
|
|
1245
1245
|
|
|
1246
1246
|
if (!res.raw.writableEnded) {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import { QueryCache } from "./sqlTranslation.js";
|
|
33
33
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
34
|
+
import type { CacheBackend } from "@tina4/core";
|
|
34
35
|
|
|
35
36
|
function isTruthy(val: string | undefined): boolean {
|
|
36
37
|
return ["true", "1", "yes", "on"].includes((val ?? "").trim().toLowerCase());
|
|
@@ -71,6 +72,25 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
71
72
|
private hits: number = 0;
|
|
72
73
|
private misses: number = 0;
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Persistent-mode distributed backend (TINA4_DB_CACHE=true). Built lazily from
|
|
77
|
+
* the SAME unified `createBackend()` factory the response/KV cache uses, so
|
|
78
|
+
* multiple Database instances share one cache with global write-invalidation
|
|
79
|
+
* (parity with Python's connection.py, which routes the persistent DB cache
|
|
80
|
+
* through `_create_backend`). The read path (`fetchAsync`/`fetchOneAsync`/
|
|
81
|
+
* `queryAsync`) is async, so the backend's async get/set work directly — no
|
|
82
|
+
* sync-path restriction. Request-scoped mode keeps the in-process QueryCache
|
|
83
|
+
* above (ephemeral, fastest, never serialized).
|
|
84
|
+
*
|
|
85
|
+
* `null` until the first async read builds it; a `memory` backend (the
|
|
86
|
+
* default) means the persistent layer behaves in-process exactly as before, so
|
|
87
|
+
* default behaviour is unchanged and only an explicit redis/etc. backend
|
|
88
|
+
* distributes.
|
|
89
|
+
*/
|
|
90
|
+
private backend: CacheBackend | null = null;
|
|
91
|
+
private backendPromise: Promise<CacheBackend | null> | null = null;
|
|
92
|
+
private backendName: string;
|
|
93
|
+
|
|
74
94
|
constructor(adapter: DatabaseAdapter, options: CachedAdapterOptions = {}) {
|
|
75
95
|
this.adapter = adapter;
|
|
76
96
|
this.cachePersistent = options.persistent ?? isTruthy(process.env.TINA4_DB_CACHE);
|
|
@@ -87,9 +107,53 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
this.cache = options.sharedCache ?? new QueryCache({ defaultTtl: this.ttl, maxSize: 10000 });
|
|
110
|
+
|
|
111
|
+
// Persistent mode now routes through the unified async backend (the read
|
|
112
|
+
// path — fetchAsync/fetchOneAsync/queryAsync — is async, so the backend's
|
|
113
|
+
// async get/set work directly). TINA4_DB_CACHE_BACKEND + TINA4_DB_CACHE_URL
|
|
114
|
+
// select the backend (default `memory` = in-process, unchanged behaviour).
|
|
115
|
+
// The backend is built lazily on first async read so an unreachable network
|
|
116
|
+
// backend degrades to `file` (via createBackend's own fallback) without
|
|
117
|
+
// blocking construction.
|
|
118
|
+
this.backendName = (process.env.TINA4_DB_CACHE_BACKEND ?? "memory").toLowerCase().trim();
|
|
119
|
+
|
|
90
120
|
CachedDatabaseAdapter.instances.add(this);
|
|
91
121
|
}
|
|
92
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Whether the persistent layer should use a distributed/serialised backend.
|
|
125
|
+
* For the default `memory` backend we keep the in-process QueryCache (fast,
|
|
126
|
+
* no serialisation) so behaviour is identical to before; only an explicit
|
|
127
|
+
* non-memory backend (redis/valkey/memcached/mongodb/database/file) routes
|
|
128
|
+
* through the unified async backend for cross-instance sharing.
|
|
129
|
+
*/
|
|
130
|
+
private usesPersistentBackend(): boolean {
|
|
131
|
+
return this.cachePersistent && this.backendName !== "memory";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Lazily build (and memoise) the persistent backend via createBackend(). */
|
|
135
|
+
private async getBackend(): Promise<CacheBackend | null> {
|
|
136
|
+
if (this.backend) return this.backend;
|
|
137
|
+
if (!this.backendPromise) {
|
|
138
|
+
this.backendPromise = (async () => {
|
|
139
|
+
try {
|
|
140
|
+
// Dynamic import keeps @tina4/orm free of an import-time cycle with
|
|
141
|
+
// @tina4/core (whose `database` backend dynamically imports @tina4/orm).
|
|
142
|
+
const core: any = await import("@tina4/core");
|
|
143
|
+
const b: CacheBackend = await core.createBackend({
|
|
144
|
+
backend: this.backendName,
|
|
145
|
+
cacheUrl: process.env.TINA4_DB_CACHE_URL,
|
|
146
|
+
});
|
|
147
|
+
this.backend = b;
|
|
148
|
+
return b;
|
|
149
|
+
} catch {
|
|
150
|
+
return null; // fall back to the in-process QueryCache
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
}
|
|
154
|
+
return this.backendPromise;
|
|
155
|
+
}
|
|
156
|
+
|
|
93
157
|
// ── Cache mode helpers ────────────────────────────────────
|
|
94
158
|
|
|
95
159
|
/** Current cache mode: "persistent" | "request" | "off". */
|
|
@@ -134,15 +198,21 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
134
198
|
|
|
135
199
|
cacheStats(): {
|
|
136
200
|
enabled: boolean; mode: "persistent" | "request" | "off";
|
|
137
|
-
hits: number; misses: number; size: number; ttl: number;
|
|
201
|
+
hits: number; misses: number; size: number; ttl: number; backend?: string;
|
|
138
202
|
} {
|
|
139
203
|
return {
|
|
140
204
|
enabled: this.enabled,
|
|
141
205
|
mode: this.cacheMode(),
|
|
142
206
|
hits: this.hits,
|
|
143
207
|
misses: this.misses,
|
|
208
|
+
// `size` is the in-process figure. When a distributed persistent backend
|
|
209
|
+
// is active the authoritative size lives in redis/etc.; the hit/miss
|
|
210
|
+
// counters here are still real (tracked locally per the read path).
|
|
144
211
|
size: this.cache.size(),
|
|
145
212
|
ttl: this.ttl,
|
|
213
|
+
// Report the actually-configured persistent backend so the operator sees
|
|
214
|
+
// where cross-instance entries land (parity with Python's cache_stats).
|
|
215
|
+
backend: this.usesPersistentBackend() ? this.backendName : "memory",
|
|
146
216
|
};
|
|
147
217
|
}
|
|
148
218
|
|
|
@@ -151,11 +221,68 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
151
221
|
this.cache.clear();
|
|
152
222
|
this.hits = 0;
|
|
153
223
|
this.misses = 0;
|
|
224
|
+
// Best-effort flush of the distributed backend too (fire-and-forget — this
|
|
225
|
+
// method is synchronous to keep db.cacheClear() ergonomic).
|
|
226
|
+
if (this.usesPersistentBackend()) {
|
|
227
|
+
void this.getBackend().then((b) => b?.clear()).catch(() => { /* best effort */ });
|
|
228
|
+
}
|
|
154
229
|
}
|
|
155
230
|
|
|
156
231
|
/** Clear the entire query cache (called on writes). */
|
|
157
232
|
private invalidate(): void {
|
|
158
233
|
this.cache.clear();
|
|
234
|
+
// Persistent distributed backend: clear it too so a write on ANY instance
|
|
235
|
+
// invalidates entries cached by ALL instances (global write-invalidation,
|
|
236
|
+
// parity with Python's _cache_invalidate → backend.clear()). Fire-and-forget
|
|
237
|
+
// on the sync write path (execute/insert/...); the async write methods below
|
|
238
|
+
// await invalidateAsync() instead for deterministic ordering.
|
|
239
|
+
if (this.usesPersistentBackend()) {
|
|
240
|
+
void this.getBackend().then((b) => b?.clear()).catch(() => { /* best effort */ });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Async write-invalidation — awaits the distributed backend clear. */
|
|
245
|
+
private async invalidateAsync(): Promise<void> {
|
|
246
|
+
this.cache.clear();
|
|
247
|
+
if (this.usesPersistentBackend()) {
|
|
248
|
+
const b = await this.getBackend();
|
|
249
|
+
if (b) { try { await b.clear(); } catch { /* best effort */ } }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Persistent-backend get/set helpers (serialised rows) ──
|
|
254
|
+
//
|
|
255
|
+
// The persistent backend stores the adapter's row payload (T[] for fetch/
|
|
256
|
+
// query, {row:T|null} for fetchOne) as plain JSON — every backend (redis
|
|
257
|
+
// SETEX, mongo doc, db row) round-trips it. fetchOne is wrapped in an object
|
|
258
|
+
// so a genuine `null` row is distinguishable from a cache miss (the backend
|
|
259
|
+
// returns undefined on miss).
|
|
260
|
+
|
|
261
|
+
private async backendGetRows<T>(key: string): Promise<T[] | undefined> {
|
|
262
|
+
const b = await this.getBackend();
|
|
263
|
+
if (!b) return undefined;
|
|
264
|
+
const raw = await b.get(key);
|
|
265
|
+
return Array.isArray(raw) ? (raw as T[]) : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async backendSetRows<T>(key: string, rows: T[]): Promise<void> {
|
|
269
|
+
const b = await this.getBackend();
|
|
270
|
+
if (b) { try { await b.set(key, rows, this.ttl); } catch { /* best effort */ } }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async backendGetOne<T>(key: string): Promise<{ row: T | null } | undefined> {
|
|
274
|
+
const b = await this.getBackend();
|
|
275
|
+
if (!b) return undefined;
|
|
276
|
+
const raw = await b.get(key);
|
|
277
|
+
if (raw && typeof raw === "object" && "row" in (raw as object)) {
|
|
278
|
+
return raw as { row: T | null };
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async backendSetOne<T>(key: string, row: T | null): Promise<void> {
|
|
284
|
+
const b = await this.getBackend();
|
|
285
|
+
if (b) { try { await b.set(key, { row }, this.ttl); } catch { /* best effort */ } }
|
|
159
286
|
}
|
|
160
287
|
|
|
161
288
|
// ── DatabaseAdapter interface — writes flush, reads cache ──
|
|
@@ -291,88 +418,113 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
291
418
|
// cache sits in front of the async path too. Reads cache; writes flush.
|
|
292
419
|
|
|
293
420
|
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
|
|
421
|
+
const run = async (): Promise<T[]> => (this.adapter as any).fetchAsync
|
|
422
|
+
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
423
|
+
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
294
424
|
if (this.enabled) {
|
|
295
425
|
const key = QueryCache.queryKey(sql + `:L${limit}:S${skip}`, params as unknown[] | undefined);
|
|
426
|
+
// Persistent distributed backend is AUTHORITATIVE (mirrors Python, where a
|
|
427
|
+
// configured _cache_backend bypasses the in-process dict). This keeps
|
|
428
|
+
// cross-instance write-invalidation deterministic: a write clears the
|
|
429
|
+
// shared backend, so every instance misses on its next read.
|
|
430
|
+
if (this.usesPersistentBackend()) {
|
|
431
|
+
const shared = await this.backendGetRows<T>(key);
|
|
432
|
+
if (shared !== undefined) { this.hits++; return shared; }
|
|
433
|
+
const result = await run();
|
|
434
|
+
await this.backendSetRows(key, result);
|
|
435
|
+
this.misses++;
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
296
438
|
const cached = this.cache.get<T[]>(key);
|
|
297
439
|
if (cached !== undefined) {
|
|
298
440
|
this.hits++;
|
|
299
441
|
return cached;
|
|
300
442
|
}
|
|
301
|
-
const result = (
|
|
302
|
-
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
303
|
-
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
443
|
+
const result = await run();
|
|
304
444
|
this.cache.set(key, result, this.ttl);
|
|
305
445
|
this.misses++;
|
|
306
446
|
return result;
|
|
307
447
|
}
|
|
308
|
-
return (
|
|
309
|
-
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
310
|
-
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
448
|
+
return run();
|
|
311
449
|
}
|
|
312
450
|
|
|
313
451
|
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
452
|
+
const run = async (): Promise<T | null> => (this.adapter as any).fetchOneAsync
|
|
453
|
+
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
454
|
+
: this.adapter.fetchOne<T>(sql, params);
|
|
314
455
|
if (this.enabled) {
|
|
315
456
|
const key = QueryCache.queryKey(sql + ":ONE", params as unknown[] | undefined);
|
|
457
|
+
if (this.usesPersistentBackend()) {
|
|
458
|
+
const shared = await this.backendGetOne<T>(key);
|
|
459
|
+
if (shared !== undefined) { this.hits++; return shared.row; }
|
|
460
|
+
const result = await run();
|
|
461
|
+
await this.backendSetOne(key, result);
|
|
462
|
+
this.misses++;
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
316
465
|
const cached = this.cache.get<T | null>(key);
|
|
317
466
|
if (cached !== undefined) {
|
|
318
467
|
this.hits++;
|
|
319
468
|
return cached;
|
|
320
469
|
}
|
|
321
|
-
const result = (
|
|
322
|
-
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
323
|
-
: this.adapter.fetchOne<T>(sql, params);
|
|
470
|
+
const result = await run();
|
|
324
471
|
this.cache.set(key, result, this.ttl);
|
|
325
472
|
this.misses++;
|
|
326
473
|
return result;
|
|
327
474
|
}
|
|
328
|
-
return (
|
|
329
|
-
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
330
|
-
: this.adapter.fetchOne<T>(sql, params);
|
|
475
|
+
return run();
|
|
331
476
|
}
|
|
332
477
|
|
|
333
478
|
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
479
|
+
const run = async (): Promise<T[]> => (this.adapter as any).queryAsync
|
|
480
|
+
? await (this.adapter as any).queryAsync(sql, params)
|
|
481
|
+
: this.adapter.query<T>(sql, params);
|
|
334
482
|
if (this.enabled) {
|
|
335
483
|
const key = QueryCache.queryKey(sql + ":Q", params as unknown[] | undefined);
|
|
484
|
+
if (this.usesPersistentBackend()) {
|
|
485
|
+
const shared = await this.backendGetRows<T>(key);
|
|
486
|
+
if (shared !== undefined) { this.hits++; return shared; }
|
|
487
|
+
const result = await run();
|
|
488
|
+
await this.backendSetRows(key, result);
|
|
489
|
+
this.misses++;
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
336
492
|
const cached = this.cache.get<T[]>(key);
|
|
337
493
|
if (cached !== undefined) {
|
|
338
494
|
this.hits++;
|
|
339
495
|
return cached;
|
|
340
496
|
}
|
|
341
|
-
const result = (
|
|
342
|
-
? await (this.adapter as any).queryAsync(sql, params)
|
|
343
|
-
: this.adapter.query<T>(sql, params);
|
|
497
|
+
const result = await run();
|
|
344
498
|
this.cache.set(key, result, this.ttl);
|
|
345
499
|
this.misses++;
|
|
346
500
|
return result;
|
|
347
501
|
}
|
|
348
|
-
return (
|
|
349
|
-
? await (this.adapter as any).queryAsync(sql, params)
|
|
350
|
-
: this.adapter.query<T>(sql, params);
|
|
502
|
+
return run();
|
|
351
503
|
}
|
|
352
504
|
|
|
353
505
|
async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
|
|
354
|
-
if (this.enabled) this.
|
|
506
|
+
if (this.enabled) await this.invalidateAsync();
|
|
355
507
|
return (this.adapter as any).executeAsync
|
|
356
508
|
? await (this.adapter as any).executeAsync(sql, params)
|
|
357
509
|
: this.adapter.execute(sql, params);
|
|
358
510
|
}
|
|
359
511
|
|
|
360
512
|
async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
|
|
361
|
-
if (this.enabled) this.
|
|
513
|
+
if (this.enabled) await this.invalidateAsync();
|
|
362
514
|
return (this.adapter as any).insertAsync
|
|
363
515
|
? await (this.adapter as any).insertAsync(table, data)
|
|
364
516
|
: this.adapter.insert(table, data);
|
|
365
517
|
}
|
|
366
518
|
|
|
367
519
|
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): Promise<DatabaseResult> {
|
|
368
|
-
if (this.enabled) this.
|
|
520
|
+
if (this.enabled) await this.invalidateAsync();
|
|
369
521
|
return (this.adapter as any).updateAsync
|
|
370
522
|
? await (this.adapter as any).updateAsync(table, data, filter, params)
|
|
371
523
|
: this.adapter.update(table, data, filter);
|
|
372
524
|
}
|
|
373
525
|
|
|
374
526
|
async deleteAsync(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): Promise<DatabaseResult> {
|
|
375
|
-
if (this.enabled) this.
|
|
527
|
+
if (this.enabled) await this.invalidateAsync();
|
|
376
528
|
return (this.adapter as any).deleteAsync
|
|
377
529
|
? await (this.adapter as any).deleteAsync(table, filter, params)
|
|
378
530
|
: this.adapter.delete(table, filter as Record<string, unknown>);
|
|
@@ -835,7 +835,7 @@ export class Database {
|
|
|
835
835
|
* we read the live counters + size + mode from it. Mirrors Python's
|
|
836
836
|
* `Database.cache_stats()`: `{ enabled, mode, hits, misses, size, ttl }`.
|
|
837
837
|
*/
|
|
838
|
-
cacheStats(): { enabled: boolean; mode: "persistent" | "request" | "off"; hits: number; misses: number; size: number; ttl: number } {
|
|
838
|
+
cacheStats(): { enabled: boolean; mode: "persistent" | "request" | "off"; hits: number; misses: number; size: number; ttl: number; backend?: string } {
|
|
839
839
|
const adapter = this.getNextAdapter();
|
|
840
840
|
if (adapter instanceof CachedDatabaseAdapter) {
|
|
841
841
|
return adapter.cacheStats();
|