@periodic/vanadium 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +846 -0
  4. package/dist/cjs/adapters/memory/index.js +134 -0
  5. package/dist/cjs/adapters/memory/index.js.map +1 -0
  6. package/dist/cjs/adapters/mongodb/index.js +189 -0
  7. package/dist/cjs/adapters/mongodb/index.js.map +1 -0
  8. package/dist/cjs/adapters/mongoose/index.js +199 -0
  9. package/dist/cjs/adapters/mongoose/index.js.map +1 -0
  10. package/dist/cjs/adapters/postgres/index.js +202 -0
  11. package/dist/cjs/adapters/postgres/index.js.map +1 -0
  12. package/dist/cjs/adapters/prisma/index.js +176 -0
  13. package/dist/cjs/adapters/prisma/index.js.map +1 -0
  14. package/dist/cjs/adapters/redis/index.js +178 -0
  15. package/dist/cjs/adapters/redis/index.js.map +1 -0
  16. package/dist/cjs/cleanup/engine.js +100 -0
  17. package/dist/cjs/cleanup/engine.js.map +1 -0
  18. package/dist/cjs/core/concurrencyGuard.js +50 -0
  19. package/dist/cjs/core/concurrencyGuard.js.map +1 -0
  20. package/dist/cjs/core/metrics.js +39 -0
  21. package/dist/cjs/core/metrics.js.map +1 -0
  22. package/dist/cjs/core/stateMachine.js +46 -0
  23. package/dist/cjs/core/stateMachine.js.map +1 -0
  24. package/dist/cjs/errors/index.js +127 -0
  25. package/dist/cjs/errors/index.js.map +1 -0
  26. package/dist/cjs/http/express.js +84 -0
  27. package/dist/cjs/http/express.js.map +1 -0
  28. package/dist/cjs/http/fastify.js +70 -0
  29. package/dist/cjs/http/fastify.js.map +1 -0
  30. package/dist/cjs/idempotency/engine.js +266 -0
  31. package/dist/cjs/idempotency/engine.js.map +1 -0
  32. package/dist/cjs/index.js +19 -0
  33. package/dist/cjs/index.js.map +1 -0
  34. package/dist/cjs/lock/engine.js +187 -0
  35. package/dist/cjs/lock/engine.js.map +1 -0
  36. package/dist/cjs/observability/metrics.js +92 -0
  37. package/dist/cjs/observability/metrics.js.map +1 -0
  38. package/dist/cjs/resilience/circuitBreaker.js +129 -0
  39. package/dist/cjs/resilience/circuitBreaker.js.map +1 -0
  40. package/dist/cjs/types/index.js +13 -0
  41. package/dist/cjs/types/index.js.map +1 -0
  42. package/dist/cjs/utils/crypto.js +64 -0
  43. package/dist/cjs/utils/crypto.js.map +1 -0
  44. package/dist/cjs/utils/keys.js +40 -0
  45. package/dist/cjs/utils/keys.js.map +1 -0
  46. package/dist/cjs/utils/sleep.js +25 -0
  47. package/dist/cjs/utils/sleep.js.map +1 -0
  48. package/dist/esm/adapters/memory/index.js +129 -0
  49. package/dist/esm/adapters/memory/index.js.map +1 -0
  50. package/dist/esm/adapters/mongodb/index.js +184 -0
  51. package/dist/esm/adapters/mongodb/index.js.map +1 -0
  52. package/dist/esm/adapters/mongoose/index.js +193 -0
  53. package/dist/esm/adapters/mongoose/index.js.map +1 -0
  54. package/dist/esm/adapters/postgres/index.js +197 -0
  55. package/dist/esm/adapters/postgres/index.js.map +1 -0
  56. package/dist/esm/adapters/prisma/index.js +171 -0
  57. package/dist/esm/adapters/prisma/index.js.map +1 -0
  58. package/dist/esm/adapters/redis/index.js +173 -0
  59. package/dist/esm/adapters/redis/index.js.map +1 -0
  60. package/dist/esm/cleanup/engine.js +95 -0
  61. package/dist/esm/cleanup/engine.js.map +1 -0
  62. package/dist/esm/core/concurrencyGuard.js +46 -0
  63. package/dist/esm/core/concurrencyGuard.js.map +1 -0
  64. package/dist/esm/core/metrics.js +35 -0
  65. package/dist/esm/core/metrics.js.map +1 -0
  66. package/dist/esm/core/stateMachine.js +40 -0
  67. package/dist/esm/core/stateMachine.js.map +1 -0
  68. package/dist/esm/errors/index.js +114 -0
  69. package/dist/esm/errors/index.js.map +1 -0
  70. package/dist/esm/http/express.js +81 -0
  71. package/dist/esm/http/express.js.map +1 -0
  72. package/dist/esm/http/fastify.js +67 -0
  73. package/dist/esm/http/fastify.js.map +1 -0
  74. package/dist/esm/idempotency/engine.js +261 -0
  75. package/dist/esm/idempotency/engine.js.map +1 -0
  76. package/dist/esm/index.js +9 -0
  77. package/dist/esm/index.js.map +1 -0
  78. package/dist/esm/lock/engine.js +182 -0
  79. package/dist/esm/lock/engine.js.map +1 -0
  80. package/dist/esm/observability/metrics.js +89 -0
  81. package/dist/esm/observability/metrics.js.map +1 -0
  82. package/dist/esm/resilience/circuitBreaker.js +124 -0
  83. package/dist/esm/resilience/circuitBreaker.js.map +1 -0
  84. package/dist/esm/types/index.js +10 -0
  85. package/dist/esm/types/index.js.map +1 -0
  86. package/dist/esm/utils/crypto.js +58 -0
  87. package/dist/esm/utils/crypto.js.map +1 -0
  88. package/dist/esm/utils/keys.js +35 -0
  89. package/dist/esm/utils/keys.js.map +1 -0
  90. package/dist/esm/utils/sleep.js +20 -0
  91. package/dist/esm/utils/sleep.js.map +1 -0
  92. package/dist/types/adapters/memory/index.d.ts +49 -0
  93. package/dist/types/adapters/memory/index.d.ts.map +1 -0
  94. package/dist/types/adapters/mongodb/index.d.ts +97 -0
  95. package/dist/types/adapters/mongodb/index.d.ts.map +1 -0
  96. package/dist/types/adapters/mongoose/index.d.ts +107 -0
  97. package/dist/types/adapters/mongoose/index.d.ts.map +1 -0
  98. package/dist/types/adapters/postgres/index.d.ts +85 -0
  99. package/dist/types/adapters/postgres/index.d.ts.map +1 -0
  100. package/dist/types/adapters/prisma/index.d.ts +73 -0
  101. package/dist/types/adapters/prisma/index.d.ts.map +1 -0
  102. package/dist/types/adapters/redis/index.d.ts +77 -0
  103. package/dist/types/adapters/redis/index.d.ts.map +1 -0
  104. package/dist/types/cleanup/engine.d.ts +41 -0
  105. package/dist/types/cleanup/engine.d.ts.map +1 -0
  106. package/dist/types/core/concurrencyGuard.d.ts +28 -0
  107. package/dist/types/core/concurrencyGuard.d.ts.map +1 -0
  108. package/dist/types/core/metrics.d.ts +13 -0
  109. package/dist/types/core/metrics.d.ts.map +1 -0
  110. package/dist/types/core/stateMachine.d.ts +20 -0
  111. package/dist/types/core/stateMachine.d.ts.map +1 -0
  112. package/dist/types/errors/index.d.ts +32 -0
  113. package/dist/types/errors/index.d.ts.map +1 -0
  114. package/dist/types/http/express.d.ts +50 -0
  115. package/dist/types/http/express.d.ts.map +1 -0
  116. package/dist/types/http/fastify.d.ts +48 -0
  117. package/dist/types/http/fastify.d.ts.map +1 -0
  118. package/dist/types/idempotency/engine.d.ts +24 -0
  119. package/dist/types/idempotency/engine.d.ts.map +1 -0
  120. package/dist/types/index.d.ts +8 -0
  121. package/dist/types/index.d.ts.map +1 -0
  122. package/dist/types/lock/engine.d.ts +28 -0
  123. package/dist/types/lock/engine.d.ts.map +1 -0
  124. package/dist/types/observability/metrics.d.ts +45 -0
  125. package/dist/types/observability/metrics.d.ts.map +1 -0
  126. package/dist/types/resilience/circuitBreaker.d.ts +48 -0
  127. package/dist/types/resilience/circuitBreaker.d.ts.map +1 -0
  128. package/dist/types/types/index.d.ts +170 -0
  129. package/dist/types/types/index.d.ts.map +1 -0
  130. package/dist/types/utils/crypto.d.ts +20 -0
  131. package/dist/types/utils/crypto.d.ts.map +1 -0
  132. package/dist/types/utils/keys.d.ts +15 -0
  133. package/dist/types/utils/keys.d.ts.map +1 -0
  134. package/dist/types/utils/sleep.d.ts +13 -0
  135. package/dist/types/utils/sleep.d.ts.map +1 -0
  136. package/package.json +140 -0
package/README.md ADDED
@@ -0,0 +1,846 @@
1
+ # โฌก Periodic Vanadium
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@periodic/vanadium.svg)](https://www.npmjs.com/package/@periodic/vanadium)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
6
+
7
+ **Production-grade, deterministic idempotency and distributed lock engine for Node.js with TypeScript support**
8
+
9
+ Part of the **Periodic** series of Node.js packages by Uday Thakur.
10
+
11
+ ---
12
+
13
+ ## ๐Ÿ’ก Why Vanadium?
14
+
15
+ **Vanadium** gets its name from the chemical element renowned for its role as a stabilizing agent โ€” added to steel to prevent structural failure under repeated stress. Just like vanadium strengthens metal against fatigue, this library **strengthens your backend against the failures that come from calling the same operation more than once**.
16
+
17
+ In chemistry, vanadium is a redox workhorse โ€” capable of holding multiple oxidation states, switching between them reliably and reversibly. Similarly, **@periodic/vanadium** manages execution state transitions with the same precision: from `IN_PROGRESS` to `COMPLETED`, from `COMPLETED` back to a cached result, from a crashed execution to a safe takeover.
18
+
19
+ The name represents:
20
+ - **Stability**: Guarantees single-execution semantics under any retry pressure
21
+ - **Resilience**: Survives crashes, restarts, and concurrent callers without corruption
22
+ - **Precision**: Deterministic state transitions with no ambiguity at the boundaries
23
+ - **Clarity**: Explains *why* an execution was skipped, not just *that* it was
24
+
25
+ Just as vanadium is the hidden ingredient that makes critical infrastructure hold together, **@periodic/vanadium** is the execution primitive that makes your critical operations safe to call more than once.
26
+
27
+ ---
28
+
29
+ ## ๐ŸŽฏ Why Choose Vanadium?
30
+
31
+ In distributed systems, the same operation can be triggered multiple times โ€” and most backends have no defense against it:
32
+
33
+ - **Network retries** silently re-submit requests that already succeeded
34
+ - **Message queues** deliver events at-least-once, never exactly-once
35
+ - **Webhook providers** re-send on timeout, no matter what happened the first time
36
+ - **UI double-submit** fires before the first response arrives
37
+ - **Cron overlap** starts two workers on the same job simultaneously
38
+ - **Crash recovery** replays operations that were mid-execution when a process died
39
+
40
+ Without idempotency primitives, each of these scenarios produces duplicate charges, duplicate emails, duplicate records, or corrupted state. **The bug is invisible until it hits production.**
41
+
42
+ **Periodic Vanadium** provides the perfect solution:
43
+
44
+ โœ… **Zero dependencies** โ€” Pure TypeScript core, adapters are opt-in
45
+ โœ… **Framework-agnostic** โ€” Works with Express, Fastify, or no framework at all
46
+ โœ… **Idempotency Engine** โ€” Guarantees a function executes exactly once per key
47
+ โœ… **Distributed Lock Engine** โ€” Mutual exclusion across processes and machines
48
+ โœ… **6 Storage Adapters** โ€” Memory, Redis, PostgreSQL, MongoDB, Mongoose, Prisma
49
+ โœ… **Circuit Breaker** โ€” Protects against storage failures cascading into outages
50
+ โœ… **HTTP Middleware** โ€” Drop-in Express and Fastify support
51
+ โœ… **Crash Recovery** โ€” Safely retakes expired IN_PROGRESS records
52
+ โœ… **Payload Hashing** โ€” Detects mismatched retries with the wrong parameters
53
+ โœ… **Lifecycle Hooks** โ€” Observable without mutating state
54
+ โœ… **OpenTelemetry** โ€” Built-in OTEL metrics exporter
55
+ โœ… **Type-safe** โ€” Strict TypeScript from the ground up
56
+ โœ… **No global state** โ€” No side effects on import
57
+ โœ… **Production-ready** โ€” Non-blocking, never crashes your app
58
+
59
+ ---
60
+
61
+ ## ๐Ÿ“ฆ Installation
62
+
63
+ ```bash
64
+ npm install @periodic/vanadium
65
+ ```
66
+
67
+ Or with yarn:
68
+
69
+ ```bash
70
+ yarn add @periodic/vanadium
71
+ ```
72
+
73
+ **Optional peer dependencies** (install only what you need):
74
+
75
+ ```bash
76
+ # Storage adapters
77
+ npm install redis # For Redis
78
+ npm install pg # For PostgreSQL
79
+ npm install mongodb # For MongoDB
80
+ npm install mongoose # For Mongoose
81
+ npm install @prisma/client # For Prisma
82
+
83
+ # Exporters
84
+ npm install @opentelemetry/api # For OpenTelemetry
85
+ ```
86
+
87
+ ---
88
+
89
+ ## ๐Ÿš€ Quick Start
90
+
91
+ ```typescript
92
+ import { createIdempotency, createMemoryAdapter } from '@periodic/vanadium';
93
+
94
+ // 1. Create an idempotency engine
95
+ const idempotency = createIdempotency({
96
+ adapter: createMemoryAdapter(),
97
+ ttlMs: 86_400_000, // cache completed results for 24 hours
98
+ });
99
+
100
+ // 2. Wrap any critical operation
101
+ const result = await idempotency.execute('payment:order_123', async () => {
102
+ return chargeCard({ amount: 100 });
103
+ });
104
+
105
+ // 3. Call it again with the same key โ€” fn never runs a second time
106
+ const same = await idempotency.execute('payment:order_123', async () => {
107
+ return chargeCard({ amount: 100 }); // skipped โ€” returns cached result
108
+ });
109
+ ```
110
+
111
+ **Example event output:**
112
+
113
+ ```json
114
+ {
115
+ "key": "payment:order_123",
116
+ "status": "COMPLETED",
117
+ "result": { "chargeId": "ch_abc123", "status": "succeeded" },
118
+ "attempts": 1,
119
+ "createdAt": 1708000000000,
120
+ "updatedAt": 1708000000312,
121
+ "expiresAt": 1708086400000
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## ๐Ÿง  Core Concepts
128
+
129
+ ### The `createIdempotency` Function
130
+
131
+ - **`createIdempotency` is the primary factory function**
132
+ - Returns a configured idempotency engine instance
133
+ - Accepts a storage adapter and flexible configuration options
134
+ - **This is the main entry point for idempotent execution**
135
+ - No global state, safe for multi-tenant apps
136
+
137
+ **Typical usage:**
138
+ - Application code creates an engine with `createIdempotency()`
139
+ - Critical operations are wrapped with `idempotency.execute(key, fn)`
140
+ - Duplicate calls with the same key return the cached result immediately
141
+ - Lifecycle hooks and metrics give full observability into every execution
142
+
143
+ ```typescript
144
+ const idempotency = createIdempotency({
145
+ adapter: createRedisAdapter({ client }),
146
+ ttlMs: 86_400_000,
147
+ inProgressExpiryMs: 300_000,
148
+ hashPayload: true,
149
+ hooks: {
150
+ onDuplicateHit: (ctx) => logger.info('duplicate deflected', ctx),
151
+ onTakeover: (ctx) => logger.warn('crash recovery takeover', ctx),
152
+ },
153
+ });
154
+ ```
155
+
156
+ ### The `createLock` Function
157
+
158
+ - **`createLock` is the factory for distributed mutual exclusion**
159
+ - Guarantees only one caller executes a block at a time, across processes
160
+ - Locks auto-expire after `ttlMs` โ€” no permanent deadlocks
161
+ - Safe release is enforced via owner tokens โ€” non-owners cannot unlock
162
+
163
+ ```typescript
164
+ const lock = createLock({
165
+ adapter: createRedisAdapter({ client }),
166
+ ttlMs: 10_000,
167
+ maxWaitMs: 5_000,
168
+ });
169
+
170
+ await lock.acquire('inventory:prod_001', async () => {
171
+ await updateInventory('prod_001'); // only one caller at a time
172
+ });
173
+ ```
174
+
175
+ ### Execution Lifecycle
176
+
177
+ **Design principle:**
178
+ > Same key โ†’ same result, always. The function runs once, the result lives forever (until TTL). Everything else is just a cache hit.
179
+
180
+ ```
181
+ First call โ†’ Write IN_PROGRESS โ†’ Execute fn โ†’ Write COMPLETED โ†’ Return result
182
+ Duplicate call โ†’ Find COMPLETED โ†’ Return cached result (fn never called)
183
+ Concurrent call โ†’ Find IN_PROGRESS (not expired) โ†’ Throw VanadiumError(IN_PROGRESS)
184
+ Crash recovery โ†’ Find IN_PROGRESS (expired) โ†’ Atomic takeover โ†’ Re-execute fn
185
+ ```
186
+
187
+ ---
188
+
189
+ ## โœจ Features
190
+
191
+ ### ๐Ÿ” Idempotency Engine
192
+
193
+ Guarantee a function executes exactly once per key, no matter how many times it's called:
194
+
195
+ ```typescript
196
+ const idempotency = createIdempotency({
197
+ adapter: createMemoryAdapter(),
198
+ ttlMs: 86_400_000,
199
+ inProgressExpiryMs: 300_000, // allow crash takeover after 5 minutes
200
+ hashPayload: true, // detect mismatched retries
201
+ cacheFailures: false, // re-execute on failure by default
202
+ });
203
+
204
+ const result = await idempotency.execute(
205
+ 'payment:order_123',
206
+ async () => chargeCard(),
207
+ { amount: 100, currency: 'USD' }, // payload hash โ€” mismatched retry = error
208
+ );
209
+ ```
210
+
211
+ ### ๐Ÿ”’ Distributed Lock Engine
212
+
213
+ Mutual exclusion across processes โ€” safe under 100+ simultaneous callers:
214
+
215
+ ```typescript
216
+ const lock = createLock({
217
+ adapter: createRedisAdapter({ client }),
218
+ ttlMs: 10_000, // auto-expire after 10s (deadlock protection)
219
+ maxWaitMs: 5_000, // wait up to 5s before failing
220
+ retryIntervalMs: 50, // check every 50ms while waiting
221
+ });
222
+
223
+ const result = await lock.acquire('inventory:prod_001', async () => {
224
+ return updateInventory('prod_001');
225
+ });
226
+ ```
227
+
228
+ ### ๐Ÿ—„๏ธ Storage Adapters
229
+
230
+ Six adapters, one interface โ€” behavior is identical across all backends:
231
+
232
+ ```typescript
233
+ // In-memory (zero dependencies โ€” dev, test, single-process)
234
+ createMemoryAdapter({ maxKeys: 50_000 })
235
+
236
+ // Redis (recommended for production)
237
+ createRedisAdapter({ client, keyPrefix: 'vanadium:', useLua: true })
238
+
239
+ // PostgreSQL
240
+ createPostgresAdapter({ client: pool, tableName: 'vanadium_records' })
241
+
242
+ // MongoDB
243
+ createMongoAdapter({ client, dbName: 'myapp', useTransactions: true })
244
+
245
+ // Mongoose
246
+ createMongooseAdapter({ model: VanadiumRecord, useTransactions: true })
247
+
248
+ // Prisma
249
+ createPrismaAdapter({ prisma, modelName: 'vanadiumRecord' })
250
+ ```
251
+
252
+ ### ๐Ÿ›ก๏ธ Circuit Breaker
253
+
254
+ Protect your app from storage failures cascading into full outages:
255
+
256
+ ```typescript
257
+ import { createCircuitBreaker } from '@periodic/vanadium';
258
+
259
+ const protectedAdapter = createCircuitBreaker(redisAdapter, {
260
+ failureThreshold: 5, // open after 5 consecutive failures
261
+ resetTimeoutMs: 30_000, // probe again after 30s
262
+ });
263
+
264
+ const idempotency = createIdempotency({ adapter: protectedAdapter, ttlMs: 60_000 });
265
+ ```
266
+
267
+ **States:** `CLOSED` (normal) โ†’ `OPEN` (all calls fail immediately) โ†’ `HALF_OPEN` (one probe) โ†’ `CLOSED`
268
+
269
+ ### ๐ŸŒ HTTP Middleware
270
+
271
+ Drop-in idempotency for Express and Fastify routes:
272
+
273
+ ```typescript
274
+ // Express
275
+ app.post('/payments', vanadiumMiddleware(idempotency), async (req, res) => {
276
+ const result = await processPayment(req.body);
277
+ res.json(result);
278
+ });
279
+
280
+ // Fastify
281
+ await app.register(vanadiumFastifyPlugin, { idempotency });
282
+ ```
283
+
284
+ **Client usage:**
285
+ ```http
286
+ POST /payments HTTP/1.1
287
+ Idempotency-Key: payment-attempt-uuid-here
288
+ Content-Type: application/json
289
+ ```
290
+
291
+ First request executes the handler and caches the response. Every duplicate request with the same key gets the cached response โ€” handler never runs again.
292
+
293
+ ### ๐Ÿช Lifecycle Hooks
294
+
295
+ Hook into execution events for observability without mutating state:
296
+
297
+ ```typescript
298
+ const idempotency = createIdempotency({
299
+ adapter,
300
+ ttlMs: 60_000,
301
+ hooks: {
302
+ onBeforeExecute: async (ctx) => logger.info('executing', { key: ctx.key }),
303
+ onAfterExecute: async (ctx) => logger.info('completed', { key: ctx.key, durationMs: ctx.durationMs }),
304
+ onDuplicateHit: async (ctx) => logger.info('duplicate deflected', { key: ctx.key }),
305
+ onTakeover: async (ctx) => logger.warn('crash recovery', { key: ctx.key, attempts: ctx.attempts }),
306
+ onStorageError: async (err, key) => Sentry.captureException(err, { extra: { key } }),
307
+ },
308
+ });
309
+ ```
310
+
311
+ ### ๐Ÿ“Š Metrics
312
+
313
+ Per-instance metrics, never global:
314
+
315
+ ```typescript
316
+ const metrics = idempotency.getMetrics();
317
+ // {
318
+ // totalExecutions: 42,
319
+ // totalDuplicates: 15,
320
+ // totalTakeovers: 2,
321
+ // totalStorageErrors: 0,
322
+ // inProgressCount: 0,
323
+ // totalPayloadMismatches: 1,
324
+ // totalFailuresCached: 0,
325
+ // }
326
+
327
+ idempotency.resetMetrics();
328
+ ```
329
+
330
+ ### ๐Ÿ“ก OpenTelemetry
331
+
332
+ Built-in OTEL metrics without hard-requiring the SDK:
333
+
334
+ ```typescript
335
+ import { createOtelExporter } from '@periodic/vanadium';
336
+
337
+ const idempotency = createIdempotency({
338
+ adapter,
339
+ ttlMs: 60_000,
340
+ hooks: createVanadiumMetrics(metrics.getMeter('my-service')),
341
+ });
342
+ ```
343
+
344
+ ---
345
+
346
+ ## ๐Ÿ“š Common Patterns
347
+
348
+ ### 1. Payment Processing
349
+
350
+ ```typescript
351
+ app.post('/payments', vanadiumMiddleware(idempotency), async (req, res) => {
352
+ const charge = await idempotency.execute(
353
+ `payment:${req.headers['idempotency-key']}`,
354
+ async () => stripe.charges.create(req.body),
355
+ req.body, // hash payload โ€” catches mismatched retries
356
+ );
357
+ res.json({ chargeId: charge.id, status: charge.status });
358
+ });
359
+ ```
360
+
361
+ ### 2. Webhook Deduplication
362
+
363
+ ```typescript
364
+ app.post('/webhooks/stripe', async (req, res) => {
365
+ await idempotency.execute(`stripe:${req.body.id}`, async () => {
366
+ const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], secret);
367
+ switch (event.type) {
368
+ case 'payment_intent.succeeded': await fulfillOrder(event.data.object); break;
369
+ case 'customer.subscription.deleted': await cancelSubscription(event.data.object); break;
370
+ }
371
+ });
372
+ res.sendStatus(200);
373
+ });
374
+ ```
375
+
376
+ ### 3. Distributed Cron Lock
377
+
378
+ ```typescript
379
+ async function runDailyReport(): Promise<void> {
380
+ await lock.acquire('cron:daily-report', async () => {
381
+ const today = new Date().toISOString().split('T')[0];
382
+ await idempotency.execute(`report:${today}`, async () => {
383
+ await generateAndSendDailyReport();
384
+ });
385
+ });
386
+ }
387
+ ```
388
+
389
+ ### 4. Crash-Safe Job Runner
390
+
391
+ ```typescript
392
+ const jobRunner = createIdempotency({
393
+ adapter: redisAdapter,
394
+ ttlMs: 7 * 24 * 60 * 60 * 1000, // cache results for 7 days
395
+ inProgressExpiryMs: 10 * 60 * 1000, // allow takeover after 10 minutes
396
+ });
397
+
398
+ async function processJob(jobId: string): Promise<void> {
399
+ await jobRunner.execute(`job:${jobId}`, async () => {
400
+ await runHeavyComputation(jobId);
401
+ });
402
+ }
403
+ ```
404
+
405
+ ### 5. Inventory Update with Lock
406
+
407
+ ```typescript
408
+ const inventoryLock = createLock({
409
+ adapter: redisAdapter,
410
+ ttlMs: 5_000,
411
+ maxWaitMs: 3_000,
412
+ retryIntervalMs: 100,
413
+ });
414
+
415
+ async function reserveInventory(productId: string, quantity: number): Promise<boolean> {
416
+ return inventoryLock.acquire(`inventory:${productId}`, async () => {
417
+ const current = await db.inventory.findOne({ productId });
418
+ if (current.stock < quantity) return false;
419
+ await db.inventory.update({ productId }, { $inc: { stock: -quantity } });
420
+ return true;
421
+ });
422
+ }
423
+ ```
424
+
425
+ ### 6. Double Submit Protection
426
+
427
+ ```typescript
428
+ // Assign a UUID to every form on load, send as Idempotency-Key on submit
429
+ app.post('/orders', async (req, res) => {
430
+ const formId = req.headers['idempotency-key'];
431
+ if (!formId) return res.status(400).json({ error: 'Missing Idempotency-Key' });
432
+
433
+ const order = await idempotency.execute(`form-submit:${formId}`, async () => {
434
+ return createOrder(req.body);
435
+ });
436
+
437
+ res.status(201).json(order);
438
+ });
439
+ ```
440
+
441
+ ### 7. Severity-Based Error Routing
442
+
443
+ ```typescript
444
+ const idempotency = createIdempotency({
445
+ adapter,
446
+ ttlMs: 60_000,
447
+ hooks: {
448
+ onStorageError: async (err, key) => {
449
+ Sentry.captureException(err, { extra: { key } });
450
+ },
451
+ onTakeover: async (ctx) => {
452
+ sendToSlack(`โš ๏ธ Crash recovery on ${ctx.key} โ€” attempt ${ctx.attempts}`);
453
+ },
454
+ },
455
+ });
456
+ ```
457
+
458
+ ### 8. Production Configuration
459
+
460
+ ```typescript
461
+ import { createIdempotency, createLock, createRedisAdapter, createCircuitBreaker } from '@periodic/vanadium';
462
+
463
+ const isDevelopment = process.env.NODE_ENV === 'development';
464
+
465
+ const adapter = isDevelopment
466
+ ? createMemoryAdapter()
467
+ : createCircuitBreaker(
468
+ createRedisAdapter({ client: redis, keyPrefix: 'vanadium:', useLua: true }),
469
+ { failureThreshold: 5, resetTimeoutMs: 30_000 },
470
+ );
471
+
472
+ export const idempotency = createIdempotency({
473
+ adapter,
474
+ ttlMs: 86_400_000,
475
+ inProgressExpiryMs: 300_000,
476
+ hashPayload: !isDevelopment,
477
+ hooks: {
478
+ onAfterExecute: (ctx) => logger.info('vanadium.execute', ctx),
479
+ onDuplicateHit: (ctx) => logger.info('vanadium.duplicate', ctx),
480
+ onStorageError: (err, key) => logger.error('vanadium.storage_error', { err, key }),
481
+ },
482
+ });
483
+
484
+ export const lock = createLock({
485
+ adapter,
486
+ ttlMs: 10_000,
487
+ maxWaitMs: isDevelopment ? 0 : 5_000,
488
+ });
489
+
490
+ export default idempotency;
491
+ ```
492
+
493
+ ---
494
+
495
+ ## ๐ŸŽ›๏ธ Configuration Options
496
+
497
+ ### `createIdempotency` Options
498
+
499
+ | Option | Type | Default | Description |
500
+ |--------|------|---------|-------------|
501
+ | `adapter` | `StorageAdapter` | required | Storage backend |
502
+ | `ttlMs` | `number` | `86_400_000` | Completed result TTL (24h) |
503
+ | `inProgressExpiryMs` | `number` | `300_000` | IN_PROGRESS expiry before crash takeover (5m) |
504
+ | `hashPayload` | `boolean` | `false` | Enable payload hash mismatch detection |
505
+ | `cacheFailures` | `boolean` | `false` | Cache thrown errors (prevents re-execution on failure) |
506
+ | `clock` | `() => number` | `Date.now` | Injectable clock for deterministic testing |
507
+ | `onDuplicate` | `(ctx) => void` | โ€” | Shorthand callback on duplicate detection |
508
+ | `hooks` | `IdempotencyHooks` | โ€” | Full lifecycle hook object |
509
+
510
+ ### `createLock` Options
511
+
512
+ | Option | Type | Default | Description |
513
+ |--------|------|---------|-------------|
514
+ | `adapter` | `StorageAdapter` | required | Storage backend |
515
+ | `ttlMs` | `number` | required | Lock TTL โ€” auto-expires to prevent deadlocks |
516
+ | `retryIntervalMs` | `number` | `50` | How often to retry when waiting for a lock |
517
+ | `maxWaitMs` | `number` | `0` | Max wait time (`0` = fail immediately if locked) |
518
+ | `clock` | `() => number` | `Date.now` | Injectable clock for deterministic testing |
519
+ | `hooks` | `LockHooks` | โ€” | Lifecycle hook object |
520
+
521
+ ### `createMemoryAdapter` Options
522
+
523
+ | Option | Type | Default | Description |
524
+ |--------|------|---------|-------------|
525
+ | `maxKeys` | `number` | `Infinity` | LRU eviction threshold |
526
+ | `clock` | `() => number` | `Date.now` | Injectable clock for testing |
527
+
528
+ ### `createCircuitBreaker` Options
529
+
530
+ | Option | Type | Default | Description |
531
+ |--------|------|---------|-------------|
532
+ | `failureThreshold` | `number` | `5` | Consecutive failures before OPEN |
533
+ | `resetTimeoutMs` | `number` | `30_000` | Time in OPEN before HALF_OPEN probe |
534
+ | `halfOpenMaxCalls` | `number` | `1` | Max probe calls in HALF_OPEN state |
535
+
536
+ ---
537
+
538
+ ## ๐Ÿ“‹ API Reference
539
+
540
+ ### Idempotency
541
+
542
+ ```typescript
543
+ createIdempotency(options: IdempotencyOptions): IdempotencyEngine
544
+ idempotency.execute(key: string, fn: () => Promise<T>, payload?: unknown): Promise<T>
545
+ idempotency.getMetrics(): VanadiumMetrics
546
+ idempotency.resetMetrics(): void
547
+ ```
548
+
549
+ ### Locks
550
+
551
+ ```typescript
552
+ createLock(options: LockOptions): LockEngine
553
+ lock.acquire(key: string, fn: () => Promise<T>): Promise<T>
554
+ lock.getMetrics(): VanadiumMetrics
555
+ ```
556
+
557
+ ### Storage Adapters
558
+
559
+ ```typescript
560
+ createMemoryAdapter(options?): MemoryAdapter
561
+ createRedisAdapter(options): RedisAdapter
562
+ createPostgresAdapter(options): PostgresAdapter
563
+ createMongoAdapter(options): MongoAdapter
564
+ createMongooseAdapter(options): MongooseAdapter
565
+ createPrismaAdapter(options): PrismaAdapter
566
+ createCircuitBreaker(adapter, options?): CircuitBreakerAdapter
567
+ ```
568
+
569
+ ### HTTP Middleware
570
+
571
+ ```typescript
572
+ vanadiumMiddleware(idempotency: IdempotencyEngine, options?): RequestHandler // Express
573
+ vanadiumFastifyPlugin(idempotency: IdempotencyEngine, options?): FastifyPlugin // Fastify
574
+ ```
575
+
576
+ ### Error Handling
577
+
578
+ ```typescript
579
+ isVanadiumError(err: unknown): err is VanadiumError
580
+
581
+ // err.type values:
582
+ 'DUPLICATE_EXECUTION'
583
+ 'IN_PROGRESS'
584
+ 'LOCK_ACQUISITION_FAILED'
585
+ 'LOCK_TIMEOUT'
586
+ 'PAYLOAD_MISMATCH'
587
+ 'CONFIGURATION_ERROR'
588
+ 'STORAGE_ERROR'
589
+ 'STATE_TRANSITION_ERROR'
590
+ ```
591
+
592
+ ### Event Structure
593
+
594
+ ```typescript
595
+ interface StoredRecord<T = unknown> {
596
+ key: string;
597
+ status: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
598
+ result?: T;
599
+ payloadHash?: string;
600
+ ownerToken?: string;
601
+ attempts: number;
602
+ createdAt: number;
603
+ updatedAt: number;
604
+ expiresAt?: number;
605
+ }
606
+ ```
607
+
608
+ ---
609
+
610
+ ## ๐Ÿงฉ Architecture
611
+
612
+ ```
613
+ @periodic/vanadium/
614
+ โ”œโ”€โ”€ src/
615
+ โ”‚ โ”œโ”€โ”€ types/
616
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # All shared type definitions (StoredRecord, StorageAdapter, etc.)
617
+ โ”‚ โ”œโ”€โ”€ errors/
618
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # VanadiumError class + all factory functions
619
+ โ”‚ โ”œโ”€โ”€ core/
620
+ โ”‚ โ”‚ โ”œโ”€โ”€ stateMachine.ts # Valid state transitions (IN_PROGRESSโ†’COMPLETED|FAILED)
621
+ โ”‚ โ”‚ โ”œโ”€โ”€ metrics.ts # MetricsStore โ€” per-instance counters
622
+ โ”‚ โ”‚ โ””โ”€โ”€ concurrencyGuard.ts # In-process deduplication (local optimization layer)
623
+ โ”‚ โ”œโ”€โ”€ idempotency/
624
+ โ”‚ โ”‚ โ””โ”€โ”€ engine.ts # IdempotencyEngineImpl + createIdempotency()
625
+ โ”‚ โ”œโ”€โ”€ lock/
626
+ โ”‚ โ”‚ โ””โ”€โ”€ engine.ts # LockEngineImpl + createLock()
627
+ โ”‚ โ”œโ”€โ”€ adapters/ # Storage adapter implementations
628
+ โ”‚ โ”‚ โ”œโ”€โ”€ memory/index.ts # MemoryAdapter (built-in, LRU, TTL, CAS, zero deps)
629
+ โ”‚ โ”‚ โ”œโ”€โ”€ redis/index.ts # RedisAdapter (Lua CAS, atomic ops)
630
+ โ”‚ โ”‚ โ”œโ”€โ”€ postgres/index.ts # PostgresAdapter (advisory locks)
631
+ โ”‚ โ”‚ โ”œโ”€โ”€ mongodb/index.ts # MongoAdapter (findOneAndUpdate CAS)
632
+ โ”‚ โ”‚ โ”œโ”€โ”€ mongoose/index.ts # MongooseAdapter
633
+ โ”‚ โ”‚ โ””โ”€โ”€ prisma/index.ts # PrismaAdapter
634
+ โ”‚ โ”œโ”€โ”€ resilience/
635
+ โ”‚ โ”‚ โ””โ”€โ”€ circuitBreaker.ts # CircuitBreakerAdapter (CLOSED/OPEN/HALF_OPEN)
636
+ โ”‚ โ”œโ”€โ”€ cleanup/
637
+ โ”‚ โ”‚ โ””โ”€โ”€ engine.ts # CleanupEngine (background stale record cleanup)
638
+ โ”‚ โ”œโ”€โ”€ http/
639
+ โ”‚ โ”‚ โ”œโ”€โ”€ express.ts # Express middleware
640
+ โ”‚ โ”‚ โ””โ”€โ”€ fastify.ts # Fastify plugin
641
+ โ”‚ โ”œโ”€โ”€ observability/
642
+ โ”‚ โ”‚ โ””โ”€โ”€ metrics.ts # OTel-compatible metrics
643
+ โ”‚ โ””โ”€โ”€ utils/
644
+ โ”‚ โ”œโ”€โ”€ crypto.ts # SHA-256 hashing, UUID token generation
645
+ โ”‚ โ”œโ”€โ”€ keys.ts # Key validation and namespacing
646
+ โ”‚ โ””โ”€โ”€ sleep.ts # Non-blocking sleep + jitter
647
+ ```
648
+
649
+ **Design Philosophy:**
650
+ - **Core** is pure TypeScript with no dependencies
651
+ - **Adapters** implement a single `StorageAdapter` interface โ€” swap without changing application code
652
+ - **HTTP middleware** is thin โ€” it delegates entirely to the idempotency engine
653
+ - **Circuit breaker** wraps any adapter โ€” composable, not built-in
654
+ - **Hooks** are observer-only โ€” they can never affect execution outcome
655
+ - Easy to extend with custom adapters
656
+
657
+ ---
658
+
659
+ ## ๐Ÿ“ˆ Performance
660
+
661
+ Vanadium is optimized for production workloads:
662
+
663
+ - **Zero blocking** โ€” All storage operations are async, never delay response
664
+ - **In-process coalescing** โ€” Concurrent calls for the same key within one process are deduplicated before hitting storage
665
+ - **LRU eviction** โ€” Memory adapter is bounded and never grows unbounded
666
+ - **Lua scripts** โ€” Redis CAS is atomic at the server, no round-trip races
667
+ - **Hook isolation** โ€” Hook errors are silently swallowed, never affect execution
668
+ - **No monkey-patching** โ€” Clean hooks only, no prototype mutation
669
+
670
+ ---
671
+
672
+ ## ๐Ÿšซ Explicit Non-Goals
673
+
674
+ This package **intentionally does not** include:
675
+
676
+ โŒ Message queuing (use BullMQ, RabbitMQ, or Kafka)
677
+ โŒ Job scheduling (use cron libraries or cloud schedulers)
678
+ โŒ Distributed consensus (use etcd or ZooKeeper)
679
+ โŒ Retry logic (it prevents redundant retries, not manages them)
680
+ โŒ Business data storage (it stores execution state, not your data)
681
+ โŒ Built-in dashboards (use Grafana, Datadog, etc.)
682
+ โŒ Blocking behavior in production
683
+ โŒ Magic or implicit behavior on import
684
+ โŒ Configuration files (configure in code)
685
+
686
+ Focus on doing one thing well: **deterministic, safe, single-execution semantics for critical operations**.
687
+
688
+ ---
689
+
690
+ ## ๐ŸŽจ TypeScript Support
691
+
692
+ Full TypeScript support with complete type safety:
693
+
694
+ ```typescript
695
+ import type {
696
+ StorageAdapter,
697
+ StoredRecord,
698
+ IdempotencyOptions,
699
+ LockOptions,
700
+ VanadiumMetrics,
701
+ VanadiumErrorType,
702
+ IdempotencyHooks,
703
+ LockHooks,
704
+ } from '@periodic/vanadium';
705
+
706
+ // Fully generic โ€” type inference works automatically
707
+ const result: string = await idempotency.execute('key', async () => 'hello');
708
+
709
+ // Explicit generic when needed
710
+ const record: { id: number } = await idempotency.execute<{ id: number }>(
711
+ 'key',
712
+ async () => ({ id: 42 }),
713
+ );
714
+ ```
715
+
716
+ ---
717
+
718
+ ## ๐Ÿงช Testing
719
+
720
+ ```bash
721
+ # Run tests
722
+ npm test
723
+
724
+ # Run tests with coverage
725
+ npm run test:coverage
726
+
727
+ # Run tests in watch mode
728
+ npm run test:watch
729
+ ```
730
+
731
+ **Note:** All tests achieve >80% code coverage.
732
+
733
+ ---
734
+
735
+ ## ๐Ÿค Related Packages
736
+
737
+ Part of the **Periodic** series by Uday Thakur:
738
+
739
+ - [**@periodic/iridium**](https://www.npmjs.com/package/@periodic/iridium) - Structured logging
740
+ - [**@periodic/arsenic**](https://www.npmjs.com/package/@periodic/arsenic) - Semantic runtime monitoring
741
+ - [**@periodic/zirconium**](https://www.npmjs.com/package/@periodic/zirconium) - Environment configuration
742
+ - [**@periodic/obsidian**](https://www.npmjs.com/package/@periodic/obsidian) - HTTP error handling
743
+ - [**@periodic/titanium**](https://www.npmjs.com/package/@periodic/titanium) - Rate limiting
744
+ - [**@periodic/osmium**](https://www.npmjs.com/package/@periodic/osmium) - Redis caching
745
+
746
+ Build complete, production-ready APIs with the Periodic series!
747
+
748
+ ---
749
+
750
+ ## ๐Ÿ“– Documentation
751
+
752
+ - [Quick Start Guide](QUICKSTART.md)
753
+ - [Storage Adapter Guide](ADAPTERS.md)
754
+ - [Contributing Guide](CONTRIBUTING.md)
755
+ - [Changelog](CHANGELOG.md)
756
+
757
+ ---
758
+
759
+ ## ๐Ÿ› ๏ธ Production Recommendations
760
+
761
+ ### Environment Variables
762
+
763
+ ```bash
764
+ NODE_ENV=production
765
+ REDIS_URL=redis://...
766
+ ```
767
+
768
+ ### Log Aggregation
769
+
770
+ Pair with `@periodic/iridium` for structured JSON output:
771
+
772
+ ```typescript
773
+ import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
774
+ import { createIdempotency } from '@periodic/vanadium';
775
+
776
+ const logger = createLogger({
777
+ transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
778
+ });
779
+
780
+ const idempotency = createIdempotency({
781
+ adapter,
782
+ ttlMs: 86_400_000,
783
+ hooks: {
784
+ onAfterExecute: (ctx) => logger.info('vanadium.execute', ctx),
785
+ onDuplicateHit: (ctx) => logger.info('vanadium.duplicate', ctx),
786
+ onStorageError: (err, key) => logger.error('vanadium.storage_error', { err, key }),
787
+ },
788
+ });
789
+
790
+ // Pipe to Elasticsearch, Datadog, CloudWatch, etc.
791
+ ```
792
+
793
+ ### Observability
794
+
795
+ Integrate with error tracking and metrics:
796
+
797
+ ```typescript
798
+ const idempotency = createIdempotency({
799
+ adapter,
800
+ ttlMs: 86_400_000,
801
+ hooks: {
802
+ onTakeover: (ctx) => {
803
+ Sentry.captureEvent({ message: `crash recovery: ${ctx.key}`, extra: ctx });
804
+ },
805
+ onStorageError: (err, key) => {
806
+ Sentry.captureException(err, { extra: { key } });
807
+ },
808
+ },
809
+ });
810
+ ```
811
+
812
+ ---
813
+
814
+ ## ๐Ÿ“ License
815
+
816
+ MIT ยฉ [Uday Thakur](LICENSE)
817
+
818
+ ---
819
+
820
+ ## ๐Ÿ™ Contributing
821
+
822
+ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:
823
+
824
+ - Code of conduct
825
+ - Development setup
826
+ - Pull request process
827
+ - Coding standards
828
+ - Architecture principles
829
+
830
+ ---
831
+
832
+ ## ๐Ÿ“ž Support
833
+
834
+ - ๐Ÿ“ง **Email:** udaythakurwork@gmail.com
835
+ - ๐Ÿ› **Issues:** [GitHub Issues](https://github.com/udaythakur7469/periodic-vanadium/issues)
836
+ - ๐Ÿ’ฌ **Discussions:** [GitHub Discussions](https://github.com/udaythakur7469/periodic-vanadium/discussions)
837
+
838
+ ---
839
+
840
+ ## ๐ŸŒŸ Show Your Support
841
+
842
+ Give a โญ๏ธ if this project helped you build better applications!
843
+
844
+ ---
845
+
846
+ **Built with โค๏ธ by Uday Thakur for production-grade Node.js applications**