fix(infra): centralize non-finite numeric option bounds

This commit is contained in:
Peter Steinberger
2026-05-28 22:48:19 -04:00
parent 6e25112aad
commit c8cc010e09
5 changed files with 33 additions and 17 deletions

View File

@@ -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<string, number>();
const touch = (key: string, now: number) => {

View File

@@ -0,0 +1,3 @@
export function resolveNonNegativeIntegerOption(value: number, fallback: number): number {
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
}

View File

@@ -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<number>(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);
});
});

View File

@@ -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<T> = {
value: T;
@@ -22,13 +23,12 @@ export function buildDirectoryCacheKey(key: DirectoryCacheKey): string {
export class DirectoryCache<T> {
private readonly cache = new Map<string, CacheEntry<T>>();
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 {

View File

@@ -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<string, Promise<boolean>>();
@@ -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,