@notchjs/core 0.2.3 → 0.4.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.
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import * as http from 'node:http';
4
5
  import * as https from 'node:https';
5
6
  import type { HttpAdapter } from '@notchjs/types';
@@ -10,6 +11,8 @@ export declare class Application {
10
11
  private readonly adapter;
11
12
  private readonly hooks;
12
13
  private readonly _environment;
14
+ private readonly activeSignals;
15
+ private cleanupRef?;
13
16
  private isInitialized;
14
17
  private isListening;
15
18
  constructor(adapter: HttpAdapter, hooks: HookCollector, environment: ApplicationEnvironment);
@@ -23,6 +26,9 @@ export declare class Application {
23
26
  listen(port: string | number, hostname: string, callback?: () => void): Promise<any>;
24
27
  close(signal?: string): Promise<void>;
25
28
  getUrl(): Promise<string>;
29
+ protected shutdownOnSignal(signals?: (NodeJS.Signals | string)[]): void;
30
+ protected listenToSignals(signals: NodeJS.Signals[]): void;
31
+ protected unsubscribeFromSignals(): void;
26
32
  protected dispose(): Promise<void>;
27
33
  private formatAddress;
28
34
  private getProtocol;
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Application = void 0;
4
4
  const node_os_1 = require("node:os");
5
5
  const notions_1 = require("@hemjs/notions");
6
+ const iterare_1 = require("iterare");
6
7
  class Application {
7
8
  constructor(adapter, hooks, environment) {
9
+ this.activeSignals = new Array();
8
10
  this.isInitialized = false;
9
11
  this.isListening = false;
10
12
  this.adapter = adapter;
@@ -20,11 +22,11 @@ class Application {
20
22
  return this;
21
23
  }
22
24
  const startAt = process.hrtime.bigint();
23
- await this.hooks.addStartupHook();
25
+ await this.hooks.onStartup();
24
26
  this.isInitialized = true;
25
27
  const finishedAt = process.hrtime.bigint();
26
- const elapsed = (Number(finishedAt - startAt) / 1e9).toFixed(3);
27
- this.environment.log?.info(`Application started in ${elapsed} seconds`);
28
+ const elapsedTime = (Number(finishedAt - startAt) / 1e9).toFixed(3);
29
+ this.environment.log?.info(`Application started in ${elapsedTime} seconds`);
28
30
  return this;
29
31
  }
30
32
  getHttpAdapter() {
@@ -43,6 +45,9 @@ class Application {
43
45
  async listen(port, ...args) {
44
46
  if (!this.isInitialized)
45
47
  await this.init();
48
+ if (this.environment.config?.shutdown?.enabled === true) {
49
+ this.shutdownOnSignal(this.environment.config?.shutdown?.signals);
50
+ }
46
51
  return new Promise((resolve, reject) => {
47
52
  const errorHandler = (e) => {
48
53
  this.environment.log?.error(e?.toString?.());
@@ -71,13 +76,14 @@ class Application {
71
76
  }
72
77
  async close(signal) {
73
78
  await this.dispose();
74
- await this.hooks.addShutdownHook(signal);
79
+ await this.hooks.onShutdown(signal);
80
+ this.unsubscribeFromSignals();
75
81
  this.isListening = false;
76
82
  }
77
83
  async getUrl() {
78
84
  return new Promise((resolve, reject) => {
79
85
  if (!this.isListening) {
80
- this.environment.log?.error('app.listen() must be called before attempting to get the URL with app.getUrl()');
86
+ this.environment.log?.error('app.listen() must be called before app.getUrl()');
81
87
  reject('Server not listening!');
82
88
  return;
83
89
  }
@@ -85,6 +91,46 @@ class Application {
85
91
  resolve(this.formatAddress(address));
86
92
  });
87
93
  }
94
+ shutdownOnSignal(signals = []) {
95
+ signals = (0, iterare_1.iterate)(Array.from(new Set(['SIGTERM', ...signals])))
96
+ .map((signal) => signal.toString().toUpperCase().trim())
97
+ .filter((signal) => !this.activeSignals.includes(signal))
98
+ .toArray();
99
+ this.listenToSignals(signals);
100
+ }
101
+ listenToSignals(signals) {
102
+ let receivedSignal = false;
103
+ const doCleanup = async (signal) => {
104
+ try {
105
+ if (receivedSignal) {
106
+ return;
107
+ }
108
+ receivedSignal = true;
109
+ this.environment.log?.info(`Received ${signal}. Shutdown initiated...`);
110
+ await this.dispose();
111
+ await this.hooks.onShutdown(signal);
112
+ signals.forEach((sig) => process.removeListener(sig, doCleanup));
113
+ process.kill(process.pid, signal);
114
+ }
115
+ catch (err) {
116
+ this.environment.log?.error(`Error during shutdown: ${err.stack}`);
117
+ process.exit(1);
118
+ }
119
+ };
120
+ this.cleanupRef = doCleanup;
121
+ signals.forEach((signal) => {
122
+ this.activeSignals.push(signal);
123
+ process.on(signal, doCleanup);
124
+ });
125
+ }
126
+ unsubscribeFromSignals() {
127
+ if (!this.cleanupRef) {
128
+ return;
129
+ }
130
+ this.activeSignals.forEach((signal) => {
131
+ process.removeListener(signal, this.cleanupRef);
132
+ });
133
+ }
88
134
  async dispose() {
89
135
  this.adapter && (await this.adapter.close());
90
136
  }
@@ -3,8 +3,8 @@ import type { HookFactory } from './hook-factory';
3
3
  export declare class HookCollector {
4
4
  private readonly records;
5
5
  constructor(factory: HookFactory, hooks?: HookProvider[]);
6
- addStartupHook(): Promise<void>;
7
- addShutdownHook(signal?: string): Promise<void>;
6
+ onStartup(): Promise<void>;
7
+ onShutdown(signal?: string): Promise<void>;
8
8
  private runStartupHook;
9
9
  private runShutdownHook;
10
10
  private isStartupHook;
@@ -7,10 +7,10 @@ class HookCollector {
7
7
  constructor(factory, hooks = []) {
8
8
  this.records = factory.prepare(hooks);
9
9
  }
10
- async addStartupHook() {
10
+ async onStartup() {
11
11
  await Promise.all(this.runStartupHook(this.records));
12
12
  }
13
- async addShutdownHook(signal) {
13
+ async onShutdown(signal) {
14
14
  await Promise.all(this.runShutdownHook(this.records, signal));
15
15
  }
16
16
  runStartupHook(records) {
@@ -1,6 +1,6 @@
1
1
  import type { HttpAdapter } from '@notchjs/types';
2
2
  export declare class HttpAdapterHost {
3
3
  private _httpAdapter;
4
- constructor(httpAdapter?: HttpAdapter);
5
- get httpAdapter(): HttpAdapter | undefined;
4
+ constructor(httpAdapter: HttpAdapter);
5
+ get httpAdapter(): HttpAdapter;
6
6
  }
@@ -1,5 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import type { TlsOptions } from 'node:tls';
3
+ import type { ShutdownOptions } from './shutdown-options';
3
4
  export interface ApplicationConfig {
4
5
  tls?: TlsOptions;
6
+ shutdown?: ShutdownOptions;
5
7
  }
@@ -0,0 +1,7 @@
1
+ /// <reference types="node" />
2
+ export interface ShutdownOptions {
3
+ enabled?: boolean;
4
+ signals?: Array<NodeJS.Signals | string>;
5
+ graceful?: boolean;
6
+ grace?: number;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from '@armscye/logging';
2
2
  export declare class LoggerHost {
3
3
  private _logger;
4
- constructor(logger?: Logger);
5
- get logger(): Logger | undefined;
4
+ constructor(logger: Logger);
5
+ get logger(): Logger;
6
6
  }
@@ -16,10 +16,13 @@ class NotchModule {
16
16
  {
17
17
  provide: http_adapter_host_1.HttpAdapterHost.name,
18
18
  useFactory: (container) => {
19
- const adapter = container.has(constants_1.HTTP_ADAPTER)
19
+ const httpAdapter = container.has(constants_1.HTTP_ADAPTER)
20
20
  ? container.get(constants_1.HTTP_ADAPTER)
21
21
  : undefined;
22
- return new http_adapter_host_1.HttpAdapterHost(adapter);
22
+ if (!httpAdapter) {
23
+ throw new Error('HTTP adapter not found. Please register a compatible HTTP adapter with the "HTTP_ADAPTER" token.');
24
+ }
25
+ return new http_adapter_host_1.HttpAdapterHost(httpAdapter);
23
26
  },
24
27
  },
25
28
  {
@@ -55,13 +58,10 @@ class NotchModule {
55
58
  {
56
59
  provide: application_1.Application.name,
57
60
  useFactory: (container) => {
58
- const httpAdapterHost = container.get(http_adapter_host_1.HttpAdapterHost.name);
59
- if (!httpAdapterHost.httpAdapter) {
60
- throw new Error('HTTP adapter missing');
61
- }
62
61
  const config = container.has('config')
63
62
  ? container.get('config')
64
63
  : {};
64
+ const httpAdapterHost = container.get(http_adapter_host_1.HttpAdapterHost.name);
65
65
  const loggerHost = container.get(logger_host_1.LoggerHost.name);
66
66
  const hooks = container.get(hook_collector_1.HookCollector.name);
67
67
  return new application_1.Application(httpAdapterHost.httpAdapter, hooks, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notchjs/core",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "A dependency injection based web framework (Node.js)",
5
5
  "license": "MIT",
6
6
  "author": "Augustus Kamau",
@@ -18,13 +18,13 @@
18
18
  "test": "jest --detectOpenHandles"
19
19
  },
20
20
  "dependencies": {
21
- "@notchjs/util": "^0.2.1",
21
+ "@notchjs/util": "^0.4.0",
22
22
  "iterare": "1.2.1",
23
23
  "tslib": "2.6.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@notchjs/express": "^0.2.2",
27
- "@notchjs/types": "^0.2.1"
26
+ "@notchjs/express": "^0.4.0",
27
+ "@notchjs/types": "^0.4.0"
28
28
  },
29
29
  "homepage": "https://github.com/notchjs/notch",
30
30
  "bugs": {
@@ -40,5 +40,5 @@
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  },
43
- "gitHead": "976b1b1e73bf54d420001e524625a24845e06bdc"
43
+ "gitHead": "309faca8c971daedad07aff9fdc69383cdeda2b3"
44
44
  }