@zero-server/sdk 0.9.0 → 0.9.2

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.
Files changed (126) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -437
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +460 -460
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +136 -136
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +254 -254
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/cluster.d.ts +75 -75
  113. package/types/env.d.ts +80 -80
  114. package/types/errors.d.ts +316 -316
  115. package/types/fetch.d.ts +43 -43
  116. package/types/grpc.d.ts +432 -432
  117. package/types/index.d.ts +384 -384
  118. package/types/lifecycle.d.ts +60 -60
  119. package/types/middleware.d.ts +320 -320
  120. package/types/observe.d.ts +304 -304
  121. package/types/orm.d.ts +1887 -1887
  122. package/types/request.d.ts +109 -109
  123. package/types/response.d.ts +157 -157
  124. package/types/router.d.ts +78 -78
  125. package/types/sse.d.ts +78 -78
  126. package/types/websocket.d.ts +126 -126
package/lib/lifecycle.js CHANGED
@@ -1,557 +1,557 @@
1
- /**
2
- * @module lifecycle
3
- * @description Graceful shutdown manager for zero-server applications.
4
- * Tracks active connections, drains in-flight requests, closes
5
- * WebSocket, SSE, and gRPC connections, and shuts down ORM
6
- * databases before exiting.
7
- *
8
- * @example
9
- * const app = createApp();
10
- * app.listen(3000);
11
- *
12
- * // Automatic — SIGTERM/SIGINT handlers registered by listen()
13
- * // Manual trigger:
14
- * await app.shutdown();
15
- */
16
- const log = require('./debug')('zero:lifecycle');
17
-
18
- /**
19
- * Lifecycle states.
20
- * @enum {string}
21
- */
22
- const LIFECYCLE_STATE = {
23
- RUNNING: 'running',
24
- DRAINING: 'draining',
25
- CLOSED: 'closed',
26
- };
27
-
28
- // -- Lifecycle Manager -------------------------
29
-
30
- /**
31
- * Manages graceful shutdown for an App instance.
32
- * Tracks active HTTP connections and coordinates shutdown
33
- * across the server, WebSocket pools, SSE streams, gRPC services,
34
- * and ORM databases.
35
- */
36
- class LifecycleManager
37
- {
38
- /**
39
- * @constructor
40
- * @param {import('./app')} app - The App instance to manage.
41
- */
42
- constructor(app)
43
- {
44
- /** @type {import('./app')} */
45
- this._app = app;
46
-
47
- /** @type {string} */
48
- this.state = LIFECYCLE_STATE.RUNNING;
49
-
50
- /** Active HTTP requests being processed. @type {Set<import('http').ServerResponse>} */
51
- this._activeRequests = new Set();
52
-
53
- /** Registered WebSocket pools for shutdown. @type {Set<import('./ws/room')>} */
54
- this._wsPools = new Set();
55
-
56
- /** Active SSE streams for shutdown. @type {Set<import('./sse/stream')>} */
57
- this._sseStreams = new Set();
58
-
59
- /** gRPC service registry for shutdown draining. @type {import('./grpc/server').GrpcServiceRegistry|null} */
60
- this._grpcRegistry = null;
61
-
62
- /** ORM Database instances for shutdown. @type {Set<import('./orm')>} */
63
- this._databases = new Set();
64
-
65
- /** Lifecycle event listeners. @type {Object<string, Function[]>} */
66
- this._listeners = {};
67
-
68
- /** Signal handler references for cleanup. @private */
69
- this._signalHandlers = {};
70
-
71
- /** Whether signal handlers have been installed. @private */
72
- this._signalsInstalled = false;
73
-
74
- /** @type {number} Default shutdown timeout in ms. */
75
- this._shutdownTimeout = 30000;
76
-
77
- /** Prevents duplicate shutdown calls. @private */
78
- this._shutdownPromise = null;
79
- }
80
-
81
- // -- Event Emitter ---------------------------------
82
-
83
- /**
84
- * Register a lifecycle event listener.
85
- *
86
- * @param {'beforeShutdown'|'shutdown'|'close'} event - Event name.
87
- * @param {Function} fn - Callback function.
88
- * @returns {LifecycleManager} this
89
- *
90
- * @example
91
- * app.on('beforeShutdown', async () => {
92
- * await flushMetrics();
93
- * });
94
- *
95
- * app.on('shutdown', () => {
96
- * console.log('server shut down');
97
- * });
98
- */
99
- on(event, fn)
100
- {
101
- if (!this._listeners[event]) this._listeners[event] = [];
102
- this._listeners[event].push(fn);
103
- return this;
104
- }
105
-
106
- /**
107
- * Remove a lifecycle event listener.
108
- *
109
- * @param {'beforeShutdown'|'shutdown'|'close'} event - Event name.
110
- * @param {Function} fn - Callback to remove.
111
- * @returns {LifecycleManager} this
112
- */
113
- off(event, fn)
114
- {
115
- const list = this._listeners[event];
116
- if (!list) return this;
117
- this._listeners[event] = list.filter(f => f !== fn);
118
- return this;
119
- }
120
-
121
- /**
122
- * Emit a lifecycle event, calling all registered listeners.
123
- * Awaits async listeners sequentially.
124
- * @private
125
- * @param {string} event - Event name.
126
- */
127
- async _emit(event)
128
- {
129
- const fns = this._listeners[event];
130
- if (!fns || fns.length === 0) return;
131
- for (const fn of fns.slice())
132
- {
133
- try { await fn(); }
134
- catch (err) { log.error('lifecycle %s listener error: %s', event, err.message); }
135
- }
136
- }
137
-
138
- // -- Connection Tracking ---------------------------
139
-
140
- /**
141
- * Track an active HTTP request. Called automatically by the App
142
- * request handler when lifecycle management is enabled.
143
- *
144
- * @param {import('http').ServerResponse} res - The raw response object.
145
- */
146
- trackRequest(res)
147
- {
148
- this._activeRequests.add(res);
149
- res.on('close', () => this._activeRequests.delete(res));
150
- }
151
-
152
- /**
153
- * Number of currently active HTTP requests.
154
- * @type {number}
155
- */
156
- get activeRequests()
157
- {
158
- return this._activeRequests.size;
159
- }
160
-
161
- /**
162
- * Register a WebSocket pool for graceful shutdown.
163
- * All connections in registered pools are closed with code `1001`
164
- * during shutdown.
165
- *
166
- * @param {import('./ws/room')} pool - WebSocket pool instance.
167
- * @returns {LifecycleManager} this
168
- *
169
- * @example
170
- * const pool = new WebSocketPool();
171
- * app.registerPool(pool);
172
- */
173
- registerPool(pool)
174
- {
175
- this._wsPools.add(pool);
176
- return this;
177
- }
178
-
179
- /**
180
- * Unregister a WebSocket pool.
181
- *
182
- * @param {import('./ws/room')} pool - WebSocket pool instance.
183
- * @returns {LifecycleManager} this
184
- */
185
- unregisterPool(pool)
186
- {
187
- this._wsPools.delete(pool);
188
- return this;
189
- }
190
-
191
- /**
192
- * Track an active SSE stream for graceful shutdown.
193
- *
194
- * @param {import('./sse/stream')} stream - SSE stream instance.
195
- * @returns {LifecycleManager} this
196
- */
197
- trackSSE(stream)
198
- {
199
- this._sseStreams.add(stream);
200
- stream.on('close', () => this._sseStreams.delete(stream));
201
- return this;
202
- }
203
-
204
- /**
205
- * Register an ORM Database instance for graceful shutdown.
206
- * The database connection is closed during shutdown.
207
- *
208
- * @param {import('./orm')} db - Database instance.
209
- * @returns {LifecycleManager} this
210
- *
211
- * @example
212
- * const db = new Database({ adapter: 'sqlite', file: ':memory:' });
213
- * app.registerDatabase(db);
214
- */
215
- registerDatabase(db)
216
- {
217
- this._databases.add(db);
218
- return this;
219
- }
220
-
221
- /**
222
- * Register the gRPC service registry for graceful shutdown.
223
- * Active gRPC calls are drained before the server closes.
224
- *
225
- * @param {import('./grpc/server').GrpcServiceRegistry} registry - gRPC registry.
226
- * @returns {LifecycleManager} this
227
- */
228
- registerGrpc(registry)
229
- {
230
- this._grpcRegistry = registry;
231
- return this;
232
- }
233
-
234
- /**
235
- * Unregister an ORM Database instance.
236
- *
237
- * @param {import('./orm')} db - Database instance.
238
- * @returns {LifecycleManager} this
239
- */
240
- unregisterDatabase(db)
241
- {
242
- this._databases.delete(db);
243
- return this;
244
- }
245
-
246
- // -- Signal Handling -------------------------------
247
-
248
- /**
249
- * Install `SIGTERM` and `SIGINT` process signal handlers that trigger
250
- * graceful shutdown. Called automatically by `app.listen()`.
251
- * Safe to call multiple times — handlers are only installed once.
252
- */
253
- installSignalHandlers()
254
- {
255
- if (this._signalsInstalled) return;
256
- this._signalsInstalled = true;
257
-
258
- const handler = (signal) =>
259
- {
260
- log.info('received %s, starting graceful shutdown', signal);
261
- this.shutdown().then(() =>
262
- {
263
- process.exit(0);
264
- }).catch((err) =>
265
- {
266
- log.error('shutdown error: %s', err.message);
267
- process.exit(1);
268
- });
269
- };
270
-
271
- this._signalHandlers.SIGTERM = () => handler('SIGTERM');
272
- this._signalHandlers.SIGINT = () => handler('SIGINT');
273
-
274
- process.on('SIGTERM', this._signalHandlers.SIGTERM);
275
- process.on('SIGINT', this._signalHandlers.SIGINT);
276
- }
277
-
278
- /**
279
- * Remove previously installed signal handlers.
280
- * Called automatically during shutdown cleanup.
281
- */
282
- removeSignalHandlers()
283
- {
284
- if (!this._signalsInstalled) return;
285
- if (this._signalHandlers.SIGTERM)
286
- {
287
- process.removeListener('SIGTERM', this._signalHandlers.SIGTERM);
288
- }
289
- if (this._signalHandlers.SIGINT)
290
- {
291
- process.removeListener('SIGINT', this._signalHandlers.SIGINT);
292
- }
293
- this._signalHandlers = {};
294
- this._signalsInstalled = false;
295
- }
296
-
297
- // -- Shutdown Sequence -----------------------------
298
-
299
- /**
300
- * Perform a full graceful shutdown.
301
- *
302
- * Shutdown sequence:
303
- * 1. Emit `'beforeShutdown'` — run pre-shutdown hooks (flush metrics, etc.)
304
- * 2. Stop accepting new connections (server.close)
305
- * 3. Close all WebSocket connections with code `1001` (Going Away)
306
- * 4. Close all SSE streams
307
- * 5. Drain active gRPC calls
308
- * 6. Wait for in-flight HTTP requests to complete (with timeout)
309
- * 7. Close all registered ORM database connections
310
- * 8. Emit `'shutdown'` — final cleanup complete
311
- *
312
- * If in-flight requests do not complete within the configured
313
- * timeout (default 30s), they are forcefully terminated.
314
- *
315
- * @param {object} [opts] - Shutdown options.
316
- * @param {number} [opts.timeout] - Maximum ms to wait for in-flight requests. Overrides the configured default.
317
- * @returns {Promise<void>} Resolves when shutdown is complete.
318
- *
319
- * @example | With Default Timeout
320
- * await app.shutdown();
321
- *
322
- * @example | With Custom Timeout
323
- * await app.shutdown({ timeout: 5000 });
324
- */
325
- async shutdown(opts = {})
326
- {
327
- // Deduplicate concurrent shutdown calls
328
- if (this._shutdownPromise) return this._shutdownPromise;
329
- if (this.state === LIFECYCLE_STATE.CLOSED) return;
330
-
331
- this._shutdownPromise = this._doShutdown(opts);
332
- return this._shutdownPromise;
333
- }
334
-
335
- /**
336
- * Internal shutdown implementation.
337
- * @private
338
- */
339
- async _doShutdown(opts)
340
- {
341
- const timeout = opts.timeout !== undefined ? opts.timeout : this._shutdownTimeout;
342
- log.info('graceful shutdown initiated (timeout=%dms)', timeout);
343
-
344
- // 1. beforeShutdown hooks
345
- this.state = LIFECYCLE_STATE.DRAINING;
346
- await this._emit('beforeShutdown');
347
-
348
- // 2. Stop accepting new connections (non-blocking — server.close
349
- // only resolves after all existing connections end, so we don't
350
- // await it here; it will resolve after the drain step finishes).
351
- const serverClosePromise = this._closeServer();
352
-
353
- // 3. Close WebSocket connections
354
- this._closeWebSockets();
355
-
356
- // 4. Close SSE streams
357
- this._closeSSEStreams();
358
-
359
- // 5. Drain active gRPC calls
360
- await this._drainGrpc(timeout);
361
-
362
- // 6. Drain in-flight requests (with timeout + force-close)
363
- await this._drainRequests(timeout);
364
-
365
- // 7. Wait for server to fully close (should be instant now that
366
- // all connections are gone)
367
- await serverClosePromise;
368
-
369
- // 8. Close databases
370
- await this._closeDatabases();
371
-
372
- // 9. Cleanup signals
373
- this.removeSignalHandlers();
374
-
375
- this.state = LIFECYCLE_STATE.CLOSED;
376
- log.info('graceful shutdown complete');
377
-
378
- // 10. Final event
379
- await this._emit('shutdown');
380
- }
381
-
382
- /**
383
- * Stop the HTTP server from accepting new connections.
384
- * @private
385
- * @returns {Promise<void>}
386
- */
387
- _closeServer()
388
- {
389
- return new Promise((resolve) =>
390
- {
391
- const server = this._app._server;
392
- if (!server) { resolve(); return; }
393
-
394
- server.close((err) =>
395
- {
396
- if (err) log.warn('server close error: %s', err.message);
397
- resolve();
398
- });
399
- });
400
- }
401
-
402
- /**
403
- * Close all WebSocket connections across registered pools.
404
- * @private
405
- */
406
- _closeWebSockets()
407
- {
408
- let closed = 0;
409
- for (const pool of this._wsPools)
410
- {
411
- closed += pool.size;
412
- pool.closeAll(1001, 'Server shutdown');
413
- }
414
- if (closed > 0) log.info('closed %d WebSocket connections', closed);
415
- }
416
-
417
- /**
418
- * Close all tracked SSE streams.
419
- * @private
420
- */
421
- _closeSSEStreams()
422
- {
423
- let closed = 0;
424
- for (const stream of this._sseStreams)
425
- {
426
- if (stream.connected)
427
- {
428
- stream.close();
429
- closed++;
430
- }
431
- }
432
- this._sseStreams.clear();
433
- if (closed > 0) log.info('closed %d SSE streams', closed);
434
- }
435
-
436
- /**
437
- * Drain active gRPC calls via the service registry.
438
- * @private
439
- * @param {number} timeout - Max wait time in ms.
440
- * @returns {Promise<void>}
441
- */
442
- async _drainGrpc(timeout)
443
- {
444
- if (!this._grpcRegistry) return;
445
- await this._grpcRegistry.drain(timeout);
446
- }
447
-
448
- /**
449
- * Wait for all in-flight HTTP requests to complete, or force-close
450
- * after the timeout.
451
- * @private
452
- * @param {number} timeout - Max wait time in ms.
453
- * @returns {Promise<void>}
454
- */
455
- _drainRequests(timeout)
456
- {
457
- return new Promise((resolve) =>
458
- {
459
- if (this._activeRequests.size === 0)
460
- {
461
- log.debug('no active requests to drain');
462
- resolve();
463
- return;
464
- }
465
-
466
- log.info('draining %d active requests', this._activeRequests.size);
467
-
468
- let resolved = false;
469
- const done = () =>
470
- {
471
- if (resolved) return;
472
- resolved = true;
473
- clearTimeout(timer);
474
- resolve();
475
- };
476
-
477
- // Check periodically if all requests have completed
478
- const check = () =>
479
- {
480
- if (this._activeRequests.size === 0) done();
481
- };
482
-
483
- // Listen for request completions
484
- const interval = setInterval(() =>
485
- {
486
- check();
487
- }, 50);
488
-
489
- // Force-close after timeout
490
- const timer = setTimeout(() =>
491
- {
492
- clearInterval(interval);
493
- if (this._activeRequests.size > 0)
494
- {
495
- log.warn('force-closing %d requests after %dms timeout', this._activeRequests.size, timeout);
496
- for (const res of this._activeRequests)
497
- {
498
- try
499
- {
500
- if (!res.writableEnded) res.end();
501
- if (res.socket && !res.socket.destroyed) res.socket.destroy();
502
- }
503
- catch (_) { /* socket may already be destroyed */ }
504
- }
505
- this._activeRequests.clear();
506
- }
507
- done();
508
- }, timeout);
509
-
510
- // Don't let timer/interval keep the process alive
511
- if (timer.unref) timer.unref();
512
- if (interval.unref) interval.unref();
513
-
514
- // Immediate check in case requests finished already
515
- check();
516
- });
517
- }
518
-
519
- /**
520
- * Close all registered ORM database connections.
521
- * @private
522
- */
523
- async _closeDatabases()
524
- {
525
- for (const db of this._databases)
526
- {
527
- try
528
- {
529
- if (typeof db.close === 'function') await db.close();
530
- }
531
- catch (err)
532
- {
533
- log.error('database close error: %s', err.message);
534
- }
535
- }
536
- }
537
-
538
- /**
539
- * Whether the server is currently draining (rejecting new requests).
540
- * @type {boolean}
541
- */
542
- get isDraining()
543
- {
544
- return this.state === LIFECYCLE_STATE.DRAINING;
545
- }
546
-
547
- /**
548
- * Whether the server has fully shut down.
549
- * @type {boolean}
550
- */
551
- get isClosed()
552
- {
553
- return this.state === LIFECYCLE_STATE.CLOSED;
554
- }
555
- }
556
-
557
- module.exports = { LifecycleManager, LIFECYCLE_STATE };
1
+ /**
2
+ * @module lifecycle
3
+ * @description Graceful shutdown manager for zero-server applications.
4
+ * Tracks active connections, drains in-flight requests, closes
5
+ * WebSocket, SSE, and gRPC connections, and shuts down ORM
6
+ * databases before exiting.
7
+ *
8
+ * @example
9
+ * const app = createApp();
10
+ * app.listen(3000);
11
+ *
12
+ * // Automatic — SIGTERM/SIGINT handlers registered by listen()
13
+ * // Manual trigger:
14
+ * await app.shutdown();
15
+ */
16
+ const log = require('./debug')('zero:lifecycle');
17
+
18
+ /**
19
+ * Lifecycle states.
20
+ * @enum {string}
21
+ */
22
+ const LIFECYCLE_STATE = {
23
+ RUNNING: 'running',
24
+ DRAINING: 'draining',
25
+ CLOSED: 'closed',
26
+ };
27
+
28
+ // -- Lifecycle Manager -------------------------
29
+
30
+ /**
31
+ * Manages graceful shutdown for an App instance.
32
+ * Tracks active HTTP connections and coordinates shutdown
33
+ * across the server, WebSocket pools, SSE streams, gRPC services,
34
+ * and ORM databases.
35
+ */
36
+ class LifecycleManager
37
+ {
38
+ /**
39
+ * @constructor
40
+ * @param {import('./app')} app - The App instance to manage.
41
+ */
42
+ constructor(app)
43
+ {
44
+ /** @type {import('./app')} */
45
+ this._app = app;
46
+
47
+ /** @type {string} */
48
+ this.state = LIFECYCLE_STATE.RUNNING;
49
+
50
+ /** Active HTTP requests being processed. @type {Set<import('http').ServerResponse>} */
51
+ this._activeRequests = new Set();
52
+
53
+ /** Registered WebSocket pools for shutdown. @type {Set<import('./ws/room')>} */
54
+ this._wsPools = new Set();
55
+
56
+ /** Active SSE streams for shutdown. @type {Set<import('./sse/stream')>} */
57
+ this._sseStreams = new Set();
58
+
59
+ /** gRPC service registry for shutdown draining. @type {import('./grpc/server').GrpcServiceRegistry|null} */
60
+ this._grpcRegistry = null;
61
+
62
+ /** ORM Database instances for shutdown. @type {Set<import('./orm')>} */
63
+ this._databases = new Set();
64
+
65
+ /** Lifecycle event listeners. @type {Object<string, Function[]>} */
66
+ this._listeners = {};
67
+
68
+ /** Signal handler references for cleanup. @private */
69
+ this._signalHandlers = {};
70
+
71
+ /** Whether signal handlers have been installed. @private */
72
+ this._signalsInstalled = false;
73
+
74
+ /** @type {number} Default shutdown timeout in ms. */
75
+ this._shutdownTimeout = 30000;
76
+
77
+ /** Prevents duplicate shutdown calls. @private */
78
+ this._shutdownPromise = null;
79
+ }
80
+
81
+ // -- Event Emitter ---------------------------------
82
+
83
+ /**
84
+ * Register a lifecycle event listener.
85
+ *
86
+ * @param {'beforeShutdown'|'shutdown'|'close'} event - Event name.
87
+ * @param {Function} fn - Callback function.
88
+ * @returns {LifecycleManager} this
89
+ *
90
+ * @example
91
+ * app.on('beforeShutdown', async () => {
92
+ * await flushMetrics();
93
+ * });
94
+ *
95
+ * app.on('shutdown', () => {
96
+ * console.log('server shut down');
97
+ * });
98
+ */
99
+ on(event, fn)
100
+ {
101
+ if (!this._listeners[event]) this._listeners[event] = [];
102
+ this._listeners[event].push(fn);
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * Remove a lifecycle event listener.
108
+ *
109
+ * @param {'beforeShutdown'|'shutdown'|'close'} event - Event name.
110
+ * @param {Function} fn - Callback to remove.
111
+ * @returns {LifecycleManager} this
112
+ */
113
+ off(event, fn)
114
+ {
115
+ const list = this._listeners[event];
116
+ if (!list) return this;
117
+ this._listeners[event] = list.filter(f => f !== fn);
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * Emit a lifecycle event, calling all registered listeners.
123
+ * Awaits async listeners sequentially.
124
+ * @private
125
+ * @param {string} event - Event name.
126
+ */
127
+ async _emit(event)
128
+ {
129
+ const fns = this._listeners[event];
130
+ if (!fns || fns.length === 0) return;
131
+ for (const fn of fns.slice())
132
+ {
133
+ try { await fn(); }
134
+ catch (err) { log.error('lifecycle %s listener error: %s', event, err.message); }
135
+ }
136
+ }
137
+
138
+ // -- Connection Tracking ---------------------------
139
+
140
+ /**
141
+ * Track an active HTTP request. Called automatically by the App
142
+ * request handler when lifecycle management is enabled.
143
+ *
144
+ * @param {import('http').ServerResponse} res - The raw response object.
145
+ */
146
+ trackRequest(res)
147
+ {
148
+ this._activeRequests.add(res);
149
+ res.on('close', () => this._activeRequests.delete(res));
150
+ }
151
+
152
+ /**
153
+ * Number of currently active HTTP requests.
154
+ * @type {number}
155
+ */
156
+ get activeRequests()
157
+ {
158
+ return this._activeRequests.size;
159
+ }
160
+
161
+ /**
162
+ * Register a WebSocket pool for graceful shutdown.
163
+ * All connections in registered pools are closed with code `1001`
164
+ * during shutdown.
165
+ *
166
+ * @param {import('./ws/room')} pool - WebSocket pool instance.
167
+ * @returns {LifecycleManager} this
168
+ *
169
+ * @example
170
+ * const pool = new WebSocketPool();
171
+ * app.registerPool(pool);
172
+ */
173
+ registerPool(pool)
174
+ {
175
+ this._wsPools.add(pool);
176
+ return this;
177
+ }
178
+
179
+ /**
180
+ * Unregister a WebSocket pool.
181
+ *
182
+ * @param {import('./ws/room')} pool - WebSocket pool instance.
183
+ * @returns {LifecycleManager} this
184
+ */
185
+ unregisterPool(pool)
186
+ {
187
+ this._wsPools.delete(pool);
188
+ return this;
189
+ }
190
+
191
+ /**
192
+ * Track an active SSE stream for graceful shutdown.
193
+ *
194
+ * @param {import('./sse/stream')} stream - SSE stream instance.
195
+ * @returns {LifecycleManager} this
196
+ */
197
+ trackSSE(stream)
198
+ {
199
+ this._sseStreams.add(stream);
200
+ stream.on('close', () => this._sseStreams.delete(stream));
201
+ return this;
202
+ }
203
+
204
+ /**
205
+ * Register an ORM Database instance for graceful shutdown.
206
+ * The database connection is closed during shutdown.
207
+ *
208
+ * @param {import('./orm')} db - Database instance.
209
+ * @returns {LifecycleManager} this
210
+ *
211
+ * @example
212
+ * const db = new Database({ adapter: 'sqlite', file: ':memory:' });
213
+ * app.registerDatabase(db);
214
+ */
215
+ registerDatabase(db)
216
+ {
217
+ this._databases.add(db);
218
+ return this;
219
+ }
220
+
221
+ /**
222
+ * Register the gRPC service registry for graceful shutdown.
223
+ * Active gRPC calls are drained before the server closes.
224
+ *
225
+ * @param {import('./grpc/server').GrpcServiceRegistry} registry - gRPC registry.
226
+ * @returns {LifecycleManager} this
227
+ */
228
+ registerGrpc(registry)
229
+ {
230
+ this._grpcRegistry = registry;
231
+ return this;
232
+ }
233
+
234
+ /**
235
+ * Unregister an ORM Database instance.
236
+ *
237
+ * @param {import('./orm')} db - Database instance.
238
+ * @returns {LifecycleManager} this
239
+ */
240
+ unregisterDatabase(db)
241
+ {
242
+ this._databases.delete(db);
243
+ return this;
244
+ }
245
+
246
+ // -- Signal Handling -------------------------------
247
+
248
+ /**
249
+ * Install `SIGTERM` and `SIGINT` process signal handlers that trigger
250
+ * graceful shutdown. Called automatically by `app.listen()`.
251
+ * Safe to call multiple times — handlers are only installed once.
252
+ */
253
+ installSignalHandlers()
254
+ {
255
+ if (this._signalsInstalled) return;
256
+ this._signalsInstalled = true;
257
+
258
+ const handler = (signal) =>
259
+ {
260
+ log.info('received %s, starting graceful shutdown', signal);
261
+ this.shutdown().then(() =>
262
+ {
263
+ process.exit(0);
264
+ }).catch((err) =>
265
+ {
266
+ log.error('shutdown error: %s', err.message);
267
+ process.exit(1);
268
+ });
269
+ };
270
+
271
+ this._signalHandlers.SIGTERM = () => handler('SIGTERM');
272
+ this._signalHandlers.SIGINT = () => handler('SIGINT');
273
+
274
+ process.on('SIGTERM', this._signalHandlers.SIGTERM);
275
+ process.on('SIGINT', this._signalHandlers.SIGINT);
276
+ }
277
+
278
+ /**
279
+ * Remove previously installed signal handlers.
280
+ * Called automatically during shutdown cleanup.
281
+ */
282
+ removeSignalHandlers()
283
+ {
284
+ if (!this._signalsInstalled) return;
285
+ if (this._signalHandlers.SIGTERM)
286
+ {
287
+ process.removeListener('SIGTERM', this._signalHandlers.SIGTERM);
288
+ }
289
+ if (this._signalHandlers.SIGINT)
290
+ {
291
+ process.removeListener('SIGINT', this._signalHandlers.SIGINT);
292
+ }
293
+ this._signalHandlers = {};
294
+ this._signalsInstalled = false;
295
+ }
296
+
297
+ // -- Shutdown Sequence -----------------------------
298
+
299
+ /**
300
+ * Perform a full graceful shutdown.
301
+ *
302
+ * Shutdown sequence:
303
+ * 1. Emit `'beforeShutdown'` — run pre-shutdown hooks (flush metrics, etc.)
304
+ * 2. Stop accepting new connections (server.close)
305
+ * 3. Close all WebSocket connections with code `1001` (Going Away)
306
+ * 4. Close all SSE streams
307
+ * 5. Drain active gRPC calls
308
+ * 6. Wait for in-flight HTTP requests to complete (with timeout)
309
+ * 7. Close all registered ORM database connections
310
+ * 8. Emit `'shutdown'` — final cleanup complete
311
+ *
312
+ * If in-flight requests do not complete within the configured
313
+ * timeout (default 30s), they are forcefully terminated.
314
+ *
315
+ * @param {object} [opts] - Shutdown options.
316
+ * @param {number} [opts.timeout] - Maximum ms to wait for in-flight requests. Overrides the configured default.
317
+ * @returns {Promise<void>} Resolves when shutdown is complete.
318
+ *
319
+ * @example | With Default Timeout
320
+ * await app.shutdown();
321
+ *
322
+ * @example | With Custom Timeout
323
+ * await app.shutdown({ timeout: 5000 });
324
+ */
325
+ async shutdown(opts = {})
326
+ {
327
+ // Deduplicate concurrent shutdown calls
328
+ if (this._shutdownPromise) return this._shutdownPromise;
329
+ if (this.state === LIFECYCLE_STATE.CLOSED) return;
330
+
331
+ this._shutdownPromise = this._doShutdown(opts);
332
+ return this._shutdownPromise;
333
+ }
334
+
335
+ /**
336
+ * Internal shutdown implementation.
337
+ * @private
338
+ */
339
+ async _doShutdown(opts)
340
+ {
341
+ const timeout = opts.timeout !== undefined ? opts.timeout : this._shutdownTimeout;
342
+ log.info('graceful shutdown initiated (timeout=%dms)', timeout);
343
+
344
+ // 1. beforeShutdown hooks
345
+ this.state = LIFECYCLE_STATE.DRAINING;
346
+ await this._emit('beforeShutdown');
347
+
348
+ // 2. Stop accepting new connections (non-blocking — server.close
349
+ // only resolves after all existing connections end, so we don't
350
+ // await it here; it will resolve after the drain step finishes).
351
+ const serverClosePromise = this._closeServer();
352
+
353
+ // 3. Close WebSocket connections
354
+ this._closeWebSockets();
355
+
356
+ // 4. Close SSE streams
357
+ this._closeSSEStreams();
358
+
359
+ // 5. Drain active gRPC calls
360
+ await this._drainGrpc(timeout);
361
+
362
+ // 6. Drain in-flight requests (with timeout + force-close)
363
+ await this._drainRequests(timeout);
364
+
365
+ // 7. Wait for server to fully close (should be instant now that
366
+ // all connections are gone)
367
+ await serverClosePromise;
368
+
369
+ // 8. Close databases
370
+ await this._closeDatabases();
371
+
372
+ // 9. Cleanup signals
373
+ this.removeSignalHandlers();
374
+
375
+ this.state = LIFECYCLE_STATE.CLOSED;
376
+ log.info('graceful shutdown complete');
377
+
378
+ // 10. Final event
379
+ await this._emit('shutdown');
380
+ }
381
+
382
+ /**
383
+ * Stop the HTTP server from accepting new connections.
384
+ * @private
385
+ * @returns {Promise<void>}
386
+ */
387
+ _closeServer()
388
+ {
389
+ return new Promise((resolve) =>
390
+ {
391
+ const server = this._app._server;
392
+ if (!server) { resolve(); return; }
393
+
394
+ server.close((err) =>
395
+ {
396
+ if (err) log.warn('server close error: %s', err.message);
397
+ resolve();
398
+ });
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Close all WebSocket connections across registered pools.
404
+ * @private
405
+ */
406
+ _closeWebSockets()
407
+ {
408
+ let closed = 0;
409
+ for (const pool of this._wsPools)
410
+ {
411
+ closed += pool.size;
412
+ pool.closeAll(1001, 'Server shutdown');
413
+ }
414
+ if (closed > 0) log.info('closed %d WebSocket connections', closed);
415
+ }
416
+
417
+ /**
418
+ * Close all tracked SSE streams.
419
+ * @private
420
+ */
421
+ _closeSSEStreams()
422
+ {
423
+ let closed = 0;
424
+ for (const stream of this._sseStreams)
425
+ {
426
+ if (stream.connected)
427
+ {
428
+ stream.close();
429
+ closed++;
430
+ }
431
+ }
432
+ this._sseStreams.clear();
433
+ if (closed > 0) log.info('closed %d SSE streams', closed);
434
+ }
435
+
436
+ /**
437
+ * Drain active gRPC calls via the service registry.
438
+ * @private
439
+ * @param {number} timeout - Max wait time in ms.
440
+ * @returns {Promise<void>}
441
+ */
442
+ async _drainGrpc(timeout)
443
+ {
444
+ if (!this._grpcRegistry) return;
445
+ await this._grpcRegistry.drain(timeout);
446
+ }
447
+
448
+ /**
449
+ * Wait for all in-flight HTTP requests to complete, or force-close
450
+ * after the timeout.
451
+ * @private
452
+ * @param {number} timeout - Max wait time in ms.
453
+ * @returns {Promise<void>}
454
+ */
455
+ _drainRequests(timeout)
456
+ {
457
+ return new Promise((resolve) =>
458
+ {
459
+ if (this._activeRequests.size === 0)
460
+ {
461
+ log.debug('no active requests to drain');
462
+ resolve();
463
+ return;
464
+ }
465
+
466
+ log.info('draining %d active requests', this._activeRequests.size);
467
+
468
+ let resolved = false;
469
+ const done = () =>
470
+ {
471
+ if (resolved) return;
472
+ resolved = true;
473
+ clearTimeout(timer);
474
+ resolve();
475
+ };
476
+
477
+ // Check periodically if all requests have completed
478
+ const check = () =>
479
+ {
480
+ if (this._activeRequests.size === 0) done();
481
+ };
482
+
483
+ // Listen for request completions
484
+ const interval = setInterval(() =>
485
+ {
486
+ check();
487
+ }, 50);
488
+
489
+ // Force-close after timeout
490
+ const timer = setTimeout(() =>
491
+ {
492
+ clearInterval(interval);
493
+ if (this._activeRequests.size > 0)
494
+ {
495
+ log.warn('force-closing %d requests after %dms timeout', this._activeRequests.size, timeout);
496
+ for (const res of this._activeRequests)
497
+ {
498
+ try
499
+ {
500
+ if (!res.writableEnded) res.end();
501
+ if (res.socket && !res.socket.destroyed) res.socket.destroy();
502
+ }
503
+ catch (_) { /* socket may already be destroyed */ }
504
+ }
505
+ this._activeRequests.clear();
506
+ }
507
+ done();
508
+ }, timeout);
509
+
510
+ // Don't let timer/interval keep the process alive
511
+ if (timer.unref) timer.unref();
512
+ if (interval.unref) interval.unref();
513
+
514
+ // Immediate check in case requests finished already
515
+ check();
516
+ });
517
+ }
518
+
519
+ /**
520
+ * Close all registered ORM database connections.
521
+ * @private
522
+ */
523
+ async _closeDatabases()
524
+ {
525
+ for (const db of this._databases)
526
+ {
527
+ try
528
+ {
529
+ if (typeof db.close === 'function') await db.close();
530
+ }
531
+ catch (err)
532
+ {
533
+ log.error('database close error: %s', err.message);
534
+ }
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Whether the server is currently draining (rejecting new requests).
540
+ * @type {boolean}
541
+ */
542
+ get isDraining()
543
+ {
544
+ return this.state === LIFECYCLE_STATE.DRAINING;
545
+ }
546
+
547
+ /**
548
+ * Whether the server has fully shut down.
549
+ * @type {boolean}
550
+ */
551
+ get isClosed()
552
+ {
553
+ return this.state === LIFECYCLE_STATE.CLOSED;
554
+ }
555
+ }
556
+
557
+ module.exports = { LifecycleManager, LIFECYCLE_STATE };