@shipfox/api-server 5.0.0 → 7.0.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/src/server.ts CHANGED
@@ -3,13 +3,14 @@ import {closeApp, createApp, listen} from '@shipfox/node-fastify';
3
3
  import {
4
4
  aggregateLoginMethods,
5
5
  initializeModules,
6
+ type ModuleService,
7
+ type ModuleServicesHandle,
6
8
  type ModuleWorker,
7
9
  type ModuleWorkersHandle,
8
10
  registerModuleMetrics,
9
- resetPublishers,
10
- resetSubscribers,
11
11
  runModuleStartupTasks,
12
12
  type ShipfoxModule,
13
+ startModuleServices,
13
14
  startModuleWorkers,
14
15
  } from '@shipfox/node-module';
15
16
  import {logger, shutdownServiceMetrics, startServiceMetrics} from '@shipfox/node-opentelemetry';
@@ -17,7 +18,7 @@ import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres'
17
18
  import {config, parseApiTrustProxy} from './config.js';
18
19
  import {createE2eAdminAuthMethod, createE2eRouteGroup} from './e2e.js';
19
20
 
20
- const WORKER_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS = 10_000;
21
+ const RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS = 10_000;
21
22
  const ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS = 2_000;
22
23
 
23
24
  let hasActiveServer = false;
@@ -25,6 +26,7 @@ let hasActiveServer = false;
25
26
  export interface CreateServerOptions {
26
27
  modules: ShipfoxModule[];
27
28
  onWorkerFailure?: (error: unknown, worker: ModuleWorker) => void | Promise<void>;
29
+ onServiceFailure?: (error: unknown, service: ModuleService) => void | Promise<void>;
28
30
  }
29
31
 
30
32
  export interface ServerHandle {
@@ -48,9 +50,11 @@ export async function createServer(options: CreateServerOptions): Promise<Server
48
50
  startServiceMetrics({serviceName: 'api'});
49
51
  createPostgresClient();
50
52
 
51
- const {auth, routes, e2eRoutes, workers} = await initializeModules({modules: options.modules});
52
- registerModuleMetrics({modules: options.modules});
53
- await runModuleStartupTasks({modules: options.modules});
53
+ const {auth, routes, e2eRoutes, workers, services, outboxRegistry} = await initializeModules({
54
+ modules: options.modules,
55
+ });
56
+ registerModuleMetrics({modules: options.modules, context: {outboxRegistry}});
57
+ await runModuleStartupTasks({modules: options.modules, context: {outboxRegistry}});
54
58
 
55
59
  const e2eAuth = config.E2E_ENABLED ? [createE2eAdminAuthMethod(config)] : [];
56
60
  const mountedE2eRoutes = createE2eRouteGroup(e2eRoutes, config);
@@ -63,6 +67,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
63
67
  });
64
68
 
65
69
  let workersHandle: ModuleWorkersHandle | undefined;
70
+ let servicesHandle: ModuleServicesHandle | undefined;
66
71
  let startPromise: Promise<string> | undefined;
67
72
  let stopPromise: Promise<void> | undefined;
68
73
  let stopped = false;
@@ -75,10 +80,19 @@ export async function createServer(options: CreateServerOptions): Promise<Server
75
80
  logger().info('Starting module workers');
76
81
  workersHandle = await startModuleWorkers({
77
82
  workers,
83
+ context: {outboxRegistry},
78
84
  onWorkerFailure: (error, worker) =>
79
85
  handleModuleWorkerFailure(error, worker, options.onWorkerFailure),
80
86
  });
81
87
 
88
+ logger().info('Starting module services');
89
+ servicesHandle = await startModuleServices({
90
+ services,
91
+ context: {outboxRegistry},
92
+ onServiceFailure: (error, service) =>
93
+ handleModuleServiceFailure(error, service, options.onServiceFailure),
94
+ });
95
+
82
96
  if (stopPromise) throw new Error('API server stopped during startup');
83
97
 
84
98
  logger().info('Starting HTTP server');
@@ -98,12 +112,13 @@ export async function createServer(options: CreateServerOptions): Promise<Server
98
112
  await startPromise?.catch(() => undefined);
99
113
  const cleanupErrors = await runCleanupSteps([
100
114
  () => closeApp(),
115
+ () => servicesHandle?.stop(),
101
116
  () => workersHandle?.stop(),
102
117
  () => shutdownServiceMetrics(),
103
118
  () => closePostgresClient(),
104
- () => resetPublishers(),
105
- () => resetSubscribers(),
106
- () => closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS),
119
+ async () => {
120
+ await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
121
+ },
107
122
  ]);
108
123
  throwCleanupErrors(cleanupErrors, 'Failed to stop API server');
109
124
  stopped = true;
@@ -120,8 +135,6 @@ export async function createServer(options: CreateServerOptions): Promise<Server
120
135
  const cleanupErrors = await runCleanupSteps([
121
136
  () => shutdownServiceMetrics(),
122
137
  () => closePostgresClient(),
123
- () => resetPublishers(),
124
- () => resetSubscribers(),
125
138
  ]);
126
139
  for (const cleanupError of cleanupErrors) {
127
140
  logger().error({err: cleanupError, bootError: error}, 'Failed to clean up API server boot');
@@ -137,6 +150,7 @@ export async function runServer(options: RunServerOptions): Promise<ServerHandle
137
150
  handle = await createServer({
138
151
  modules: options.modules,
139
152
  onWorkerFailure: () => process.exit(1),
153
+ onServiceFailure: () => process.exit(1),
140
154
  });
141
155
  } catch (error) {
142
156
  await reportStartupFailure(error, options.onStartupFailure);
@@ -204,21 +218,48 @@ async function handleModuleWorkerFailure(
204
218
  worker: ModuleWorker,
205
219
  onWorkerFailure: CreateServerOptions['onWorkerFailure'],
206
220
  ): Promise<void> {
207
- logger().error({err: error, taskQueue: worker.taskQueue}, 'Module worker stopped unexpectedly');
208
- captureException(error);
221
+ await handleModuleRuntimeFailure({
222
+ error,
223
+ fields: {taskQueue: worker.taskQueue},
224
+ message: 'Module worker stopped unexpectedly',
225
+ onFailure: () => onWorkerFailure?.(error, worker),
226
+ });
227
+ }
228
+
229
+ async function handleModuleServiceFailure(
230
+ error: unknown,
231
+ service: ModuleService,
232
+ onServiceFailure: CreateServerOptions['onServiceFailure'],
233
+ ): Promise<void> {
234
+ await handleModuleRuntimeFailure({
235
+ error,
236
+ fields: {service: service.name},
237
+ message: 'Module service stopped unexpectedly',
238
+ onFailure: () => onServiceFailure?.(error, service),
239
+ });
240
+ }
241
+
242
+ async function handleModuleRuntimeFailure(options: {
243
+ error: unknown;
244
+ fields: Record<string, string>;
245
+ message: string;
246
+ onFailure(): void | Promise<void>;
247
+ }): Promise<void> {
248
+ logger().error({err: options.error, ...options.fields}, options.message);
249
+ captureException(options.error);
209
250
 
210
251
  try {
211
- await closeHttpServerAfterWorkerFailure();
252
+ await closeHttpServerAfterRuntimeFailure();
212
253
  await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
213
254
  } finally {
214
- await onWorkerFailure?.(error, worker);
255
+ await options.onFailure();
215
256
  }
216
257
  }
217
258
 
218
- async function closeHttpServerAfterWorkerFailure(): Promise<void> {
259
+ async function closeHttpServerAfterRuntimeFailure(): Promise<void> {
219
260
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
220
261
  const timeout = new Promise<'timeout'>((resolve) => {
221
- timeoutId = setTimeout(() => resolve('timeout'), WORKER_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS);
262
+ timeoutId = setTimeout(() => resolve('timeout'), RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS);
222
263
  });
223
264
  try {
224
265
  const result = await Promise.race([closeApp().then(() => 'closed' as const), timeout]).finally(
@@ -230,12 +271,12 @@ async function closeHttpServerAfterWorkerFailure(): Promise<void> {
230
271
  );
231
272
  if (result === 'timeout') {
232
273
  logger().error(
233
- {timeoutMs: WORKER_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS},
234
- 'Timed out closing HTTP server after worker failure',
274
+ {timeoutMs: RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS},
275
+ 'Timed out closing HTTP server after module runtime failure',
235
276
  );
236
277
  }
237
278
  } catch (error) {
238
- logger().error({err: error}, 'Failed to close HTTP server after worker failure');
279
+ logger().error({err: error}, 'Failed to close HTTP server after module runtime failure');
239
280
  } finally {
240
281
  if (timeoutId) {
241
282
  clearTimeout(timeoutId);