@saga-bus/express 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 +21 -0
- package/README.md +273 -0
- package/dist/index.cjs +176 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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,273 @@
|
|
|
1
|
+
# @saga-bus/express
|
|
2
|
+
|
|
3
|
+
Express.js integration for saga-bus with middleware, health checks, and graceful shutdown.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @saga-bus/express express
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @saga-bus/express express
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Bus Middleware**: Attaches bus instance to `req.bus`
|
|
16
|
+
- **Correlation ID**: Extract or generate correlation IDs from headers
|
|
17
|
+
- **Health Checks**: Ready-to-use health and readiness endpoints
|
|
18
|
+
- **Error Handler**: Saga-specific error handling middleware
|
|
19
|
+
- **Graceful Shutdown**: Clean shutdown with bus draining
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import express from "express";
|
|
25
|
+
import { createBus } from "@saga-bus/core";
|
|
26
|
+
import {
|
|
27
|
+
sagaBusMiddleware,
|
|
28
|
+
sagaErrorHandler,
|
|
29
|
+
createHealthRouter,
|
|
30
|
+
setupGracefulShutdown,
|
|
31
|
+
} from "@saga-bus/express";
|
|
32
|
+
|
|
33
|
+
const bus = createBus({ /* config */ });
|
|
34
|
+
await bus.start();
|
|
35
|
+
|
|
36
|
+
const app = express();
|
|
37
|
+
|
|
38
|
+
// Attach bus to requests
|
|
39
|
+
app.use(sagaBusMiddleware({ bus }));
|
|
40
|
+
|
|
41
|
+
// Health check endpoint
|
|
42
|
+
app.use(createHealthRouter({ bus }));
|
|
43
|
+
|
|
44
|
+
// Your routes
|
|
45
|
+
app.post("/orders", async (req, res) => {
|
|
46
|
+
await req.bus.publish({
|
|
47
|
+
type: "CreateOrder",
|
|
48
|
+
payload: req.body,
|
|
49
|
+
});
|
|
50
|
+
res.json({ correlationId: req.correlationId });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Error handler (must be last)
|
|
54
|
+
app.use(sagaErrorHandler());
|
|
55
|
+
|
|
56
|
+
const server = app.listen(3000);
|
|
57
|
+
|
|
58
|
+
// Graceful shutdown
|
|
59
|
+
setupGracefulShutdown(server, { bus });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### sagaBusMiddleware(options)
|
|
65
|
+
|
|
66
|
+
Creates middleware that attaches the bus instance to requests.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
interface SagaBusExpressOptions {
|
|
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
|
+
Example:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
app.use(sagaBusMiddleware({
|
|
88
|
+
bus,
|
|
89
|
+
correlationIdHeader: "x-request-id",
|
|
90
|
+
correlationIdGenerator: () => `req-${Date.now()}`,
|
|
91
|
+
}));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### sagaErrorHandler()
|
|
95
|
+
|
|
96
|
+
Error handler middleware for saga-related errors.
|
|
97
|
+
|
|
98
|
+
- **SagaTimeoutError**: Returns 408 Request Timeout
|
|
99
|
+
- **ConcurrencyError**: Returns 409 Conflict
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
app.use(sagaErrorHandler());
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### createHealthRouter(options)
|
|
106
|
+
|
|
107
|
+
Creates a health check router.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface HealthCheckOptions {
|
|
111
|
+
/** The bus instance to check */
|
|
112
|
+
bus: Bus;
|
|
113
|
+
|
|
114
|
+
/** Path for health endpoint (default: "/health") */
|
|
115
|
+
path?: string;
|
|
116
|
+
|
|
117
|
+
/** Additional health checks */
|
|
118
|
+
checks?: Array<{
|
|
119
|
+
name: string;
|
|
120
|
+
check: () => Promise<boolean>;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
app.use(createHealthRouter({
|
|
129
|
+
bus,
|
|
130
|
+
path: "/health",
|
|
131
|
+
checks: [
|
|
132
|
+
{
|
|
133
|
+
name: "database",
|
|
134
|
+
check: async () => {
|
|
135
|
+
await pool.query("SELECT 1");
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Response format:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"status": "healthy",
|
|
148
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
149
|
+
"checks": {
|
|
150
|
+
"bus": { "status": "pass" },
|
|
151
|
+
"database": { "status": "pass" }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### createReadinessRouter(options)
|
|
157
|
+
|
|
158
|
+
Same as `createHealthRouter` but defaults to `/ready` path.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
app.use(createReadinessRouter({ bus }));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### setupGracefulShutdown(server, options)
|
|
165
|
+
|
|
166
|
+
Sets up graceful shutdown with bus draining.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface GracefulShutdownOptions {
|
|
170
|
+
/** The bus instance to drain */
|
|
171
|
+
bus: Bus;
|
|
172
|
+
|
|
173
|
+
/** Timeout for graceful shutdown in ms (default: 30000) */
|
|
174
|
+
timeoutMs?: number;
|
|
175
|
+
|
|
176
|
+
/** Callback before shutdown starts */
|
|
177
|
+
onShutdownStart?: () => void | Promise<void>;
|
|
178
|
+
|
|
179
|
+
/** Callback after shutdown completes */
|
|
180
|
+
onShutdownComplete?: () => void | Promise<void>;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
setupGracefulShutdown(server, {
|
|
188
|
+
bus,
|
|
189
|
+
timeoutMs: 60000,
|
|
190
|
+
onShutdownStart: async () => {
|
|
191
|
+
console.log("Stopping background jobs...");
|
|
192
|
+
},
|
|
193
|
+
onShutdownComplete: async () => {
|
|
194
|
+
await pool.end();
|
|
195
|
+
console.log("Cleanup complete");
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## TypeScript Support
|
|
201
|
+
|
|
202
|
+
The package extends Express types to add `bus` and `correlationId` to requests:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// In your route handlers
|
|
206
|
+
app.post("/orders", async (req, res) => {
|
|
207
|
+
// req.bus is typed as Bus
|
|
208
|
+
await req.bus.publish(message);
|
|
209
|
+
|
|
210
|
+
// req.correlationId is typed as string | undefined
|
|
211
|
+
console.log(`Processing ${req.correlationId}`);
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Example: Complete Application
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import express from "express";
|
|
219
|
+
import { createBus, InMemoryTransport, InMemorySagaStore } from "@saga-bus/core";
|
|
220
|
+
import {
|
|
221
|
+
sagaBusMiddleware,
|
|
222
|
+
sagaErrorHandler,
|
|
223
|
+
createHealthRouter,
|
|
224
|
+
createReadinessRouter,
|
|
225
|
+
setupGracefulShutdown,
|
|
226
|
+
} from "@saga-bus/express";
|
|
227
|
+
|
|
228
|
+
// Create bus
|
|
229
|
+
const bus = createBus({
|
|
230
|
+
transport: new InMemoryTransport(),
|
|
231
|
+
store: new InMemorySagaStore(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await bus.start();
|
|
235
|
+
|
|
236
|
+
// Create Express app
|
|
237
|
+
const app = express();
|
|
238
|
+
app.use(express.json());
|
|
239
|
+
|
|
240
|
+
// Saga bus middleware
|
|
241
|
+
app.use(sagaBusMiddleware({ bus }));
|
|
242
|
+
|
|
243
|
+
// Health endpoints
|
|
244
|
+
app.use(createHealthRouter({ bus }));
|
|
245
|
+
app.use(createReadinessRouter({ bus }));
|
|
246
|
+
|
|
247
|
+
// Routes
|
|
248
|
+
app.post("/messages", async (req, res) => {
|
|
249
|
+
await req.bus.publish({
|
|
250
|
+
type: req.body.type,
|
|
251
|
+
payload: req.body.payload,
|
|
252
|
+
});
|
|
253
|
+
res.json({ success: true, correlationId: req.correlationId });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Error handler (must be last middleware)
|
|
257
|
+
app.use(sagaErrorHandler());
|
|
258
|
+
|
|
259
|
+
// Start server
|
|
260
|
+
const server = app.listen(3000, () => {
|
|
261
|
+
console.log("Server running on port 3000");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Graceful shutdown
|
|
265
|
+
setupGracefulShutdown(server, {
|
|
266
|
+
bus,
|
|
267
|
+
timeoutMs: 30000,
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
createHealthRouter: () => createHealthRouter,
|
|
24
|
+
createReadinessRouter: () => createReadinessRouter,
|
|
25
|
+
sagaBusMiddleware: () => sagaBusMiddleware,
|
|
26
|
+
sagaErrorHandler: () => sagaErrorHandler,
|
|
27
|
+
setupGracefulShutdown: () => setupGracefulShutdown
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/middleware.ts
|
|
32
|
+
var import_crypto = require("crypto");
|
|
33
|
+
function sagaBusMiddleware(options) {
|
|
34
|
+
const {
|
|
35
|
+
bus,
|
|
36
|
+
correlationIdHeader = "x-correlation-id",
|
|
37
|
+
generateCorrelationId = true,
|
|
38
|
+
correlationIdGenerator = import_crypto.randomUUID
|
|
39
|
+
} = options;
|
|
40
|
+
return (req, res, next) => {
|
|
41
|
+
req.bus = bus;
|
|
42
|
+
let correlationId = req.headers[correlationIdHeader.toLowerCase()];
|
|
43
|
+
if (!correlationId && generateCorrelationId) {
|
|
44
|
+
correlationId = correlationIdGenerator();
|
|
45
|
+
}
|
|
46
|
+
if (correlationId) {
|
|
47
|
+
req.correlationId = correlationId;
|
|
48
|
+
res.setHeader(correlationIdHeader, correlationId);
|
|
49
|
+
}
|
|
50
|
+
next();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function sagaErrorHandler() {
|
|
54
|
+
return (err, req, res, next) => {
|
|
55
|
+
if (err.name === "SagaTimeoutError") {
|
|
56
|
+
res.status(408).json({
|
|
57
|
+
error: "Saga Timeout",
|
|
58
|
+
message: err.message,
|
|
59
|
+
correlationId: req.correlationId
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (err.name === "ConcurrencyError") {
|
|
64
|
+
res.status(409).json({
|
|
65
|
+
error: "Concurrency Conflict",
|
|
66
|
+
message: err.message,
|
|
67
|
+
correlationId: req.correlationId
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
next(err);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/health.ts
|
|
76
|
+
var import_express = require("express");
|
|
77
|
+
function createHealthRouter(options) {
|
|
78
|
+
const {
|
|
79
|
+
bus,
|
|
80
|
+
path = "/health",
|
|
81
|
+
checks = []
|
|
82
|
+
} = options;
|
|
83
|
+
const router = (0, import_express.Router)();
|
|
84
|
+
router.get(path, async (_req, res) => {
|
|
85
|
+
const healthStatus = {
|
|
86
|
+
status: "healthy",
|
|
87
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
+
checks: {}
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
if (bus) {
|
|
92
|
+
healthStatus.checks.bus = {
|
|
93
|
+
status: "pass"
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error("Bus not available");
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
healthStatus.status = "unhealthy";
|
|
100
|
+
healthStatus.checks.bus = {
|
|
101
|
+
status: "fail",
|
|
102
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
for (const check of checks) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await check.check();
|
|
108
|
+
healthStatus.checks[check.name] = {
|
|
109
|
+
status: result ? "pass" : "fail"
|
|
110
|
+
};
|
|
111
|
+
if (!result) {
|
|
112
|
+
healthStatus.status = "unhealthy";
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
healthStatus.status = "unhealthy";
|
|
116
|
+
healthStatus.checks[check.name] = {
|
|
117
|
+
status: "fail",
|
|
118
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const statusCode = healthStatus.status === "healthy" ? 200 : 503;
|
|
123
|
+
res.status(statusCode).json(healthStatus);
|
|
124
|
+
});
|
|
125
|
+
return router;
|
|
126
|
+
}
|
|
127
|
+
function createReadinessRouter(options) {
|
|
128
|
+
return createHealthRouter({
|
|
129
|
+
...options,
|
|
130
|
+
path: options.path ?? "/ready"
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/index.ts
|
|
135
|
+
function setupGracefulShutdown(server, options) {
|
|
136
|
+
const {
|
|
137
|
+
bus,
|
|
138
|
+
timeoutMs = 3e4,
|
|
139
|
+
onShutdownStart,
|
|
140
|
+
onShutdownComplete
|
|
141
|
+
} = options;
|
|
142
|
+
const shutdown = async (signal) => {
|
|
143
|
+
console.log(`Received ${signal}, starting graceful shutdown...`);
|
|
144
|
+
if (onShutdownStart) {
|
|
145
|
+
await onShutdownStart();
|
|
146
|
+
}
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
console.error("Graceful shutdown timeout, forcing exit");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}, timeoutMs);
|
|
151
|
+
try {
|
|
152
|
+
server.close();
|
|
153
|
+
await bus.stop();
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
if (onShutdownComplete) {
|
|
156
|
+
await onShutdownComplete();
|
|
157
|
+
}
|
|
158
|
+
console.log("Graceful shutdown complete");
|
|
159
|
+
process.exit(0);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error("Error during graceful shutdown:", error);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
166
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
167
|
+
}
|
|
168
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
169
|
+
0 && (module.exports = {
|
|
170
|
+
createHealthRouter,
|
|
171
|
+
createReadinessRouter,
|
|
172
|
+
sagaBusMiddleware,
|
|
173
|
+
sagaErrorHandler,
|
|
174
|
+
setupGracefulShutdown
|
|
175
|
+
});
|
|
176
|
+
//# 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 { createHealthRouter, createReadinessRouter } from \"./health.js\";\nexport type {\n SagaBusExpressOptions,\n HealthCheckOptions,\n GracefulShutdownOptions,\n} from \"./types.js\";\nexport type { HealthStatus } from \"./health.js\";\n\n// Re-export graceful shutdown helper\nimport type { Server } from \"http\";\nimport type { GracefulShutdownOptions } from \"./types.js\";\n\n/**\n * Sets up graceful shutdown for Express server with bus drain.\n */\nexport function setupGracefulShutdown(\n server: Server,\n options: GracefulShutdownOptions\n): void {\n const {\n bus,\n timeoutMs = 30000,\n onShutdownStart,\n onShutdownComplete,\n } = options;\n\n const shutdown = async (signal: string) => {\n console.log(`Received ${signal}, starting graceful shutdown...`);\n\n if (onShutdownStart) {\n await onShutdownStart();\n }\n\n // Set shutdown timeout\n const timeout = setTimeout(() => {\n console.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, timeoutMs);\n\n try {\n // Stop accepting new connections\n server.close();\n\n // Stop the bus (drains workers)\n await bus.stop();\n\n clearTimeout(timeout);\n\n if (onShutdownComplete) {\n await onShutdownComplete();\n }\n\n console.log(\"Graceful shutdown complete\");\n process.exit(0);\n } catch (error) {\n console.error(\"Error during graceful shutdown:\", error);\n process.exit(1);\n }\n };\n\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n}\n","import type { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from \"express\";\nimport { randomUUID } from \"crypto\";\nimport type { SagaBusExpressOptions } from \"./types.js\";\n\n/**\n * Creates middleware that attaches the bus instance to requests.\n */\nexport function sagaBusMiddleware(options: SagaBusExpressOptions): RequestHandler {\n const {\n bus,\n correlationIdHeader = \"x-correlation-id\",\n generateCorrelationId = true,\n correlationIdGenerator = randomUUID,\n } = options;\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Attach bus to request\n req.bus = bus;\n\n // Extract or generate correlation ID\n let correlationId = req.headers[correlationIdHeader.toLowerCase()] as string | undefined;\n\n if (!correlationId && generateCorrelationId) {\n correlationId = correlationIdGenerator();\n }\n\n if (correlationId) {\n req.correlationId = correlationId;\n // Also set on response for tracing\n res.setHeader(correlationIdHeader, correlationId);\n }\n\n next();\n };\n}\n\n/**\n * Error handler middleware for saga-related errors.\n */\nexport function sagaErrorHandler(): ErrorRequestHandler {\n return (err: Error, req: Request, res: Response, next: NextFunction) => {\n // Check if it's a saga-related error\n if (err.name === \"SagaTimeoutError\") {\n res.status(408).json({\n error: \"Saga Timeout\",\n message: err.message,\n correlationId: req.correlationId,\n });\n return;\n }\n\n if (err.name === \"ConcurrencyError\") {\n res.status(409).json({\n error: \"Concurrency Conflict\",\n message: err.message,\n correlationId: req.correlationId,\n });\n return;\n }\n\n // Pass to default error handler\n next(err);\n };\n}\n","import type { Request, Response, Router } from \"express\";\nimport { Router as createRouter } from \"express\";\nimport type { HealthCheckOptions } from \"./types.js\";\n\nexport interface HealthStatus {\n status: \"healthy\" | \"unhealthy\";\n timestamp: string;\n checks: Record<string, {\n status: \"pass\" | \"fail\";\n message?: string;\n }>;\n}\n\n/**\n * Creates a health check router for the bus.\n */\nexport function createHealthRouter(options: HealthCheckOptions): Router {\n const {\n bus,\n path = \"/health\",\n checks = [],\n } = options;\n\n const router = createRouter();\n\n router.get(path, async (_req: Request, res: Response) => {\n const healthStatus: HealthStatus = {\n status: \"healthy\",\n timestamp: new Date().toISOString(),\n checks: {},\n };\n\n // Check bus status\n try {\n // Simple check - bus exists and is accessible\n if (bus) {\n healthStatus.checks.bus = {\n status: \"pass\",\n };\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 res.status(statusCode).json(healthStatus);\n });\n\n return router;\n}\n\n/**\n * Creates a readiness check router.\n */\nexport function createReadinessRouter(options: HealthCheckOptions): Router {\n return createHealthRouter({\n ...options,\n path: options.path ?? \"/ready\",\n });\n}\n"],"mappings":"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","names":["createRouter"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { RequestHandler, ErrorRequestHandler, Router } from 'express';
|
|
2
|
+
import { Bus } from '@saga-bus/core';
|
|
3
|
+
import { Server } from 'http';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Request {
|
|
8
|
+
bus?: Bus;
|
|
9
|
+
correlationId?: string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
interface SagaBusExpressOptions {
|
|
14
|
+
/** The bus instance to attach */
|
|
15
|
+
bus: Bus;
|
|
16
|
+
/** Header name for correlation ID */
|
|
17
|
+
correlationIdHeader?: string;
|
|
18
|
+
/** Whether to generate correlation ID if not present */
|
|
19
|
+
generateCorrelationId?: boolean;
|
|
20
|
+
/** Custom correlation ID generator */
|
|
21
|
+
correlationIdGenerator?: () => string;
|
|
22
|
+
}
|
|
23
|
+
interface HealthCheckOptions {
|
|
24
|
+
/** The bus instance to check */
|
|
25
|
+
bus: Bus;
|
|
26
|
+
/** Path for health endpoint */
|
|
27
|
+
path?: string;
|
|
28
|
+
/** Additional health checks */
|
|
29
|
+
checks?: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
check: () => Promise<boolean>;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
interface GracefulShutdownOptions {
|
|
35
|
+
/** The bus instance to drain */
|
|
36
|
+
bus: Bus;
|
|
37
|
+
/** Timeout for graceful shutdown in ms */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** Callback before shutdown starts */
|
|
40
|
+
onShutdownStart?: () => void | Promise<void>;
|
|
41
|
+
/** Callback after shutdown completes */
|
|
42
|
+
onShutdownComplete?: () => void | Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates middleware that attaches the bus instance to requests.
|
|
47
|
+
*/
|
|
48
|
+
declare function sagaBusMiddleware(options: SagaBusExpressOptions): RequestHandler;
|
|
49
|
+
/**
|
|
50
|
+
* Error handler middleware for saga-related errors.
|
|
51
|
+
*/
|
|
52
|
+
declare function sagaErrorHandler(): ErrorRequestHandler;
|
|
53
|
+
|
|
54
|
+
interface HealthStatus {
|
|
55
|
+
status: "healthy" | "unhealthy";
|
|
56
|
+
timestamp: string;
|
|
57
|
+
checks: Record<string, {
|
|
58
|
+
status: "pass" | "fail";
|
|
59
|
+
message?: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a health check router for the bus.
|
|
64
|
+
*/
|
|
65
|
+
declare function createHealthRouter(options: HealthCheckOptions): Router;
|
|
66
|
+
/**
|
|
67
|
+
* Creates a readiness check router.
|
|
68
|
+
*/
|
|
69
|
+
declare function createReadinessRouter(options: HealthCheckOptions): Router;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sets up graceful shutdown for Express server with bus drain.
|
|
73
|
+
*/
|
|
74
|
+
declare function setupGracefulShutdown(server: Server, options: GracefulShutdownOptions): void;
|
|
75
|
+
|
|
76
|
+
export { type GracefulShutdownOptions, type HealthCheckOptions, type HealthStatus, type SagaBusExpressOptions, createHealthRouter, createReadinessRouter, sagaBusMiddleware, sagaErrorHandler, setupGracefulShutdown };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { RequestHandler, ErrorRequestHandler, Router } from 'express';
|
|
2
|
+
import { Bus } from '@saga-bus/core';
|
|
3
|
+
import { Server } from 'http';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Request {
|
|
8
|
+
bus?: Bus;
|
|
9
|
+
correlationId?: string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
interface SagaBusExpressOptions {
|
|
14
|
+
/** The bus instance to attach */
|
|
15
|
+
bus: Bus;
|
|
16
|
+
/** Header name for correlation ID */
|
|
17
|
+
correlationIdHeader?: string;
|
|
18
|
+
/** Whether to generate correlation ID if not present */
|
|
19
|
+
generateCorrelationId?: boolean;
|
|
20
|
+
/** Custom correlation ID generator */
|
|
21
|
+
correlationIdGenerator?: () => string;
|
|
22
|
+
}
|
|
23
|
+
interface HealthCheckOptions {
|
|
24
|
+
/** The bus instance to check */
|
|
25
|
+
bus: Bus;
|
|
26
|
+
/** Path for health endpoint */
|
|
27
|
+
path?: string;
|
|
28
|
+
/** Additional health checks */
|
|
29
|
+
checks?: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
check: () => Promise<boolean>;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
interface GracefulShutdownOptions {
|
|
35
|
+
/** The bus instance to drain */
|
|
36
|
+
bus: Bus;
|
|
37
|
+
/** Timeout for graceful shutdown in ms */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** Callback before shutdown starts */
|
|
40
|
+
onShutdownStart?: () => void | Promise<void>;
|
|
41
|
+
/** Callback after shutdown completes */
|
|
42
|
+
onShutdownComplete?: () => void | Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates middleware that attaches the bus instance to requests.
|
|
47
|
+
*/
|
|
48
|
+
declare function sagaBusMiddleware(options: SagaBusExpressOptions): RequestHandler;
|
|
49
|
+
/**
|
|
50
|
+
* Error handler middleware for saga-related errors.
|
|
51
|
+
*/
|
|
52
|
+
declare function sagaErrorHandler(): ErrorRequestHandler;
|
|
53
|
+
|
|
54
|
+
interface HealthStatus {
|
|
55
|
+
status: "healthy" | "unhealthy";
|
|
56
|
+
timestamp: string;
|
|
57
|
+
checks: Record<string, {
|
|
58
|
+
status: "pass" | "fail";
|
|
59
|
+
message?: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a health check router for the bus.
|
|
64
|
+
*/
|
|
65
|
+
declare function createHealthRouter(options: HealthCheckOptions): Router;
|
|
66
|
+
/**
|
|
67
|
+
* Creates a readiness check router.
|
|
68
|
+
*/
|
|
69
|
+
declare function createReadinessRouter(options: HealthCheckOptions): Router;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sets up graceful shutdown for Express server with bus drain.
|
|
73
|
+
*/
|
|
74
|
+
declare function setupGracefulShutdown(server: Server, options: GracefulShutdownOptions): void;
|
|
75
|
+
|
|
76
|
+
export { type GracefulShutdownOptions, type HealthCheckOptions, type HealthStatus, type SagaBusExpressOptions, createHealthRouter, createReadinessRouter, sagaBusMiddleware, sagaErrorHandler, setupGracefulShutdown };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
function sagaBusMiddleware(options) {
|
|
4
|
+
const {
|
|
5
|
+
bus,
|
|
6
|
+
correlationIdHeader = "x-correlation-id",
|
|
7
|
+
generateCorrelationId = true,
|
|
8
|
+
correlationIdGenerator = randomUUID
|
|
9
|
+
} = options;
|
|
10
|
+
return (req, res, next) => {
|
|
11
|
+
req.bus = bus;
|
|
12
|
+
let correlationId = req.headers[correlationIdHeader.toLowerCase()];
|
|
13
|
+
if (!correlationId && generateCorrelationId) {
|
|
14
|
+
correlationId = correlationIdGenerator();
|
|
15
|
+
}
|
|
16
|
+
if (correlationId) {
|
|
17
|
+
req.correlationId = correlationId;
|
|
18
|
+
res.setHeader(correlationIdHeader, correlationId);
|
|
19
|
+
}
|
|
20
|
+
next();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function sagaErrorHandler() {
|
|
24
|
+
return (err, req, res, next) => {
|
|
25
|
+
if (err.name === "SagaTimeoutError") {
|
|
26
|
+
res.status(408).json({
|
|
27
|
+
error: "Saga Timeout",
|
|
28
|
+
message: err.message,
|
|
29
|
+
correlationId: req.correlationId
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (err.name === "ConcurrencyError") {
|
|
34
|
+
res.status(409).json({
|
|
35
|
+
error: "Concurrency Conflict",
|
|
36
|
+
message: err.message,
|
|
37
|
+
correlationId: req.correlationId
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
next(err);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/health.ts
|
|
46
|
+
import { Router as createRouter } from "express";
|
|
47
|
+
function createHealthRouter(options) {
|
|
48
|
+
const {
|
|
49
|
+
bus,
|
|
50
|
+
path = "/health",
|
|
51
|
+
checks = []
|
|
52
|
+
} = options;
|
|
53
|
+
const router = createRouter();
|
|
54
|
+
router.get(path, async (_req, res) => {
|
|
55
|
+
const healthStatus = {
|
|
56
|
+
status: "healthy",
|
|
57
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
58
|
+
checks: {}
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
if (bus) {
|
|
62
|
+
healthStatus.checks.bus = {
|
|
63
|
+
status: "pass"
|
|
64
|
+
};
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error("Bus not available");
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
healthStatus.status = "unhealthy";
|
|
70
|
+
healthStatus.checks.bus = {
|
|
71
|
+
status: "fail",
|
|
72
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
for (const check of checks) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await check.check();
|
|
78
|
+
healthStatus.checks[check.name] = {
|
|
79
|
+
status: result ? "pass" : "fail"
|
|
80
|
+
};
|
|
81
|
+
if (!result) {
|
|
82
|
+
healthStatus.status = "unhealthy";
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
healthStatus.status = "unhealthy";
|
|
86
|
+
healthStatus.checks[check.name] = {
|
|
87
|
+
status: "fail",
|
|
88
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const statusCode = healthStatus.status === "healthy" ? 200 : 503;
|
|
93
|
+
res.status(statusCode).json(healthStatus);
|
|
94
|
+
});
|
|
95
|
+
return router;
|
|
96
|
+
}
|
|
97
|
+
function createReadinessRouter(options) {
|
|
98
|
+
return createHealthRouter({
|
|
99
|
+
...options,
|
|
100
|
+
path: options.path ?? "/ready"
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/index.ts
|
|
105
|
+
function setupGracefulShutdown(server, options) {
|
|
106
|
+
const {
|
|
107
|
+
bus,
|
|
108
|
+
timeoutMs = 3e4,
|
|
109
|
+
onShutdownStart,
|
|
110
|
+
onShutdownComplete
|
|
111
|
+
} = options;
|
|
112
|
+
const shutdown = async (signal) => {
|
|
113
|
+
console.log(`Received ${signal}, starting graceful shutdown...`);
|
|
114
|
+
if (onShutdownStart) {
|
|
115
|
+
await onShutdownStart();
|
|
116
|
+
}
|
|
117
|
+
const timeout = setTimeout(() => {
|
|
118
|
+
console.error("Graceful shutdown timeout, forcing exit");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
try {
|
|
122
|
+
server.close();
|
|
123
|
+
await bus.stop();
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
if (onShutdownComplete) {
|
|
126
|
+
await onShutdownComplete();
|
|
127
|
+
}
|
|
128
|
+
console.log("Graceful shutdown complete");
|
|
129
|
+
process.exit(0);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error("Error during graceful shutdown:", error);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
136
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
createHealthRouter,
|
|
140
|
+
createReadinessRouter,
|
|
141
|
+
sagaBusMiddleware,
|
|
142
|
+
sagaErrorHandler,
|
|
143
|
+
setupGracefulShutdown
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/health.ts","../src/index.ts"],"sourcesContent":["import type { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from \"express\";\nimport { randomUUID } from \"crypto\";\nimport type { SagaBusExpressOptions } from \"./types.js\";\n\n/**\n * Creates middleware that attaches the bus instance to requests.\n */\nexport function sagaBusMiddleware(options: SagaBusExpressOptions): RequestHandler {\n const {\n bus,\n correlationIdHeader = \"x-correlation-id\",\n generateCorrelationId = true,\n correlationIdGenerator = randomUUID,\n } = options;\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Attach bus to request\n req.bus = bus;\n\n // Extract or generate correlation ID\n let correlationId = req.headers[correlationIdHeader.toLowerCase()] as string | undefined;\n\n if (!correlationId && generateCorrelationId) {\n correlationId = correlationIdGenerator();\n }\n\n if (correlationId) {\n req.correlationId = correlationId;\n // Also set on response for tracing\n res.setHeader(correlationIdHeader, correlationId);\n }\n\n next();\n };\n}\n\n/**\n * Error handler middleware for saga-related errors.\n */\nexport function sagaErrorHandler(): ErrorRequestHandler {\n return (err: Error, req: Request, res: Response, next: NextFunction) => {\n // Check if it's a saga-related error\n if (err.name === \"SagaTimeoutError\") {\n res.status(408).json({\n error: \"Saga Timeout\",\n message: err.message,\n correlationId: req.correlationId,\n });\n return;\n }\n\n if (err.name === \"ConcurrencyError\") {\n res.status(409).json({\n error: \"Concurrency Conflict\",\n message: err.message,\n correlationId: req.correlationId,\n });\n return;\n }\n\n // Pass to default error handler\n next(err);\n };\n}\n","import type { Request, Response, Router } from \"express\";\nimport { Router as createRouter } from \"express\";\nimport type { HealthCheckOptions } from \"./types.js\";\n\nexport interface HealthStatus {\n status: \"healthy\" | \"unhealthy\";\n timestamp: string;\n checks: Record<string, {\n status: \"pass\" | \"fail\";\n message?: string;\n }>;\n}\n\n/**\n * Creates a health check router for the bus.\n */\nexport function createHealthRouter(options: HealthCheckOptions): Router {\n const {\n bus,\n path = \"/health\",\n checks = [],\n } = options;\n\n const router = createRouter();\n\n router.get(path, async (_req: Request, res: Response) => {\n const healthStatus: HealthStatus = {\n status: \"healthy\",\n timestamp: new Date().toISOString(),\n checks: {},\n };\n\n // Check bus status\n try {\n // Simple check - bus exists and is accessible\n if (bus) {\n healthStatus.checks.bus = {\n status: \"pass\",\n };\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 res.status(statusCode).json(healthStatus);\n });\n\n return router;\n}\n\n/**\n * Creates a readiness check router.\n */\nexport function createReadinessRouter(options: HealthCheckOptions): Router {\n return createHealthRouter({\n ...options,\n path: options.path ?? \"/ready\",\n });\n}\n","export { sagaBusMiddleware, sagaErrorHandler } from \"./middleware.js\";\nexport { createHealthRouter, createReadinessRouter } from \"./health.js\";\nexport type {\n SagaBusExpressOptions,\n HealthCheckOptions,\n GracefulShutdownOptions,\n} from \"./types.js\";\nexport type { HealthStatus } from \"./health.js\";\n\n// Re-export graceful shutdown helper\nimport type { Server } from \"http\";\nimport type { GracefulShutdownOptions } from \"./types.js\";\n\n/**\n * Sets up graceful shutdown for Express server with bus drain.\n */\nexport function setupGracefulShutdown(\n server: Server,\n options: GracefulShutdownOptions\n): void {\n const {\n bus,\n timeoutMs = 30000,\n onShutdownStart,\n onShutdownComplete,\n } = options;\n\n const shutdown = async (signal: string) => {\n console.log(`Received ${signal}, starting graceful shutdown...`);\n\n if (onShutdownStart) {\n await onShutdownStart();\n }\n\n // Set shutdown timeout\n const timeout = setTimeout(() => {\n console.error(\"Graceful shutdown timeout, forcing exit\");\n process.exit(1);\n }, timeoutMs);\n\n try {\n // Stop accepting new connections\n server.close();\n\n // Stop the bus (drains workers)\n await bus.stop();\n\n clearTimeout(timeout);\n\n if (onShutdownComplete) {\n await onShutdownComplete();\n }\n\n console.log(\"Graceful shutdown complete\");\n process.exit(0);\n } catch (error) {\n console.error(\"Error during graceful shutdown:\", error);\n process.exit(1);\n }\n };\n\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saga-bus/express",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Express.js 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/express"
|
|
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
|
+
"express",
|
|
36
|
+
"middleware",
|
|
37
|
+
"integration"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@saga-bus/core": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/express": "^4.17.21",
|
|
44
|
+
"express": "^4.21.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
|
+
"express": ">=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
|
+
}
|