can1357/oh-my-pi · error · TypeError

${name} must be a non-negative integer

Error message

${name} must be a non-negative integer

What it means

LRUCache validates its numeric options (max, maxSize, maxEntrySize, ttl) through the positiveInteger helper: values must be undefined (treated as 0/disabled) or a non-negative integer. Passing a negative number, a fraction, NaN, Infinity, or a non-number type throws this TypeError naming the offending option.

Source

Thrown at packages/utils/src/lru.ts:32

	/** Calculates an entry's size. */
	sizeCalculation?: (value: V, key: K) => number;
	/** Entry lifetime in milliseconds; zero disables expiry. */
	ttl?: number;
	/** Refreshes an entry's lifetime when it is read. */
	updateAgeOnGet?: boolean;
	/** Called synchronously before an entry is removed. */
	dispose?: (value: V, key: K, reason: DisposeReason) => void;
}

interface Entry<V> {
	value: V;
	size: number;
	start: number;
}

function positiveInteger(value: number | undefined, name: string): number {
	if (value === undefined) return 0;
	if (!Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
	return value;
}

/** A bounded least-recently-used cache with optional size and lifetime limits. */
export class LRUCache<K, V> {
	readonly #entries = new Map<K, Entry<V>>();
	readonly #max: number;
	readonly #maxSize: number;
	readonly #maxEntrySize: number;
	readonly #sizeCalculation: ((value: V, key: K) => number) | undefined;
	readonly #ttl: number;
	readonly #updateAgeOnGet: boolean;
	readonly #dispose: ((value: V, key: K, reason: DisposeReason) => void) | undefined;
	#calculatedSize = 0;

	/** Creates an empty cache. */
	constructor(options: LRUCacheOptions<K, V>) {
		this.#max = positiveInteger(options.max, "max");

View on GitHub (pinned to 9690622007)

Solutions

  1. Coerce and validate before constructing: max = Number.parseInt(raw, 10) with a Number.isInteger check.
  2. Round computed sizes: Math.floor(bytes / 1024) so the value is an integer.
  3. Guard against NaN/Infinity: use Number.isFinite(v) && Number.isInteger(v) && v >= 0.
  4. Fix the config source producing the bad value (env var, JSON field, CLI flag).

Example fix

// before
const cache = new LRUCache({ max: Number(process.env.CACHE_MAX) }); // '100' or NaN → TypeError
// after
const raw = Number(process.env.CACHE_MAX);
const max = Number.isInteger(raw) && raw >= 0 ? raw : 100;
const cache = new LRUCache({ max });
Defensive patterns

Strategy: validation

Validate before calling

function toNonNegativeInt(raw: unknown): number {
  const n = typeof raw === "number" ? raw : Number(raw);
  if (!Number.isInteger(n) || n < 0) throw new TypeError(`limit must be a non-negative integer, got ${raw}`);
  return n;
}

Type guard

function isNonNegativeInt(v: unknown): v is number {
  return typeof v === "number" && Number.isInteger(v) && v >= 0;
}

Try / catch

try {
  cache = new LRUCache(options);
} catch (err) {
  if (err instanceof TypeError && err.message.includes("must be a non-negative integer")) {
    logger.warn("Invalid LRU limit, falling back to defaults", { option: err.message.split(" ")[0] });
    cache = new LRUCache({ max: 1000 });
  } else throw err;
}

Prevention

When it happens

Trigger: new LRUCache({ max: -1 }), new LRUCache({ max: 10.5 }), new LRUCache({ ttl: Number.NaN }), new LRUCache({ max: '100' }) (string), or calling setter/option paths like explicitMaxEntrySize with a malformed value from config.

Common situations: Reading limits from env vars or config files without parseInt/Number conversion ('100' string), arithmetic producing NaN (undefined * 1024), fractions from byte-size division (maxSize = bytes/1024 yielding 1.5), or negated defaults.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a7c66708db9e4790. Report an issue: GitHub.