@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
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Fastify plugin for @periodic/vanadium
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import Fastify from 'fastify';
7
+ * import { createIdempotency, createMemoryAdapter } from '@periodic/vanadium';
8
+ * import { vanadiumFastifyPlugin } from '@periodic/vanadium/http/fastify';
9
+ *
10
+ * const app = Fastify();
11
+ * const idempotency = createIdempotency({ adapter: createMemoryAdapter(), ttlMs: 86_400_000 });
12
+ *
13
+ * await app.register(vanadiumFastifyPlugin, { idempotency });
14
+ *
15
+ * app.post('/payments', async (request, reply) => {
16
+ * const result = await chargeCard(request.body);
17
+ * return result;
18
+ * });
19
+ * ```
20
+ */
21
+ import { isVanadiumError } from '../errors/index.js';
22
+ // ─── Plugin ───────────────────────────────────────────────────────────────────
23
+ /**
24
+ * Register Vanadium idempotency as a Fastify plugin.
25
+ */
26
+ export async function vanadiumFastifyPlugin(fastify, options) {
27
+ const { idempotency } = options;
28
+ const headerName = options.headerName ?? 'idempotency-key';
29
+ const methods = (options.methods ?? ['POST', 'PUT', 'PATCH']).map((m) => m.toUpperCase());
30
+ fastify.addHook('preHandler', async (request, reply) => {
31
+ if (!methods.includes(request.method.toUpperCase()))
32
+ return;
33
+ const rawHeader = request.headers[headerName.toLowerCase()];
34
+ const idempotencyKey = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
35
+ if (!idempotencyKey)
36
+ return;
37
+ const fullKey = `http:${request.method}:${request.url}:${idempotencyKey}`;
38
+ try {
39
+ // For Fastify, we check if a COMPLETED result exists and replay it
40
+ // The actual execution is handled downstream
41
+ const existing = await idempotency
42
+ .execute(fullKey, async () => {
43
+ // placeholder — real response will be captured in onSend
44
+ return null;
45
+ })
46
+ .catch((err) => {
47
+ if (isVanadiumError(err) && err.type === 'IN_PROGRESS') {
48
+ reply.code(409).send({
49
+ error: 'Request is currently being processed',
50
+ type: 'IN_PROGRESS',
51
+ key: err.key,
52
+ });
53
+ }
54
+ throw err;
55
+ });
56
+ if (existing !== null && reply.sent === false) {
57
+ reply.code(200).send(existing);
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (isVanadiumError(err) && err.type === 'DUPLICATE_EXECUTION') {
62
+ return; // Already sent cached response
63
+ }
64
+ }
65
+ });
66
+ }
67
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../../src/http/fastify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAkCrD,iFAAiF;AAEjF;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAAwB,EACxB,OAA+B;IAE/B,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC;IAC3D,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAE1F,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACrD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAAE,OAAO;QAE5D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;QAC5D,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3E,IAAI,CAAC,cAAc;YAAE,OAAO;QAE5B,MAAM,OAAO,GAAG,QAAQ,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,IAAI,cAAc,EAAE,CAAC;QAE1E,IAAI,CAAC;YACH,mEAAmE;YACnE,6CAA6C;YAC7C,MAAM,QAAQ,GAAG,MAAM,WAAW;iBAC/B,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBAC3B,yDAAyD;gBACzD,OAAO,IAAI,CAAC;YACd,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACb,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACvD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,KAAK,EAAE,sCAAsC;wBAC7C,IAAI,EAAE,aAAa;wBACnB,GAAG,EAAE,GAAG,CAAC,GAAG;qBACb,CAAC,CAAC;gBACL,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC,CAAC,CAAC;YAEL,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;gBAC9C,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;gBAC/D,OAAO,CAAC,+BAA+B;YACzC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,261 @@
1
+ import { createDuplicateExecutionError, createInProgressError, createPayloadMismatchError, createStorageError, } from '../errors/index.js';
2
+ import { MetricsStore } from '../core/metrics.js';
3
+ import { ConcurrencyGuard } from '../core/concurrencyGuard.js';
4
+ import { canTakeover, assertValidTransition } from '../core/stateMachine.js';
5
+ import { hashPayload, generateOwnerToken } from '../utils/crypto.js';
6
+ import { validateKey } from '../utils/keys.js';
7
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
8
+ const DEFAULT_IN_PROGRESS_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
9
+ // ─── Idempotency Engine ───────────────────────────────────────────────────────
10
+ export class IdempotencyEngineImpl {
11
+ constructor(options) {
12
+ this.metrics = new MetricsStore();
13
+ this.guard = new ConcurrencyGuard();
14
+ this.opts = {
15
+ adapter: options.adapter,
16
+ ttlMs: options.ttlMs ?? DEFAULT_TTL_MS,
17
+ inProgressExpiryMs: options.inProgressExpiryMs ?? DEFAULT_IN_PROGRESS_EXPIRY_MS,
18
+ hashPayload: options.hashPayload ?? false,
19
+ cacheFailures: options.cacheFailures ?? false,
20
+ clock: options.clock ?? Date.now,
21
+ onDuplicate: options.onDuplicate,
22
+ hooks: options.hooks,
23
+ };
24
+ }
25
+ // ── Public API ────────────────────────────────────────────────────────────
26
+ async execute(key, fn, payload) {
27
+ validateKey(key, this.opts.adapter.name);
28
+ return this.guard.wrap(key, () => this._executeInternal(key, fn, payload));
29
+ }
30
+ getMetrics() {
31
+ return this.metrics.get();
32
+ }
33
+ resetMetrics() {
34
+ this.metrics.reset();
35
+ }
36
+ // ── Core Execution Flow ───────────────────────────────────────────────────
37
+ async _executeInternal(key, fn, payload) {
38
+ const { adapter, clock,
39
+ // ttlMs,
40
+ inProgressExpiryMs, hashPayload: shouldHash, cacheFailures, } = this.opts;
41
+ const now = clock();
42
+ const ownerToken = generateOwnerToken();
43
+ const incomingHash = shouldHash && payload !== undefined ? hashPayload(payload) : undefined;
44
+ // ── Step 1: Check existing record ────────────────────────────────────────
45
+ let existing = null;
46
+ try {
47
+ existing = await adapter.get(key);
48
+ }
49
+ catch (err) {
50
+ this.metrics.increment('totalStorageErrors');
51
+ await this._runHook('onStorageError', err, key);
52
+ throw createStorageError(key, adapter.name, err, clock);
53
+ }
54
+ // ── Step 2: Handle existing record ────────────────────────────────────────
55
+ if (existing !== null) {
56
+ // Payload mismatch detection
57
+ if (incomingHash !== undefined && existing.payloadHash !== undefined) {
58
+ if (existing.payloadHash !== incomingHash) {
59
+ this.metrics.increment('totalPayloadMismatches');
60
+ throw createPayloadMismatchError(key, adapter.name, existing.payloadHash, incomingHash, clock);
61
+ }
62
+ }
63
+ // Completed record — return cached result
64
+ if (existing.status === 'COMPLETED') {
65
+ this.metrics.increment('totalDuplicates');
66
+ this.opts.onDuplicate?.({
67
+ key,
68
+ status: existing.status,
69
+ attempts: existing.attempts,
70
+ adapterName: adapter.name,
71
+ });
72
+ await this._runHook('onDuplicateHit', {
73
+ key,
74
+ status: existing.status,
75
+ attempts: existing.attempts,
76
+ adapterName: adapter.name,
77
+ });
78
+ return existing.result;
79
+ }
80
+ // Failed record with cacheFailures enabled — rethrow cached error
81
+ if (existing.status === 'FAILED' && cacheFailures) {
82
+ this.metrics.increment('totalDuplicates');
83
+ throw createDuplicateExecutionError(key, adapter.name, clock);
84
+ }
85
+ // IN_PROGRESS: check for expired takeover
86
+ if (existing.status === 'IN_PROGRESS') {
87
+ if (canTakeover(existing.status, existing.expiresAt, clock)) {
88
+ // Takeover: increment attempts, assign new owner
89
+ const takeoverRecord = {
90
+ ...existing,
91
+ ownerToken,
92
+ attempts: existing.attempts + 1,
93
+ updatedAt: now,
94
+ expiresAt: now + inProgressExpiryMs,
95
+ };
96
+ assertValidTransition('IN_PROGRESS', 'IN_PROGRESS', key, adapter.name, clock);
97
+ try {
98
+ if (adapter.compareAndSet) {
99
+ const success = await adapter.compareAndSet(key, existing.ownerToken ?? '', takeoverRecord, inProgressExpiryMs);
100
+ if (!success) {
101
+ // Another node beat us to the takeover
102
+ throw createInProgressError(key, adapter.name, existing.attempts, clock);
103
+ }
104
+ }
105
+ else {
106
+ await adapter.set(key, takeoverRecord, inProgressExpiryMs);
107
+ }
108
+ }
109
+ catch (err) {
110
+ if (err.type === 'IN_PROGRESS')
111
+ throw err;
112
+ this.metrics.increment('totalStorageErrors');
113
+ throw createStorageError(key, adapter.name, err, clock);
114
+ }
115
+ this.metrics.increment('totalTakeovers');
116
+ await this._runHook('onTakeover', {
117
+ key,
118
+ previousOwnerToken: existing.ownerToken,
119
+ newOwnerToken: ownerToken,
120
+ attempts: takeoverRecord.attempts,
121
+ adapterName: adapter.name,
122
+ });
123
+ return this._runFunction(key, fn, ownerToken, takeoverRecord.attempts, incomingHash);
124
+ }
125
+ // Truly in-progress — reject
126
+ this.metrics.increment('totalDuplicates');
127
+ throw createInProgressError(key, adapter.name, existing.attempts, clock);
128
+ }
129
+ }
130
+ // ── Step 3: Claim ownership — write IN_PROGRESS ───────────────────────────
131
+ const progressRecord = {
132
+ key,
133
+ status: 'IN_PROGRESS',
134
+ ownerToken,
135
+ attempts: 1,
136
+ payloadHash: incomingHash,
137
+ createdAt: now,
138
+ updatedAt: now,
139
+ expiresAt: now + inProgressExpiryMs,
140
+ };
141
+ try {
142
+ await adapter.set(key, progressRecord, inProgressExpiryMs);
143
+ }
144
+ catch (err) {
145
+ this.metrics.increment('totalStorageErrors');
146
+ await this._runHook('onStorageError', err, key);
147
+ throw createStorageError(key, adapter.name, err, clock);
148
+ }
149
+ this.metrics.increment('inProgressCount');
150
+ this.metrics.increment('totalExecutions');
151
+ return this._runFunction(key, fn, ownerToken, 1, incomingHash);
152
+ }
153
+ // ── Function Execution ────────────────────────────────────────────────────
154
+ async _runFunction(key, fn, ownerToken, attempt, incomingHash) {
155
+ const { adapter, clock, ttlMs, cacheFailures } = this.opts;
156
+ const startedAt = clock();
157
+ await this._runHook('onBeforeExecute', {
158
+ key,
159
+ attempt,
160
+ adapterName: adapter.name,
161
+ startedAt,
162
+ });
163
+ try {
164
+ const result = await fn();
165
+ const completedAt = clock();
166
+ // Write COMPLETED record
167
+ assertValidTransition('IN_PROGRESS', 'COMPLETED', key, adapter.name, clock);
168
+ const completedRecord = {
169
+ key,
170
+ status: 'COMPLETED',
171
+ result,
172
+ ownerToken,
173
+ attempts: attempt,
174
+ payloadHash: incomingHash,
175
+ createdAt: startedAt,
176
+ updatedAt: completedAt,
177
+ expiresAt: completedAt + ttlMs,
178
+ };
179
+ try {
180
+ await adapter.set(key, completedRecord, ttlMs);
181
+ }
182
+ catch (err) {
183
+ this.metrics.increment('totalStorageErrors');
184
+ await this._runHook('onStorageError', err, key);
185
+ throw createStorageError(key, adapter.name, err, clock);
186
+ }
187
+ this.metrics.decrement('inProgressCount');
188
+ await this._runHook('onAfterExecute', {
189
+ key,
190
+ attempt,
191
+ adapterName: adapter.name,
192
+ startedAt,
193
+ durationMs: completedAt - startedAt,
194
+ });
195
+ return result;
196
+ }
197
+ catch (err) {
198
+ // If it's already a VanadiumError from storage, re-throw
199
+ if (err.type !== undefined)
200
+ throw err;
201
+ const failedAt = clock();
202
+ assertValidTransition('IN_PROGRESS', 'FAILED', key, adapter.name, clock);
203
+ if (cacheFailures) {
204
+ const failedRecord = {
205
+ key,
206
+ status: 'FAILED',
207
+ result: undefined,
208
+ ownerToken,
209
+ attempts: attempt,
210
+ createdAt: startedAt,
211
+ updatedAt: failedAt,
212
+ };
213
+ try {
214
+ await adapter.set(key, failedRecord);
215
+ }
216
+ catch {
217
+ // Best effort — don't mask original error
218
+ }
219
+ this.metrics.increment('totalFailuresCached');
220
+ }
221
+ else {
222
+ try {
223
+ await adapter.delete(key);
224
+ }
225
+ catch {
226
+ // Best effort
227
+ }
228
+ }
229
+ this.metrics.decrement('inProgressCount');
230
+ throw err;
231
+ }
232
+ }
233
+ // ── Hook Runner ───────────────────────────────────────────────────────────
234
+ async _runHook(hookName,
235
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ ...args) {
237
+ const hook = this.opts.hooks?.[hookName];
238
+ if (hook) {
239
+ try {
240
+ await hook(...args);
241
+ }
242
+ catch {
243
+ // Hooks must never break execution flow
244
+ }
245
+ }
246
+ }
247
+ }
248
+ // ─── Factory ──────────────────────────────────────────────────────────────────
249
+ /**
250
+ * Create a new idempotency engine instance.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * const idempotency = createIdempotency({ adapter, ttlMs: 60_000 });
255
+ * const result = await idempotency.execute('payment:123', () => chargeCard());
256
+ * ```
257
+ */
258
+ export function createIdempotency(options) {
259
+ return new IdempotencyEngineImpl(options);
260
+ }
261
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/idempotency/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ // ─── Core ─────────────────────────────────────────────────────────────────────
2
+ export { createIdempotency } from './idempotency/engine.js';
3
+ export { createLock } from './lock/engine.js';
4
+ export { createMemoryAdapter } from './adapters/memory/index.js';
5
+ export { createCircuitBreaker } from './resilience/circuitBreaker.js';
6
+ export { createVanadiumMetrics } from './observability/metrics.js';
7
+ // ─── Errors ───────────────────────────────────────────────────────────────────
8
+ export { VanadiumError, isVanadiumError } from './errors/index.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,iFAAiF;AACjF,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,182 @@
1
+ import { createLockAcquisitionFailedError, createLockTimeoutError, createStorageError, } from '../errors/index.js';
2
+ import { MetricsStore } from '../core/metrics.js';
3
+ import { generateOwnerToken } from '../utils/crypto.js';
4
+ import { validateKey } from '../utils/keys.js';
5
+ import { sleep } from '../utils/sleep.js';
6
+ const DEFAULT_RETRY_INTERVAL_MS = 50;
7
+ const DEFAULT_MAX_WAIT_MS = 0; // 0 = no wait, fail immediately
8
+ // ─── Lock Engine ──────────────────────────────────────────────────────────────
9
+ export class LockEngineImpl {
10
+ constructor(options) {
11
+ this.metrics = new MetricsStore();
12
+ this.opts = {
13
+ adapter: options.adapter,
14
+ ttlMs: options.ttlMs,
15
+ retryIntervalMs: options.retryIntervalMs ?? DEFAULT_RETRY_INTERVAL_MS,
16
+ maxWaitMs: options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS,
17
+ clock: options.clock ?? Date.now,
18
+ hooks: options.hooks,
19
+ };
20
+ }
21
+ // ── Public API ────────────────────────────────────────────────────────────
22
+ /**
23
+ * Acquire an exclusive lock for `key`, execute `fn`, then release the lock.
24
+ * Guarantees only one concurrent execution per key across all nodes.
25
+ *
26
+ * @throws {VanadiumError} LOCK_ACQUISITION_FAILED | LOCK_TIMEOUT
27
+ */
28
+ async acquire(key, fn) {
29
+ validateKey(key, this.opts.adapter.name);
30
+ const ownerToken = generateOwnerToken();
31
+ const { adapter, ttlMs, retryIntervalMs, maxWaitMs, clock } = this.opts;
32
+ const startedAt = clock();
33
+ const deadline = maxWaitMs > 0 ? startedAt + maxWaitMs : null;
34
+ // ── Acquire Phase ─────────────────────────────────────────────────────────
35
+ while (true) {
36
+ const acquired = await this._tryAcquire(key, ownerToken, ttlMs);
37
+ if (acquired) {
38
+ break;
39
+ }
40
+ const now = clock();
41
+ if (deadline !== null && now >= deadline) {
42
+ this.metrics.increment('totalLockFailures');
43
+ await this._runHook('onLockFailed', key, 'timeout');
44
+ throw createLockTimeoutError(key, adapter.name, maxWaitMs, clock);
45
+ }
46
+ if (deadline === null) {
47
+ // No wait — fail immediately
48
+ this.metrics.increment('totalLockFailures');
49
+ await this._runHook('onLockFailed', key, 'immediate_fail');
50
+ throw createLockAcquisitionFailedError(key, adapter.name, clock);
51
+ }
52
+ await sleep(retryIntervalMs);
53
+ }
54
+ const acquiredAt = clock();
55
+ this.metrics.increment('totalLocksAcquired');
56
+ await this._runHook('onLockAcquired', {
57
+ key,
58
+ ownerToken,
59
+ adapterName: adapter.name,
60
+ acquiredAt,
61
+ ttlMs,
62
+ });
63
+ // ── Execution Phase ───────────────────────────────────────────────────────
64
+ try {
65
+ return await fn();
66
+ }
67
+ finally {
68
+ // ── Release Phase (always) ────────────────────────────────────────────
69
+ await this._safeRelease(key, ownerToken);
70
+ const releasedAt = clock();
71
+ await this._runHook('onLockReleased', {
72
+ key,
73
+ ownerToken,
74
+ adapterName: adapter.name,
75
+ acquiredAt,
76
+ durationMs: releasedAt - acquiredAt,
77
+ ttlMs,
78
+ });
79
+ }
80
+ }
81
+ getMetrics() {
82
+ const m = this.metrics.get();
83
+ return {
84
+ totalLocksAcquired: m.totalLocksAcquired,
85
+ totalLockFailures: m.totalLockFailures,
86
+ };
87
+ }
88
+ // ── Private ───────────────────────────────────────────────────────────────
89
+ async _tryAcquire(key, ownerToken, ttlMs) {
90
+ const { adapter, clock } = this.opts;
91
+ const now = clock();
92
+ try {
93
+ const existing = await adapter.get(key);
94
+ if (existing !== null) {
95
+ // Lock is held — check if TTL has expired (deadlock protection)
96
+ if (existing.expiresAt !== undefined && now >= existing.expiresAt) {
97
+ // Expired lock — attempt CAS takeover
98
+ if (adapter.compareAndSet) {
99
+ const lockRecord = {
100
+ key,
101
+ status: 'IN_PROGRESS',
102
+ ownerToken,
103
+ attempts: (existing.attempts ?? 0) + 1,
104
+ createdAt: existing.createdAt,
105
+ updatedAt: now,
106
+ expiresAt: now + ttlMs,
107
+ };
108
+ const taken = await adapter.compareAndSet(key, existing.ownerToken ?? '', lockRecord, ttlMs);
109
+ return taken;
110
+ }
111
+ // No CAS support — overwrite (less safe, but acceptable for memory adapter)
112
+ }
113
+ else {
114
+ return false;
115
+ }
116
+ }
117
+ const lockRecord = {
118
+ key,
119
+ status: 'IN_PROGRESS',
120
+ ownerToken,
121
+ attempts: 1,
122
+ createdAt: now,
123
+ updatedAt: now,
124
+ expiresAt: now + ttlMs,
125
+ };
126
+ // Use setIfAbsent if available for atomic initial acquisition
127
+ if (adapter.setIfAbsent) {
128
+ return await adapter.setIfAbsent(key, lockRecord, ttlMs);
129
+ }
130
+ await adapter.set(key, lockRecord, ttlMs);
131
+ return true;
132
+ return true;
133
+ }
134
+ catch (err) {
135
+ if (err.type !== undefined)
136
+ throw err;
137
+ throw createStorageError(key, adapter.name, err, clock);
138
+ }
139
+ }
140
+ async _safeRelease(key, ownerToken) {
141
+ const { adapter, clock } = this.opts;
142
+ try {
143
+ const existing = await adapter.get(key);
144
+ // Only release if we still own the lock
145
+ if (existing?.ownerToken === ownerToken) {
146
+ await adapter.delete(key);
147
+ }
148
+ }
149
+ catch {
150
+ // Best-effort release — TTL will handle cleanup
151
+ void clock; // reference to avoid unused var
152
+ }
153
+ }
154
+ // ── Hook Runner ───────────────────────────────────────────────────────────
155
+ async _runHook(hookName,
156
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
+ ...args) {
158
+ const hook = this.opts.hooks?.[hookName];
159
+ if (hook) {
160
+ try {
161
+ await hook(...args);
162
+ }
163
+ catch {
164
+ // Hooks must never break execution
165
+ }
166
+ }
167
+ }
168
+ }
169
+ // ─── Factory ──────────────────────────────────────────────────────────────────
170
+ /**
171
+ * Create a new distributed lock engine.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const lock = createLock({ adapter, ttlMs: 10_000, maxWaitMs: 5_000 });
176
+ * const result = await lock.acquire('order:123', () => processOrder());
177
+ * ```
178
+ */
179
+ export function createLock(options) {
180
+ return new LockEngineImpl(options);
181
+ }
182
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/lock/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * OpenTelemetry-compatible metrics for @periodic/vanadium
3
+ *
4
+ * Peer dependency (optional): "@opentelemetry/api" >= 1.0.0
5
+ *
6
+ * If OpenTelemetry is not configured, all metrics gracefully no-op.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { VanadiumInstrumentation } from '@periodic/vanadium';
11
+ * // or in your setup:
12
+ * import { createVanadiumMetrics } from '@periodic/vanadium/observability';
13
+ *
14
+ * const metrics = createVanadiumMetrics('my-service');
15
+ * metrics.recordExecution('payment:123', 'payments', 42);
16
+ * ```
17
+ */
18
+ /* eslint-enable @typescript-eslint/no-explicit-any */
19
+ // ─── No-op instruments ────────────────────────────────────────────────────────
20
+ const noopCounter = { add: () => undefined };
21
+ const noopHistogram = { record: () => undefined };
22
+ const noopUpDownCounter = { add: () => undefined };
23
+ /**
24
+ * Create metrics instruments. Pass an OTel Meter if you have one,
25
+ * otherwise all instruments are graceful no-ops.
26
+ */
27
+ export function createVanadiumMetrics(meter, scopeName = 'periodic.vanadium') {
28
+ const executionTotal = meter?.createCounter(`${scopeName}.execution_total`, {
29
+ description: 'Total idempotent executions',
30
+ }) ?? noopCounter;
31
+ const duplicateTotal = meter?.createCounter(`${scopeName}.duplicate_hit_total`, {
32
+ description: 'Total duplicate execution attempts deflected',
33
+ }) ?? noopCounter;
34
+ const takeoverTotal = meter?.createCounter(`${scopeName}.takeover_total`, {
35
+ description: 'Total expired in-progress record takeovers',
36
+ }) ?? noopCounter;
37
+ const storageErrorTotal = meter?.createCounter(`${scopeName}.storage_error_total`, {
38
+ description: 'Total storage adapter errors',
39
+ }) ?? noopCounter;
40
+ const payloadMismatchTotal = meter?.createCounter(`${scopeName}.payload_mismatch_total`, {
41
+ description: 'Total payload hash mismatch rejections',
42
+ }) ?? noopCounter;
43
+ const lockAcquiredTotal = meter?.createCounter(`${scopeName}.lock_acquired_total`, {
44
+ description: 'Total distributed locks acquired',
45
+ }) ?? noopCounter;
46
+ const lockFailedTotal = meter?.createCounter(`${scopeName}.lock_failed_total`, {
47
+ description: 'Total lock acquisition failures',
48
+ }) ?? noopCounter;
49
+ const executionDuration = meter?.createHistogram(`${scopeName}.execution_duration_ms`, {
50
+ description: 'Duration of idempotent function executions in milliseconds',
51
+ unit: 'ms',
52
+ }) ?? noopHistogram;
53
+ const lockDuration = meter?.createHistogram(`${scopeName}.lock_duration_ms`, {
54
+ description: 'Duration locks were held in milliseconds',
55
+ unit: 'ms',
56
+ }) ?? noopHistogram;
57
+ const inProgressGauge = meter?.createUpDownCounter(`${scopeName}.in_progress_count`, {
58
+ description: 'Current number of in-progress executions',
59
+ }) ?? noopUpDownCounter;
60
+ return {
61
+ recordExecution(key, adapterName, durationMs) {
62
+ executionTotal.add(1, { 'idempotency.key': key, 'adapter.name': adapterName });
63
+ executionDuration.record(durationMs, { 'adapter.name': adapterName });
64
+ },
65
+ recordDuplicate(key, adapterName) {
66
+ duplicateTotal.add(1, { 'idempotency.key': key, 'adapter.name': adapterName });
67
+ },
68
+ recordTakeover(key, adapterName) {
69
+ takeoverTotal.add(1, { 'idempotency.key': key, 'adapter.name': adapterName });
70
+ },
71
+ recordStorageError(key, adapterName) {
72
+ storageErrorTotal.add(1, { 'idempotency.key': key, 'adapter.name': adapterName });
73
+ },
74
+ recordPayloadMismatch(key, adapterName) {
75
+ payloadMismatchTotal.add(1, { 'idempotency.key': key, 'adapter.name': adapterName });
76
+ },
77
+ recordLockAcquired(key, adapterName, durationMs) {
78
+ lockAcquiredTotal.add(1, { 'lock.key': key, 'adapter.name': adapterName });
79
+ lockDuration.record(durationMs, { 'adapter.name': adapterName });
80
+ },
81
+ recordLockFailed(key, adapterName, reason) {
82
+ lockFailedTotal.add(1, { 'lock.key': key, 'adapter.name': adapterName, reason });
83
+ },
84
+ setInProgressCount(count, adapterName) {
85
+ inProgressGauge.add(count, { 'adapter.name': adapterName });
86
+ },
87
+ };
88
+ }
89
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/observability/metrics.ts"],"names":[],"mappings":"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"}