{"record":{"id":"cea3dd6678638f0e","repo":"koala73/worldmonitor","slug":"maxcommands-must-be-a-positive-integer","errorCode":null,"errorMessage":"maxCommands must be a positive integer","messagePattern":"maxCommands must be a positive integer","errorType":"exception","errorClass":"RangeError","httpStatus":null,"severity":"error","filePath":"server/worldmonitor/news/v1/list-feed-digest.ts","lineNumber":1658,"sourceCode":"    'HMGET', STORY_TRACK_KEY(h), ...fields,\n  ]);\n  const timeoutMs = redisTimeoutForDeadline(deadlineAt);\n  if (timeoutMs === undefined) return new Map();\n  const results = await runRedisPipeline(commands, false, timeoutMs);\n  const map = new Map<string, StoryTrack>();\n  for (let i = 0; i < titleHashes.length; i++) {\n    const vals = results[i]?.result as string[] | null;\n    if (!vals || !vals[0]) continue; // firstSeen missing → new story\n    map.set(titleHashes[i]!, {\n      firstSeen:    Number(vals[0]),\n      lastSeen:     Number(vals[1] ?? 0),\n      mentionCount: Number(vals[2] ?? 0),\n      sourceCount:  Number(vals[3] ?? 0),\n    });\n  }\n  return map;\n}\n\nfunction parseRedisTimestamp(value: unknown): number | undefined {\n  if (value === null || value === undefined) return undefined;\n  if (typeof value !== 'string' && typeof value !== 'number') return undefined;\n  if (typeof value === 'string' && value.trim().length === 0) return undefined;\n  const parsed = Number(value);\n  return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction chunkRedisCommands(\n  commands: Array<Array<string | number>>,\n  maxCommands: number = MAX_REDIS_PIPELINE_COMMANDS,\n): Array<Array<Array<string | number>>> {\n  if (!Number.isInteger(maxCommands) || maxCommands < 1) {\n    throw new RangeError('maxCommands must be a positive integer');\n  }\n  const chunks: Array<Array<Array<string | number>>> = [];\n  for (let offset = 0; offset < commands.length; offset += maxCommands) {\n    chunks.push(commands.slice(offset, offset + maxCommands));","sourceCodeStart":1640,"sourceCodeEnd":1676,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/news/v1/list-feed-digest.ts#L1640-L1676","documentation":"chunkRedisCommands splits an array of Redis pipeline commands into chunks of at most maxCommands (default MAX_REDIS_PIPELINE_COMMANDS = 1000). It throws a RangeError when maxCommands is not an integer or is less than 1, because a non-positive chunk size would produce an infinite/invalid loop or empty chunks. This is an argument-validation guard on a helper used when writing feed-digest data to Redis.","triggerScenarios":"Calling chunkRedisCommands with an explicit maxCommands argument that is 0, negative, non-integer (e.g. NaN, 0.5), or a value derived from config/env math that evaluates to NaN.","commonSituations":"Env/config variable missing so Number(...) yields NaN; a computed limit like Math.floor(total/buckets) reaching 0; refactoring changed the default constant to a misparsed value; a caller passing user-supplied batch size unvalidated.","solutions":["Pass a positive integer explicitly, or omit the argument to use MAX_REDIS_PIPELINE_COMMANDS (1000)","Fix the config/env source: coerce and validate with Number.isInteger before calling, and provide a sane fallback","If the value is computed, clamp it: maxCommands = Math.max(1, Math.floor(raw))","Add a unit test covering 0, negative, and NaN inputs to lock the validation behavior"],"exampleFix":"// before\nchunkRedisCommands(cmds, Number(env.PIPELINE_BATCH)) // NaN\n// after\nconst batch = Number(env.PIPELINE_BATCH);\nchunkRedisCommands(cmds, Number.isInteger(batch) && batch > 0 ? batch : MAX_REDIS_PIPELINE_COMMANDS)","handlingStrategy":"validation","validationCode":"function assertPositiveInt(n) {\n  if (!Number.isInteger(n) || n < 1) throw new RangeError(`maxCommands must be a positive integer, got ${n}`);\n  return n;\n}\nchunkRedisCommands(cmds, assertPositiveInt(cfg.maxCommands));","typeGuard":"function isPositiveInt(v: unknown): v is number {\n  return typeof v === 'number' && Number.isInteger(v) && v >= 1;\n}","tryCatchPattern":"try {\n  chunks = chunkRedisCommands(cmds, maxCommands);\n} catch (e) {\n  if (e instanceof RangeError) chunks = chunkRedisCommands(cmds); // fall back to default 1000\n  else throw e;\n}","preventionTips":["Validate any config/env-derived batch size with Number.isInteger and > 0 before use","Clamp computed values: Math.max(1, Math.floor(raw))","Rely on the default (MAX_REDIS_PIPELINE_COMMANDS = 1000) unless you have a reason","Unit-test chunkRedisCommands with 0, -1, NaN, and fractional inputs"],"tags":["validation","range-error","redis","pipeline"],"backgroundTag":"invalid-argument-range","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}