Skip to content

Commit 66f25d0

Browse files
authored
Merge pull request #3 from TykTechnologies/feat/storage-binding-mocks
Mock TykStorage* bindings in harness + idempotency-guard example
2 parents 118829e + d6ce6d8 commit 66f25d0

13 files changed

Lines changed: 3919 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ That's the inner loop. **No Tyk component required to run any of it.**
2020

2121
- **Working starter plugin** in `src/plugin.ts` — injects an `X-Trace-Id` header on every request. Replace with your logic.
2222
- **TypeScript types** for the Tyk plugin API via [`@tyk-technologies/tyk-plugin-types`](https://www.npmjs.com/package/@tyk-technologies/tyk-plugin-types) on npm — autocomplete in any IDE.
23-
- **Local test harness** in `test/_harness.ts` — mocks the goja runtime so tests run in plain Node via vitest.
23+
- **Local test harness** in `test/_harness.ts` — mocks the goja runtime so tests run in plain Node via vitest. Includes faithful mocks of the six shared storage bindings (`TykStorageGet/Set/SetNX/Del/TTL/Incr`), TTL and input limits included, plus a `mockStorage` helper to seed/inspect storage from tests.
2424
- **AGENTS.md** — the constraints brief for AI assistants. Keeps Claude/Cursor/Copilot from suggesting Node APIs like `import axios` or runtime module loading the goja runtime can't do.
2525
- **Webpack config** targeting ES2020 by default (run by goja v5.14+; the guaranteed floor is ES5.1) — bundles your TypeScript plus npm deps into a single self-contained JS file the gateway can run.
2626
- **GitHub Actions** — runs tests and builds on every push, plus an end-to-end suite that exercises every example against a real goja-enabled Tyk OSS gateway in Docker (see [e2e/](./e2e/)).

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Marquee plugin patterns demonstrating common Tyk plugin shapes. Copy a folder, m
1212
| [post-key-auth-tenant-context](./post-key-auth-tenant-context/) | `post_key_auth` | Read `session.meta_data.tenant_id`, inject `X-Tenant-Id` header for upstream. |
1313
| [post-correlation-id](./post-correlation-id/) | `post` | Preserve inbound `X-Correlation-Id` or generate UUID v4; uses the `uuid` npm package. |
1414
| [response-pii-redaction](./response-pii-redaction/) | `response` | Mask SSN-shaped patterns in response bodies before they reach the client. |
15+
| [idempotency-guard](./idempotency-guard/) | `pre` | Reject duplicate `Idempotency-Key` requests via the atomic `TykStorageSetNX` claim pattern (shared storage bindings). |
1516

1617
Each example is unit-tested locally **and** runs against a real Tyk OSS gateway in CI via `e2e/` — see [e2e/README.md](../e2e/README.md).
1718

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Idempotency Guard
2+
3+
Reject duplicate requests that reuse an `Idempotency-Key` header, using the gateway's shared storage bindings (`TykStorage*`). Demonstrates the **atomic SetNX claim pattern** — the canonical way to do "first one wins" across gateway workers without a read-then-write race.
4+
5+
## What it does
6+
7+
On the `pre` hook:
8+
9+
1. Reads the `Idempotency-Key` request header. **No header → pass through** (idempotency is client opt-in).
10+
2. Calls `TykStorageSetNX('idem:' + key, 'pending', 60)` — an atomic "set if not exists" against the gateway's Redis.
11+
- **Claimed (true)** → this is the first request with that key. Pass through.
12+
- **Not claimed (false)** → a request with the same key already ran within the last 60s. Return **409 Conflict**.
13+
3. If storage itself errors, the plugin **fails closed** with a 500 — without the guard it can't rule out a duplicate side effect, so refusing is safer than letting it through.
14+
15+
The claim expires after 60 seconds (`CLAIM_TTL_SECONDS` in `src/plugin.ts`) — tune it to your clients' retry window.
16+
17+
## Why SetNX and not Get-then-Set?
18+
19+
Two concurrent requests with the same key would both `Get` (miss), then both `Set` and both proceed. `TykStorageSetNX` collapses check-and-claim into one atomic Redis operation, so exactly one request wins even under concurrency.
20+
21+
## Caveat: atomicity scope
22+
23+
Atomicity is **per Redis instance**. Each gateway's `TykStorage*` bindings talk to that gateway's configured Redis — in a multi-node **hybrid** deployment where data planes run separate Redis instances, each Redis enforces the guard independently. For a **global** guarantee, all gateways enforcing the guard must share the same Redis.
24+
25+
## Try it
26+
27+
```bash
28+
npm install
29+
npm test # pure Node, no gateway needed
30+
npm run build:bundle # dist/bundle.zip
31+
```
32+
33+
```bash
34+
curl -X POST localhost:8080/<plugin>/post -H 'Idempotency-Key: order-123' # 200 — claimed
35+
curl -X POST localhost:8080/<plugin>/post -H 'Idempotency-Key: order-123' # 409 — duplicate
36+
```
37+
38+
## Notes
39+
40+
- `TykStorage*` bindings throw on invalid input: empty key, key > 256 bytes, value > 64KB. The test harness enforces the same limits, so violations surface in `npm test`, not in production.
41+
- This guard only *blocks* duplicates; it doesn't *replay* the first response (full idempotency-key semantics à la Stripe). For that you'd `TykStorageSet` the response on the `response` hook and serve it from storage on a duplicate.
42+
- The bindings aren't in `@tyk-technologies/tyk-plugin-types` yet — the plugin carries a local `declare function` for `TykStorageSetNX`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"file_list": ["plugin.js"],
3+
"custom_middleware": {
4+
"pre": [
5+
{
6+
"name": "handler",
7+
"path": "plugin.js",
8+
"require_session": false,
9+
"raw_body_only": false
10+
}
11+
],
12+
"driver": "javascript"
13+
},
14+
"checksum": ""
15+
}

0 commit comments

Comments
 (0)