lambda-deadline-middleware 0.0.0 → 1.0.1
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 +1 -1
- package/LICENSES/MIT.txt +18 -0
- package/README.md +107 -153
- package/REUSE.toml +9 -0
- package/SECURITY.md +34 -38
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -3
- package/dist/config.js.map +1 -1
- package/dist/context-store.d.ts +2 -2
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +5 -5
- package/dist/context-store.js.map +1 -1
- package/dist/error.d.ts +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -2
- package/dist/error.js.map +1 -1
- package/dist/handler-wrapper.d.ts +2 -2
- package/dist/handler-wrapper.d.ts.map +1 -1
- package/dist/handler-wrapper.js +2 -4
- package/dist/handler-wrapper.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +7 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +33 -33
- package/dist/middleware.js.map +1 -1
- package/dist/registration.d.ts +4 -6
- package/dist/registration.d.ts.map +1 -1
- package/dist/registration.js +5 -11
- package/dist/registration.js.map +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +4 -4
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +36 -30
- package/src/config.ts +5 -3
- package/src/context-store.ts +7 -5
- package/src/error.ts +3 -3
- package/src/handler-wrapper.ts +7 -7
- package/src/index.ts +2 -2
- package/src/middleware.ts +43 -41
- package/src/registration.ts +9 -18
- package/src/telemetry.ts +9 -6
- package/src/types.ts +7 -5
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambda-deadline-middleware",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "AWS SDK v3 middleware for automatic Lambda deadline propagation via AbortController-based timeouts",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/mikkopiu/lambda-deadline-middleware"
|
|
9
|
+
},
|
|
6
10
|
"files": [
|
|
7
11
|
"dist",
|
|
8
12
|
"src",
|
|
9
13
|
"LICENSE",
|
|
14
|
+
"LICENSES",
|
|
15
|
+
"REUSE.toml",
|
|
10
16
|
"SECURITY.md"
|
|
11
17
|
],
|
|
12
18
|
"type": "module",
|
|
@@ -20,28 +26,47 @@
|
|
|
20
26
|
"access": "public",
|
|
21
27
|
"provenance": true
|
|
22
28
|
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node --experimental-strip-types scripts/build.ts",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"bench": "vitest bench --run --outputJson=bench-results.json && node --experimental-strip-types scripts/validate-bench.ts",
|
|
34
|
+
"typecheck": "tsc",
|
|
35
|
+
"lint": "oxlint --type-aware --type-check --fix .",
|
|
36
|
+
"lint:check": "oxlint --type-aware --type-check .",
|
|
37
|
+
"fmt": "oxfmt .",
|
|
38
|
+
"fmt:check": "oxfmt --check .",
|
|
39
|
+
"lint:knip": "knip",
|
|
40
|
+
"sast": "scripts/ensure-opengrep.sh scan .",
|
|
41
|
+
"sast:check": "scripts/ensure-opengrep.sh scan --error .",
|
|
42
|
+
"sca": "podman run --rm -v \"$(pwd):/src:Z\" -v trivy-cache:/root/.cache/trivy:Z ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln --include-dev-deps --exit-code 1 --severity HIGH,CRITICAL /src",
|
|
43
|
+
"sca:full": "podman run --rm -v \"$(pwd):/src:Z\" -v trivy-cache:/root/.cache/trivy:Z ghcr.io/aquasecurity/trivy:0.71.0@sha256:016eae51fdcf989332a5404af7e8f625cd5d95d7c0907a221d080a996f556500 fs --scanners vuln,secret,misconfig --include-dev-deps /src",
|
|
44
|
+
"actionlint": "podman run --rm -w /repo -v \"$(pwd):/repo:Z\" docker.io/rhysd/actionlint:1.7.12@sha256:9d36088643581e728c969f35141f88139fec77280b2be23c1f66f8e40e1025e7",
|
|
45
|
+
"security": "pnpm run sast:check && pnpm run sca && pnpm run actionlint",
|
|
46
|
+
"prepare": "lefthook install",
|
|
47
|
+
"sbom": "pnpm sbom --sbom-format cyclonedx --sbom-spec-version 1.5 --sbom-type library > sbom.cdx.json",
|
|
48
|
+
"release": "semantic-release"
|
|
49
|
+
},
|
|
23
50
|
"devDependencies": {
|
|
24
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
25
|
-
"@aws-sdk/client-s3": "^3.
|
|
26
|
-
"@aws-sdk/client-sqs": "^3.
|
|
51
|
+
"@aws-sdk/client-dynamodb": "^3.1064.0",
|
|
52
|
+
"@aws-sdk/client-s3": "^3.1064.0",
|
|
53
|
+
"@aws-sdk/client-sqs": "^3.1064.0",
|
|
27
54
|
"@commitlint/cli": "^21.0.2",
|
|
28
55
|
"@commitlint/config-conventional": "^21.0.2",
|
|
29
56
|
"@fast-check/vitest": "^0.4.1",
|
|
30
|
-
"@semantic-release/changelog": "6.0.3",
|
|
31
57
|
"@semantic-release/commit-analyzer": "13.0.1",
|
|
32
|
-
"@semantic-release/git": "10.0.1",
|
|
33
58
|
"@semantic-release/github": "12.0.8",
|
|
34
59
|
"@semantic-release/npm": "13.1.5",
|
|
35
60
|
"@semantic-release/release-notes-generator": "14.1.1",
|
|
36
61
|
"@smithy/types": "^4.14.3",
|
|
37
|
-
"@types/node": "^25.9.
|
|
62
|
+
"@types/node": "^25.9.2",
|
|
38
63
|
"@vitest/coverage-v8": "^4.1.8",
|
|
39
64
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
40
|
-
"knip": "^6.
|
|
65
|
+
"knip": "^6.16.1",
|
|
41
66
|
"lefthook": "^2.1.9",
|
|
42
67
|
"oxc-transform": "^0.134.0",
|
|
43
68
|
"oxfmt": "^0.53.0",
|
|
44
|
-
"oxlint": "^1.
|
|
69
|
+
"oxlint": "^1.69.0",
|
|
45
70
|
"oxlint-tsgolint": "^0.23.0",
|
|
46
71
|
"semantic-release": "25.0.3",
|
|
47
72
|
"typescript": "^6.0.3",
|
|
@@ -50,24 +75,5 @@
|
|
|
50
75
|
"engines": {
|
|
51
76
|
"node": ">=24"
|
|
52
77
|
},
|
|
53
|
-
"
|
|
54
|
-
|
|
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
|
-
}
|
|
78
|
+
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
|
|
79
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import { flushBufferMs } from "./types.js";
|
|
5
5
|
import type { DeadlineMiddlewareConfig, DeadlineOptions } from "./types.js";
|
|
6
6
|
|
|
7
|
-
|
|
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 => {
|
|
8
10
|
const buffer = raw?.flushBufferMs ?? 1000;
|
|
9
11
|
if (buffer < 0) {
|
|
10
12
|
throw new TypeError(`flushBufferMs option must be non-negative, received: ${buffer}`);
|
|
@@ -13,4 +15,4 @@ export function parseConfig(raw: DeadlineOptions | undefined): DeadlineMiddlewar
|
|
|
13
15
|
flushBufferMs: flushBufferMs(buffer),
|
|
14
16
|
telemetryEnabled: raw?.telemetryEnabled ?? true,
|
|
15
17
|
};
|
|
16
|
-
}
|
|
18
|
+
};
|
package/src/context-store.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
|
|
6
|
+
// AsyncLocalStorage propagates the Lambda context through the entire async call chain without parameter threading.
|
|
7
|
+
// SDK middleware executes deep in the Smithy stack where we can't pass the context through function signatures.
|
|
6
8
|
export interface LambdaContextLike {
|
|
7
9
|
getRemainingTimeInMillis?: () => number;
|
|
8
10
|
}
|
|
@@ -16,12 +18,12 @@ type StoreValue = LambdaContextLike | typeof NO_CONTEXT;
|
|
|
16
18
|
|
|
17
19
|
const contextStorage = new AsyncLocalStorage<StoreValue>();
|
|
18
20
|
|
|
19
|
-
export
|
|
21
|
+
export const run = <T>(context: LambdaContextLike | null | undefined, fn: () => T): T => {
|
|
20
22
|
const value: StoreValue = context ?? NO_CONTEXT;
|
|
21
23
|
return contextStorage.run(value, fn);
|
|
22
|
-
}
|
|
24
|
+
};
|
|
23
25
|
|
|
24
|
-
export
|
|
26
|
+
export const getRemainingTimeInMillis = (): number | undefined => {
|
|
25
27
|
const store = contextStorage.getStore();
|
|
26
28
|
|
|
27
29
|
if (store === undefined || store === NO_CONTEXT) {
|
|
@@ -33,4 +35,4 @@ export function getRemainingTimeInMillis(): number | undefined {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
return store.getRemainingTimeInMillis();
|
|
36
|
-
}
|
|
38
|
+
};
|
package/src/error.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import type { FlushBufferMs, Milliseconds } from "./types.js";
|
|
@@ -27,8 +27,8 @@ export class DeadlineExceededError extends Error {
|
|
|
27
27
|
|
|
28
28
|
// Structural check rather than instanceof — works across module boundaries
|
|
29
29
|
// and serialization boundaries where prototype chain may be broken.
|
|
30
|
-
export
|
|
30
|
+
export const isDeadlineExceeded = (error: unknown): error is DeadlineExceededError => {
|
|
31
31
|
if (error === null || error === undefined) return false;
|
|
32
32
|
if (typeof error !== "object") return false;
|
|
33
33
|
return (error as { name?: unknown }).name === "DeadlineExceededError";
|
|
34
|
-
}
|
|
34
|
+
};
|
package/src/handler-wrapper.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import { run } from "./context-store.js";
|
|
@@ -10,10 +10,10 @@ type AsyncHandler<TEvent, TResult> = (
|
|
|
10
10
|
context: LambdaContextLike,
|
|
11
11
|
) => Promise<TResult>;
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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> =>
|
|
18
19
|
run(context, async () => handler(event, context));
|
|
19
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
export { withLambdaDeadline } from "./handler-wrapper.js";
|
|
5
|
-
export { deadlineMiddleware
|
|
5
|
+
export { deadlineMiddleware } from "./registration.js";
|
|
6
6
|
export { DeadlineExceededError, isDeadlineExceeded } from "./error.js";
|
|
7
7
|
export { getRemainingTimeInMillis } from "./context-store.js";
|
|
8
8
|
|
package/src/middleware.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import { getRemainingTimeInMillis } from "./context-store.js";
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
HandlerExecutionContext,
|
|
15
15
|
} from "@smithy/types";
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const computeDeadline = (config: DeadlineMiddlewareConfig): DeadlineComputation => {
|
|
18
18
|
const remaining = getRemainingTimeInMillis();
|
|
19
19
|
|
|
20
20
|
if (remaining === undefined) {
|
|
@@ -33,17 +33,17 @@ export function computeDeadline(config: DeadlineMiddlewareConfig): DeadlineCompu
|
|
|
33
33
|
|
|
34
34
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded narrowing: deadline is validated > 0 above
|
|
35
35
|
return { kind: "deadline", value: deadline as RequestDeadlineMs };
|
|
36
|
-
}
|
|
36
|
+
};
|
|
37
37
|
|
|
38
38
|
export interface DeadlineTimer {
|
|
39
39
|
readonly controller: AbortController;
|
|
40
40
|
[Symbol.dispose]: () => void;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export
|
|
43
|
+
export const createDeadlineTimer = (
|
|
44
44
|
deadlineMs: RequestDeadlineMs,
|
|
45
45
|
config: DeadlineMiddlewareConfig,
|
|
46
|
-
): DeadlineTimer {
|
|
46
|
+
): DeadlineTimer => {
|
|
47
47
|
const controller = new AbortController();
|
|
48
48
|
const remaining = milliseconds(deadlineMs + config.flushBufferMs);
|
|
49
49
|
const error = new DeadlineExceededError({
|
|
@@ -60,50 +60,52 @@ export function createDeadlineTimer(
|
|
|
60
60
|
clearTimeout(timeoutId);
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
|
-
}
|
|
63
|
+
};
|
|
64
64
|
|
|
65
|
-
export
|
|
65
|
+
export const composeSignals = (
|
|
66
66
|
existing: AbortSignal | undefined,
|
|
67
67
|
deadline: AbortSignal,
|
|
68
|
-
): AbortSignal {
|
|
68
|
+
): AbortSignal => {
|
|
69
69
|
if (existing === undefined) return deadline;
|
|
70
70
|
return AbortSignal.any([existing, deadline]);
|
|
71
|
-
}
|
|
71
|
+
};
|
|
72
72
|
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
export const deadlineMiddlewareHandler =
|
|
74
|
+
<Input extends object, Output extends object>(
|
|
75
|
+
config: DeadlineMiddlewareConfig,
|
|
76
|
+
): FinalizeRequestMiddleware<Input, Output> =>
|
|
77
|
+
(
|
|
78
|
+
next: FinalizeHandler<Input, Output>,
|
|
78
79
|
_context: HandlerExecutionContext,
|
|
79
|
-
): FinalizeHandler<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
): FinalizeHandler<Input, Output> =>
|
|
81
|
+
// oxlint-disable-next-line typescript/consistent-return -- switch is exhaustive over DeadlineComputation discriminated union
|
|
82
|
+
async (args: FinalizeHandlerArguments<Input>): Promise<FinalizeHandlerOutput<Output>> => {
|
|
83
|
+
const computation = computeDeadline(config);
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
switch (computation.kind) {
|
|
86
|
+
case "no-context":
|
|
87
|
+
return next(args);
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
case "insufficient-time":
|
|
90
|
+
throw new DeadlineExceededError({
|
|
91
|
+
deadlineMs: milliseconds(0),
|
|
92
|
+
flushBufferMs: computation.buffer,
|
|
93
|
+
remainingMs: computation.remaining,
|
|
94
|
+
});
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
96
|
+
case "deadline": {
|
|
97
|
+
// `using` guarantees cleanup (clearTimeout) even if next() throws, the promise rejects,
|
|
98
|
+
// or an external abort signal fires — strictly more reliable than try/finally.
|
|
99
|
+
using timer = createDeadlineTimer(computation.value, config);
|
|
100
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Smithy request is an opaque object; we access optional signal property
|
|
101
|
+
const request = args.request as { signal?: AbortSignal } | undefined;
|
|
102
|
+
const signal = composeSignals(request?.signal, timer.controller.signal);
|
|
103
|
+
const result = await next({
|
|
104
|
+
...args,
|
|
105
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- spreading opaque Smithy request to add signal
|
|
106
|
+
request: { ...(args.request as object), signal },
|
|
107
|
+
});
|
|
108
|
+
return result;
|
|
107
109
|
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
+
}
|
|
111
|
+
};
|
package/src/registration.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import type { Pluggable } from "@smithy/types";
|
|
@@ -7,30 +7,21 @@ import { parseConfig } from "./config.js";
|
|
|
7
7
|
import { deadlineMiddlewareHandler } from "./middleware.js";
|
|
8
8
|
import type { DeadlineOptions } from "./types.js";
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export const deadlineMiddleware = <Input extends object, Output extends object>(
|
|
11
|
+
options?: DeadlineOptions,
|
|
12
|
+
): Pluggable<Input, Output> => {
|
|
11
13
|
const config = parseConfig(options);
|
|
12
14
|
|
|
13
15
|
return {
|
|
14
16
|
applyToStack(stack) {
|
|
15
|
-
|
|
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), {
|
|
16
21
|
step: "finalizeRequest",
|
|
17
22
|
name: "deadlineMiddleware",
|
|
18
23
|
override: true,
|
|
19
24
|
});
|
|
20
25
|
},
|
|
21
26
|
};
|
|
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
|
-
}
|
|
27
|
+
};
|
package/src/telemetry.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 lambda-deadline-middleware contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import type { DeadlineExceededError } from "./error.js";
|
|
5
5
|
import type { DeadlineMiddlewareConfig } from "./types.js";
|
|
6
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.
|
|
7
10
|
interface AbortDetails {
|
|
8
11
|
readonly deadlineMs: number;
|
|
9
12
|
readonly flushBufferMs: number;
|
|
@@ -18,7 +21,7 @@ interface TelemetryEmitter {
|
|
|
18
21
|
let emitter: TelemetryEmitter | undefined;
|
|
19
22
|
let detected = false;
|
|
20
23
|
|
|
21
|
-
async
|
|
24
|
+
const detectEmitter = async (): Promise<TelemetryEmitter | undefined> => {
|
|
22
25
|
if (detected) return emitter;
|
|
23
26
|
detected = true;
|
|
24
27
|
|
|
@@ -104,12 +107,12 @@ async function detectEmitter(): Promise<TelemetryEmitter | undefined> {
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
return emitter;
|
|
107
|
-
}
|
|
110
|
+
};
|
|
108
111
|
|
|
109
|
-
export async
|
|
112
|
+
export const emitDeadlineAbort = async (
|
|
110
113
|
error: DeadlineExceededError,
|
|
111
114
|
config: DeadlineMiddlewareConfig,
|
|
112
|
-
): Promise<void> {
|
|
115
|
+
): Promise<void> => {
|
|
113
116
|
try {
|
|
114
117
|
if (!config.telemetryEnabled) return;
|
|
115
118
|
|
|
@@ -126,4 +129,4 @@ export async function emitDeadlineAbort(
|
|
|
126
129
|
} catch {
|
|
127
130
|
// Telemetry must never disrupt request processing
|
|
128
131
|
}
|
|
129
|
-
}
|
|
132
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText:
|
|
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.
|
|
4
6
|
declare const BrandSymbol: unique symbol;
|
|
5
7
|
|
|
6
8
|
type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
|
|
@@ -11,15 +13,15 @@ export type FlushBufferMs = Brand<number, "FlushBufferMs">;
|
|
|
11
13
|
|
|
12
14
|
export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
|
|
13
15
|
|
|
14
|
-
export
|
|
16
|
+
export const milliseconds = (value: number): Milliseconds => {
|
|
15
17
|
if (!Number.isFinite(value)) {
|
|
16
18
|
throw new TypeError(`milliseconds value must be finite, received: ${value}`);
|
|
17
19
|
}
|
|
18
20
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
19
21
|
return value as Milliseconds;
|
|
20
|
-
}
|
|
22
|
+
};
|
|
21
23
|
|
|
22
|
-
export
|
|
24
|
+
export const flushBufferMs = (value: number): FlushBufferMs => {
|
|
23
25
|
if (!Number.isFinite(value)) {
|
|
24
26
|
throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);
|
|
25
27
|
}
|
|
@@ -28,7 +30,7 @@ export function flushBufferMs(value: number): FlushBufferMs {
|
|
|
28
30
|
}
|
|
29
31
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
30
32
|
return value as FlushBufferMs;
|
|
31
|
-
}
|
|
33
|
+
};
|
|
32
34
|
|
|
33
35
|
export type DeadlineComputation =
|
|
34
36
|
| { readonly kind: "deadline"; readonly value: RequestDeadlineMs }
|