lambda-deadline-middleware 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +163 -85
  2. package/dist/context-store.d.ts +25 -2
  3. package/dist/context-store.d.ts.map +1 -1
  4. package/dist/context-store.js +45 -14
  5. package/dist/context-store.js.map +1 -1
  6. package/dist/error.d.ts +6 -7
  7. package/dist/error.d.ts.map +1 -1
  8. package/dist/error.js.map +1 -1
  9. package/dist/index.d.ts +3 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware.d.ts +3 -11
  14. package/dist/middleware.d.ts.map +1 -1
  15. package/dist/middleware.js +22 -73
  16. package/dist/middleware.js.map +1 -1
  17. package/dist/types.d.ts +0 -28
  18. package/dist/types.d.ts.map +1 -1
  19. package/dist/types.js +1 -17
  20. package/dist/types.js.map +1 -1
  21. package/package.json +22 -22
  22. package/src/context-store.ts +73 -21
  23. package/src/error.ts +6 -8
  24. package/src/index.ts +3 -12
  25. package/src/middleware.ts +36 -101
  26. package/src/types.ts +0 -46
  27. package/dist/config.d.ts +0 -4
  28. package/dist/config.d.ts.map +0 -1
  29. package/dist/config.js +0 -17
  30. package/dist/config.js.map +0 -1
  31. package/dist/handler-wrapper.d.ts +0 -13
  32. package/dist/handler-wrapper.d.ts.map +0 -1
  33. package/dist/handler-wrapper.js +0 -6
  34. package/dist/handler-wrapper.js.map +0 -1
  35. package/dist/registration.d.ts +0 -8
  36. package/dist/registration.d.ts.map +0 -1
  37. package/dist/registration.js +0 -17
  38. package/dist/registration.js.map +0 -1
  39. package/dist/telemetry.d.ts +0 -5
  40. package/dist/telemetry.d.ts.map +0 -1
  41. package/dist/telemetry.js +0 -82
  42. package/dist/telemetry.js.map +0 -1
  43. package/src/config.ts +0 -18
  44. package/src/handler-wrapper.ts +0 -19
  45. package/src/registration.ts +0 -27
  46. package/src/telemetry.ts +0 -132
package/src/types.ts CHANGED
@@ -1,52 +1,6 @@
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.
6
- declare const BrandSymbol: unique symbol;
7
-
8
- type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
9
-
10
- export type Milliseconds = Brand<number, "Milliseconds">;
11
-
12
- export type FlushBufferMs = Brand<number, "FlushBufferMs">;
13
-
14
- export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
15
-
16
- export const milliseconds = (value: number): Milliseconds => {
17
- if (!Number.isFinite(value)) {
18
- throw new TypeError(`milliseconds value must be finite, received: ${value}`);
19
- }
20
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
21
- return value as Milliseconds;
22
- };
23
-
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
4
  export interface DeadlineOptions {
50
5
  readonly flushBufferMs?: number;
51
- readonly telemetryEnabled?: boolean;
52
6
  }
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,13 +0,0 @@
1
- import type { LambdaContextLike } from "./context-store.js";
2
- import type { DeadlineOptions } from "./types.js";
3
- type AsyncHandler<
4
- TEvent,
5
- TResult
6
- > = (event: TEvent, context: LambdaContextLike) => Promise<TResult>;
7
- export declare const withLambdaDeadline: <
8
- TEvent,
9
- TResult
10
- >(handler: AsyncHandler<TEvent, TResult>, _options?: DeadlineOptions) => AsyncHandler<TEvent, TResult>;
11
- export {};
12
-
13
- //# sourceMappingURL=handler-wrapper.d.ts.map
@@ -1 +0,0 @@
1
- {"mappings":"AAIA,cAAc,yBAAyB;AACvC,cAAc,uBAAuB;KAEhC;CAAa;CAAQ;KACxB,OAAO,QACP,SAAS,sBACN,QAAQ;AAEb,OAAO,cAAM;CACV;CAAQ;EACP,SAAS,aAAa,QAAQ,UAC9B,WAAW,oBACV,aAAa,QAAQ","names":[],"sources":["src/handler-wrapper.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { run } from \"./context-store.js\";\nimport type { LambdaContextLike } from \"./context-store.js\";\nimport type { DeadlineOptions } from \"./types.js\";\n\ntype AsyncHandler<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TResult>(\n handler: AsyncHandler<TEvent, TResult>,\n _options?: DeadlineOptions,\n ): AsyncHandler<TEvent, TResult> =>\n async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>\n run(context, async () => handler(event, context));\n"]}
@@ -1,6 +0,0 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
- import { run } from "./context-store.js";
4
- export const withLambdaDeadline = (handler, _options) => async (event, context) => run(context, async () => handler(event, context));
5
-
6
- //# sourceMappingURL=handler-wrapper.js.map
@@ -1 +0,0 @@
1
- {"mappings":";;AAGA,SAAS,WAAW;AASpB,OAAO,MAAM,sBAET,SACA,aAEF,OAAO,OAAe,YACpB,IAAI,SAAS,YAAY,QAAQ,OAAO,OAAO,CAAC","names":[],"sources":["src/handler-wrapper.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { run } from \"./context-store.js\";\nimport type { LambdaContextLike } from \"./context-store.js\";\nimport type { DeadlineOptions } from \"./types.js\";\n\ntype AsyncHandler<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult>;\n\nexport const withLambdaDeadline =\n <TEvent, TResult>(\n handler: AsyncHandler<TEvent, TResult>,\n _options?: DeadlineOptions,\n ): AsyncHandler<TEvent, TResult> =>\n async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>\n run(context, async () => handler(event, context));\n"]}
@@ -1,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,19 +0,0 @@
1
- // SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
2
- // SPDX-License-Identifier: MIT
3
-
4
- import { run } from "./context-store.js";
5
- import type { LambdaContextLike } from "./context-store.js";
6
- import type { DeadlineOptions } from "./types.js";
7
-
8
- type AsyncHandler<TEvent, TResult> = (
9
- event: TEvent,
10
- context: LambdaContextLike,
11
- ) => Promise<TResult>;
12
-
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> =>
19
- run(context, async () => handler(event, context));
@@ -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
- };
package/src/telemetry.ts DELETED
@@ -1,132 +0,0 @@
1
- // SPDX-FileCopyrightText: 2026 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
- // OpenTelemetry is detected dynamically rather than declared as a peerDependency.
8
- // This avoids forcing all consumers to install @opentelemetry/api or suppress peer warnings.
9
- // Detection happens once at first use and the result is cached for the process lifetime.
10
- interface AbortDetails {
11
- readonly deadlineMs: number;
12
- readonly flushBufferMs: number;
13
- readonly remainingMs: number;
14
- }
15
-
16
- interface TelemetryEmitter {
17
- recordDeadlineAbort: (details: AbortDetails) => void;
18
- setDeadlineErrorStatus: (error: DeadlineExceededError) => void;
19
- }
20
-
21
- let emitter: TelemetryEmitter | undefined;
22
- let detected = false;
23
-
24
- const detectEmitter = async (): Promise<TelemetryEmitter | undefined> => {
25
- if (detected) return emitter;
26
- detected = true;
27
-
28
- try {
29
- // Variable indirection prevents TypeScript from resolving the module
30
- // at compile time — keeps @opentelemetry/api as a purely optional runtime dep.
31
- const moduleName = "@opentelemetry/api";
32
- // oxlint-disable-next-line typescript/no-unsafe-assignment -- dynamic import of optional runtime dependency
33
- const otelApi: Record<string, unknown> = await import(/* webpackIgnore: true */ moduleName);
34
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
35
- const trace = otelApi["trace"] as { getActiveSpan: () => unknown } | undefined;
36
-
37
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
38
- const SpanStatusCode = otelApi["SpanStatusCode"] as
39
- | {
40
- ERROR: number;
41
- }
42
- | undefined;
43
-
44
- if (!trace || !SpanStatusCode) {
45
- emitter = undefined;
46
- return emitter;
47
- }
48
-
49
- emitter = {
50
- recordDeadlineAbort(details: AbortDetails): void {
51
- try {
52
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
53
- const span = trace.getActiveSpan() as Record<string, unknown> | undefined;
54
- if (!span) return;
55
-
56
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.addEvent method
57
- const addEvent = span["addEvent"] as
58
- | ((name: string, attributes: Record<string, unknown>) => void)
59
- | undefined;
60
- if (typeof addEvent !== "function") return;
61
-
62
- addEvent.call(span, "lambda-deadline-middleware.abort", {
63
- "deadline.duration_ms": details.deadlineMs,
64
- "deadline.flush_buffer_ms": details.flushBufferMs,
65
- "deadline.remaining_ms": details.remainingMs,
66
- });
67
- } catch {
68
- // Telemetry must never disrupt request processing
69
- }
70
- },
71
-
72
- setDeadlineErrorStatus(error: DeadlineExceededError): void {
73
- try {
74
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
75
- const span = trace.getActiveSpan() as Record<string, unknown> | undefined;
76
- if (!span) return;
77
-
78
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setStatus method
79
- const setStatus = span["setStatus"] as
80
- | ((status: { code: number; message: string }) => void)
81
- | undefined;
82
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setAttribute method
83
- const setAttribute = span["setAttribute"] as
84
- | ((key: string, value: unknown) => void)
85
- | undefined;
86
-
87
- if (typeof setStatus === "function") {
88
- setStatus.call(span, {
89
- code: SpanStatusCode.ERROR,
90
- message: error.message,
91
- });
92
- }
93
-
94
- if (typeof setAttribute === "function") {
95
- setAttribute.call(span, "error.type", "DeadlineExceededError");
96
- setAttribute.call(span, "deadline.duration_ms", error.deadlineMs);
97
- setAttribute.call(span, "deadline.flush_buffer_ms", error.flushBufferMs);
98
- setAttribute.call(span, "deadline.remaining_ms", error.remainingMs);
99
- }
100
- } catch {
101
- // Telemetry must never disrupt request processing
102
- }
103
- },
104
- };
105
- } catch {
106
- emitter = undefined;
107
- }
108
-
109
- return emitter;
110
- };
111
-
112
- export const emitDeadlineAbort = async (
113
- error: DeadlineExceededError,
114
- config: DeadlineMiddlewareConfig,
115
- ): Promise<void> => {
116
- try {
117
- if (!config.telemetryEnabled) return;
118
-
119
- const em = await detectEmitter();
120
- if (!em) return;
121
-
122
- em.recordDeadlineAbort({
123
- deadlineMs: error.deadlineMs,
124
- flushBufferMs: error.flushBufferMs,
125
- remainingMs: error.remainingMs,
126
- });
127
-
128
- em.setDeadlineErrorStatus(error);
129
- } catch {
130
- // Telemetry must never disrupt request processing
131
- }
132
- };