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

7.7 KiB
Raw Permalink Blame History

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: BatchQrCodeRequest interface — { id?: string; value: string; options?: QRCodeOptions }. Per-item id is optional; when omitted the request index is used as the result key.
  • ADDED: BatchQrCodeResult discriminated union — { id: string; index: number; ok: true; result: QRCodeResult } | { id: string; index: number; ok: false; error: QRCodeError }. Failed items carry a coded QRCodeError, never a bare string.
  • ADDED: BatchQrCodeOptions interface — { concurrency?: number; signal?: AbortSignal; onItem?: (result: BatchQrCodeResult) => void }. concurrency defaults to 1 (sequential) and is clamped to [1, 64].
  • ADDED: BatchQrCodeSummary interface — { total: number; succeeded: number; failed: number; durationMs: number; results: BatchQrCodeResult[] }. durationMs uses an injectable Clock.
  • ADDED: generateBatch(requests, options?): Promise<BatchQrCodeSummary> — produces one BatchQrCodeResult per 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); honours concurrency and signal.
  • ADDED: BatchQrCodeGenerator class — 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 ./batch mapping to dist/core/batch/batch-generator.js.
  • ADDED: error code QRCODE.BATCH_ABORTED registered on QRCODE_CODES, thrown when the supplied AbortSignal fires 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-scan change already in openspec/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/qrcode plus 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 — covers BatchQrCodeRequest validation, in-order results from generateBatch, completion-order from generateBatchAsync, per-item failure isolation, concurrency clamping, onItem callback invocation, abort-via-AbortSignal for both shapes.
  • src/core/batch/batch-generator.spec-conformance.test.ts — pins injectable Clock (deterministic durationMs), Zod-validated BatchQrCodeRequest shape, QRCODE.BATCH_ABORTED error code emission, OTel counter qrcode.batch.items_total / qrcode.batch.items_failed / histogram qrcode.batch.duration_ms recorded against an in-memory MeterProvider.

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 only packages/barcode/qrcode/package.json itself).

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.