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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/SECURITY.md +95 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -0
- package/dist/context-store.d.ts +7 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +24 -0
- package/dist/context-store.js.map +1 -0
- package/dist/error.d.ts +17 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +21 -0
- package/dist/error.js.map +1 -0
- package/dist/handler-wrapper.d.ts +13 -0
- package/dist/handler-wrapper.d.ts.map +1 -0
- package/dist/handler-wrapper.js +8 -0
- package/dist/handler-wrapper.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +12 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +76 -0
- package/dist/middleware.js.map +1 -0
- package/dist/registration.d.ts +10 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +23 -0
- package/dist/registration.js.map +1 -0
- package/dist/telemetry.d.ts +5 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +82 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -0
- package/src/config.ts +16 -0
- package/src/context-store.ts +36 -0
- package/src/error.ts +34 -0
- package/src/handler-wrapper.ts +19 -0
- package/src/index.ts +18 -0
- package/src/middleware.ts +109 -0
- package/src/registration.ts +36 -0
- package/src/telemetry.ts +129 -0
- package/src/types.ts +50 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { getRemainingTimeInMillis } from "./context-store.js";
|
|
4
|
+
import { DeadlineExceededError } from "./error.js";
|
|
5
|
+
import { milliseconds } from "./types.js";
|
|
6
|
+
export function computeDeadline(config) {
|
|
7
|
+
const remaining = getRemainingTimeInMillis();
|
|
8
|
+
if (remaining === undefined) {
|
|
9
|
+
return { kind: "no-context" };
|
|
10
|
+
}
|
|
11
|
+
const deadline = remaining - config.flushBufferMs;
|
|
12
|
+
if (deadline <= 0) {
|
|
13
|
+
return {
|
|
14
|
+
kind: "insufficient-time",
|
|
15
|
+
remaining: milliseconds(remaining),
|
|
16
|
+
buffer: config.flushBufferMs
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above
|
|
20
|
+
return {
|
|
21
|
+
kind: "deadline",
|
|
22
|
+
value: deadline
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function createDeadlineTimer(deadlineMs, config) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const remaining = milliseconds(deadlineMs + config.flushBufferMs);
|
|
28
|
+
const error = new DeadlineExceededError({
|
|
29
|
+
deadlineMs: milliseconds(deadlineMs),
|
|
30
|
+
flushBufferMs: config.flushBufferMs,
|
|
31
|
+
remainingMs: remaining
|
|
32
|
+
});
|
|
33
|
+
const timeoutId = setTimeout(() => {
|
|
34
|
+
controller.abort(error);
|
|
35
|
+
}, deadlineMs);
|
|
36
|
+
return {
|
|
37
|
+
controller,
|
|
38
|
+
[Symbol.dispose]() {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function composeSignals(existing, deadline) {
|
|
44
|
+
if (existing === undefined) return deadline;
|
|
45
|
+
return AbortSignal.any([existing, deadline]);
|
|
46
|
+
}
|
|
47
|
+
export function deadlineMiddlewareHandler(config) {
|
|
48
|
+
return (next, _context) => async (args) => {
|
|
49
|
+
const computation = computeDeadline(config);
|
|
50
|
+
switch (computation.kind) {
|
|
51
|
+
case "no-context": return next(args);
|
|
52
|
+
case "insufficient-time": throw new DeadlineExceededError({
|
|
53
|
+
deadlineMs: milliseconds(0),
|
|
54
|
+
flushBufferMs: computation.buffer,
|
|
55
|
+
remainingMs: computation.remaining
|
|
56
|
+
});
|
|
57
|
+
case "deadline": {
|
|
58
|
+
using timer = createDeadlineTimer(computation.value, config);
|
|
59
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
|
|
60
|
+
const request = args.request;
|
|
61
|
+
const signal = composeSignals(request?.signal, timer.controller.signal);
|
|
62
|
+
const result = await next({
|
|
63
|
+
...args,
|
|
64
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
|
|
65
|
+
request: {
|
|
66
|
+
...args.request,
|
|
67
|
+
signal
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":";;AAGA,SAAS,gCAAgC;AACzC,SAAS,6BAA6B;AAEtC,SAAS,oBAAoB;AAU7B,OAAO,SAAS,gBAAgB,QAAuD;CACrF,MAAM,YAAY,yBAAyB;CAE3C,IAAI,cAAc,WAAW;EAC3B,OAAO,EAAE,MAAM,aAAa;CAC9B;CAEA,MAAM,WAAW,YAAY,OAAO;CAEpC,IAAI,YAAY,GAAG;EACjB,OAAO;GACL,MAAM;GACN,WAAW,aAAa,SAAS;GACjC,QAAQ,OAAO;EACjB;CACF;;CAGA,OAAO;EAAE,MAAM;EAAY,OAAO;CAA8B;AAClE;AAOA,OAAO,SAAS,oBACd,YACA,QACe;CACf,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,YAAY,aAAa,aAAa,OAAO,aAAa;CAChE,MAAM,QAAQ,IAAI,sBAAsB;EACtC,YAAY,aAAa,UAAU;EACnC,eAAe,OAAO;EACtB,aAAa;CACf,CAAC;CACD,MAAM,YAAY,iBAAiB;EACjC,WAAW,MAAM,KAAK;CACxB,GAAG,UAAU;CACb,OAAO;EACL;EACA,CAAC,OAAO,WAAW;GACjB,aAAa,SAAS;EACxB;CACF;AACF;AAEA,OAAO,SAAS,eACd,UACA,UACa;CACb,IAAI,aAAa,WAAW,OAAO;CACnC,OAAO,YAAY,IAAI,CAAC,UAAU,QAAQ,CAAC;AAC7C;AAEA,OAAO,SAAS,0BACd,QAC2C;CAC3C,QACE,MACA,aAGA,OAAO,SAAmF;EACxF,MAAM,cAAc,gBAAgB,MAAM;EAE1C,QAAQ,YAAY,MAApB;GACE,KAAK,cACH,OAAO,KAAK,IAAI;GAElB,KAAK,qBACH,MAAM,IAAI,sBAAsB;IAC9B,YAAY,aAAa,CAAC;IAC1B,eAAe,YAAY;IAC3B,aAAa,YAAY;GAC3B,CAAC;GAEH,KAAK,YAAY;IACf,MAAM,QAAQ,oBAAoB,YAAY,OAAO,MAAM;;IAE3D,MAAM,UAAU,KAAK;IACrB,MAAM,SAAS,eAAe,SAAS,QAAQ,MAAM,WAAW,MAAM;IACtE,MAAM,SAAS,MAAM,KAAK;KACxB,GAAG;;KAEH,SAAS;MAAE,GAAI,KAAK;MAAoB;KAAO;IACjD,CAAC;IACD,OAAO;GACT;EACF;CACF;AACJ","names":[],"sources":["src/middleware.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\nimport { getRemainingTimeInMillis } from \"./context-store.js\";\nimport { DeadlineExceededError } from \"./error.js\";\nimport type { DeadlineComputation, DeadlineMiddlewareConfig, RequestDeadlineMs } from \"./types.js\";\nimport { milliseconds } from \"./types.js\";\n\nimport type {\n FinalizeHandler,\n FinalizeHandlerArguments,\n FinalizeHandlerOutput,\n FinalizeRequestMiddleware,\n HandlerExecutionContext,\n} from \"@smithy/types\";\n\nexport function computeDeadline(config: DeadlineMiddlewareConfig): DeadlineComputation {\n const remaining = getRemainingTimeInMillis();\n\n if (remaining === undefined) {\n return { kind: \"no-context\" };\n }\n\n const deadline = remaining - config.flushBufferMs;\n\n if (deadline <= 0) {\n return {\n kind: \"insufficient-time\",\n remaining: milliseconds(remaining),\n buffer: config.flushBufferMs,\n };\n }\n\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above\n return { kind: \"deadline\", value: deadline as RequestDeadlineMs };\n}\n\nexport interface DeadlineTimer {\n readonly controller: AbortController;\n [Symbol.dispose]: () => void;\n}\n\nexport function createDeadlineTimer(\n deadlineMs: RequestDeadlineMs,\n config: DeadlineMiddlewareConfig,\n): DeadlineTimer {\n const controller = new AbortController();\n const remaining = milliseconds(deadlineMs + config.flushBufferMs);\n const error = new DeadlineExceededError({\n deadlineMs: milliseconds(deadlineMs),\n flushBufferMs: config.flushBufferMs,\n remainingMs: remaining,\n });\n const timeoutId = setTimeout(() => {\n controller.abort(error);\n }, deadlineMs);\n return {\n controller,\n [Symbol.dispose]() {\n clearTimeout(timeoutId);\n },\n };\n}\n\nexport function composeSignals(\n existing: AbortSignal | undefined,\n deadline: AbortSignal,\n): AbortSignal {\n if (existing === undefined) return deadline;\n return AbortSignal.any([existing, deadline]);\n}\n\nexport function deadlineMiddlewareHandler(\n config: DeadlineMiddlewareConfig,\n): FinalizeRequestMiddleware<object, object> {\n return (\n next: FinalizeHandler<object, object>,\n _context: HandlerExecutionContext,\n ): FinalizeHandler<object, object> =>\n // oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union\n async (args: FinalizeHandlerArguments<object>): Promise<FinalizeHandlerOutput<object>> => {\n const computation = computeDeadline(config);\n\n switch (computation.kind) {\n case \"no-context\":\n return next(args);\n\n case \"insufficient-time\":\n throw new DeadlineExceededError({\n deadlineMs: milliseconds(0),\n flushBufferMs: computation.buffer,\n remainingMs: computation.remaining,\n });\n\n case \"deadline\": {\n using timer = createDeadlineTimer(computation.value, config);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property\n const request = args.request as { signal?: AbortSignal } | undefined;\n const signal = composeSignals(request?.signal, timer.controller.signal);\n const result = await next({\n ...args,\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal\n request: { ...(args.request as object), signal },\n });\n return result;\n }\n }\n };\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare const BrandSymbol: unique symbol;
|
|
2
|
+
type Brand<
|
|
3
|
+
T,
|
|
4
|
+
B extends string
|
|
5
|
+
> = T & {
|
|
6
|
+
readonly [BrandSymbol]: B;
|
|
7
|
+
};
|
|
8
|
+
export type Milliseconds = Brand<number, "Milliseconds">;
|
|
9
|
+
export type FlushBufferMs = Brand<number, "FlushBufferMs">;
|
|
10
|
+
export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
|
|
11
|
+
export declare function milliseconds(value: number): Milliseconds;
|
|
12
|
+
export declare function flushBufferMs(value: number): FlushBufferMs;
|
|
13
|
+
export type DeadlineComputation = {
|
|
14
|
+
readonly kind: "deadline";
|
|
15
|
+
readonly value: RequestDeadlineMs;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "insufficient-time";
|
|
18
|
+
readonly remaining: Milliseconds;
|
|
19
|
+
readonly buffer: FlushBufferMs;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "no-context";
|
|
22
|
+
};
|
|
23
|
+
export interface DeadlineMiddlewareConfig {
|
|
24
|
+
readonly flushBufferMs: FlushBufferMs;
|
|
25
|
+
readonly telemetryEnabled: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface DeadlineOptions {
|
|
28
|
+
readonly flushBufferMs?: number;
|
|
29
|
+
readonly telemetryEnabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAGA,cAAc;KAET;CAAM;CAAG;IAAoB,IAAI;WAAY,cAAc;AAAE;AAElE,YAAY,eAAe,cAAc;AAEzC,YAAY,gBAAgB,cAAc;AAE1C,YAAY,oBAAoB,cAAc;AAE9C,OAAO,iBAAS,aAAa,gBAAgB;AAQ7C,OAAO,iBAAS,cAAc,gBAAgB;AAW9C,YAAY,sBACR;UAAW,MAAM;UAAqB,OAAO;AAAkB,IAC/D;UACW,MAAM;UACN,WAAW;UACX,QAAQ;AACnB,IACA;UAAW,MAAM;AAAa;AAElC,iBAAiB,yBAAyB;UAC/B,eAAe;UACf;AACX;AAEA,iBAAiB,gBAAgB;UACtB;UACA;AACX","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\ndeclare const BrandSymbol: unique symbol;\n\ntype Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };\n\nexport type Milliseconds = Brand<number, \"Milliseconds\">;\n\nexport type FlushBufferMs = Brand<number, \"FlushBufferMs\">;\n\nexport type RequestDeadlineMs = Brand<number, \"RequestDeadlineMs\">;\n\nexport function milliseconds(value: number): Milliseconds {\n if (!Number.isFinite(value)) {\n throw new TypeError(`milliseconds value must be finite, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as Milliseconds;\n}\n\nexport function flushBufferMs(value: number): FlushBufferMs {\n if (!Number.isFinite(value)) {\n throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);\n }\n if (value < 0) {\n throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as FlushBufferMs;\n}\n\nexport type DeadlineComputation =\n | { readonly kind: \"deadline\"; readonly value: RequestDeadlineMs }\n | {\n readonly kind: \"insufficient-time\";\n readonly remaining: Milliseconds;\n readonly buffer: FlushBufferMs;\n }\n | { readonly kind: \"no-context\" };\n\nexport interface DeadlineMiddlewareConfig {\n readonly flushBufferMs: FlushBufferMs;\n readonly telemetryEnabled: boolean;\n}\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n readonly telemetryEnabled?: boolean;\n}\n"]}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function milliseconds(value) {
|
|
2
|
+
if (!Number.isFinite(value)) {
|
|
3
|
+
throw new TypeError(`milliseconds value must be finite, received: ${value}`);
|
|
4
|
+
}
|
|
5
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function flushBufferMs(value) {
|
|
9
|
+
if (!Number.isFinite(value)) {
|
|
10
|
+
throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);
|
|
11
|
+
}
|
|
12
|
+
if (value < 0) {
|
|
13
|
+
throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);
|
|
14
|
+
}
|
|
15
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAaA,OAAO,SAAS,aAAa,OAA6B;CACxD,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,UAAU,gDAAgD,OAAO;CAC7E;;CAEA,OAAO;AACT;AAEA,OAAO,SAAS,cAAc,OAA8B;CAC1D,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;EAC3B,MAAM,IAAI,UAAU,iDAAiD,OAAO;CAC9E;CACA,IAAI,QAAQ,GAAG;EACb,MAAM,IAAI,UAAU,iDAAiD,OAAO;CAC9E;;CAEA,OAAO;AACT","names":[],"sources":["src/types.ts"],"version":3,"sourcesContent":["// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors\n// SPDX-License-Identifier: MIT\n\ndeclare const BrandSymbol: unique symbol;\n\ntype Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };\n\nexport type Milliseconds = Brand<number, \"Milliseconds\">;\n\nexport type FlushBufferMs = Brand<number, \"FlushBufferMs\">;\n\nexport type RequestDeadlineMs = Brand<number, \"RequestDeadlineMs\">;\n\nexport function milliseconds(value: number): Milliseconds {\n if (!Number.isFinite(value)) {\n throw new TypeError(`milliseconds value must be finite, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as Milliseconds;\n}\n\nexport function flushBufferMs(value: number): FlushBufferMs {\n if (!Number.isFinite(value)) {\n throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);\n }\n if (value < 0) {\n throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);\n }\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above\n return value as FlushBufferMs;\n}\n\nexport type DeadlineComputation =\n | { readonly kind: \"deadline\"; readonly value: RequestDeadlineMs }\n | {\n readonly kind: \"insufficient-time\";\n readonly remaining: Milliseconds;\n readonly buffer: FlushBufferMs;\n }\n | { readonly kind: \"no-context\" };\n\nexport interface DeadlineMiddlewareConfig {\n readonly flushBufferMs: FlushBufferMs;\n readonly telemetryEnabled: boolean;\n}\n\nexport interface DeadlineOptions {\n readonly flushBufferMs?: number;\n readonly telemetryEnabled?: boolean;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lambda-deadline-middleware",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "AWS SDK v3 middleware for automatic Lambda deadline propagation via AbortController-based timeouts",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"SECURITY.md"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"provenance": true
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@aws-sdk/client-dynamodb": "^3.1062.0",
|
|
25
|
+
"@aws-sdk/client-s3": "^3.1062.0",
|
|
26
|
+
"@aws-sdk/client-sqs": "^3.1062.0",
|
|
27
|
+
"@commitlint/cli": "^21.0.2",
|
|
28
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
29
|
+
"@fast-check/vitest": "^0.4.1",
|
|
30
|
+
"@semantic-release/changelog": "6.0.3",
|
|
31
|
+
"@semantic-release/commit-analyzer": "13.0.1",
|
|
32
|
+
"@semantic-release/git": "10.0.1",
|
|
33
|
+
"@semantic-release/github": "12.0.8",
|
|
34
|
+
"@semantic-release/npm": "13.1.5",
|
|
35
|
+
"@semantic-release/release-notes-generator": "14.1.1",
|
|
36
|
+
"@smithy/types": "^4.14.3",
|
|
37
|
+
"@types/node": "^25.9.1",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
39
|
+
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
40
|
+
"knip": "^6.15.0",
|
|
41
|
+
"lefthook": "^2.1.9",
|
|
42
|
+
"oxc-transform": "^0.134.0",
|
|
43
|
+
"oxfmt": "^0.53.0",
|
|
44
|
+
"oxlint": "^1.68.0",
|
|
45
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
46
|
+
"semantic-release": "25.0.3",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vitest": "^4.1.8"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=24"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "node --experimental-strip-types scripts/build.ts",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"bench": "vitest bench --run --outputJson=bench-results.json && node --experimental-strip-types scripts/validate-bench.ts",
|
|
58
|
+
"typecheck": "tsc",
|
|
59
|
+
"lint": "oxlint --type-aware --type-check --fix .",
|
|
60
|
+
"lint:check": "oxlint --type-aware --type-check .",
|
|
61
|
+
"fmt": "oxfmt .",
|
|
62
|
+
"fmt:check": "oxfmt --check .",
|
|
63
|
+
"lint:knip": "knip",
|
|
64
|
+
"sast": "scripts/ensure-opengrep.sh scan .",
|
|
65
|
+
"sast:check": "scripts/ensure-opengrep.sh scan --error .",
|
|
66
|
+
"sca": "podman run --rm -v \"$(pwd):/src:Z\" ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln --include-dev-deps --exit-code 1 --severity HIGH,CRITICAL /src",
|
|
67
|
+
"sca:full": "podman run --rm -v \"$(pwd):/src:Z\" ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln,secret,misconfig --include-dev-deps /src",
|
|
68
|
+
"actionlint": "podman run --rm -w /repo -v \"$(pwd):/repo:Z\" docker.io/rhysd/actionlint:1.7.12@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7",
|
|
69
|
+
"security": "pnpm run sast:check && pnpm run sca && pnpm run actionlint",
|
|
70
|
+
"sbom": "pnpm sbom --sbom-format cyclonedx --sbom-spec-version 1.5 --sbom-type library > sbom.cdx.json",
|
|
71
|
+
"release": "semantic-release"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
|
|
6
|
+
export interface LambdaContextLike {
|
|
7
|
+
getRemainingTimeInMillis?: () => number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Sentinel allows AsyncLocalStorage.run() to accept null/undefined context
|
|
11
|
+
// without throwing, while the accessor can distinguish "no context stored"
|
|
12
|
+
// from "context present but missing the method".
|
|
13
|
+
const NO_CONTEXT: unique symbol = Symbol("no-context");
|
|
14
|
+
|
|
15
|
+
type StoreValue = LambdaContextLike | typeof NO_CONTEXT;
|
|
16
|
+
|
|
17
|
+
const contextStorage = new AsyncLocalStorage<StoreValue>();
|
|
18
|
+
|
|
19
|
+
export function run<T>(context: LambdaContextLike | null | undefined, fn: () => T): T {
|
|
20
|
+
const value: StoreValue = context ?? NO_CONTEXT;
|
|
21
|
+
return contextStorage.run(value, fn);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getRemainingTimeInMillis(): number | undefined {
|
|
25
|
+
const store = contextStorage.getStore();
|
|
26
|
+
|
|
27
|
+
if (store === undefined || store === NO_CONTEXT) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof store.getRemainingTimeInMillis !== "function") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return store.getRemainingTimeInMillis();
|
|
36
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { FlushBufferMs, Milliseconds } from "./types.js";
|
|
5
|
+
|
|
6
|
+
interface DeadlineExceededInit {
|
|
7
|
+
readonly deadlineMs: Milliseconds;
|
|
8
|
+
readonly flushBufferMs: FlushBufferMs;
|
|
9
|
+
readonly remainingMs: Milliseconds;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class DeadlineExceededError extends Error {
|
|
13
|
+
override readonly name = "DeadlineExceededError" as const;
|
|
14
|
+
readonly deadlineMs: Milliseconds;
|
|
15
|
+
readonly flushBufferMs: FlushBufferMs;
|
|
16
|
+
readonly remainingMs: Milliseconds;
|
|
17
|
+
|
|
18
|
+
constructor(init: DeadlineExceededInit) {
|
|
19
|
+
super(
|
|
20
|
+
`Request deadline exceeded: ${init.deadlineMs}ms deadline (${init.flushBufferMs}ms flush buffer)`,
|
|
21
|
+
);
|
|
22
|
+
this.deadlineMs = init.deadlineMs;
|
|
23
|
+
this.flushBufferMs = init.flushBufferMs;
|
|
24
|
+
this.remainingMs = init.remainingMs;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Structural check rather than instanceof — works across module boundaries
|
|
29
|
+
// and serialization boundaries where prototype chain may be broken.
|
|
30
|
+
export function isDeadlineExceeded(error: unknown): error is DeadlineExceededError {
|
|
31
|
+
if (error === null || error === undefined) return false;
|
|
32
|
+
if (typeof error !== "object") return false;
|
|
33
|
+
return (error as { name?: unknown }).name === "DeadlineExceededError";
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 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 function withLambdaDeadline<TEvent, TResult>(
|
|
14
|
+
handler: AsyncHandler<TEvent, TResult>,
|
|
15
|
+
_options?: DeadlineOptions,
|
|
16
|
+
): AsyncHandler<TEvent, TResult> {
|
|
17
|
+
return async (event: TEvent, context: LambdaContextLike): Promise<TResult> =>
|
|
18
|
+
run(context, async () => handler(event, context));
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
export { withLambdaDeadline } from "./handler-wrapper.js";
|
|
5
|
+
export { deadlineMiddleware, withDeadline } from "./registration.js";
|
|
6
|
+
export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
|
|
7
|
+
export { getRemainingTimeInMillis } from "./context-store.js";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
Milliseconds,
|
|
11
|
+
FlushBufferMs,
|
|
12
|
+
RequestDeadlineMs,
|
|
13
|
+
DeadlineComputation,
|
|
14
|
+
DeadlineMiddlewareConfig,
|
|
15
|
+
DeadlineOptions,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
export type { LambdaContextLike } from "./context-store.js";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
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
|
+
import type {
|
|
10
|
+
FinalizeHandler,
|
|
11
|
+
FinalizeHandlerArguments,
|
|
12
|
+
FinalizeHandlerOutput,
|
|
13
|
+
FinalizeRequestMiddleware,
|
|
14
|
+
HandlerExecutionContext,
|
|
15
|
+
} from "@smithy/types";
|
|
16
|
+
|
|
17
|
+
export function computeDeadline(config: DeadlineMiddlewareConfig): DeadlineComputation {
|
|
18
|
+
const remaining = getRemainingTimeInMillis();
|
|
19
|
+
|
|
20
|
+
if (remaining === undefined) {
|
|
21
|
+
return { kind: "no-context" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deadline = remaining - config.flushBufferMs;
|
|
25
|
+
|
|
26
|
+
if (deadline <= 0) {
|
|
27
|
+
return {
|
|
28
|
+
kind: "insufficient-time",
|
|
29
|
+
remaining: milliseconds(remaining),
|
|
30
|
+
buffer: config.flushBufferMs,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above
|
|
35
|
+
return { kind: "deadline", value: deadline as RequestDeadlineMs };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DeadlineTimer {
|
|
39
|
+
readonly controller: AbortController;
|
|
40
|
+
[Symbol.dispose]: () => void;
|
|
41
|
+
}
|
|
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
|
+
return {
|
|
58
|
+
controller,
|
|
59
|
+
[Symbol.dispose]() {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
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
|
+
}
|
|
72
|
+
|
|
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);
|
|
83
|
+
|
|
84
|
+
switch (computation.kind) {
|
|
85
|
+
case "no-context":
|
|
86
|
+
return next(args);
|
|
87
|
+
|
|
88
|
+
case "insufficient-time":
|
|
89
|
+
throw new DeadlineExceededError({
|
|
90
|
+
deadlineMs: milliseconds(0),
|
|
91
|
+
flushBufferMs: computation.buffer,
|
|
92
|
+
remainingMs: computation.remaining,
|
|
93
|
+
});
|
|
94
|
+
|
|
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
|
+
}
|