@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.
- package/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +846 -0
- package/dist/cjs/adapters/memory/index.js +134 -0
- package/dist/cjs/adapters/memory/index.js.map +1 -0
- package/dist/cjs/adapters/mongodb/index.js +189 -0
- package/dist/cjs/adapters/mongodb/index.js.map +1 -0
- package/dist/cjs/adapters/mongoose/index.js +199 -0
- package/dist/cjs/adapters/mongoose/index.js.map +1 -0
- package/dist/cjs/adapters/postgres/index.js +202 -0
- package/dist/cjs/adapters/postgres/index.js.map +1 -0
- package/dist/cjs/adapters/prisma/index.js +176 -0
- package/dist/cjs/adapters/prisma/index.js.map +1 -0
- package/dist/cjs/adapters/redis/index.js +178 -0
- package/dist/cjs/adapters/redis/index.js.map +1 -0
- package/dist/cjs/cleanup/engine.js +100 -0
- package/dist/cjs/cleanup/engine.js.map +1 -0
- package/dist/cjs/core/concurrencyGuard.js +50 -0
- package/dist/cjs/core/concurrencyGuard.js.map +1 -0
- package/dist/cjs/core/metrics.js +39 -0
- package/dist/cjs/core/metrics.js.map +1 -0
- package/dist/cjs/core/stateMachine.js +46 -0
- package/dist/cjs/core/stateMachine.js.map +1 -0
- package/dist/cjs/errors/index.js +127 -0
- package/dist/cjs/errors/index.js.map +1 -0
- package/dist/cjs/http/express.js +84 -0
- package/dist/cjs/http/express.js.map +1 -0
- package/dist/cjs/http/fastify.js +70 -0
- package/dist/cjs/http/fastify.js.map +1 -0
- package/dist/cjs/idempotency/engine.js +266 -0
- package/dist/cjs/idempotency/engine.js.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lock/engine.js +187 -0
- package/dist/cjs/lock/engine.js.map +1 -0
- package/dist/cjs/observability/metrics.js +92 -0
- package/dist/cjs/observability/metrics.js.map +1 -0
- package/dist/cjs/resilience/circuitBreaker.js +129 -0
- package/dist/cjs/resilience/circuitBreaker.js.map +1 -0
- package/dist/cjs/types/index.js +13 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/utils/crypto.js +64 -0
- package/dist/cjs/utils/crypto.js.map +1 -0
- package/dist/cjs/utils/keys.js +40 -0
- package/dist/cjs/utils/keys.js.map +1 -0
- package/dist/cjs/utils/sleep.js +25 -0
- package/dist/cjs/utils/sleep.js.map +1 -0
- package/dist/esm/adapters/memory/index.js +129 -0
- package/dist/esm/adapters/memory/index.js.map +1 -0
- package/dist/esm/adapters/mongodb/index.js +184 -0
- package/dist/esm/adapters/mongodb/index.js.map +1 -0
- package/dist/esm/adapters/mongoose/index.js +193 -0
- package/dist/esm/adapters/mongoose/index.js.map +1 -0
- package/dist/esm/adapters/postgres/index.js +197 -0
- package/dist/esm/adapters/postgres/index.js.map +1 -0
- package/dist/esm/adapters/prisma/index.js +171 -0
- package/dist/esm/adapters/prisma/index.js.map +1 -0
- package/dist/esm/adapters/redis/index.js +173 -0
- package/dist/esm/adapters/redis/index.js.map +1 -0
- package/dist/esm/cleanup/engine.js +95 -0
- package/dist/esm/cleanup/engine.js.map +1 -0
- package/dist/esm/core/concurrencyGuard.js +46 -0
- package/dist/esm/core/concurrencyGuard.js.map +1 -0
- package/dist/esm/core/metrics.js +35 -0
- package/dist/esm/core/metrics.js.map +1 -0
- package/dist/esm/core/stateMachine.js +40 -0
- package/dist/esm/core/stateMachine.js.map +1 -0
- package/dist/esm/errors/index.js +114 -0
- package/dist/esm/errors/index.js.map +1 -0
- package/dist/esm/http/express.js +81 -0
- package/dist/esm/http/express.js.map +1 -0
- package/dist/esm/http/fastify.js +67 -0
- package/dist/esm/http/fastify.js.map +1 -0
- package/dist/esm/idempotency/engine.js +261 -0
- package/dist/esm/idempotency/engine.js.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lock/engine.js +182 -0
- package/dist/esm/lock/engine.js.map +1 -0
- package/dist/esm/observability/metrics.js +89 -0
- package/dist/esm/observability/metrics.js.map +1 -0
- package/dist/esm/resilience/circuitBreaker.js +124 -0
- package/dist/esm/resilience/circuitBreaker.js.map +1 -0
- package/dist/esm/types/index.js +10 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/utils/crypto.js +58 -0
- package/dist/esm/utils/crypto.js.map +1 -0
- package/dist/esm/utils/keys.js +35 -0
- package/dist/esm/utils/keys.js.map +1 -0
- package/dist/esm/utils/sleep.js +20 -0
- package/dist/esm/utils/sleep.js.map +1 -0
- package/dist/types/adapters/memory/index.d.ts +49 -0
- package/dist/types/adapters/memory/index.d.ts.map +1 -0
- package/dist/types/adapters/mongodb/index.d.ts +97 -0
- package/dist/types/adapters/mongodb/index.d.ts.map +1 -0
- package/dist/types/adapters/mongoose/index.d.ts +107 -0
- package/dist/types/adapters/mongoose/index.d.ts.map +1 -0
- package/dist/types/adapters/postgres/index.d.ts +85 -0
- package/dist/types/adapters/postgres/index.d.ts.map +1 -0
- package/dist/types/adapters/prisma/index.d.ts +73 -0
- package/dist/types/adapters/prisma/index.d.ts.map +1 -0
- package/dist/types/adapters/redis/index.d.ts +77 -0
- package/dist/types/adapters/redis/index.d.ts.map +1 -0
- package/dist/types/cleanup/engine.d.ts +41 -0
- package/dist/types/cleanup/engine.d.ts.map +1 -0
- package/dist/types/core/concurrencyGuard.d.ts +28 -0
- package/dist/types/core/concurrencyGuard.d.ts.map +1 -0
- package/dist/types/core/metrics.d.ts +13 -0
- package/dist/types/core/metrics.d.ts.map +1 -0
- package/dist/types/core/stateMachine.d.ts +20 -0
- package/dist/types/core/stateMachine.d.ts.map +1 -0
- package/dist/types/errors/index.d.ts +32 -0
- package/dist/types/errors/index.d.ts.map +1 -0
- package/dist/types/http/express.d.ts +50 -0
- package/dist/types/http/express.d.ts.map +1 -0
- package/dist/types/http/fastify.d.ts +48 -0
- package/dist/types/http/fastify.d.ts.map +1 -0
- package/dist/types/idempotency/engine.d.ts +24 -0
- package/dist/types/idempotency/engine.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lock/engine.d.ts +28 -0
- package/dist/types/lock/engine.d.ts.map +1 -0
- package/dist/types/observability/metrics.d.ts +45 -0
- package/dist/types/observability/metrics.d.ts.map +1 -0
- package/dist/types/resilience/circuitBreaker.d.ts +48 -0
- package/dist/types/resilience/circuitBreaker.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +170 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/utils/crypto.d.ts +20 -0
- package/dist/types/utils/crypto.d.ts.map +1 -0
- package/dist/types/utils/keys.d.ts +15 -0
- package/dist/types/utils/keys.d.ts.map +1 -0
- package/dist/types/utils/sleep.d.ts +13 -0
- package/dist/types/utils/sleep.d.ts.map +1 -0
- package/package.json +140 -0
package/README.md
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
# โฌก Periodic Vanadium
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@periodic/vanadium)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](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**
|