Skip to content

New sub-package: univeros/idempotency — PSR-15 idempotency-key middleware + job decorator #165

@tonydspaniard

Description

@tonydspaniard

Tracking issue. Lands as a new sub-package at src/Altair/Idempotency/, registered via composer replace like the existing 37 sub-packages. Same CI, same release cadence, same .agent/packages/idempotency.md once bin/altair manifest:generate picks it up.

Why

Idempotency is a first-class concern for any API that gets retried by clients (mobile, queues, webhooks, agents). Stripe set the bar: clients pass Idempotency-Key, the server stores the (key, request fingerprint, response) tuple, and replays match future requests with the same key. PHP has nothing canonical here — every shop hand-rolls it badly, or ships without it and absorbs the duplicate writes.

Under the 2am.tech brief, this is a verified AI-era gap: agents retry. Without server-side idempotency, every agent integration is a footgun.

Grep-verified absent from univeros/framework as of 2026-05-30: zero Idempotency-Key middleware, zero job-handler decorator. The framework already ships rate-limiting, CSRF, auth (Basic/Digest/Token/JWT) and queue infrastructure (Messaging), but no idempotency layer.

Why as a sub-package (not a separate repo)

Same category as the existing cross-cutting HTTP concerns in src/Altair/Http/Middleware/ (rate-limit, CSRF, auth, sessions). All required deps already exist as sub-packages: Cache, Http, Messaging, Persistence. Living inside the monorepo means:

  • Shows up in bin/altair manifest:generate, .agent/packages/idempotency.md, Doctor, Introspection — discoverable by agents and humans.
  • One coherent framework version via composer replace.
  • "Univeros ships idempotency built-in" is the marketing claim, not "compose Univeros + a third-party library."

Scope

PSR-only. No illuminate/*, no symfony/* runtime deps — pure PSR-7/15/16 plus the existing framework sub-packages.

Required surface

  • Altair\Idempotency\Middleware\IdempotencyMiddleware (PSR-15) — reads Idempotency-Key header, looks up store, returns cached response or wraps the handler.
  • Altair\Idempotency\Contracts\IdempotencyStoreInterfacefind($key, $requestFingerprint), store($key, $request, $response, $ttl), lockAcquire($key, $ttl), lockRelease($key).
  • Backends:
    • RedisIdempotencyStore (ext-redis or predis adapter, runtime choice)
    • PdoIdempotencyStore (Postgres + MySQL, schema migration shipped via Persistence / MigrationIntelligence)
    • InMemoryIdempotencyStore (testing)
  • Altair\Idempotency\Decorator\IdempotentHandler — queue-handler decorator usable from Messaging (Symfony Messenger bridge); contract lives here, bridge wiring lives in Messaging or in host config.
  • Configurable: TTL (default 24h), header name (default Idempotency-Key), fingerprinting strategy (method + path + body hash by default), response replay headers.
  • Concurrent-request guard: lock acquire while first request is in flight; second request blocks or 409s — configurable.
  • Streaming response handling (or explicit "not idempotent" sentinel for stream bodies).

Out of scope

  • No client-side helper (Stripe SDK pattern is fine; this is server-side).
  • No key generator — clients pick them.

Acceptance

  • src/Altair/Idempotency/ exists with composer.json (PSR-4 Altair\\Idempotency\\), follows the layout of existing sub-packages.
  • Registered in root composer.json replace block as univeros/idempotency.
  • docs/packages/idempotency.md shipped (per the docs-per-package rule).
  • PSR-15 middleware passes a conformance suite: same key → same response, different key → re-execute, missing key → pass through, lock contention → 409 (or block per config).
  • composer cs && composer stan && composer rector clean.
  • 80%+ coverage on the middleware + store contracts (tests/Idempotency/).
  • Integration test against real Redis + real Postgres (CI service containers, like the other Postgres-touching packages).
  • bin/altair manifest:generate picks up .agent/packages/idempotency.md automatically.

Reference reading

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions