tina4-nodejs 3.13.23 → 3.13.24
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 +26 -16
- package/package.json +3 -2
- package/packages/core/src/cache.ts +964 -123
- package/packages/core/src/index.ts +2 -2
- package/packages/core/src/mcp.ts +14 -2
- package/packages/orm/src/cachedDatabase.ts +47 -19
- 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
|
}
|
|
@@ -36,6 +36,12 @@ function isTruthy(val: string | undefined): boolean {
|
|
|
36
36
|
return ["true", "1", "yes", "on"].includes((val ?? "").trim().toLowerCase());
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Network backends that cannot serve a SYNCHRONOUS db.fetch() path. */
|
|
40
|
+
const NETWORK_DB_CACHE_BACKENDS = new Set(["redis", "valkey", "memcached", "memcache", "mongodb", "mongo"]);
|
|
41
|
+
|
|
42
|
+
/** One-time guard so the distributed-DB-cache warning logs once per process. */
|
|
43
|
+
let _warnedNetworkDbCache = false;
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Options for wrapping an adapter with a query cache. When several pooled
|
|
41
47
|
* connections must share one cache store (so a write on any connection
|
|
@@ -87,6 +93,30 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
this.cache = options.sharedCache ?? new QueryCache({ defaultTtl: this.ttl, maxSize: 10000 });
|
|
96
|
+
|
|
97
|
+
// Persistent mode runs on the SYNCHRONOUS db.fetch() path, which cannot
|
|
98
|
+
// await an async cache backend. So the persistent DB-query cache is always
|
|
99
|
+
// the in-process QueryCache above (synchronous, no serialization-over-the-
|
|
100
|
+
// wire). If the operator selects a NETWORK backend via TINA4_DB_CACHE_BACKEND
|
|
101
|
+
// (redis/valkey/memcached/mongodb), warn once and fall back to in-process
|
|
102
|
+
// memory — distributed DB-query caching isn't available on Node's sync
|
|
103
|
+
// fetch path; use the response cache (cacheGet/cacheSet) for distributed
|
|
104
|
+
// caching. (File/database/memory selections also resolve to the in-process
|
|
105
|
+
// store here — the unified network backends live behind the async KV API.)
|
|
106
|
+
if (this.cachePersistent) {
|
|
107
|
+
const backendName = (process.env.TINA4_DB_CACHE_BACKEND ?? "memory").toLowerCase().trim();
|
|
108
|
+
if (NETWORK_DB_CACHE_BACKENDS.has(backendName) && !_warnedNetworkDbCache) {
|
|
109
|
+
_warnedNetworkDbCache = true;
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.warn(
|
|
112
|
+
`[tina4] TINA4_DB_CACHE_BACKEND='${backendName}' selects a distributed cache, ` +
|
|
113
|
+
`but Node's db.fetch() is synchronous so distributed DB-query caching is not ` +
|
|
114
|
+
`available on that path — falling back to the in-process memory store. ` +
|
|
115
|
+
`For distributed caching use the response cache (cacheGet/cacheSet).`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
CachedDatabaseAdapter.instances.add(this);
|
|
91
121
|
}
|
|
92
122
|
|
|
@@ -134,7 +164,7 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
134
164
|
|
|
135
165
|
cacheStats(): {
|
|
136
166
|
enabled: boolean; mode: "persistent" | "request" | "off";
|
|
137
|
-
hits: number; misses: number; size: number; ttl: number;
|
|
167
|
+
hits: number; misses: number; size: number; ttl: number; backend?: string;
|
|
138
168
|
} {
|
|
139
169
|
return {
|
|
140
170
|
enabled: this.enabled,
|
|
@@ -143,6 +173,7 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
143
173
|
misses: this.misses,
|
|
144
174
|
size: this.cache.size(),
|
|
145
175
|
ttl: this.ttl,
|
|
176
|
+
backend: "memory",
|
|
146
177
|
};
|
|
147
178
|
}
|
|
148
179
|
|
|
@@ -291,6 +322,9 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
291
322
|
// cache sits in front of the async path too. Reads cache; writes flush.
|
|
292
323
|
|
|
293
324
|
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
|
|
325
|
+
const run = async (): Promise<T[]> => (this.adapter as any).fetchAsync
|
|
326
|
+
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
327
|
+
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
294
328
|
if (this.enabled) {
|
|
295
329
|
const key = QueryCache.queryKey(sql + `:L${limit}:S${skip}`, params as unknown[] | undefined);
|
|
296
330
|
const cached = this.cache.get<T[]>(key);
|
|
@@ -298,19 +332,18 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
298
332
|
this.hits++;
|
|
299
333
|
return cached;
|
|
300
334
|
}
|
|
301
|
-
const result = (
|
|
302
|
-
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
303
|
-
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
335
|
+
const result = await run();
|
|
304
336
|
this.cache.set(key, result, this.ttl);
|
|
305
337
|
this.misses++;
|
|
306
338
|
return result;
|
|
307
339
|
}
|
|
308
|
-
return (
|
|
309
|
-
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
310
|
-
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
340
|
+
return run();
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
344
|
+
const run = async (): Promise<T | null> => (this.adapter as any).fetchOneAsync
|
|
345
|
+
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
346
|
+
: this.adapter.fetchOne<T>(sql, params);
|
|
314
347
|
if (this.enabled) {
|
|
315
348
|
const key = QueryCache.queryKey(sql + ":ONE", params as unknown[] | undefined);
|
|
316
349
|
const cached = this.cache.get<T | null>(key);
|
|
@@ -318,19 +351,18 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
318
351
|
this.hits++;
|
|
319
352
|
return cached;
|
|
320
353
|
}
|
|
321
|
-
const result = (
|
|
322
|
-
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
323
|
-
: this.adapter.fetchOne<T>(sql, params);
|
|
354
|
+
const result = await run();
|
|
324
355
|
this.cache.set(key, result, this.ttl);
|
|
325
356
|
this.misses++;
|
|
326
357
|
return result;
|
|
327
358
|
}
|
|
328
|
-
return (
|
|
329
|
-
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
330
|
-
: this.adapter.fetchOne<T>(sql, params);
|
|
359
|
+
return run();
|
|
331
360
|
}
|
|
332
361
|
|
|
333
362
|
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
363
|
+
const run = async (): Promise<T[]> => (this.adapter as any).queryAsync
|
|
364
|
+
? await (this.adapter as any).queryAsync(sql, params)
|
|
365
|
+
: this.adapter.query<T>(sql, params);
|
|
334
366
|
if (this.enabled) {
|
|
335
367
|
const key = QueryCache.queryKey(sql + ":Q", params as unknown[] | undefined);
|
|
336
368
|
const cached = this.cache.get<T[]>(key);
|
|
@@ -338,16 +370,12 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
338
370
|
this.hits++;
|
|
339
371
|
return cached;
|
|
340
372
|
}
|
|
341
|
-
const result = (
|
|
342
|
-
? await (this.adapter as any).queryAsync(sql, params)
|
|
343
|
-
: this.adapter.query<T>(sql, params);
|
|
373
|
+
const result = await run();
|
|
344
374
|
this.cache.set(key, result, this.ttl);
|
|
345
375
|
this.misses++;
|
|
346
376
|
return result;
|
|
347
377
|
}
|
|
348
|
-
return (
|
|
349
|
-
? await (this.adapter as any).queryAsync(sql, params)
|
|
350
|
-
: this.adapter.query<T>(sql, params);
|
|
378
|
+
return run();
|
|
351
379
|
}
|
|
352
380
|
|
|
353
381
|
async executeAsync(sql: string, params?: unknown[]): Promise<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();
|