lambda-deadline-middleware 0.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/SECURITY.md +95 -0
  4. package/dist/config.d.ts +4 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +15 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/context-store.d.ts +7 -0
  9. package/dist/context-store.d.ts.map +1 -0
  10. package/dist/context-store.js +24 -0
  11. package/dist/context-store.js.map +1 -0
  12. package/dist/error.d.ts +17 -0
  13. package/dist/error.d.ts.map +1 -0
  14. package/dist/error.js +21 -0
  15. package/dist/error.js.map +1 -0
  16. package/dist/handler-wrapper.d.ts +13 -0
  17. package/dist/handler-wrapper.d.ts.map +1 -0
  18. package/dist/handler-wrapper.js +8 -0
  19. package/dist/handler-wrapper.js.map +1 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +8 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware.d.ts +12 -0
  25. package/dist/middleware.d.ts.map +1 -0
  26. package/dist/middleware.js +76 -0
  27. package/dist/middleware.js.map +1 -0
  28. package/dist/registration.d.ts +10 -0
  29. package/dist/registration.d.ts.map +1 -0
  30. package/dist/registration.js +23 -0
  31. package/dist/registration.js.map +1 -0
  32. package/dist/telemetry.d.ts +5 -0
  33. package/dist/telemetry.d.ts.map +1 -0
  34. package/dist/telemetry.js +82 -0
  35. package/dist/telemetry.js.map +1 -0
  36. package/dist/types.d.ts +33 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +19 -0
  39. package/dist/types.js.map +1 -0
  40. package/package.json +73 -0
  41. package/src/config.ts +16 -0
  42. package/src/context-store.ts +36 -0
  43. package/src/error.ts +34 -0
  44. package/src/handler-wrapper.ts +19 -0
  45. package/src/index.ts +18 -0
  46. package/src/middleware.ts +109 -0
  47. package/src/registration.ts +36 -0
  48. package/src/telemetry.ts +129 -0
  49. package/src/types.ts +50 -0
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,129 @@
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
+ }
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ declare const BrandSymbol: unique symbol;
5
+
6
+ type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
7
+
8
+ export type Milliseconds = Brand<number, "Milliseconds">;
9
+
10
+ export type FlushBufferMs = Brand<number, "FlushBufferMs">;
11
+
12
+ export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
13
+
14
+ export function milliseconds(value: number): Milliseconds {
15
+ if (!Number.isFinite(value)) {
16
+ throw new TypeError(`milliseconds value must be finite, received: ${value}`);
17
+ }
18
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
19
+ 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
+ }
46
+
47
+ export interface DeadlineOptions {
48
+ readonly flushBufferMs?: number;
49
+ readonly telemetryEnabled?: boolean;
50
+ }