@venturekit/runtime 0.0.0-dev.20260312012510 → 0.0.0-dev.20260329102450
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/README.md +1 -1
- package/dist/config/env.d.ts +62 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +98 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/secrets.d.ts +67 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +132 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/handler/adapter.d.ts.map +1 -0
- package/dist/{adapter.js → handler/adapter.js} +5 -44
- package/dist/handler/adapter.js.map +1 -0
- package/dist/{context.d.ts → handler/context.d.ts} +44 -3
- package/dist/handler/context.d.ts.map +1 -0
- package/dist/{context.js → handler/context.js} +9 -9
- package/dist/handler/context.js.map +1 -0
- package/dist/handler/errors.d.ts.map +1 -0
- package/dist/{errors.js → handler/errors.js} +11 -25
- package/dist/handler/errors.js.map +1 -0
- package/dist/{handler.d.ts → handler/handler.d.ts} +36 -3
- package/dist/handler/handler.d.ts.map +1 -0
- package/dist/{handler.js → handler/handler.js} +59 -23
- package/dist/handler/handler.js.map +1 -0
- package/dist/handler/response.d.ts.map +1 -0
- package/dist/{response.js → handler/response.js} +9 -17
- package/dist/handler/response.js.map +1 -0
- package/dist/handler/task-handler.d.ts +192 -0
- package/dist/handler/task-handler.d.ts.map +1 -0
- package/dist/handler/task-handler.js +262 -0
- package/dist/handler/task-handler.js.map +1 -0
- package/dist/index.d.ts +33 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -48
- package/dist/index.js.map +1 -1
- package/dist/{logger.d.ts → logging/logger.d.ts} +1 -1
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/{logger.js → logging/logger.js} +8 -16
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/tracing.d.ts +51 -0
- package/dist/logging/tracing.d.ts.map +1 -0
- package/dist/logging/tracing.js +77 -0
- package/dist/logging/tracing.js.map +1 -0
- package/dist/middleware/middleware.d.ts +120 -0
- package/dist/middleware/middleware.d.ts.map +1 -0
- package/dist/middleware/middleware.js +270 -0
- package/dist/middleware/middleware.js.map +1 -0
- package/dist/middleware/rate-limit-store.d.ts +153 -0
- package/dist/middleware/rate-limit-store.d.ts.map +1 -0
- package/dist/middleware/rate-limit-store.js +182 -0
- package/dist/middleware/rate-limit-store.js.map +1 -0
- package/dist/openapi/openapi.d.ts +147 -0
- package/dist/openapi/openapi.d.ts.map +1 -0
- package/dist/openapi/openapi.js +235 -0
- package/dist/openapi/openapi.js.map +1 -0
- package/dist/openapi.d.ts +10 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +9 -0
- package/dist/openapi.js.map +1 -0
- package/dist/patterns/circuit-breaker.d.ts +78 -0
- package/dist/patterns/circuit-breaker.d.ts.map +1 -0
- package/dist/patterns/circuit-breaker.js +113 -0
- package/dist/patterns/circuit-breaker.js.map +1 -0
- package/dist/patterns/idempotency.d.ts +175 -0
- package/dist/patterns/idempotency.d.ts.map +1 -0
- package/dist/patterns/idempotency.js +305 -0
- package/dist/patterns/idempotency.js.map +1 -0
- package/dist/patterns/invoke.d.ts +279 -0
- package/dist/patterns/invoke.d.ts.map +1 -0
- package/dist/patterns/invoke.js +581 -0
- package/dist/patterns/invoke.js.map +1 -0
- package/dist/patterns/saga.d.ts +391 -0
- package/dist/patterns/saga.d.ts.map +1 -0
- package/dist/patterns/saga.js +556 -0
- package/dist/patterns/saga.js.map +1 -0
- package/dist/patterns.d.ts +17 -0
- package/dist/patterns.d.ts.map +1 -0
- package/dist/patterns.js +17 -0
- package/dist/patterns.js.map +1 -0
- package/dist/rate-limit.d.ts +16 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +15 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/secrets.d.ts +11 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +10 -0
- package/dist/secrets.js.map +1 -0
- package/dist/testing/test-utils.d.ts +99 -0
- package/dist/testing/test-utils.d.ts.map +1 -0
- package/dist/testing/test-utils.js +153 -0
- package/dist/testing/test-utils.js.map +1 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +9 -0
- package/dist/testing.js.map +1 -0
- package/dist/ws/ws.d.ts +139 -0
- package/dist/ws/ws.d.ts.map +1 -0
- package/dist/ws/ws.js +294 -0
- package/dist/ws/ws.js.map +1 -0
- package/dist/ws.d.ts +5 -133
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +4 -291
- package/dist/ws.js.map +1 -1
- package/package.json +47 -5
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/handler.d.ts.map +0 -1
- package/dist/handler.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/middleware.d.ts +0 -47
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -147
- package/dist/middleware.js.map +0 -1
- package/dist/response.d.ts.map +0 -1
- package/dist/response.js.map +0 -1
- /package/dist/{adapter.d.ts → handler/adapter.d.ts} +0 -0
- /package/dist/{errors.d.ts → handler/errors.d.ts} +0 -0
- /package/dist/{response.d.ts → handler/response.d.ts} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VentureKit Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Prevents cascading failures when invoking other Lambda functions.
|
|
5
|
+
* Tracks failures per target and "opens" the circuit after a threshold,
|
|
6
|
+
* returning a configurable default/fallback result instead of attempting
|
|
7
|
+
* the call.
|
|
8
|
+
*
|
|
9
|
+
* ## States
|
|
10
|
+
*
|
|
11
|
+
* - **Closed** — normal operation, requests pass through.
|
|
12
|
+
* - **Open** — too many recent failures, requests are short-circuited
|
|
13
|
+
* with the fallback result.
|
|
14
|
+
* - **Half-open** — after a cooldown period, one probe request is
|
|
15
|
+
* allowed through. If it succeeds the circuit closes; if it fails
|
|
16
|
+
* the circuit re-opens.
|
|
17
|
+
*
|
|
18
|
+
* ## Quick Start
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createCircuitBreaker } from '@venturekit/runtime';
|
|
22
|
+
*
|
|
23
|
+
* const breaker = createCircuitBreaker({
|
|
24
|
+
* failureThreshold: 5, // open after 5 failures
|
|
25
|
+
* resetTimeoutMs: 30_000, // try again after 30 s
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* const result = await breaker.call(
|
|
29
|
+
* 'billing-dev-fn-charge',
|
|
30
|
+
* () => invoke({ function: 'charge', project: 'billing' }, { payload }),
|
|
31
|
+
* { statusCode: 503, data: null, headers: {} }, // fallback
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
36
|
+
export interface CircuitBreakerOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Number of consecutive failures before the circuit opens.
|
|
39
|
+
* @default 5
|
|
40
|
+
*/
|
|
41
|
+
failureThreshold?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Time in ms to wait before switching from open → half-open.
|
|
44
|
+
* @default 30000 (30 seconds)
|
|
45
|
+
*/
|
|
46
|
+
resetTimeoutMs?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Optional callback fired when the circuit state changes.
|
|
49
|
+
*/
|
|
50
|
+
onStateChange?: (target: string, from: CircuitState, to: CircuitState) => void;
|
|
51
|
+
}
|
|
52
|
+
export interface CircuitBreaker {
|
|
53
|
+
/**
|
|
54
|
+
* Execute `fn` with circuit breaker protection.
|
|
55
|
+
*
|
|
56
|
+
* @param target - Logical name of the target (e.g. function name)
|
|
57
|
+
* @param fn - The async function to execute
|
|
58
|
+
* @param fallback - Value returned when the circuit is open
|
|
59
|
+
* @returns The result of `fn`, or `fallback` if the circuit is open.
|
|
60
|
+
*/
|
|
61
|
+
call<T>(target: string, fn: () => Promise<T>, fallback: T): Promise<T>;
|
|
62
|
+
/** Get the current state for a target. */
|
|
63
|
+
getState(target: string): CircuitState;
|
|
64
|
+
/** Manually reset a target to closed. */
|
|
65
|
+
reset(target: string): void;
|
|
66
|
+
/** Reset all targets. */
|
|
67
|
+
resetAll(): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a circuit breaker instance.
|
|
71
|
+
*
|
|
72
|
+
* The returned object is stateful (in-process) — each Lambda warm
|
|
73
|
+
* container gets its own breaker. This is intentional: a single
|
|
74
|
+
* container seeing repeated failures should stop hammering the target,
|
|
75
|
+
* while other containers may still be healthy.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createCircuitBreaker(options?: CircuitBreakerOptions): CircuitBreaker;
|
|
78
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/patterns/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE3D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,KAAK,IAAI,CAAC;CAChF;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;OAOG;IACH,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAEvE,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC;IAEvC,yCAAyC;IACzC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B,yBAAyB;IACzB,QAAQ,IAAI,IAAI,CAAC;CAClB;AAgBD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAiFxF"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VentureKit Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Prevents cascading failures when invoking other Lambda functions.
|
|
5
|
+
* Tracks failures per target and "opens" the circuit after a threshold,
|
|
6
|
+
* returning a configurable default/fallback result instead of attempting
|
|
7
|
+
* the call.
|
|
8
|
+
*
|
|
9
|
+
* ## States
|
|
10
|
+
*
|
|
11
|
+
* - **Closed** — normal operation, requests pass through.
|
|
12
|
+
* - **Open** — too many recent failures, requests are short-circuited
|
|
13
|
+
* with the fallback result.
|
|
14
|
+
* - **Half-open** — after a cooldown period, one probe request is
|
|
15
|
+
* allowed through. If it succeeds the circuit closes; if it fails
|
|
16
|
+
* the circuit re-opens.
|
|
17
|
+
*
|
|
18
|
+
* ## Quick Start
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createCircuitBreaker } from '@venturekit/runtime';
|
|
22
|
+
*
|
|
23
|
+
* const breaker = createCircuitBreaker({
|
|
24
|
+
* failureThreshold: 5, // open after 5 failures
|
|
25
|
+
* resetTimeoutMs: 30_000, // try again after 30 s
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* const result = await breaker.call(
|
|
29
|
+
* 'billing-dev-fn-charge',
|
|
30
|
+
* () => invoke({ function: 'charge', project: 'billing' }, { payload }),
|
|
31
|
+
* { statusCode: 503, data: null, headers: {} }, // fallback
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Implementation
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Create a circuit breaker instance.
|
|
40
|
+
*
|
|
41
|
+
* The returned object is stateful (in-process) — each Lambda warm
|
|
42
|
+
* container gets its own breaker. This is intentional: a single
|
|
43
|
+
* container seeing repeated failures should stop hammering the target,
|
|
44
|
+
* while other containers may still be healthy.
|
|
45
|
+
*/
|
|
46
|
+
export function createCircuitBreaker(options = {}) {
|
|
47
|
+
const { failureThreshold = 5, resetTimeoutMs = 30_000, onStateChange, } = options;
|
|
48
|
+
const targets = new Map();
|
|
49
|
+
function getOrCreate(target) {
|
|
50
|
+
let ts = targets.get(target);
|
|
51
|
+
if (!ts) {
|
|
52
|
+
ts = { state: 'closed', failures: 0, lastFailureAt: 0 };
|
|
53
|
+
targets.set(target, ts);
|
|
54
|
+
}
|
|
55
|
+
return ts;
|
|
56
|
+
}
|
|
57
|
+
function transition(target, ts, to) {
|
|
58
|
+
if (ts.state === to)
|
|
59
|
+
return;
|
|
60
|
+
const from = ts.state;
|
|
61
|
+
ts.state = to;
|
|
62
|
+
onStateChange?.(target, from, to);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
async call(target, fn, fallback) {
|
|
66
|
+
const ts = getOrCreate(target);
|
|
67
|
+
// --- Open state ---
|
|
68
|
+
if (ts.state === 'open') {
|
|
69
|
+
// Check if cooldown has elapsed → half-open
|
|
70
|
+
if (Date.now() - ts.lastFailureAt >= resetTimeoutMs) {
|
|
71
|
+
transition(target, ts, 'half-open');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Still open — return fallback
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// --- Closed or Half-open: attempt the call ---
|
|
79
|
+
try {
|
|
80
|
+
const result = await fn();
|
|
81
|
+
// Success: reset to closed
|
|
82
|
+
ts.failures = 0;
|
|
83
|
+
transition(target, ts, 'closed');
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
ts.failures++;
|
|
88
|
+
ts.lastFailureAt = Date.now();
|
|
89
|
+
if (ts.state === 'half-open') {
|
|
90
|
+
// Probe failed — re-open immediately
|
|
91
|
+
transition(target, ts, 'open');
|
|
92
|
+
return fallback;
|
|
93
|
+
}
|
|
94
|
+
if (ts.failures >= failureThreshold) {
|
|
95
|
+
transition(target, ts, 'open');
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
// Below threshold — propagate error normally
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
getState(target) {
|
|
103
|
+
return getOrCreate(target).state;
|
|
104
|
+
},
|
|
105
|
+
reset(target) {
|
|
106
|
+
targets.delete(target);
|
|
107
|
+
},
|
|
108
|
+
resetAll() {
|
|
109
|
+
targets.clear();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/patterns/circuit-breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VentureKit Idempotency Support
|
|
3
|
+
*
|
|
4
|
+
* Middleware for ensuring handler idempotency.
|
|
5
|
+
* Stores request results keyed by an idempotency key, returning
|
|
6
|
+
* cached responses for duplicate requests within a TTL window.
|
|
7
|
+
*
|
|
8
|
+
* Storage backend is pluggable (DynamoDB, Postgres, in-memory, etc.).
|
|
9
|
+
*/
|
|
10
|
+
import type { RequestContext } from '../handler/context.js';
|
|
11
|
+
import type { Middleware } from '../middleware/middleware.js';
|
|
12
|
+
/**
|
|
13
|
+
* Stored idempotency record
|
|
14
|
+
*/
|
|
15
|
+
export interface IdempotencyRecord {
|
|
16
|
+
/** The idempotency key */
|
|
17
|
+
key: string;
|
|
18
|
+
/** The cached response (serialized) */
|
|
19
|
+
response: string;
|
|
20
|
+
/** Status of the record */
|
|
21
|
+
status: 'pending' | 'completed';
|
|
22
|
+
/** Expiry timestamp (ms since epoch) */
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Idempotency store interface.
|
|
27
|
+
* Implement this to plug in any storage backend.
|
|
28
|
+
*/
|
|
29
|
+
export interface IdempotencyStore {
|
|
30
|
+
/**
|
|
31
|
+
* Get a stored record by key.
|
|
32
|
+
* Returns null if not found or expired.
|
|
33
|
+
*/
|
|
34
|
+
get(key: string): Promise<IdempotencyRecord | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Save a record. If a record with the same key already exists
|
|
37
|
+
* and is 'pending', throw a ConflictError (concurrent request).
|
|
38
|
+
*/
|
|
39
|
+
save(record: IdempotencyRecord): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Update an existing record (e.g. from pending → completed).
|
|
42
|
+
*/
|
|
43
|
+
update(key: string, updates: Partial<IdempotencyRecord>): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Delete a record (e.g. on failure, to allow retry).
|
|
46
|
+
*/
|
|
47
|
+
delete(key: string): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Key extraction strategy
|
|
51
|
+
*/
|
|
52
|
+
export type IdempotencyKeyExtractor = (ctx: RequestContext) => string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Extract idempotency key from a header
|
|
55
|
+
*/
|
|
56
|
+
export declare function headerKeyExtractor(headerName: string): IdempotencyKeyExtractor;
|
|
57
|
+
/**
|
|
58
|
+
* Extract idempotency key from a body field
|
|
59
|
+
*/
|
|
60
|
+
export declare function bodyFieldKeyExtractor(fieldName: string): IdempotencyKeyExtractor;
|
|
61
|
+
/**
|
|
62
|
+
* Idempotency middleware options
|
|
63
|
+
*/
|
|
64
|
+
export interface IdempotencyOptions {
|
|
65
|
+
/** Storage backend for idempotency records */
|
|
66
|
+
store: IdempotencyStore;
|
|
67
|
+
/**
|
|
68
|
+
* Extract the idempotency key from the request.
|
|
69
|
+
* Default: reads from 'Idempotency-Key' header.
|
|
70
|
+
*/
|
|
71
|
+
keyExtractor?: IdempotencyKeyExtractor;
|
|
72
|
+
/** TTL in seconds for idempotency records (default: 86400 = 24h) */
|
|
73
|
+
ttlSeconds?: number;
|
|
74
|
+
/**
|
|
75
|
+
* If true, requests without an idempotency key are rejected with 400.
|
|
76
|
+
* If false (default), requests without a key pass through without idempotency.
|
|
77
|
+
*/
|
|
78
|
+
required?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Idempotency middleware
|
|
82
|
+
*
|
|
83
|
+
* Ensures that duplicate requests with the same idempotency key
|
|
84
|
+
* return the same response without re-executing the handler.
|
|
85
|
+
*
|
|
86
|
+
* Flow:
|
|
87
|
+
* 1. Extract idempotency key from request
|
|
88
|
+
* 2. Check store for existing record
|
|
89
|
+
* - If completed: return cached response
|
|
90
|
+
* - If pending: return 409 Conflict (concurrent request in flight)
|
|
91
|
+
* 3. Save 'pending' record
|
|
92
|
+
* 4. Execute handler
|
|
93
|
+
* 5. Update record to 'completed' with response
|
|
94
|
+
* 6. On error: delete record so the request can be retried
|
|
95
|
+
*/
|
|
96
|
+
export declare function idempotencyMiddleware(options: IdempotencyOptions): Middleware<RequestContext>;
|
|
97
|
+
/**
|
|
98
|
+
* In-memory idempotency store (for development/testing).
|
|
99
|
+
*
|
|
100
|
+
* **WARNING:** State is per-process. Does NOT work correctly with
|
|
101
|
+
* multiple Lambda instances. Use only for local dev/testing.
|
|
102
|
+
* For production, use `createDynamoDBIdempotencyStore()`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function createMemoryIdempotencyStore(): IdempotencyStore;
|
|
105
|
+
/**
|
|
106
|
+
* DynamoDB idempotency store options.
|
|
107
|
+
*/
|
|
108
|
+
export interface DynamoDBIdempotencyStoreOptions {
|
|
109
|
+
/** DynamoDB table name (default: 'venturekit-idempotency') */
|
|
110
|
+
tableName?: string;
|
|
111
|
+
/** AWS region (default: process.env.AWS_REGION) */
|
|
112
|
+
region?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create a DynamoDB-backed idempotency store.
|
|
116
|
+
*
|
|
117
|
+
* Uses conditional writes for conflict detection and DynamoDB TTL
|
|
118
|
+
* for automatic cleanup of expired records.
|
|
119
|
+
*
|
|
120
|
+
* ### DynamoDB Table Schema
|
|
121
|
+
*
|
|
122
|
+
* The table must have:
|
|
123
|
+
* - **Partition key:** `pk` (String) — the idempotency key
|
|
124
|
+
* - **TTL attribute:** `ttl` (Number) — epoch seconds for auto-cleanup
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* import { idempotencyMiddleware, createDynamoDBIdempotencyStore } from '@venturekit/runtime';
|
|
129
|
+
*
|
|
130
|
+
* const store = await createDynamoDBIdempotencyStore();
|
|
131
|
+
* const idempotency = idempotencyMiddleware({ store });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export declare function createDynamoDBIdempotencyStore(options?: DynamoDBIdempotencyStoreOptions): Promise<IdempotencyStore>;
|
|
135
|
+
/**
|
|
136
|
+
* Error thrown when an idempotency conflict is detected
|
|
137
|
+
* (a request with the same key is already in progress).
|
|
138
|
+
*/
|
|
139
|
+
export declare class IdempotencyConflictError extends Error {
|
|
140
|
+
readonly code = "IDEMPOTENCY_CONFLICT";
|
|
141
|
+
constructor(key: string);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Error thrown when a required idempotency key is missing.
|
|
145
|
+
*/
|
|
146
|
+
export declare class MissingIdempotencyKeyError extends Error {
|
|
147
|
+
readonly code = "MISSING_IDEMPOTENCY_KEY";
|
|
148
|
+
constructor();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Options for the default idempotency store.
|
|
152
|
+
*/
|
|
153
|
+
export interface DefaultIdempotencyStoreOptions {
|
|
154
|
+
/** DynamoDB table name */
|
|
155
|
+
tableName?: string;
|
|
156
|
+
/** Force in-memory store even in production */
|
|
157
|
+
forceMemory?: boolean;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Create an idempotency store with sensible defaults.
|
|
161
|
+
*
|
|
162
|
+
* - In production (Lambda): uses DynamoDB for distributed idempotency
|
|
163
|
+
* - In local dev: uses in-memory store
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* import { idempotencyMiddleware, createDefaultIdempotencyStore } from '@venturekit/runtime';
|
|
168
|
+
*
|
|
169
|
+
* // Zero-config — auto-selects DynamoDB (Lambda) or memory (local)
|
|
170
|
+
* const store = await createDefaultIdempotencyStore();
|
|
171
|
+
* const idempotency = idempotencyMiddleware({ store });
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export declare function createDefaultIdempotencyStore(options?: DefaultIdempotencyStoreOptions): Promise<IdempotencyStore>;
|
|
175
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/patterns/idempotency.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAEpD;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExE;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,CAAC;AAE7E;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,uBAAuB,CAK9E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,uBAAuB,CAShF;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,8CAA8C;IAC9C,KAAK,EAAE,gBAAgB,CAAC;IAExB;;;OAGG;IACH,YAAY,CAAC,EAAE,uBAAuB,CAAC;IAEvC,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU,CAAC,cAAc,CAAC,CA0E7F;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,IAAI,gBAAgB,CAiC/D;AAMD;;GAEG;AACH,MAAM,WAAW,+BAA+B;IAC9C,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,gBAAgB,CAAC,CAwG3B;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,SAAgB,IAAI,0BAA0B;gBAClC,GAAG,EAAE,MAAM;CAIxB;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,IAAI,6BAA6B;;CAKlD;AAMD;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC7C,0BAA0B;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,gBAAgB,CAAC,CAU3B"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VentureKit Idempotency Support
|
|
3
|
+
*
|
|
4
|
+
* Middleware for ensuring handler idempotency.
|
|
5
|
+
* Stores request results keyed by an idempotency key, returning
|
|
6
|
+
* cached responses for duplicate requests within a TTL window.
|
|
7
|
+
*
|
|
8
|
+
* Storage backend is pluggable (DynamoDB, Postgres, in-memory, etc.).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Extract idempotency key from a header
|
|
12
|
+
*/
|
|
13
|
+
export function headerKeyExtractor(headerName) {
|
|
14
|
+
return (ctx) => {
|
|
15
|
+
const key = ctx.rawEvent.headers?.[headerName.toLowerCase()];
|
|
16
|
+
return key ?? null;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract idempotency key from a body field
|
|
21
|
+
*/
|
|
22
|
+
export function bodyFieldKeyExtractor(fieldName) {
|
|
23
|
+
return (ctx) => {
|
|
24
|
+
try {
|
|
25
|
+
const body = ctx.rawEvent.body ? JSON.parse(ctx.rawEvent.body) : null;
|
|
26
|
+
return body?.[fieldName] ?? null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Idempotency middleware
|
|
35
|
+
*
|
|
36
|
+
* Ensures that duplicate requests with the same idempotency key
|
|
37
|
+
* return the same response without re-executing the handler.
|
|
38
|
+
*
|
|
39
|
+
* Flow:
|
|
40
|
+
* 1. Extract idempotency key from request
|
|
41
|
+
* 2. Check store for existing record
|
|
42
|
+
* - If completed: return cached response
|
|
43
|
+
* - If pending: return 409 Conflict (concurrent request in flight)
|
|
44
|
+
* 3. Save 'pending' record
|
|
45
|
+
* 4. Execute handler
|
|
46
|
+
* 5. Update record to 'completed' with response
|
|
47
|
+
* 6. On error: delete record so the request can be retried
|
|
48
|
+
*/
|
|
49
|
+
export function idempotencyMiddleware(options) {
|
|
50
|
+
const { store, keyExtractor = headerKeyExtractor('idempotency-key'), ttlSeconds = 86400, required = false, } = options;
|
|
51
|
+
return {
|
|
52
|
+
name: 'idempotency',
|
|
53
|
+
fn: async (ctx, next) => {
|
|
54
|
+
const key = keyExtractor(ctx);
|
|
55
|
+
// No idempotency key
|
|
56
|
+
if (!key) {
|
|
57
|
+
if (required) {
|
|
58
|
+
return {
|
|
59
|
+
statusCode: 400,
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
error: { code: 'MISSING_IDEMPOTENCY_KEY', message: 'Idempotency-Key header is required' },
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Pass through without idempotency
|
|
66
|
+
return next();
|
|
67
|
+
}
|
|
68
|
+
// Check for existing record
|
|
69
|
+
const existing = await store.get(key);
|
|
70
|
+
if (existing) {
|
|
71
|
+
if (existing.status === 'completed') {
|
|
72
|
+
// Return cached response
|
|
73
|
+
return JSON.parse(existing.response);
|
|
74
|
+
}
|
|
75
|
+
if (existing.status === 'pending') {
|
|
76
|
+
// Concurrent request — reject
|
|
77
|
+
return {
|
|
78
|
+
statusCode: 409,
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
error: { code: 'IDEMPOTENCY_CONFLICT', message: 'A request with this idempotency key is already in progress' },
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Save pending record
|
|
86
|
+
const record = {
|
|
87
|
+
key,
|
|
88
|
+
response: '',
|
|
89
|
+
status: 'pending',
|
|
90
|
+
expiresAt: Date.now() + (ttlSeconds * 1000),
|
|
91
|
+
};
|
|
92
|
+
await store.save(record);
|
|
93
|
+
try {
|
|
94
|
+
// Execute handler
|
|
95
|
+
const response = await next();
|
|
96
|
+
// Store completed response
|
|
97
|
+
await store.update(key, {
|
|
98
|
+
status: 'completed',
|
|
99
|
+
response: JSON.stringify(response),
|
|
100
|
+
});
|
|
101
|
+
return response;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Delete record so request can be retried
|
|
105
|
+
await store.delete(key);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* In-memory idempotency store (for development/testing).
|
|
113
|
+
*
|
|
114
|
+
* **WARNING:** State is per-process. Does NOT work correctly with
|
|
115
|
+
* multiple Lambda instances. Use only for local dev/testing.
|
|
116
|
+
* For production, use `createDynamoDBIdempotencyStore()`.
|
|
117
|
+
*/
|
|
118
|
+
export function createMemoryIdempotencyStore() {
|
|
119
|
+
const records = new Map();
|
|
120
|
+
return {
|
|
121
|
+
async get(key) {
|
|
122
|
+
const record = records.get(key);
|
|
123
|
+
if (!record)
|
|
124
|
+
return null;
|
|
125
|
+
if (record.expiresAt < Date.now()) {
|
|
126
|
+
records.delete(key);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return record;
|
|
130
|
+
},
|
|
131
|
+
async save(record) {
|
|
132
|
+
const existing = records.get(record.key);
|
|
133
|
+
if (existing && existing.status === 'pending' && existing.expiresAt >= Date.now()) {
|
|
134
|
+
throw new Error('Concurrent idempotency conflict');
|
|
135
|
+
}
|
|
136
|
+
records.set(record.key, record);
|
|
137
|
+
},
|
|
138
|
+
async update(key, updates) {
|
|
139
|
+
const existing = records.get(key);
|
|
140
|
+
if (existing) {
|
|
141
|
+
records.set(key, { ...existing, ...updates });
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async delete(key) {
|
|
145
|
+
records.delete(key);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create a DynamoDB-backed idempotency store.
|
|
151
|
+
*
|
|
152
|
+
* Uses conditional writes for conflict detection and DynamoDB TTL
|
|
153
|
+
* for automatic cleanup of expired records.
|
|
154
|
+
*
|
|
155
|
+
* ### DynamoDB Table Schema
|
|
156
|
+
*
|
|
157
|
+
* The table must have:
|
|
158
|
+
* - **Partition key:** `pk` (String) — the idempotency key
|
|
159
|
+
* - **TTL attribute:** `ttl` (Number) — epoch seconds for auto-cleanup
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* import { idempotencyMiddleware, createDynamoDBIdempotencyStore } from '@venturekit/runtime';
|
|
164
|
+
*
|
|
165
|
+
* const store = await createDynamoDBIdempotencyStore();
|
|
166
|
+
* const idempotency = idempotencyMiddleware({ store });
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export async function createDynamoDBIdempotencyStore(options = {}) {
|
|
170
|
+
const tableName = options.tableName
|
|
171
|
+
|| process.env.VENTURE_IDEMPOTENCY_TABLE
|
|
172
|
+
|| 'venturekit-idempotency';
|
|
173
|
+
const region = options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
|
|
174
|
+
const { DynamoDBClient, PutItemCommand, GetItemCommand, UpdateItemCommand, DeleteItemCommand, } = await import('@aws-sdk/client-dynamodb');
|
|
175
|
+
const client = new DynamoDBClient({ region });
|
|
176
|
+
return {
|
|
177
|
+
async get(key) {
|
|
178
|
+
const result = await client.send(new GetItemCommand({
|
|
179
|
+
TableName: tableName,
|
|
180
|
+
Key: { pk: { S: key } },
|
|
181
|
+
}));
|
|
182
|
+
if (!result.Item)
|
|
183
|
+
return null;
|
|
184
|
+
const expiresAt = parseInt(result.Item.expiresAt?.N ?? '0', 10);
|
|
185
|
+
if (expiresAt < Date.now())
|
|
186
|
+
return null;
|
|
187
|
+
return {
|
|
188
|
+
key: result.Item.pk.S,
|
|
189
|
+
response: result.Item.response?.S ?? '',
|
|
190
|
+
status: result.Item.status?.S,
|
|
191
|
+
expiresAt,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
async save(record) {
|
|
195
|
+
const ttl = Math.ceil(record.expiresAt / 1000);
|
|
196
|
+
try {
|
|
197
|
+
await client.send(new PutItemCommand({
|
|
198
|
+
TableName: tableName,
|
|
199
|
+
Item: {
|
|
200
|
+
pk: { S: record.key },
|
|
201
|
+
response: { S: record.response },
|
|
202
|
+
status: { S: record.status },
|
|
203
|
+
expiresAt: { N: String(record.expiresAt) },
|
|
204
|
+
ttl: { N: String(ttl) },
|
|
205
|
+
},
|
|
206
|
+
// Conditional: only allow write if no pending record exists
|
|
207
|
+
ConditionExpression: 'attribute_not_exists(pk) OR #s <> :pending OR #exp < :now',
|
|
208
|
+
ExpressionAttributeNames: { '#s': 'status', '#exp': 'expiresAt' },
|
|
209
|
+
ExpressionAttributeValues: {
|
|
210
|
+
':pending': { S: 'pending' },
|
|
211
|
+
':now': { N: String(Date.now()) },
|
|
212
|
+
},
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (err.name === 'ConditionalCheckFailedException') {
|
|
217
|
+
throw new Error('Concurrent idempotency conflict');
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
async update(key, updates) {
|
|
223
|
+
const expressionParts = [];
|
|
224
|
+
const names = {};
|
|
225
|
+
const values = {};
|
|
226
|
+
let counter = 0;
|
|
227
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
228
|
+
if (v === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
counter++;
|
|
231
|
+
const attrName = `#a${counter}`;
|
|
232
|
+
const attrValue = `:v${counter}`;
|
|
233
|
+
names[attrName] = k;
|
|
234
|
+
if (typeof v === 'number') {
|
|
235
|
+
values[attrValue] = { N: String(v) };
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
values[attrValue] = { S: String(v) };
|
|
239
|
+
}
|
|
240
|
+
expressionParts.push(`${attrName} = ${attrValue}`);
|
|
241
|
+
}
|
|
242
|
+
if (expressionParts.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
await client.send(new UpdateItemCommand({
|
|
245
|
+
TableName: tableName,
|
|
246
|
+
Key: { pk: { S: key } },
|
|
247
|
+
UpdateExpression: `SET ${expressionParts.join(', ')}`,
|
|
248
|
+
ExpressionAttributeNames: names,
|
|
249
|
+
ExpressionAttributeValues: values,
|
|
250
|
+
}));
|
|
251
|
+
},
|
|
252
|
+
async delete(key) {
|
|
253
|
+
await client.send(new DeleteItemCommand({
|
|
254
|
+
TableName: tableName,
|
|
255
|
+
Key: { pk: { S: key } },
|
|
256
|
+
}));
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Error thrown when an idempotency conflict is detected
|
|
262
|
+
* (a request with the same key is already in progress).
|
|
263
|
+
*/
|
|
264
|
+
export class IdempotencyConflictError extends Error {
|
|
265
|
+
code = 'IDEMPOTENCY_CONFLICT';
|
|
266
|
+
constructor(key) {
|
|
267
|
+
super(`A request with idempotency key '${key}' is already in progress`);
|
|
268
|
+
this.name = 'IdempotencyConflictError';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Error thrown when a required idempotency key is missing.
|
|
273
|
+
*/
|
|
274
|
+
export class MissingIdempotencyKeyError extends Error {
|
|
275
|
+
code = 'MISSING_IDEMPOTENCY_KEY';
|
|
276
|
+
constructor() {
|
|
277
|
+
super('Idempotency key is required but was not found in the request');
|
|
278
|
+
this.name = 'MissingIdempotencyKeyError';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Create an idempotency store with sensible defaults.
|
|
283
|
+
*
|
|
284
|
+
* - In production (Lambda): uses DynamoDB for distributed idempotency
|
|
285
|
+
* - In local dev: uses in-memory store
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* import { idempotencyMiddleware, createDefaultIdempotencyStore } from '@venturekit/runtime';
|
|
290
|
+
*
|
|
291
|
+
* // Zero-config — auto-selects DynamoDB (Lambda) or memory (local)
|
|
292
|
+
* const store = await createDefaultIdempotencyStore();
|
|
293
|
+
* const idempotency = idempotencyMiddleware({ store });
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export async function createDefaultIdempotencyStore(options = {}) {
|
|
297
|
+
const isLocal = !process.env.AWS_LAMBDA_FUNCTION_NAME && !process.env.AWS_EXECUTION_ENV;
|
|
298
|
+
if (isLocal || options.forceMemory) {
|
|
299
|
+
return createMemoryIdempotencyStore();
|
|
300
|
+
}
|
|
301
|
+
return createDynamoDBIdempotencyStore({
|
|
302
|
+
tableName: options.tableName,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=idempotency.js.map
|