lambda-deadline-middleware 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +9 -23
  2. package/dist/context-store.d.ts.map +1 -1
  3. package/dist/context-store.js +4 -12
  4. package/dist/context-store.js.map +1 -1
  5. package/dist/error.d.ts +3 -3
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/error.js.map +1 -1
  8. package/dist/handler-wrapper.d.ts +4 -3
  9. package/dist/handler-wrapper.d.ts.map +1 -1
  10. package/dist/handler-wrapper.js +1 -1
  11. package/dist/handler-wrapper.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/middleware.d.ts +4 -10
  17. package/dist/middleware.d.ts.map +1 -1
  18. package/dist/middleware.js +39 -53
  19. package/dist/middleware.js.map +1 -1
  20. package/dist/types.d.ts +0 -18
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/types.js +0 -10
  23. package/dist/types.js.map +1 -1
  24. package/package.json +4 -4
  25. package/src/context-store.ts +5 -19
  26. package/src/error.ts +3 -3
  27. package/src/handler-wrapper.ts +7 -8
  28. package/src/index.ts +2 -9
  29. package/src/middleware.ts +72 -87
  30. package/src/types.ts +2 -32
  31. package/dist/config.d.ts +0 -4
  32. package/dist/config.d.ts.map +0 -1
  33. package/dist/config.js +0 -17
  34. package/dist/config.js.map +0 -1
  35. package/dist/registration.d.ts +0 -8
  36. package/dist/registration.d.ts.map +0 -1
  37. package/dist/registration.js +0 -17
  38. package/dist/registration.js.map +0 -1
  39. package/dist/telemetry.d.ts +0 -5
  40. package/dist/telemetry.d.ts.map +0 -1
  41. package/dist/telemetry.js +0 -82
  42. package/dist/telemetry.js.map +0 -1
  43. package/src/config.ts +0 -18
  44. package/src/registration.ts +0 -27
  45. package/src/telemetry.ts +0 -132
package/src/error.ts CHANGED
@@ -1,18 +1,18 @@
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";
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) {
@@ -2,18 +2,17 @@
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
13
  export const withLambdaDeadline =
14
- <TEvent, TResult>(
15
- handler: AsyncHandler<TEvent, TResult>,
16
- _options?: DeadlineOptions,
17
- ): AsyncHandler<TEvent, TResult> =>
18
- async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>
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> =>
19
18
  run(context, async () => handler(event, context));
package/src/index.ts CHANGED
@@ -2,17 +2,10 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  export { withLambdaDeadline } from "./handler-wrapper.js";
5
- export { deadlineMiddleware } from "./registration.js";
5
+ export { deadlineMiddleware } from "./middleware.js";
6
6
  export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
7
7
  export { getRemainingTimeInMillis } from "./context-store.js";
8
8
 
9
- export type {
10
- Milliseconds,
11
- FlushBufferMs,
12
- RequestDeadlineMs,
13
- DeadlineComputation,
14
- DeadlineMiddlewareConfig,
15
- DeadlineOptions,
16
- } from "./types.js";
9
+ export type { Milliseconds, DeadlineOptions } from "./types.js";
17
10
 
18
11
  export type { LambdaContextLike } from "./context-store.js";
package/src/middleware.ts CHANGED
@@ -1,66 +1,19 @@
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
- }
12
+ import { getRemainingTimeInMillis } from "./context-store.js";
13
+ import { DeadlineExceededError } from "./error.js";
14
+ import { milliseconds } from "./types.js";
42
15
 
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
- };
16
+ import type { DeadlineOptions } from "./types.js";
64
17
 
65
18
  export const composeSignals = (
66
19
  existing: AbortSignal | undefined,
@@ -70,42 +23,74 @@ export const composeSignals = (
70
23
  return AbortSignal.any([existing, deadline]);
71
24
  };
72
25
 
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);
26
+ export const deadlineMiddleware = <Input extends object, Output extends object>(
27
+ options?: DeadlineOptions,
28
+ ): Pluggable<Input, Output> => {
29
+ const raw = options?.flushBufferMs ?? 1000;
30
+ if (raw < 0) {
31
+ throw new TypeError(`flushBufferMs option must be non-negative, received: ${raw}`);
32
+ }
33
+ const flushBufferMs = milliseconds(raw);
84
34
 
85
- switch (computation.kind) {
86
- case "no-context":
87
- return next(args);
35
+ return {
36
+ applyToStack(stack) {
37
+ // Registered at "finalizeRequest" (attempt level) rather than API-call level so each retry gets a deadline
38
+ // computed from the actual remaining time at that moment. API-call level would cache a stale deadline
39
+ // across retries, which grow more dangerous after backoff delays eat into remaining time.
40
+ stack.add(
41
+ (
42
+ next: FinalizeHandler<Input, Output>,
43
+ _context: HandlerExecutionContext,
44
+ ): FinalizeHandler<Input, Output> =>
45
+ async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {
46
+ const remaining = getRemainingTimeInMillis();
47
+ if (remaining === undefined) return next(args);
48
+
49
+ const deadline = remaining - flushBufferMs;
50
+
51
+ if (deadline <= 0) {
52
+ throw new DeadlineExceededError({
53
+ deadlineMs: milliseconds(0),
54
+ flushBufferMs,
55
+ remainingMs: milliseconds(remaining),
56
+ });
57
+ }
88
58
 
89
- case "insufficient-time":
90
- throw new DeadlineExceededError({
91
- deadlineMs: milliseconds(0),
92
- flushBufferMs: computation.buffer,
93
- remainingMs: computation.remaining,
94
- });
59
+ const controller = new AbortController();
60
+ const timeoutId = setTimeout(() => {
61
+ controller.abort(
62
+ new DeadlineExceededError({
63
+ deadlineMs: milliseconds(deadline),
64
+ flushBufferMs,
65
+ remainingMs: milliseconds(remaining),
66
+ }),
67
+ );
68
+ }, deadline);
95
69
 
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
- }
70
+ // `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
71
+ // or an external abort signal fires strictly more reliable than try/finally.
72
+ using _timer = {
73
+ [Symbol.dispose]() {
74
+ clearTimeout(timeoutId);
75
+ },
76
+ };
77
+
78
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
79
+ const request = args.request as { signal?: AbortSignal } | undefined;
80
+ const signal = composeSignals(request?.signal, controller.signal);
81
+ const result = await next({
82
+ ...args,
83
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
84
+ request: { ...(args.request as object), signal },
85
+ });
86
+ return result;
87
+ },
88
+ {
89
+ step: "finalizeRequest",
90
+ name: "deadlineMiddleware",
91
+ override: true,
92
+ },
93
+ );
94
+ },
111
95
  };
96
+ };
package/src/types.ts CHANGED
@@ -1,18 +1,14 @@
1
1
  // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- // Branded types prevent interchange errors at compile time (e.g. passing seconds where milliseconds are expected).
5
- // Zero runtime cost. Smart constructors below validate at the boundary and brand the value.
4
+ // Branded type prevents interchange errors at compile time (e.g. passing seconds where milliseconds are expected).
5
+ // Zero runtime cost. Smart constructor below validates at the boundary and brands the value.
6
6
  declare const BrandSymbol: unique symbol;
7
7
 
8
8
  type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
9
9
 
10
10
  export type Milliseconds = Brand<number, "Milliseconds">;
11
11
 
12
- export type FlushBufferMs = Brand<number, "FlushBufferMs">;
13
-
14
- export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
15
-
16
12
  export const milliseconds = (value: number): Milliseconds => {
17
13
  if (!Number.isFinite(value)) {
18
14
  throw new TypeError(`milliseconds value must be finite, received: ${value}`);
@@ -21,32 +17,6 @@ export const milliseconds = (value: number): Milliseconds => {
21
17
  return value as Milliseconds;
22
18
  };
23
19
 
24
- export const flushBufferMs = (value: number): FlushBufferMs => {
25
- if (!Number.isFinite(value)) {
26
- throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);
27
- }
28
- if (value < 0) {
29
- throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);
30
- }
31
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
32
- return value as FlushBufferMs;
33
- };
34
-
35
- export type DeadlineComputation =
36
- | { readonly kind: "deadline"; readonly value: RequestDeadlineMs }
37
- | {
38
- readonly kind: "insufficient-time";
39
- readonly remaining: Milliseconds;
40
- readonly buffer: FlushBufferMs;
41
- }
42
- | { readonly kind: "no-context" };
43
-
44
- export interface DeadlineMiddlewareConfig {
45
- readonly flushBufferMs: FlushBufferMs;
46
- readonly telemetryEnabled: boolean;
47
- }
48
-
49
20
  export interface DeadlineOptions {
50
21
  readonly flushBufferMs?: number;
51
- readonly telemetryEnabled?: boolean;
52
22
  }
package/dist/config.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import type { DeadlineMiddlewareConfig, DeadlineOptions } from "./types.js";
2
- export declare const parseConfig: (raw: DeadlineOptions | undefined) => DeadlineMiddlewareConfig;
3
-
4
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"mappings":"AAIA,cAAc,0BAA0B,uBAAuB;AAI/D,OAAO,cAAM,cAAe,KAAK,gCAA8B","names":[],"sources":["src/config.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { flushBufferMs } from \"./types.js\";\nimport type { DeadlineMiddlewareConfig, DeadlineOptions } from \"./types.js\";\n\n// \"Parse, don't validate\": config is validated once here and returned as branded types.\n// Internal code can't receive unvalidated values. Invalid config throws TypeError at startup, not during requests.\nexport const parseConfig = (raw: DeadlineOptions | undefined): DeadlineMiddlewareConfig => {\n const buffer = raw?.flushBufferMs ?? 1000;\n if (buffer < 0) {\n throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);\n }\n return {\n flushBufferMs: flushBufferMs(buffer),\n telemetryEnabled: raw?.telemetryEnabled ?? true,\n };\n};\n"]}
package/dist/config.js DELETED
@@ -1,17 +0,0 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
- import { flushBufferMs } from "./types.js";
4
- // "Parse, don't validate": config is validated once here and returned as branded types.
5
- // Internal code can't receive unvalidated values. Invalid config throws TypeError at startup, not during requests.
6
- export const parseConfig = (raw) => {
7
- const buffer = raw?.flushBufferMs ?? 1e3;
8
- if (buffer < 0) {
9
- throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);
10
- }
11
- return {
12
- flushBufferMs: flushBufferMs(buffer),
13
- telemetryEnabled: raw?.telemetryEnabled ?? true
14
- };
15
- };
16
-
17
- //# sourceMappingURL=config.js.map
@@ -1 +0,0 @@
1
- {"mappings":";;AAGA,SAAS,qBAAqB;;;AAK9B,OAAO,MAAM,eAAe,QAA+D;CACzF,MAAM,SAAS,KAAK,iBAAiB;CACrC,IAAI,SAAS,GAAG;EACd,MAAM,IAAI,UAAU,wDAAwD,QAAQ;CACtF;CACA,OAAO;EACL,eAAe,cAAc,MAAM;EACnC,kBAAkB,KAAK,oBAAoB;CAC7C;AACF","names":[],"sources":["src/config.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { flushBufferMs } from \"./types.js\";\nimport type { DeadlineMiddlewareConfig, DeadlineOptions } from \"./types.js\";\n\n// \"Parse, don't validate\": config is validated once here and returned as branded types.\n// Internal code can't receive unvalidated values. Invalid config throws TypeError at startup, not during requests.\nexport const parseConfig = (raw: DeadlineOptions | undefined): DeadlineMiddlewareConfig => {\n const buffer = raw?.flushBufferMs ?? 1000;\n if (buffer < 0) {\n throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);\n }\n return {\n flushBufferMs: flushBufferMs(buffer),\n telemetryEnabled: raw?.telemetryEnabled ?? true,\n };\n};\n"]}
@@ -1,8 +0,0 @@
1
- import type { Pluggable } from "@smithy/types";
2
- import type { DeadlineOptions } from "./types.js";
3
- export declare const deadlineMiddleware: <
4
- Input extends object,
5
- Output extends object
6
- >(options?: DeadlineOptions) => Pluggable<Input, Output>;
7
-
8
- //# sourceMappingURL=registration.d.ts.map
@@ -1 +0,0 @@
1
- {"mappings":"AAGA,cAAc,iBAAiB;AAI/B,cAAc,uBAAuB;AAErC,OAAO,cAAM;CAAsB;CAAsB;EACvD,UAAU,oBACT,UAAU,OAAO","names":[],"sources":["src/registration.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { Pluggable } from \"@smithy/types\";\n\nimport { parseConfig } from \"./config.js\";\nimport { deadlineMiddlewareHandler } from \"./middleware.js\";\nimport type { DeadlineOptions } from \"./types.js\";\n\nexport const deadlineMiddleware = <Input extends object, Output extends object>(\n options?: DeadlineOptions,\n): Pluggable<Input, Output> => {\n const config = parseConfig(options);\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(deadlineMiddlewareHandler<Input, Output>(config), {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n });\n },\n };\n};\n"]}
@@ -1,17 +0,0 @@
1
- import { parseConfig } from "./config.js";
2
- import { deadlineMiddlewareHandler } from "./middleware.js";
3
- export const deadlineMiddleware = (options) => {
4
- const config = parseConfig(options);
5
- return { applyToStack(stack) {
6
- // Registered at "finalizeRequest" (attempt level) rather than API-call level so each retry gets a deadline
7
- // computed from the actual remaining time at that moment. API-call level would cache a stale deadline
8
- // across retries, which grow more dangerous after backoff delays eat into remaining time.
9
- stack.add(deadlineMiddlewareHandler(config), {
10
- step: "finalizeRequest",
11
- name: "deadlineMiddleware",
12
- override: true
13
- });
14
- } };
15
- };
16
-
17
- //# sourceMappingURL=registration.js.map
@@ -1 +0,0 @@
1
- {"mappings":"AAKA,SAAS,mBAAmB;AAC5B,SAAS,iCAAiC;AAG1C,OAAO,MAAM,sBACX,YAC6B;CAC7B,MAAM,SAAS,YAAY,OAAO;CAElC,OAAO,EACL,aAAa,OAAO;;;;EAIlB,MAAM,IAAI,0BAAyC,MAAM,GAAG;GAC1D,MAAM;GACN,MAAM;GACN,UAAU;EACZ,CAAC;CACH,EACF;AACF","names":[],"sources":["src/registration.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { Pluggable } from \"@smithy/types\";\n\nimport { parseConfig } from \"./config.js\";\nimport { deadlineMiddlewareHandler } from \"./middleware.js\";\nimport type { DeadlineOptions } from \"./types.js\";\n\nexport const deadlineMiddleware = <Input extends object, Output extends object>(\n options?: DeadlineOptions,\n): Pluggable<Input, Output> => {\n const config = parseConfig(options);\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(deadlineMiddlewareHandler<Input, Output>(config), {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n });\n },\n };\n};\n"]}
@@ -1,5 +0,0 @@
1
- import type { DeadlineExceededError } from "./error.js";
2
- import type { DeadlineMiddlewareConfig } from "./types.js";
3
- export declare const emitDeadlineAbort: (error: DeadlineExceededError, config: DeadlineMiddlewareConfig) => Promise<void>;
4
-
5
- //# sourceMappingURL=telemetry.d.ts.map
@@ -1 +0,0 @@
1
- {"mappings":"AAGA,cAAc,6BAA6B;AAC3C,cAAc,gCAAgC;AA2G9C,OAAO,cAAM,oBACX,OAAO,uBACP,QAAQ,6BACP","names":[],"sources":["src/telemetry.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineMiddlewareConfig } from \"./types.js\";\n\n// OpenTelemetry is detected dynamically rather than declared as a peerDependency.\n// This avoids forcing all consumers to install @opentelemetry/api or suppress peer warnings.\n// Detection happens once at first use and the result is cached for the process lifetime.\ninterface AbortDetails {\n readonly deadlineMs: number;\n readonly flushBufferMs: number;\n readonly remainingMs: number;\n}\n\ninterface TelemetryEmitter {\n recordDeadlineAbort: (details: AbortDetails) => void;\n setDeadlineErrorStatus: (error: DeadlineExceededError) => void;\n}\n\nlet emitter: TelemetryEmitter | undefined;\nlet detected = false;\n\nconst detectEmitter = async (): Promise<TelemetryEmitter | undefined> => {\n if (detected) return emitter;\n detected = true;\n\n try {\n // Variable indirection prevents TypeScript from resolving the module\n // at compile time — keeps @opentelemetry/api as a purely optional runtime dep.\n const moduleName = \"@opentelemetry/api\";\n // oxlint-disable-next-line typescript/no-unsafe-assignment -- dynamic import of optional runtime dependency\n const otelApi: Record<string, unknown> = await import(/* webpackIgnore: true */ moduleName);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface\n const trace = otelApi[\"trace\"] as { getActiveSpan: () => unknown } | undefined;\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface\n const SpanStatusCode = otelApi[\"SpanStatusCode\"] as\n | {\n ERROR: number;\n }\n | undefined;\n\n if (!trace || !SpanStatusCode) {\n emitter = undefined;\n return emitter;\n }\n\n emitter = {\n recordDeadlineAbort(details: AbortDetails): void {\n try {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()\n const span = trace.getActiveSpan() as Record<string, unknown> | undefined;\n if (!span) return;\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.addEvent method\n const addEvent = span[\"addEvent\"] as\n | ((name: string, attributes: Record<string, unknown>) => void)\n | undefined;\n if (typeof addEvent !== \"function\") return;\n\n addEvent.call(span, \"lambda-deadline-middleware.abort\", {\n \"deadline.duration_ms\": details.deadlineMs,\n \"deadline.flush_buffer_ms\": details.flushBufferMs,\n \"deadline.remaining_ms\": details.remainingMs,\n });\n } catch {\n // Telemetry must never disrupt request processing\n }\n },\n\n setDeadlineErrorStatus(error: DeadlineExceededError): void {\n try {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()\n const span = trace.getActiveSpan() as Record<string, unknown> | undefined;\n if (!span) return;\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setStatus method\n const setStatus = span[\"setStatus\"] as\n | ((status: { code: number; message: string }) => void)\n | undefined;\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setAttribute method\n const setAttribute = span[\"setAttribute\"] as\n | ((key: string, value: unknown) => void)\n | undefined;\n\n if (typeof setStatus === \"function\") {\n setStatus.call(span, {\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n }\n\n if (typeof setAttribute === \"function\") {\n setAttribute.call(span, \"error.type\", \"DeadlineExceededError\");\n setAttribute.call(span, \"deadline.duration_ms\", error.deadlineMs);\n setAttribute.call(span, \"deadline.flush_buffer_ms\", error.flushBufferMs);\n setAttribute.call(span, \"deadline.remaining_ms\", error.remainingMs);\n }\n } catch {\n // Telemetry must never disrupt request processing\n }\n },\n };\n } catch {\n emitter = undefined;\n }\n\n return emitter;\n};\n\nexport const emitDeadlineAbort = async (\n error: DeadlineExceededError,\n config: DeadlineMiddlewareConfig,\n): Promise<void> => {\n try {\n if (!config.telemetryEnabled) return;\n\n const em = await detectEmitter();\n if (!em) return;\n\n em.recordDeadlineAbort({\n deadlineMs: error.deadlineMs,\n flushBufferMs: error.flushBufferMs,\n remainingMs: error.remainingMs,\n });\n\n em.setDeadlineErrorStatus(error);\n } catch {\n // Telemetry must never disrupt request processing\n }\n};\n"]}
package/dist/telemetry.js DELETED
@@ -1,82 +0,0 @@
1
- let emitter;
2
- let detected = false;
3
- const detectEmitter = async () => {
4
- if (detected) return emitter;
5
- detected = true;
6
- try {
7
- // Variable indirection prevents TypeScript from resolving the module
8
- // at compile time — keeps @opentelemetry/api as a purely optional runtime dep.
9
- const moduleName = "@opentelemetry/api";
10
- // oxlint-disable-next-line typescript/no-unsafe-assignment -- dynamic import of optional runtime dependency
11
- const otelApi = await import(
12
- /* webpackIgnore: true */
13
- moduleName
14
- );
15
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
16
- const trace = otelApi["trace"];
17
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
18
- const SpanStatusCode = otelApi["SpanStatusCode"];
19
- if (!trace || !SpanStatusCode) {
20
- emitter = undefined;
21
- return emitter;
22
- }
23
- emitter = {
24
- recordDeadlineAbort(details) {
25
- try {
26
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
27
- const span = trace.getActiveSpan();
28
- if (!span) return;
29
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.addEvent method
30
- const addEvent = span["addEvent"];
31
- if (typeof addEvent !== "function") return;
32
- addEvent.call(span, "lambda-deadline-middleware.abort", {
33
- "deadline.duration_ms": details.deadlineMs,
34
- "deadline.flush_buffer_ms": details.flushBufferMs,
35
- "deadline.remaining_ms": details.remainingMs
36
- });
37
- } catch {}
38
- },
39
- setDeadlineErrorStatus(error) {
40
- try {
41
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
42
- const span = trace.getActiveSpan();
43
- if (!span) return;
44
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setStatus method
45
- const setStatus = span["setStatus"];
46
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setAttribute method
47
- const setAttribute = span["setAttribute"];
48
- if (typeof setStatus === "function") {
49
- setStatus.call(span, {
50
- code: SpanStatusCode.ERROR,
51
- message: error.message
52
- });
53
- }
54
- if (typeof setAttribute === "function") {
55
- setAttribute.call(span, "error.type", "DeadlineExceededError");
56
- setAttribute.call(span, "deadline.duration_ms", error.deadlineMs);
57
- setAttribute.call(span, "deadline.flush_buffer_ms", error.flushBufferMs);
58
- setAttribute.call(span, "deadline.remaining_ms", error.remainingMs);
59
- }
60
- } catch {}
61
- }
62
- };
63
- } catch {
64
- emitter = undefined;
65
- }
66
- return emitter;
67
- };
68
- export const emitDeadlineAbort = async (error, config) => {
69
- try {
70
- if (!config.telemetryEnabled) return;
71
- const em = await detectEmitter();
72
- if (!em) return;
73
- em.recordDeadlineAbort({
74
- deadlineMs: error.deadlineMs,
75
- flushBufferMs: error.flushBufferMs,
76
- remainingMs: error.remainingMs
77
- });
78
- em.setDeadlineErrorStatus(error);
79
- } catch {}
80
- };
81
-
82
- //# sourceMappingURL=telemetry.js.map
@@ -1 +0,0 @@
1
- {"mappings":"AAoBA,IAAI;AACJ,IAAI,WAAW;AAEf,MAAM,gBAAgB,YAAmD;CACvE,IAAI,UAAU,OAAO;CACrB,WAAW;CAEX,IAAI;;;EAGF,MAAM,aAAa;;EAEnB,MAAM,UAAmC,MAAM;;GAAiC;;;EAEhF,MAAM,QAAQ,QAAQ;;EAGtB,MAAM,iBAAiB,QAAQ;EAM/B,IAAI,CAAC,SAAS,CAAC,gBAAgB;GAC7B,UAAU;GACV,OAAO;EACT;EAEA,UAAU;GACR,oBAAoB,SAA6B;IAC/C,IAAI;;KAEF,MAAM,OAAO,MAAM,cAAc;KACjC,IAAI,CAAC,MAAM;;KAGX,MAAM,WAAW,KAAK;KAGtB,IAAI,OAAO,aAAa,YAAY;KAEpC,SAAS,KAAK,MAAM,oCAAoC;MACtD,wBAAwB,QAAQ;MAChC,4BAA4B,QAAQ;MACpC,yBAAyB,QAAQ;KACnC,CAAC;IACH,QAAQ,CAER;GACF;GAEA,uBAAuB,OAAoC;IACzD,IAAI;;KAEF,MAAM,OAAO,MAAM,cAAc;KACjC,IAAI,CAAC,MAAM;;KAGX,MAAM,YAAY,KAAK;;KAIvB,MAAM,eAAe,KAAK;KAI1B,IAAI,OAAO,cAAc,YAAY;MACnC,UAAU,KAAK,MAAM;OACnB,MAAM,eAAe;OACrB,SAAS,MAAM;MACjB,CAAC;KACH;KAEA,IAAI,OAAO,iBAAiB,YAAY;MACtC,aAAa,KAAK,MAAM,cAAc,uBAAuB;MAC7D,aAAa,KAAK,MAAM,wBAAwB,MAAM,UAAU;MAChE,aAAa,KAAK,MAAM,4BAA4B,MAAM,aAAa;MACvE,aAAa,KAAK,MAAM,yBAAyB,MAAM,WAAW;KACpE;IACF,QAAQ,CAER;GACF;EACF;CACF,QAAQ;EACN,UAAU;CACZ;CAEA,OAAO;AACT;AAEA,OAAO,MAAM,oBAAoB,OAC/B,OACA,WACkB;CAClB,IAAI;EACF,IAAI,CAAC,OAAO,kBAAkB;EAE9B,MAAM,KAAK,MAAM,cAAc;EAC/B,IAAI,CAAC,IAAI;EAET,GAAG,oBAAoB;GACrB,YAAY,MAAM;GAClB,eAAe,MAAM;GACrB,aAAa,MAAM;EACrB,CAAC;EAED,GAAG,uBAAuB,KAAK;CACjC,QAAQ,CAER;AACF","names":[],"sources":["src/telemetry.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineMiddlewareConfig } from \"./types.js\";\n\n// OpenTelemetry is detected dynamically rather than declared as a peerDependency.\n// This avoids forcing all consumers to install @opentelemetry/api or suppress peer warnings.\n// Detection happens once at first use and the result is cached for the process lifetime.\ninterface AbortDetails {\n readonly deadlineMs: number;\n readonly flushBufferMs: number;\n readonly remainingMs: number;\n}\n\ninterface TelemetryEmitter {\n recordDeadlineAbort: (details: AbortDetails) => void;\n setDeadlineErrorStatus: (error: DeadlineExceededError) => void;\n}\n\nlet emitter: TelemetryEmitter | undefined;\nlet detected = false;\n\nconst detectEmitter = async (): Promise<TelemetryEmitter | undefined> => {\n if (detected) return emitter;\n detected = true;\n\n try {\n // Variable indirection prevents TypeScript from resolving the module\n // at compile time — keeps @opentelemetry/api as a purely optional runtime dep.\n const moduleName = \"@opentelemetry/api\";\n // oxlint-disable-next-line typescript/no-unsafe-assignment -- dynamic import of optional runtime dependency\n const otelApi: Record<string, unknown> = await import(/* webpackIgnore: true */ moduleName);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface\n const trace = otelApi[\"trace\"] as { getActiveSpan: () => unknown } | undefined;\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface\n const SpanStatusCode = otelApi[\"SpanStatusCode\"] as\n | {\n ERROR: number;\n }\n | undefined;\n\n if (!trace || !SpanStatusCode) {\n emitter = undefined;\n return emitter;\n }\n\n emitter = {\n recordDeadlineAbort(details: AbortDetails): void {\n try {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()\n const span = trace.getActiveSpan() as Record<string, unknown> | undefined;\n if (!span) return;\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.addEvent method\n const addEvent = span[\"addEvent\"] as\n | ((name: string, attributes: Record<string, unknown>) => void)\n | undefined;\n if (typeof addEvent !== \"function\") return;\n\n addEvent.call(span, \"lambda-deadline-middleware.abort\", {\n \"deadline.duration_ms\": details.deadlineMs,\n \"deadline.flush_buffer_ms\": details.flushBufferMs,\n \"deadline.remaining_ms\": details.remainingMs,\n });\n } catch {\n // Telemetry must never disrupt request processing\n }\n },\n\n setDeadlineErrorStatus(error: DeadlineExceededError): void {\n try {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()\n const span = trace.getActiveSpan() as Record<string, unknown> | undefined;\n if (!span) return;\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setStatus method\n const setStatus = span[\"setStatus\"] as\n | ((status: { code: number; message: string }) => void)\n | undefined;\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setAttribute method\n const setAttribute = span[\"setAttribute\"] as\n | ((key: string, value: unknown) => void)\n | undefined;\n\n if (typeof setStatus === \"function\") {\n setStatus.call(span, {\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n }\n\n if (typeof setAttribute === \"function\") {\n setAttribute.call(span, \"error.type\", \"DeadlineExceededError\");\n setAttribute.call(span, \"deadline.duration_ms\", error.deadlineMs);\n setAttribute.call(span, \"deadline.flush_buffer_ms\", error.flushBufferMs);\n setAttribute.call(span, \"deadline.remaining_ms\", error.remainingMs);\n }\n } catch {\n // Telemetry must never disrupt request processing\n }\n },\n };\n } catch {\n emitter = undefined;\n }\n\n return emitter;\n};\n\nexport const emitDeadlineAbort = async (\n error: DeadlineExceededError,\n config: DeadlineMiddlewareConfig,\n): Promise<void> => {\n try {\n if (!config.telemetryEnabled) return;\n\n const em = await detectEmitter();\n if (!em) return;\n\n em.recordDeadlineAbort({\n deadlineMs: error.deadlineMs,\n flushBufferMs: error.flushBufferMs,\n remainingMs: error.remainingMs,\n });\n\n em.setDeadlineErrorStatus(error);\n } catch {\n // Telemetry must never disrupt request processing\n }\n};\n"]}
package/src/config.ts DELETED
@@ -1,18 +0,0 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
-
4
- import { flushBufferMs } from "./types.js";
5
- import type { DeadlineMiddlewareConfig, DeadlineOptions } from "./types.js";
6
-
7
- // "Parse, don't validate": config is validated once here and returned as branded types.
8
- // Internal code can't receive unvalidated values. Invalid config throws TypeError at startup, not during requests.
9
- export const parseConfig = (raw: DeadlineOptions | undefined): DeadlineMiddlewareConfig => {
10
- const buffer = raw?.flushBufferMs ?? 1000;
11
- if (buffer < 0) {
12
- throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);
13
- }
14
- return {
15
- flushBufferMs: flushBufferMs(buffer),
16
- telemetryEnabled: raw?.telemetryEnabled ?? true,
17
- };
18
- };
@@ -1,27 +0,0 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
-
4
- import type { Pluggable } from "@smithy/types";
5
-
6
- import { parseConfig } from "./config.js";
7
- import { deadlineMiddlewareHandler } from "./middleware.js";
8
- import type { DeadlineOptions } from "./types.js";
9
-
10
- export const deadlineMiddleware = <Input extends object, Output extends object>(
11
- options?: DeadlineOptions,
12
- ): Pluggable<Input, Output> => {
13
- const config = parseConfig(options);
14
-
15
- return {
16
- applyToStack(stack) {
17
- // Registered at "finalizeRequest" (attempt level) rather than API-call level so each retry gets a deadline
18
- // computed from the actual remaining time at that moment. API-call level would cache a stale deadline
19
- // across retries, which grow more dangerous after backoff delays eat into remaining time.
20
- stack.add(deadlineMiddlewareHandler<Input, Output>(config), {
21
- step: "finalizeRequest",
22
- name: "deadlineMiddleware",
23
- override: true,
24
- });
25
- },
26
- };
27
- };