@saga-bus/fastify 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE 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,265 @@
1
+ # @saga-bus/fastify
2
+
3
+ Fastify plugin for saga-bus with lifecycle management, request decoration, and health checks.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @saga-bus/fastify fastify
9
+ # or
10
+ pnpm add @saga-bus/fastify fastify
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Plugin Registration**: Register bus as a Fastify plugin
16
+ - **Request Decoration**: Access bus via `request.bus`
17
+ - **Correlation ID**: Automatic extraction and generation
18
+ - **Lifecycle Hooks**: Auto-start/stop bus with Fastify
19
+ - **Health Check**: Built-in health check route
20
+ - **Error Handler**: Saga-specific error handling
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import Fastify from "fastify";
26
+ import { createBus } from "@saga-bus/core";
27
+ import { sagaBusFastifyPlugin } from "@saga-bus/fastify";
28
+
29
+ const bus = createBus({ /* config */ });
30
+
31
+ const app = Fastify({ logger: true });
32
+
33
+ // Register plugin
34
+ await app.register(sagaBusFastifyPlugin, {
35
+ bus,
36
+ healthCheck: true,
37
+ });
38
+
39
+ // Your routes
40
+ app.post("/orders", async (request, reply) => {
41
+ await request.bus.publish({
42
+ type: "CreateOrder",
43
+ payload: request.body,
44
+ });
45
+ return { correlationId: request.correlationId };
46
+ });
47
+
48
+ await app.listen({ port: 3000 });
49
+ ```
50
+
51
+ ## API Reference
52
+
53
+ ### sagaBusFastifyPlugin
54
+
55
+ Fastify plugin that integrates saga-bus.
56
+
57
+ ```typescript
58
+ interface SagaBusFastifyOptions {
59
+ /** The bus instance to register */
60
+ bus: Bus;
61
+
62
+ /** Whether to start bus when Fastify starts (default: true) */
63
+ autoStart?: boolean;
64
+
65
+ /** Whether to stop bus when Fastify closes (default: true) */
66
+ autoStop?: boolean;
67
+
68
+ /** Header name for correlation ID (default: "x-correlation-id") */
69
+ correlationIdHeader?: string;
70
+
71
+ /** Whether to generate correlation ID if not present (default: true) */
72
+ generateCorrelationId?: boolean;
73
+
74
+ /** Custom correlation ID generator */
75
+ correlationIdGenerator?: () => string;
76
+
77
+ /** Enable health check route */
78
+ healthCheck?: boolean | HealthCheckConfig;
79
+ }
80
+
81
+ interface HealthCheckConfig {
82
+ /** Route path for health check (default: "/health") */
83
+ path?: string;
84
+
85
+ /** Additional health checks */
86
+ checks?: Array<{
87
+ name: string;
88
+ check: () => Promise<boolean>;
89
+ }>;
90
+ }
91
+ ```
92
+
93
+ ## Examples
94
+
95
+ ### Basic Usage
96
+
97
+ ```typescript
98
+ import Fastify from "fastify";
99
+ import { createBus } from "@saga-bus/core";
100
+ import { sagaBusFastifyPlugin } from "@saga-bus/fastify";
101
+
102
+ const bus = createBus({ /* config */ });
103
+ const app = Fastify();
104
+
105
+ await app.register(sagaBusFastifyPlugin, { bus });
106
+
107
+ app.post("/messages", async (request) => {
108
+ await request.bus.publish({
109
+ type: request.body.type,
110
+ payload: request.body.payload,
111
+ });
112
+ return { success: true, correlationId: request.correlationId };
113
+ });
114
+
115
+ await app.listen({ port: 3000 });
116
+ ```
117
+
118
+ ### With Health Check
119
+
120
+ ```typescript
121
+ await app.register(sagaBusFastifyPlugin, {
122
+ bus,
123
+ healthCheck: {
124
+ path: "/health",
125
+ checks: [
126
+ {
127
+ name: "database",
128
+ check: async () => {
129
+ await pool.query("SELECT 1");
130
+ return true;
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ });
136
+ ```
137
+
138
+ Health check response:
139
+
140
+ ```json
141
+ {
142
+ "status": "healthy",
143
+ "timestamp": "2024-01-01T00:00:00.000Z",
144
+ "checks": {
145
+ "bus": { "status": "pass" },
146
+ "database": { "status": "pass" }
147
+ }
148
+ }
149
+ ```
150
+
151
+ ### Custom Correlation ID
152
+
153
+ ```typescript
154
+ await app.register(sagaBusFastifyPlugin, {
155
+ bus,
156
+ correlationIdHeader: "x-request-id",
157
+ correlationIdGenerator: () => `req-${Date.now()}-${Math.random().toString(36).slice(2)}`,
158
+ });
159
+ ```
160
+
161
+ ### Manual Lifecycle Control
162
+
163
+ ```typescript
164
+ await app.register(sagaBusFastifyPlugin, {
165
+ bus,
166
+ autoStart: false,
167
+ autoStop: false,
168
+ });
169
+
170
+ // Start bus manually
171
+ await bus.start();
172
+
173
+ // Stop bus manually before close
174
+ app.addHook("onClose", async () => {
175
+ await bus.stop();
176
+ });
177
+ ```
178
+
179
+ ## TypeScript Support
180
+
181
+ The plugin extends Fastify types:
182
+
183
+ ```typescript
184
+ // In your route handlers
185
+ app.get("/", async (request, reply) => {
186
+ // request.bus is typed as Bus
187
+ await request.bus.publish(message);
188
+
189
+ // request.correlationId is typed as string
190
+ console.log(`Processing ${request.correlationId}`);
191
+
192
+ // fastify.bus is also available
193
+ await request.server.bus.publish(message);
194
+ });
195
+ ```
196
+
197
+ ## Error Handling
198
+
199
+ The plugin installs a custom error handler for saga-specific errors:
200
+
201
+ - **SagaTimeoutError**: Returns 408 Request Timeout
202
+ - **ConcurrencyError**: Returns 409 Conflict
203
+
204
+ ```typescript
205
+ app.get("/order/:id", async (request) => {
206
+ // If this throws a ConcurrencyError, client gets 409
207
+ await request.bus.publish({ type: "UpdateOrder", payload: { id: request.params.id } });
208
+ });
209
+ ```
210
+
211
+ Response on ConcurrencyError:
212
+
213
+ ```json
214
+ {
215
+ "error": "Concurrency Conflict",
216
+ "message": "Expected version 1, but found 2",
217
+ "correlationId": "abc-123"
218
+ }
219
+ ```
220
+
221
+ ## Example: Complete Application
222
+
223
+ ```typescript
224
+ import Fastify from "fastify";
225
+ import { createBus, InMemoryTransport, InMemorySagaStore } from "@saga-bus/core";
226
+ import { sagaBusFastifyPlugin } from "@saga-bus/fastify";
227
+
228
+ // Create bus
229
+ const bus = createBus({
230
+ transport: new InMemoryTransport(),
231
+ store: new InMemorySagaStore(),
232
+ });
233
+
234
+ // Create Fastify app
235
+ const app = Fastify({ logger: true });
236
+
237
+ // Register saga-bus plugin
238
+ await app.register(sagaBusFastifyPlugin, {
239
+ bus,
240
+ healthCheck: true,
241
+ });
242
+
243
+ // Routes
244
+ app.post("/orders", async (request) => {
245
+ await request.bus.publish({
246
+ type: "CreateOrder",
247
+ payload: request.body,
248
+ });
249
+ return { orderId: "new-order", correlationId: request.correlationId };
250
+ });
251
+
252
+ app.get("/orders/:id", async (request) => {
253
+ // Access bus from request
254
+ const state = await request.bus.getSagaState("OrderSaga", request.params.id);
255
+ return state;
256
+ });
257
+
258
+ // Start server
259
+ await app.listen({ port: 3000 });
260
+ console.log("Server running on port 3000");
261
+ ```
262
+
263
+ ## License
264
+
265
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ sagaBusFastifyPlugin: () => sagaBusFastifyPlugin
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/plugin.ts
38
+ var import_fastify_plugin = __toESM(require("fastify-plugin"), 1);
39
+ var import_crypto = require("crypto");
40
+ var sagaBusPlugin = async (fastify, options) => {
41
+ const {
42
+ bus,
43
+ autoStart = true,
44
+ autoStop = true,
45
+ correlationIdHeader = "x-correlation-id",
46
+ generateCorrelationId = true,
47
+ correlationIdGenerator = import_crypto.randomUUID,
48
+ healthCheck = false
49
+ } = options;
50
+ fastify.decorate("bus", bus);
51
+ fastify.decorateRequest("bus", null);
52
+ fastify.decorateRequest("correlationId", "");
53
+ fastify.addHook("onRequest", async (request, reply) => {
54
+ request.bus = bus;
55
+ let correlationId = request.headers[correlationIdHeader.toLowerCase()];
56
+ if (!correlationId && generateCorrelationId) {
57
+ correlationId = correlationIdGenerator();
58
+ }
59
+ if (correlationId) {
60
+ request.correlationId = correlationId;
61
+ reply.header(correlationIdHeader, correlationId);
62
+ }
63
+ });
64
+ if (autoStart) {
65
+ fastify.addHook("onReady", async () => {
66
+ await bus.start();
67
+ fastify.log.info("Saga bus started");
68
+ });
69
+ }
70
+ if (autoStop) {
71
+ fastify.addHook("onClose", async () => {
72
+ await bus.stop();
73
+ fastify.log.info("Saga bus stopped");
74
+ });
75
+ }
76
+ if (healthCheck) {
77
+ const config = typeof healthCheck === "boolean" ? { path: "/health", checks: [] } : { path: "/health", checks: [], ...healthCheck };
78
+ fastify.get(config.path, async (_request, reply) => {
79
+ const healthStatus = {
80
+ status: "healthy",
81
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
82
+ checks: {}
83
+ };
84
+ try {
85
+ if (bus) {
86
+ healthStatus.checks.bus = { status: "pass" };
87
+ } else {
88
+ throw new Error("Bus not available");
89
+ }
90
+ } catch (error) {
91
+ healthStatus.status = "unhealthy";
92
+ healthStatus.checks.bus = {
93
+ status: "fail",
94
+ message: error instanceof Error ? error.message : "Unknown error"
95
+ };
96
+ }
97
+ for (const check of config.checks || []) {
98
+ try {
99
+ const result = await check.check();
100
+ healthStatus.checks[check.name] = {
101
+ status: result ? "pass" : "fail"
102
+ };
103
+ if (!result) {
104
+ healthStatus.status = "unhealthy";
105
+ }
106
+ } catch (error) {
107
+ healthStatus.status = "unhealthy";
108
+ healthStatus.checks[check.name] = {
109
+ status: "fail",
110
+ message: error instanceof Error ? error.message : "Unknown error"
111
+ };
112
+ }
113
+ }
114
+ const statusCode = healthStatus.status === "healthy" ? 200 : 503;
115
+ reply.code(statusCode).send(healthStatus);
116
+ });
117
+ }
118
+ fastify.setErrorHandler((error, request, reply) => {
119
+ if (error.name === "SagaTimeoutError") {
120
+ reply.code(408).send({
121
+ error: "Saga Timeout",
122
+ message: error.message,
123
+ correlationId: request.correlationId
124
+ });
125
+ return;
126
+ }
127
+ if (error.name === "ConcurrencyError") {
128
+ reply.code(409).send({
129
+ error: "Concurrency Conflict",
130
+ message: error.message,
131
+ correlationId: request.correlationId
132
+ });
133
+ return;
134
+ }
135
+ reply.send(error);
136
+ });
137
+ };
138
+ var sagaBusFastifyPlugin = (0, import_fastify_plugin.default)(sagaBusPlugin, {
139
+ fastify: ">=4.0.0",
140
+ name: "@saga-bus/fastify"
141
+ });
142
+ // Annotate the CommonJS export names for ESM import in node:
143
+ 0 && (module.exports = {
144
+ sagaBusFastifyPlugin
145
+ });
146
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/plugin.ts"],"sourcesContent":["export { sagaBusFastifyPlugin } from \"./plugin.js\";\nexport type {\n SagaBusFastifyOptions,\n HealthCheckConfig,\n HealthStatus,\n} from \"./types.js\";\n","import type { FastifyPluginAsync, FastifyError } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport { randomUUID } from \"crypto\";\nimport type { SagaBusFastifyOptions, HealthCheckConfig, HealthStatus } from \"./types.js\";\n\nconst sagaBusPlugin: FastifyPluginAsync<SagaBusFastifyOptions> = async (\n fastify,\n options\n) => {\n const {\n bus,\n autoStart = true,\n autoStop = true,\n correlationIdHeader = \"x-correlation-id\",\n generateCorrelationId = true,\n correlationIdGenerator = randomUUID,\n healthCheck = false,\n } = options;\n\n // Decorate fastify instance with bus\n fastify.decorate(\"bus\", bus);\n\n // Decorate request with bus and correlationId\n // @ts-expect-error - Fastify decorateRequest typing is complex, null is valid for initial decoration\n fastify.decorateRequest(\"bus\", null);\n fastify.decorateRequest(\"correlationId\", \"\");\n\n // Add hook to set bus and correlation ID on request\n fastify.addHook(\"onRequest\", async (request, reply) => {\n request.bus = bus;\n\n // Extract or generate correlation ID\n let correlationId = request.headers[correlationIdHeader.toLowerCase()] as string | undefined;\n\n if (!correlationId && generateCorrelationId) {\n correlationId = correlationIdGenerator();\n }\n\n if (correlationId) {\n request.correlationId = correlationId;\n reply.header(correlationIdHeader, correlationId);\n }\n });\n\n // Auto-start bus\n if (autoStart) {\n fastify.addHook(\"onReady\", async () => {\n await bus.start();\n fastify.log.info(\"Saga bus started\");\n });\n }\n\n // Auto-stop bus\n if (autoStop) {\n fastify.addHook(\"onClose\", async () => {\n await bus.stop();\n fastify.log.info(\"Saga bus stopped\");\n });\n }\n\n // Register health check route\n if (healthCheck) {\n const config: HealthCheckConfig = typeof healthCheck === \"boolean\"\n ? { path: \"/health\", checks: [] }\n : { path: \"/health\", checks: [], ...healthCheck };\n\n fastify.get(config.path!, async (_request, reply) => {\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 config.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 reply.code(statusCode).send(healthStatus);\n });\n }\n\n // Error handler for saga errors\n fastify.setErrorHandler((error: FastifyError, request, reply) => {\n if (error.name === \"SagaTimeoutError\") {\n reply.code(408).send({\n error: \"Saga Timeout\",\n message: error.message,\n correlationId: request.correlationId,\n });\n return;\n }\n\n if (error.name === \"ConcurrencyError\") {\n reply.code(409).send({\n error: \"Concurrency Conflict\",\n message: error.message,\n correlationId: request.correlationId,\n });\n return;\n }\n\n // Default error handling\n reply.send(error);\n });\n};\n\nexport const sagaBusFastifyPlugin = fp(sagaBusPlugin, {\n fastify: \">=4.0.0\",\n name: \"@saga-bus/fastify\",\n});\n"],"mappings":"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","names":["fp"]}
@@ -0,0 +1,49 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { Bus } from '@saga-bus/core';
3
+
4
+ declare module "fastify" {
5
+ interface FastifyRequest {
6
+ bus: Bus;
7
+ correlationId: string;
8
+ }
9
+ interface FastifyInstance {
10
+ bus: Bus;
11
+ }
12
+ }
13
+ interface SagaBusFastifyOptions {
14
+ /** The bus instance to register */
15
+ bus: Bus;
16
+ /** Whether to start bus when Fastify starts */
17
+ autoStart?: boolean;
18
+ /** Whether to stop bus when Fastify closes */
19
+ autoStop?: boolean;
20
+ /** Header name for correlation ID */
21
+ correlationIdHeader?: string;
22
+ /** Whether to generate correlation ID if not present */
23
+ generateCorrelationId?: boolean;
24
+ /** Custom correlation ID generator */
25
+ correlationIdGenerator?: () => string;
26
+ /** Enable health check route */
27
+ healthCheck?: boolean | HealthCheckConfig;
28
+ }
29
+ interface HealthCheckConfig {
30
+ /** Route path for health check */
31
+ path?: string;
32
+ /** Additional health checks */
33
+ checks?: Array<{
34
+ name: string;
35
+ check: () => Promise<boolean>;
36
+ }>;
37
+ }
38
+ interface HealthStatus {
39
+ status: "healthy" | "unhealthy";
40
+ timestamp: string;
41
+ checks: Record<string, {
42
+ status: "pass" | "fail";
43
+ message?: string;
44
+ }>;
45
+ }
46
+
47
+ declare const sagaBusFastifyPlugin: FastifyPluginAsync<SagaBusFastifyOptions>;
48
+
49
+ export { type HealthCheckConfig, type HealthStatus, type SagaBusFastifyOptions, sagaBusFastifyPlugin };
@@ -0,0 +1,49 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { Bus } from '@saga-bus/core';
3
+
4
+ declare module "fastify" {
5
+ interface FastifyRequest {
6
+ bus: Bus;
7
+ correlationId: string;
8
+ }
9
+ interface FastifyInstance {
10
+ bus: Bus;
11
+ }
12
+ }
13
+ interface SagaBusFastifyOptions {
14
+ /** The bus instance to register */
15
+ bus: Bus;
16
+ /** Whether to start bus when Fastify starts */
17
+ autoStart?: boolean;
18
+ /** Whether to stop bus when Fastify closes */
19
+ autoStop?: boolean;
20
+ /** Header name for correlation ID */
21
+ correlationIdHeader?: string;
22
+ /** Whether to generate correlation ID if not present */
23
+ generateCorrelationId?: boolean;
24
+ /** Custom correlation ID generator */
25
+ correlationIdGenerator?: () => string;
26
+ /** Enable health check route */
27
+ healthCheck?: boolean | HealthCheckConfig;
28
+ }
29
+ interface HealthCheckConfig {
30
+ /** Route path for health check */
31
+ path?: string;
32
+ /** Additional health checks */
33
+ checks?: Array<{
34
+ name: string;
35
+ check: () => Promise<boolean>;
36
+ }>;
37
+ }
38
+ interface HealthStatus {
39
+ status: "healthy" | "unhealthy";
40
+ timestamp: string;
41
+ checks: Record<string, {
42
+ status: "pass" | "fail";
43
+ message?: string;
44
+ }>;
45
+ }
46
+
47
+ declare const sagaBusFastifyPlugin: FastifyPluginAsync<SagaBusFastifyOptions>;
48
+
49
+ export { type HealthCheckConfig, type HealthStatus, type SagaBusFastifyOptions, sagaBusFastifyPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,109 @@
1
+ // src/plugin.ts
2
+ import fp from "fastify-plugin";
3
+ import { randomUUID } from "crypto";
4
+ var sagaBusPlugin = async (fastify, options) => {
5
+ const {
6
+ bus,
7
+ autoStart = true,
8
+ autoStop = true,
9
+ correlationIdHeader = "x-correlation-id",
10
+ generateCorrelationId = true,
11
+ correlationIdGenerator = randomUUID,
12
+ healthCheck = false
13
+ } = options;
14
+ fastify.decorate("bus", bus);
15
+ fastify.decorateRequest("bus", null);
16
+ fastify.decorateRequest("correlationId", "");
17
+ fastify.addHook("onRequest", async (request, reply) => {
18
+ request.bus = bus;
19
+ let correlationId = request.headers[correlationIdHeader.toLowerCase()];
20
+ if (!correlationId && generateCorrelationId) {
21
+ correlationId = correlationIdGenerator();
22
+ }
23
+ if (correlationId) {
24
+ request.correlationId = correlationId;
25
+ reply.header(correlationIdHeader, correlationId);
26
+ }
27
+ });
28
+ if (autoStart) {
29
+ fastify.addHook("onReady", async () => {
30
+ await bus.start();
31
+ fastify.log.info("Saga bus started");
32
+ });
33
+ }
34
+ if (autoStop) {
35
+ fastify.addHook("onClose", async () => {
36
+ await bus.stop();
37
+ fastify.log.info("Saga bus stopped");
38
+ });
39
+ }
40
+ if (healthCheck) {
41
+ const config = typeof healthCheck === "boolean" ? { path: "/health", checks: [] } : { path: "/health", checks: [], ...healthCheck };
42
+ fastify.get(config.path, async (_request, reply) => {
43
+ const healthStatus = {
44
+ status: "healthy",
45
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
46
+ checks: {}
47
+ };
48
+ try {
49
+ if (bus) {
50
+ healthStatus.checks.bus = { status: "pass" };
51
+ } else {
52
+ throw new Error("Bus not available");
53
+ }
54
+ } catch (error) {
55
+ healthStatus.status = "unhealthy";
56
+ healthStatus.checks.bus = {
57
+ status: "fail",
58
+ message: error instanceof Error ? error.message : "Unknown error"
59
+ };
60
+ }
61
+ for (const check of config.checks || []) {
62
+ try {
63
+ const result = await check.check();
64
+ healthStatus.checks[check.name] = {
65
+ status: result ? "pass" : "fail"
66
+ };
67
+ if (!result) {
68
+ healthStatus.status = "unhealthy";
69
+ }
70
+ } catch (error) {
71
+ healthStatus.status = "unhealthy";
72
+ healthStatus.checks[check.name] = {
73
+ status: "fail",
74
+ message: error instanceof Error ? error.message : "Unknown error"
75
+ };
76
+ }
77
+ }
78
+ const statusCode = healthStatus.status === "healthy" ? 200 : 503;
79
+ reply.code(statusCode).send(healthStatus);
80
+ });
81
+ }
82
+ fastify.setErrorHandler((error, request, reply) => {
83
+ if (error.name === "SagaTimeoutError") {
84
+ reply.code(408).send({
85
+ error: "Saga Timeout",
86
+ message: error.message,
87
+ correlationId: request.correlationId
88
+ });
89
+ return;
90
+ }
91
+ if (error.name === "ConcurrencyError") {
92
+ reply.code(409).send({
93
+ error: "Concurrency Conflict",
94
+ message: error.message,
95
+ correlationId: request.correlationId
96
+ });
97
+ return;
98
+ }
99
+ reply.send(error);
100
+ });
101
+ };
102
+ var sagaBusFastifyPlugin = fp(sagaBusPlugin, {
103
+ fastify: ">=4.0.0",
104
+ name: "@saga-bus/fastify"
105
+ });
106
+ export {
107
+ sagaBusFastifyPlugin
108
+ };
109
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { FastifyPluginAsync, FastifyError } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport { randomUUID } from \"crypto\";\nimport type { SagaBusFastifyOptions, HealthCheckConfig, HealthStatus } from \"./types.js\";\n\nconst sagaBusPlugin: FastifyPluginAsync<SagaBusFastifyOptions> = async (\n fastify,\n options\n) => {\n const {\n bus,\n autoStart = true,\n autoStop = true,\n correlationIdHeader = \"x-correlation-id\",\n generateCorrelationId = true,\n correlationIdGenerator = randomUUID,\n healthCheck = false,\n } = options;\n\n // Decorate fastify instance with bus\n fastify.decorate(\"bus\", bus);\n\n // Decorate request with bus and correlationId\n // @ts-expect-error - Fastify decorateRequest typing is complex, null is valid for initial decoration\n fastify.decorateRequest(\"bus\", null);\n fastify.decorateRequest(\"correlationId\", \"\");\n\n // Add hook to set bus and correlation ID on request\n fastify.addHook(\"onRequest\", async (request, reply) => {\n request.bus = bus;\n\n // Extract or generate correlation ID\n let correlationId = request.headers[correlationIdHeader.toLowerCase()] as string | undefined;\n\n if (!correlationId && generateCorrelationId) {\n correlationId = correlationIdGenerator();\n }\n\n if (correlationId) {\n request.correlationId = correlationId;\n reply.header(correlationIdHeader, correlationId);\n }\n });\n\n // Auto-start bus\n if (autoStart) {\n fastify.addHook(\"onReady\", async () => {\n await bus.start();\n fastify.log.info(\"Saga bus started\");\n });\n }\n\n // Auto-stop bus\n if (autoStop) {\n fastify.addHook(\"onClose\", async () => {\n await bus.stop();\n fastify.log.info(\"Saga bus stopped\");\n });\n }\n\n // Register health check route\n if (healthCheck) {\n const config: HealthCheckConfig = typeof healthCheck === \"boolean\"\n ? { path: \"/health\", checks: [] }\n : { path: \"/health\", checks: [], ...healthCheck };\n\n fastify.get(config.path!, async (_request, reply) => {\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 config.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 reply.code(statusCode).send(healthStatus);\n });\n }\n\n // Error handler for saga errors\n fastify.setErrorHandler((error: FastifyError, request, reply) => {\n if (error.name === \"SagaTimeoutError\") {\n reply.code(408).send({\n error: \"Saga Timeout\",\n message: error.message,\n correlationId: request.correlationId,\n });\n return;\n }\n\n if (error.name === \"ConcurrencyError\") {\n reply.code(409).send({\n error: \"Concurrency Conflict\",\n message: error.message,\n correlationId: request.correlationId,\n });\n return;\n }\n\n // Default error handling\n reply.send(error);\n });\n};\n\nexport const sagaBusFastifyPlugin = fp(sagaBusPlugin, {\n fastify: \">=4.0.0\",\n name: \"@saga-bus/fastify\",\n});\n"],"mappings":";AACA,OAAO,QAAQ;AACf,SAAS,kBAAkB;AAG3B,IAAM,gBAA2D,OAC/D,SACA,YACG;AACH,QAAM;AAAA,IACJ;AAAA,IACA,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,sBAAsB;AAAA,IACtB,wBAAwB;AAAA,IACxB,yBAAyB;AAAA,IACzB,cAAc;AAAA,EAChB,IAAI;AAGJ,UAAQ,SAAS,OAAO,GAAG;AAI3B,UAAQ,gBAAgB,OAAO,IAAI;AACnC,UAAQ,gBAAgB,iBAAiB,EAAE;AAG3C,UAAQ,QAAQ,aAAa,OAAO,SAAS,UAAU;AACrD,YAAQ,MAAM;AAGd,QAAI,gBAAgB,QAAQ,QAAQ,oBAAoB,YAAY,CAAC;AAErE,QAAI,CAAC,iBAAiB,uBAAuB;AAC3C,sBAAgB,uBAAuB;AAAA,IACzC;AAEA,QAAI,eAAe;AACjB,cAAQ,gBAAgB;AACxB,YAAM,OAAO,qBAAqB,aAAa;AAAA,IACjD;AAAA,EACF,CAAC;AAGD,MAAI,WAAW;AACb,YAAQ,QAAQ,WAAW,YAAY;AACrC,YAAM,IAAI,MAAM;AAChB,cAAQ,IAAI,KAAK,kBAAkB;AAAA,IACrC,CAAC;AAAA,EACH;AAGA,MAAI,UAAU;AACZ,YAAQ,QAAQ,WAAW,YAAY;AACrC,YAAM,IAAI,KAAK;AACf,cAAQ,IAAI,KAAK,kBAAkB;AAAA,IACrC,CAAC;AAAA,EACH;AAGA,MAAI,aAAa;AACf,UAAM,SAA4B,OAAO,gBAAgB,YACrD,EAAE,MAAM,WAAW,QAAQ,CAAC,EAAE,IAC9B,EAAE,MAAM,WAAW,QAAQ,CAAC,GAAG,GAAG,YAAY;AAElD,YAAQ,IAAI,OAAO,MAAO,OAAO,UAAU,UAAU;AACnD,YAAM,eAA6B;AAAA,QACjC,QAAQ;AAAA,QACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,QAAQ,CAAC;AAAA,MACX;AAGA,UAAI;AACF,YAAI,KAAK;AACP,uBAAa,OAAO,MAAM,EAAE,QAAQ,OAAO;AAAA,QAC7C,OAAO;AACL,gBAAM,IAAI,MAAM,mBAAmB;AAAA,QACrC;AAAA,MACF,SAAS,OAAO;AACd,qBAAa,SAAS;AACtB,qBAAa,OAAO,MAAM;AAAA,UACxB,QAAQ;AAAA,UACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACpD;AAAA,MACF;AAGA,iBAAW,SAAS,OAAO,UAAU,CAAC,GAAG;AACvC,YAAI;AACF,gBAAM,SAAS,MAAM,MAAM,MAAM;AACjC,uBAAa,OAAO,MAAM,IAAI,IAAI;AAAA,YAChC,QAAQ,SAAS,SAAS;AAAA,UAC5B;AACA,cAAI,CAAC,QAAQ;AACX,yBAAa,SAAS;AAAA,UACxB;AAAA,QACF,SAAS,OAAO;AACd,uBAAa,SAAS;AACtB,uBAAa,OAAO,MAAM,IAAI,IAAI;AAAA,YAChC,QAAQ;AAAA,YACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,UACpD;AAAA,QACF;AAAA,MACF;AAEA,YAAM,aAAa,aAAa,WAAW,YAAY,MAAM;AAC7D,YAAM,KAAK,UAAU,EAAE,KAAK,YAAY;AAAA,IAC1C,CAAC;AAAA,EACH;AAGA,UAAQ,gBAAgB,CAAC,OAAqB,SAAS,UAAU;AAC/D,QAAI,MAAM,SAAS,oBAAoB;AACrC,YAAM,KAAK,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,SAAS,MAAM;AAAA,QACf,eAAe,QAAQ;AAAA,MACzB,CAAC;AACD;AAAA,IACF;AAEA,QAAI,MAAM,SAAS,oBAAoB;AACrC,YAAM,KAAK,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,SAAS,MAAM;AAAA,QACf,eAAe,QAAQ;AAAA,MACzB,CAAC;AACD;AAAA,IACF;AAGA,UAAM,KAAK,KAAK;AAAA,EAClB,CAAC;AACH;AAEO,IAAM,uBAAuB,GAAG,eAAe;AAAA,EACpD,SAAS;AAAA,EACT,MAAM;AACR,CAAC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@saga-bus/fastify",
3
+ "version": "1.0.0",
4
+ "description": "Fastify integration for saga-bus",
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/deanforan/saga-bus.git",
26
+ "directory": "packages/fastify"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/deanforan/saga-bus/issues"
30
+ },
31
+ "homepage": "https://github.com/deanforan/saga-bus#readme",
32
+ "keywords": [
33
+ "saga",
34
+ "message-bus",
35
+ "fastify",
36
+ "plugin",
37
+ "integration"
38
+ ],
39
+ "dependencies": {
40
+ "fastify-plugin": "^5.0.0",
41
+ "@saga-bus/core": "0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "fastify": "^5.0.0",
45
+ "tsup": "^8.0.0",
46
+ "typescript": "^5.9.2",
47
+ "vitest": "^3.0.0",
48
+ "@repo/eslint-config": "0.0.0",
49
+ "@repo/typescript-config": "0.0.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@saga-bus/core": ">=0.1.0",
53
+ "fastify": ">=4.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsup --watch",
58
+ "lint": "eslint src/",
59
+ "check-types": "tsc --noEmit",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest"
62
+ }
63
+ }