@moreapp/common-nodejs 0.14.0 → 0.15.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.
@@ -5,7 +5,7 @@ class ExpressRequestTracker {
5
5
  track() {
6
6
  return (_req, res, next) => {
7
7
  this.activeRequests++;
8
- res.on("finish", () => {
8
+ res.once("close", () => {
9
9
  this.activeRequests--;
10
10
  });
11
11
  next();
@@ -35,6 +35,10 @@ class TerminationHandler {
35
35
  logger_1.logger.error("Server threw an unrecoverable error, shutting down", err);
36
36
  this.shutdown();
37
37
  });
38
+ process.on("unhandledRejection", (reason) => {
39
+ logger_1.logger.error("Unhandled rejection, shutting down", { reason });
40
+ this.shutdown();
41
+ });
38
42
  }
39
43
  shutdown() {
40
44
  if (this.isShuttingDown) {
@@ -5,12 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const vitest_1 = require("vitest");
7
7
  const node_events_1 = require("node:events");
8
+ const node_http_1 = __importDefault(require("node:http"));
9
+ const express_1 = __importDefault(require("express"));
8
10
  const ExpressRequestTracker_1 = __importDefault(require("../ExpressRequestTracker"));
9
- // Create a minimal Response mock that can emit the 'finish' event
11
+ // Create a minimal Response mock that can emit the 'close' event
10
12
  const createMockResponse = () => {
11
13
  const emitter = new node_events_1.EventEmitter();
12
14
  return Object.assign(emitter, {
13
- emitFinish: () => emitter.emit("finish"),
15
+ emitClose: () => emitter.emit("close"),
14
16
  });
15
17
  };
16
18
  (0, vitest_1.describe)("ExpressRequestTracker", () => {
@@ -23,8 +25,8 @@ const createMockResponse = () => {
23
25
  middleware({}, res, next);
24
26
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(1);
25
27
  (0, vitest_1.expect)(next).toHaveBeenCalledTimes(1);
26
- // Finish should decrement
27
- res.emitFinish();
28
+ // Close should decrement
29
+ res.emitClose();
28
30
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(0);
29
31
  });
30
32
  (0, vitest_1.test)("decrements active requests when response finishes", () => {
@@ -33,7 +35,7 @@ const createMockResponse = () => {
33
35
  const res = createMockResponse();
34
36
  middleware({}, res, (() => { }));
35
37
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(1);
36
- res.emitFinish();
38
+ res.emitClose();
37
39
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(0);
38
40
  });
39
41
  (0, vitest_1.test)("tracks multiple concurrent requests and decrements in order", () => {
@@ -45,10 +47,55 @@ const createMockResponse = () => {
45
47
  middleware({}, res2, (() => { }));
46
48
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(2);
47
49
  // Finish the first request
48
- res1.emitFinish();
50
+ res1.emitClose();
49
51
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(1);
50
52
  // Finish the second request
51
- res2.emitFinish();
53
+ res2.emitClose();
52
54
  (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(0);
53
55
  });
56
+ (0, vitest_1.describe)("with a real HTTP server", () => {
57
+ const servers = [];
58
+ (0, vitest_1.afterEach)(() => {
59
+ servers.splice(0).forEach((server) => {
60
+ server.closeAllConnections();
61
+ server.close();
62
+ });
63
+ });
64
+ const startServer = async (handler) => {
65
+ const tracker = new ExpressRequestTracker_1.default();
66
+ const app = (0, express_1.default)();
67
+ app.use(tracker.track());
68
+ app.get("/", handler);
69
+ const server = app.listen(0);
70
+ servers.push(server);
71
+ await (0, node_events_1.once)(server, "listening");
72
+ const { port } = server.address();
73
+ return { tracker, port };
74
+ };
75
+ (0, vitest_1.test)("decrements once when the response completes", async () => {
76
+ let responseClosed;
77
+ const { tracker, port } = await startServer((_req, res) => {
78
+ responseClosed = (0, node_events_1.once)(res, "close");
79
+ res.send("ok");
80
+ });
81
+ const response = await fetch(`http://localhost:${port}/`);
82
+ await response.text();
83
+ await responseClosed;
84
+ (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(0);
85
+ });
86
+ (0, vitest_1.test)("decrements when the client aborts before the response finishes", async () => {
87
+ let responseClosed;
88
+ const { tracker, port } = await startServer((_req, res) => {
89
+ responseClosed = (0, node_events_1.once)(res, "close");
90
+ res.write("partial");
91
+ });
92
+ const req = node_http_1.default.get({ port, path: "/" });
93
+ const [response] = (await (0, node_events_1.once)(req, "response"));
94
+ await (0, node_events_1.once)(response, "data");
95
+ (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(1);
96
+ req.destroy();
97
+ await responseClosed;
98
+ (0, vitest_1.expect)(tracker.nrOfActiveRequests()).toBe(0);
99
+ });
100
+ });
54
101
  });
@@ -219,6 +219,30 @@ vitest_1.vi.mock("node:timers", () => ({
219
219
  await new Promise((resolve) => setImmediate(resolve));
220
220
  (0, vitest_1.expect)(exitSpy).toHaveBeenCalledWith(0);
221
221
  });
222
+ (0, vitest_1.test)("shuts down on an unhandled rejection", async () => {
223
+ const server = createFakeServer();
224
+ const hs = {
225
+ setReady: vitest_1.vi.fn(),
226
+ stop: vitest_1.vi.fn(() => Promise.resolve()),
227
+ };
228
+ const requestTracker = { nrOfActiveRequests: vitest_1.vi.fn(() => 0) };
229
+ const exitSpy = vitest_1.vi.spyOn(process, "exit").mockImplementation(() => undefined);
230
+ new TerminationHandler_1.default(server, hs, requestTracker);
231
+ // Call the listener directly; emitting on process would also reach vitest's own handler
232
+ const listener = process.listeners("unhandledRejection").at(-1);
233
+ process.off("unhandledRejection", listener);
234
+ const reason = new Error("boom");
235
+ listener(reason, Promise.resolve());
236
+ (0, vitest_1.expect)(vitest_1.vi.mocked(logger_1.logger.error)).toHaveBeenCalledWith("Unhandled rejection, shutting down", {
237
+ reason,
238
+ });
239
+ (0, vitest_1.expect)(hs.setReady).toHaveBeenCalledWith(false);
240
+ // eslint-disable-next-line @typescript-eslint/unbound-method
241
+ (0, vitest_1.expect)(server.close).toHaveBeenCalled();
242
+ intervalCb?.();
243
+ await new Promise((resolve) => setImmediate(resolve));
244
+ (0, vitest_1.expect)(exitSpy).toHaveBeenCalledWith(0);
245
+ });
222
246
  (0, vitest_1.test)("ignores a second shutdown call", () => {
223
247
  const server = createFakeServer();
224
248
  const hs = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moreapp/common-nodejs",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "UNLICENSED",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,46 +30,47 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@opentelemetry/api": "1.9.1",
33
- "@opentelemetry/core": "2.11.0",
34
- "@opentelemetry/exporter-trace-otlp-grpc": "0.222.0",
35
- "@opentelemetry/instrumentation": "0.222.0",
36
- "@opentelemetry/instrumentation-dns": "0.65.0",
37
- "@opentelemetry/instrumentation-express": "0.70.0",
38
- "@opentelemetry/instrumentation-http": "0.222.0",
39
- "@opentelemetry/instrumentation-winston": "0.66.0",
40
- "@opentelemetry/resources": "2.11.0",
41
- "@opentelemetry/sdk-node": "0.222.0",
42
- "@opentelemetry/sdk-trace-base": "2.11.0",
43
- "@opentelemetry/sdk-trace-node": "2.11.0",
33
+ "@opentelemetry/core": "2.12.0",
34
+ "@opentelemetry/exporter-trace-otlp-grpc": "0.223.0",
35
+ "@opentelemetry/instrumentation": "0.223.0",
36
+ "@opentelemetry/instrumentation-dns": "0.66.0",
37
+ "@opentelemetry/instrumentation-express": "0.71.1",
38
+ "@opentelemetry/instrumentation-http": "0.223.0",
39
+ "@opentelemetry/instrumentation-winston": "0.68.1",
40
+ "@opentelemetry/resources": "2.12.0",
41
+ "@opentelemetry/sdk-node": "0.223.0",
42
+ "@opentelemetry/sdk-trace-base": "2.12.0",
43
+ "@opentelemetry/sdk-trace-node": "2.12.0",
44
44
  "@opentelemetry/semantic-conventions": "1.43.0",
45
45
  "axios": "1.20.0",
46
46
  "content-disposition-parser": "1.0.2",
47
- "date-and-time": "4.5.2",
48
- "express": "5.2.1",
47
+ "date-and-time": "4.6.0",
48
+ "express": "5.3.0",
49
49
  "lodash": "4.18.1",
50
50
  "prom-client": "15.1.3",
51
51
  "winston": "3.19.0",
52
- "zod": "4.6.5"
52
+ "zod": "4.6.7"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/express": "5.0.6",
56
- "@types/lodash": "4.17.25",
57
- "@types/node": "24.13.4",
58
- "@typescript-eslint/eslint-plugin": "8.70.0",
59
- "@typescript-eslint/parser": "8.70.0",
60
- "@vitest/coverage-v8": "4.1.11",
61
- "@vitest/ui": "4.1.11",
56
+ "@types/lodash": "4.17.26",
57
+ "@types/node": "26.6.5",
58
+ "@typescript-eslint/eslint-plugin": "8.72.0",
59
+ "@typescript-eslint/parser": "8.72.0",
60
+ "@vitest/coverage-v8": "5.0.5",
61
+ "@vitest/ui": "5.0.5",
62
62
  "eslint": "9.39.5",
63
63
  "eslint-config-prettier": "10.1.8",
64
- "eslint-plugin-prettier": "5.5.6",
64
+ "eslint-plugin-prettier": "5.5.7",
65
65
  "husky": "9.1.7",
66
- "lint-staged": "17.5.1",
67
- "nock": "14.0.17",
68
- "prettier": "3.9.6",
66
+ "lint-staged": "17.6.0",
67
+ "nock": "15.0.4",
68
+ "prettier": "3.9.10",
69
69
  "ts-node": "10.9.2",
70
70
  "typescript": "6.0.3",
71
- "typescript-eslint": "8.70.0",
72
- "vitest": "4.1.11",
71
+ "typescript-eslint": "8.72.0",
72
+ "vite": "8.3.4",
73
+ "vitest": "5.0.5",
73
74
  "vitest-mock-extended": "5.1.1"
74
75
  },
75
76
  "prettier": {