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.
Files changed (51) hide show
  1. package/LICENSE +1 -1
  2. package/LICENSES/MIT.txt +18 -0
  3. package/README.md +107 -153
  4. package/REUSE.toml +9 -0
  5. package/SECURITY.md +34 -38
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +5 -3
  9. package/dist/config.js.map +1 -1
  10. package/dist/context-store.d.ts +2 -2
  11. package/dist/context-store.d.ts.map +1 -1
  12. package/dist/context-store.js +5 -5
  13. package/dist/context-store.js.map +1 -1
  14. package/dist/error.d.ts +1 -1
  15. package/dist/error.d.ts.map +1 -1
  16. package/dist/error.js +2 -2
  17. package/dist/error.js.map +1 -1
  18. package/dist/handler-wrapper.d.ts +2 -2
  19. package/dist/handler-wrapper.d.ts.map +1 -1
  20. package/dist/handler-wrapper.js +2 -4
  21. package/dist/handler-wrapper.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/middleware.d.ts +7 -4
  27. package/dist/middleware.d.ts.map +1 -1
  28. package/dist/middleware.js +33 -33
  29. package/dist/middleware.js.map +1 -1
  30. package/dist/registration.d.ts +4 -6
  31. package/dist/registration.d.ts.map +1 -1
  32. package/dist/registration.js +5 -11
  33. package/dist/registration.js.map +1 -1
  34. package/dist/telemetry.d.ts +1 -1
  35. package/dist/telemetry.d.ts.map +1 -1
  36. package/dist/telemetry.js +4 -4
  37. package/dist/telemetry.js.map +1 -1
  38. package/dist/types.d.ts +2 -2
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +4 -4
  41. package/dist/types.js.map +1 -1
  42. package/package.json +36 -30
  43. package/src/config.ts +5 -3
  44. package/src/context-store.ts +7 -5
  45. package/src/error.ts +3 -3
  46. package/src/handler-wrapper.ts +7 -7
  47. package/src/index.ts +2 -2
  48. package/src/middleware.ts +43 -41
  49. package/src/registration.ts +9 -18
  50. package/src/telemetry.ts +9 -6
  51. package/src/types.ts +7 -5
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "lambda-deadline-middleware",
3
- "version": "0.0.0",
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.1062.0",
25
- "@aws-sdk/client-s3": "^3.1062.0",
26
- "@aws-sdk/client-sqs": "^3.1062.0",
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.1",
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.15.0",
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.68.0",
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
- "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
- }
78
+ "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
79
+ }
package/src/config.ts CHANGED
@@ -1,10 +1,12 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
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
- export function parseConfig(raw: DeadlineOptions | undefined): DeadlineMiddlewareConfig {
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
+ };
@@ -1,8 +1,10 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
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 function run<T>(context: LambdaContextLike | null | undefined, fn: () => T): T {
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 function getRemainingTimeInMillis(): number | undefined {
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: 2024 lambda-deadline-middleware contributors
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 function isDeadlineExceeded(error: unknown): error is DeadlineExceededError {
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
+ };
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
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 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> =>
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: 2024 lambda-deadline-middleware contributors
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, withDeadline } from "./registration.js";
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: 2024 lambda-deadline-middleware contributors
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 function computeDeadline(config: DeadlineMiddlewareConfig): DeadlineComputation {
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 function createDeadlineTimer(
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 function composeSignals(
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 function deadlineMiddlewareHandler(
74
- config: DeadlineMiddlewareConfig,
75
- ): FinalizeRequestMiddleware<object, object> {
76
- return (
77
- next: FinalizeHandler<object, object>,
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<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);
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
- switch (computation.kind) {
85
- case "no-context":
86
- return next(args);
85
+ switch (computation.kind) {
86
+ case "no-context":
87
+ return next(args);
87
88
 
88
- case "insufficient-time":
89
- throw new DeadlineExceededError({
90
- deadlineMs: milliseconds(0),
91
- flushBufferMs: computation.buffer,
92
- remainingMs: computation.remaining,
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
- 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
- }
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
+ };
@@ -1,4 +1,4 @@
1
- // SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
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 function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object> {
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
- stack.add(deadlineMiddlewareHandler(config), {
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: 2024 lambda-deadline-middleware contributors
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 function detectEmitter(): Promise<TelemetryEmitter | undefined> {
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 function emitDeadlineAbort(
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: 2024 lambda-deadline-middleware contributors
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 function milliseconds(value: number): Milliseconds {
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 function flushBufferMs(value: number): FlushBufferMs {
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 }