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.
- package/README.md +163 -85
- package/dist/context-store.d.ts +25 -2
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +45 -14
- package/dist/context-store.js.map +1 -1
- package/dist/error.d.ts +6 -7
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +3 -11
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +22 -73
- package/dist/middleware.js.map +1 -1
- package/dist/types.d.ts +0 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -17
- package/dist/types.js.map +1 -1
- package/package.json +22 -22
- package/src/context-store.ts +73 -21
- package/src/error.ts +6 -8
- package/src/index.ts +3 -12
- package/src/middleware.ts +36 -101
- package/src/types.ts +0 -46
- package/dist/config.d.ts +0 -4
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -17
- package/dist/config.js.map +0 -1
- package/dist/handler-wrapper.d.ts +0 -13
- package/dist/handler-wrapper.d.ts.map +0 -1
- package/dist/handler-wrapper.js +0 -6
- package/dist/handler-wrapper.js.map +0 -1
- package/dist/registration.d.ts +0 -8
- package/dist/registration.d.ts.map +0 -1
- package/dist/registration.js +0 -17
- package/dist/registration.js.map +0 -1
- package/dist/telemetry.d.ts +0 -5
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -82
- package/dist/telemetry.js.map +0 -1
- package/src/config.ts +0 -18
- package/src/handler-wrapper.ts +0 -19
- package/src/registration.ts +0 -27
- 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
package/dist/config.d.ts.map
DELETED
|
@@ -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
|
package/dist/config.js.map
DELETED
|
@@ -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"]}
|
package/dist/handler-wrapper.js
DELETED
|
@@ -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"]}
|
package/dist/registration.d.ts
DELETED
|
@@ -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"]}
|
package/dist/registration.js
DELETED
|
@@ -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
|
package/dist/registration.js.map
DELETED
|
@@ -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"]}
|
package/dist/telemetry.d.ts
DELETED
|
@@ -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
|
package/dist/telemetry.d.ts.map
DELETED
|
@@ -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
|
package/dist/telemetry.js.map
DELETED
|
@@ -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
|
-
};
|
package/src/handler-wrapper.ts
DELETED
|
@@ -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));
|
package/src/registration.ts
DELETED
|
@@ -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
|
-
};
|