(
sessionId: string,
drafts: unknown,
context: TaskLedgerMutationContext = {},
)
| 186 | } |
| 187 | |
| 188 | async create( |
| 189 | sessionId: string, |
| 190 | drafts: unknown, |
| 191 | context: TaskLedgerMutationContext = {}, |
| 192 | ): Promise<{ created: Task[]; total: number }> { |
| 193 | assertSafeSessionId(sessionId); |
| 194 | if (!Array.isArray(drafts) || drafts.length === 0) { |
| 195 | throw new Error('Task creation requires at least one task draft'); |
| 196 | } |
| 197 | // Front-door the per-batch cap before generating ids or normalizing drafts: |
| 198 | // a single call can never add more than the absolute ledger cap, and rejecting |
| 199 | // here avoids generating N uuids for a batch the write-queue total check |
| 200 | // would refuse anyway. The total (existing + new) cap is still enforced |
| 201 | // inside the serialized mutate callback below. |
| 202 | if (drafts.length > TASK_LEDGER_MAX_TASKS) { |
| 203 | throw new Error( |
| 204 | `Task creation batch of ${drafts.length} tasks exceeds the ${TASK_LEDGER_MAX_TASKS}-task per-batch cap; split the work into smaller calls.`, |
| 205 | ); |
| 206 | } |
| 207 | const normalizedDrafts = drafts.map((draft) => { |
| 208 | const normalized = normalizeCreateTaskInput(draft); |
| 209 | if (!normalized.ok) throw new Error(normalized.message); |
| 210 | return normalized.value; |
| 211 | }); |
| 212 | const created: Task[] = []; |
| 213 | // Cap check runs inside the serialized mutate callback (after reading the |
| 214 | // current ledger) so concurrent creates cannot race past the limit, and a |
| 215 | // rejected create never touches the file. |
| 216 | const all = await this.mutate( |
| 217 | sessionId, |
| 218 | (tasks) => { |
| 219 | if (tasks.length + normalizedDrafts.length > TASK_LEDGER_MAX_TASKS) { |
| 220 | throw new Error( |
| 221 | `Task ledger is limited to ${TASK_LEDGER_MAX_TASKS} tasks total per session ` + |
| 222 | `(currently ${tasks.length}, adding ${normalizedDrafts.length}). This is a hard runaway guard on the ` + |
| 223 | 'total count — completed or cancelled tasks still count, so batch related work into fewer, ' + |
| 224 | 'coarser tasks instead.', |
| 225 | ); |
| 226 | } |
| 227 | const now = Date.now(); |
| 228 | for (const draft of normalizedDrafts) { |
| 229 | const parent = draft.parentId ? findTaskByRef(tasks, draft.parentId) : undefined; |
| 230 | if (draft.parentId && !parent) throw new Error(`No such parent task: ${draft.parentId}`); |
| 231 | if (parent && isTerminalTaskStatus(parent.status)) { |
| 232 | throw new Error(`Cannot create a child under terminal task ${parent.key}`); |
| 233 | } |
| 234 | const task: Task = { |
| 235 | id: randomUUID(), |
| 236 | key: nextTaskKey([...tasks, ...created], parent), |
| 237 | subject: draft.subject, |
| 238 | status: 'pending', |
| 239 | createdAt: now, |
| 240 | updatedAt: now, |
| 241 | ...(parent ? { parentId: parent.id } : {}), |
| 242 | ...(ownerFromContext(context) ? { owner: ownerFromContext(context) } : {}), |
| 243 | }; |
| 244 | created.push(task); |
| 245 | } |
nothing calls this directly
no test coverage detected