lambda-deadline-middleware 1.0.1 → 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.
Files changed (46) hide show
  1. package/README.md +163 -85
  2. package/dist/context-store.d.ts +25 -2
  3. package/dist/context-store.d.ts.map +1 -1
  4. package/dist/context-store.js +45 -14
  5. package/dist/context-store.js.map +1 -1
  6. package/dist/error.d.ts +6 -7
  7. package/dist/error.d.ts.map +1 -1
  8. package/dist/error.js.map +1 -1
  9. package/dist/index.d.ts +3 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware.d.ts +3 -11
  14. package/dist/middleware.d.ts.map +1 -1
  15. package/dist/middleware.js +22 -73
  16. package/dist/middleware.js.map +1 -1
  17. package/dist/types.d.ts +0 -28
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/types.js +1 -17
  20. package/dist/types.js.map +1 -1
  21. package/package.json +22 -22
  22. package/src/context-store.ts +73 -21
  23. package/src/error.ts +6 -8
  24. package/src/index.ts +3 -12
  25. package/src/middleware.ts +36 -101
  26. package/src/types.ts +0 -46
  27. package/dist/config.d.ts +0 -4
  28. package/dist/config.d.ts.map +0 -1
  29. package/dist/config.js +0 -17
  30. package/dist/config.js.map +0 -1
  31. package/dist/handler-wrapper.d.ts +0 -13
  32. package/dist/handler-wrapper.d.ts.map +0 -1
  33. package/dist/handler-wrapper.js +0 -6
  34. package/dist/handler-wrapper.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/handler-wrapper.ts +0 -19
  45. package/src/registration.ts +0 -27
  46. package/src/telemetry.ts +0 -132
@@ -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;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"]}
@@ -1,76 +1,25 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
- import { getRemainingTimeInMillis } from "./context-store.js";
4
- import { DeadlineExceededError } from "./error.js";
5
- 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
- export const composeSignals = (existing, deadline) => {
44
- if (existing === undefined) return deadline;
45
- return AbortSignal.any([existing, deadline]);
46
- };
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
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
16
+ }
55
17
  });
56
- case "deadline": {
57
- // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
58
- // or an external abort signal fires — strictly more reliable than try/finally.
59
- using timer = createDeadlineTimer(computation.value, config);
60
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
61
- const request = args.request;
62
- const signal = composeSignals(request?.signal, timer.controller.signal);
63
- const result = await next({
64
- ...args,
65
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
66
- request: {
67
- ...args.request,
68
- signal
69
- }
70
- });
71
- return result;
72
- }
73
- }
74
- };
18
+ }, {
19
+ step: "finalizeRequest",
20
+ name: "deadlineMiddleware",
21
+ override: true
22
+ });
23
+ } });
75
24
 
76
25
  //# 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,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,33 +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 type FlushBufferMs = Brand<number, "FlushBufferMs">;
10
- export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
11
- 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
1
  export interface DeadlineOptions {
28
2
  readonly flushBufferMs?: number;
29
- readonly telemetryEnabled?: boolean;
30
3
  }
31
- export {};
32
4
 
33
5
  //# sourceMappingURL=types.d.ts.map
@@ -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":"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,19 +1,3 @@
1
- export const milliseconds = (value) => {
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
- };
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
- };
1
+ export {};
18
2
 
19
3
  //# 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":"","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": "1.0.1",
4
- "description": "AWS SDK v3 middleware for automatic Lambda deadline propagation via AbortController-based timeouts",
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 --type-aware --type-check --fix .",
36
- "lint:check": "oxlint --type-aware --type-check .",
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.1064.0",
52
- "@aws-sdk/client-s3": "^3.1064.0",
53
- "@aws-sdk/client-sqs": "^3.1064.0",
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.14.3",
62
- "@types/node": "^25.9.2",
63
- "@vitest/coverage-v8": "^4.1.8",
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.16.1",
65
+ "knip": "^6.17.1",
66
66
  "lefthook": "^2.1.9",
67
- "oxc-transform": "^0.134.0",
68
- "oxfmt": "^0.53.0",
69
- "oxlint": "^1.69.0",
67
+ "oxc-transform": "^0.135.0",
68
+ "oxfmt": "^0.54.0",
69
+ "oxlint": "^1.70.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
- "vitest": "^4.1.8"
73
+ "vitest": "^4.1.9"
74
74
  },
75
75
  "engines": {
76
76
  "node": ">=24"
@@ -3,36 +3,88 @@
3
3
 
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
 
6
- // AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.
7
- // SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.
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
- // 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;
16
+ interface DeadlineStore {
17
+ signal?: AbortSignal;
18
+ }
18
19
 
19
- const contextStorage = new AsyncLocalStorage<StoreValue>();
20
+ const contextStorage = new AsyncLocalStorage<DeadlineStore>();
20
21
 
21
- export const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {
22
- const value: StoreValue = context ?? NO_CONTEXT;
23
- return contextStorage.run(value, fn);
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;
24
36
  };
25
37
 
26
- export const getRemainingTimeInMillis = (): number | undefined => {
38
+ /**
39
+ * Retrieve the deadline signal for the current invocation, if one exists.
40
+ */
41
+ export const getDeadlineSignal = (): AbortSignal | undefined => {
27
42
  const store = contextStorage.getStore();
43
+ if (store === undefined) return undefined;
44
+ return store.signal;
45
+ };
28
46
 
29
- if (store === undefined || store === NO_CONTEXT) {
30
- return undefined;
31
- }
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>;
32
52
 
33
- if (typeof store.getRemainingTimeInMillis !== "function") {
34
- return undefined;
35
- }
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 = {};
36
60
 
37
- return store.getRemainingTimeInMillis();
38
- };
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 { FlushBufferMs, Milliseconds } from "./types.js";
5
-
6
4
  interface DeadlineExceededInit {
7
- readonly deadlineMs: Milliseconds;
8
- readonly flushBufferMs: FlushBufferMs;
9
- readonly remainingMs: Milliseconds;
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: Milliseconds;
15
- readonly flushBufferMs: FlushBufferMs;
16
- readonly remainingMs: Milliseconds;
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,18 +1,9 @@
1
1
  // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- export { withLambdaDeadline } from "./handler-wrapper.js";
5
- export { deadlineMiddleware } from "./registration.js";
4
+ export { withLambdaDeadline, setDeadlineSignal } from "./context-store.js";
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 {
10
- Milliseconds,
11
- FlushBufferMs,
12
- RequestDeadlineMs,
13
- DeadlineComputation,
14
- DeadlineMiddlewareConfig,
15
- DeadlineOptions,
16
- } from "./types.js";
17
7
 
8
+ export type { DeadlineOptions } from "./types.js";
18
9
  export type { LambdaContextLike } from "./context-store.js";
package/src/middleware.ts CHANGED
@@ -1,111 +1,46 @@
1
1
  // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import { getRemainingTimeInMillis } from "./context-store.js";
5
- import { DeadlineExceededError } from "./error.js";
6
- import type { DeadlineComputation, DeadlineMiddlewareConfig, RequestDeadlineMs } from "./types.js";
7
- import { milliseconds } from "./types.js";
8
-
9
4
  import type {
10
5
  FinalizeHandler,
11
6
  FinalizeHandlerArguments,
12
7
  FinalizeHandlerOutput,
13
- FinalizeRequestMiddleware,
14
8
  HandlerExecutionContext,
9
+ Pluggable,
15
10
  } from "@smithy/types";
16
11
 
17
- export const computeDeadline = (config: DeadlineMiddlewareConfig): DeadlineComputation => {
18
- const remaining = getRemainingTimeInMillis();
19
-
20
- if (remaining === undefined) {
21
- return { kind: "no-context" };
22
- }
23
-
24
- const deadline = remaining - config.flushBufferMs;
25
-
26
- if (deadline <= 0) {
27
- return {
28
- kind: "insufficient-time",
29
- remaining: milliseconds(remaining),
30
- buffer: config.flushBufferMs,
31
- };
32
- }
33
-
34
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above
35
- return { kind: "deadline", value: deadline as RequestDeadlineMs };
36
- };
37
-
38
- export interface DeadlineTimer {
39
- readonly controller: AbortController;
40
- [Symbol.dispose]: () => void;
41
- }
42
-
43
- export const createDeadlineTimer = (
44
- deadlineMs: RequestDeadlineMs,
45
- config: DeadlineMiddlewareConfig,
46
- ): DeadlineTimer => {
47
- const controller = new AbortController();
48
- const remaining = milliseconds(deadlineMs + config.flushBufferMs);
49
- const error = new DeadlineExceededError({
50
- deadlineMs: milliseconds(deadlineMs),
51
- flushBufferMs: config.flushBufferMs,
52
- remainingMs: remaining,
53
- });
54
- const timeoutId = setTimeout(() => {
55
- controller.abort(error);
56
- }, deadlineMs);
57
- return {
58
- controller,
59
- [Symbol.dispose]() {
60
- clearTimeout(timeoutId);
61
- },
62
- };
63
- };
64
-
65
- export const composeSignals = (
66
- existing: AbortSignal | undefined,
67
- deadline: AbortSignal,
68
- ): AbortSignal => {
69
- if (existing === undefined) return deadline;
70
- return AbortSignal.any([existing, deadline]);
71
- };
72
-
73
- export const deadlineMiddlewareHandler =
74
- <Input extends object, Output extends object>(
75
- config: DeadlineMiddlewareConfig,
76
- ): FinalizeRequestMiddleware<Input, Output> =>
77
- (
78
- next: FinalizeHandler<Input, Output>,
79
- _context: HandlerExecutionContext,
80
- ): FinalizeHandler<Input, Output> =>
81
- // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union
82
- async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {
83
- const computation = computeDeadline(config);
84
-
85
- switch (computation.kind) {
86
- case "no-context":
87
- return next(args);
88
-
89
- case "insufficient-time":
90
- throw new DeadlineExceededError({
91
- deadlineMs: milliseconds(0),
92
- flushBufferMs: computation.buffer,
93
- remainingMs: computation.remaining,
94
- });
95
-
96
- case "deadline": {
97
- // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
98
- // or an external abort signal fires — strictly more reliable than try/finally.
99
- using timer = createDeadlineTimer(computation.value, config);
100
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
101
- const request = args.request as { signal?: AbortSignal } | undefined;
102
- const signal = composeSignals(request?.signal, timer.controller.signal);
103
- const result = await next({
104
- ...args,
105
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
106
- request: { ...(args.request as object), signal },
107
- });
108
- return result;
109
- }
110
- }
111
- };
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
+ });
38
+ },
39
+ {
40
+ step: "finalizeRequest",
41
+ name: "deadlineMiddleware",
42
+ override: true,
43
+ },
44
+ );
45
+ },
46
+ });