lambda-deadline-middleware 1.1.0 → 2.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/README.md +168 -76
- package/dist/context-store.d.ts +25 -2
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +45 -6
- package/dist/context-store.js.map +1 -1
- package/dist/error.d.ts +6 -7
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +1 -3
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +21 -58
- package/dist/middleware.js.map +1 -1
- package/dist/types.d.ts +0 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -7
- package/dist/types.js.map +1 -1
- package/package.json +19 -19
- package/src/context-store.ts +75 -9
- package/src/error.ts +6 -8
- package/src/index.ts +2 -4
- package/src/middleware.ts +34 -84
- package/src/types.ts +0 -16
- package/dist/handler-wrapper.d.ts +0 -14
- package/dist/handler-wrapper.d.ts.map +0 -1
- package/dist/handler-wrapper.js +0 -6
- package/dist/handler-wrapper.js.map +0 -1
- package/src/handler-wrapper.ts +0 -18
package/README.md
CHANGED
|
@@ -3,21 +3,65 @@
|
|
|
3
3
|
|
|
4
4
|
# lambda-deadline-middleware
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
[](https://www.npmjs.com/package/lambda-deadline-middleware)
|
|
7
|
+
[](https://github.com/mikkopiu/lambda-deadline-middleware/actions/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Zero-dependency AWS SDK v3 middleware that propagates Lambda deadlines to outgoing SDK calls via `AbortSignal`.
|
|
11
|
+
|
|
12
|
+
When an AWS SDK call hangs inside a Lambda, the runtime kills the process without throwing an error or giving your code
|
|
13
|
+
a chance to react. This library attaches an `AbortSignal` to every outgoing SDK request so your calls fail fast instead
|
|
14
|
+
of getting killed silently. The signal comes either from an external source you provide or is computed once from the
|
|
15
|
+
Lambda's remaining execution time.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { withLambdaDeadline, deadlineMiddleware } from "lambda-deadline-middleware";
|
|
19
|
+
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
|
|
20
|
+
|
|
21
|
+
const dynamodb = new DynamoDBClient({});
|
|
22
|
+
dynamodb.middlewareStack.use(deadlineMiddleware());
|
|
23
|
+
|
|
24
|
+
export const handler = withLambdaDeadline(async (event, context) => {
|
|
25
|
+
return dynamodb.send(new GetItemCommand({ TableName: "my-table", Key: { id: { S: event.id } } }));
|
|
26
|
+
});
|
|
27
|
+
```
|
|
12
28
|
|
|
13
29
|
## Features
|
|
14
30
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
31
|
+
- Zero runtime dependencies
|
|
32
|
+
- Bring your own `AbortSignal` (from Middy, a manual `AbortController`, or any framework), or let the library compute
|
|
33
|
+
one from `getRemainingTimeInMillis()` at invocation start
|
|
34
|
+
- One signal per invocation: covers all SDK calls and retries within the handler
|
|
35
|
+
- Works alongside per-request `abortSignal` options you pass to `.send()`
|
|
36
|
+
- Complete no-op outside Lambda (safe in local dev and tests)
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
flowchart LR
|
|
42
|
+
subgraph Handler startup
|
|
43
|
+
A[Lambda Runtime] --> B[withLambdaDeadline]
|
|
44
|
+
B -->|compute signal once| C[AbortSignal.timeout]
|
|
45
|
+
B --> D[Your Handler]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
subgraph Per outgoing SDK request
|
|
49
|
+
E[SDK .send] --> F[Deadline Middleware]
|
|
50
|
+
F -->|signal exists?| G[Compose with request signal]
|
|
51
|
+
F -->|no signal| H[Pass through]
|
|
52
|
+
G --> I[HTTP Request]
|
|
53
|
+
H --> I
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
D --> E
|
|
57
|
+
|
|
58
|
+
style F fill:#f9a825,stroke:#f57f17
|
|
59
|
+
style B fill:#66bb6a,stroke:#2e7d32
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`withLambdaDeadline` computes `AbortSignal.timeout(remaining - flushBuffer)` once at invocation start and stores it in
|
|
63
|
+
`AsyncLocalStorage`. The middleware reads it on each outgoing request and composes it with any per-request signal. If
|
|
64
|
+
`setDeadlineSignal()` was called, that signal is used instead.
|
|
21
65
|
|
|
22
66
|
## Requirements
|
|
23
67
|
|
|
@@ -27,18 +71,17 @@ deadlines from the Lambda's remaining execution time and aborting requests befor
|
|
|
27
71
|
## Installation
|
|
28
72
|
|
|
29
73
|
```bash
|
|
30
|
-
|
|
74
|
+
npm install lambda-deadline-middleware
|
|
31
75
|
```
|
|
32
76
|
|
|
33
77
|
## Usage
|
|
34
78
|
|
|
35
|
-
Setup
|
|
79
|
+
Setup has two parts:
|
|
36
80
|
|
|
37
|
-
1. **Wrap your handler** with `withLambdaDeadline`.
|
|
38
|
-
`
|
|
39
|
-
no access to the Lambda context on its own.
|
|
81
|
+
1. **Wrap your handler** with `withLambdaDeadline`. Computes the deadline signal once and stores it in
|
|
82
|
+
`AsyncLocalStorage`.
|
|
40
83
|
|
|
41
|
-
2. **Register the middleware** on each SDK client
|
|
84
|
+
2. **Register the middleware** on each SDK client.
|
|
42
85
|
|
|
43
86
|
```typescript
|
|
44
87
|
import { withLambdaDeadline, deadlineMiddleware } from "lambda-deadline-middleware";
|
|
@@ -57,66 +100,114 @@ export const handler = withLambdaDeadline(async (event, context) => {
|
|
|
57
100
|
});
|
|
58
101
|
```
|
|
59
102
|
|
|
60
|
-
Every SDK call through `dynamodb`
|
|
61
|
-
|
|
103
|
+
Every SDK call through `dynamodb` gets an `AbortSignal` that fires at `remainingTime - flushBuffer` ms from handler
|
|
104
|
+
start.
|
|
62
105
|
|
|
63
|
-
##
|
|
106
|
+
## External Signal (Middy, manual AbortController, etc.)
|
|
64
107
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
subgraph Lambda Invocation
|
|
68
|
-
direction LR
|
|
69
|
-
A[Lambda Runtime] --> B[withLambdaDeadline]
|
|
70
|
-
B --> C[Your Handler]
|
|
71
|
-
C --> D[SDK .send]
|
|
72
|
-
end
|
|
108
|
+
If you already have an `AbortSignal` (from Middy's `timeoutEarlyInMillis`, a manual controller, or anything else), pass
|
|
109
|
+
it in directly:
|
|
73
110
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
```typescript
|
|
112
|
+
import {
|
|
113
|
+
withLambdaDeadline,
|
|
114
|
+
deadlineMiddleware,
|
|
115
|
+
setDeadlineSignal,
|
|
116
|
+
} from "lambda-deadline-middleware";
|
|
117
|
+
import middy from "@middy/core";
|
|
118
|
+
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
const dynamodb = new DynamoDBClient({});
|
|
121
|
+
dynamodb.middlewareStack.use(deadlineMiddleware());
|
|
122
|
+
|
|
123
|
+
// Middy passes { signal } as the third argument when timeoutEarlyInMillis is set
|
|
124
|
+
const baseHandler = async (event, context, { signal }) => {
|
|
125
|
+
setDeadlineSignal(signal);
|
|
126
|
+
const result = await dynamodb.send(
|
|
127
|
+
new GetItemCommand({
|
|
128
|
+
/* ... */
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
return { statusCode: 200, body: JSON.stringify(result) };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const handler = withLambdaDeadline(
|
|
135
|
+
middy({ timeoutEarlyInMillis: 1000 }).handler(baseHandler),
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
When an external signal is set via `setDeadlineSignal()`:
|
|
140
|
+
|
|
141
|
+
- It **replaces** the auto-computed signal for the current invocation
|
|
142
|
+
- The signal is composed with any per-request `AbortSignal` via `AbortSignal.any()`
|
|
143
|
+
- You control when and why the abort fires
|
|
144
|
+
|
|
145
|
+
When no external signal is set, the auto-computed signal (from `withLambdaDeadline`) is used.
|
|
146
|
+
|
|
147
|
+
**Important:** `withLambdaDeadline` must wrap the outside. It creates the `AsyncLocalStorage` scope that
|
|
148
|
+
`setDeadlineSignal` writes into.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// ✅ Correct: withLambdaDeadline on the outside
|
|
152
|
+
export const handler = withLambdaDeadline(
|
|
153
|
+
middy({ timeoutEarlyInMillis: 1000 }).handler(baseHandler),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// ❌ Wrong: store doesn't exist yet when baseHandler runs
|
|
157
|
+
export const handler = middy({ timeoutEarlyInMillis: 1000 }).handler(
|
|
158
|
+
withLambdaDeadline(baseHandler),
|
|
159
|
+
);
|
|
83
160
|
```
|
|
84
161
|
|
|
85
|
-
`
|
|
86
|
-
|
|
162
|
+
`setDeadlineSignal` only affects AWS SDK calls (via the Smithy middleware stack). For other async work, pass the signal
|
|
163
|
+
yourself:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const baseHandler = async (event, context, { signal }) => {
|
|
167
|
+
setDeadlineSignal(signal);
|
|
168
|
+
|
|
169
|
+
// SDK calls: aborted via middleware
|
|
170
|
+
const item = await dynamodb.send(
|
|
171
|
+
new GetItemCommand({
|
|
172
|
+
/* ... */
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Non-SDK calls: pass signal manually
|
|
177
|
+
const response = await fetch("https://api.example.com/data", { signal });
|
|
178
|
+
};
|
|
179
|
+
```
|
|
87
180
|
|
|
88
181
|
## Configuration
|
|
89
182
|
|
|
90
183
|
### Flush Buffer
|
|
91
184
|
|
|
92
|
-
|
|
185
|
+
Subtracted from remaining Lambda time to leave room for cleanup:
|
|
93
186
|
|
|
94
187
|
```typescript
|
|
95
|
-
// Default: 1000ms
|
|
96
|
-
|
|
188
|
+
// Default: 1000ms flush buffer
|
|
189
|
+
export const handler = withLambdaDeadline(myHandler);
|
|
97
190
|
|
|
98
|
-
// Custom: 500ms
|
|
99
|
-
|
|
191
|
+
// Custom: 500ms flush buffer
|
|
192
|
+
export const handler = withLambdaDeadline(myHandler, { flushBufferMs: 500 });
|
|
100
193
|
```
|
|
101
194
|
|
|
195
|
+
Only applies to automatic signal computation. With `setDeadlineSignal()`, you control timing yourself.
|
|
196
|
+
|
|
102
197
|
## Error Handling
|
|
103
198
|
|
|
104
|
-
When remaining time
|
|
105
|
-
without dispatching an HTTP request.
|
|
199
|
+
When remaining time ≤ flush buffer, `withLambdaDeadline` throws `DeadlineExceededError` without calling the handler:
|
|
106
200
|
|
|
107
201
|
```typescript
|
|
108
202
|
import { isDeadlineExceeded } from "lambda-deadline-middleware";
|
|
109
203
|
|
|
110
204
|
try {
|
|
111
|
-
await
|
|
112
|
-
new GetItemCommand({
|
|
113
|
-
/* ... */
|
|
114
|
-
}),
|
|
115
|
-
);
|
|
205
|
+
await handler(event, context);
|
|
116
206
|
} catch (error) {
|
|
117
207
|
if (isDeadlineExceeded(error)) {
|
|
118
|
-
console.log(
|
|
119
|
-
|
|
208
|
+
console.log(
|
|
209
|
+
`Deadline exceeded: remaining ${error.remainingMs}ms, buffer ${error.flushBufferMs}ms`,
|
|
210
|
+
);
|
|
120
211
|
}
|
|
121
212
|
throw error;
|
|
122
213
|
}
|
|
@@ -124,11 +215,11 @@ try {
|
|
|
124
215
|
|
|
125
216
|
## Signal Composition
|
|
126
217
|
|
|
127
|
-
If you pass an `AbortSignal` to a request, the middleware composes both
|
|
218
|
+
If you pass an `AbortSignal` to a specific SDK request, the middleware composes both. Whichever fires first wins:
|
|
128
219
|
|
|
129
220
|
```typescript
|
|
130
221
|
const controller = new AbortController();
|
|
131
|
-
setTimeout(() => controller.abort(),
|
|
222
|
+
setTimeout(() => controller.abort(), 2000);
|
|
132
223
|
|
|
133
224
|
await dynamodb.send(
|
|
134
225
|
new GetItemCommand({
|
|
@@ -142,36 +233,36 @@ await dynamodb.send(
|
|
|
142
233
|
|
|
143
234
|
## API Reference
|
|
144
235
|
|
|
145
|
-
### `withLambdaDeadline(handler)`
|
|
236
|
+
### `withLambdaDeadline(handler, options?)`
|
|
146
237
|
|
|
147
|
-
Wraps a Lambda handler
|
|
148
|
-
`getRemainingTimeInMillis()`.
|
|
238
|
+
Wraps a Lambda handler. Computes `AbortSignal.timeout(remaining - flushBuffer)` and stores it in `AsyncLocalStorage`.
|
|
149
239
|
|
|
150
240
|
```typescript
|
|
151
241
|
function withLambdaDeadline<TEvent, TResult>(
|
|
152
242
|
handler: (event: TEvent, context: LambdaContextLike) => Promise<TResult>,
|
|
243
|
+
options?: DeadlineOptions,
|
|
153
244
|
): (event: TEvent, context: LambdaContextLike) => Promise<TResult>;
|
|
154
245
|
```
|
|
155
246
|
|
|
156
|
-
### `deadlineMiddleware(
|
|
247
|
+
### `deadlineMiddleware()`
|
|
157
248
|
|
|
158
|
-
Returns a `Pluggable` for `client.middlewareStack.use()`.
|
|
249
|
+
Returns a `Pluggable` for `client.middlewareStack.use()`. Attaches the stored deadline signal to outgoing requests.
|
|
159
250
|
|
|
160
251
|
```typescript
|
|
161
|
-
function deadlineMiddleware(
|
|
252
|
+
function deadlineMiddleware(): Pluggable<object, object>;
|
|
162
253
|
```
|
|
163
254
|
|
|
164
|
-
### `
|
|
255
|
+
### `setDeadlineSignal(signal)`
|
|
165
256
|
|
|
166
|
-
|
|
257
|
+
Replaces the deadline signal for the current invocation. Must be called within a `withLambdaDeadline()` scope.
|
|
167
258
|
|
|
168
259
|
```typescript
|
|
169
|
-
function
|
|
260
|
+
function setDeadlineSignal(signal: AbortSignal): void;
|
|
170
261
|
```
|
|
171
262
|
|
|
172
263
|
### `isDeadlineExceeded(error)`
|
|
173
264
|
|
|
174
|
-
Type guard for deadline-triggered
|
|
265
|
+
Type guard for deadline-triggered errors.
|
|
175
266
|
|
|
176
267
|
```typescript
|
|
177
268
|
function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
|
|
@@ -179,12 +270,14 @@ function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
|
|
|
179
270
|
|
|
180
271
|
### `DeadlineExceededError`
|
|
181
272
|
|
|
273
|
+
Thrown by `withLambdaDeadline` when `remainingTime ≤ flushBufferMs`.
|
|
274
|
+
|
|
182
275
|
```typescript
|
|
183
276
|
class DeadlineExceededError extends Error {
|
|
184
277
|
readonly name: "DeadlineExceededError";
|
|
185
|
-
readonly deadlineMs:
|
|
186
|
-
readonly flushBufferMs:
|
|
187
|
-
readonly remainingMs:
|
|
278
|
+
readonly deadlineMs: number;
|
|
279
|
+
readonly flushBufferMs: number;
|
|
280
|
+
readonly remainingMs: number;
|
|
188
281
|
}
|
|
189
282
|
```
|
|
190
283
|
|
|
@@ -198,20 +291,19 @@ interface DeadlineOptions {
|
|
|
198
291
|
|
|
199
292
|
### Types
|
|
200
293
|
|
|
201
|
-
| Type | Description
|
|
202
|
-
| ------------------- |
|
|
203
|
-
| `
|
|
204
|
-
| `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?(): number }` |
|
|
294
|
+
| Type | Description |
|
|
295
|
+
| ------------------- | ---------------------------------------------------------------- |
|
|
296
|
+
| `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?: () => number }` |
|
|
205
297
|
|
|
206
298
|
## Reporting Bugs
|
|
207
299
|
|
|
208
|
-
Found a bug? Please open a [GitHub Issue](https://github.com/mikkopiu/lambda-deadline-middleware/issues/new) with
|
|
300
|
+
Found a bug? Please open a [GitHub Issue](https://github.com/mikkopiu/lambda-deadline-middleware/issues/new) with a
|
|
301
|
+
minimal reproduction, your Node.js version, and AWS SDK version. For security vulnerabilities, see
|
|
302
|
+
[SECURITY.md](SECURITY.md).
|
|
209
303
|
|
|
210
|
-
|
|
211
|
-
- A minimal code snippet reproducing the problem
|
|
212
|
-
- Expected vs actual behavior
|
|
304
|
+
## Changelog
|
|
213
305
|
|
|
214
|
-
|
|
306
|
+
See [GitHub Releases](https://github.com/mikkopiu/lambda-deadline-middleware/releases).
|
|
215
307
|
|
|
216
308
|
## License
|
|
217
309
|
|
package/dist/context-store.d.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
+
import type { DeadlineOptions } from "./types.js";
|
|
1
2
|
export interface LambdaContextLike {
|
|
2
3
|
getRemainingTimeInMillis?: () => number;
|
|
3
4
|
}
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Store an external AbortSignal for the current invocation.
|
|
7
|
+
* When set, the SDK middleware uses this signal directly instead of
|
|
8
|
+
* the auto-computed deadline signal.
|
|
9
|
+
*
|
|
10
|
+
* Call this at the start of your handler, before any SDK calls.
|
|
11
|
+
* The signal is scoped to the current async context (AsyncLocalStorage).
|
|
12
|
+
*/
|
|
13
|
+
export declare const setDeadlineSignal: (signal: AbortSignal) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve the deadline signal for the current invocation, if one exists.
|
|
16
|
+
*/
|
|
17
|
+
export declare const getDeadlineSignal: () => AbortSignal | undefined;
|
|
18
|
+
type AsyncHandler<
|
|
19
|
+
TEvent,
|
|
20
|
+
TContext extends LambdaContextLike,
|
|
21
|
+
TResult
|
|
22
|
+
> = (event: TEvent, context: TContext) => Promise<TResult>;
|
|
23
|
+
export declare const withLambdaDeadline: <
|
|
24
|
+
TEvent,
|
|
25
|
+
TContext extends LambdaContextLike,
|
|
26
|
+
TResult
|
|
27
|
+
>(handler: AsyncHandler<TEvent, TContext, TResult>, options?: DeadlineOptions) => AsyncHandler<TEvent, TContext, TResult>;
|
|
28
|
+
export {};
|
|
6
29
|
|
|
7
30
|
//# sourceMappingURL=context-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAOA,iBAAiB,kBAAkB;CACjC;AACF
|
|
1
|
+
{"mappings":"AAOA,cAAc,uBAAuB;AAIrC,iBAAiB,kBAAkB;CACjC;AACF;;;;;;;;;AAgBA,OAAO,cAAM,oBAAqB,QAAQ;;;;AAW1C,OAAO,cAAM,yBAAwB;KAOhC;CAAa;CAAQ,iBAAiB;CAAmB;KAC5D,OAAO,QACP,SAAS,aACN,QAAQ;AAEb,OAAO,cAAM;CACV;CAAQ,iBAAiB;CAAmB;EAC3C,SAAS,aAAa,QAAQ,UAAU,UACxC,UAAU,oBACT,aAAa,QAAQ,UAAU","names":[],"sources":["src/context-store.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport { DeadlineExceededError } from \"./error.js\";\n\nimport type { DeadlineOptions } from \"./types.js\";\n\n// AsyncLocalStorage propagates the deadline signal through the entire async call chain.\n// SDK middleware executes deep in the Smithy stack where we can't pass it through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\ninterface DeadlineStore {\n signal?: AbortSignal;\n}\n\nconst contextStorage = new AsyncLocalStorage<DeadlineStore>();\n\n/**\n * Store an external AbortSignal for the current invocation.\n * When set, the SDK middleware uses this signal directly instead of\n * the auto-computed deadline signal.\n *\n * Call this at the start of your handler, before any SDK calls.\n * The signal is scoped to the current async context (AsyncLocalStorage).\n */\nexport const setDeadlineSignal = (signal: AbortSignal): void => {\n const store = contextStorage.getStore();\n if (store === undefined) {\n throw new Error(\"setDeadlineSignal() must be called within a withLambdaDeadline() scope\");\n }\n store.signal = signal;\n};\n\n/**\n * Retrieve the deadline signal for the current invocation, if one exists.\n */\nexport const getDeadlineSignal = (): AbortSignal | undefined => {\n const store = contextStorage.getStore();\n if (store === undefined) return undefined;\n return store.signal;\n};\n\n// Handler wrapper — computes the deadline signal once at invocation start and stores it via AsyncLocalStorage.\ntype AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (\n event: TEvent,\n context: TContext,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TContext extends LambdaContextLike, TResult>(\n handler: AsyncHandler<TEvent, TContext, TResult>,\n options?: DeadlineOptions,\n ): AsyncHandler<TEvent, TContext, TResult> =>\n async (event: TEvent, context: TContext): Promise<TResult> => {\n const store: DeadlineStore = {};\n\n // Compute the auto-deadline signal once, up front.\n // If the user calls setDeadlineSignal() later, it overwrites this.\n // oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime safety: context may be null/undefined despite types (e.g. untyped callers)\n if (context !== null && context !== undefined) {\n const remaining =\n typeof context.getRemainingTimeInMillis === \"function\"\n ? context.getRemainingTimeInMillis()\n : undefined;\n\n if (remaining !== undefined) {\n const rawBuffer = options?.flushBufferMs ?? 1000;\n if (rawBuffer < 0) {\n throw new TypeError(`flushBufferMs option must be non-negative, received: ${rawBuffer}`);\n }\n const deadline = remaining - rawBuffer;\n\n if (deadline <= 0) {\n throw new DeadlineExceededError({\n deadlineMs: 0,\n flushBufferMs: rawBuffer,\n remainingMs: remaining,\n });\n }\n\n store.signal = AbortSignal.timeout(deadline);\n }\n }\n\n return contextStorage.run(store, async () => handler(event, context));\n };\n"]}
|
package/dist/context-store.js
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { DeadlineExceededError } from "./error.js";
|
|
4
5
|
const contextStorage = new AsyncLocalStorage();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Store an external AbortSignal for the current invocation.
|
|
8
|
+
* When set, the SDK middleware uses this signal directly instead of
|
|
9
|
+
* the auto-computed deadline signal.
|
|
10
|
+
*
|
|
11
|
+
* Call this at the start of your handler, before any SDK calls.
|
|
12
|
+
* The signal is scoped to the current async context (AsyncLocalStorage).
|
|
13
|
+
*/
|
|
14
|
+
export const setDeadlineSignal = (signal) => {
|
|
15
|
+
const store = contextStorage.getStore();
|
|
16
|
+
if (store === undefined) {
|
|
17
|
+
throw new Error("setDeadlineSignal() must be called within a withLambdaDeadline() scope");
|
|
18
|
+
}
|
|
19
|
+
store.signal = signal;
|
|
8
20
|
};
|
|
9
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Retrieve the deadline signal for the current invocation, if one exists.
|
|
23
|
+
*/
|
|
24
|
+
export const getDeadlineSignal = () => {
|
|
10
25
|
const store = contextStorage.getStore();
|
|
11
26
|
if (store === undefined) return undefined;
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
return store.signal;
|
|
28
|
+
};
|
|
29
|
+
export const withLambdaDeadline = (handler, options) => async (event, context) => {
|
|
30
|
+
const store = {};
|
|
31
|
+
// Compute the auto-deadline signal once, up front.
|
|
32
|
+
// If the user calls setDeadlineSignal() later, it overwrites this.
|
|
33
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime safety: context may be null/undefined despite types (e.g. untyped callers)
|
|
34
|
+
if (context !== null && context !== undefined) {
|
|
35
|
+
const remaining = typeof context.getRemainingTimeInMillis === "function" ? context.getRemainingTimeInMillis() : undefined;
|
|
36
|
+
if (remaining !== undefined) {
|
|
37
|
+
const rawBuffer = options?.flushBufferMs ?? 1e3;
|
|
38
|
+
if (rawBuffer < 0) {
|
|
39
|
+
throw new TypeError(`flushBufferMs option must be non-negative, received: ${rawBuffer}`);
|
|
40
|
+
}
|
|
41
|
+
const deadline = remaining - rawBuffer;
|
|
42
|
+
if (deadline <= 0) {
|
|
43
|
+
throw new DeadlineExceededError({
|
|
44
|
+
deadlineMs: 0,
|
|
45
|
+
flushBufferMs: rawBuffer,
|
|
46
|
+
remainingMs: remaining
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
store.signal = AbortSignal.timeout(deadline);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return contextStorage.run(store, async () => handler(event, context));
|
|
14
53
|
};
|
|
15
54
|
|
|
16
55
|
//# sourceMappingURL=context-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;AAGA,SAAS,yBAAyB;
|
|
1
|
+
{"mappings":";;AAGA,SAAS,yBAAyB;AAElC,SAAS,6BAA6B;AActC,MAAM,iBAAiB,IAAI,kBAAiC;;;;;;;;;AAU5D,OAAO,MAAM,qBAAqB,WAA8B;CAC9D,MAAM,QAAQ,eAAe,SAAS;CACtC,IAAI,UAAU,WAAW;EACvB,MAAM,IAAI,MAAM,wEAAwE;CAC1F;CACA,MAAM,SAAS;AACjB;;;;AAKA,OAAO,MAAM,0BAAmD;CAC9D,MAAM,QAAQ,eAAe,SAAS;CACtC,IAAI,UAAU,WAAW,OAAO;CAChC,OAAO,MAAM;AACf;AAQA,OAAO,MAAM,sBAET,SACA,YAEF,OAAO,OAAe,YAAwC;CAC5D,MAAM,QAAuB,CAAC;;;;CAK9B,IAAI,YAAY,QAAQ,YAAY,WAAW;EAC7C,MAAM,YACJ,OAAO,QAAQ,6BAA6B,aACxC,QAAQ,yBAAyB,IACjC;EAEN,IAAI,cAAc,WAAW;GAC3B,MAAM,YAAY,SAAS,iBAAiB;GAC5C,IAAI,YAAY,GAAG;IACjB,MAAM,IAAI,UAAU,wDAAwD,WAAW;GACzF;GACA,MAAM,WAAW,YAAY;GAE7B,IAAI,YAAY,GAAG;IACjB,MAAM,IAAI,sBAAsB;KAC9B,YAAY;KACZ,eAAe;KACf,aAAa;IACf,CAAC;GACH;GAEA,MAAM,SAAS,YAAY,QAAQ,QAAQ;EAC7C;CACF;CAEA,OAAO,eAAe,IAAI,OAAO,YAAY,QAAQ,OAAO,OAAO,CAAC;AACtE","names":[],"sources":["src/context-store.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport { DeadlineExceededError } from \"./error.js\";\n\nimport type { DeadlineOptions } from \"./types.js\";\n\n// AsyncLocalStorage propagates the deadline signal through the entire async call chain.\n// SDK middleware executes deep in the Smithy stack where we can't pass it through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\ninterface DeadlineStore {\n signal?: AbortSignal;\n}\n\nconst contextStorage = new AsyncLocalStorage<DeadlineStore>();\n\n/**\n * Store an external AbortSignal for the current invocation.\n * When set, the SDK middleware uses this signal directly instead of\n * the auto-computed deadline signal.\n *\n * Call this at the start of your handler, before any SDK calls.\n * The signal is scoped to the current async context (AsyncLocalStorage).\n */\nexport const setDeadlineSignal = (signal: AbortSignal): void => {\n const store = contextStorage.getStore();\n if (store === undefined) {\n throw new Error(\"setDeadlineSignal() must be called within a withLambdaDeadline() scope\");\n }\n store.signal = signal;\n};\n\n/**\n * Retrieve the deadline signal for the current invocation, if one exists.\n */\nexport const getDeadlineSignal = (): AbortSignal | undefined => {\n const store = contextStorage.getStore();\n if (store === undefined) return undefined;\n return store.signal;\n};\n\n// Handler wrapper — computes the deadline signal once at invocation start and stores it via AsyncLocalStorage.\ntype AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (\n event: TEvent,\n context: TContext,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TContext extends LambdaContextLike, TResult>(\n handler: AsyncHandler<TEvent, TContext, TResult>,\n options?: DeadlineOptions,\n ): AsyncHandler<TEvent, TContext, TResult> =>\n async (event: TEvent, context: TContext): Promise<TResult> => {\n const store: DeadlineStore = {};\n\n // Compute the auto-deadline signal once, up front.\n // If the user calls setDeadlineSignal() later, it overwrites this.\n // oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime safety: context may be null/undefined despite types (e.g. untyped callers)\n if (context !== null && context !== undefined) {\n const remaining =\n typeof context.getRemainingTimeInMillis === \"function\"\n ? context.getRemainingTimeInMillis()\n : undefined;\n\n if (remaining !== undefined) {\n const rawBuffer = options?.flushBufferMs ?? 1000;\n if (rawBuffer < 0) {\n throw new TypeError(`flushBufferMs option must be non-negative, received: ${rawBuffer}`);\n }\n const deadline = remaining - rawBuffer;\n\n if (deadline <= 0) {\n throw new DeadlineExceededError({\n deadlineMs: 0,\n flushBufferMs: rawBuffer,\n remainingMs: remaining,\n });\n }\n\n store.signal = AbortSignal.timeout(deadline);\n }\n }\n\n return contextStorage.run(store, async () => handler(event, context));\n };\n"]}
|
package/dist/error.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { Milliseconds } from "./types.js";
|
|
2
1
|
interface DeadlineExceededInit {
|
|
3
|
-
readonly deadlineMs:
|
|
4
|
-
readonly flushBufferMs:
|
|
5
|
-
readonly remainingMs:
|
|
2
|
+
readonly deadlineMs: number;
|
|
3
|
+
readonly flushBufferMs: number;
|
|
4
|
+
readonly remainingMs: number;
|
|
6
5
|
}
|
|
7
6
|
export declare class DeadlineExceededError extends Error {
|
|
8
7
|
override readonly name = "DeadlineExceededError";
|
|
9
|
-
readonly deadlineMs:
|
|
10
|
-
readonly flushBufferMs:
|
|
11
|
-
readonly remainingMs:
|
|
8
|
+
readonly deadlineMs: number;
|
|
9
|
+
readonly flushBufferMs: number;
|
|
10
|
+
readonly remainingMs: number;
|
|
12
11
|
constructor(init: DeadlineExceededInit);
|
|
13
12
|
}
|
|
14
13
|
export declare const isDeadlineExceeded: (error: unknown) => error is DeadlineExceededError;
|
package/dist/error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":"UAGU,qBAAqB;UACpB;UACA;UACA;AACX;AAEA,OAAO,cAAM,8BAA8B,MAAM;CAC/C,kBAAkB,OAAO;CACzB,SAAS;CACT,SAAS;CACT,SAAS;CAET,YAAY,MAAM;AAQpB;AAIA,OAAO,cAAM,qBAAsB,mBAAiB,SAAS","names":[],"sources":["src/error.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: number;\n readonly flushBufferMs: number;\n readonly remainingMs: number;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: number;\n readonly flushBufferMs: number;\n readonly remainingMs: number;\n\n constructor(init: DeadlineExceededInit) {\n super(\n `Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,\n );\n this.deadlineMs = init.deadlineMs;\n this.flushBufferMs = init.flushBufferMs;\n this.remainingMs = init.remainingMs;\n }\n}\n\n// Structural check rather than instanceof — works across module boundaries\n// and serialization boundaries where prototype chain may be broken.\nexport const isDeadlineExceeded = (error: unknown): error is DeadlineExceededError => {\n if (error === null || error === undefined) return false;\n if (typeof error !== \"object\") return false;\n return (error as { name?: unknown }).name === \"DeadlineExceededError\";\n};\n"]}
|
package/dist/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":"AASA,OAAO,MAAM,8BAA8B,MAAM;CAC/C,AAAkB,OAAO;CACzB,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAA4B;EACtC,MACE,8BAA8B,KAAK,WAAW,eAAe,KAAK,cAAc,iBAClF;EACA,KAAK,aAAa,KAAK;EACvB,KAAK,gBAAgB,KAAK;EAC1B,KAAK,cAAc,KAAK;CAC1B;AACF;;;AAIA,OAAO,MAAM,sBAAsB,UAAmD;CACpF,IAAI,UAAU,QAAQ,UAAU,WAAW,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,OAAQ,MAA6B,SAAS;AAChD","names":[],"sources":["src/error.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: number;\n readonly flushBufferMs: number;\n readonly remainingMs: number;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: number;\n readonly flushBufferMs: number;\n readonly remainingMs: number;\n\n constructor(init: DeadlineExceededInit) {\n super(\n `Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,\n );\n this.deadlineMs = init.deadlineMs;\n this.flushBufferMs = init.flushBufferMs;\n this.remainingMs = init.remainingMs;\n }\n}\n\n// Structural check rather than instanceof — works across module boundaries\n// and serialization boundaries where prototype chain may be broken.\nexport const isDeadlineExceeded = (error: unknown): error is DeadlineExceededError => {\n if (error === null || error === undefined) return false;\n if (typeof error !== \"object\") return false;\n return (error as { name?: unknown }).name === \"DeadlineExceededError\";\n};\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
export { withLambdaDeadline } from "./
|
|
1
|
+
export { withLambdaDeadline, setDeadlineSignal } from "./context-store.js";
|
|
2
2
|
export { deadlineMiddleware } from "./middleware.js";
|
|
3
3
|
export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
|
|
4
|
-
export {
|
|
5
|
-
export type { Milliseconds, DeadlineOptions } from "./types.js";
|
|
4
|
+
export type { DeadlineOptions } from "./types.js";
|
|
6
5
|
export type { LambdaContextLike } from "./context-store.js";
|
|
7
6
|
|
|
8
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAGA,SAAS,
|
|
1
|
+
{"mappings":"AAGA,SAAS,oBAAoB,yBAAyB;AACtD,SAAS,0BAA0B;AACnC,SAAS,uBAAuB,0BAA0B;AAE1D,cAAc,uBAAuB;AACrC,cAAc,yBAAyB","names":[],"sources":["src/index.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport { withLambdaDeadline, setDeadlineSignal } from \"./context-store.js\";\nexport { deadlineMiddleware } from \"./middleware.js\";\nexport { DeadlineExceededError, isDeadlineExceeded } from \"./error.js\";\n\nexport type { DeadlineOptions } from \"./types.js\";\nexport type { LambdaContextLike } from \"./context-store.js\";\n"]}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
export { withLambdaDeadline } from "./
|
|
3
|
+
export { withLambdaDeadline, setDeadlineSignal } from "./context-store.js";
|
|
4
4
|
export { deadlineMiddleware } from "./middleware.js";
|
|
5
5
|
export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
|
|
6
|
-
export { getRemainingTimeInMillis } from "./context-store.js";
|
|
7
6
|
|
|
8
7
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;AAGA,SAAS,
|
|
1
|
+
{"mappings":";;AAGA,SAAS,oBAAoB,yBAAyB;AACtD,SAAS,0BAA0B;AACnC,SAAS,uBAAuB,0BAA0B","names":[],"sources":["src/index.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport { withLambdaDeadline, setDeadlineSignal } from \"./context-store.js\";\nexport { deadlineMiddleware } from \"./middleware.js\";\nexport { DeadlineExceededError, isDeadlineExceeded } from \"./error.js\";\n\nexport type { DeadlineOptions } from \"./types.js\";\nexport type { LambdaContextLike } from \"./context-store.js\";\n"]}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { Pluggable } from "@smithy/types";
|
|
2
|
-
import type { DeadlineOptions } from "./types.js";
|
|
3
|
-
export declare const composeSignals: (existing: AbortSignal | undefined, deadline: AbortSignal) => AbortSignal;
|
|
4
2
|
export declare const deadlineMiddleware: <
|
|
5
3
|
Input extends object,
|
|
6
4
|
Output extends object
|
|
7
|
-
>(
|
|
5
|
+
>() => Pluggable<Input, Output>;
|
|
8
6
|
|
|
9
7
|
//# sourceMappingURL=middleware.d.ts.map
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAGA,cAKE,iBACK;
|
|
1
|
+
{"mappings":"AAGA,cAKE,iBACK;AAIP,OAAO,cAAM;CAAsB;CAAsB;OAA0B,UACjF,OACA","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type {\n FinalizeHandler,\n FinalizeHandlerArguments,\n FinalizeHandlerOutput,\n HandlerExecutionContext,\n Pluggable,\n} from \"@smithy/types\";\n\nimport { getDeadlineSignal } from \"./context-store.js\";\n\nexport const deadlineMiddleware = <Input extends object, Output extends object>(): Pluggable<\n Input,\n Output\n> => ({\n applyToStack(stack) {\n stack.add(\n (\n next: FinalizeHandler<Input, Output>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<Input, Output> =>\n async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {\n const deadlineSignal = getDeadlineSignal();\n if (deadlineSignal === undefined) return next(args);\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object\n const request = args.request as { signal?: AbortSignal } | undefined;\n const existing = request?.signal;\n const signal = existing ? AbortSignal.any([existing, deadlineSignal]) : deadlineSignal;\n\n return next({\n ...args,\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal\n request: { ...(args.request as object), signal },\n });\n },\n {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n },\n );\n },\n});\n"]}
|
package/dist/middleware.js
CHANGED
|
@@ -1,62 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// computed from the actual remaining time at that moment. API-call level would cache a stale deadline
|
|
17
|
-
// across retries, which grow more dangerous after backoff delays eat into remaining time.
|
|
18
|
-
stack.add((next, _context) => async (args) => {
|
|
19
|
-
const remaining = getRemainingTimeInMillis();
|
|
20
|
-
if (remaining === undefined) return next(args);
|
|
21
|
-
const deadline = remaining - flushBufferMs;
|
|
22
|
-
if (deadline <= 0) {
|
|
23
|
-
throw new DeadlineExceededError({
|
|
24
|
-
deadlineMs: milliseconds(0),
|
|
25
|
-
flushBufferMs,
|
|
26
|
-
remainingMs: milliseconds(remaining)
|
|
27
|
-
});
|
|
1
|
+
import { getDeadlineSignal } from "./context-store.js";
|
|
2
|
+
export const deadlineMiddleware = () => ({ applyToStack(stack) {
|
|
3
|
+
stack.add((next, _context) => async (args) => {
|
|
4
|
+
const deadlineSignal = getDeadlineSignal();
|
|
5
|
+
if (deadlineSignal === undefined) return next(args);
|
|
6
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object
|
|
7
|
+
const request = args.request;
|
|
8
|
+
const existing = request?.signal;
|
|
9
|
+
const signal = existing ? AbortSignal.any([existing, deadlineSignal]) : deadlineSignal;
|
|
10
|
+
return next({
|
|
11
|
+
...args,
|
|
12
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
|
|
13
|
+
request: {
|
|
14
|
+
...args.request,
|
|
15
|
+
signal
|
|
28
16
|
}
|
|
29
|
-
const controller = new AbortController();
|
|
30
|
-
const timeoutId = setTimeout(() => {
|
|
31
|
-
controller.abort(new DeadlineExceededError({
|
|
32
|
-
deadlineMs: milliseconds(deadline),
|
|
33
|
-
flushBufferMs,
|
|
34
|
-
remainingMs: milliseconds(remaining)
|
|
35
|
-
}));
|
|
36
|
-
}, deadline);
|
|
37
|
-
// `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
|
|
38
|
-
// or an external abort signal fires — strictly more reliable than try/finally.
|
|
39
|
-
using _timer = { [Symbol.dispose]() {
|
|
40
|
-
clearTimeout(timeoutId);
|
|
41
|
-
} };
|
|
42
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
|
|
43
|
-
const request = args.request;
|
|
44
|
-
const signal = composeSignals(request?.signal, controller.signal);
|
|
45
|
-
const result = await next({
|
|
46
|
-
...args,
|
|
47
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
|
|
48
|
-
request: {
|
|
49
|
-
...args.request,
|
|
50
|
-
signal
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
return result;
|
|
54
|
-
}, {
|
|
55
|
-
step: "finalizeRequest",
|
|
56
|
-
name: "deadlineMiddleware",
|
|
57
|
-
override: true
|
|
58
17
|
});
|
|
59
|
-
}
|
|
60
|
-
|
|
18
|
+
}, {
|
|
19
|
+
step: "finalizeRequest",
|
|
20
|
+
name: "deadlineMiddleware",
|
|
21
|
+
override: true
|
|
22
|
+
});
|
|
23
|
+
} });
|
|
61
24
|
|
|
62
25
|
//# sourceMappingURL=middleware.js.map
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAWA,SAAS,
|
|
1
|
+
{"mappings":"AAWA,SAAS,yBAAyB;AAElC,OAAO,MAAM,4BAGP,EACJ,aAAa,OAAO;CAClB,MAAM,KAEF,MACA,aAEA,OAAO,SAAkF;EACvF,MAAM,iBAAiB,kBAAkB;EACzC,IAAI,mBAAmB,WAAW,OAAO,KAAK,IAAI;;EAGlD,MAAM,UAAU,KAAK;EACrB,MAAM,WAAW,SAAS;EAC1B,MAAM,SAAS,WAAW,YAAY,IAAI,CAAC,UAAU,cAAc,CAAC,IAAI;EAExE,OAAO,KAAK;GACV,GAAG;;GAEH,SAAS;IAAE,GAAI,KAAK;IAAoB;GAAO;EACjD,CAAC;CACH,GACF;EACE,MAAM;EACN,MAAM;EACN,UAAU;CACZ,CACF;AACF,EACF","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type {\n FinalizeHandler,\n FinalizeHandlerArguments,\n FinalizeHandlerOutput,\n HandlerExecutionContext,\n Pluggable,\n} from \"@smithy/types\";\n\nimport { getDeadlineSignal } from \"./context-store.js\";\n\nexport const deadlineMiddleware = <Input extends object, Output extends object>(): Pluggable<\n Input,\n Output\n> => ({\n applyToStack(stack) {\n stack.add(\n (\n next: FinalizeHandler<Input, Output>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<Input, Output> =>\n async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {\n const deadlineSignal = getDeadlineSignal();\n if (deadlineSignal === undefined) return next(args);\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object\n const request = args.request as { signal?: AbortSignal } | undefined;\n const existing = request?.signal;\n const signal = existing ? AbortSignal.any([existing, deadlineSignal]) : deadlineSignal;\n\n return next({\n ...args,\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal\n request: { ...(args.request as object), signal },\n });\n },\n {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n },\n );\n },\n});\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
declare const BrandSymbol: unique symbol;
|
|
2
|
-
type Brand<
|
|
3
|
-
T,
|
|
4
|
-
B extends string
|
|
5
|
-
> = T & {
|
|
6
|
-
readonly [BrandSymbol]: B;
|
|
7
|
-
};
|
|
8
|
-
export type Milliseconds = Brand<number, "Milliseconds">;
|
|
9
|
-
export declare const milliseconds: (value: number) => Milliseconds;
|
|
10
1
|
export interface DeadlineOptions {
|
|
11
2
|
readonly flushBufferMs?: number;
|
|
12
3
|
}
|
|
13
|
-
export {};
|
|
14
4
|
|
|
15
5
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":"AAGA,iBAAiB,gBAAgB;UACtB;AACX","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n}\n"]}
|
package/dist/types.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
if (!Number.isFinite(value)) {
|
|
3
|
-
throw new TypeError(`milliseconds value must be finite, received: ${value}`);
|
|
4
|
-
}
|
|
5
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
6
|
-
return value;
|
|
7
|
-
};
|
|
1
|
+
export {};
|
|
8
2
|
|
|
9
3
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"
|
|
1
|
+
{"mappings":"","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambda-deadline-middleware",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AWS SDK v3 middleware
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Zero-dependency AWS SDK v3 middleware that propagates Lambda deadlines to outgoing SDK calls via AbortSignal",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -28,19 +28,19 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node --experimental-strip-types scripts/build.ts",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"test:watch": "vitest",
|
|
33
|
-
"bench": "vitest bench --run --outputJson=bench-results.json && node --experimental-strip-types scripts/validate-bench.ts",
|
|
31
|
+
"test": "vitest run --config .config/vitest.config.ts",
|
|
32
|
+
"test:watch": "vitest --config .config/vitest.config.ts",
|
|
33
|
+
"bench": "vitest bench --run --config .config/vitest.config.ts --outputJson=bench-results.json && node --experimental-strip-types scripts/validate-bench.ts",
|
|
34
34
|
"typecheck": "tsc",
|
|
35
|
-
"lint": "oxlint --
|
|
36
|
-
"lint:check": "oxlint
|
|
35
|
+
"lint": "oxlint --fix .",
|
|
36
|
+
"lint:check": "oxlint .",
|
|
37
37
|
"fmt": "oxfmt .",
|
|
38
38
|
"fmt:check": "oxfmt --check .",
|
|
39
|
-
"lint:knip": "knip",
|
|
39
|
+
"lint:knip": "knip --config .config/knip.js",
|
|
40
40
|
"sast": "scripts/ensure-opengrep.sh scan .",
|
|
41
41
|
"sast:check": "scripts/ensure-opengrep.sh scan --error .",
|
|
42
|
-
"sca": "podman run --rm -v \"$(pwd):/src:Z\" -v trivy-cache:/root/.cache/trivy:Z ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln --include-dev-deps --exit-code 1 --severity HIGH,CRITICAL /src",
|
|
43
|
-
"sca:full": "podman run --rm -v \"$(pwd):/src:Z\" -v trivy-cache:/root/.cache/trivy:Z ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln,secret,misconfig --include-dev-deps /src",
|
|
42
|
+
"sca": "podman run --rm -v \"$(pwd):/src:Z\" -v trivy-cache:/root/.cache/trivy:Z ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --config /src/.config/trivy.yaml --ignorefile /src/.config/.trivyignore --scanners vuln --include-dev-deps --exit-code 1 --severity HIGH,CRITICAL /src",
|
|
43
|
+
"sca:full": "podman run --rm -v \"$(pwd):/src:Z\" -v trivy-cache:/root/.cache/trivy:Z ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --config /src/.config/trivy.yaml --ignorefile /src/.config/.trivyignore --scanners vuln,secret,misconfig --include-dev-deps /src",
|
|
44
44
|
"actionlint": "podman run --rm -w /repo -v \"$(pwd):/repo:Z\" docker.io/rhysd/actionlint:1.7.12@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7",
|
|
45
45
|
"security": "pnpm run sast:check && pnpm run sca && pnpm run actionlint",
|
|
46
46
|
"prepare": "lefthook install",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"release": "semantic-release"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
52
|
-
"@aws-sdk/client-s3": "^3.
|
|
53
|
-
"@aws-sdk/client-sqs": "^3.
|
|
51
|
+
"@aws-sdk/client-dynamodb": "^3.1073.0",
|
|
52
|
+
"@aws-sdk/client-s3": "^3.1073.0",
|
|
53
|
+
"@aws-sdk/client-sqs": "^3.1073.0",
|
|
54
54
|
"@commitlint/cli": "^21.0.2",
|
|
55
55
|
"@commitlint/config-conventional": "^21.0.2",
|
|
56
56
|
"@fast-check/vitest": "^0.4.1",
|
|
@@ -58,19 +58,19 @@
|
|
|
58
58
|
"@semantic-release/github": "12.0.8",
|
|
59
59
|
"@semantic-release/npm": "13.1.5",
|
|
60
60
|
"@semantic-release/release-notes-generator": "14.1.1",
|
|
61
|
-
"@smithy/types": "^4.
|
|
62
|
-
"@types/node": "^25.9.
|
|
63
|
-
"@vitest/coverage-v8": "^4.1.
|
|
61
|
+
"@smithy/types": "^4.15.0",
|
|
62
|
+
"@types/node": "^25.9.4",
|
|
63
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
64
64
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
65
|
-
"knip": "^6.
|
|
65
|
+
"knip": "^6.17.1",
|
|
66
66
|
"lefthook": "^2.1.9",
|
|
67
67
|
"oxc-transform": "^0.135.0",
|
|
68
68
|
"oxfmt": "^0.54.0",
|
|
69
|
-
"oxlint": "^1.
|
|
69
|
+
"oxlint": "^1.70.0",
|
|
70
70
|
"oxlint-tsgolint": "^0.23.0",
|
|
71
71
|
"semantic-release": "25.0.5",
|
|
72
72
|
"typescript": "^6.0.3",
|
|
73
|
-
"vitest": "^4.1.
|
|
73
|
+
"vitest": "^4.1.9"
|
|
74
74
|
},
|
|
75
75
|
"engines": {
|
|
76
76
|
"node": ">=24"
|
package/src/context-store.ts
CHANGED
|
@@ -3,22 +3,88 @@
|
|
|
3
3
|
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { DeadlineExceededError } from "./error.js";
|
|
7
|
+
|
|
8
|
+
import type { DeadlineOptions } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// AsyncLocalStorage propagates the deadline signal through the entire async call chain.
|
|
11
|
+
// SDK middleware executes deep in the Smithy stack where we can't pass it through function signatures.
|
|
8
12
|
export interface LambdaContextLike {
|
|
9
13
|
getRemainingTimeInMillis?: () => number;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
interface DeadlineStore {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const contextStorage = new AsyncLocalStorage<DeadlineStore>();
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Store an external AbortSignal for the current invocation.
|
|
24
|
+
* When set, the SDK middleware uses this signal directly instead of
|
|
25
|
+
* the auto-computed deadline signal.
|
|
26
|
+
*
|
|
27
|
+
* Call this at the start of your handler, before any SDK calls.
|
|
28
|
+
* The signal is scoped to the current async context (AsyncLocalStorage).
|
|
29
|
+
*/
|
|
30
|
+
export const setDeadlineSignal = (signal: AbortSignal): void => {
|
|
31
|
+
const store = contextStorage.getStore();
|
|
32
|
+
if (store === undefined) {
|
|
33
|
+
throw new Error("setDeadlineSignal() must be called within a withLambdaDeadline() scope");
|
|
34
|
+
}
|
|
35
|
+
store.signal = signal;
|
|
17
36
|
};
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Retrieve the deadline signal for the current invocation, if one exists.
|
|
40
|
+
*/
|
|
41
|
+
export const getDeadlineSignal = (): AbortSignal | undefined => {
|
|
20
42
|
const store = contextStorage.getStore();
|
|
21
43
|
if (store === undefined) return undefined;
|
|
22
|
-
|
|
23
|
-
return store.getRemainingTimeInMillis();
|
|
44
|
+
return store.signal;
|
|
24
45
|
};
|
|
46
|
+
|
|
47
|
+
// Handler wrapper — computes the deadline signal once at invocation start and stores it via AsyncLocalStorage.
|
|
48
|
+
type AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (
|
|
49
|
+
event: TEvent,
|
|
50
|
+
context: TContext,
|
|
51
|
+
) => Promise<TResult>;
|
|
52
|
+
|
|
53
|
+
export const withLambdaDeadline =
|
|
54
|
+
<TEvent, TContext extends LambdaContextLike, TResult>(
|
|
55
|
+
handler: AsyncHandler<TEvent, TContext, TResult>,
|
|
56
|
+
options?: DeadlineOptions,
|
|
57
|
+
): AsyncHandler<TEvent, TContext, TResult> =>
|
|
58
|
+
async (event: TEvent, context: TContext): Promise<TResult> => {
|
|
59
|
+
const store: DeadlineStore = {};
|
|
60
|
+
|
|
61
|
+
// Compute the auto-deadline signal once, up front.
|
|
62
|
+
// If the user calls setDeadlineSignal() later, it overwrites this.
|
|
63
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime safety: context may be null/undefined despite types (e.g. untyped callers)
|
|
64
|
+
if (context !== null && context !== undefined) {
|
|
65
|
+
const remaining =
|
|
66
|
+
typeof context.getRemainingTimeInMillis === "function"
|
|
67
|
+
? context.getRemainingTimeInMillis()
|
|
68
|
+
: undefined;
|
|
69
|
+
|
|
70
|
+
if (remaining !== undefined) {
|
|
71
|
+
const rawBuffer = options?.flushBufferMs ?? 1000;
|
|
72
|
+
if (rawBuffer < 0) {
|
|
73
|
+
throw new TypeError(`flushBufferMs option must be non-negative, received: ${rawBuffer}`);
|
|
74
|
+
}
|
|
75
|
+
const deadline = remaining - rawBuffer;
|
|
76
|
+
|
|
77
|
+
if (deadline <= 0) {
|
|
78
|
+
throw new DeadlineExceededError({
|
|
79
|
+
deadlineMs: 0,
|
|
80
|
+
flushBufferMs: rawBuffer,
|
|
81
|
+
remainingMs: remaining,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
store.signal = AbortSignal.timeout(deadline);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return contextStorage.run(store, async () => handler(event, context));
|
|
90
|
+
};
|
package/src/error.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import type { Milliseconds } from "./types.js";
|
|
5
|
-
|
|
6
4
|
interface DeadlineExceededInit {
|
|
7
|
-
readonly deadlineMs:
|
|
8
|
-
readonly flushBufferMs:
|
|
9
|
-
readonly remainingMs:
|
|
5
|
+
readonly deadlineMs: number;
|
|
6
|
+
readonly flushBufferMs: number;
|
|
7
|
+
readonly remainingMs: number;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export class DeadlineExceededError extends Error {
|
|
13
11
|
override readonly name = "DeadlineExceededError" as const;
|
|
14
|
-
readonly deadlineMs:
|
|
15
|
-
readonly flushBufferMs:
|
|
16
|
-
readonly remainingMs:
|
|
12
|
+
readonly deadlineMs: number;
|
|
13
|
+
readonly flushBufferMs: number;
|
|
14
|
+
readonly remainingMs: number;
|
|
17
15
|
|
|
18
16
|
constructor(init: DeadlineExceededInit) {
|
|
19
17
|
super(
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
export { withLambdaDeadline } from "./
|
|
4
|
+
export { withLambdaDeadline, setDeadlineSignal } from "./context-store.js";
|
|
5
5
|
export { deadlineMiddleware } from "./middleware.js";
|
|
6
6
|
export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
|
|
7
|
-
export { getRemainingTimeInMillis } from "./context-store.js";
|
|
8
|
-
|
|
9
|
-
export type { Milliseconds, DeadlineOptions } from "./types.js";
|
|
10
7
|
|
|
8
|
+
export type { DeadlineOptions } from "./types.js";
|
|
11
9
|
export type { LambdaContextLike } from "./context-store.js";
|
package/src/middleware.ts
CHANGED
|
@@ -9,88 +9,38 @@ import type {
|
|
|
9
9
|
Pluggable,
|
|
10
10
|
} from "@smithy/types";
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// computed from the actual remaining time at that moment. API-call level would cache a stale deadline
|
|
39
|
-
// across retries, which grow more dangerous after backoff delays eat into remaining time.
|
|
40
|
-
stack.add(
|
|
41
|
-
(
|
|
42
|
-
next: FinalizeHandler<Input, Output>,
|
|
43
|
-
_context: HandlerExecutionContext,
|
|
44
|
-
): FinalizeHandler<Input, Output> =>
|
|
45
|
-
async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {
|
|
46
|
-
const remaining = getRemainingTimeInMillis();
|
|
47
|
-
if (remaining === undefined) return next(args);
|
|
48
|
-
|
|
49
|
-
const deadline = remaining - flushBufferMs;
|
|
50
|
-
|
|
51
|
-
if (deadline <= 0) {
|
|
52
|
-
throw new DeadlineExceededError({
|
|
53
|
-
deadlineMs: milliseconds(0),
|
|
54
|
-
flushBufferMs,
|
|
55
|
-
remainingMs: milliseconds(remaining),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const controller = new AbortController();
|
|
60
|
-
const timeoutId = setTimeout(() => {
|
|
61
|
-
controller.abort(
|
|
62
|
-
new DeadlineExceededError({
|
|
63
|
-
deadlineMs: milliseconds(deadline),
|
|
64
|
-
flushBufferMs,
|
|
65
|
-
remainingMs: milliseconds(remaining),
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
}, deadline);
|
|
69
|
-
|
|
70
|
-
// `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
|
|
71
|
-
// or an external abort signal fires — strictly more reliable than try/finally.
|
|
72
|
-
using _timer = {
|
|
73
|
-
[Symbol.dispose]() {
|
|
74
|
-
clearTimeout(timeoutId);
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
|
|
79
|
-
const request = args.request as { signal?: AbortSignal } | undefined;
|
|
80
|
-
const signal = composeSignals(request?.signal, controller.signal);
|
|
81
|
-
const result = await next({
|
|
82
|
-
...args,
|
|
83
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
|
|
84
|
-
request: { ...(args.request as object), signal },
|
|
85
|
-
});
|
|
86
|
-
return result;
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
step: "finalizeRequest",
|
|
90
|
-
name: "deadlineMiddleware",
|
|
91
|
-
override: true,
|
|
12
|
+
import { getDeadlineSignal } from "./context-store.js";
|
|
13
|
+
|
|
14
|
+
export const deadlineMiddleware = <Input extends object, Output extends object>(): Pluggable<
|
|
15
|
+
Input,
|
|
16
|
+
Output
|
|
17
|
+
> => ({
|
|
18
|
+
applyToStack(stack) {
|
|
19
|
+
stack.add(
|
|
20
|
+
(
|
|
21
|
+
next: FinalizeHandler<Input, Output>,
|
|
22
|
+
_context: HandlerExecutionContext,
|
|
23
|
+
): FinalizeHandler<Input, Output> =>
|
|
24
|
+
async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {
|
|
25
|
+
const deadlineSignal = getDeadlineSignal();
|
|
26
|
+
if (deadlineSignal === undefined) return next(args);
|
|
27
|
+
|
|
28
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object
|
|
29
|
+
const request = args.request as { signal?: AbortSignal } | undefined;
|
|
30
|
+
const existing = request?.signal;
|
|
31
|
+
const signal = existing ? AbortSignal.any([existing, deadlineSignal]) : deadlineSignal;
|
|
32
|
+
|
|
33
|
+
return next({
|
|
34
|
+
...args,
|
|
35
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
|
|
36
|
+
request: { ...(args.request as object), signal },
|
|
37
|
+
});
|
|
92
38
|
},
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
39
|
+
{
|
|
40
|
+
step: "finalizeRequest",
|
|
41
|
+
name: "deadlineMiddleware",
|
|
42
|
+
override: true,
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
// Branded type prevents interchange errors at compile time (e.g. passing seconds where milliseconds are expected).
|
|
5
|
-
// Zero runtime cost. Smart constructor below validates at the boundary and brands the value.
|
|
6
|
-
declare const BrandSymbol: unique symbol;
|
|
7
|
-
|
|
8
|
-
type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
|
|
9
|
-
|
|
10
|
-
export type Milliseconds = Brand<number, "Milliseconds">;
|
|
11
|
-
|
|
12
|
-
export const milliseconds = (value: number): Milliseconds => {
|
|
13
|
-
if (!Number.isFinite(value)) {
|
|
14
|
-
throw new TypeError(`milliseconds value must be finite, received: ${value}`);
|
|
15
|
-
}
|
|
16
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
17
|
-
return value as Milliseconds;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
4
|
export interface DeadlineOptions {
|
|
21
5
|
readonly flushBufferMs?: number;
|
|
22
6
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { LambdaContextLike } from "./context-store.js";
|
|
2
|
-
type AsyncHandler<
|
|
3
|
-
TEvent,
|
|
4
|
-
TContext extends LambdaContextLike,
|
|
5
|
-
TResult
|
|
6
|
-
> = (event: TEvent, context: TContext) => Promise<TResult>;
|
|
7
|
-
export declare const withLambdaDeadline: <
|
|
8
|
-
TEvent,
|
|
9
|
-
TContext extends LambdaContextLike,
|
|
10
|
-
TResult
|
|
11
|
-
>(handler: AsyncHandler<TEvent, TContext, TResult>) => AsyncHandler<TEvent, TContext, TResult>;
|
|
12
|
-
export {};
|
|
13
|
-
|
|
14
|
-
//# sourceMappingURL=handler-wrapper.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"mappings":"AAKA,cAAc,yBAAyB;KAElC;CAAa;CAAQ,iBAAiB;CAAmB;KAC5D,OAAO,QACP,SAAS,aACN,QAAQ;AAEb,OAAO,cAAM;CACV;CAAQ,iBAAiB;CAAmB;EAC3C,SAAS,aAAa,QAAQ,UAAU,aACvC,aAAa,QAAQ,UAAU","names":[],"sources":["src/handler-wrapper.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { run } from \"./context-store.js\";\n\nimport type { LambdaContextLike } from \"./context-store.js\";\n\ntype AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (\n event: TEvent,\n context: TContext,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TContext extends LambdaContextLike, TResult>(\n handler: AsyncHandler<TEvent, TContext, TResult>,\n ): AsyncHandler<TEvent, TContext, TResult> =>\n async (event: TEvent, context: TContext): Promise<TResult> =>\n run(context, async () => handler(event, context));\n"]}
|
package/dist/handler-wrapper.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { run } from "./context-store.js";
|
|
4
|
-
export const withLambdaDeadline = (handler) => async (event, context) => run(context, async () => handler(event, context));
|
|
5
|
-
|
|
6
|
-
//# sourceMappingURL=handler-wrapper.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"mappings":";;AAGA,SAAS,WAAW;AASpB,OAAO,MAAM,sBAET,YAEF,OAAO,OAAe,YACpB,IAAI,SAAS,YAAY,QAAQ,OAAO,OAAO,CAAC","names":[],"sources":["src/handler-wrapper.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { run } from \"./context-store.js\";\n\nimport type { LambdaContextLike } from \"./context-store.js\";\n\ntype AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (\n event: TEvent,\n context: TContext,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TContext extends LambdaContextLike, TResult>(\n handler: AsyncHandler<TEvent, TContext, TResult>,\n ): AsyncHandler<TEvent, TContext, TResult> =>\n async (event: TEvent, context: TContext): Promise<TResult> =>\n run(context, async () => handler(event, context));\n"]}
|
package/src/handler-wrapper.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
import { run } from "./context-store.js";
|
|
5
|
-
|
|
6
|
-
import type { LambdaContextLike } from "./context-store.js";
|
|
7
|
-
|
|
8
|
-
type AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (
|
|
9
|
-
event: TEvent,
|
|
10
|
-
context: TContext,
|
|
11
|
-
) => Promise<TResult>;
|
|
12
|
-
|
|
13
|
-
export const withLambdaDeadline =
|
|
14
|
-
<TEvent, TContext extends LambdaContextLike, TResult>(
|
|
15
|
-
handler: AsyncHandler<TEvent, TContext, TResult>,
|
|
16
|
-
): AsyncHandler<TEvent, TContext, TResult> =>
|
|
17
|
-
async (event: TEvent, context: TContext): Promise<TResult> =>
|
|
18
|
-
run(context, async () => handler(event, context));
|