@workers-powertools/hono 0.1.0 → 0.2.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/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @workers-powertools/hono
2
+
3
+ Hono middleware adapters for all Workers Powertools utilities. A single package for integrating logger, metrics, tracer, and idempotency into your Hono application.
4
+
5
+ Part of [Workers Powertools](../../README.md) — a developer toolkit for observability and reliability best practices on Cloudflare Workers, inspired by [Powertools for AWS Lambda](https://docs.powertools.aws.dev/lambda/typescript/latest/).
6
+
7
+ > **Framework adapters** — this is the Hono adapter. More framework adapters (Astro, etc.) may be added in the future. The core packages (`logger`, `metrics`, `tracer`, `idempotency`) are framework-agnostic and can be used directly in any Workers project.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm add @workers-powertools/hono
13
+ ```
14
+
15
+ ## Middleware
16
+
17
+ ### `injectLogger(logger)`
18
+
19
+ Enriches the logger with request context (CF properties, correlation ID) before the handler runs, and clears temporary keys afterward.
20
+
21
+ ```typescript
22
+ import { injectLogger } from "@workers-powertools/hono";
23
+
24
+ app.use(injectLogger(logger));
25
+ ```
26
+
27
+ ### `injectMetrics(metrics, options?)`
28
+
29
+ Records `request_duration` and `request_count` metrics with `route`, `method`, and `status` dimensions, then flushes via `ctx.waitUntil()`.
30
+
31
+ ```typescript
32
+ import { injectMetrics } from "@workers-powertools/hono";
33
+ import { PipelinesBackend } from "@workers-powertools/metrics";
34
+
35
+ // Default — uses env.METRICS_PIPELINE automatically
36
+ app.use(injectMetrics(metrics));
37
+
38
+ // Custom binding name
39
+ app.use(
40
+ injectMetrics(metrics, {
41
+ backendFactory: (env) =>
42
+ new PipelinesBackend({ binding: env.MY_PIPELINE as PipelineBinding }),
43
+ }),
44
+ );
45
+ ```
46
+
47
+ The backend is only created once per binding reference — `setBackend()` is idempotent when the underlying binding hasn't changed.
48
+
49
+ ### `injectTracer(tracer)`
50
+
51
+ Extracts correlation ID, wraps the handler in a route-level span (`METHOD /path`), and annotates it with `http.method`, `http.route`, `http.url`, and `http.status`.
52
+
53
+ ```typescript
54
+ import { injectTracer } from "@workers-powertools/hono";
55
+
56
+ app.use(injectTracer(tracer));
57
+ ```
58
+
59
+ ### `injectIdempotency(options)`
60
+
61
+ Checks idempotency before the handler runs. If a completed record exists, returns the stored response. Concurrent duplicates receive 409 Conflict.
62
+
63
+ Apply per-route to state-mutating endpoints only:
64
+
65
+ ```typescript
66
+ import { injectIdempotency } from "@workers-powertools/hono";
67
+
68
+ app.post(
69
+ "/orders",
70
+ async (c, next) => {
71
+ persistenceLayer ??= new KVPersistenceLayer({ binding: c.env.IDEMPOTENCY_KV });
72
+ return injectIdempotency({ persistenceLayer, config: idempotencyConfig })(c, next);
73
+ },
74
+ async (c) => {
75
+ const body = await c.req.json();
76
+ return c.json({ orderId: body.orderId, status: "created" }, 201);
77
+ },
78
+ );
79
+ ```
80
+
81
+ ## Full Example
82
+
83
+ ```typescript
84
+ import { Hono } from "hono";
85
+ import { Logger } from "@workers-powertools/logger";
86
+ import { Metrics, MetricUnit, PipelinesBackend } from "@workers-powertools/metrics";
87
+ import type { PipelineBinding } from "@workers-powertools/metrics";
88
+ import { Tracer } from "@workers-powertools/tracer";
89
+ import {
90
+ injectLogger,
91
+ injectMetrics,
92
+ injectTracer,
93
+ injectIdempotency,
94
+ } from "@workers-powertools/hono";
95
+ import { IdempotencyConfig } from "@workers-powertools/idempotency";
96
+ import { KVPersistenceLayer } from "@workers-powertools/idempotency/kv";
97
+
98
+ const logger = new Logger();
99
+ const metrics = new Metrics();
100
+ const tracer = new Tracer();
101
+
102
+ let persistenceLayer: KVPersistenceLayer | undefined;
103
+ const idempotencyConfig = new IdempotencyConfig({
104
+ eventKeyPath: "$",
105
+ expiresAfterSeconds: 3600,
106
+ });
107
+
108
+ const app = new Hono<{ Bindings: Env }>();
109
+
110
+ app.use(injectLogger(logger));
111
+ app.use(injectTracer(tracer));
112
+ app.use(
113
+ injectMetrics(metrics, {
114
+ backendFactory: (env) =>
115
+ new PipelinesBackend({ binding: env.METRICS_PIPELINE as PipelineBinding }),
116
+ }),
117
+ );
118
+
119
+ app.get("/hello", (c) => c.json({ message: "hello" }));
120
+
121
+ app.post(
122
+ "/orders",
123
+ async (c, next) => {
124
+ persistenceLayer ??= new KVPersistenceLayer({ binding: c.env.IDEMPOTENCY_KV });
125
+ return injectIdempotency({ persistenceLayer, config: idempotencyConfig })(c, next);
126
+ },
127
+ async (c) => {
128
+ const body = await c.req.json();
129
+ return c.json({ status: "created" }, 201);
130
+ },
131
+ );
132
+
133
+ export default app;
134
+ ```
package/dist/index.d.ts CHANGED
@@ -19,8 +19,11 @@ declare function injectLogger(logger: Logger): MiddlewareHandler;
19
19
  */
20
20
  interface InjectMetricsOptions {
21
21
  /**
22
- * Factory function called once per request to construct the backend
23
- * from the Hono environment. Receives c.env so bindings are available.
22
+ * Factory function called once to construct the backend from the
23
+ * Hono environment. Receives c.env so bindings are available.
24
+ *
25
+ * The factory is only invoked when no backend is set, or when the
26
+ * binding reference has changed — not on every request.
24
27
  *
25
28
  * Defaults to a PipelinesBackend resolved from env.METRICS_PIPELINE
26
29
  * if that binding exists, otherwise no backend is set and a warning
@@ -38,13 +41,14 @@ interface InjectMetricsOptions {
38
41
  /**
39
42
  * Hono middleware that instruments each request with business metrics.
40
43
  *
41
- * Resolves the metrics backend per-request via backendFactory (so env
42
- * bindings are available), adds route and method dimensions, records
43
- * request duration, and flushes asynchronously via ctx.waitUntil.
44
+ * Resolves the metrics backend on the first request (or when the binding
45
+ * reference changes), records request duration and count with per-metric
46
+ * dimensions (route, method, status), and flushes asynchronously via
47
+ * ctx.waitUntil.
44
48
  *
45
- * The default backendFactory resolves a PipelinesBackend from
46
- * env.METRICS_PIPELINE. Override backendFactory for custom backends
47
- * or a different binding name.
49
+ * Dimensions are passed per-metric rather than accumulated on the Metrics
50
+ * instance, avoiding concurrency hazards when multiple requests share the
51
+ * same isolate.
48
52
  *
49
53
  * @example
50
54
  * // Default — uses env.METRICS_PIPELINE automatically
package/dist/index.js CHANGED
@@ -34,15 +34,21 @@ function injectMetrics(metrics, options) {
34
34
  }
35
35
  }
36
36
  const startTime = Date.now();
37
- metrics.addDimension("route", c.req.routePath);
38
- metrics.addDimension("method", c.req.method);
39
37
  try {
40
38
  await next();
41
- metrics.addDimension("status", String(c.res.status));
39
+ const httpDimensions = {
40
+ route: c.req.routePath,
41
+ method: c.req.method,
42
+ status: String(c.res.status)
43
+ };
44
+ metrics.addMetric(
45
+ "request_duration",
46
+ MetricUnit.Milliseconds,
47
+ Date.now() - startTime,
48
+ httpDimensions
49
+ );
50
+ metrics.addMetric("request_count", MetricUnit.Count, 1, httpDimensions);
42
51
  } finally {
43
- const durationMs = Date.now() - startTime;
44
- metrics.addMetric("request_duration", MetricUnit.Milliseconds, durationMs);
45
- metrics.addMetric("request_count", MetricUnit.Count, 1);
46
52
  c.executionCtx.waitUntil(metrics.flush());
47
53
  }
48
54
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/logger.ts","../src/metrics.ts","../src/tracer.ts","../src/idempotency.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type { Logger } from \"@workers-powertools/logger\";\n\n/**\n * Hono middleware that injects logger context for each request.\n *\n * Calls `logger.addContext` with the raw request and execution\n * context, then clears temporary keys once the handler completes.\n *\n * @param logger - A configured Logger instance.\n */\nexport function injectLogger(logger: Logger): MiddlewareHandler {\n return createMiddleware(async (c, next) => {\n // Enrich the logger with CF properties, correlation ID, etc.\n // Pass c.env so POWERTOOLS_SERVICE_NAME and POWERTOOLS_LOG_LEVEL are applied.\n logger.addContext(\n c.req.raw,\n c.executionCtx as unknown as ExecutionContext,\n c.env as unknown as Record<string, unknown>,\n );\n\n try {\n await next();\n } finally {\n // Clear per-request temporary keys so they don't leak\n // into subsequent requests when the logger is reused.\n logger.clearTemporaryKeys();\n }\n });\n}\n","import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type {\n Metrics,\n MetricsBackend,\n PipelineBinding,\n} from \"@workers-powertools/metrics\";\nimport { MetricUnit, PipelinesBackend } from \"@workers-powertools/metrics\";\n\n/**\n * Options for the injectMetrics middleware.\n */\nexport interface InjectMetricsOptions {\n /**\n * Factory function called once per request to construct the backend\n * from the Hono environment. Receives c.env so bindings are available.\n *\n * Defaults to a PipelinesBackend resolved from env.METRICS_PIPELINE\n * if that binding exists, otherwise no backend is set and a warning\n * is emitted.\n *\n * @example\n * // Pipelines (recommended)\n * backendFactory: (env) => new PipelinesBackend({ binding: env.METRICS_PIPELINE })\n *\n * // Analytics Engine (explicit opt-in — see AnalyticsEngineBackend docs)\n * backendFactory: (env) => new AnalyticsEngineBackend({ binding: env.ANALYTICS })\n */\n backendFactory?: (env: Record<string, unknown>) => MetricsBackend;\n}\n\n/**\n * Hono middleware that instruments each request with business metrics.\n *\n * Resolves the metrics backend per-request via backendFactory (so env\n * bindings are available), adds route and method dimensions, records\n * request duration, and flushes asynchronously via ctx.waitUntil.\n *\n * The default backendFactory resolves a PipelinesBackend from\n * env.METRICS_PIPELINE. Override backendFactory for custom backends\n * or a different binding name.\n *\n * @example\n * // Default — uses env.METRICS_PIPELINE automatically\n * app.use(injectMetrics(metrics));\n *\n * // Custom binding name\n * app.use(injectMetrics(metrics, {\n * backendFactory: (env) => new PipelinesBackend({ binding: env.MY_PIPELINE }),\n * }));\n *\n * // Analytics Engine (opt-in)\n * app.use(injectMetrics(metrics, {\n * backendFactory: (env) => new AnalyticsEngineBackend({ binding: env.ANALYTICS }),\n * }));\n */\nexport function injectMetrics(\n metrics: Metrics,\n options?: InjectMetricsOptions,\n): MiddlewareHandler {\n return createMiddleware(async (c, next) => {\n const env = c.env as Record<string, unknown>;\n\n // Resolve backend via factory, or fall back to the default Pipelines\n // binding (env.METRICS_PIPELINE) if present.\n if (options?.backendFactory) {\n metrics.setBackend(options.backendFactory(env));\n } else {\n const defaultBinding = env[\"METRICS_PIPELINE\"];\n if (defaultBinding) {\n metrics.setBackend(\n new PipelinesBackend({\n binding: defaultBinding as PipelineBinding,\n }),\n );\n }\n // If neither factory nor default binding exist, Metrics.flush() will\n // emit a warning — no silent failure.\n }\n\n const startTime = Date.now();\n\n // Use the matched Hono route pattern (e.g. \"/orders/:id\") as the\n // dimension rather than the raw URL, so cardinality stays bounded.\n metrics.addDimension(\"route\", c.req.routePath);\n metrics.addDimension(\"method\", c.req.method);\n\n try {\n await next();\n\n metrics.addDimension(\"status\", String(c.res.status));\n } finally {\n const durationMs = Date.now() - startTime;\n metrics.addMetric(\"request_duration\", MetricUnit.Milliseconds, durationMs);\n metrics.addMetric(\"request_count\", MetricUnit.Count, 1);\n\n // Non-blocking flush — writes happen after the response is returned.\n c.executionCtx.waitUntil(metrics.flush());\n }\n });\n}\n","import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type { Tracer } from \"@workers-powertools/tracer\";\n\n/**\n * Hono middleware that injects tracer context for each request.\n *\n * Extracts the correlation ID from the incoming request, then\n * wraps the downstream handler in a `captureAsync` span named\n * after the HTTP method and matched route pattern.\n *\n * @param tracer - A configured Tracer instance.\n */\nexport function injectTracer(tracer: Tracer): MiddlewareHandler {\n return createMiddleware(async (c, next) => {\n // Extract correlation ID and enrich the tracer.\n // Pass c.env so POWERTOOLS_SERVICE_NAME is applied at runtime.\n tracer.addContext(\n c.req.raw,\n c.executionCtx as unknown as ExecutionContext,\n c.env as unknown as Record<string, unknown>,\n );\n\n const spanName = `${c.req.method} ${c.req.routePath}`;\n\n await tracer.captureAsync(spanName, async (span) => {\n // Annotate the span with useful request metadata.\n span.annotations[\"http.method\"] = c.req.method;\n span.annotations[\"http.route\"] = c.req.routePath;\n span.annotations[\"http.url\"] = c.req.url;\n\n await next();\n\n // Capture the response status after the handler runs.\n span.annotations[\"http.status\"] = String(c.res.status);\n });\n });\n}\n","import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { makeIdempotent, IdempotencyConfig } from \"@workers-powertools/idempotency\";\nimport type { PersistenceLayer } from \"@workers-powertools/idempotency\";\n\n/**\n * Options for the idempotency middleware.\n */\nexport interface InjectIdempotencyOptions {\n /** Persistence layer for storing idempotency records (KV, D1, etc.). */\n persistenceLayer: PersistenceLayer;\n\n /** Configuration for key extraction and TTL. */\n config: IdempotencyConfig;\n\n /**\n * Header name that carries the idempotency key.\n * @default \"idempotency-key\"\n */\n headerName?: string;\n}\n\n/**\n * Hono middleware that checks idempotency before the handler runs.\n *\n * If a matching completed record exists in the persistence layer,\n * the stored response is returned immediately without executing the\n * downstream handler. Concurrent duplicate requests receive a 409.\n *\n * @param options - Idempotency configuration, persistence layer, and optional header name.\n */\nexport function injectIdempotency(options: InjectIdempotencyOptions): MiddlewareHandler {\n const { config, headerName = \"idempotency-key\" } = options;\n\n return createMiddleware(async (c, next) => {\n const idempotencyKey = c.req.header(headerName);\n\n // If no idempotency key header is present, skip the check\n // and let the request proceed normally.\n if (!idempotencyKey) {\n await next();\n return;\n }\n\n // Track whether the handler actually ran so we know if the result\n // came from cache or a live execution.\n let handlerRan = false;\n\n const idempotentHandler = makeIdempotent(\n async (_event: { key: string }) => {\n handlerRan = true;\n await next();\n\n // Capture the response body and status to store as the result.\n const body = await c.res.clone().text();\n const status = c.res.status;\n const contentType = c.res.headers.get(\"content-type\") ?? \"application/json\";\n\n return { body, status, contentType };\n },\n {\n get persistenceLayer() {\n return options.persistenceLayer;\n },\n // Wrap the key string in an object so extractKeyFromEvent can\n // traverse it with a simple dot-notation path.\n config: new IdempotencyConfig({\n eventKeyPath: \"key\",\n expiresAfterSeconds: config.expiresAfterSeconds,\n payloadValidationEnabled: config.payloadValidationEnabled,\n }),\n },\n );\n\n try {\n const result = await idempotentHandler({ key: idempotencyKey });\n\n // If the result came from cache (handler did not run this time),\n // reconstruct the response from the stored data.\n if (!handlerRan) {\n c.res = new Response(result.body, {\n status: result.status,\n headers: { \"content-type\": result.contentType },\n });\n }\n } catch (error) {\n // IdempotencyConflictError means a duplicate request is\n // already in progress — return 409 Conflict.\n if (error instanceof Error && error.name === \"IdempotencyConflictError\") {\n c.res = new Response(\n JSON.stringify({ error: \"Request is already being processed\" }),\n {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n },\n );\n return;\n }\n throw error;\n }\n });\n}\n"],"mappings":"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","names":["createMiddleware","createMiddleware","createMiddleware"]}
1
+ {"version":3,"sources":["../src/logger.ts","../src/metrics.ts","../src/tracer.ts","../src/idempotency.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type { Logger } from \"@workers-powertools/logger\";\n\n/**\n * Hono middleware that injects logger context for each request.\n *\n * Calls `logger.addContext` with the raw request and execution\n * context, then clears temporary keys once the handler completes.\n *\n * @param logger - A configured Logger instance.\n */\nexport function injectLogger(logger: Logger): MiddlewareHandler {\n return createMiddleware(async (c, next) => {\n // Enrich the logger with CF properties, correlation ID, etc.\n // Pass c.env so POWERTOOLS_SERVICE_NAME and POWERTOOLS_LOG_LEVEL are applied.\n logger.addContext(\n c.req.raw,\n c.executionCtx as unknown as ExecutionContext,\n c.env as unknown as Record<string, unknown>,\n );\n\n try {\n await next();\n } finally {\n // Clear per-request temporary keys so they don't leak\n // into subsequent requests when the logger is reused.\n logger.clearTemporaryKeys();\n }\n });\n}\n","import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type {\n Metrics,\n MetricsBackend,\n PipelineBinding,\n} from \"@workers-powertools/metrics\";\nimport { MetricUnit, PipelinesBackend } from \"@workers-powertools/metrics\";\n\n/**\n * Options for the injectMetrics middleware.\n */\nexport interface InjectMetricsOptions {\n /**\n * Factory function called once to construct the backend from the\n * Hono environment. Receives c.env so bindings are available.\n *\n * The factory is only invoked when no backend is set, or when the\n * binding reference has changed — not on every request.\n *\n * Defaults to a PipelinesBackend resolved from env.METRICS_PIPELINE\n * if that binding exists, otherwise no backend is set and a warning\n * is emitted.\n *\n * @example\n * // Pipelines (recommended)\n * backendFactory: (env) => new PipelinesBackend({ binding: env.METRICS_PIPELINE })\n *\n * // Analytics Engine (explicit opt-in — see AnalyticsEngineBackend docs)\n * backendFactory: (env) => new AnalyticsEngineBackend({ binding: env.ANALYTICS })\n */\n backendFactory?: (env: Record<string, unknown>) => MetricsBackend;\n}\n\n/**\n * Hono middleware that instruments each request with business metrics.\n *\n * Resolves the metrics backend on the first request (or when the binding\n * reference changes), records request duration and count with per-metric\n * dimensions (route, method, status), and flushes asynchronously via\n * ctx.waitUntil.\n *\n * Dimensions are passed per-metric rather than accumulated on the Metrics\n * instance, avoiding concurrency hazards when multiple requests share the\n * same isolate.\n *\n * @example\n * // Default — uses env.METRICS_PIPELINE automatically\n * app.use(injectMetrics(metrics));\n *\n * // Custom binding name\n * app.use(injectMetrics(metrics, {\n * backendFactory: (env) => new PipelinesBackend({ binding: env.MY_PIPELINE }),\n * }));\n *\n * // Analytics Engine (opt-in)\n * app.use(injectMetrics(metrics, {\n * backendFactory: (env) => new AnalyticsEngineBackend({ binding: env.ANALYTICS }),\n * }));\n */\nexport function injectMetrics(\n metrics: Metrics,\n options?: InjectMetricsOptions,\n): MiddlewareHandler {\n return createMiddleware(async (c, next) => {\n const env = c.env as Record<string, unknown>;\n\n if (options?.backendFactory) {\n metrics.setBackend(options.backendFactory(env));\n } else {\n const defaultBinding = env[\"METRICS_PIPELINE\"];\n if (defaultBinding) {\n metrics.setBackend(\n new PipelinesBackend({\n binding: defaultBinding as PipelineBinding,\n }),\n );\n }\n }\n\n const startTime = Date.now();\n\n try {\n await next();\n\n const httpDimensions = {\n route: c.req.routePath,\n method: c.req.method,\n status: String(c.res.status),\n };\n\n metrics.addMetric(\n \"request_duration\",\n MetricUnit.Milliseconds,\n Date.now() - startTime,\n httpDimensions,\n );\n metrics.addMetric(\"request_count\", MetricUnit.Count, 1, httpDimensions);\n } finally {\n c.executionCtx.waitUntil(metrics.flush());\n }\n });\n}\n","import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type { Tracer } from \"@workers-powertools/tracer\";\n\n/**\n * Hono middleware that injects tracer context for each request.\n *\n * Extracts the correlation ID from the incoming request, then\n * wraps the downstream handler in a `captureAsync` span named\n * after the HTTP method and matched route pattern.\n *\n * @param tracer - A configured Tracer instance.\n */\nexport function injectTracer(tracer: Tracer): MiddlewareHandler {\n return createMiddleware(async (c, next) => {\n // Extract correlation ID and enrich the tracer.\n // Pass c.env so POWERTOOLS_SERVICE_NAME is applied at runtime.\n tracer.addContext(\n c.req.raw,\n c.executionCtx as unknown as ExecutionContext,\n c.env as unknown as Record<string, unknown>,\n );\n\n const spanName = `${c.req.method} ${c.req.routePath}`;\n\n await tracer.captureAsync(spanName, async (span) => {\n // Annotate the span with useful request metadata.\n span.annotations[\"http.method\"] = c.req.method;\n span.annotations[\"http.route\"] = c.req.routePath;\n span.annotations[\"http.url\"] = c.req.url;\n\n await next();\n\n // Capture the response status after the handler runs.\n span.annotations[\"http.status\"] = String(c.res.status);\n });\n });\n}\n","import { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { makeIdempotent, IdempotencyConfig } from \"@workers-powertools/idempotency\";\nimport type { PersistenceLayer } from \"@workers-powertools/idempotency\";\n\n/**\n * Options for the idempotency middleware.\n */\nexport interface InjectIdempotencyOptions {\n /** Persistence layer for storing idempotency records (KV, D1, etc.). */\n persistenceLayer: PersistenceLayer;\n\n /** Configuration for key extraction and TTL. */\n config: IdempotencyConfig;\n\n /**\n * Header name that carries the idempotency key.\n * @default \"idempotency-key\"\n */\n headerName?: string;\n}\n\n/**\n * Hono middleware that checks idempotency before the handler runs.\n *\n * If a matching completed record exists in the persistence layer,\n * the stored response is returned immediately without executing the\n * downstream handler. Concurrent duplicate requests receive a 409.\n *\n * @param options - Idempotency configuration, persistence layer, and optional header name.\n */\nexport function injectIdempotency(options: InjectIdempotencyOptions): MiddlewareHandler {\n const { config, headerName = \"idempotency-key\" } = options;\n\n return createMiddleware(async (c, next) => {\n const idempotencyKey = c.req.header(headerName);\n\n // If no idempotency key header is present, skip the check\n // and let the request proceed normally.\n if (!idempotencyKey) {\n await next();\n return;\n }\n\n // Track whether the handler actually ran so we know if the result\n // came from cache or a live execution.\n let handlerRan = false;\n\n const idempotentHandler = makeIdempotent(\n async (_event: { key: string }) => {\n handlerRan = true;\n await next();\n\n // Capture the response body and status to store as the result.\n const body = await c.res.clone().text();\n const status = c.res.status;\n const contentType = c.res.headers.get(\"content-type\") ?? \"application/json\";\n\n return { body, status, contentType };\n },\n {\n get persistenceLayer() {\n return options.persistenceLayer;\n },\n // Wrap the key string in an object so extractKeyFromEvent can\n // traverse it with a simple dot-notation path.\n config: new IdempotencyConfig({\n eventKeyPath: \"key\",\n expiresAfterSeconds: config.expiresAfterSeconds,\n payloadValidationEnabled: config.payloadValidationEnabled,\n }),\n },\n );\n\n try {\n const result = await idempotentHandler({ key: idempotencyKey });\n\n // If the result came from cache (handler did not run this time),\n // reconstruct the response from the stored data.\n if (!handlerRan) {\n c.res = new Response(result.body, {\n status: result.status,\n headers: { \"content-type\": result.contentType },\n });\n }\n } catch (error) {\n // IdempotencyConflictError means a duplicate request is\n // already in progress — return 409 Conflict.\n if (error instanceof Error && error.name === \"IdempotencyConflictError\") {\n c.res = new Response(\n JSON.stringify({ error: \"Request is already being processed\" }),\n {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n },\n );\n return;\n }\n throw error;\n }\n });\n}\n"],"mappings":"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","names":["createMiddleware","createMiddleware","createMiddleware"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workers-powertools/hono",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Hono middleware adapters for Workers Powertools (logger, metrics, tracer, idempotency)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,21 +17,21 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@workers-powertools/commons": ">=0.1.0",
21
- "@workers-powertools/idempotency": ">=0.1.0",
22
- "@workers-powertools/logger": ">=0.1.0",
23
- "@workers-powertools/metrics": ">=0.1.0",
24
- "@workers-powertools/tracer": ">=0.1.0",
20
+ "@workers-powertools/commons": ">=0.1.1",
21
+ "@workers-powertools/idempotency": ">=0.1.1",
22
+ "@workers-powertools/logger": ">=0.1.1",
23
+ "@workers-powertools/metrics": ">=0.3.0",
24
+ "@workers-powertools/tracer": ">=0.1.1",
25
25
  "hono": "^4.12.12"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@cloudflare/workers-types": "^4.20260408.1",
29
29
  "hono": "^4.12.12",
30
- "@workers-powertools/commons": "0.1.0",
31
- "@workers-powertools/idempotency": "0.1.0",
32
- "@workers-powertools/logger": "0.1.0",
33
- "@workers-powertools/metrics": "0.1.0",
34
- "@workers-powertools/tracer": "0.1.0"
30
+ "@workers-powertools/idempotency": "0.1.1",
31
+ "@workers-powertools/commons": "0.1.1",
32
+ "@workers-powertools/logger": "0.1.1",
33
+ "@workers-powertools/metrics": "0.3.0",
34
+ "@workers-powertools/tracer": "0.1.1"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup",