9.2 KiB
Capability Spec Delta: barcode (barcode-batch-generator)
Package: @suaveplan/barcode
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: BatchBarcodeGenerator with per-item failure isolation
The package SHALL expose a generateBatch(requests, options?): Promise<BatchBarcodeSummary> async function and an equivalent BatchBarcodeGenerator class. The function SHALL produce one BatchBarcodeResult per input request in input order. A failure on any single request — encoder rejection, validator rejection, or any thrown exception inside the per-item path — SHALL NOT short-circuit the batch; the failure SHALL surface as { ok: false, error: BarcodeError } for that index while every other request continues to generate. Successful items SHALL surface as { ok: true, result: BarcodeResult }. 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 validCode 128requests - THEN
summary.total === 5ANDsummary.succeeded === 5ANDsummary.failed === 0AND everyresults[i].ok === trueANDresults[i].result.symbology === "code128"
Scenario: one request throws — others still succeed
- WHEN
generateBatchis called with three valid Code 128 requests and one EAN-13 request whose value"abc"violates EAN-13's numeric-only rule - THEN
summary.succeeded === 3ANDsummary.failed === 1ANDresults[3].ok === falseANDresults[3].erroris aBarcodeErrorwith code fromBARCODE_CODESAND the three Code 128 results are present and valid
Scenario: input order is preserved
- WHEN
generateBatchis called withconcurrency: 8over 16 mixed-symbology 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: Concurrency control with safe clamping
The package SHALL accept an optional concurrency option on BatchBarcodeOptions. The default SHALL be 1 (sequential). The value SHALL be clamped to the inclusive range [1, 64] — values below 1 are treated as 1; values above 64 are treated as 64. Concurrent execution SHALL still preserve input-order results.
Scenario: default is sequential
- WHEN
generateBatch(requests)is called withoutoptions.concurrency - THEN at most one barcode is being generated at any time AND
summary.resultsis in input order
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 the
onItemcallback) AND no error is thrown for the out-of-range input
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 BatchBarcodeOptions. When the signal fires before the batch resolves, the returned promise SHALL reject with a BarcodeError whose code === "BARCODE.BATCH_ABORTED". 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
BarcodeErrorANDerror.code === "BARCODE.BATCH_ABORTED"AND no item is generated
Scenario: signal fires mid-batch
- WHEN
generateBatch(requests, { signal })is called with 100 requests at concurrency 1, andsignal.abort()is called after 10 items - THEN the returned promise rejects with
BarcodeErrorof code"BARCODE.BATCH_ABORTED"AND fewer than 100 items were 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::"generateBatch rejects mid-batch when signal fires" AND src/core/batch/batch-generator.spec-conformance.test.ts::"abort path emits BARCODE.BATCH_ABORTED with chained cause"
Requirement: onItem progress callback emits coded results
The package SHALL accept an optional onItem: (result: BatchBarcodeResult) => void callback on BatchBarcodeOptions. The callback SHALL fire exactly once per completed request, with the same BatchBarcodeResult that ends up in summary.results. The callback SHALL fire in completion order (not input order), so concurrent batches MAY observe out-of-order callbacks even though the final summary is sorted.
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 BatchBarcodeGenerator constructor. durationMs on BatchBarcodeSummary SHALL be computed as clock.now() - startedAt. The free generateBatch function 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
BatchBarcodeGeneratoris constructed with aFakeClockthat advances by 7 ms after eachnow()call, andgenerateis invoked over 4 requests - THEN
summary.durationMs === 7(start-to-finish span of one tick) — exact value pinned by the fake clock
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 (only the constructor-default factory may resolve
performance.now, and that factory lives behind theClockinterface)
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: BARCODE.BATCH_ABORTED registered in BARCODE_CODES
The package SHALL register BARCODE.BATCH_ABORTED in BARCODE_CODES (the stable error-code registry). The code SHALL be the only error thrown from the abort path; per-item failures continue to use the existing per-symbology codes.
Scenario: code is registered
- WHEN
BARCODE_CODESis enumerated - THEN the array includes
"BARCODE.BATCH_ABORTED"
Scenario: code is the documented type
- WHEN the
BarcodeCodeexported type is inspected - THEN
"BARCODE.BATCH_ABORTED"is one of its members (verified byexpectTypeOf<"BARCODE.BATCH_ABORTED">().toMatchTypeOf<BarcodeCode>())
Verified by: src/errors/barcode-error.test.ts::"BARCODE_CODES includes BATCH_ABORTED" AND src/core/batch/batch-generator.spec-conformance.test.ts::"BarcodeCode 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 graph for callers that don't use it.
Scenario: subpath resolves to the same function as the root export
- WHEN a consumer imports
{ generateBatch } from "@suaveplan/barcode/batch"and{ generateBatch as fromRoot } from "@suaveplan/barcode" - 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"