lambda-deadline-middleware 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +9 -23
  2. package/dist/context-store.d.ts.map +1 -1
  3. package/dist/context-store.js +4 -12
  4. package/dist/context-store.js.map +1 -1
  5. package/dist/error.d.ts +3 -3
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/error.js.map +1 -1
  8. package/dist/handler-wrapper.d.ts +4 -3
  9. package/dist/handler-wrapper.d.ts.map +1 -1
  10. package/dist/handler-wrapper.js +1 -1
  11. package/dist/handler-wrapper.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/middleware.d.ts +4 -10
  17. package/dist/middleware.d.ts.map +1 -1
  18. package/dist/middleware.js +39 -53
  19. package/dist/middleware.js.map +1 -1
  20. package/dist/types.d.ts +0 -18
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/types.js +0 -10
  23. package/dist/types.js.map +1 -1
  24. package/package.json +4 -4
  25. package/src/context-store.ts +5 -19
  26. package/src/error.ts +3 -3
  27. package/src/handler-wrapper.ts +7 -8
  28. package/src/index.ts +2 -9
  29. package/src/middleware.ts +72 -87
  30. package/src/types.ts +2 -32
  31. package/dist/config.d.ts +0 -4
  32. package/dist/config.d.ts.map +0 -1
  33. package/dist/config.js +0 -17
  34. package/dist/config.js.map +0 -1
  35. package/dist/registration.d.ts +0 -8
  36. package/dist/registration.d.ts.map +0 -1
  37. package/dist/registration.js +0 -17
  38. package/dist/registration.js.map +0 -1
  39. package/dist/telemetry.d.ts +0 -5
  40. package/dist/telemetry.d.ts.map +0 -1
  41. package/dist/telemetry.js +0 -82
  42. package/dist/telemetry.js.map +0 -1
  43. package/src/config.ts +0 -18
  44. package/src/registration.ts +0 -27
  45. package/src/telemetry.ts +0 -132
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
  Zero-dependency AWS SDK v3 middleware that automatically propagates Lambda execution deadlines to outgoing SDK calls via
7
7
  `AbortController`-based timeouts.
8
8
 
9
- When an AWS SDK call hangs inside a Lambda function, the runtime terminates the process at the configured timeout,
10
- destroying in-flight OpenTelemetry/X-Ray spans without export. This library prevents that by computing per-request
9
+ When an AWS SDK call hangs inside a Lambda function, the runtime terminates the process at the configured timeout
10
+ without throwing an error or giving your code a chance to react. This library prevents that by computing per-request
11
11
  deadlines from the Lambda's remaining execution time and aborting requests before the hard timeout fires.
12
12
 
13
13
  ## Features
@@ -17,7 +17,6 @@ deadlines from the Lambda's remaining execution time and aborting requests befor
17
17
  - Signal composition: preserves caller-provided `AbortSignal` via `AbortSignal.any()`
18
18
  - Zero runtime dependencies (`@smithy/types` is compile-time only)
19
19
  - Complete no-op when no Lambda context is available
20
- - Optional OpenTelemetry span events on deadline aborts (detected dynamically)
21
20
  - Branded types prevent millisecond/buffer interchange at compile time
22
21
 
23
22
  ## Requirements
@@ -90,7 +89,7 @@ flowchart LR
90
89
 
91
90
  ### Flush Buffer
92
91
 
93
- The flush buffer is subtracted from the remaining Lambda time to leave room for telemetry export and error handling:
92
+ The flush buffer is subtracted from the remaining Lambda time to leave room for graceful shutdown and error handling:
94
93
 
95
94
  ```typescript
96
95
  // Default: 1000ms
@@ -100,14 +99,6 @@ dynamodb.middlewareStack.use(deadlineMiddleware());
100
99
  dynamodb.middlewareStack.use(deadlineMiddleware({ flushBufferMs: 500 }));
101
100
  ```
102
101
 
103
- ### Telemetry
104
-
105
- If `@opentelemetry/api` is installed, span events are emitted on deadline aborts. Disable with:
106
-
107
- ```typescript
108
- dynamodb.middlewareStack.use(deadlineMiddleware({ telemetryEnabled: false }));
109
- ```
110
-
111
102
  ## Error Handling
112
103
 
113
104
  When remaining time is less than or equal to the flush buffer, the middleware throws `DeadlineExceededError` immediately
@@ -151,7 +142,7 @@ await dynamodb.send(
151
142
 
152
143
  ## API Reference
153
144
 
154
- ### `withLambdaDeadline(handler, options?)`
145
+ ### `withLambdaDeadline(handler)`
155
146
 
156
147
  Wraps a Lambda handler to store the Lambda context in `AsyncLocalStorage`. Required for the middleware to access
157
148
  `getRemainingTimeInMillis()`.
@@ -159,7 +150,6 @@ Wraps a Lambda handler to store the Lambda context in `AsyncLocalStorage`. Requi
159
150
  ```typescript
160
151
  function withLambdaDeadline<TEvent, TResult>(
161
152
  handler: (event: TEvent, context: LambdaContextLike) => Promise<TResult>,
162
- options?: DeadlineOptions,
163
153
  ): (event: TEvent, context: LambdaContextLike) => Promise<TResult>;
164
154
  ```
165
155
 
@@ -193,7 +183,7 @@ function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
193
183
  class DeadlineExceededError extends Error {
194
184
  readonly name: "DeadlineExceededError";
195
185
  readonly deadlineMs: Milliseconds;
196
- readonly flushBufferMs: FlushBufferMs;
186
+ readonly flushBufferMs: Milliseconds;
197
187
  readonly remainingMs: Milliseconds;
198
188
  }
199
189
  ```
@@ -203,19 +193,15 @@ class DeadlineExceededError extends Error {
203
193
  ```typescript
204
194
  interface DeadlineOptions {
205
195
  readonly flushBufferMs?: number; // Default: 1000
206
- readonly telemetryEnabled?: boolean; // Default: true
207
196
  }
208
197
  ```
209
198
 
210
199
  ### Types
211
200
 
212
- | Type | Description |
213
- | --------------------- | ---------------------------------------------------------------------------- |
214
- | `Milliseconds` | Branded number representing a duration in ms |
215
- | `FlushBufferMs` | Branded number for the flush buffer |
216
- | `RequestDeadlineMs` | Branded number for a computed deadline |
217
- | `DeadlineComputation` | Discriminated union: `"deadline"` \| `"insufficient-time"` \| `"no-context"` |
218
- | `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?(): number }` |
201
+ | Type | Description |
202
+ | ------------------- | ------------------------------------------------------------ |
203
+ | `Milliseconds` | Branded number representing a duration in ms |
204
+ | `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?(): number }` |
219
205
 
220
206
  ## Reporting Bugs
221
207
 
@@ -1 +1 @@
1
- {"mappings":"AAOA,iBAAiB,kBAAkB;CACjC;AACF;AAWA,OAAO,cAAM,MAAO,GAAG,SAAS,sCAAsC,UAAU,MAAI;AAKpF,OAAO,cAAM","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\n// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.\n// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\n// Sentinel allows AsyncLocalStorage.run() to accept null/undefined context\n// without throwing, while the accessor can distinguish \"no context stored\"\n// from \"context present but missing the method\".\nconst NO_CONTEXT: unique symbol = Symbol(\"no-context\");\n\ntype StoreValue = LambdaContextLike | typeof NO_CONTEXT;\n\nconst contextStorage = new AsyncLocalStorage<StoreValue>();\n\nexport const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {\n const value: StoreValue = context ?? NO_CONTEXT;\n return contextStorage.run(value, fn);\n};\n\nexport const getRemainingTimeInMillis = (): number | undefined => {\n const store = contextStorage.getStore();\n\n if (store === undefined || store === NO_CONTEXT) {\n return undefined;\n }\n\n if (typeof store.getRemainingTimeInMillis !== \"function\") {\n return undefined;\n }\n\n return store.getRemainingTimeInMillis();\n};\n"]}
1
+ {"mappings":"AAOA,iBAAiB,kBAAkB;CACjC;AACF;AAIA,OAAO,cAAM,MAAO,GAAG,SAAS,sCAAsC,UAAU,MAAI;AAKpF,OAAO,cAAM","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\n// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.\n// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\nconst contextStorage = new AsyncLocalStorage<LambdaContextLike>();\n\nexport const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {\n if (context === null || context === undefined) return fn();\n return contextStorage.run(context, fn);\n};\n\nexport const getRemainingTimeInMillis = (): number | undefined => {\n const store = contextStorage.getStore();\n if (store === undefined) return undefined;\n if (typeof store.getRemainingTimeInMillis !== \"function\") return undefined;\n return store.getRemainingTimeInMillis();\n};\n"]}
@@ -1,23 +1,15 @@
1
1
  // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- // Sentinel allows AsyncLocalStorage.run() to accept null/undefined context
5
- // without throwing, while the accessor can distinguish "no context stored"
6
- // from "context present but missing the method".
7
- const NO_CONTEXT = Symbol("no-context");
8
4
  const contextStorage = new AsyncLocalStorage();
9
5
  export const run = (context, fn) => {
10
- const value = context ?? NO_CONTEXT;
11
- return contextStorage.run(value, fn);
6
+ if (context === null || context === undefined) return fn();
7
+ return contextStorage.run(context, fn);
12
8
  };
13
9
  export const getRemainingTimeInMillis = () => {
14
10
  const store = contextStorage.getStore();
15
- if (store === undefined || store === NO_CONTEXT) {
16
- return undefined;
17
- }
18
- if (typeof store.getRemainingTimeInMillis !== "function") {
19
- return undefined;
20
- }
11
+ if (store === undefined) return undefined;
12
+ if (typeof store.getRemainingTimeInMillis !== "function") return undefined;
21
13
  return store.getRemainingTimeInMillis();
22
14
  };
23
15
 
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,yBAAyB;;;;AAWlC,MAAM,aAA4B,OAAO,YAAY;AAIrD,MAAM,iBAAiB,IAAI,kBAA8B;AAEzD,OAAO,MAAM,OAAU,SAA+C,OAAmB;CACvF,MAAM,QAAoB,WAAW;CACrC,OAAO,eAAe,IAAI,OAAO,EAAE;AACrC;AAEA,OAAO,MAAM,iCAAqD;CAChE,MAAM,QAAQ,eAAe,SAAS;CAEtC,IAAI,UAAU,aAAa,UAAU,YAAY;EAC/C,OAAO;CACT;CAEA,IAAI,OAAO,MAAM,6BAA6B,YAAY;EACxD,OAAO;CACT;CAEA,OAAO,MAAM,yBAAyB;AACxC","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\n// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.\n// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\n// Sentinel allows AsyncLocalStorage.run() to accept null/undefined context\n// without throwing, while the accessor can distinguish \"no context stored\"\n// from \"context present but missing the method\".\nconst NO_CONTEXT: unique symbol = Symbol(\"no-context\");\n\ntype StoreValue = LambdaContextLike | typeof NO_CONTEXT;\n\nconst contextStorage = new AsyncLocalStorage<StoreValue>();\n\nexport const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {\n const value: StoreValue = context ?? NO_CONTEXT;\n return contextStorage.run(value, fn);\n};\n\nexport const getRemainingTimeInMillis = (): number | undefined => {\n const store = contextStorage.getStore();\n\n if (store === undefined || store === NO_CONTEXT) {\n return undefined;\n }\n\n if (typeof store.getRemainingTimeInMillis !== \"function\") {\n return undefined;\n }\n\n return store.getRemainingTimeInMillis();\n};\n"]}
1
+ {"mappings":";;AAGA,SAAS,yBAAyB;AAQlC,MAAM,iBAAiB,IAAI,kBAAqC;AAEhE,OAAO,MAAM,OAAU,SAA+C,OAAmB;CACvF,IAAI,YAAY,QAAQ,YAAY,WAAW,OAAO,GAAG;CACzD,OAAO,eAAe,IAAI,SAAS,EAAE;AACvC;AAEA,OAAO,MAAM,iCAAqD;CAChE,MAAM,QAAQ,eAAe,SAAS;CACtC,IAAI,UAAU,WAAW,OAAO;CAChC,IAAI,OAAO,MAAM,6BAA6B,YAAY,OAAO;CACjE,OAAO,MAAM,yBAAyB;AACxC","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\n// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.\n// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.\nexport interface LambdaContextLike {\n getRemainingTimeInMillis?: () => number;\n}\n\nconst contextStorage = new AsyncLocalStorage<LambdaContextLike>();\n\nexport const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {\n if (context === null || context === undefined) return fn();\n return contextStorage.run(context, fn);\n};\n\nexport const getRemainingTimeInMillis = (): number | undefined => {\n const store = contextStorage.getStore();\n if (store === undefined) return undefined;\n if (typeof store.getRemainingTimeInMillis !== \"function\") return undefined;\n return store.getRemainingTimeInMillis();\n};\n"]}
package/dist/error.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- import type { FlushBufferMs, Milliseconds } from "./types.js";
1
+ import type { Milliseconds } from "./types.js";
2
2
  interface DeadlineExceededInit {
3
3
  readonly deadlineMs: Milliseconds;
4
- readonly flushBufferMs: FlushBufferMs;
4
+ readonly flushBufferMs: Milliseconds;
5
5
  readonly remainingMs: Milliseconds;
6
6
  }
7
7
  export declare class DeadlineExceededError extends Error {
8
8
  override readonly name = "DeadlineExceededError";
9
9
  readonly deadlineMs: Milliseconds;
10
- readonly flushBufferMs: FlushBufferMs;
10
+ readonly flushBufferMs: Milliseconds;
11
11
  readonly remainingMs: Milliseconds;
12
12
  constructor(init: DeadlineExceededInit);
13
13
  }
@@ -1 +1 @@
1
- {"mappings":"AAGA,cAAc,eAAe,oBAAoB;UAEvC,qBAAqB;UACpB,YAAY;UACZ,eAAe;UACf,aAAa;AACxB;AAEA,OAAO,cAAM,8BAA8B,MAAM;CAC/C,kBAAkB,OAAO;CACzB,SAAS,YAAY;CACrB,SAAS,eAAe;CACxB,SAAS,aAAa;CAEtB,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\nimport type { FlushBufferMs, Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\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"]}
1
+ {"mappings":"AAGA,cAAc,oBAAoB;UAExB,qBAAqB;UACpB,YAAY;UACZ,eAAe;UACf,aAAa;AACxB;AAEA,OAAO,cAAM,8BAA8B,MAAM;CAC/C,kBAAkB,OAAO;CACzB,SAAS,YAAY;CACrB,SAAS,eAAe;CACxB,SAAS,aAAa;CAEtB,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\nimport type { Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\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":"AAWA,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\nimport type { FlushBufferMs, Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: FlushBufferMs;\n readonly remainingMs: Milliseconds;\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"]}
1
+ {"mappings":"AAWA,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\nimport type { Milliseconds } from \"./types.js\";\n\ninterface DeadlineExceededInit {\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\n}\n\nexport class DeadlineExceededError extends Error {\n override readonly name = \"DeadlineExceededError\" as const;\n readonly deadlineMs: Milliseconds;\n readonly flushBufferMs: Milliseconds;\n readonly remainingMs: Milliseconds;\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"]}
@@ -1,13 +1,14 @@
1
1
  import type { LambdaContextLike } from "./context-store.js";
2
- import type { DeadlineOptions } from "./types.js";
3
2
  type AsyncHandler<
4
3
  TEvent,
4
+ TContext extends LambdaContextLike,
5
5
  TResult
6
- > = (event: TEvent, context: LambdaContextLike) => Promise<TResult>;
6
+ > = (event: TEvent, context: TContext) => Promise<TResult>;
7
7
  export declare const withLambdaDeadline: <
8
8
  TEvent,
9
+ TContext extends LambdaContextLike,
9
10
  TResult
10
- >(handler: AsyncHandler<TEvent, TResult>, _options?: DeadlineOptions) => AsyncHandler<TEvent, TResult>;
11
+ >(handler: AsyncHandler<TEvent, TContext, TResult>) => AsyncHandler<TEvent, TContext, TResult>;
11
12
  export {};
12
13
 
13
14
  //# sourceMappingURL=handler-wrapper.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"AAIA,cAAc,yBAAyB;AACvC,cAAc,uBAAuB;KAEhC;CAAa;CAAQ;KACxB,OAAO,QACP,SAAS,sBACN,QAAQ;AAEb,OAAO,cAAM;CACV;CAAQ;EACP,SAAS,aAAa,QAAQ,UAC9B,WAAW,oBACV,aAAa,QAAQ","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\";\nimport type { LambdaContextLike } from \"./context-store.js\";\nimport type { DeadlineOptions } from \"./types.js\";\n\ntype AsyncHandler<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TResult>(\n handler: AsyncHandler<TEvent, TResult>,\n _options?: DeadlineOptions,\n ): AsyncHandler<TEvent, TResult> =>\n async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>\n run(context, async () => handler(event, context));\n"]}
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"]}
@@ -1,6 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { run } from "./context-store.js";
4
- export const withLambdaDeadline = (handler, _options) => async (event, context) => run(context, async () => handler(event, context));
4
+ export const withLambdaDeadline = (handler) => async (event, context) => run(context, async () => handler(event, context));
5
5
 
6
6
  //# sourceMappingURL=handler-wrapper.js.map
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,WAAW;AASpB,OAAO,MAAM,sBAET,SACA,aAEF,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\";\nimport type { LambdaContextLike } from \"./context-store.js\";\nimport type { DeadlineOptions } from \"./types.js\";\n\ntype AsyncHandler<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TResult>(\n handler: AsyncHandler<TEvent, TResult>,\n _options?: DeadlineOptions,\n ): AsyncHandler<TEvent, TResult> =>\n async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>\n run(context, async () => handler(event, context));\n"]}
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/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { withLambdaDeadline } from "./handler-wrapper.js";
2
- export { deadlineMiddleware } from "./registration.js";
2
+ export { deadlineMiddleware } from "./middleware.js";
3
3
  export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
4
4
  export { getRemainingTimeInMillis } from "./context-store.js";
5
- export type { Milliseconds, FlushBufferMs, RequestDeadlineMs, DeadlineComputation, DeadlineMiddlewareConfig, DeadlineOptions } from "./types.js";
5
+ export type { Milliseconds, DeadlineOptions } from "./types.js";
6
6
  export type { LambdaContextLike } from "./context-store.js";
7
7
 
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"AAGA,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,gCAAgC;AAEzC,cACE,cACA,eACA,mBACA,qBACA,0BACA,uBACK;AAEP,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 } from \"./handler-wrapper.js\";\nexport { deadlineMiddleware } from \"./registration.js\";\nexport { DeadlineExceededError, isDeadlineExceeded } from \"./error.js\";\nexport { getRemainingTimeInMillis } from \"./context-store.js\";\n\nexport type {\n Milliseconds,\n FlushBufferMs,\n RequestDeadlineMs,\n DeadlineComputation,\n DeadlineMiddlewareConfig,\n DeadlineOptions,\n} from \"./types.js\";\n\nexport type { LambdaContextLike } from \"./context-store.js\";\n"]}
1
+ {"mappings":"AAGA,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,gCAAgC;AAEzC,cAAc,cAAc,uBAAuB;AAEnD,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 } from \"./handler-wrapper.js\";\nexport { deadlineMiddleware } from \"./middleware.js\";\nexport { DeadlineExceededError, isDeadlineExceeded } from \"./error.js\";\nexport { getRemainingTimeInMillis } from \"./context-store.js\";\n\nexport type { Milliseconds, DeadlineOptions } from \"./types.js\";\n\nexport type { LambdaContextLike } from \"./context-store.js\";\n"]}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  export { withLambdaDeadline } from "./handler-wrapper.js";
4
- export { deadlineMiddleware } from "./registration.js";
4
+ export { deadlineMiddleware } from "./middleware.js";
5
5
  export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
6
6
  export { getRemainingTimeInMillis } from "./context-store.js";
7
7
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,gCAAgC","names":[],"sources":["src/index.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport { withLambdaDeadline } from \"./handler-wrapper.js\";\nexport { deadlineMiddleware } from \"./registration.js\";\nexport { DeadlineExceededError, isDeadlineExceeded } from \"./error.js\";\nexport { getRemainingTimeInMillis } from \"./context-store.js\";\n\nexport type {\n Milliseconds,\n FlushBufferMs,\n RequestDeadlineMs,\n DeadlineComputation,\n DeadlineMiddlewareConfig,\n DeadlineOptions,\n} from \"./types.js\";\n\nexport type { LambdaContextLike } from \"./context-store.js\";\n"]}
1
+ {"mappings":";;AAGA,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,gCAAgC","names":[],"sources":["src/index.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport { withLambdaDeadline } from \"./handler-wrapper.js\";\nexport { deadlineMiddleware } from \"./middleware.js\";\nexport { DeadlineExceededError, isDeadlineExceeded } from \"./error.js\";\nexport { getRemainingTimeInMillis } from \"./context-store.js\";\n\nexport type { Milliseconds, DeadlineOptions } from \"./types.js\";\n\nexport type { LambdaContextLike } from \"./context-store.js\";\n"]}
@@ -1,15 +1,9 @@
1
- import type { DeadlineComputation, DeadlineMiddlewareConfig, RequestDeadlineMs } from "./types.js";
2
- import type { FinalizeRequestMiddleware } from "@smithy/types";
3
- export declare const computeDeadline: (config: DeadlineMiddlewareConfig) => DeadlineComputation;
4
- export interface DeadlineTimer {
5
- readonly controller: AbortController;
6
- [Symbol.dispose]: () => void;
7
- }
8
- export declare const createDeadlineTimer: (deadlineMs: RequestDeadlineMs, config: DeadlineMiddlewareConfig) => DeadlineTimer;
1
+ import type { Pluggable } from "@smithy/types";
2
+ import type { DeadlineOptions } from "./types.js";
9
3
  export declare const composeSignals: (existing: AbortSignal | undefined, deadline: AbortSignal) => AbortSignal;
10
- export declare const deadlineMiddlewareHandler: <
4
+ export declare const deadlineMiddleware: <
11
5
  Input extends object,
12
6
  Output extends object
13
- >(config: DeadlineMiddlewareConfig) => FinalizeRequestMiddleware<Input, Output>;
7
+ >(options?: DeadlineOptions) => Pluggable<Input, Output>;
14
8
 
15
9
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"AAKA,cAAc,qBAAqB,0BAA0B,yBAAyB;AAGtF,cAIE,iCAEK;AAEP,OAAO,cAAM,kBAAmB,QAAQ,6BAA2B;AAqBnE,iBAAiB,cAAc;UACpB,YAAY;EACpB,OAAO;AACV;AAEA,OAAO,cAAM,sBACX,YAAY,mBACZ,QAAQ,6BACP;AAmBH,OAAO,cAAM,iBACX,UAAU,yBACV,UAAU,gBACT;AAKH,OAAO,cAAM;CACV;CAAsB;EACrB,QAAQ,6BACP,0BAA0B,OAAO","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { getRemainingTimeInMillis } from \"./context-store.js\";\nimport { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineComputation, DeadlineMiddlewareConfig, RequestDeadlineMs } from \"./types.js\";\nimport { milliseconds } from \"./types.js\";\n\nimport type {\n FinalizeHandler,\n FinalizeHandlerArguments,\n FinalizeHandlerOutput,\n FinalizeRequestMiddleware,\n HandlerExecutionContext,\n} from \"@smithy/types\";\n\nexport const computeDeadline = (config: DeadlineMiddlewareConfig): DeadlineComputation => {\n const remaining = getRemainingTimeInMillis();\n\n if (remaining === undefined) {\n return { kind: \"no-context\" };\n }\n\n const deadline = remaining - config.flushBufferMs;\n\n if (deadline <= 0) {\n return {\n kind: \"insufficient-time\",\n remaining: milliseconds(remaining),\n buffer: config.flushBufferMs,\n };\n }\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above\n return { kind: \"deadline\", value: deadline as RequestDeadlineMs };\n};\n\nexport interface DeadlineTimer {\n readonly controller: AbortController;\n [Symbol.dispose]: () => void;\n}\n\nexport const createDeadlineTimer = (\n deadlineMs: RequestDeadlineMs,\n config: DeadlineMiddlewareConfig,\n): DeadlineTimer => {\n const controller = new AbortController();\n const remaining = milliseconds(deadlineMs + config.flushBufferMs);\n const error = new DeadlineExceededError({\n deadlineMs: milliseconds(deadlineMs),\n flushBufferMs: config.flushBufferMs,\n remainingMs: remaining,\n });\n const timeoutId = setTimeout(() => {\n controller.abort(error);\n }, deadlineMs);\n return {\n controller,\n [Symbol.dispose]() {\n clearTimeout(timeoutId);\n },\n };\n};\n\nexport const composeSignals = (\n existing: AbortSignal | undefined,\n deadline: AbortSignal,\n): AbortSignal => {\n if (existing === undefined) return deadline;\n return AbortSignal.any([existing, deadline]);\n};\n\nexport const deadlineMiddlewareHandler =\n <Input extends object, Output extends object>(\n config: DeadlineMiddlewareConfig,\n ): FinalizeRequestMiddleware<Input, Output> =>\n (\n next: FinalizeHandler<Input, Output>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<Input, Output> =>\n // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union\n async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {\n const computation = computeDeadline(config);\n\n switch (computation.kind) {\n case \"no-context\":\n return next(args);\n\n case \"insufficient-time\":\n throw new DeadlineExceededError({\n deadlineMs: milliseconds(0),\n flushBufferMs: computation.buffer,\n remainingMs: computation.remaining,\n });\n\n case \"deadline\": {\n // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,\n // or an external abort signal fires — strictly more reliable than try/finally.\n using timer = createDeadlineTimer(computation.value, config);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property\n const request = args.request as { signal?: AbortSignal } | undefined;\n const signal = composeSignals(request?.signal, timer.controller.signal);\n const result = await 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 return result;\n }\n }\n };\n"]}
1
+ {"mappings":"AAGA,cAKE,iBACK;AAMP,cAAc,uBAAuB;AAErC,OAAO,cAAM,iBACX,UAAU,yBACV,UAAU,gBACT;AAKH,OAAO,cAAM;CAAsB;CAAsB;EACvD,UAAU,oBACT,UAAU,OAAO","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 { getRemainingTimeInMillis } from \"./context-store.js\";\nimport { DeadlineExceededError } from \"./error.js\";\nimport { milliseconds } from \"./types.js\";\n\nimport type { DeadlineOptions } from \"./types.js\";\n\nexport const composeSignals = (\n existing: AbortSignal | undefined,\n deadline: AbortSignal,\n): AbortSignal => {\n if (existing === undefined) return deadline;\n return AbortSignal.any([existing, deadline]);\n};\n\nexport const deadlineMiddleware = <Input extends object, Output extends object>(\n options?: DeadlineOptions,\n): Pluggable<Input, Output> => {\n const raw = options?.flushBufferMs ?? 1000;\n if (raw < 0) {\n throw new TypeError(`flushBufferMs option must be non-negative, received: ${raw}`);\n }\n const flushBufferMs = milliseconds(raw);\n\n return {\n applyToStack(stack) {\n // Registered at \"finalizeRequest\" (attempt level) rather than API-call level so each retry gets a deadline\n // computed from the actual remaining time at that moment. API-call level would cache a stale deadline\n // across retries, which grow more dangerous after backoff delays eat into remaining time.\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 remaining = getRemainingTimeInMillis();\n if (remaining === undefined) return next(args);\n\n const deadline = remaining - flushBufferMs;\n\n if (deadline <= 0) {\n throw new DeadlineExceededError({\n deadlineMs: milliseconds(0),\n flushBufferMs,\n remainingMs: milliseconds(remaining),\n });\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort(\n new DeadlineExceededError({\n deadlineMs: milliseconds(deadline),\n flushBufferMs,\n remainingMs: milliseconds(remaining),\n }),\n );\n }, deadline);\n\n // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,\n // or an external abort signal fires — strictly more reliable than try/finally.\n using _timer = {\n [Symbol.dispose]() {\n clearTimeout(timeoutId);\n },\n };\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property\n const request = args.request as { signal?: AbortSignal } | undefined;\n const signal = composeSignals(request?.signal, controller.signal);\n const result = await 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 return result;\n },\n {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n },\n );\n },\n };\n};\n"]}
@@ -1,65 +1,47 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
1
  import { getRemainingTimeInMillis } from "./context-store.js";
4
2
  import { DeadlineExceededError } from "./error.js";
5
3
  import { milliseconds } from "./types.js";
6
- export const computeDeadline = (config) => {
7
- const remaining = getRemainingTimeInMillis();
8
- if (remaining === undefined) {
9
- return { kind: "no-context" };
10
- }
11
- const deadline = remaining - config.flushBufferMs;
12
- if (deadline <= 0) {
13
- return {
14
- kind: "insufficient-time",
15
- remaining: milliseconds(remaining),
16
- buffer: config.flushBufferMs
17
- };
18
- }
19
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above
20
- return {
21
- kind: "deadline",
22
- value: deadline
23
- };
24
- };
25
- export const createDeadlineTimer = (deadlineMs, config) => {
26
- const controller = new AbortController();
27
- const remaining = milliseconds(deadlineMs + config.flushBufferMs);
28
- const error = new DeadlineExceededError({
29
- deadlineMs: milliseconds(deadlineMs),
30
- flushBufferMs: config.flushBufferMs,
31
- remainingMs: remaining
32
- });
33
- const timeoutId = setTimeout(() => {
34
- controller.abort(error);
35
- }, deadlineMs);
36
- return {
37
- controller,
38
- [Symbol.dispose]() {
39
- clearTimeout(timeoutId);
40
- }
41
- };
42
- };
43
4
  export const composeSignals = (existing, deadline) => {
44
5
  if (existing === undefined) return deadline;
45
6
  return AbortSignal.any([existing, deadline]);
46
7
  };
47
- export const deadlineMiddlewareHandler = (config) => (next, _context) => async (args) => {
48
- const computation = computeDeadline(config);
49
- switch (computation.kind) {
50
- case "no-context": return next(args);
51
- case "insufficient-time": throw new DeadlineExceededError({
52
- deadlineMs: milliseconds(0),
53
- flushBufferMs: computation.buffer,
54
- remainingMs: computation.remaining
55
- });
56
- case "deadline": {
8
+ export const deadlineMiddleware = (options) => {
9
+ const raw = options?.flushBufferMs ?? 1e3;
10
+ if (raw < 0) {
11
+ throw new TypeError(`flushBufferMs option must be non-negative, received: ${raw}`);
12
+ }
13
+ const flushBufferMs = milliseconds(raw);
14
+ return { applyToStack(stack) {
15
+ // Registered at "finalizeRequest" (attempt level) rather than API-call level so each retry gets a deadline
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
+ });
28
+ }
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);
57
37
  // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
58
38
  // or an external abort signal fires — strictly more reliable than try/finally.
59
- using timer = createDeadlineTimer(computation.value, config);
39
+ using _timer = { [Symbol.dispose]() {
40
+ clearTimeout(timeoutId);
41
+ } };
60
42
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
61
43
  const request = args.request;
62
- const signal = composeSignals(request?.signal, timer.controller.signal);
44
+ const signal = composeSignals(request?.signal, controller.signal);
63
45
  const result = await next({
64
46
  ...args,
65
47
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
@@ -69,8 +51,12 @@ export const deadlineMiddlewareHandler = (config) => (next, _context) => async (
69
51
  }
70
52
  });
71
53
  return result;
72
- }
73
- }
54
+ }, {
55
+ step: "finalizeRequest",
56
+ name: "deadlineMiddleware",
57
+ override: true
58
+ });
59
+ } };
74
60
  };
75
61
 
76
62
  //# sourceMappingURL=middleware.js.map
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,gCAAgC;AACzC,SAAS,6BAA6B;AAEtC,SAAS,oBAAoB;AAU7B,OAAO,MAAM,mBAAmB,WAA0D;CACxF,MAAM,YAAY,yBAAyB;CAE3C,IAAI,cAAc,WAAW;EAC3B,OAAO,EAAE,MAAM,aAAa;CAC9B;CAEA,MAAM,WAAW,YAAY,OAAO;CAEpC,IAAI,YAAY,GAAG;EACjB,OAAO;GACL,MAAM;GACN,WAAW,aAAa,SAAS;GACjC,QAAQ,OAAO;EACjB;CACF;;CAGA,OAAO;EAAE,MAAM;EAAY,OAAO;CAA8B;AAClE;AAOA,OAAO,MAAM,uBACX,YACA,WACkB;CAClB,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,YAAY,aAAa,aAAa,OAAO,aAAa;CAChE,MAAM,QAAQ,IAAI,sBAAsB;EACtC,YAAY,aAAa,UAAU;EACnC,eAAe,OAAO;EACtB,aAAa;CACf,CAAC;CACD,MAAM,YAAY,iBAAiB;EACjC,WAAW,MAAM,KAAK;CACxB,GAAG,UAAU;CACb,OAAO;EACL;EACA,CAAC,OAAO,WAAW;GACjB,aAAa,SAAS;EACxB;CACF;AACF;AAEA,OAAO,MAAM,kBACX,UACA,aACgB;CAChB,IAAI,aAAa,WAAW,OAAO;CACnC,OAAO,YAAY,IAAI,CAAC,UAAU,QAAQ,CAAC;AAC7C;AAEA,OAAO,MAAM,6BAET,YAGA,MACA,aAGF,OAAO,SAAkF;CACvF,MAAM,cAAc,gBAAgB,MAAM;CAE1C,QAAQ,YAAY,MAApB;EACE,KAAK,cACH,OAAO,KAAK,IAAI;EAElB,KAAK,qBACH,MAAM,IAAI,sBAAsB;GAC9B,YAAY,aAAa,CAAC;GAC1B,eAAe,YAAY;GAC3B,aAAa,YAAY;EAC3B,CAAC;EAEH,KAAK,YAAY;;;GAGf,MAAM,QAAQ,oBAAoB,YAAY,OAAO,MAAM;;GAE3D,MAAM,UAAU,KAAK;GACrB,MAAM,SAAS,eAAe,SAAS,QAAQ,MAAM,WAAW,MAAM;GACtE,MAAM,SAAS,MAAM,KAAK;IACxB,GAAG;;IAEH,SAAS;KAAE,GAAI,KAAK;KAAoB;IAAO;GACjD,CAAC;GACD,OAAO;EACT;CACF;AACF","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { getRemainingTimeInMillis } from \"./context-store.js\";\nimport { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineComputation, DeadlineMiddlewareConfig, RequestDeadlineMs } from \"./types.js\";\nimport { milliseconds } from \"./types.js\";\n\nimport type {\n FinalizeHandler,\n FinalizeHandlerArguments,\n FinalizeHandlerOutput,\n FinalizeRequestMiddleware,\n HandlerExecutionContext,\n} from \"@smithy/types\";\n\nexport const computeDeadline = (config: DeadlineMiddlewareConfig): DeadlineComputation => {\n const remaining = getRemainingTimeInMillis();\n\n if (remaining === undefined) {\n return { kind: \"no-context\" };\n }\n\n const deadline = remaining - config.flushBufferMs;\n\n if (deadline <= 0) {\n return {\n kind: \"insufficient-time\",\n remaining: milliseconds(remaining),\n buffer: config.flushBufferMs,\n };\n }\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above\n return { kind: \"deadline\", value: deadline as RequestDeadlineMs };\n};\n\nexport interface DeadlineTimer {\n readonly controller: AbortController;\n [Symbol.dispose]: () => void;\n}\n\nexport const createDeadlineTimer = (\n deadlineMs: RequestDeadlineMs,\n config: DeadlineMiddlewareConfig,\n): DeadlineTimer => {\n const controller = new AbortController();\n const remaining = milliseconds(deadlineMs + config.flushBufferMs);\n const error = new DeadlineExceededError({\n deadlineMs: milliseconds(deadlineMs),\n flushBufferMs: config.flushBufferMs,\n remainingMs: remaining,\n });\n const timeoutId = setTimeout(() => {\n controller.abort(error);\n }, deadlineMs);\n return {\n controller,\n [Symbol.dispose]() {\n clearTimeout(timeoutId);\n },\n };\n};\n\nexport const composeSignals = (\n existing: AbortSignal | undefined,\n deadline: AbortSignal,\n): AbortSignal => {\n if (existing === undefined) return deadline;\n return AbortSignal.any([existing, deadline]);\n};\n\nexport const deadlineMiddlewareHandler =\n <Input extends object, Output extends object>(\n config: DeadlineMiddlewareConfig,\n ): FinalizeRequestMiddleware<Input, Output> =>\n (\n next: FinalizeHandler<Input, Output>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<Input, Output> =>\n // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union\n async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {\n const computation = computeDeadline(config);\n\n switch (computation.kind) {\n case \"no-context\":\n return next(args);\n\n case \"insufficient-time\":\n throw new DeadlineExceededError({\n deadlineMs: milliseconds(0),\n flushBufferMs: computation.buffer,\n remainingMs: computation.remaining,\n });\n\n case \"deadline\": {\n // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,\n // or an external abort signal fires — strictly more reliable than try/finally.\n using timer = createDeadlineTimer(computation.value, config);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property\n const request = args.request as { signal?: AbortSignal } | undefined;\n const signal = composeSignals(request?.signal, timer.controller.signal);\n const result = await 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 return result;\n }\n }\n };\n"]}
1
+ {"mappings":"AAWA,SAAS,gCAAgC;AACzC,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAI7B,OAAO,MAAM,kBACX,UACA,aACgB;CAChB,IAAI,aAAa,WAAW,OAAO;CACnC,OAAO,YAAY,IAAI,CAAC,UAAU,QAAQ,CAAC;AAC7C;AAEA,OAAO,MAAM,sBACX,YAC6B;CAC7B,MAAM,MAAM,SAAS,iBAAiB;CACtC,IAAI,MAAM,GAAG;EACX,MAAM,IAAI,UAAU,wDAAwD,KAAK;CACnF;CACA,MAAM,gBAAgB,aAAa,GAAG;CAEtC,OAAO,EACL,aAAa,OAAO;;;;EAIlB,MAAM,KAEF,MACA,aAEA,OAAO,SAAkF;GACvF,MAAM,YAAY,yBAAyB;GAC3C,IAAI,cAAc,WAAW,OAAO,KAAK,IAAI;GAE7C,MAAM,WAAW,YAAY;GAE7B,IAAI,YAAY,GAAG;IACjB,MAAM,IAAI,sBAAsB;KAC9B,YAAY,aAAa,CAAC;KAC1B;KACA,aAAa,aAAa,SAAS;IACrC,CAAC;GACH;GAEA,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,YAAY,iBAAiB;IACjC,WAAW,MACT,IAAI,sBAAsB;KACxB,YAAY,aAAa,QAAQ;KACjC;KACA,aAAa,aAAa,SAAS;IACrC,CAAC,CACH;GACF,GAAG,QAAQ;;;GAIX,MAAM,SAAS,EACb,CAAC,OAAO,WAAW;IACjB,aAAa,SAAS;GACxB,EACF;;GAGA,MAAM,UAAU,KAAK;GACrB,MAAM,SAAS,eAAe,SAAS,QAAQ,WAAW,MAAM;GAChE,MAAM,SAAS,MAAM,KAAK;IACxB,GAAG;;IAEH,SAAS;KAAE,GAAI,KAAK;KAAoB;IAAO;GACjD,CAAC;GACD,OAAO;EACT,GACF;GACE,MAAM;GACN,MAAM;GACN,UAAU;EACZ,CACF;CACF,EACF;AACF","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 { getRemainingTimeInMillis } from \"./context-store.js\";\nimport { DeadlineExceededError } from \"./error.js\";\nimport { milliseconds } from \"./types.js\";\n\nimport type { DeadlineOptions } from \"./types.js\";\n\nexport const composeSignals = (\n existing: AbortSignal | undefined,\n deadline: AbortSignal,\n): AbortSignal => {\n if (existing === undefined) return deadline;\n return AbortSignal.any([existing, deadline]);\n};\n\nexport const deadlineMiddleware = <Input extends object, Output extends object>(\n options?: DeadlineOptions,\n): Pluggable<Input, Output> => {\n const raw = options?.flushBufferMs ?? 1000;\n if (raw < 0) {\n throw new TypeError(`flushBufferMs option must be non-negative, received: ${raw}`);\n }\n const flushBufferMs = milliseconds(raw);\n\n return {\n applyToStack(stack) {\n // Registered at \"finalizeRequest\" (attempt level) rather than API-call level so each retry gets a deadline\n // computed from the actual remaining time at that moment. API-call level would cache a stale deadline\n // across retries, which grow more dangerous after backoff delays eat into remaining time.\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 remaining = getRemainingTimeInMillis();\n if (remaining === undefined) return next(args);\n\n const deadline = remaining - flushBufferMs;\n\n if (deadline <= 0) {\n throw new DeadlineExceededError({\n deadlineMs: milliseconds(0),\n flushBufferMs,\n remainingMs: milliseconds(remaining),\n });\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort(\n new DeadlineExceededError({\n deadlineMs: milliseconds(deadline),\n flushBufferMs,\n remainingMs: milliseconds(remaining),\n }),\n );\n }, deadline);\n\n // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,\n // or an external abort signal fires — strictly more reliable than try/finally.\n using _timer = {\n [Symbol.dispose]() {\n clearTimeout(timeoutId);\n },\n };\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property\n const request = args.request as { signal?: AbortSignal } | undefined;\n const signal = composeSignals(request?.signal, controller.signal);\n const result = await 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 return result;\n },\n {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n },\n );\n },\n };\n};\n"]}
package/dist/types.d.ts CHANGED
@@ -6,27 +6,9 @@ type Brand<
6
6
  readonly [BrandSymbol]: B;
7
7
  };
8
8
  export type Milliseconds = Brand<number, "Milliseconds">;
9
- export type FlushBufferMs = Brand<number, "FlushBufferMs">;
10
- export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
11
9
  export declare const milliseconds: (value: number) => Milliseconds;
12
- export declare const flushBufferMs: (value: number) => FlushBufferMs;
13
- export type DeadlineComputation = {
14
- readonly kind: "deadline";
15
- readonly value: RequestDeadlineMs;
16
- } | {
17
- readonly kind: "insufficient-time";
18
- readonly remaining: Milliseconds;
19
- readonly buffer: FlushBufferMs;
20
- } | {
21
- readonly kind: "no-context";
22
- };
23
- export interface DeadlineMiddlewareConfig {
24
- readonly flushBufferMs: FlushBufferMs;
25
- readonly telemetryEnabled: boolean;
26
- }
27
10
  export interface DeadlineOptions {
28
11
  readonly flushBufferMs?: number;
29
- readonly telemetryEnabled?: boolean;
30
12
  }
31
13
  export {};
32
14
 
@@ -1 +1 @@
1
- {"mappings":"AAKA,cAAc;KAET;CAAM;CAAG;IAAoB,IAAI;WAAY,cAAc;AAAE;AAElE,YAAY,eAAe,cAAc;AAEzC,YAAY,gBAAgB,cAAc;AAE1C,YAAY,oBAAoB,cAAc;AAE9C,OAAO,cAAM,eAAgB,kBAAgB;AAQ7C,OAAO,cAAM,gBAAiB,kBAAgB;AAW9C,YAAY,sBACR;UAAW,MAAM;UAAqB,OAAO;AAAkB,IAC/D;UACW,MAAM;UACN,WAAW;UACX,QAAQ;AACnB,IACA;UAAW,MAAM;AAAa;AAElC,iBAAiB,yBAAyB;UAC/B,eAAe;UACf;AACX;AAEA,iBAAiB,gBAAgB;UACtB;UACA;AACX","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\n// Branded types prevent interchange errors at compile time (e.g. passing seconds where milliseconds are expected).\n// Zero runtime cost. Smart constructors below validate at the boundary and brand the value.\ndeclare const BrandSymbol: unique symbol;\n\ntype Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };\n\nexport type Milliseconds = Brand<number, \"Milliseconds\">;\n\nexport type FlushBufferMs = Brand<number, \"FlushBufferMs\">;\n\nexport type RequestDeadlineMs = Brand<number, \"RequestDeadlineMs\">;\n\nexport const milliseconds = (value: number): Milliseconds => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`milliseconds value must be finite, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as Milliseconds;\n};\n\nexport const flushBufferMs = (value: number): FlushBufferMs => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);\n }\n if (value < 0) {\n throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as FlushBufferMs;\n};\n\nexport type DeadlineComputation =\n | { readonly kind: \"deadline\"; readonly value: RequestDeadlineMs }\n | {\n readonly kind: \"insufficient-time\";\n readonly remaining: Milliseconds;\n readonly buffer: FlushBufferMs;\n }\n | { readonly kind: \"no-context\" };\n\nexport interface DeadlineMiddlewareConfig {\n readonly flushBufferMs: FlushBufferMs;\n readonly telemetryEnabled: boolean;\n}\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n readonly telemetryEnabled?: boolean;\n}\n"]}
1
+ {"mappings":"AAKA,cAAc;KAET;CAAM;CAAG;IAAoB,IAAI;WAAY,cAAc;AAAE;AAElE,YAAY,eAAe,cAAc;AAEzC,OAAO,cAAM,eAAgB,kBAAgB;AAQ7C,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\n// Branded type prevents interchange errors at compile time (e.g. passing seconds where milliseconds are expected).\n// Zero runtime cost. Smart constructor below validates at the boundary and brands the value.\ndeclare const BrandSymbol: unique symbol;\n\ntype Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };\n\nexport type Milliseconds = Brand<number, \"Milliseconds\">;\n\nexport const milliseconds = (value: number): Milliseconds => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`milliseconds value must be finite, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as Milliseconds;\n};\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n}\n"]}
package/dist/types.js CHANGED
@@ -5,15 +5,5 @@ export const milliseconds = (value) => {
5
5
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
6
6
  return value;
7
7
  };
8
- export const flushBufferMs = (value) => {
9
- if (!Number.isFinite(value)) {
10
- throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);
11
- }
12
- if (value < 0) {
13
- throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);
14
- }
15
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
16
- return value;
17
- };
18
8
 
19
9
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"mappings":"AAeA,OAAO,MAAM,gBAAgB,UAAgC;CAC3D,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,UAAU,gDAAgD,OAAO;CAC7E;;CAEA,OAAO;AACT;AAEA,OAAO,MAAM,iBAAiB,UAAiC;CAC7D,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,UAAU,iDAAiD,OAAO;CAC9E;CACA,IAAI,QAAQ,GAAG;EACb,MAAM,IAAI,UAAU,iDAAiD,OAAO;CAC9E;;CAEA,OAAO;AACT","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\n// Branded types prevent interchange errors at compile time (e.g. passing seconds where milliseconds are expected).\n// Zero runtime cost. Smart constructors below validate at the boundary and brand the value.\ndeclare const BrandSymbol: unique symbol;\n\ntype Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };\n\nexport type Milliseconds = Brand<number, \"Milliseconds\">;\n\nexport type FlushBufferMs = Brand<number, \"FlushBufferMs\">;\n\nexport type RequestDeadlineMs = Brand<number, \"RequestDeadlineMs\">;\n\nexport const milliseconds = (value: number): Milliseconds => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`milliseconds value must be finite, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as Milliseconds;\n};\n\nexport const flushBufferMs = (value: number): FlushBufferMs => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);\n }\n if (value < 0) {\n throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as FlushBufferMs;\n};\n\nexport type DeadlineComputation =\n | { readonly kind: \"deadline\"; readonly value: RequestDeadlineMs }\n | {\n readonly kind: \"insufficient-time\";\n readonly remaining: Milliseconds;\n readonly buffer: FlushBufferMs;\n }\n | { readonly kind: \"no-context\" };\n\nexport interface DeadlineMiddlewareConfig {\n readonly flushBufferMs: FlushBufferMs;\n readonly telemetryEnabled: boolean;\n}\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n readonly telemetryEnabled?: boolean;\n}\n"]}
1
+ {"mappings":"AAWA,OAAO,MAAM,gBAAgB,UAAgC;CAC3D,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,UAAU,gDAAgD,OAAO;CAC7E;;CAEA,OAAO;AACT","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\n// Branded type prevents interchange errors at compile time (e.g. passing seconds where milliseconds are expected).\n// Zero runtime cost. Smart constructor below validates at the boundary and brands the value.\ndeclare const BrandSymbol: unique symbol;\n\ntype Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };\n\nexport type Milliseconds = Brand<number, \"Milliseconds\">;\n\nexport const milliseconds = (value: number): Milliseconds => {\n if (!Number.isFinite(value)) {\n throw new TypeError(`milliseconds value must be finite, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as Milliseconds;\n};\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambda-deadline-middleware",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "AWS SDK v3 middleware for automatic Lambda deadline propagation via AbortController-based timeouts",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,11 +64,11 @@
64
64
  "conventional-changelog-conventionalcommits": "^9.3.1",
65
65
  "knip": "^6.16.1",
66
66
  "lefthook": "^2.1.9",
67
- "oxc-transform": "^0.134.0",
68
- "oxfmt": "^0.53.0",
67
+ "oxc-transform": "^0.135.0",
68
+ "oxfmt": "^0.54.0",
69
69
  "oxlint": "^1.69.0",
70
70
  "oxlint-tsgolint": "^0.23.0",
71
- "semantic-release": "25.0.3",
71
+ "semantic-release": "25.0.5",
72
72
  "typescript": "^6.0.3",
73
73
  "vitest": "^4.1.8"
74
74
  },
@@ -9,30 +9,16 @@ export interface LambdaContextLike {
9
9
  getRemainingTimeInMillis?: () => number;
10
10
  }
11
11
 
12
- // Sentinel allows AsyncLocalStorage.run() to accept null/undefined context
13
- // without throwing, while the accessor can distinguish "no context stored"
14
- // from "context present but missing the method".
15
- const NO_CONTEXT: unique symbol = Symbol("no-context");
16
-
17
- type StoreValue = LambdaContextLike | typeof NO_CONTEXT;
18
-
19
- const contextStorage = new AsyncLocalStorage<StoreValue>();
12
+ const contextStorage = new AsyncLocalStorage<LambdaContextLike>();
20
13
 
21
14
  export const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {
22
- const value: StoreValue = context ?? NO_CONTEXT;
23
- return contextStorage.run(value, fn);
15
+ if (context === null || context === undefined) return fn();
16
+ return contextStorage.run(context, fn);
24
17
  };
25
18
 
26
19
  export const getRemainingTimeInMillis = (): number | undefined => {
27
20
  const store = contextStorage.getStore();
28
-
29
- if (store === undefined || store === NO_CONTEXT) {
30
- return undefined;
31
- }
32
-
33
- if (typeof store.getRemainingTimeInMillis !== "function") {
34
- return undefined;
35
- }
36
-
21
+ if (store === undefined) return undefined;
22
+ if (typeof store.getRemainingTimeInMillis !== "function") return undefined;
37
23
  return store.getRemainingTimeInMillis();
38
24
  };