Make a retry reuse its first result
IdemStep associates an action with a stable key. Its JavaScript wrapper reuses completed step results, and its HTTP proxy replays cached responses for repeated keyed requests.
A slow response can make a browser workflow retry a submit that already completed. Keeping one key for that logical action lets the local store recognize the retry, while a new key explicitly starts a new action.
- Reuse a result — Completed keys replay cached wrapper values or HTTP responses.
- Coalesce concurrent retries — Same-key callers share an in-flight operation within the process.
- Choose the retention window — Use an in-memory store or JSON persistence, with optional committed-key TTL.
idemStep owns the local function lifecycle. IdemStore tracks pending and committed records plus cached values. startProxy reads x-idem-key, forwards the first request, stores the full response and serves later same-key calls from that cache. In-flight maps coalesce concurrent callers; JSON persistence keeps completed records for a restart.
| Component | Responsibility |
|---|---|
Caller |
stable logical-action key |
Wrapper / proxy |
first call or replay |
IdemStore |
pending and committed records |
HTTP upstream |
first keyed request |
Node.js 20+. The demo uses built-in fetch and a loopback HTTP fixture, so it needs no browser download.
git clone https://github.com/SuperMarioYL/idemstep.git
cd idemstep
npm ci
npm run buildThe script starts a real local proxy and a synthetic order endpoint. Two POSTs share order-demo-1; the third uses order-demo-2. It counts actual upstream calls and reads the replay header.
node examples/presentation-demo.mjsThree local HTTP requests reach the upstream twice; one duplicate response is replayed.
{"key":"order-demo-1","status":200,"replayed":false,"body":{"order":1}}
{"key":"order-demo-1","status":200,"replayed":true,"body":{"order":1}}
{"key":"order-demo-2","status":200,"replayed":false,"body":{"order":2}}
upstream calls=2; suppressed retries=1
Scope: local HTTP fixture; no browser, payment provider or external order.
The complete command and output are recorded in docs/demo-results.json. Inputs and reproduction code are included in the repository.
The existing recording is retained for context; the text example above documents the reproducible scenario.
For the library, import idemStep, generateKey and IdemStore from idemstep. Generate the key once for a logical operation, keep it across retries, and pass the same store to calls that share results. For the proxy, send x-idem-key with the intercepted request; see examples/presentation-demo.mjs for complete HTTP input. A new intent needs a new key even if its body is identical.
node dist/index.js proxy --host 127.0.0.1 --port 8473 --store ./idemstep-state.json --ttl 86400000 --prune-interval 60000--host 127.0.0.1 keeps the proxy local; omitted host binds all interfaces. --store enables JSON persistence. --ttl expires committed keys, after which the same key may forward again; --prune-interval sweeps expired records. --upstream-timeout bounds idle upstream traffic. --https requires OpenSSL and client trust in the generated CA. Hosted mode adds configurable API-key namespaces but does not provide a distributed transaction store or billing service.
The wrapper and proxy are separate integration points. The wrapper does not automatically attach network headers to a browser request: configure the browser’s proxy and request headers explicitly. HTTP requests without an idempotency key pass through without deduplication.
| Route | Implemented role |
|---|---|
| JavaScript | idemStep wrapper |
| HTTP proxy | x-idem-key request routing |
| Playwright | explicit proxy and headers |
| JSON file | completed-record persistence |
| HTTPS CONNECT | optional local CA interception |
- This is local deduplication, not an end-to-end exactly-once guarantee. A crash after an upstream side effect but before durable commit can leave an ambiguous outcome.
- The store is not designed for independently running processes to concurrently share one JSON file.
- The demo covers local HTTP responses; it does not validate browser integration, payment APIs or HTTPS trust setup.
Implemented: wrapper result reuse, HTTP replay, concurrent-call coalescing, JSON persistence, TTL and optional CONNECT interception. Release progression: v0.7 bounded every upstream forward with an idle timeout; v0.8 surfaced mid-session durable-write failures and closed the hosted CONNECT open relay; v0.9 streamed non-transactional responses and stripped Proxy-Authorization upstream; v0.10 caught the non-transactional forward's unhandled rejection and terminated mid-stream clients; v0.11 added the --version surface with lockstep version pinning; v0.12 made the MITM leaf-cert mint async so CONNECT tunnels no longer block the event loop. Future directions include stronger recovery semantics and distributed storage. Review CHANGELOG.md and store warnings before depending on restart durability.
See LICENSE. When reporting an issue, include a minimal input, the command, and the observed output.
