@juspay/neurolink 10.1.1 → 10.2.0

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.
@@ -700,6 +700,40 @@ export type ProxyStats = {
700
700
  totalQuotaRateLimits: number;
701
701
  accounts: Record<string, AccountStats>;
702
702
  };
703
+ /** Durability and reconciliation state for the proxy usage counters. */
704
+ export type ProxyStatsPersistenceStatus = {
705
+ enabled: boolean;
706
+ filePath: string | null;
707
+ revision: number;
708
+ pendingMutations: number;
709
+ inFlightMutations: number;
710
+ unpersistedMutations: number;
711
+ lastFlushedAt: number | null;
712
+ lastReconciledAt: number | null;
713
+ lastRecoveryAt: number | null;
714
+ lastError: string | null;
715
+ };
716
+ /** Versioned on-disk snapshot shared by overlapping proxy workers. */
717
+ export type PersistedProxyStatsSnapshot = {
718
+ schemaVersion: 1;
719
+ revision: number;
720
+ updatedAt: number;
721
+ stats: ProxyStats;
722
+ };
723
+ /** Ownership metadata for the proxy statistics cross-process lock. */
724
+ export type ProxyStatsLockOwner = {
725
+ token: string;
726
+ pid: number;
727
+ acquiredAt: number;
728
+ };
729
+ /** Construction options for an isolated proxy statistics store. */
730
+ export type ProxyUsageStatsStoreOptions = {
731
+ filePath?: string;
732
+ flushIntervalMs?: number;
733
+ lockTimeoutMs?: number;
734
+ staleLockMs?: number;
735
+ now?: () => number;
736
+ };
703
737
  export type RefreshableAccount = {
704
738
  token: string;
705
739
  refreshToken?: string;
@@ -874,6 +908,8 @@ export type ProxyPaths = {
874
908
  quotaFile: string;
875
909
  /** account-cooldowns.json — restart-safe account cooldown state */
876
910
  cooldownFile: string;
911
+ /** proxy-usage-stats.json — restart- and handoff-safe usage counters */
912
+ statsFile?: string;
877
913
  /** Whether this is a dev-mode isolated instance */
878
914
  isDev: boolean;
879
915
  };
@@ -1876,6 +1912,7 @@ export type ProxyStartApp = {
1876
1912
  };
1877
1913
  /** Stats shape consumed by the proxy status printer. */
1878
1914
  export type StatusStats = {
1915
+ startedAt?: number;
1879
1916
  totalAttempts?: number;
1880
1917
  totalAttemptErrors?: number;
1881
1918
  totalRequests: number;
@@ -1896,7 +1933,9 @@ export type StatusStats = {
1896
1933
  transientRateLimits?: number;
1897
1934
  quotaRateLimits?: number;
1898
1935
  cooling: boolean;
1936
+ status?: "active" | "cooling" | "disabled" | "excluded" | "removed";
1899
1937
  }[];
1938
+ persistence?: ProxyStatsPersistenceStatus;
1900
1939
  };
1901
1940
  /** Sub-action of the `proxy telemetry` CLI command. */
1902
1941
  export type ProxyTelemetryAction = "setup" | "start" | "stop" | "status" | "logs" | "import-dashboard";
@@ -23,7 +23,11 @@ import { normalizeUrlForCache, redactUrlForError } from "./logSanitize.js";
23
23
  */
24
24
  export class ImageCache {
25
25
  cache = new Map();
26
- contentHashIndex = new Map(); // contentHash -> url
26
+ // contentHash -> set of normalized urls whose cache entry holds that
27
+ // content. A Set (not a single url) so that evicting/deleting one url
28
+ // doesn't drop the dedup mapping for other urls still caching the same
29
+ // content (#1213).
30
+ contentHashIndex = new Map();
27
31
  maxSize;
28
32
  ttlMs;
29
33
  maxImageSize;
@@ -164,11 +168,37 @@ export class ImageCache {
164
168
  * Useful for deduplication when the same image is accessed via different URLs
165
169
  */
166
170
  getByContentHash(contentHash) {
167
- const url = this.contentHashIndex.get(contentHash);
168
- if (!url) {
171
+ const urlSet = this.contentHashIndex.get(contentHash);
172
+ if (!urlSet) {
169
173
  return null;
170
174
  }
171
- return this.get(url);
175
+ // Find the first url whose cache entry is still live, pruning any url
176
+ // whose entry no longer exists in `this.cache` along the way. Entries
177
+ // that exist but are expired are left in the set (not "gone") - the
178
+ // normal TTL/get() path is responsible for reaping those.
179
+ let liveUrl = null;
180
+ // Iterate a copy so reaping below can mutate the set safely.
181
+ for (const url of [...urlSet]) {
182
+ const entry = this.cache.get(url);
183
+ if (!entry) {
184
+ urlSet.delete(url);
185
+ continue;
186
+ }
187
+ if (this.isExpired(entry)) {
188
+ // Reap expired entries here too (matching get() semantics) so stale
189
+ // urls don't accumulate when callers primarily use getByContentHash.
190
+ this.cache.delete(url);
191
+ urlSet.delete(url);
192
+ continue;
193
+ }
194
+ if (liveUrl === null) {
195
+ liveUrl = url;
196
+ }
197
+ }
198
+ if (urlSet.size === 0) {
199
+ this.contentHashIndex.delete(contentHash);
200
+ }
201
+ return liveUrl ? this.get(liveUrl) : null;
172
202
  }
173
203
  /**
174
204
  * Store an image in the cache
@@ -192,21 +222,42 @@ export class ImageCache {
192
222
  }
193
223
  // Generate content hash
194
224
  const contentHash = this.generateContentHash(imageData);
195
- // Check if same content already exists under different URL
196
- const existingUrl = this.contentHashIndex.get(contentHash);
197
- if (existingUrl && existingUrl !== normalizedUrl) {
198
- // Content already cached under different URL - create a shallow copy
199
- const existingEntry = this.cache.get(existingUrl);
200
- if (existingEntry && !this.isExpired(existingEntry)) {
201
- // Create a shallow copy for the new URL to avoid shared reference issues
202
- this.cache.set(normalizedUrl, { ...existingEntry });
203
- // Update content hash index to point to the new URL as well
204
- this.contentHashIndex.set(contentHash, normalizedUrl);
205
- logger.debug("Image cache dedup hit", {
206
- newUrl: redactUrlForError(normalizedUrl),
207
- existingUrl: redactUrlForError(existingUrl),
208
- });
209
- return;
225
+ // Check if same content already exists under a different URL
226
+ const existingUrls = this.contentHashIndex.get(contentHash);
227
+ if (existingUrls) {
228
+ for (const existingUrl of existingUrls) {
229
+ if (existingUrl === normalizedUrl) {
230
+ continue;
231
+ }
232
+ // Content already cached under a different URL - create a shallow copy
233
+ const existingEntry = this.cache.get(existingUrl);
234
+ if (existingEntry && !this.isExpired(existingEntry)) {
235
+ // A dedup hit still adds a cache entry for the new URL, so enforce
236
+ // capacity first — otherwise many URLs sharing one content hash could
237
+ // grow the cache past maxSize.
238
+ while (this.cache.size >= this.maxSize && this.cache.size > 0) {
239
+ this.evictOldest();
240
+ }
241
+ // Create a shallow copy for the new URL to avoid shared reference issues
242
+ this.cache.set(normalizedUrl, { ...existingEntry });
243
+ // Re-fetch (or re-create) the hash's URL set: the eviction above may
244
+ // have removed this hash's last url, emptying the set and deleting it
245
+ // from contentHashIndex — reusing the stale `existingUrls` reference
246
+ // would add the new url to an orphaned set that the index no longer
247
+ // points to, leaving a live entry unreachable by content hash
248
+ // (the exact #1213 failure mode). Never replace an existing mapping.
249
+ let hashUrls = this.contentHashIndex.get(contentHash);
250
+ if (!hashUrls) {
251
+ hashUrls = new Set();
252
+ this.contentHashIndex.set(contentHash, hashUrls);
253
+ }
254
+ hashUrls.add(normalizedUrl);
255
+ logger.debug("Image cache dedup hit", {
256
+ newUrl: redactUrlForError(normalizedUrl),
257
+ existingUrl: redactUrlForError(existingUrl),
258
+ });
259
+ return;
260
+ }
210
261
  }
211
262
  }
212
263
  // Evict if at capacity
@@ -224,7 +275,12 @@ export class ImageCache {
224
275
  accessCount: 1,
225
276
  };
226
277
  this.cache.set(normalizedUrl, entry);
227
- this.contentHashIndex.set(contentHash, normalizedUrl);
278
+ let urlSet = this.contentHashIndex.get(contentHash);
279
+ if (!urlSet) {
280
+ urlSet = new Set();
281
+ this.contentHashIndex.set(contentHash, urlSet);
282
+ }
283
+ urlSet.add(normalizedUrl);
228
284
  logger.debug("Image cached", {
229
285
  url: redactUrlForError(normalizedUrl),
230
286
  size,
@@ -239,9 +295,14 @@ export class ImageCache {
239
295
  const normalizedUrl = this.normalizeUrl(url);
240
296
  const entry = this.cache.get(normalizedUrl);
241
297
  if (entry) {
242
- // Remove from content hash index
243
- if (this.contentHashIndex.get(entry.contentHash) === normalizedUrl) {
244
- this.contentHashIndex.delete(entry.contentHash);
298
+ // Remove this URL from its content hash's URL set; only drop the
299
+ // hash entry once no URL maps to that content anymore (#1213).
300
+ const urlSet = this.contentHashIndex.get(entry.contentHash);
301
+ if (urlSet) {
302
+ urlSet.delete(normalizedUrl);
303
+ if (urlSet.size === 0) {
304
+ this.contentHashIndex.delete(entry.contentHash);
305
+ }
245
306
  }
246
307
  this.cache.delete(normalizedUrl);
247
308
  return true;
@@ -257,8 +318,15 @@ export class ImageCache {
257
318
  if (oldestKey !== undefined) {
258
319
  const entry = this.cache.get(oldestKey);
259
320
  if (entry) {
260
- if (this.contentHashIndex.get(entry.contentHash) === oldestKey) {
261
- this.contentHashIndex.delete(entry.contentHash);
321
+ // Remove only the evicted URL from its content hash's URL set; the
322
+ // hash entry is dropped only once no URL maps to it anymore
323
+ // (#1213).
324
+ const urlSet = this.contentHashIndex.get(entry.contentHash);
325
+ if (urlSet) {
326
+ urlSet.delete(oldestKey);
327
+ if (urlSet.size === 0) {
328
+ this.contentHashIndex.delete(entry.contentHash);
329
+ }
262
330
  }
263
331
  }
264
332
  this.cache.delete(oldestKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.1.1",
3
+ "version": "10.2.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -179,9 +179,10 @@
179
179
  "proxy:observability:doctor": "node scripts/observability/check-proxy-telemetry.mjs",
180
180
  "proxy:observability:import-dashboard": "bash scripts/observability/manage-local-openobserve.sh import-dashboard",
181
181
  "proxy:performance:lifecycle": "tsx tools/testing/proxyLifecycleBenchmark.ts",
182
+ "proxy:performance:stats": "tsx tools/testing/proxyUsageStatsBenchmark.ts",
182
183
  "proxy:performance:transport": "tsx tools/testing/proxyTransportBenchmark.ts",
183
184
  "proxy:performance:rolling": "tsx tools/testing/proxyRollingHandoffBenchmark.ts",
184
- "proxy:performance": "pnpm run proxy:performance:lifecycle && pnpm run proxy:performance:transport && pnpm run proxy:performance:rolling",
185
+ "proxy:performance": "pnpm run proxy:performance:lifecycle && pnpm run proxy:performance:stats && pnpm run proxy:performance:transport && pnpm run proxy:performance:rolling",
185
186
  "// Development & Monitoring": "",
186
187
  "dev:health": "tsx tools/development/healthMonitor.ts",
187
188
  "dev:demo": "concurrently \"pnpm run dev\" \"node neurolink-demo/complete-enhanced-server.js\"",
@@ -621,7 +622,8 @@
621
622
  "esbuild@>=0.17.0 <0.28.1": ">=0.28.1",
622
623
  "rollup@>=4.0.0 <4.59.0": ">=4.59.0",
623
624
  "shell-quote@<1.8.4": ">=1.8.4",
624
- "undici@>=8.0.0": ">=7.24.0 <8.0.0"
625
+ "undici@>=8.0.0": ">=7.24.0 <8.0.0",
626
+ "pdfjs-dist": "5.4.624"
625
627
  },
626
628
  "patchedDependencies": {
627
629
  "mammoth@1.12.0": "patches/mammoth@1.12.0.patch"