@molecule/api-server-default-express 1.0.3 → 1.1.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/README.md CHANGED
@@ -3,15 +3,11 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-06T05:03:58.023Z
6
+ Generated: 2026-10-19T14:30:49.145Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-server-default-express
10
10
 
11
- > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
- > It is written to be read by coding agents as much as by people, and is generated from this
13
- > package's source — edit `src/index.ts` JSDoc, not this file.
14
-
15
11
  `@molecule/api-server-default-express` — drop-in Express server
16
12
  factory used by the molecule fleet's `api/src/server.ts`.
17
13
 
@@ -20,6 +16,16 @@ returns a `(port?) => Promise<server>` function that runs
20
16
  migrations, wires bonds, mounts router + middleware, and starts
21
17
  an HTTP (or self-signed HTTPS for local dev) listener.
22
18
 
19
+ It also wires the process lifecycle by default: `GET /health/live` and
20
+ `GET /health/ready`, in-flight request tracking with `Connection: close`
21
+ while draining, and SIGTERM/SIGINT graceful shutdown (drain bound
22
+ `SHUTDOWN_TIMEOUT_MS`, default 25 s) that runs ordered shutdown hooks and
23
+ exits 0. Pass `lifecycle: { disabled: true }` to opt out.
24
+
25
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
26
+ > It is written to be read by coding agents as much as by people, and is generated from this
27
+ > package's source — edit `src/index.ts` JSDoc, not this file.
28
+
23
29
  ## Quick Start
24
30
 
25
31
  ```ts
@@ -50,7 +56,7 @@ await create()
50
56
  ## Installation
51
57
 
52
58
  ```bash
53
- npm install @molecule/api-server-default-express @molecule/api-error-tracking @molecule/api-logger @molecule/api-middleware-body-parser @molecule/api-middleware-cookie-parser @molecule/api-middleware-cors @molecule/api-secrets express pem
59
+ npm install @molecule/api-server-default-express @molecule/api-error-tracking @molecule/api-logger @molecule/api-middleware-body-parser @molecule/api-middleware-cookie-parser @molecule/api-middleware-cors @molecule/api-process-lifecycle @molecule/api-process-lifecycle-express @molecule/api-secrets express pem
54
60
  npm install -D @types/express
55
61
  ```
56
62
 
@@ -58,6 +64,21 @@ npm install -D @types/express
58
64
 
59
65
  ### Interfaces
60
66
 
67
+ #### `TaggedError`
68
+
69
+ A deliberately-tagged molecule error mapped to a real HTTP status by the API.
70
+
71
+ ```typescript
72
+ interface TaggedError {
73
+ /** HTTP status to return (e.g. 503 for a missing provider config). */
74
+ statusCode: number
75
+ /** Machine-readable key the app/IDE maps to a friendly message. */
76
+ errorKey: string
77
+ /** Human-readable message. */
78
+ message: string
79
+ }
80
+ ```
81
+
61
82
  #### `CreateServerOptions`
62
83
 
63
84
  Options for `createServerFactory`.
@@ -93,21 +114,34 @@ interface CreateServerOptions {
93
114
  * resource router.
94
115
  */
95
116
  preApiRouter?: (app: express.Express) => Promise<void> | void
117
+ /**
118
+ * Process-lifecycle tuning. Omitted, the canonical defaults apply: health
119
+ * routes at `/health/live` and `/health/ready`, a 25 s drain bound
120
+ * (`SHUTDOWN_TIMEOUT_MS`), a 5 s load-balancer flip wait in production
121
+ * (`SHUTDOWN_LB_FLIP_WAIT_MS`), and SIGTERM/SIGINT handlers that exit 0.
122
+ * Set `{ disabled: true }` to keep a legacy process with no lifecycle
123
+ * wiring (health routes and signal handlers are not installed).
124
+ */
125
+ lifecycle?: ServerLifecycleOptions
96
126
  }
97
127
  ```
98
128
 
99
- #### `TaggedError`
129
+ #### `ServerLifecycleOptions`
100
130
 
101
- A deliberately-tagged molecule error mapped to a real HTTP status by the API.
131
+ Options for the process-lifecycle wiring inside {@link createServerFactory}.
102
132
 
103
133
  ```typescript
104
- interface TaggedError {
105
- /** HTTP status to return (e.g. 503 for a missing provider config). */
106
- statusCode: number
107
- /** Machine-readable key the app/IDE maps to a friendly message. */
108
- errorKey: string
109
- /** Human-readable message. */
110
- message: string
134
+ interface ServerLifecycleOptions {
135
+ /** Skip the lifecycle wiring entirely. Defaults to `false`. */
136
+ disabled?: boolean
137
+ /** Maximum time to drain in-flight requests. Defaults to `SHUTDOWN_TIMEOUT_MS` / 25 s. */
138
+ drainTimeoutMs?: number
139
+ /** Time to keep listening after draining starts. Defaults to `SHUTDOWN_LB_FLIP_WAIT_MS` / 5 s in production. */
140
+ flipWaitMs?: number
141
+ /** Exit function override (tests). Defaults to `process.exit`. */
142
+ exit?: (code: number) => void
143
+ /** Signals to handle. Defaults to `['SIGTERM', 'SIGINT']`. */
144
+ signals?: NodeJS.Signals[]
111
145
  }
112
146
  ```
113
147
 
@@ -135,30 +169,6 @@ function classifyTaggedError(error: unknown): TaggedError | null
135
169
 
136
170
  **Returns:** The classified tagged error, or `null` if it isn't a molecule-tagged error.
137
171
 
138
- #### `createServerFactory(opts)`
139
-
140
- Returns an Express server-creation function bound to the given
141
- setupBonds / runMigrations / router loaders. The returned `create`
142
- builds the canonical molecule fleet server:
143
-
144
- - Migrations run first, then bonds, then router import.
145
- - Global browser-security headers ({@link securityHeadersMiddleware})
146
- applied before any router (anti-clickjacking + nosniff + referrer baseline).
147
- - `bodyParser` / `cookieParser` / `cors` middleware applied.
148
- - Router mounted at `/api`.
149
- - `/health` endpoint with `{ status: 'ok', timestamp }`.
150
- - Bare-string `Unauthorized` / `Unauthorized.` errors normalized to 401.
151
- - HTTPS in dev if `process.env.HTTPS` is set, using self-signed
152
- certs from optional dependency `pem`.
153
- - `process.on('uncaughtException')` + `unhandledRejection` registered
154
- on first call (idempotent across multiple `create()` invocations).
155
-
156
- ```typescript
157
- function createServerFactory(
158
- opts: CreateServerOptions,
159
- ): (port?: number) => Promise<express.Express | https.Server>
160
- ```
161
-
162
172
  #### `errorMiddleware(error, req, res, _next)`
163
173
 
164
174
  Terminal Express error middleware for the canonical molecule fleet server.
@@ -185,8 +195,8 @@ depend on `NODE_ENV`. Calling `next(error)` here would fall through to Express's
185
195
  `finalhandler`, which embeds `err.stack` in the HTTP response body whenever
186
196
  `app.get('env') !== 'production'` (the default when `NODE_ENV` is unset or
187
197
  `development`), disclosing absolute server paths, module layout, dependency
188
- versions, and query/data fragments. Returning the opaque 500 unconditionally
189
- removes that leak for every flagship app regardless of how it is deployed.
198
+
199
+ _(description truncated — read the full text in `node_modules/@molecule/api-server-default-express/dist/server.d.ts`.)_
190
200
 
191
201
  ```typescript
192
202
  function errorMiddleware(
@@ -248,6 +258,30 @@ function securityHeadersMiddleware(
248
258
  - `res` — The response to set headers on.
249
259
  - `next` — Express next.
250
260
 
261
+ #### `createServerFactory(opts)`
262
+
263
+ Returns an Express server-creation function bound to the given
264
+ setupBonds / runMigrations / router loaders. The returned `create`
265
+ builds the canonical molecule fleet server:
266
+
267
+ - Migrations run first, then bonds, then router import.
268
+ - Global browser-security headers ({@link securityHeadersMiddleware})
269
+ applied before any router (anti-clickjacking + nosniff + referrer baseline).
270
+ - `bodyParser` / `cookieParser` / `cors` middleware applied.
271
+ - Router mounted at `/api`.
272
+ - `/health` endpoint with `{ status: 'ok', timestamp }`.
273
+ - Bare-string `Unauthorized` / `Unauthorized.` errors normalized to 401.
274
+ - HTTPS in dev if `process.env.HTTPS` is set, using self-signed
275
+ certs from optional dependency `pem`.
276
+ - `process.on('uncaughtException')` + `unhandledRejection` registered
277
+ on first call (idempotent across multiple `create()` invocations).
278
+
279
+ ```typescript
280
+ function createServerFactory(
281
+ opts: CreateServerOptions,
282
+ ): (port?: number) => Promise<express.Express | https.Server>
283
+ ```
284
+
251
285
  ## Injection Notes
252
286
 
253
287
  ### Requirements
@@ -270,6 +304,8 @@ Peer dependencies:
270
304
  - `@molecule/api-middleware-body-parser`
271
305
  - `@molecule/api-middleware-cookie-parser`
272
306
  - `@molecule/api-middleware-cors`
307
+ - `@molecule/api-process-lifecycle`
308
+ - `@molecule/api-process-lifecycle-express`
273
309
  - `@molecule/api-secrets`
274
310
  - `express`
275
311
  - `pem`
package/dist/index.d.ts CHANGED
@@ -7,6 +7,12 @@
7
7
  * migrations, wires bonds, mounts router + middleware, and starts
8
8
  * an HTTP (or self-signed HTTPS for local dev) listener.
9
9
  *
10
+ * It also wires the process lifecycle by default: `GET /health/live` and
11
+ * `GET /health/ready`, in-flight request tracking with `Connection: close`
12
+ * while draining, and SIGTERM/SIGINT graceful shutdown (drain bound
13
+ * `SHUTDOWN_TIMEOUT_MS`, default 25 s) that runs ordered shutdown hooks and
14
+ * exits 0. Pass `lifecycle: { disabled: true }` to opt out.
15
+ *
10
16
  * @example
11
17
  * ```ts
12
18
  * import { createServerFactory } from '@molecule/api-server-default-express'
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -7,6 +7,12 @@
7
7
  * migrations, wires bonds, mounts router + middleware, and starts
8
8
  * an HTTP (or self-signed HTTPS for local dev) listener.
9
9
  *
10
+ * It also wires the process lifecycle by default: `GET /health/live` and
11
+ * `GET /health/ready`, in-flight request tracking with `Connection: close`
12
+ * while draining, and SIGTERM/SIGINT graceful shutdown (drain bound
13
+ * `SHUTDOWN_TIMEOUT_MS`, default 25 s) that runs ordered shutdown hooks and
14
+ * exits 0. Pass `lifecycle: { disabled: true }` to opt out.
15
+ *
10
16
  * @example
11
17
  * ```ts
12
18
  * import { createServerFactory } from '@molecule/api-server-default-express'
package/dist/server.d.ts CHANGED
@@ -94,6 +94,30 @@ export interface CreateServerOptions {
94
94
  * resource router.
95
95
  */
96
96
  preApiRouter?: (app: express.Express) => Promise<void> | void;
97
+ /**
98
+ * Process-lifecycle tuning. Omitted, the canonical defaults apply: health
99
+ * routes at `/health/live` and `/health/ready`, a 25 s drain bound
100
+ * (`SHUTDOWN_TIMEOUT_MS`), a 5 s load-balancer flip wait in production
101
+ * (`SHUTDOWN_LB_FLIP_WAIT_MS`), and SIGTERM/SIGINT handlers that exit 0.
102
+ * Set `{ disabled: true }` to keep a legacy process with no lifecycle
103
+ * wiring (health routes and signal handlers are not installed).
104
+ */
105
+ lifecycle?: ServerLifecycleOptions;
106
+ }
107
+ /**
108
+ * Options for the process-lifecycle wiring inside {@link createServerFactory}.
109
+ */
110
+ export interface ServerLifecycleOptions {
111
+ /** Skip the lifecycle wiring entirely. Defaults to `false`. */
112
+ disabled?: boolean;
113
+ /** Maximum time to drain in-flight requests. Defaults to `SHUTDOWN_TIMEOUT_MS` / 25 s. */
114
+ drainTimeoutMs?: number;
115
+ /** Time to keep listening after draining starts. Defaults to `SHUTDOWN_LB_FLIP_WAIT_MS` / 5 s in production. */
116
+ flipWaitMs?: number;
117
+ /** Exit function override (tests). Defaults to `process.exit`. */
118
+ exit?: (code: number) => void;
119
+ /** Signals to handle. Defaults to `['SIGTERM', 'SIGINT']`. */
120
+ signals?: NodeJS.Signals[];
97
121
  }
98
122
  /**
99
123
  * Register a hook to run with the HTTP(S) server right before it listens.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,MAAM,YAAY,CAAA;AAI9B,OAAO,OAAO,MAAM,SAAS,CAAA;AAS7B,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAetE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,mBA6CrC,CAAA;AAED,yCAAyC;AACzC,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,6EAA6E;IAC7E,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;OAGG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAA;KAAE,CAAC,CAAA;IACpD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC9D;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC3C;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC9D;AAYD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACjE,IAAI,CAEN;AAYD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,yBAAyB,EAAE,OAAO,CAAC,cAU/C,CAAA;AA6BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,mBAAmB,GACxB,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAuG5D"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,MAAM,YAAY,CAAA;AAI9B,OAAO,OAAO,MAAM,SAAS,CAAA;AAgB7B,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAetE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,mBA6CrC,CAAA;AAED,yCAAyC;AACzC,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,6EAA6E;IAC7E,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;OAGG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAA;KAAE,CAAC,CAAA;IACpD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC9D;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC3C;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7D;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAA;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gHAAgH;IAChH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;CAC3B;AAmBD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACjE,IAAI,CAEN;AAYD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,yBAAyB,EAAE,OAAO,CAAC,cAU/C,CAAA;AA6BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,mBAAmB,GACxB,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAyJ5D"}
package/dist/server.js CHANGED
@@ -8,6 +8,8 @@ import { logger } from '@molecule/api-logger';
8
8
  import { bodyParser as bodyParserMiddleware } from '@molecule/api-middleware-body-parser';
9
9
  import { cookieParser as cookieParserMiddleware } from '@molecule/api-middleware-cookie-parser';
10
10
  import { cors as corsMiddleware } from '@molecule/api-middleware-cors';
11
+ import { setProvider as setLifecycleProvider } from '@molecule/api-process-lifecycle';
12
+ import { installExpressLifecycle, provider as sharedLifecycleProvider, } from '@molecule/api-process-lifecycle-express';
11
13
  import { buildConfigReport, logConfigReport } from '@molecule/api-secrets';
12
14
  /**
13
15
  * Classify a thrown value for the API error middleware. Returns a {@link TaggedError}
@@ -118,6 +120,12 @@ export const errorMiddleware = (error, req, res, _next) => {
118
120
  res.status(500).json({ error: 'Internal Server Error' });
119
121
  }
120
122
  };
123
+ /**
124
+ * The lifecycle handle of the most recently created server in this process.
125
+ * Repeated `create()` calls (tests, embedded servers) replace it so signal
126
+ * handlers never stack.
127
+ */
128
+ let activeLifecycle = null;
121
129
  /**
122
130
  * Generic server-lifecycle hooks invoked with the real HTTP(S) server once it
123
131
  * has been created in `createServerFactory`, before it starts listening. Lets a
@@ -228,6 +236,21 @@ export function createServerFactory(opts) {
228
236
  captureException(error, { tags: { source: 'unhandledRejection' } });
229
237
  });
230
238
  }
239
+ // Bond the lifecycle provider BEFORE the app's own setup runs, so a bond's
240
+ // setup (database, Redis, queue) can register readiness checks and shutdown
241
+ // hooks at the moment it knows what it owns.
242
+ const lifecycleOptions = opts.lifecycle;
243
+ let lifecycleProvider = null;
244
+ if (!lifecycleOptions?.disabled) {
245
+ activeLifecycle?.uninstall();
246
+ activeLifecycle = null;
247
+ // The provider is the bond package's process-wide singleton, so a
248
+ // generated `bonds/process-lifecycle-express.ts` (which calls
249
+ // `setProvider(provider)` during setupBonds) wires the SAME instance the
250
+ // health routes read and app code registers checks on.
251
+ lifecycleProvider = sharedLifecycleProvider;
252
+ setLifecycleProvider(lifecycleProvider);
253
+ }
231
254
  await opts.runMigrations();
232
255
  await opts.setupBonds();
233
256
  if (opts.postBondsSetup)
@@ -242,6 +265,19 @@ export function createServerFactory(opts) {
242
265
  // /api, /health and error responses all carry them (mirrors the platform
243
266
  // server). Anti-clickjacking + nosniff + referrer baseline; HSTS in prod only.
244
267
  app.use(securityHeadersMiddleware);
268
+ // Process lifecycle: count every request (before any route), mount
269
+ // GET /health/live + /health/ready, and carry the drain state. The signal
270
+ // handlers are installed once the real HTTP server exists (below).
271
+ let lifecycle = null;
272
+ if (lifecycleProvider) {
273
+ lifecycle = installExpressLifecycle({
274
+ app,
275
+ provider: lifecycleProvider,
276
+ wireProvider: false,
277
+ ...(lifecycleOptions ?? {}),
278
+ });
279
+ activeLifecycle = lifecycle;
280
+ }
245
281
  if (opts.preBodyParser) {
246
282
  // Custom-ordering path: mount cors+cookie before custom hooks
247
283
  // (e.g. multipart uploads) which run BEFORE the body parser
@@ -288,8 +324,27 @@ export function createServerFactory(opts) {
288
324
  const httpServer = process.env.HTTPS
289
325
  ? server
290
326
  : http.createServer(app);
327
+ // Outlive every connection pool that sits in front of this server. Node's
328
+ // default keepAliveTimeout is 5000 ms — exactly what Node's global http
329
+ // agent (what a proxy's `http.request` uses with no agent given) keeps an
330
+ // idle socket for. With both sides on the same 5 s timer, an idle
331
+ // connection is closed by the server at the very moment a proxy reuses it,
332
+ // and that one request is lost (measured against the platform front: a
333
+ // dropped reuse answers it a fast ECONNRESET, which costs a retry). A
334
+ // server-side timeout well above the client's makes the CLIENT always let
335
+ // go first, so a reuse can never land on a socket the server is closing.
336
+ // `headersTimeout` must stay greater than `keepAliveTimeout` (Node throws
337
+ // on boot otherwise).
338
+ httpServer.keepAliveTimeout = 65_000;
339
+ httpServer.headersTimeout = 66_000;
291
340
  await runServerCreatedHooks(httpServer);
341
+ // Signal handlers are installed before the listener starts: a SIGTERM that
342
+ // arrives during boot must still drain rather than kill the process mid-boot.
343
+ lifecycle?.attachServer(httpServer);
292
344
  httpServer.listen(port, () => {
345
+ // The boot sequence is complete and the listener owns the port: from here
346
+ // /health/ready answers 200 (unless a critical check fails).
347
+ lifecycle?.markReady();
293
348
  logger.info(`Started ${process.env.NODE_ENV} ${process.env.HTTPS ? 'HTTPS' : 'HTTP'} server on port ${port}.`);
294
349
  // Boot-time configuration report: bonds self-register their secret
295
350
  // definitions at import; log which are configured vs missing (each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-server-default-express",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Default Express server factory: bonds setup, DB migrations, body/cookie/cors middleware, /api router mount, /health endpoint, 401 normalization, optional HTTPS via self-signed pem certs. Extracts 80-line server.ts shipped by 79 fleet apps.",
5
5
  "homepage": "https://www.molecule.dev/packages/api-server-default-express",
6
6
  "type": "module",
@@ -28,6 +28,10 @@
28
28
  "express"
29
29
  ],
30
30
  "license": "Apache-2.0",
31
+ "dependencies": {
32
+ "@molecule/api-process-lifecycle": "1.1.0",
33
+ "@molecule/api-process-lifecycle-express": "1.1.0"
34
+ },
31
35
  "repository": {
32
36
  "type": "git",
33
37
  "url": "https://github.com/molecule-dev/molecule.git",