@saga-bus/hono 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dean Foran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # @saga-bus/hono
2
+
3
+ Hono integration for saga-bus, designed for edge runtimes (Cloudflare Workers, Deno, Bun).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @saga-bus/hono hono
9
+ # or
10
+ pnpm add @saga-bus/hono hono
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Edge Runtime Compatible**: Works with Cloudflare Workers, Deno, Bun
16
+ - **Context Variables**: Type-safe `c.var.bus` and `c.var.correlationId`
17
+ - **Health Handler**: Factory for health check routes
18
+ - **Error Handler**: Saga-specific error responses
19
+ - **No Node.js Dependencies**: Uses simple UUID generator
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { Hono } from "hono";
25
+ import { createBus } from "@saga-bus/core";
26
+ import {
27
+ sagaBusMiddleware,
28
+ sagaErrorHandler,
29
+ createHealthHandler,
30
+ } from "@saga-bus/hono";
31
+
32
+ const bus = createBus({ /* config */ });
33
+ await bus.start();
34
+
35
+ const app = new Hono();
36
+
37
+ // Error handler first
38
+ app.use("*", sagaErrorHandler());
39
+
40
+ // Attach bus to context
41
+ app.use("*", sagaBusMiddleware({ bus }));
42
+
43
+ // Health check
44
+ app.get("/health", createHealthHandler({ bus }));
45
+
46
+ // Your routes
47
+ app.post("/orders", async (c) => {
48
+ const bus = c.get("bus");
49
+ const correlationId = c.get("correlationId");
50
+
51
+ await bus.publish({
52
+ type: "CreateOrder",
53
+ payload: await c.req.json(),
54
+ });
55
+
56
+ return c.json({ correlationId });
57
+ });
58
+
59
+ export default app;
60
+ ```
61
+
62
+ ## API Reference
63
+
64
+ ### sagaBusMiddleware(options)
65
+
66
+ Creates middleware that attaches the bus instance and correlation ID to context.
67
+
68
+ ```typescript
69
+ interface SagaBusHonoOptions {
70
+ /** The bus instance to attach */
71
+ bus: Bus;
72
+
73
+ /** Header name for correlation ID (default: "x-correlation-id") */
74
+ correlationIdHeader?: string;
75
+
76
+ /** Whether to generate correlation ID if not present (default: true) */
77
+ generateCorrelationId?: boolean;
78
+
79
+ /** Custom correlation ID generator */
80
+ correlationIdGenerator?: () => string;
81
+ }
82
+ ```
83
+
84
+ ### sagaErrorHandler()
85
+
86
+ Error handler middleware for saga-related errors.
87
+
88
+ - **SagaTimeoutError**: Returns 408 Request Timeout
89
+ - **ConcurrencyError**: Returns 409 Conflict
90
+
91
+ ### createHealthHandler(options)
92
+
93
+ Creates a health check handler.
94
+
95
+ ```typescript
96
+ interface HealthCheckOptions {
97
+ /** The bus instance to check */
98
+ bus: Bus;
99
+
100
+ /** Additional health checks */
101
+ checks?: Array<{
102
+ name: string;
103
+ check: () => Promise<boolean>;
104
+ }>;
105
+ }
106
+ ```
107
+
108
+ ### SagaBusEnv
109
+
110
+ Type definition for Hono context variables.
111
+
112
+ ```typescript
113
+ interface SagaBusEnv extends Env {
114
+ Variables: {
115
+ bus: Bus;
116
+ correlationId: string;
117
+ };
118
+ }
119
+ ```
120
+
121
+ ## Examples
122
+
123
+ ### Basic Usage
124
+
125
+ ```typescript
126
+ import { Hono } from "hono";
127
+ import { sagaBusMiddleware, SagaBusEnv } from "@saga-bus/hono";
128
+
129
+ const app = new Hono<SagaBusEnv>();
130
+
131
+ app.use("*", sagaBusMiddleware({ bus }));
132
+
133
+ app.post("/messages", async (c) => {
134
+ const bus = c.get("bus");
135
+ const correlationId = c.get("correlationId");
136
+
137
+ await bus.publish({
138
+ type: c.req.query("type") || "Message",
139
+ payload: await c.req.json(),
140
+ });
141
+
142
+ return c.json({ success: true, correlationId });
143
+ });
144
+ ```
145
+
146
+ ### With Health Checks
147
+
148
+ ```typescript
149
+ import { createHealthHandler } from "@saga-bus/hono";
150
+
151
+ app.get("/health", createHealthHandler({
152
+ bus,
153
+ checks: [
154
+ {
155
+ name: "database",
156
+ check: async () => {
157
+ await db.query("SELECT 1");
158
+ return true;
159
+ },
160
+ },
161
+ ],
162
+ }));
163
+ ```
164
+
165
+ ### Cloudflare Workers
166
+
167
+ ```typescript
168
+ import { Hono } from "hono";
169
+ import { sagaBusMiddleware, SagaBusEnv } from "@saga-bus/hono";
170
+ import { createBus } from "@saga-bus/core";
171
+
172
+ interface Env {
173
+ DB: D1Database;
174
+ }
175
+
176
+ const app = new Hono<SagaBusEnv & { Bindings: Env }>();
177
+
178
+ // Create bus per-request from environment
179
+ app.use("*", async (c, next) => {
180
+ const bus = createBusFromEnv(c.env);
181
+ return sagaBusMiddleware({ bus })(c, next);
182
+ });
183
+
184
+ app.post("/messages", async (c) => {
185
+ await c.get("bus").publish({ type: "Message", payload: {} });
186
+ return c.json({ ok: true });
187
+ });
188
+
189
+ export default app;
190
+ ```
191
+
192
+ ### Deno
193
+
194
+ ```typescript
195
+ import { Hono } from "https://deno.land/x/hono/mod.ts";
196
+ import { sagaBusMiddleware, SagaBusEnv } from "@saga-bus/hono";
197
+
198
+ const app = new Hono<SagaBusEnv>();
199
+ app.use("*", sagaBusMiddleware({ bus }));
200
+
201
+ Deno.serve(app.fetch);
202
+ ```
203
+
204
+ ### Custom Correlation ID Generator
205
+
206
+ ```typescript
207
+ app.use("*", sagaBusMiddleware({
208
+ bus,
209
+ correlationIdHeader: "x-request-id",
210
+ correlationIdGenerator: () => `req-${Date.now()}-${Math.random().toString(36).slice(2)}`,
211
+ }));
212
+ ```
213
+
214
+ ## TypeScript Support
215
+
216
+ The package provides full type safety for context variables:
217
+
218
+ ```typescript
219
+ import { Hono } from "hono";
220
+ import { SagaBusEnv } from "@saga-bus/hono";
221
+
222
+ // Use SagaBusEnv for typed context
223
+ const app = new Hono<SagaBusEnv>();
224
+
225
+ app.get("/", (c) => {
226
+ // c.get("bus") is typed as Bus
227
+ const bus = c.get("bus");
228
+
229
+ // c.get("correlationId") is typed as string
230
+ const correlationId = c.get("correlationId");
231
+
232
+ return c.json({ correlationId });
233
+ });
234
+ ```
235
+
236
+ ## Error Handling
237
+
238
+ ```typescript
239
+ app.use("*", sagaErrorHandler());
240
+
241
+ app.get("/order/:id", async (c) => {
242
+ // If this throws a ConcurrencyError, client gets 409
243
+ await c.get("bus").publish({
244
+ type: "UpdateOrder",
245
+ payload: { id: c.req.param("id") },
246
+ });
247
+ return c.json({ updated: true });
248
+ });
249
+ ```
250
+
251
+ Response on ConcurrencyError:
252
+
253
+ ```json
254
+ {
255
+ "error": "Concurrency Conflict",
256
+ "message": "Expected version 1, but found 2",
257
+ "correlationId": "abc-123"
258
+ }
259
+ ```
260
+
261
+ ## License
262
+
263
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createHealthHandler: () => createHealthHandler,
24
+ createReadinessHandler: () => createReadinessHandler,
25
+ sagaBusMiddleware: () => sagaBusMiddleware,
26
+ sagaErrorHandler: () => sagaErrorHandler
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/middleware.ts
31
+ function generateId() {
32
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
33
+ const r = Math.random() * 16 | 0;
34
+ const v = c === "x" ? r : r & 3 | 8;
35
+ return v.toString(16);
36
+ });
37
+ }
38
+ function sagaBusMiddleware(options) {
39
+ const {
40
+ bus,
41
+ correlationIdHeader = "x-correlation-id",
42
+ generateCorrelationId = true,
43
+ correlationIdGenerator = generateId
44
+ } = options;
45
+ return async (c, next) => {
46
+ c.set("bus", bus);
47
+ let correlationId = c.req.header(correlationIdHeader);
48
+ if (!correlationId && generateCorrelationId) {
49
+ correlationId = correlationIdGenerator();
50
+ }
51
+ if (correlationId) {
52
+ c.set("correlationId", correlationId);
53
+ c.header(correlationIdHeader, correlationId);
54
+ }
55
+ await next();
56
+ };
57
+ }
58
+ function sagaErrorHandler() {
59
+ return (err, c) => {
60
+ if (err instanceof Error) {
61
+ if (err.name === "SagaTimeoutError") {
62
+ return c.json(
63
+ {
64
+ error: "Saga Timeout",
65
+ message: err.message,
66
+ correlationId: c.get("correlationId")
67
+ },
68
+ 408
69
+ );
70
+ }
71
+ if (err.name === "ConcurrencyError") {
72
+ return c.json(
73
+ {
74
+ error: "Concurrency Conflict",
75
+ message: err.message,
76
+ correlationId: c.get("correlationId")
77
+ },
78
+ 409
79
+ );
80
+ }
81
+ }
82
+ return c.json(
83
+ {
84
+ error: "Internal Server Error",
85
+ message: err instanceof Error ? err.message : "Unknown error",
86
+ correlationId: c.get("correlationId")
87
+ },
88
+ 500
89
+ );
90
+ };
91
+ }
92
+
93
+ // src/health.ts
94
+ function createHealthHandler(options) {
95
+ const { bus, checks = [] } = options;
96
+ return async (c) => {
97
+ const healthStatus = {
98
+ status: "healthy",
99
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
100
+ checks: {}
101
+ };
102
+ try {
103
+ if (bus) {
104
+ healthStatus.checks.bus = { status: "pass" };
105
+ } else {
106
+ throw new Error("Bus not available");
107
+ }
108
+ } catch (error) {
109
+ healthStatus.status = "unhealthy";
110
+ healthStatus.checks.bus = {
111
+ status: "fail",
112
+ message: error instanceof Error ? error.message : "Unknown error"
113
+ };
114
+ }
115
+ for (const check of checks) {
116
+ try {
117
+ const result = await check.check();
118
+ healthStatus.checks[check.name] = {
119
+ status: result ? "pass" : "fail"
120
+ };
121
+ if (!result) {
122
+ healthStatus.status = "unhealthy";
123
+ }
124
+ } catch (error) {
125
+ healthStatus.status = "unhealthy";
126
+ healthStatus.checks[check.name] = {
127
+ status: "fail",
128
+ message: error instanceof Error ? error.message : "Unknown error"
129
+ };
130
+ }
131
+ }
132
+ const statusCode = healthStatus.status === "healthy" ? 200 : 503;
133
+ return c.json(healthStatus, statusCode);
134
+ };
135
+ }
136
+ function createReadinessHandler(options) {
137
+ return createHealthHandler(options);
138
+ }
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ createHealthHandler,
142
+ createReadinessHandler,
143
+ sagaBusMiddleware,
144
+ sagaErrorHandler
145
+ });
146
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/health.ts"],"sourcesContent":["export { sagaBusMiddleware, sagaErrorHandler } from \"./middleware.js\";\nexport { createHealthHandler, createReadinessHandler } from \"./health.js\";\nexport type {\n SagaBusEnv,\n SagaBusHonoOptions,\n HealthCheckOptions,\n HealthStatus,\n} from \"./types.js\";\n","import type { MiddlewareHandler, ErrorHandler } from \"hono\";\nimport type { SagaBusEnv, SagaBusHonoOptions } from \"./types.js\";\n\n// Simple UUID generator for edge compatibility (no crypto.randomUUID dependency)\nfunction generateId(): string {\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Creates middleware that attaches the bus instance and correlation ID to context.\n */\nexport function sagaBusMiddleware(\n options: SagaBusHonoOptions\n): MiddlewareHandler<SagaBusEnv> {\n const {\n bus,\n correlationIdHeader = \"x-correlation-id\",\n generateCorrelationId = true,\n correlationIdGenerator = generateId,\n } = options;\n\n return async (c, next) => {\n // Attach bus to context\n c.set(\"bus\", bus);\n\n // Extract or generate correlation ID\n let correlationId = c.req.header(correlationIdHeader);\n\n if (!correlationId && generateCorrelationId) {\n correlationId = correlationIdGenerator();\n }\n\n if (correlationId) {\n c.set(\"correlationId\", correlationId);\n c.header(correlationIdHeader, correlationId);\n }\n\n await next();\n };\n}\n\n/**\n * Error handler for saga-related errors.\n * Use with app.onError(sagaErrorHandler())\n */\nexport function sagaErrorHandler(): ErrorHandler<SagaBusEnv> {\n return (err, c) => {\n if (err instanceof Error) {\n if (err.name === \"SagaTimeoutError\") {\n return c.json(\n {\n error: \"Saga Timeout\",\n message: err.message,\n correlationId: c.get(\"correlationId\"),\n },\n 408\n );\n }\n\n if (err.name === \"ConcurrencyError\") {\n return c.json(\n {\n error: \"Concurrency Conflict\",\n message: err.message,\n correlationId: c.get(\"correlationId\"),\n },\n 409\n );\n }\n }\n\n // Return generic error for other cases\n return c.json(\n {\n error: \"Internal Server Error\",\n message: err instanceof Error ? err.message : \"Unknown error\",\n correlationId: c.get(\"correlationId\"),\n },\n 500\n );\n };\n}\n","import type { Context } from \"hono\";\nimport type { HealthCheckOptions, HealthStatus, SagaBusEnv } from \"./types.js\";\n\n/**\n * Creates a health check handler.\n */\nexport function createHealthHandler(options: HealthCheckOptions) {\n const { bus, checks = [] } = options;\n\n return async (c: Context<SagaBusEnv>) => {\n const healthStatus: HealthStatus = {\n status: \"healthy\",\n timestamp: new Date().toISOString(),\n checks: {},\n };\n\n // Check bus\n try {\n if (bus) {\n healthStatus.checks.bus = { status: \"pass\" };\n } else {\n throw new Error(\"Bus not available\");\n }\n } catch (error) {\n healthStatus.status = \"unhealthy\";\n healthStatus.checks.bus = {\n status: \"fail\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n\n // Run additional checks\n for (const check of checks) {\n try {\n const result = await check.check();\n healthStatus.checks[check.name] = {\n status: result ? \"pass\" : \"fail\",\n };\n if (!result) {\n healthStatus.status = \"unhealthy\";\n }\n } catch (error) {\n healthStatus.status = \"unhealthy\";\n healthStatus.checks[check.name] = {\n status: \"fail\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n }\n\n const statusCode = healthStatus.status === \"healthy\" ? 200 : 503;\n return c.json(healthStatus, statusCode);\n };\n}\n\n/**\n * Creates a readiness check handler (alias for health).\n */\nexport function createReadinessHandler(options: HealthCheckOptions) {\n return createHealthHandler(options);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,76 @@
1
+ import * as hono from 'hono';
2
+ import { Env, MiddlewareHandler, ErrorHandler, Context } from 'hono';
3
+ import { Bus } from '@saga-bus/core';
4
+
5
+ interface SagaBusEnv extends Env {
6
+ Variables: {
7
+ bus: Bus;
8
+ correlationId: string;
9
+ };
10
+ }
11
+ interface SagaBusHonoOptions {
12
+ /** The bus instance to attach */
13
+ bus: Bus;
14
+ /** Header name for correlation ID */
15
+ correlationIdHeader?: string;
16
+ /** Whether to generate correlation ID if not present */
17
+ generateCorrelationId?: boolean;
18
+ /** Custom correlation ID generator */
19
+ correlationIdGenerator?: () => string;
20
+ }
21
+ interface HealthCheckOptions {
22
+ /** The bus instance to check */
23
+ bus: Bus;
24
+ /** Additional health checks */
25
+ checks?: Array<{
26
+ name: string;
27
+ check: () => Promise<boolean>;
28
+ }>;
29
+ }
30
+ interface HealthStatus {
31
+ status: "healthy" | "unhealthy";
32
+ timestamp: string;
33
+ checks: Record<string, {
34
+ status: "pass" | "fail";
35
+ message?: string;
36
+ }>;
37
+ }
38
+
39
+ /**
40
+ * Creates middleware that attaches the bus instance and correlation ID to context.
41
+ */
42
+ declare function sagaBusMiddleware(options: SagaBusHonoOptions): MiddlewareHandler<SagaBusEnv>;
43
+ /**
44
+ * Error handler for saga-related errors.
45
+ * Use with app.onError(sagaErrorHandler())
46
+ */
47
+ declare function sagaErrorHandler(): ErrorHandler<SagaBusEnv>;
48
+
49
+ /**
50
+ * Creates a health check handler.
51
+ */
52
+ declare function createHealthHandler(options: HealthCheckOptions): (c: Context<SagaBusEnv>) => Promise<Response & hono.TypedResponse<{
53
+ status: "healthy" | "unhealthy";
54
+ timestamp: string;
55
+ checks: {
56
+ [x: string]: {
57
+ status: "pass" | "fail";
58
+ message?: string | undefined;
59
+ };
60
+ };
61
+ }, 200 | 503, "json">>;
62
+ /**
63
+ * Creates a readiness check handler (alias for health).
64
+ */
65
+ declare function createReadinessHandler(options: HealthCheckOptions): (c: Context<SagaBusEnv>) => Promise<Response & hono.TypedResponse<{
66
+ status: "healthy" | "unhealthy";
67
+ timestamp: string;
68
+ checks: {
69
+ [x: string]: {
70
+ status: "pass" | "fail";
71
+ message?: string | undefined;
72
+ };
73
+ };
74
+ }, 200 | 503, "json">>;
75
+
76
+ export { type HealthCheckOptions, type HealthStatus, type SagaBusEnv, type SagaBusHonoOptions, createHealthHandler, createReadinessHandler, sagaBusMiddleware, sagaErrorHandler };
@@ -0,0 +1,76 @@
1
+ import * as hono from 'hono';
2
+ import { Env, MiddlewareHandler, ErrorHandler, Context } from 'hono';
3
+ import { Bus } from '@saga-bus/core';
4
+
5
+ interface SagaBusEnv extends Env {
6
+ Variables: {
7
+ bus: Bus;
8
+ correlationId: string;
9
+ };
10
+ }
11
+ interface SagaBusHonoOptions {
12
+ /** The bus instance to attach */
13
+ bus: Bus;
14
+ /** Header name for correlation ID */
15
+ correlationIdHeader?: string;
16
+ /** Whether to generate correlation ID if not present */
17
+ generateCorrelationId?: boolean;
18
+ /** Custom correlation ID generator */
19
+ correlationIdGenerator?: () => string;
20
+ }
21
+ interface HealthCheckOptions {
22
+ /** The bus instance to check */
23
+ bus: Bus;
24
+ /** Additional health checks */
25
+ checks?: Array<{
26
+ name: string;
27
+ check: () => Promise<boolean>;
28
+ }>;
29
+ }
30
+ interface HealthStatus {
31
+ status: "healthy" | "unhealthy";
32
+ timestamp: string;
33
+ checks: Record<string, {
34
+ status: "pass" | "fail";
35
+ message?: string;
36
+ }>;
37
+ }
38
+
39
+ /**
40
+ * Creates middleware that attaches the bus instance and correlation ID to context.
41
+ */
42
+ declare function sagaBusMiddleware(options: SagaBusHonoOptions): MiddlewareHandler<SagaBusEnv>;
43
+ /**
44
+ * Error handler for saga-related errors.
45
+ * Use with app.onError(sagaErrorHandler())
46
+ */
47
+ declare function sagaErrorHandler(): ErrorHandler<SagaBusEnv>;
48
+
49
+ /**
50
+ * Creates a health check handler.
51
+ */
52
+ declare function createHealthHandler(options: HealthCheckOptions): (c: Context<SagaBusEnv>) => Promise<Response & hono.TypedResponse<{
53
+ status: "healthy" | "unhealthy";
54
+ timestamp: string;
55
+ checks: {
56
+ [x: string]: {
57
+ status: "pass" | "fail";
58
+ message?: string | undefined;
59
+ };
60
+ };
61
+ }, 200 | 503, "json">>;
62
+ /**
63
+ * Creates a readiness check handler (alias for health).
64
+ */
65
+ declare function createReadinessHandler(options: HealthCheckOptions): (c: Context<SagaBusEnv>) => Promise<Response & hono.TypedResponse<{
66
+ status: "healthy" | "unhealthy";
67
+ timestamp: string;
68
+ checks: {
69
+ [x: string]: {
70
+ status: "pass" | "fail";
71
+ message?: string | undefined;
72
+ };
73
+ };
74
+ }, 200 | 503, "json">>;
75
+
76
+ export { type HealthCheckOptions, type HealthStatus, type SagaBusEnv, type SagaBusHonoOptions, createHealthHandler, createReadinessHandler, sagaBusMiddleware, sagaErrorHandler };
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ // src/middleware.ts
2
+ function generateId() {
3
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
4
+ const r = Math.random() * 16 | 0;
5
+ const v = c === "x" ? r : r & 3 | 8;
6
+ return v.toString(16);
7
+ });
8
+ }
9
+ function sagaBusMiddleware(options) {
10
+ const {
11
+ bus,
12
+ correlationIdHeader = "x-correlation-id",
13
+ generateCorrelationId = true,
14
+ correlationIdGenerator = generateId
15
+ } = options;
16
+ return async (c, next) => {
17
+ c.set("bus", bus);
18
+ let correlationId = c.req.header(correlationIdHeader);
19
+ if (!correlationId && generateCorrelationId) {
20
+ correlationId = correlationIdGenerator();
21
+ }
22
+ if (correlationId) {
23
+ c.set("correlationId", correlationId);
24
+ c.header(correlationIdHeader, correlationId);
25
+ }
26
+ await next();
27
+ };
28
+ }
29
+ function sagaErrorHandler() {
30
+ return (err, c) => {
31
+ if (err instanceof Error) {
32
+ if (err.name === "SagaTimeoutError") {
33
+ return c.json(
34
+ {
35
+ error: "Saga Timeout",
36
+ message: err.message,
37
+ correlationId: c.get("correlationId")
38
+ },
39
+ 408
40
+ );
41
+ }
42
+ if (err.name === "ConcurrencyError") {
43
+ return c.json(
44
+ {
45
+ error: "Concurrency Conflict",
46
+ message: err.message,
47
+ correlationId: c.get("correlationId")
48
+ },
49
+ 409
50
+ );
51
+ }
52
+ }
53
+ return c.json(
54
+ {
55
+ error: "Internal Server Error",
56
+ message: err instanceof Error ? err.message : "Unknown error",
57
+ correlationId: c.get("correlationId")
58
+ },
59
+ 500
60
+ );
61
+ };
62
+ }
63
+
64
+ // src/health.ts
65
+ function createHealthHandler(options) {
66
+ const { bus, checks = [] } = options;
67
+ return async (c) => {
68
+ const healthStatus = {
69
+ status: "healthy",
70
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
71
+ checks: {}
72
+ };
73
+ try {
74
+ if (bus) {
75
+ healthStatus.checks.bus = { status: "pass" };
76
+ } else {
77
+ throw new Error("Bus not available");
78
+ }
79
+ } catch (error) {
80
+ healthStatus.status = "unhealthy";
81
+ healthStatus.checks.bus = {
82
+ status: "fail",
83
+ message: error instanceof Error ? error.message : "Unknown error"
84
+ };
85
+ }
86
+ for (const check of checks) {
87
+ try {
88
+ const result = await check.check();
89
+ healthStatus.checks[check.name] = {
90
+ status: result ? "pass" : "fail"
91
+ };
92
+ if (!result) {
93
+ healthStatus.status = "unhealthy";
94
+ }
95
+ } catch (error) {
96
+ healthStatus.status = "unhealthy";
97
+ healthStatus.checks[check.name] = {
98
+ status: "fail",
99
+ message: error instanceof Error ? error.message : "Unknown error"
100
+ };
101
+ }
102
+ }
103
+ const statusCode = healthStatus.status === "healthy" ? 200 : 503;
104
+ return c.json(healthStatus, statusCode);
105
+ };
106
+ }
107
+ function createReadinessHandler(options) {
108
+ return createHealthHandler(options);
109
+ }
110
+ export {
111
+ createHealthHandler,
112
+ createReadinessHandler,
113
+ sagaBusMiddleware,
114
+ sagaErrorHandler
115
+ };
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/middleware.ts","../src/health.ts"],"sourcesContent":["import type { MiddlewareHandler, ErrorHandler } from \"hono\";\nimport type { SagaBusEnv, SagaBusHonoOptions } from \"./types.js\";\n\n// Simple UUID generator for edge compatibility (no crypto.randomUUID dependency)\nfunction generateId(): string {\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Creates middleware that attaches the bus instance and correlation ID to context.\n */\nexport function sagaBusMiddleware(\n options: SagaBusHonoOptions\n): MiddlewareHandler<SagaBusEnv> {\n const {\n bus,\n correlationIdHeader = \"x-correlation-id\",\n generateCorrelationId = true,\n correlationIdGenerator = generateId,\n } = options;\n\n return async (c, next) => {\n // Attach bus to context\n c.set(\"bus\", bus);\n\n // Extract or generate correlation ID\n let correlationId = c.req.header(correlationIdHeader);\n\n if (!correlationId && generateCorrelationId) {\n correlationId = correlationIdGenerator();\n }\n\n if (correlationId) {\n c.set(\"correlationId\", correlationId);\n c.header(correlationIdHeader, correlationId);\n }\n\n await next();\n };\n}\n\n/**\n * Error handler for saga-related errors.\n * Use with app.onError(sagaErrorHandler())\n */\nexport function sagaErrorHandler(): ErrorHandler<SagaBusEnv> {\n return (err, c) => {\n if (err instanceof Error) {\n if (err.name === \"SagaTimeoutError\") {\n return c.json(\n {\n error: \"Saga Timeout\",\n message: err.message,\n correlationId: c.get(\"correlationId\"),\n },\n 408\n );\n }\n\n if (err.name === \"ConcurrencyError\") {\n return c.json(\n {\n error: \"Concurrency Conflict\",\n message: err.message,\n correlationId: c.get(\"correlationId\"),\n },\n 409\n );\n }\n }\n\n // Return generic error for other cases\n return c.json(\n {\n error: \"Internal Server Error\",\n message: err instanceof Error ? err.message : \"Unknown error\",\n correlationId: c.get(\"correlationId\"),\n },\n 500\n );\n };\n}\n","import type { Context } from \"hono\";\nimport type { HealthCheckOptions, HealthStatus, SagaBusEnv } from \"./types.js\";\n\n/**\n * Creates a health check handler.\n */\nexport function createHealthHandler(options: HealthCheckOptions) {\n const { bus, checks = [] } = options;\n\n return async (c: Context<SagaBusEnv>) => {\n const healthStatus: HealthStatus = {\n status: \"healthy\",\n timestamp: new Date().toISOString(),\n checks: {},\n };\n\n // Check bus\n try {\n if (bus) {\n healthStatus.checks.bus = { status: \"pass\" };\n } else {\n throw new Error(\"Bus not available\");\n }\n } catch (error) {\n healthStatus.status = \"unhealthy\";\n healthStatus.checks.bus = {\n status: \"fail\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n\n // Run additional checks\n for (const check of checks) {\n try {\n const result = await check.check();\n healthStatus.checks[check.name] = {\n status: result ? \"pass\" : \"fail\",\n };\n if (!result) {\n healthStatus.status = \"unhealthy\";\n }\n } catch (error) {\n healthStatus.status = \"unhealthy\";\n healthStatus.checks[check.name] = {\n status: \"fail\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n }\n\n const statusCode = healthStatus.status === \"healthy\" ? 200 : 503;\n return c.json(healthStatus, statusCode);\n };\n}\n\n/**\n * Creates a readiness check handler (alias for health).\n */\nexport function createReadinessHandler(options: HealthCheckOptions) {\n return createHealthHandler(options);\n}\n"],"mappings":";AAIA,SAAS,aAAqB;AAC5B,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,UAAM,IAAI,MAAM,MAAM,IAAK,IAAI,IAAO;AACtC,WAAO,EAAE,SAAS,EAAE;AAAA,EACtB,CAAC;AACH;AAKO,SAAS,kBACd,SAC+B;AAC/B,QAAM;AAAA,IACJ;AAAA,IACA,sBAAsB;AAAA,IACtB,wBAAwB;AAAA,IACxB,yBAAyB;AAAA,EAC3B,IAAI;AAEJ,SAAO,OAAO,GAAG,SAAS;AAExB,MAAE,IAAI,OAAO,GAAG;AAGhB,QAAI,gBAAgB,EAAE,IAAI,OAAO,mBAAmB;AAEpD,QAAI,CAAC,iBAAiB,uBAAuB;AAC3C,sBAAgB,uBAAuB;AAAA,IACzC;AAEA,QAAI,eAAe;AACjB,QAAE,IAAI,iBAAiB,aAAa;AACpC,QAAE,OAAO,qBAAqB,aAAa;AAAA,IAC7C;AAEA,UAAM,KAAK;AAAA,EACb;AACF;AAMO,SAAS,mBAA6C;AAC3D,SAAO,CAAC,KAAK,MAAM;AACjB,QAAI,eAAe,OAAO;AACxB,UAAI,IAAI,SAAS,oBAAoB;AACnC,eAAO,EAAE;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,SAAS,IAAI;AAAA,YACb,eAAe,EAAE,IAAI,eAAe;AAAA,UACtC;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,oBAAoB;AACnC,eAAO,EAAE;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,SAAS,IAAI;AAAA,YACb,eAAe,EAAE,IAAI,eAAe;AAAA,UACtC;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,WAAO,EAAE;AAAA,MACP;AAAA,QACE,OAAO;AAAA,QACP,SAAS,eAAe,QAAQ,IAAI,UAAU;AAAA,QAC9C,eAAe,EAAE,IAAI,eAAe;AAAA,MACtC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;;;AC/EO,SAAS,oBAAoB,SAA6B;AAC/D,QAAM,EAAE,KAAK,SAAS,CAAC,EAAE,IAAI;AAE7B,SAAO,OAAO,MAA2B;AACvC,UAAM,eAA6B;AAAA,MACjC,QAAQ;AAAA,MACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,QAAQ,CAAC;AAAA,IACX;AAGA,QAAI;AACF,UAAI,KAAK;AACP,qBAAa,OAAO,MAAM,EAAE,QAAQ,OAAO;AAAA,MAC7C,OAAO;AACL,cAAM,IAAI,MAAM,mBAAmB;AAAA,MACrC;AAAA,IACF,SAAS,OAAO;AACd,mBAAa,SAAS;AACtB,mBAAa,OAAO,MAAM;AAAA,QACxB,QAAQ;AAAA,QACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACpD;AAAA,IACF;AAGA,eAAW,SAAS,QAAQ;AAC1B,UAAI;AACF,cAAM,SAAS,MAAM,MAAM,MAAM;AACjC,qBAAa,OAAO,MAAM,IAAI,IAAI;AAAA,UAChC,QAAQ,SAAS,SAAS;AAAA,QAC5B;AACA,YAAI,CAAC,QAAQ;AACX,uBAAa,SAAS;AAAA,QACxB;AAAA,MACF,SAAS,OAAO;AACd,qBAAa,SAAS;AACtB,qBAAa,OAAO,MAAM,IAAI,IAAI;AAAA,UAChC,QAAQ;AAAA,UACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AAEA,UAAM,aAAa,aAAa,WAAW,YAAY,MAAM;AAC7D,WAAO,EAAE,KAAK,cAAc,UAAU;AAAA,EACxC;AACF;AAKO,SAAS,uBAAuB,SAA6B;AAClE,SAAO,oBAAoB,OAAO;AACpC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@saga-bus/hono",
3
+ "version": "0.1.1",
4
+ "description": "Hono integration for saga-bus (edge runtime compatible)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/d-e-a-n-f/saga-bus.git",
26
+ "directory": "packages/hono"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/d-e-a-n-f/saga-bus/issues"
30
+ },
31
+ "homepage": "https://github.com/d-e-a-n-f/saga-bus#readme",
32
+ "keywords": [
33
+ "saga",
34
+ "message-bus",
35
+ "hono",
36
+ "middleware",
37
+ "edge",
38
+ "cloudflare-workers",
39
+ "deno",
40
+ "bun"
41
+ ],
42
+ "dependencies": {
43
+ "@saga-bus/core": "0.1.1"
44
+ },
45
+ "devDependencies": {
46
+ "hono": "^4.6.0",
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.9.2",
49
+ "vitest": "^3.0.0",
50
+ "@repo/eslint-config": "0.0.0",
51
+ "@repo/typescript-config": "0.0.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@saga-bus/core": ">=0.1.1",
55
+ "hono": ">=4.0.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "dev": "tsup --watch",
60
+ "lint": "eslint src/",
61
+ "check-types": "tsc --noEmit",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest"
64
+ }
65
+ }