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.
@@ -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";
@@ -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 { cacheStats } = require("@tina4/core");
1163
- return cacheStats?.() ?? {};
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 = (this.adapter as any).fetchAsync
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 (this.adapter as any).fetchAsync
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 = (this.adapter as any).fetchOneAsync
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 (this.adapter as any).fetchOneAsync
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 = (this.adapter as any).queryAsync
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 (this.adapter as any).queryAsync
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();