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
package/src/index.ts CHANGED
@@ -1,18 +1,11 @@
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
  export { withLambdaDeadline } from "./handler-wrapper.js";
5
- export { deadlineMiddleware, withDeadline } 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,109 +1,96 @@
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 { 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 function computeDeadline(config: DeadlineMiddlewareConfig): DeadlineComputation {
18
- const remaining = getRemainingTimeInMillis();
12
+ import { getRemainingTimeInMillis } from "./context-store.js";
13
+ import { DeadlineExceededError } from "./error.js";
14
+ import { milliseconds } from "./types.js";
19
15
 
20
- if (remaining === undefined) {
21
- return { kind: "no-context" };
22
- }
16
+ import type { DeadlineOptions } from "./types.js";
23
17
 
24
- const deadline = remaining - config.flushBufferMs;
18
+ export const composeSignals = (
19
+ existing: AbortSignal | undefined,
20
+ deadline: AbortSignal,
21
+ ): AbortSignal => {
22
+ if (existing === undefined) return deadline;
23
+ return AbortSignal.any([existing, deadline]);
24
+ };
25
25
 
26
- if (deadline <= 0) {
27
- return {
28
- kind: "insufficient-time",
29
- remaining: milliseconds(remaining),
30
- buffer: config.flushBufferMs,
31
- };
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
32
  }
33
+ const flushBufferMs = milliseconds(raw);
33
34
 
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 function 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
35
  return {
58
- controller,
59
- [Symbol.dispose]() {
60
- clearTimeout(timeoutId);
61
- },
62
- };
63
- }
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);
64
48
 
65
- export function composeSignals(
66
- existing: AbortSignal | undefined,
67
- deadline: AbortSignal,
68
- ): AbortSignal {
69
- if (existing === undefined) return deadline;
70
- return AbortSignal.any([existing, deadline]);
71
- }
49
+ const deadline = remaining - flushBufferMs;
72
50
 
73
- export function deadlineMiddlewareHandler(
74
- config: DeadlineMiddlewareConfig,
75
- ): FinalizeRequestMiddleware<object, object> {
76
- return (
77
- next: FinalizeHandler<object, object>,
78
- _context: HandlerExecutionContext,
79
- ): FinalizeHandler<object, object> =>
80
- // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union
81
- async (args: FinalizeHandlerArguments<object>): Promise<FinalizeHandlerOutput<object>> => {
82
- const computation = computeDeadline(config);
51
+ if (deadline <= 0) {
52
+ throw new DeadlineExceededError({
53
+ deadlineMs: milliseconds(0),
54
+ flushBufferMs,
55
+ remainingMs: milliseconds(remaining),
56
+ });
57
+ }
83
58
 
84
- switch (computation.kind) {
85
- case "no-context":
86
- return next(args);
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);
87
69
 
88
- case "insufficient-time":
89
- throw new DeadlineExceededError({
90
- deadlineMs: milliseconds(0),
91
- flushBufferMs: computation.buffer,
92
- remainingMs: computation.remaining,
93
- });
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
+ };
94
77
 
95
- case "deadline": {
96
- using timer = createDeadlineTimer(computation.value, config);
97
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
98
- const request = args.request as { signal?: AbortSignal } | undefined;
99
- const signal = composeSignals(request?.signal, timer.controller.signal);
100
- const result = await next({
101
- ...args,
102
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
103
- request: { ...(args.request as object), signal },
104
- });
105
- return result;
106
- }
107
- }
108
- };
109
- }
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
+ },
95
+ };
96
+ };
package/src/types.ts CHANGED
@@ -1,50 +1,22 @@
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
+ // 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.
4
6
  declare const BrandSymbol: unique symbol;
5
7
 
6
8
  type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
7
9
 
8
10
  export type Milliseconds = Brand<number, "Milliseconds">;
9
11
 
10
- export type FlushBufferMs = Brand<number, "FlushBufferMs">;
11
-
12
- export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
13
-
14
- export function milliseconds(value: number): Milliseconds {
12
+ export const milliseconds = (value: number): Milliseconds => {
15
13
  if (!Number.isFinite(value)) {
16
14
  throw new TypeError(`milliseconds value must be finite, received: ${value}`);
17
15
  }
18
16
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
19
17
  return value as Milliseconds;
20
- }
21
-
22
- export function flushBufferMs(value: number): FlushBufferMs {
23
- if (!Number.isFinite(value)) {
24
- throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);
25
- }
26
- if (value < 0) {
27
- throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);
28
- }
29
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
30
- return value as FlushBufferMs;
31
- }
32
-
33
- export type DeadlineComputation =
34
- | { readonly kind: "deadline"; readonly value: RequestDeadlineMs }
35
- | {
36
- readonly kind: "insufficient-time";
37
- readonly remaining: Milliseconds;
38
- readonly buffer: FlushBufferMs;
39
- }
40
- | { readonly kind: "no-context" };
41
-
42
- export interface DeadlineMiddlewareConfig {
43
- readonly flushBufferMs: FlushBufferMs;
44
- readonly telemetryEnabled: boolean;
45
- }
18
+ };
46
19
 
47
20
  export interface DeadlineOptions {
48
21
  readonly flushBufferMs?: number;
49
- readonly telemetryEnabled?: boolean;
50
22
  }
package/dist/config.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import type { DeadlineMiddlewareConfig, DeadlineOptions } from "./types.js";
2
- export declare function parseConfig(raw: DeadlineOptions | undefined): DeadlineMiddlewareConfig;
3
-
4
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"mappings":"AAIA,cAAc,0BAA0B,uBAAuB;AAE/D,OAAO,iBAAS,YAAY,KAAK,8BAA8B","names":[],"sources":["src/config.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { flushBufferMs } from \"./types.js\";\nimport type { DeadlineMiddlewareConfig, DeadlineOptions } from \"./types.js\";\n\nexport function 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,15 +0,0 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
- import { flushBufferMs } from "./types.js";
4
- export function parseConfig(raw) {
5
- const buffer = raw?.flushBufferMs ?? 1e3;
6
- if (buffer < 0) {
7
- throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);
8
- }
9
- return {
10
- flushBufferMs: flushBufferMs(buffer),
11
- telemetryEnabled: raw?.telemetryEnabled ?? true
12
- };
13
- }
14
-
15
- //# sourceMappingURL=config.js.map
@@ -1 +0,0 @@
1
- {"mappings":";;AAGA,SAAS,qBAAqB;AAG9B,OAAO,SAAS,YAAY,KAA4D;CACtF,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: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { flushBufferMs } from \"./types.js\";\nimport type { DeadlineMiddlewareConfig, DeadlineOptions } from \"./types.js\";\n\nexport function 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,10 +0,0 @@
1
- import type { Pluggable } from "@smithy/types";
2
- import type { DeadlineOptions } from "./types.js";
3
- export declare function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object>;
4
- export declare function withDeadline<T extends {
5
- middlewareStack: {
6
- use: (pluggable: Pluggable<any, any>) => void;
7
- };
8
- }>(client: T, options?: DeadlineOptions): T;
9
-
10
- //# sourceMappingURL=registration.d.ts.map
@@ -1 +0,0 @@
1
- {"mappings":"AAGA,cAAc,iBAAiB;AAI/B,cAAc,uBAAuB;AAErC,OAAO,iBAAS,mBAAmB,UAAU,kBAAkB;AAkB/D,OAAO,iBAAS,aAEd,UAAU;CAAE,iBAAiB;EAAE,MAAM,WAAW;CAA6B;AAAE,GAC/E,QAAQ,GAAG,UAAU,kBAAkB","names":[],"sources":["src/registration.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 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 function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object> {\n const config = parseConfig(options);\n\n return {\n applyToStack(stack) {\n stack.add(deadlineMiddlewareHandler(config), {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n });\n },\n };\n}\n\n// WeakSet allows GC of discarded clients in long-running processes\n// while still preventing duplicate middleware registration.\nconst registeredClients = new WeakSet<object>();\n\nexport function withDeadline<\n // oxlint-disable-next-line typescript/no-explicit-any -- AWS SDK clients use varying ServiceInput/OutputTypes generics, making `any` the only correct constraint here\n T extends { middlewareStack: { use: (pluggable: Pluggable<any, any>) => void } },\n>(client: T, options?: DeadlineOptions): T {\n if (registeredClients.has(client)) return client;\n registeredClients.add(client);\n client.middlewareStack.use(deadlineMiddleware(options));\n return client;\n}\n"]}
@@ -1,23 +0,0 @@
1
- import { parseConfig } from "./config.js";
2
- import { deadlineMiddlewareHandler } from "./middleware.js";
3
- export function deadlineMiddleware(options) {
4
- const config = parseConfig(options);
5
- return { applyToStack(stack) {
6
- stack.add(deadlineMiddlewareHandler(config), {
7
- step: "finalizeRequest",
8
- name: "deadlineMiddleware",
9
- override: true
10
- });
11
- } };
12
- }
13
- // WeakSet allows GC of discarded clients in long-running processes
14
- // while still preventing duplicate middleware registration.
15
- const registeredClients = new WeakSet();
16
- export function withDeadline(client, options) {
17
- if (registeredClients.has(client)) return client;
18
- registeredClients.add(client);
19
- client.middlewareStack.use(deadlineMiddleware(options));
20
- return client;
21
- }
22
-
23
- //# sourceMappingURL=registration.js.map
@@ -1 +0,0 @@
1
- {"mappings":"AAKA,SAAS,mBAAmB;AAC5B,SAAS,iCAAiC;AAG1C,OAAO,SAAS,mBAAmB,SAAsD;CACvF,MAAM,SAAS,YAAY,OAAO;CAElC,OAAO,EACL,aAAa,OAAO;EAClB,MAAM,IAAI,0BAA0B,MAAM,GAAG;GAC3C,MAAM;GACN,MAAM;GACN,UAAU;EACZ,CAAC;CACH,EACF;AACF;;;AAIA,MAAM,oBAAoB,IAAI,QAAgB;AAE9C,OAAO,SAAS,aAGd,QAAW,SAA8B;CACzC,IAAI,kBAAkB,IAAI,MAAM,GAAG,OAAO;CAC1C,kBAAkB,IAAI,MAAM;CAC5B,OAAO,gBAAgB,IAAI,mBAAmB,OAAO,CAAC;CACtD,OAAO;AACT","names":[],"sources":["src/registration.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 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 function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object> {\n const config = parseConfig(options);\n\n return {\n applyToStack(stack) {\n stack.add(deadlineMiddlewareHandler(config), {\n step: \"finalizeRequest\",\n name: \"deadlineMiddleware\",\n override: true,\n });\n },\n };\n}\n\n// WeakSet allows GC of discarded clients in long-running processes\n// while still preventing duplicate middleware registration.\nconst registeredClients = new WeakSet<object>();\n\nexport function withDeadline<\n // oxlint-disable-next-line typescript/no-explicit-any -- AWS SDK clients use varying ServiceInput/OutputTypes generics, making `any` the only correct constraint here\n T extends { middlewareStack: { use: (pluggable: Pluggable<any, any>) => void } },\n>(client: T, options?: DeadlineOptions): T {\n if (registeredClients.has(client)) return client;\n registeredClients.add(client);\n client.middlewareStack.use(deadlineMiddleware(options));\n return client;\n}\n"]}
@@ -1,5 +0,0 @@
1
- import type { DeadlineExceededError } from "./error.js";
2
- import type { DeadlineMiddlewareConfig } from "./types.js";
3
- export declare function 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;AAwG9C,OAAO,iBAAe,kBACpB,OAAO,uBACP,QAAQ,2BACP","names":[],"sources":["src/telemetry.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineMiddlewareConfig } from \"./types.js\";\n\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\nasync function detectEmitter(): 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 async function emitDeadlineAbort(\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
- async function detectEmitter() {
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 async function emitDeadlineAbort(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":"AAiBA,IAAI;AACJ,IAAI,WAAW;AAEf,eAAe,gBAAuD;CACpE,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,eAAe,kBACpB,OACA,QACe;CACf,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: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport type { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineMiddlewareConfig } from \"./types.js\";\n\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\nasync function detectEmitter(): 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 async function emitDeadlineAbort(\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,16 +0,0 @@
1
- // SPDX-FileCopyrightText: 2024 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
- export function parseConfig(raw: DeadlineOptions | undefined): DeadlineMiddlewareConfig {
8
- const buffer = raw?.flushBufferMs ?? 1000;
9
- if (buffer < 0) {
10
- throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);
11
- }
12
- return {
13
- flushBufferMs: flushBufferMs(buffer),
14
- telemetryEnabled: raw?.telemetryEnabled ?? true,
15
- };
16
- }
@@ -1,36 +0,0 @@
1
- // SPDX-FileCopyrightText: 2024 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 function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object> {
11
- const config = parseConfig(options);
12
-
13
- return {
14
- applyToStack(stack) {
15
- stack.add(deadlineMiddlewareHandler(config), {
16
- step: "finalizeRequest",
17
- name: "deadlineMiddleware",
18
- override: true,
19
- });
20
- },
21
- };
22
- }
23
-
24
- // WeakSet allows GC of discarded clients in long-running processes
25
- // while still preventing duplicate middleware registration.
26
- const registeredClients = new WeakSet<object>();
27
-
28
- export function withDeadline<
29
- // oxlint-disable-next-line typescript/no-explicit-any -- AWS SDK clients use varying ServiceInput/OutputTypes generics, making `any` the only correct constraint here
30
- T extends { middlewareStack: { use: (pluggable: Pluggable<any, any>) => void } },
31
- >(client: T, options?: DeadlineOptions): T {
32
- if (registeredClients.has(client)) return client;
33
- registeredClients.add(client);
34
- client.middlewareStack.use(deadlineMiddleware(options));
35
- return client;
36
- }
package/src/telemetry.ts DELETED
@@ -1,129 +0,0 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
-
4
- import type { DeadlineExceededError } from "./error.js";
5
- import type { DeadlineMiddlewareConfig } from "./types.js";
6
-
7
- interface AbortDetails {
8
- readonly deadlineMs: number;
9
- readonly flushBufferMs: number;
10
- readonly remainingMs: number;
11
- }
12
-
13
- interface TelemetryEmitter {
14
- recordDeadlineAbort: (details: AbortDetails) => void;
15
- setDeadlineErrorStatus: (error: DeadlineExceededError) => void;
16
- }
17
-
18
- let emitter: TelemetryEmitter | undefined;
19
- let detected = false;
20
-
21
- async function detectEmitter(): Promise<TelemetryEmitter | undefined> {
22
- if (detected) return emitter;
23
- detected = true;
24
-
25
- try {
26
- // Variable indirection prevents TypeScript from resolving the module
27
- // at compile time — keeps @opentelemetry/api as a purely optional runtime dep.
28
- const moduleName = "@opentelemetry/api";
29
- // oxlint-disable-next-line typescript/no-unsafe-assignment -- dynamic import of optional runtime dependency
30
- const otelApi: Record<string, unknown> = await import(/* webpackIgnore: true */ moduleName);
31
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
32
- const trace = otelApi["trace"] as { getActiveSpan: () => unknown } | undefined;
33
-
34
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
35
- const SpanStatusCode = otelApi["SpanStatusCode"] as
36
- | {
37
- ERROR: number;
38
- }
39
- | undefined;
40
-
41
- if (!trace || !SpanStatusCode) {
42
- emitter = undefined;
43
- return emitter;
44
- }
45
-
46
- emitter = {
47
- recordDeadlineAbort(details: AbortDetails): void {
48
- try {
49
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
50
- const span = trace.getActiveSpan() as Record<string, unknown> | undefined;
51
- if (!span) return;
52
-
53
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.addEvent method
54
- const addEvent = span["addEvent"] as
55
- | ((name: string, attributes: Record<string, unknown>) => void)
56
- | undefined;
57
- if (typeof addEvent !== "function") return;
58
-
59
- addEvent.call(span, "lambda-deadline-middleware.abort", {
60
- "deadline.duration_ms": details.deadlineMs,
61
- "deadline.flush_buffer_ms": details.flushBufferMs,
62
- "deadline.remaining_ms": details.remainingMs,
63
- });
64
- } catch {
65
- // Telemetry must never disrupt request processing
66
- }
67
- },
68
-
69
- setDeadlineErrorStatus(error: DeadlineExceededError): void {
70
- try {
71
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
72
- const span = trace.getActiveSpan() as Record<string, unknown> | undefined;
73
- if (!span) return;
74
-
75
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setStatus method
76
- const setStatus = span["setStatus"] as
77
- | ((status: { code: number; message: string }) => void)
78
- | undefined;
79
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setAttribute method
80
- const setAttribute = span["setAttribute"] as
81
- | ((key: string, value: unknown) => void)
82
- | undefined;
83
-
84
- if (typeof setStatus === "function") {
85
- setStatus.call(span, {
86
- code: SpanStatusCode.ERROR,
87
- message: error.message,
88
- });
89
- }
90
-
91
- if (typeof setAttribute === "function") {
92
- setAttribute.call(span, "error.type", "DeadlineExceededError");
93
- setAttribute.call(span, "deadline.duration_ms", error.deadlineMs);
94
- setAttribute.call(span, "deadline.flush_buffer_ms", error.flushBufferMs);
95
- setAttribute.call(span, "deadline.remaining_ms", error.remainingMs);
96
- }
97
- } catch {
98
- // Telemetry must never disrupt request processing
99
- }
100
- },
101
- };
102
- } catch {
103
- emitter = undefined;
104
- }
105
-
106
- return emitter;
107
- }
108
-
109
- export async function emitDeadlineAbort(
110
- error: DeadlineExceededError,
111
- config: DeadlineMiddlewareConfig,
112
- ): Promise<void> {
113
- try {
114
- if (!config.telemetryEnabled) return;
115
-
116
- const em = await detectEmitter();
117
- if (!em) return;
118
-
119
- em.recordDeadlineAbort({
120
- deadlineMs: error.deadlineMs,
121
- flushBufferMs: error.flushBufferMs,
122
- remainingMs: error.remainingMs,
123
- });
124
-
125
- em.setDeadlineErrorStatus(error);
126
- } catch {
127
- // Telemetry must never disrupt request processing
128
- }
129
- }