diff --git a/src/infra/dedupe.ts b/src/infra/dedupe.ts index 6ee53b3dafa8..821e07cecbdc 100644 --- a/src/infra/dedupe.ts +++ b/src/infra/dedupe.ts @@ -1,5 +1,6 @@ import { resolveGlobalSingleton } from "../shared/global-singleton.js"; import { pruneMapToMaxSize } from "./map-size.js"; +import { resolveNonNegativeIntegerOption } from "./numeric-options.js"; export type DedupeCache = { check: (key: string | undefined | null, now?: number) => boolean; @@ -14,13 +15,12 @@ export type DedupeCacheOptions = { maxSize: number; }; -export function resolveDedupeNonNegativeInteger(value: number, fallback: number): number { - return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback; -} +/** @deprecated Use resolveNonNegativeIntegerOption for new internal numeric option normalization. */ +export { resolveNonNegativeIntegerOption as resolveDedupeNonNegativeInteger }; export function createDedupeCache(options: DedupeCacheOptions): DedupeCache { - const ttlMs = resolveDedupeNonNegativeInteger(options.ttlMs, 0); - const maxSize = resolveDedupeNonNegativeInteger(options.maxSize, 0); + const ttlMs = resolveNonNegativeIntegerOption(options.ttlMs, 0); + const maxSize = resolveNonNegativeIntegerOption(options.maxSize, 0); const cache = new Map(); const touch = (key: string, now: number) => { diff --git a/src/infra/numeric-options.ts b/src/infra/numeric-options.ts new file mode 100644 index 000000000000..2907881db379 --- /dev/null +++ b/src/infra/numeric-options.ts @@ -0,0 +1,3 @@ +export function resolveNonNegativeIntegerOption(value: number, fallback: number): number { + return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback; +} diff --git a/src/infra/outbound/directory-cache.test.ts b/src/infra/outbound/directory-cache.test.ts index 9ccd586d6a27..45f67769c05b 100644 --- a/src/infra/outbound/directory-cache.test.ts +++ b/src/infra/outbound/directory-cache.test.ts @@ -69,4 +69,16 @@ describe("DirectoryCache", () => { cache.clear(cfg); expect(cache.get("a", cfg)).toBeUndefined(); }); + + it("uses the default max size when maxSize is non-finite", () => { + const cache = new DirectoryCache(60_000, Number.NaN); + const cfg = {} as OpenClawConfig; + + for (let i = 0; i <= 2000; i++) { + cache.set(`key-${i}`, i, cfg); + } + + expect(cache.get("key-0", cfg)).toBeUndefined(); + expect(cache.get("key-2000", cfg)).toBe(2000); + }); }); diff --git a/src/infra/outbound/directory-cache.ts b/src/infra/outbound/directory-cache.ts index bbe7428f4982..14b2b1df5202 100644 --- a/src/infra/outbound/directory-cache.ts +++ b/src/infra/outbound/directory-cache.ts @@ -1,5 +1,6 @@ import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.public.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { resolveNonNegativeIntegerOption } from "../numeric-options.js"; type CacheEntry = { value: T; @@ -22,13 +23,12 @@ export function buildDirectoryCacheKey(key: DirectoryCacheKey): string { export class DirectoryCache { private readonly cache = new Map>(); private lastConfigRef: OpenClawConfig | null = null; + private readonly ttlMs: number; private readonly maxSize: number; - constructor( - private readonly ttlMs: number, - maxSize = 2000, - ) { - this.maxSize = Math.max(1, Math.floor(maxSize)); + constructor(ttlMs: number, maxSize = 2000) { + this.ttlMs = resolveNonNegativeIntegerOption(ttlMs, 0); + this.maxSize = Math.max(1, resolveNonNegativeIntegerOption(maxSize, 2000)); } get(key: string, cfg: OpenClawConfig): T | undefined { diff --git a/src/plugin-sdk/persistent-dedupe.ts b/src/plugin-sdk/persistent-dedupe.ts index ddaf29ac0e32..b130e1f671c0 100644 --- a/src/plugin-sdk/persistent-dedupe.ts +++ b/src/plugin-sdk/persistent-dedupe.ts @@ -1,4 +1,5 @@ -import { createDedupeCache, resolveDedupeNonNegativeInteger } from "../infra/dedupe.js"; +import { createDedupeCache } from "../infra/dedupe.js"; +import { resolveNonNegativeIntegerOption } from "../infra/numeric-options.js"; import type { FileLockOptions } from "./file-lock.js"; import { withFileLock } from "./file-lock.js"; import { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store.js"; @@ -148,9 +149,9 @@ function isRecentTimestamp(seenAt: number | undefined, ttlMs: number, now: numbe /** Create a dedupe helper that combines in-memory fast checks with a lock-protected disk store. */ export function createPersistentDedupe(options: PersistentDedupeOptions): PersistentDedupe { - const ttlMs = resolveDedupeNonNegativeInteger(options.ttlMs, 0); - const memoryMaxSize = resolveDedupeNonNegativeInteger(options.memoryMaxSize, 0); - const fileMaxEntries = Math.max(1, resolveDedupeNonNegativeInteger(options.fileMaxEntries, 1)); + const ttlMs = resolveNonNegativeIntegerOption(options.ttlMs, 0); + const memoryMaxSize = resolveNonNegativeIntegerOption(options.memoryMaxSize, 0); + const fileMaxEntries = Math.max(1, resolveNonNegativeIntegerOption(options.fileMaxEntries, 1)); const lockOptions = mergeLockOptions(options.lockOptions); const memory = createDedupeCache({ ttlMs, maxSize: memoryMaxSize }); const inflight = new Map>(); @@ -324,15 +325,15 @@ function createReleasedClaimError(scopedKey: string): Error { /** Create a claim/commit/release dedupe guard backed by memory and optional persistent storage. */ export function createClaimableDedupe(options: ClaimableDedupeOptions): ClaimableDedupe { - const ttlMs = resolveDedupeNonNegativeInteger(options.ttlMs, 0); - const memoryMaxSize = resolveDedupeNonNegativeInteger(options.memoryMaxSize, 0); + const ttlMs = resolveNonNegativeIntegerOption(options.ttlMs, 0); + const memoryMaxSize = resolveNonNegativeIntegerOption(options.memoryMaxSize, 0); const memory = createDedupeCache({ ttlMs, maxSize: memoryMaxSize }); const persistent = options.resolveFilePath != null ? createPersistentDedupe({ ttlMs, memoryMaxSize, - fileMaxEntries: Math.max(1, resolveDedupeNonNegativeInteger(options.fileMaxEntries, 1)), + fileMaxEntries: Math.max(1, resolveNonNegativeIntegerOption(options.fileMaxEntries, 1)), resolveFilePath: options.resolveFilePath, lockOptions: options.lockOptions, onDiskError: options.onDiskError,