lambda-deadline-middleware 0.0.0 → 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 (51) hide show
  1. package/LICENSE +1 -1
  2. package/LICENSES/MIT.txt +18 -0
  3. package/README.md +99 -159
  4. package/REUSE.toml +9 -0
  5. package/SECURITY.md +34 -38
  6. package/dist/context-store.d.ts +2 -2
  7. package/dist/context-store.d.ts.map +1 -1
  8. package/dist/context-store.js +9 -17
  9. package/dist/context-store.js.map +1 -1
  10. package/dist/error.d.ts +4 -4
  11. package/dist/error.d.ts.map +1 -1
  12. package/dist/error.js +2 -2
  13. package/dist/error.js.map +1 -1
  14. package/dist/handler-wrapper.d.ts +5 -4
  15. package/dist/handler-wrapper.d.ts.map +1 -1
  16. package/dist/handler-wrapper.js +2 -4
  17. package/dist/handler-wrapper.js.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/middleware.d.ts +7 -10
  23. package/dist/middleware.d.ts.map +1 -1
  24. package/dist/middleware.js +53 -67
  25. package/dist/middleware.js.map +1 -1
  26. package/dist/types.d.ts +1 -19
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js +2 -12
  29. package/dist/types.js.map +1 -1
  30. package/package.json +39 -33
  31. package/src/context-store.ts +12 -24
  32. package/src/error.ts +6 -6
  33. package/src/handler-wrapper.ts +9 -10
  34. package/src/index.ts +3 -10
  35. package/src/middleware.ts +76 -89
  36. package/src/types.ts +5 -33
  37. package/dist/config.d.ts +0 -4
  38. package/dist/config.d.ts.map +0 -1
  39. package/dist/config.js +0 -15
  40. package/dist/config.js.map +0 -1
  41. package/dist/registration.d.ts +0 -10
  42. package/dist/registration.d.ts.map +0 -1
  43. package/dist/registration.js +0 -23
  44. package/dist/registration.js.map +0 -1
  45. package/dist/telemetry.d.ts +0 -5
  46. package/dist/telemetry.d.ts.map +0 -1
  47. package/dist/telemetry.js +0 -82
  48. package/dist/telemetry.js.map +0 -1
  49. package/src/config.ts +0 -16
  50. package/src/registration.ts +0 -36
  51. package/src/telemetry.ts +0 -129
@@ -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>;
7
- export declare function withLambdaDeadline<
6
+ > = (event: TEvent, context: TContext) => Promise<TResult>;
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,iBAAS;CAAmB;CAAQ;EACzC,SAAS,aAAa,QAAQ,UAC9B,WAAW,kBACV,aAAa,QAAQ","names":[],"sources":["src/handler-wrapper.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 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 function withLambdaDeadline<TEvent, TResult>(\n handler: AsyncHandler<TEvent, TResult>,\n _options?: DeadlineOptions,\n): AsyncHandler<TEvent, TResult> {\n return async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>\n run(context, async () => handler(event, context));\n}\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,8 +1,6 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
1
+ // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { run } from "./context-store.js";
4
- export function withLambdaDeadline(handler, _options) {
5
- return async (event, context) => run(context, async () => handler(event, context));
6
- }
4
+ export const withLambdaDeadline = (handler) => async (event, context) => run(context, async () => handler(event, context));
7
5
 
8
6
  //# sourceMappingURL=handler-wrapper.js.map
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,WAAW;AASpB,OAAO,SAAS,mBACd,SACA,UAC+B;CAC/B,OAAO,OAAO,OAAe,YAC3B,IAAI,SAAS,YAAY,QAAQ,OAAO,OAAO,CAAC;AACpD","names":[],"sources":["src/handler-wrapper.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 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 function withLambdaDeadline<TEvent, TResult>(\n handler: AsyncHandler<TEvent, TResult>,\n _options?: DeadlineOptions,\n): AsyncHandler<TEvent, TResult> {\n return async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>\n run(context, async () => handler(event, context));\n}\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, withDeadline } 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,oBAAoB,oBAAoB;AACjD,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: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport { withLambdaDeadline } from \"./handler-wrapper.js\";\nexport { deadlineMiddleware, withDeadline } 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
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
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, withDeadline } 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,oBAAoB,oBAAoB;AACjD,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,gCAAgC","names":[],"sources":["src/index.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nexport { withLambdaDeadline } from \"./handler-wrapper.js\";\nexport { deadlineMiddleware, withDeadline } 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,12 +1,9 @@
1
- import type { DeadlineComputation, DeadlineMiddlewareConfig, RequestDeadlineMs } from "./types.js";
2
- import type { FinalizeRequestMiddleware } from "@smithy/types";
3
- export declare function computeDeadline(config: DeadlineMiddlewareConfig): DeadlineComputation;
4
- export interface DeadlineTimer {
5
- readonly controller: AbortController;
6
- [Symbol.dispose]: () => void;
7
- }
8
- export declare function createDeadlineTimer(deadlineMs: RequestDeadlineMs, config: DeadlineMiddlewareConfig): DeadlineTimer;
9
- export declare function composeSignals(existing: AbortSignal | undefined, deadline: AbortSignal): AbortSignal;
10
- export declare function deadlineMiddlewareHandler(config: DeadlineMiddlewareConfig): FinalizeRequestMiddleware<object, object>;
1
+ import type { Pluggable } from "@smithy/types";
2
+ import type { DeadlineOptions } from "./types.js";
3
+ export declare const composeSignals: (existing: AbortSignal | undefined, deadline: AbortSignal) => AbortSignal;
4
+ export declare const deadlineMiddleware: <
5
+ Input extends object,
6
+ Output extends object
7
+ >(options?: DeadlineOptions) => Pluggable<Input, Output>;
11
8
 
12
9
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"mappings":"AAKA,cAAc,qBAAqB,0BAA0B,yBAAyB;AAGtF,cAIE,iCAEK;AAEP,OAAO,iBAAS,gBAAgB,QAAQ,2BAA2B;AAqBnE,iBAAiB,cAAc;UACpB,YAAY;EACpB,OAAO;AACV;AAEA,OAAO,iBAAS,oBACd,YAAY,mBACZ,QAAQ,2BACP;AAmBH,OAAO,iBAAS,eACd,UAAU,yBACV,UAAU,cACT;AAKH,OAAO,iBAAS,0BACd,QAAQ,2BACP","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 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 function 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 function 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 function 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 function deadlineMiddlewareHandler(\n config: DeadlineMiddlewareConfig,\n): FinalizeRequestMiddleware<object, object> {\n return (\n next: FinalizeHandler<object, object>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<object, object> =>\n // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union\n async (args: FinalizeHandlerArguments<object>): Promise<FinalizeHandlerOutput<object>> => {\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 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}\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,76 +1,62 @@
1
- // SPDX-FileCopyrightText: 2024 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 function 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 function 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 function composeSignals(existing, deadline) {
4
+ export const composeSignals = (existing, deadline) => {
44
5
  if (existing === undefined) return deadline;
45
6
  return AbortSignal.any([existing, deadline]);
46
- }
47
- export function deadlineMiddlewareHandler(config) {
48
- return (next, _context) => async (args) => {
49
- const computation = computeDeadline(config);
50
- switch (computation.kind) {
51
- case "no-context": return next(args);
52
- case "insufficient-time": throw new DeadlineExceededError({
53
- deadlineMs: milliseconds(0),
54
- flushBufferMs: computation.buffer,
55
- remainingMs: computation.remaining
56
- });
57
- case "deadline": {
58
- using timer = createDeadlineTimer(computation.value, config);
59
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
60
- const request = args.request;
61
- const signal = composeSignals(request?.signal, timer.controller.signal);
62
- const result = await next({
63
- ...args,
64
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
65
- request: {
66
- ...args.request,
67
- signal
68
- }
7
+ };
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)
69
27
  });
70
- return result;
71
28
  }
72
- }
73
- };
74
- }
29
+ const controller = new AbortController();
30
+ const timeoutId = setTimeout(() => {
31
+ controller.abort(new DeadlineExceededError({
32
+ deadlineMs: milliseconds(deadline),
33
+ flushBufferMs,
34
+ remainingMs: milliseconds(remaining)
35
+ }));
36
+ }, deadline);
37
+ // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
38
+ // or an external abort signal fires — strictly more reliable than try/finally.
39
+ using _timer = { [Symbol.dispose]() {
40
+ clearTimeout(timeoutId);
41
+ } };
42
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
43
+ const request = args.request;
44
+ const signal = composeSignals(request?.signal, controller.signal);
45
+ const result = await next({
46
+ ...args,
47
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
48
+ request: {
49
+ ...args.request,
50
+ signal
51
+ }
52
+ });
53
+ return result;
54
+ }, {
55
+ step: "finalizeRequest",
56
+ name: "deadlineMiddleware",
57
+ override: true
58
+ });
59
+ } };
60
+ };
75
61
 
76
62
  //# sourceMappingURL=middleware.js.map
@@ -1 +1 @@
1
- {"mappings":";;AAGA,SAAS,gCAAgC;AACzC,SAAS,6BAA6B;AAEtC,SAAS,oBAAoB;AAU7B,OAAO,SAAS,gBAAgB,QAAuD;CACrF,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,SAAS,oBACd,YACA,QACe;CACf,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,SAAS,eACd,UACA,UACa;CACb,IAAI,aAAa,WAAW,OAAO;CACnC,OAAO,YAAY,IAAI,CAAC,UAAU,QAAQ,CAAC;AAC7C;AAEA,OAAO,SAAS,0BACd,QAC2C;CAC3C,QACE,MACA,aAGA,OAAO,SAAmF;EACxF,MAAM,cAAc,gBAAgB,MAAM;EAE1C,QAAQ,YAAY,MAApB;GACE,KAAK,cACH,OAAO,KAAK,IAAI;GAElB,KAAK,qBACH,MAAM,IAAI,sBAAsB;IAC9B,YAAY,aAAa,CAAC;IAC1B,eAAe,YAAY;IAC3B,aAAa,YAAY;GAC3B,CAAC;GAEH,KAAK,YAAY;IACf,MAAM,QAAQ,oBAAoB,YAAY,OAAO,MAAM;;IAE3D,MAAM,UAAU,KAAK;IACrB,MAAM,SAAS,eAAe,SAAS,QAAQ,MAAM,WAAW,MAAM;IACtE,MAAM,SAAS,MAAM,KAAK;KACxB,GAAG;;KAEH,SAAS;MAAE,GAAI,KAAK;MAAoB;KAAO;IACjD,CAAC;IACD,OAAO;GACT;EACF;CACF;AACJ","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 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 function 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 function 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 function 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 function deadlineMiddlewareHandler(\n config: DeadlineMiddlewareConfig,\n): FinalizeRequestMiddleware<object, object> {\n return (\n next: FinalizeHandler<object, object>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<object, object> =>\n // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union\n async (args: FinalizeHandlerArguments<object>): Promise<FinalizeHandlerOutput<object>> => {\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 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}\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
- export declare function milliseconds(value: number): Milliseconds;
12
- export declare function 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
- }
9
+ export declare const milliseconds: (value: number) => Milliseconds;
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":"AAGA,cAAc;KAET;CAAM;CAAG;IAAoB,IAAI;WAAY,cAAc;AAAE;AAElE,YAAY,eAAe,cAAc;AAEzC,YAAY,gBAAgB,cAAc;AAE1C,YAAY,oBAAoB,cAAc;AAE9C,OAAO,iBAAS,aAAa,gBAAgB;AAQ7C,OAAO,iBAAS,cAAc,gBAAgB;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: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\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 function 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 function 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
@@ -1,19 +1,9 @@
1
- export function milliseconds(value) {
1
+ export const milliseconds = (value) => {
2
2
  if (!Number.isFinite(value)) {
3
3
  throw new TypeError(`milliseconds value must be finite, received: ${value}`);
4
4
  }
5
5
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
6
6
  return value;
7
- }
8
- export function 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
- }
7
+ };
18
8
 
19
9
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"mappings":"AAaA,OAAO,SAAS,aAAa,OAA6B;CACxD,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,UAAU,gDAAgD,OAAO;CAC7E;;CAEA,OAAO;AACT;AAEA,OAAO,SAAS,cAAc,OAA8B;CAC1D,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: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\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 function 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 function 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,12 +1,18 @@
1
1
  {
2
2
  "name": "lambda-deadline-middleware",
3
- "version": "0.0.0",
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
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mikkopiu/lambda-deadline-middleware"
9
+ },
6
10
  "files": [
7
11
  "dist",
8
12
  "src",
9
13
  "LICENSE",
14
+ "LICENSES",
15
+ "REUSE.toml",
10
16
  "SECURITY.md"
11
17
  ],
12
18
  "type": "module",
@@ -20,54 +26,54 @@
20
26
  "access": "public",
21
27
  "provenance": true
22
28
  },
29
+ "scripts": {
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",
34
+ "typecheck": "tsc",
35
+ "lint": "oxlint --type-aware --type-check --fix .",
36
+ "lint:check": "oxlint --type-aware --type-check .",
37
+ "fmt": "oxfmt .",
38
+ "fmt:check": "oxfmt --check .",
39
+ "lint:knip": "knip",
40
+ "sast": "scripts/ensure-opengrep.sh scan .",
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",
44
+ "actionlint": "podman run --rm -w /repo -v \"$(pwd):/repo:Z\" docker.io/rhysd/actionlint:1.7.12@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7",
45
+ "security": "pnpm run sast:check && pnpm run sca && pnpm run actionlint",
46
+ "prepare": "lefthook install",
47
+ "sbom": "pnpm sbom --sbom-format cyclonedx --sbom-spec-version 1.5 --sbom-type library > sbom.cdx.json",
48
+ "release": "semantic-release"
49
+ },
23
50
  "devDependencies": {
24
- "@aws-sdk/client-dynamodb": "^3.1062.0",
25
- "@aws-sdk/client-s3": "^3.1062.0",
26
- "@aws-sdk/client-sqs": "^3.1062.0",
51
+ "@aws-sdk/client-dynamodb": "^3.1064.0",
52
+ "@aws-sdk/client-s3": "^3.1064.0",
53
+ "@aws-sdk/client-sqs": "^3.1064.0",
27
54
  "@commitlint/cli": "^21.0.2",
28
55
  "@commitlint/config-conventional": "^21.0.2",
29
56
  "@fast-check/vitest": "^0.4.1",
30
- "@semantic-release/changelog": "6.0.3",
31
57
  "@semantic-release/commit-analyzer": "13.0.1",
32
- "@semantic-release/git": "10.0.1",
33
58
  "@semantic-release/github": "12.0.8",
34
59
  "@semantic-release/npm": "13.1.5",
35
60
  "@semantic-release/release-notes-generator": "14.1.1",
36
61
  "@smithy/types": "^4.14.3",
37
- "@types/node": "^25.9.1",
62
+ "@types/node": "^25.9.2",
38
63
  "@vitest/coverage-v8": "^4.1.8",
39
64
  "conventional-changelog-conventionalcommits": "^9.3.1",
40
- "knip": "^6.15.0",
65
+ "knip": "^6.16.1",
41
66
  "lefthook": "^2.1.9",
42
- "oxc-transform": "^0.134.0",
43
- "oxfmt": "^0.53.0",
44
- "oxlint": "^1.68.0",
67
+ "oxc-transform": "^0.135.0",
68
+ "oxfmt": "^0.54.0",
69
+ "oxlint": "^1.69.0",
45
70
  "oxlint-tsgolint": "^0.23.0",
46
- "semantic-release": "25.0.3",
71
+ "semantic-release": "25.0.5",
47
72
  "typescript": "^6.0.3",
48
73
  "vitest": "^4.1.8"
49
74
  },
50
75
  "engines": {
51
76
  "node": ">=24"
52
77
  },
53
- "scripts": {
54
- "build": "node --experimental-strip-types scripts/build.ts",
55
- "test": "vitest run",
56
- "test:watch": "vitest",
57
- "bench": "vitest bench --run --outputJson=bench-results.json && node --experimental-strip-types scripts/validate-bench.ts",
58
- "typecheck": "tsc",
59
- "lint": "oxlint --type-aware --type-check --fix .",
60
- "lint:check": "oxlint --type-aware --type-check .",
61
- "fmt": "oxfmt .",
62
- "fmt:check": "oxfmt --check .",
63
- "lint:knip": "knip",
64
- "sast": "scripts/ensure-opengrep.sh scan .",
65
- "sast:check": "scripts/ensure-opengrep.sh scan --error .",
66
- "sca": "podman run --rm -v \"$(pwd):/src:Z\" ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln --include-dev-deps --exit-code 1 --severity HIGH,CRITICAL /src",
67
- "sca:full": "podman run --rm -v \"$(pwd):/src:Z\" ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln,secret,misconfig --include-dev-deps /src",
68
- "actionlint": "podman run --rm -w /repo -v \"$(pwd):/repo:Z\" docker.io/rhysd/actionlint:1.7.12@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7",
69
- "security": "pnpm run sast:check && pnpm run sca && pnpm run actionlint",
70
- "sbom": "pnpm sbom --sbom-format cyclonedx --sbom-spec-version 1.5 --sbom-type library > sbom.cdx.json",
71
- "release": "semantic-release"
72
- }
73
- }
78
+ "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
79
+ }
@@ -1,36 +1,24 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
1
+ // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
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
8
  export interface LambdaContextLike {
7
9
  getRemainingTimeInMillis?: () => number;
8
10
  }
9
11
 
10
- // Sentinel allows AsyncLocalStorage.run() to accept null/undefined context
11
- // without throwing, while the accessor can distinguish "no context stored"
12
- // from "context present but missing the method".
13
- const NO_CONTEXT: unique symbol = Symbol("no-context");
12
+ const contextStorage = new AsyncLocalStorage<LambdaContextLike>();
14
13
 
15
- type StoreValue = LambdaContextLike | typeof NO_CONTEXT;
14
+ export const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {
15
+ if (context === null || context === undefined) return fn();
16
+ return contextStorage.run(context, fn);
17
+ };
16
18
 
17
- const contextStorage = new AsyncLocalStorage<StoreValue>();
18
-
19
- export function run<T>(context: LambdaContextLike | null | undefined, fn: () => T): T {
20
- const value: StoreValue = context ?? NO_CONTEXT;
21
- return contextStorage.run(value, fn);
22
- }
23
-
24
- export function getRemainingTimeInMillis(): number | undefined {
19
+ export const getRemainingTimeInMillis = (): number | undefined => {
25
20
  const store = contextStorage.getStore();
26
-
27
- if (store === undefined || store === NO_CONTEXT) {
28
- return undefined;
29
- }
30
-
31
- if (typeof store.getRemainingTimeInMillis !== "function") {
32
- return undefined;
33
- }
34
-
21
+ if (store === undefined) return undefined;
22
+ if (typeof store.getRemainingTimeInMillis !== "function") return undefined;
35
23
  return store.getRemainingTimeInMillis();
36
- }
24
+ };
package/src/error.ts CHANGED
@@ -1,18 +1,18 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
1
+ // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import type { FlushBufferMs, Milliseconds } from "./types.js";
4
+ import type { Milliseconds } from "./types.js";
5
5
 
6
6
  interface DeadlineExceededInit {
7
7
  readonly deadlineMs: Milliseconds;
8
- readonly flushBufferMs: FlushBufferMs;
8
+ readonly flushBufferMs: Milliseconds;
9
9
  readonly remainingMs: Milliseconds;
10
10
  }
11
11
 
12
12
  export class DeadlineExceededError extends Error {
13
13
  override readonly name = "DeadlineExceededError" as const;
14
14
  readonly deadlineMs: Milliseconds;
15
- readonly flushBufferMs: FlushBufferMs;
15
+ readonly flushBufferMs: Milliseconds;
16
16
  readonly remainingMs: Milliseconds;
17
17
 
18
18
  constructor(init: DeadlineExceededInit) {
@@ -27,8 +27,8 @@ export class DeadlineExceededError extends Error {
27
27
 
28
28
  // Structural check rather than instanceof — works across module boundaries
29
29
  // and serialization boundaries where prototype chain may be broken.
30
- export function isDeadlineExceeded(error: unknown): error is DeadlineExceededError {
30
+ export const isDeadlineExceeded = (error: unknown): error is DeadlineExceededError => {
31
31
  if (error === null || error === undefined) return false;
32
32
  if (typeof error !== "object") return false;
33
33
  return (error as { name?: unknown }).name === "DeadlineExceededError";
34
- }
34
+ };
@@ -1,19 +1,18 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
1
+ // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { run } from "./context-store.js";
5
+
5
6
  import type { LambdaContextLike } from "./context-store.js";
6
- import type { DeadlineOptions } from "./types.js";
7
7
 
8
- type AsyncHandler<TEvent, TResult> = (
8
+ type AsyncHandler<TEvent, TContext extends LambdaContextLike, TResult> = (
9
9
  event: TEvent,
10
- context: LambdaContextLike,
10
+ context: TContext,
11
11
  ) => Promise<TResult>;
12
12
 
13
- export function withLambdaDeadline<TEvent, TResult>(
14
- handler: AsyncHandler<TEvent, TResult>,
15
- _options?: DeadlineOptions,
16
- ): AsyncHandler<TEvent, TResult> {
17
- return async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>
13
+ export const withLambdaDeadline =
14
+ <TEvent, TContext extends LambdaContextLike, TResult>(
15
+ handler: AsyncHandler<TEvent, TContext, TResult>,
16
+ ): AsyncHandler<TEvent, TContext, TResult> =>
17
+ async (event: TEvent, context: TContext): Promise<TResult> =>
18
18
  run(context, async () => handler(event, context));
19
- }