{"record":{"id":"f9fad5ed9c5b49da","repo":"koala73/worldmonitor","slug":"persistpickedbatch-chunk-too-large-contacts","errorCode":null,"errorMessage":"[_persistPickedBatch] chunk too large: ${contacts.length} > ${PERSIST_CHUNK_SIZE}","messagePattern":"\\[_persistPickedBatch\\] chunk too large: (.+?) > (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/waveRuns.ts","lineNumber":564,"sourceCode":"      createdAt: now,\n      updatedAt: now,\n    });\n    return { ok: true, runId: args.runId };\n  },\n});\n\n/**\n * Insert a chunk of picked-contact rows. Called repeatedly from\n * `pickWaveAction` to stay under Convex per-mutation write limits.\n */\nexport const _persistPickedBatch = internalMutation({\n  args: {\n    runId: v.string(),\n    contacts: v.array(v.string()), // normalizedEmails\n  },\n  handler: async (ctx, { runId, contacts }) => {\n    if (contacts.length > PERSIST_CHUNK_SIZE) {\n      throw new Error(\n        `[_persistPickedBatch] chunk too large: ${contacts.length} > ${PERSIST_CHUNK_SIZE}`,\n      );\n    }\n    const now = Date.now();\n    for (const email of contacts) {\n      await ctx.db.insert(\"wavePickedContacts\", {\n        runId,\n        normalizedEmail: email,\n        status: \"pending\",\n      });\n    }\n    // Bump updatedAt so the in-flight guard's lastActivityAt fallback sees fresh activity.\n    const run = await ctx.db\n      .query(\"waveRuns\")\n      .withIndex(\"by_runId\", (q) => q.eq(\"runId\", runId))\n      .unique();\n    if (run) await ctx.db.patch(run._id, { updatedAt: now });\n    return { inserted: contacts.length };","sourceCodeStart":546,"sourceCodeEnd":582,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/waveRuns.ts#L546-L582","documentation":"Thrown by _persistPickedBatch when the contacts array length exceeds PERSIST_CHUNK_SIZE (defined as 500 in waveRuns.ts:77). The mutation exists specifically to stay under Convex's per-mutation write limit, so it refuses oversized chunks rather than attempting a write that Convex would reject. The caller (pickWaveAction) is responsible for slicing into 500-row chunks.","triggerScenarios":"pickWaveAction calls runMutation('_persistPickedBatch', {contacts}) with an array longer than 500 because the slicing loop (waveRuns.ts:869-870) was modified or bypassed. A different caller invokes _persistPickedBatch directly with an unsliced batch.","commonSituations":"A developer changes PERSIST_CHUNK_SIZE to a smaller number but forgets to update the slicing loop, or vice versa. A new code path calls _persistPickedBatch without slicing. A test passes a large fixture array directly.","solutions":["Ensure the caller slices contacts into chunks of at most PERSIST_CHUNK_SIZE (500): `for (let i = 0; i < picked.length; i += PERSIST_CHUNK_SIZE) { const chunk = picked.slice(i, i + PERSIST_CHUNK_SIZE); await runMutation(..., {contacts: chunk}); }`.","If you intentionally changed PERSIST_CHUNK_SIZE, update every call site that slices by it.","Never call _persistPickedBatch with an unsliced array from a new action."],"exampleFix":"// before — caller passes unsliced batch\nawait ctx.runMutation(internal.broadcast.waveRuns._persistPickedBatch, { runId, contacts: picked });\n// after — caller slices into 500-row chunks\nconst PERSIST_CHUNK_SIZE = 500;\nfor (let i = 0; i < picked.length; i += PERSIST_CHUNK_SIZE) {\n  const chunk = picked.slice(i, i + PERSIST_CHUNK_SIZE);\n  await ctx.runMutation(internal.broadcast.waveRuns._persistPickedBatch, { runId, contacts: chunk });\n}","handlingStrategy":"validation","validationCode":"const PERSIST_CHUNK_SIZE = 500;\nfunction assertChunkSize(contacts: string[]): void {\n  if (contacts.length > PERSIST_CHUNK_SIZE) {\n    throw new Error(`Batch too large: ${contacts.length} > ${PERSIST_CHUNK_SIZE}. Slice the caller's array.`);\n  }\n}\n// slice before calling: for (let i = 0; i < picked.length; i += PERSIST_CHUNK_SIZE) { ... }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always slice contacts into PERSIST_CHUNK_SIZE (500) chunks before calling _persistPickedBatch.","Import PERSIST_CHUNK_SIZE from the same module rather than hardcoding the number in the caller.","Never call _persistPickedBatch from a new code path without first checking the batch size."],"tags":["convex","batching","write-limit","wave-runs","internal-mutation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}