10 KiB
Capability Spec Delta: qrcode (qrcode-batch-generator)
Package: @suaveplan/qrcode
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: BatchQrCodeGenerator with per-item failure isolation
The package SHALL expose generateBatch(requests, options?): Promise<BatchQrCodeSummary> and BatchQrCodeGenerator.generate(). The function SHALL produce one BatchQrCodeResult per input request in input order. A failure on any single request — encoder rejection, Reed-Solomon overflow, or any thrown exception inside the per-item path — SHALL NOT short-circuit the batch; the failure SHALL surface as { ok: false, error: QRCodeError } for that index while every other request continues to generate. Successful items SHALL surface as { ok: true, result: QRCodeResult }. The summary SHALL include total, succeeded, failed, durationMs (from injected Clock), and the full results array.
Scenario: every request succeeds
- WHEN
generateBatchis called with five valid requests at error-correctionM - THEN
summary.total === 5ANDsummary.succeeded === 5ANDsummary.failed === 0AND everyresults[i].ok === trueANDresults[i].result.errorCorrectionLevel === "M"
Scenario: one request exceeds capacity — others still succeed
- WHEN
generateBatchis called with three short valid requests and one request whose value exceeds the version-10 byte capacity at error-correctionH - THEN
summary.succeeded === 3ANDsummary.failed === 1ANDresults[3].ok === falseANDresults[3].erroris aQRCodeErrorwith code"QRCODE.DATA_TOO_LARGE"(or whatever capacity-overflow codeQRCODE_CODESdefines) AND the three short results are present and valid
Scenario: input order is preserved by generateBatch
- WHEN
generateBatchis called withconcurrency: 8over 16 requests - THEN
results[i].index === ifor everyiANDresults.length === 16regardless of completion order
Verified by: src/core/batch/batch-generator.test.ts::"generateBatch returns one result per input in input order" AND src/core/batch/batch-generator.test.ts::"generateBatch isolates per-item failures and continues" AND src/core/batch/batch-generator.test.ts::"generateBatch preserves input order under concurrency 8"
Requirement: generateBatchAsync yields in completion order
The package SHALL expose generateBatchAsync(requests, options?): AsyncGenerator<BatchQrCodeResult> and BatchQrCodeGenerator.stream(). The generator SHALL yield one BatchQrCodeResult per input request, in completion order (not input order). The generator SHALL honour concurrency strictly (no more than concurrency requests in-flight at once) and SHALL honour signal (subsequent next() after abort rejects with BARCODE.BATCH_ABORTED).
Scenario: completion-order yield
- WHEN
generateBatchAsyncis iterated over four requests where the third has artificially-delayed encoding (viaconcurrency: 4) - THEN the slow item is yielded last AND every yielded result still carries the original
indexfield matching its position in the input array
Scenario: concurrency throttles in-flight requests
- WHEN
generateBatchAsyncis iterated withconcurrency: 2and anonItemspy that records the count of currently in-flight items - THEN the recorded peak in-flight count never exceeds 2
Verified by: src/core/batch/batch-generator.test.ts::"generateBatchAsync yields in completion order" AND src/core/batch/batch-generator.test.ts::"generateBatchAsync throttles in-flight requests to concurrency"
Requirement: Concurrency control with safe clamping
The package SHALL accept an optional concurrency option on BatchQrCodeOptions. Default for generateBatch SHALL be 1 (sequential). Default for generateBatchAsync SHALL be 4 (matching legacy). Both SHALL clamp the value to the inclusive range [1, 64].
Scenario: generateBatch defaults to sequential
- WHEN
generateBatch(requests)is called withoutoptions.concurrency - THEN at most one QR code is being generated at any time
Scenario: concurrency clamps to upper bound
- WHEN
generateBatch(requests, { concurrency: 1000 })is called - THEN the internal worker pool is sized at 64 (verified by counting active in-flight promises via
onItem) AND no error is thrown
Scenario: concurrency clamps to lower bound
- WHEN
generateBatch(requests, { concurrency: 0 })is called - THEN execution is sequential (effective concurrency 1) AND no error is thrown
Verified by: src/core/batch/batch-generator.test.ts::"generateBatch defaults to sequential execution" AND src/core/batch/batch-generator.test.ts::"generateBatch clamps concurrency above 64 to 64" AND src/core/batch/batch-generator.test.ts::"generateBatch clamps concurrency below 1 to 1"
Requirement: AbortSignal cancels the batch
The package SHALL accept an optional AbortSignal on BatchQrCodeOptions. For generateBatch, when the signal fires the returned promise SHALL reject with a QRCodeError whose code === "QRCODE.BATCH_ABORTED". For generateBatchAsync, when the signal fires the next await iterator.next() SHALL reject with the same coded error. In-flight per-item generations MAY complete or be discarded; queued items SHALL NOT be started. The error SHALL carry the original AbortSignal.reason as cause if present.
Scenario: pre-fired signal aborts immediately
- WHEN
generateBatch(requests, { signal })is called with an already-abortedAbortSignal - THEN the returned promise rejects with
QRCodeErrorANDerror.code === "QRCODE.BATCH_ABORTED"AND no item is generated
Scenario: signal fires mid-stream
- WHEN
generateBatchAsync(requests, { signal })is iterated for 10 yields, thensignal.abort()is called - THEN the next
iterator.next()rejects withQRCodeErrorof code"QRCODE.BATCH_ABORTED"AND no further items are generated
Verified by: src/core/batch/batch-generator.test.ts::"generateBatch rejects immediately when signal is pre-aborted" AND src/core/batch/batch-generator.test.ts::"generateBatchAsync rejects next() after signal fires mid-stream" AND src/core/batch/batch-generator.spec-conformance.test.ts::"abort path emits QRCODE.BATCH_ABORTED with chained cause"
Requirement: onItem progress callback emits coded results
The package SHALL accept an optional onItem: (result: BatchQrCodeResult) => void callback on BatchQrCodeOptions. The callback SHALL fire exactly once per completed request. For generateBatch the callback fires in completion order; for generateBatchAsync the callback fires immediately before each yield. A throw from onItem SHALL NOT abort the batch.
Scenario: onItem fires once per request
- WHEN
generateBatchis called with 20 requests and anonItemspy - THEN the spy is called 20 times AND every emitted result has the same identity as the corresponding
summary.results[i]
Scenario: onItem callback throwing does not break the batch
- WHEN the
onItemcallback throws for one item - THEN the batch still resolves normally AND the throw is captured into the OTel span as a non-fatal event
Verified by: src/core/batch/batch-generator.test.ts::"onItem fires exactly once per completed request" AND src/core/batch/batch-generator.spec-conformance.test.ts::"onItem callback errors are isolated from the batch"
Requirement: Deterministic durationMs via injectable Clock
The package SHALL accept an injectable Clock (@suaveplan/types::Clock) on the BatchQrCodeGenerator constructor. durationMs on BatchQrCodeSummary SHALL be computed as clock.now() - startedAt. The free generateBatch and generateBatchAsync functions SHALL use a default real clock; tests SHALL be able to inject a deterministic clock for stable assertions.
Scenario: injected clock yields deterministic duration
- WHEN a
BatchQrCodeGeneratoris constructed with aFakeClockthat advances by 11 ms after eachnow()call, andgenerateis invoked over 4 requests - THEN
summary.durationMs === 11(start-to-finish span of one tick)
Scenario: no module-scope Date.now / performance.now
- WHEN the
batch-generator.tssource is grepped forDate.now(,performance.now(,Math.random( - THEN zero matches are found at module scope
Verified by: src/core/batch/batch-generator.spec-conformance.test.ts::"injected FakeClock yields deterministic durationMs" AND src/core/batch/batch-generator.spec-conformance.test.ts::"batch-generator source has no module-scope Date.now or Math.random"
Requirement: QRCODE.BATCH_ABORTED registered in QRCODE_CODES
The package SHALL register QRCODE.BATCH_ABORTED in QRCODE_CODES (the stable error-code registry). The code SHALL be the only error thrown from the abort path; per-item failures continue to use existing codes (capacity overflow, mode-mismatch, etc.).
Scenario: code is registered
- WHEN
QRCODE_CODESis enumerated - THEN the array includes
"QRCODE.BATCH_ABORTED"
Scenario: code is the documented type
- WHEN the
QRCodeCodeexported type is inspected - THEN
"QRCODE.BATCH_ABORTED"is one of its members (verified byexpectTypeOf<"QRCODE.BATCH_ABORTED">().toMatchTypeOf<QRCodeCode>())
Verified by: src/errors/qrcode-error.test.ts::"QRCODE_CODES includes BATCH_ABORTED" AND src/core/batch/batch-generator.spec-conformance.test.ts::"QRCodeCode union includes BATCH_ABORTED"
Requirement: ./batch subpath export and tree-shakeability
The package SHALL expose ./batch as a tree-shakeable subpath in package.json exports, mapping to dist/core/batch/batch-generator.js. Importing only generateBatch SHALL NOT pull in the entire core/encoder and core/reed-solomon graph for callers that don't use them transitively.
Scenario: subpath resolves to the same function as the root export
- WHEN a consumer imports
{ generateBatch } from "@suaveplan/qrcode/batch"and{ generateBatch as fromRoot } from "@suaveplan/qrcode" - THEN the two references are identity-equal
Verified by: src/core/batch/batch-generator.test.ts::"./batch subpath and root export resolve to the same function"