7.7 KiB
QR code batch generator
Package: @suaveplan/qrcode
Status: PROPOSED
Type: spec-conformance + feature
Why
The 2026-05-02 audit of @suaveplan/qrcode against legacy packages/universal/qrcode/src/ found that genesis ships only the single-call generate() and the core/* building blocks. Legacy ships a full batch/batch-generator.ts exposing three flavours of batch surface — generateBatch, generateBatchAsync (async generator with concurrency), and generateBatchWithProgress — used by every consumer that produces many QR codes at once: ticketing, vCard imports, asset registers, and bulk loyalty-card issuance. CLAUDE.md scopes this package as "generator only" — qr-reader, styled-qr (logos / colours / shapes), and qr-exporter (PNG/PDF) are intentionally out of scope. The batch generator is unambiguously part of the generator surface, not any of those scoped-out caps. Its absence forces every consumer to re-implement an error-isolating loop in user code, which §0a (migration parity) bans.
What changes
- ADDED:
BatchQrCodeRequestinterface —{ id?: string; value: string; options?: QRCodeOptions }. Per-itemidis optional; when omitted the request index is used as the result key. - ADDED:
BatchQrCodeResultdiscriminated union —{ id: string; index: number; ok: true; result: QRCodeResult } | { id: string; index: number; ok: false; error: QRCodeError }. Failed items carry a codedQRCodeError, never a bare string. - ADDED:
BatchQrCodeOptionsinterface —{ concurrency?: number; signal?: AbortSignal; onItem?: (result: BatchQrCodeResult) => void }.concurrencydefaults to1(sequential) and is clamped to[1, 64]. - ADDED:
BatchQrCodeSummaryinterface —{ total: number; succeeded: number; failed: number; durationMs: number; results: BatchQrCodeResult[] }.durationMsuses an injectableClock. - ADDED:
generateBatch(requests, options?): Promise<BatchQrCodeSummary>— produces oneBatchQrCodeResultper input request in input order; per-item failures isolated. - ADDED:
generateBatchAsync(requests, options?): AsyncGenerator<BatchQrCodeResult>— async iterable that yields results as they complete (completion order, not input order); honoursconcurrencyandsignal. - ADDED:
BatchQrCodeGeneratorclass —class BatchQrCodeGenerator { constructor(deps?: { clock?: Clock }); generate(requests, options?): Promise<BatchQrCodeSummary>; stream(requests, options?): AsyncGenerator<BatchQrCodeResult> }for callers that want to inject a deterministic clock or share a generator across calls. - ADDED: subpath export
./batchmapping todist/core/batch/batch-generator.js. - ADDED: error code
QRCODE.BATCH_ABORTEDregistered onQRCODE_CODES, thrown when the suppliedAbortSignalfires before completion.
Out of scope
- QR reading / decoding (qr-reader) — explicitly out of scope per CLAUDE.md package description ("generator only"). Tracked under separate
qrcode-scanchange already inopenspec/changes/. - Styled QR (logos / colour gradients / module shapes — styled-qr) — intentionally out of scope per CLAUDE.md. Would be its own package.
- Raster export (PNG / PDF — qr-exporter) — intentionally out of scope per CLAUDE.md. The package emits SVG/matrix/terminal/dataurl only; raster export belongs in a downstream package that depends on
@suaveplan/qrcodeplus a rasteriser. - Higher QR versions (11 – 40) — current package supports versions 1 – 10 only. Extending the version table is its own change; batch behaviour is independent of version count and works identically once added.
Source mapping
This change derives from ../suaveplan/packages/universal/qrcode/src/batch/.
| Requirement | Source lines | Notes |
|---|---|---|
BatchQrCodeRequest shape |
types/qr.types.ts L94-L98 |
Reworded: data → value; added optional id; removed outputFormat (legacy BatchItem carries it; new shape relies on QRCodeOptions/QRCodeResult having every output already) |
BatchQrCodeResult discriminated union |
(no source line) | Legacy returns QrCode[] with no per-item failure mode — every failure throws and aborts the batch. New shape adds discriminated-union failure isolation (parity with barcode-batch-generator). |
BatchQrCodeOptions shape with concurrency, signal, onItem |
types/qr.types.ts L103-L106 (legacy BatchOptions) |
Extended: legacy has onProgress: (completed, total) => void and concurrency; new shape replaces onProgress with onItem (richer payload), adds signal |
generateBatch Promise-based function |
batch/batch-generator.ts L12-L29 |
Verbatim semantics for the success path; new failure-isolation behaviour replaces legacy throw-and-abort (legacy bug was that one bad input poisoned the whole batch) |
generateBatchAsync async-iterable function |
batch/batch-generator.ts L35-L59 |
Verbatim — yields per item, supports concurrency |
BatchQrCodeGenerator class |
(no source line) | New: legacy is free functions only; class added for clock injection |
Requirements with no source line
| Requirement | Justification |
|---|---|
Per-item failure isolation in generateBatch |
Legacy throws on first per-item failure (generator.generate rejects the loop). Genesis-wide CLAUDE.md §Workflow rule 2 ("no half-baked libraries") plus the §0a migration-parity rule mean we keep legacy's batch surface but fix the obvious correctness bug rather than ship the broken contract. |
BatchQrCodeGenerator class with injectable Clock |
Genesis-wide CLAUDE.md determinism rule (enterprise/bitpacker.md L40 "no module-scope Date.now()"). Legacy reads Date.now() implicitly through performance.now; genesis routes through @suaveplan/types::Clock. |
QRCODE.BATCH_ABORTED error code on QRCODE_CODES |
CLAUDE.md §Workflow rule 2 — every thrown error routes through @suaveplan/error with a stable code. Legacy throws plain Error from the loop (no abort plumbed in legacy at all). |
concurrency clamping to [1, 64] |
Genesis-wide guardrail — unbounded Promise.all over user input is a denial-of-service vector. Legacy clamps to a default of 4 but accepts any number; genesis hardens the upper bound. |
AbortSignal support |
Legacy has none. Genesis-wide pattern (every async API in @suaveplan/retry, @suaveplan/idempotency, etc.) accepts an AbortSignal. |
Verification approach
src/core/batch/batch-generator.test.ts— coversBatchQrCodeRequestvalidation, in-order results fromgenerateBatch, completion-order fromgenerateBatchAsync, per-item failure isolation,concurrencyclamping,onItemcallback invocation, abort-via-AbortSignalfor both shapes.src/core/batch/batch-generator.spec-conformance.test.ts— pins injectableClock(deterministicdurationMs), Zod-validatedBatchQrCodeRequestshape,QRCODE.BATCH_ABORTEDerror code emission, OTel counterqrcode.batch.items_total/qrcode.batch.items_failed/ histogramqrcode.batch.duration_msrecorded against an in-memoryMeterProvider.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/qrcode to enumerate. As of 2026-05-02 there are no in-repo dependents:
- No dependents (verified by
grep -rl "@suaveplan/qrcode" /home/ubuntu/web/genesis/packages/ --include="package.json"returning onlypackages/barcode/qrcode/package.jsonitself).
This change is purely additive (new exports, no signature changes to existing surface), so the absence of dependents is not a risk gate.
Migration impact
None — additive only. Existing generate(), the core/* building blocks, the renderers, and QRCODE_CODES are unchanged. Consumers wanting batch generation opt in by importing generateBatch / generateBatchAsync from @suaveplan/qrcode or @suaveplan/qrcode/batch.