Files
2026-05-27 23:29:53 +01:00

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. (See openspec/AGENTS.md Section 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 generateBatch is called with five valid requests at error-correction M
  • THEN summary.total === 5 AND summary.succeeded === 5 AND summary.failed === 0 AND every results[i].ok === true AND results[i].result.errorCorrectionLevel === "M"

Scenario: one request exceeds capacity — others still succeed

  • WHEN generateBatch is called with three short valid requests and one request whose value exceeds the version-10 byte capacity at error-correction H
  • THEN summary.succeeded === 3 AND summary.failed === 1 AND results[3].ok === false AND results[3].error is a QRCodeError with code "QRCODE.DATA_TOO_LARGE" (or whatever capacity-overflow code QRCODE_CODES defines) AND the three short results are present and valid

Scenario: input order is preserved by generateBatch

  • WHEN generateBatch is called with concurrency: 8 over 16 requests
  • THEN results[i].index === i for every i AND results.length === 16 regardless 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 generateBatchAsync is iterated over four requests where the third has artificially-delayed encoding (via concurrency: 4)
  • THEN the slow item is yielded last AND every yielded result still carries the original index field matching its position in the input array

Scenario: concurrency throttles in-flight requests

  • WHEN generateBatchAsync is iterated with concurrency: 2 and an onItem spy 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 without options.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-aborted AbortSignal
  • THEN the returned promise rejects with QRCodeError AND error.code === "QRCODE.BATCH_ABORTED" AND no item is generated

Scenario: signal fires mid-stream

  • WHEN generateBatchAsync(requests, { signal }) is iterated for 10 yields, then signal.abort() is called
  • THEN the next iterator.next() rejects with QRCodeError of 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 generateBatch is called with 20 requests and an onItem spy
  • 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 onItem callback 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 BatchQrCodeGenerator is constructed with a FakeClock that advances by 11 ms after each now() call, and generate is 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.ts source is grepped for Date.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_CODES is enumerated
  • THEN the array includes "QRCODE.BATCH_ABORTED"

Scenario: code is the documented type

  • WHEN the QRCodeCode exported type is inspected
  • THEN "QRCODE.BATCH_ABORTED" is one of its members (verified by expectTypeOf<"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"