@foxford/den 4.0.0 → 4.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.mdx CHANGED
@@ -182,6 +182,23 @@ describeDocument(): DocumentContribution {
182
182
  }
183
183
  ```
184
184
 
185
+ Для нескольких процессов одного приложения:
186
+
187
+ ```bash
188
+ WORKERS=2 node build/server.mjs
189
+ ```
190
+
191
+ По умолчанию `WORKERS=1`. Все воркеры слушают общий прикладной порт, primary держит
192
+ один служебный порт с их суммарными метриками. `server.configure` выполняется в каждом
193
+ воркере; память и контейнеры процессов отдельные. Значение задаётся каждому приложению
194
+ и должно быть положительным целым. После обновления Den приложение нужно пересобрать,
195
+ чтобы новая точка входа начала читать `WORKERS`. Переменная работает и в `den dev`,
196
+ но локальное хранилище dev-трасс между воркерами не объединяется.
197
+
198
+ При остановке Den дожидается активных запросов; зависшие воркеры принудительно
199
+ завершаются через 10 секунд. Ошибка запуска или выход любого воркера завершает группу
200
+ с ошибкой, чтобы внешний супервизор мог перезапустить приложение целиком.
201
+
185
202
  Рантайм вокруг дескриптора отдаёт три ручки:
186
203
 
187
204
  | Ручка | Ответ |
@@ -189,7 +206,7 @@ describeDocument(): DocumentContribution {
189
206
  | `QUERY /_render` + `{ url, inputs?, backendHeaders? }` | `200` + ответ `renderApp` телом |
190
207
  | `GET /_render?url=<path+search>` | то же без входных данных хоста |
191
208
  | `GET /_routes` | `200` + массив regex-строк заявки на адреса |
192
- | `GET /healthz`, `GET /livez` на втором порту | `200 ok` |
209
+ | `GET /healthz`, `GET /livez` на втором порту | `200 ok`; в кластере `/livez` проверяет heartbeat воркеров |
193
210
  | `GET /metrics` на втором порту | Prometheus-метрики Node/process и HTTP |
194
211
 
195
212
  `QUERY` — безопасный метод с телом (RFC 9110): адрес и входные данные хоста едут JSON-ом,
@@ -215,10 +232,19 @@ describeDocument(): DocumentContribution {
215
232
  Для совместной разработки объявляются локальные `server.port` и `server.opsPort`;
216
233
  production server.mjs использует production-умолчания.
217
234
 
218
- `/healthz` — простой GET с `200`, `/livez` — `200`, `/metrics` — экспорт полного
235
+ `/healthz` — простой GET с `200`, `/livez` — проверка доступности, `/metrics` — экспорт полного
219
236
  штатного набора Node/process, дополнительных resourceUsage и HTTP-измерений.
220
237
  На основном порту служебные пути отвечают `404`.
221
- Метрики не зависят от DSN Sentry; `instance.close()` закрывает оба сервера.
238
+ Метрики не зависят от DSN Sentry. Метод `close()` результата `await serveAppProcess(...)`
239
+ закрывает всю группу; у прямого `serveApp` прежний `instance.close()` закрывает оба
240
+ сервера одного процесса.
241
+
242
+ Для занятости event loop используйте `nodejs_eventloop_utilization_ratio`:
243
+ в кластере это максимум по воркерам, от 0 до 1. Сопоставляйте его с lag и временем
244
+ ответа. Штатный `nodejs_eventloop_utilization_summary` суммирует quantile воркеров
245
+ и для такого алерта не подходит. `/livez` остаётся зелёным, пока жив хотя бы один
246
+ воркер; `/metrics` при неответившем воркере вернёт `503` через 5 секунд. Ошибки и
247
+ длительность scrape тоже должны попадать в мониторинг.
222
248
 
223
249
  Для своего сервера доступен отдельный подпуть без Fastify:
224
250
 
@@ -345,6 +371,6 @@ export default defineAppConfig({
345
371
  | `@foxford/den/island` | остров: жизненный цикл, шов `renderApp`, порт `ViewAdapter`, очередь клиентских падений (`reportIslandError`) |
346
372
 
347
373
 
348
- | `@foxford/den/serve` | сетевой транспорт: `serveApp`, `appPlugin` |
374
+ | `@foxford/den/serve` | сетевой транспорт: `serveAppProcess`, `serveApp`, `appPlugin` |
349
375
  | `@foxford/den/build` | конфиг приложения и сборка: `defineAppConfig`, `buildApp` |
350
376
  | `@foxford/den/bin` | точка входа команды `den` |
package/bin/den.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
 
4
- var _chunkOC4LQ2CKcjs = require('../chunk-OC4LQ2CK.cjs');
4
+ var _chunkIATA5QZ7cjs = require('../chunk-IATA5QZ7.cjs');
5
5
  require('../chunk-KQAB6C6Q.cjs');
6
6
  require('../chunk-A6ZPAM6Z.cjs');
7
7
 
@@ -70,11 +70,11 @@ function run(argv) {
70
70
  }
71
71
  const target = { base: options["base"], root: (_a = options["root"]) != null ? _a : process.cwd() };
72
72
  if (command === "build") {
73
- yield _chunkOC4LQ2CKcjs.buildApp.call(void 0, target);
73
+ yield _chunkIATA5QZ7cjs.buildApp.call(void 0, target);
74
74
  return;
75
75
  }
76
76
  if (command === "dev") {
77
- yield _chunkOC4LQ2CKcjs.devApp.call(void 0, target);
77
+ yield _chunkIATA5QZ7cjs.devApp.call(void 0, target);
78
78
  return;
79
79
  }
80
80
  fail(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \xAB${command}\xBB`);
package/bin/den.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildApp,
3
3
  devApp
4
- } from "../chunk-FMRORLLE.js";
4
+ } from "../chunk-7FOG2MJV.js";
5
5
  import "../chunk-RG7ZV5BD.js";
6
6
  import "../chunk-TPBI6TOU.js";
7
7
  import {
package/builder/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkOC4LQ2CKcjs = require('../chunk-OC4LQ2CK.cjs');
5
+ var _chunkIATA5QZ7cjs = require('../chunk-IATA5QZ7.cjs');
6
6
  require('../chunk-KQAB6C6Q.cjs');
7
7
  require('../chunk-A6ZPAM6Z.cjs');
8
8
  require('../chunk-T5QVCXVB.cjs');
@@ -10,4 +10,4 @@ require('../chunk-T5QVCXVB.cjs');
10
10
 
11
11
 
12
12
 
13
- exports.buildApp = _chunkOC4LQ2CKcjs.buildApp; exports.defineAppConfig = _chunkOC4LQ2CKcjs.defineAppConfig; exports.devApp = _chunkOC4LQ2CKcjs.devApp;
13
+ exports.buildApp = _chunkIATA5QZ7cjs.buildApp; exports.defineAppConfig = _chunkIATA5QZ7cjs.defineAppConfig; exports.devApp = _chunkIATA5QZ7cjs.devApp;
package/builder/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  buildApp,
3
3
  defineAppConfig,
4
4
  devApp
5
- } from "../chunk-FMRORLLE.js";
5
+ } from "../chunk-7FOG2MJV.js";
6
6
  import "../chunk-RG7ZV5BD.js";
7
7
  import "../chunk-TPBI6TOU.js";
8
8
  import "../chunk-6FNC3XMI.js";
@@ -134,7 +134,7 @@ function generateServerProcess(configPath, base, mode) {
134
134
  ` : "";
135
135
  return `import { fileURLToPath } from 'node:url'
136
136
 
137
- import { loadAppEnv, serveApp } from '@foxford/den/serve'
137
+ import { loadAppEnv, serveAppProcess } from '@foxford/den/serve'
138
138
 
139
139
  import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
140
140
  import config from ${JSON.stringify(configPath)}
@@ -142,11 +142,12 @@ import config from ${JSON.stringify(configPath)}
142
142
  // \u0423\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u2014 \u0434\u043E \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0447\u0442\u0435\u043D\u0438\u044F: \u043F\u043E\u0440\u0442 \u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0438\u0436\u0435
143
143
  loadAppEnv(fileURLToPath(new URL('..', import.meta.url)))
144
144
 
145
- await serveApp(app, {
145
+ await serveAppProcess(app, {
146
146
  ${assets} configure: config.server?.configure,
147
147
  host: process.env.HOST,
148
148
  ops: { host: process.env.OPS_HOST, port: Number(process.env.SERVICE_PORT ?? ${mode === "development" ? "config.server?.opsPort ?? 9090" : "9090"}) },
149
149
  port: Number(process.env.PORT ?? ${mode === "development" ? "config.server?.port ?? " : ""}${String(DEFAULT_PORT)}),
150
+ workers: Number(process.env.WORKERS ?? 1),
150
151
  })
151
152
  `;
152
153
  }
@@ -11,17 +11,30 @@ import { Counter, Gauge as Gauge2, Histogram, Registry } from "@prometheus-io/cl
11
11
 
12
12
  // src/ops/runtime.ts
13
13
  import cluster from "cluster";
14
+ import { performance as performance2 } from "perf_hooks";
14
15
  import { collectDefaultMetrics, Gauge } from "@prometheus-io/client";
15
16
  function collectRuntime(registry, appName) {
16
17
  registry.setDefaultLabels({ app: appName, process_role: cluster.isWorker ? "worker" : "main" });
17
18
  collectDefaultMetrics({ register: registry });
19
+ let previousElu = performance2.eventLoopUtilization();
20
+ new Gauge({
21
+ aggregator: "max",
22
+ collect() {
23
+ const current = performance2.eventLoopUtilization();
24
+ this.set(performance2.eventLoopUtilization(current, previousElu).utilization);
25
+ previousElu = current;
26
+ },
27
+ help: "Event loop utilization since the last scrape; maximum across workers",
28
+ name: "nodejs_eventloop_utilization_ratio",
29
+ registers: [registry]
30
+ });
18
31
  let previousCpu = process.cpuUsage();
19
- let previousTime = performance.now();
32
+ let previousTime = performance2.now();
20
33
  new Gauge({
21
34
  aggregator: "sum",
22
35
  collect() {
23
36
  const currentCpu = process.cpuUsage();
24
- const currentTime = performance.now();
37
+ const currentTime = performance2.now();
25
38
  const elapsed = currentTime - previousTime;
26
39
  this.set(
27
40
  elapsed > 0 ? (currentCpu.user - previousCpu.user + currentCpu.system - previousCpu.system) / (elapsed * 1e3) : 0
@@ -11,17 +11,30 @@ var _client = require('@prometheus-io/client');
11
11
 
12
12
  // src/ops/runtime.ts
13
13
  var _cluster = require('cluster'); var _cluster2 = _interopRequireDefault(_cluster);
14
+ var _perf_hooks = require('perf_hooks');
14
15
 
15
16
  function collectRuntime(registry, appName) {
16
17
  registry.setDefaultLabels({ app: appName, process_role: _cluster2.default.isWorker ? "worker" : "main" });
17
18
  _client.collectDefaultMetrics.call(void 0, { register: registry });
19
+ let previousElu = _perf_hooks.performance.eventLoopUtilization();
20
+ new (0, _client.Gauge)({
21
+ aggregator: "max",
22
+ collect() {
23
+ const current = _perf_hooks.performance.eventLoopUtilization();
24
+ this.set(_perf_hooks.performance.eventLoopUtilization(current, previousElu).utilization);
25
+ previousElu = current;
26
+ },
27
+ help: "Event loop utilization since the last scrape; maximum across workers",
28
+ name: "nodejs_eventloop_utilization_ratio",
29
+ registers: [registry]
30
+ });
18
31
  let previousCpu = process.cpuUsage();
19
- let previousTime = performance.now();
32
+ let previousTime = _perf_hooks.performance.now();
20
33
  new (0, _client.Gauge)({
21
34
  aggregator: "sum",
22
35
  collect() {
23
36
  const currentCpu = process.cpuUsage();
24
- const currentTime = performance.now();
37
+ const currentTime = _perf_hooks.performance.now();
25
38
  const elapsed = currentTime - previousTime;
26
39
  this.set(
27
40
  elapsed > 0 ? (currentCpu.user - previousCpu.user + currentCpu.system - previousCpu.system) / (elapsed * 1e3) : 0
@@ -134,7 +134,7 @@ function generateServerProcess(configPath, base, mode) {
134
134
  ` : "";
135
135
  return `import { fileURLToPath } from 'node:url'
136
136
 
137
- import { loadAppEnv, serveApp } from '@foxford/den/serve'
137
+ import { loadAppEnv, serveAppProcess } from '@foxford/den/serve'
138
138
 
139
139
  import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
140
140
  import config from ${JSON.stringify(configPath)}
@@ -142,11 +142,12 @@ import config from ${JSON.stringify(configPath)}
142
142
  // \u0423\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u2014 \u0434\u043E \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0447\u0442\u0435\u043D\u0438\u044F: \u043F\u043E\u0440\u0442 \u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0438\u0436\u0435
143
143
  loadAppEnv(fileURLToPath(new URL('..', import.meta.url)))
144
144
 
145
- await serveApp(app, {
145
+ await serveAppProcess(app, {
146
146
  ${assets} configure: config.server?.configure,
147
147
  host: process.env.HOST,
148
148
  ops: { host: process.env.OPS_HOST, port: Number(process.env.SERVICE_PORT ?? ${mode === "development" ? "config.server?.opsPort ?? 9090" : "9090"}) },
149
149
  port: Number(process.env.PORT ?? ${mode === "development" ? "config.server?.port ?? " : ""}${String(DEFAULT_PORT)}),
150
+ workers: Number(process.env.WORKERS ?? 1),
150
151
  })
151
152
  `;
152
153
  }
package/ops/index.cjs CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
 
7
7
 
8
- var _chunk2P5TAWRWcjs = require('../chunk-2P5TAWRW.cjs');
8
+ var _chunkHJFBEJT6cjs = require('../chunk-HJFBEJT6.cjs');
9
9
  require('../chunk-T5QVCXVB.cjs');
10
10
 
11
11
 
@@ -14,4 +14,4 @@ require('../chunk-T5QVCXVB.cjs');
14
14
 
15
15
 
16
16
 
17
- exports.ProcessMetrics = _chunk2P5TAWRWcjs.ProcessMetrics; exports.closeHttpServer = _chunk2P5TAWRWcjs.closeHttpServer; exports.clusterMetrics = _chunk2P5TAWRWcjs.clusterMetrics; exports.getProcessMetrics = _chunk2P5TAWRWcjs.getProcessMetrics; exports.isOpsPath = _chunk2P5TAWRWcjs.isOpsPath; exports.startOpsServer = _chunk2P5TAWRWcjs.startOpsServer;
17
+ exports.ProcessMetrics = _chunkHJFBEJT6cjs.ProcessMetrics; exports.closeHttpServer = _chunkHJFBEJT6cjs.closeHttpServer; exports.clusterMetrics = _chunkHJFBEJT6cjs.clusterMetrics; exports.getProcessMetrics = _chunkHJFBEJT6cjs.getProcessMetrics; exports.isOpsPath = _chunkHJFBEJT6cjs.isOpsPath; exports.startOpsServer = _chunkHJFBEJT6cjs.startOpsServer;
package/ops/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  getProcessMetrics,
6
6
  isOpsPath,
7
7
  startOpsServer
8
- } from "../chunk-DUU4MQ4B.js";
8
+ } from "../chunk-A2CYL5LY.js";
9
9
  import "../chunk-6FNC3XMI.js";
10
10
  export {
11
11
  ProcessMetrics,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxford/den",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Den — декларативный метафреймворк Foxford (core runtime)",
5
5
  "keywords": [
6
6
  "foxford",
@@ -95,19 +95,19 @@
95
95
  "adapter.js",
96
96
  "bin",
97
97
  "builder",
98
- "chunk-2P5TAWRW.cjs",
99
98
  "chunk-3RX2OSUL.js",
100
99
  "chunk-6FNC3XMI.js",
101
100
  "chunk-6QV4L6R2.cjs",
101
+ "chunk-7FOG2MJV.js",
102
+ "chunk-A2CYL5LY.js",
102
103
  "chunk-A6ZPAM6Z.cjs",
103
104
  "chunk-C7BM4DGX.cjs",
104
- "chunk-DUU4MQ4B.js",
105
105
  "chunk-E23E2VUC.js",
106
106
  "chunk-E56BKBQZ.js",
107
- "chunk-FMRORLLE.js",
108
107
  "chunk-H5DKRHHQ.cjs",
108
+ "chunk-HJFBEJT6.cjs",
109
+ "chunk-IATA5QZ7.cjs",
109
110
  "chunk-KQAB6C6Q.cjs",
110
- "chunk-OC4LQ2CK.cjs",
111
111
  "chunk-QTWJTYZW.cjs",
112
112
  "chunk-RG7ZV5BD.js",
113
113
  "chunk-T5QVCXVB.cjs",
@@ -143,6 +143,6 @@
143
143
  "view-adapter-CrHh7CjB.d.cts",
144
144
  "view-adapter-DPDvv09J.d.ts"
145
145
  ],
146
- "sha": "46c9d1a",
146
+ "sha": "c941b51",
147
147
  "scripts": {}
148
148
  }
package/serve/index.cjs CHANGED
@@ -17,7 +17,9 @@ var _chunkA6ZPAM6Zcjs = require('../chunk-A6ZPAM6Z.cjs');
17
17
 
18
18
 
19
19
 
20
- var _chunk2P5TAWRWcjs = require('../chunk-2P5TAWRW.cjs');
20
+
21
+ var _chunkHJFBEJT6cjs = require('../chunk-HJFBEJT6.cjs');
22
+
21
23
 
22
24
 
23
25
 
@@ -177,7 +179,203 @@ var appPlugin = (instance, options) => {
177
179
  return Promise.resolve();
178
180
  };
179
181
 
182
+ // src/serve/process.ts
183
+ var _cluster = require('cluster'); var _cluster2 = _interopRequireDefault(_cluster);
184
+
185
+
186
+ // src/serve/worker.ts
187
+
188
+ var ALIVE = "den:app-worker-alive";
189
+ var SIGNALS = ["SIGTERM", "SIGINT"];
190
+ var SHUTDOWN_MS = 1e4;
191
+ function startWorker(appName, start) {
192
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
193
+ const aggregation = _chunkHJFBEJT6cjs.clusterMetrics.call(void 0, );
194
+ const logger2 = _logger.log.getLogger(appName).getLogger("cluster");
195
+ let timer;
196
+ let closing;
197
+ const starting = start();
198
+ const close = () => {
199
+ closing != null ? closing : closing = (() => _chunkT5QVCXVBcjs.__async.call(void 0, null, null, function* () {
200
+ clearInterval(timer);
201
+ try {
202
+ const instance = yield starting;
203
+ yield instance.close();
204
+ } finally {
205
+ try {
206
+ if (process.connected) yield aggregation.shutdown();
207
+ } finally {
208
+ for (const signal of SIGNALS) process.removeListener(signal, shutdown);
209
+ process.removeListener("disconnect", shutdown);
210
+ if (process.connected) process.disconnect();
211
+ }
212
+ }
213
+ }))();
214
+ return closing;
215
+ };
216
+ const shutdown = () => {
217
+ const deadline = setTimeout(() => process.exit(1), SHUTDOWN_MS);
218
+ deadline.unref();
219
+ void close().then(() => process.exit(0)).catch((error) => {
220
+ logger2.error("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0432\u043E\u0440\u043A\u0435\u0440\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C:", error);
221
+ process.exit(1);
222
+ });
223
+ };
224
+ for (const signal of SIGNALS) process.on(signal, shutdown);
225
+ process.once("disconnect", shutdown);
226
+ try {
227
+ yield starting;
228
+ if (!closing) {
229
+ const notify = () => {
230
+ var _a;
231
+ if (process.connected) (_a = process.send) == null ? void 0 : _a.call(process, { type: ALIVE });
232
+ };
233
+ timer = setInterval(notify, 2e3);
234
+ timer.unref();
235
+ notify();
236
+ }
237
+ } catch (error) {
238
+ yield close().catch(() => void 0);
239
+ throw error;
240
+ }
241
+ return { close };
242
+ });
243
+ }
244
+
245
+ // src/serve/process.ts
246
+ var STARTUP_MS = 6e4;
247
+ function startAppProcess(appName, options, start) {
248
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
249
+ var _a;
250
+ const workers = (_a = options.workers) != null ? _a : 1;
251
+ if (!Number.isSafeInteger(workers) || workers < 1) {
252
+ throw new Error("WORKERS must be a positive integer");
253
+ }
254
+ if (workers === 1) {
255
+ const instance = yield start(options);
256
+ return { close: () => instance.close() };
257
+ }
258
+ const metrics = _chunkHJFBEJT6cjs.getProcessMetrics.call(void 0, );
259
+ metrics.startRuntime(appName);
260
+ if (_cluster2.default.isWorker) {
261
+ return startWorker(appName, () => start(_chunkT5QVCXVBcjs.__spreadProps.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, options), { gracefulShutdown: false, ops: false })));
262
+ }
263
+ return startPrimary(appName, options, workers);
264
+ });
265
+ }
266
+ function startPrimary(appName, options, workers) {
267
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
268
+ const aggregation = _chunkHJFBEJT6cjs.clusterMetrics.call(void 0, );
269
+ const logger2 = _logger.log.getLogger(appName).getLogger("cluster");
270
+ const children = /* @__PURE__ */ new Set();
271
+ const alive = /* @__PURE__ */ new Map();
272
+ let ops;
273
+ let closing;
274
+ let started = false;
275
+ let resolveReady = () => void 0;
276
+ let rejectReady = () => void 0;
277
+ const ready = new Promise((resolve, reject) => {
278
+ resolveReady = resolve;
279
+ rejectReady = reject;
280
+ });
281
+ void ready.catch(() => void 0);
282
+ const timeout = setTimeout(() => rejectReady(new Error("Den workers startup timeout")), STARTUP_MS);
283
+ const onMessage = (worker, message) => {
284
+ if (children.has(worker) && typeof message === "object" && message !== null && "type" in message && message.type === ALIVE) {
285
+ alive.set(worker.id, Date.now());
286
+ if (alive.size === workers) resolveReady();
287
+ }
288
+ };
289
+ const close = () => {
290
+ closing != null ? closing : closing = (() => _chunkT5QVCXVBcjs.__async.call(void 0, null, null, function* () {
291
+ clearTimeout(timeout);
292
+ resolveReady();
293
+ _cluster2.default.removeListener("message", onMessage);
294
+ yield Promise.all([...children].map(stopWorker));
295
+ if (ops) yield _chunkHJFBEJT6cjs.closeHttpServer.call(void 0, ops);
296
+ yield aggregation.shutdown();
297
+ for (const signal of SIGNALS) process.removeListener(signal, shutdown);
298
+ }))();
299
+ return closing;
300
+ };
301
+ const shutdown = () => {
302
+ void close().then(() => process.exit()).catch((error) => {
303
+ logger2.error("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043A\u043B\u0430\u0441\u0442\u0435\u0440\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C:", error);
304
+ process.exit(1);
305
+ });
306
+ };
307
+ const failed = (error) => {
308
+ if (closing) return;
309
+ if (!started) {
310
+ rejectReady(error);
311
+ } else {
312
+ logger2.error(error);
313
+ process.exitCode = 1;
314
+ shutdown();
315
+ }
316
+ };
317
+ _cluster2.default.on("message", onMessage);
318
+ if (options.gracefulShutdown !== false) {
319
+ for (const signal of SIGNALS) process.on(signal, shutdown);
320
+ }
321
+ try {
322
+ if (options.ops !== false) {
323
+ const supplied = options.ops;
324
+ ops = yield _chunkHJFBEJT6cjs.startOpsServer.call(void 0, _chunkT5QVCXVBcjs.__spreadProps.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, supplied), {
325
+ collect: aggregation.collect,
326
+ contentType: aggregation.contentType,
327
+ isLive: () => {
328
+ var _a;
329
+ return [...children].some((worker) => {
330
+ var _a2;
331
+ return worker.isConnected() && Date.now() - ((_a2 = alive.get(worker.id)) != null ? _a2 : 0) < 1e4;
332
+ }) && ((_a = supplied == null ? void 0 : supplied.isLive) == null ? void 0 : _a.call(supplied)) !== false;
333
+ }
334
+ }));
335
+ }
336
+ if (closing) {
337
+ if (ops) yield _chunkHJFBEJT6cjs.closeHttpServer.call(void 0, ops);
338
+ yield closing;
339
+ return { close };
340
+ }
341
+ for (let i = 0; i < workers; i++) {
342
+ const worker = _cluster2.default.fork();
343
+ children.add(worker);
344
+ worker.once("error", failed);
345
+ worker.once("exit", (code, signal) => {
346
+ alive.delete(worker.id);
347
+ failed(new Error(`Den worker ${String(worker.process.pid)} exited (${String(code != null ? code : signal)})`));
348
+ });
349
+ }
350
+ yield ready;
351
+ started = true;
352
+ clearTimeout(timeout);
353
+ if (!closing) logger2.info(`\u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u0432\u043E\u0440\u043A\u0435\u0440\u043E\u0432: ${String(workers)}`);
354
+ return { close };
355
+ } catch (error) {
356
+ yield close();
357
+ throw error;
358
+ }
359
+ });
360
+ }
361
+ function stopWorker(worker) {
362
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
363
+ if (worker.isDead()) return;
364
+ yield new Promise((resolve) => {
365
+ const timeout = setTimeout(() => worker.kill("SIGKILL"), SHUTDOWN_MS);
366
+ worker.once("exit", () => {
367
+ clearTimeout(timeout);
368
+ resolve();
369
+ });
370
+ worker.kill("SIGTERM");
371
+ });
372
+ });
373
+ }
374
+
180
375
  // src/serve/index.ts
376
+ function serveAppProcess(app, options = {}) {
377
+ return startAppProcess(app.name, options, (serveOptions) => serveApp(app, serveOptions));
378
+ }
181
379
  var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
182
380
  function serveApp(_0) {
183
381
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, arguments, function* (app, options = {}) {
@@ -185,14 +383,14 @@ function serveApp(_0) {
185
383
  const appLog = (_a = _logger.log.getLogger(app.name)) != null ? _a : _chunkA6ZPAM6Zcjs.logger;
186
384
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
187
385
  const instance = _fastify2.default.call(void 0, { logger: false });
188
- const metrics = _chunk2P5TAWRWcjs.getProcessMetrics.call(void 0, );
386
+ const metrics = _chunkHJFBEJT6cjs.getProcessMetrics.call(void 0, );
189
387
  metrics.startRuntime(app.name);
190
388
  let ops;
191
389
  const shutdown = () => {
192
390
  void instance.close();
193
391
  };
194
392
  instance.addHook("onRequest", (req, reply, done) => {
195
- if (_chunk2P5TAWRWcjs.isOpsPath.call(void 0, req.url)) {
393
+ if (_chunkHJFBEJT6cjs.isOpsPath.call(void 0, req.url)) {
196
394
  void reply.code(404).send();
197
395
  return;
198
396
  }
@@ -207,7 +405,7 @@ function serveApp(_0) {
207
405
  process.removeListener(signal, shutdown);
208
406
  }
209
407
  if (ops) {
210
- yield _chunk2P5TAWRWcjs.closeHttpServer.call(void 0, ops);
408
+ yield _chunkHJFBEJT6cjs.closeHttpServer.call(void 0, ops);
211
409
  }
212
410
  }));
213
411
  yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
@@ -226,7 +424,7 @@ function serveApp(_0) {
226
424
  try {
227
425
  yield (_c = options.configure) == null ? void 0 : _c.call(options, instance);
228
426
  if (options.ops !== false) {
229
- ops = yield _chunk2P5TAWRWcjs.startOpsServer.call(void 0, options.ops);
427
+ ops = yield _chunkHJFBEJT6cjs.startOpsServer.call(void 0, options.ops);
230
428
  }
231
429
  yield instance.listen({ host: (_d = options.host) != null ? _d : "0.0.0.0", port: (_e = options.port) != null ? _e : 8080 });
232
430
  } catch (error) {
@@ -249,4 +447,5 @@ function serveApp(_0) {
249
447
 
250
448
 
251
449
 
252
- exports.appPlugin = appPlugin; exports.loadAppEnv = _chunkKQAB6C6Qcjs.loadAppEnv; exports.registerAssets = registerAssets; exports.serveApp = serveApp;
450
+
451
+ exports.appPlugin = appPlugin; exports.loadAppEnv = _chunkKQAB6C6Qcjs.loadAppEnv; exports.registerAssets = registerAssets; exports.serveApp = serveApp; exports.serveAppProcess = serveAppProcess;
package/serve/index.d.cts CHANGED
@@ -44,6 +44,14 @@ interface AppPluginOptions {
44
44
  */
45
45
  declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
46
46
 
47
+ interface AppProcess {
48
+ close: () => Promise<void>;
49
+ }
50
+ interface ServeAppProcessOptions extends ServeAppOptions {
51
+ /** Число процессов приложения; по умолчанию 1. Каждый воркер имеет свой контейнер. */
52
+ workers?: number;
53
+ }
54
+
47
55
  /**
48
56
  * Кладёт умолчания из `<root>/.env` в окружение процесса.
49
57
  *
@@ -53,6 +61,8 @@ declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
53
61
  */
54
62
  declare function loadAppEnv(root: string): void;
55
63
 
64
+ /** Точка входа самостоятельного процесса: один инстанс либо cluster с общим портом. */
65
+ declare function serveAppProcess(app: ServableApp, options?: ServeAppProcessOptions): Promise<AppProcess>;
56
66
  /** Настройки инстанса. */
57
67
  interface ServeAppOptions {
58
68
  /** Порт; `0` — свободный, фактический смотри в `server.addresses()`. */
@@ -93,4 +103,4 @@ interface ServeAppOptions {
93
103
  */
94
104
  declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
95
105
 
96
- export { type AppAssetsSource, type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, loadAppEnv, registerAssets, serveApp };
106
+ export { type AppAssetsSource, type AppPluginOptions, type AppProcess, type ServableApp, type ServeAppOptions, type ServeAppProcessOptions, appPlugin, loadAppEnv, registerAssets, serveApp, serveAppProcess };
package/serve/index.d.ts CHANGED
@@ -44,6 +44,14 @@ interface AppPluginOptions {
44
44
  */
45
45
  declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
46
46
 
47
+ interface AppProcess {
48
+ close: () => Promise<void>;
49
+ }
50
+ interface ServeAppProcessOptions extends ServeAppOptions {
51
+ /** Число процессов приложения; по умолчанию 1. Каждый воркер имеет свой контейнер. */
52
+ workers?: number;
53
+ }
54
+
47
55
  /**
48
56
  * Кладёт умолчания из `<root>/.env` в окружение процесса.
49
57
  *
@@ -53,6 +61,8 @@ declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
53
61
  */
54
62
  declare function loadAppEnv(root: string): void;
55
63
 
64
+ /** Точка входа самостоятельного процесса: один инстанс либо cluster с общим портом. */
65
+ declare function serveAppProcess(app: ServableApp, options?: ServeAppProcessOptions): Promise<AppProcess>;
56
66
  /** Настройки инстанса. */
57
67
  interface ServeAppOptions {
58
68
  /** Порт; `0` — свободный, фактический смотри в `server.addresses()`. */
@@ -93,4 +103,4 @@ interface ServeAppOptions {
93
103
  */
94
104
  declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
95
105
 
96
- export { type AppAssetsSource, type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, loadAppEnv, registerAssets, serveApp };
106
+ export { type AppAssetsSource, type AppPluginOptions, type AppProcess, type ServableApp, type ServeAppOptions, type ServeAppProcessOptions, appPlugin, loadAppEnv, registerAssets, serveApp, serveAppProcess };
package/serve/index.js CHANGED
@@ -14,18 +14,20 @@ import {
14
14
  } from "../chunk-TPBI6TOU.js";
15
15
  import {
16
16
  closeHttpServer,
17
+ clusterMetrics,
17
18
  getProcessMetrics,
18
19
  isOpsPath,
19
20
  startOpsServer
20
- } from "../chunk-DUU4MQ4B.js";
21
+ } from "../chunk-A2CYL5LY.js";
21
22
  import {
22
23
  __async,
24
+ __spreadProps,
23
25
  __spreadValues
24
26
  } from "../chunk-6FNC3XMI.js";
25
27
 
26
28
  // src/serve/index.ts
27
29
  import Fastify from "fastify";
28
- import { log as log2 } from "@foxford/logger";
30
+ import { log as log4 } from "@foxford/logger";
29
31
 
30
32
  // src/serve/assets.ts
31
33
  import { createReadStream, existsSync, statSync } from "fs";
@@ -177,12 +179,208 @@ var appPlugin = (instance, options) => {
177
179
  return Promise.resolve();
178
180
  };
179
181
 
182
+ // src/serve/process.ts
183
+ import cluster from "cluster";
184
+ import { log as log3 } from "@foxford/logger";
185
+
186
+ // src/serve/worker.ts
187
+ import { log as log2 } from "@foxford/logger";
188
+ var ALIVE = "den:app-worker-alive";
189
+ var SIGNALS = ["SIGTERM", "SIGINT"];
190
+ var SHUTDOWN_MS = 1e4;
191
+ function startWorker(appName, start) {
192
+ return __async(this, null, function* () {
193
+ const aggregation = clusterMetrics();
194
+ const logger2 = log2.getLogger(appName).getLogger("cluster");
195
+ let timer;
196
+ let closing;
197
+ const starting = start();
198
+ const close = () => {
199
+ closing != null ? closing : closing = (() => __async(null, null, function* () {
200
+ clearInterval(timer);
201
+ try {
202
+ const instance = yield starting;
203
+ yield instance.close();
204
+ } finally {
205
+ try {
206
+ if (process.connected) yield aggregation.shutdown();
207
+ } finally {
208
+ for (const signal of SIGNALS) process.removeListener(signal, shutdown);
209
+ process.removeListener("disconnect", shutdown);
210
+ if (process.connected) process.disconnect();
211
+ }
212
+ }
213
+ }))();
214
+ return closing;
215
+ };
216
+ const shutdown = () => {
217
+ const deadline = setTimeout(() => process.exit(1), SHUTDOWN_MS);
218
+ deadline.unref();
219
+ void close().then(() => process.exit(0)).catch((error) => {
220
+ logger2.error("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0432\u043E\u0440\u043A\u0435\u0440\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C:", error);
221
+ process.exit(1);
222
+ });
223
+ };
224
+ for (const signal of SIGNALS) process.on(signal, shutdown);
225
+ process.once("disconnect", shutdown);
226
+ try {
227
+ yield starting;
228
+ if (!closing) {
229
+ const notify = () => {
230
+ var _a;
231
+ if (process.connected) (_a = process.send) == null ? void 0 : _a.call(process, { type: ALIVE });
232
+ };
233
+ timer = setInterval(notify, 2e3);
234
+ timer.unref();
235
+ notify();
236
+ }
237
+ } catch (error) {
238
+ yield close().catch(() => void 0);
239
+ throw error;
240
+ }
241
+ return { close };
242
+ });
243
+ }
244
+
245
+ // src/serve/process.ts
246
+ var STARTUP_MS = 6e4;
247
+ function startAppProcess(appName, options, start) {
248
+ return __async(this, null, function* () {
249
+ var _a;
250
+ const workers = (_a = options.workers) != null ? _a : 1;
251
+ if (!Number.isSafeInteger(workers) || workers < 1) {
252
+ throw new Error("WORKERS must be a positive integer");
253
+ }
254
+ if (workers === 1) {
255
+ const instance = yield start(options);
256
+ return { close: () => instance.close() };
257
+ }
258
+ const metrics = getProcessMetrics();
259
+ metrics.startRuntime(appName);
260
+ if (cluster.isWorker) {
261
+ return startWorker(appName, () => start(__spreadProps(__spreadValues({}, options), { gracefulShutdown: false, ops: false })));
262
+ }
263
+ return startPrimary(appName, options, workers);
264
+ });
265
+ }
266
+ function startPrimary(appName, options, workers) {
267
+ return __async(this, null, function* () {
268
+ const aggregation = clusterMetrics();
269
+ const logger2 = log3.getLogger(appName).getLogger("cluster");
270
+ const children = /* @__PURE__ */ new Set();
271
+ const alive = /* @__PURE__ */ new Map();
272
+ let ops;
273
+ let closing;
274
+ let started = false;
275
+ let resolveReady = () => void 0;
276
+ let rejectReady = () => void 0;
277
+ const ready = new Promise((resolve, reject) => {
278
+ resolveReady = resolve;
279
+ rejectReady = reject;
280
+ });
281
+ void ready.catch(() => void 0);
282
+ const timeout = setTimeout(() => rejectReady(new Error("Den workers startup timeout")), STARTUP_MS);
283
+ const onMessage = (worker, message) => {
284
+ if (children.has(worker) && typeof message === "object" && message !== null && "type" in message && message.type === ALIVE) {
285
+ alive.set(worker.id, Date.now());
286
+ if (alive.size === workers) resolveReady();
287
+ }
288
+ };
289
+ const close = () => {
290
+ closing != null ? closing : closing = (() => __async(null, null, function* () {
291
+ clearTimeout(timeout);
292
+ resolveReady();
293
+ cluster.removeListener("message", onMessage);
294
+ yield Promise.all([...children].map(stopWorker));
295
+ if (ops) yield closeHttpServer(ops);
296
+ yield aggregation.shutdown();
297
+ for (const signal of SIGNALS) process.removeListener(signal, shutdown);
298
+ }))();
299
+ return closing;
300
+ };
301
+ const shutdown = () => {
302
+ void close().then(() => process.exit()).catch((error) => {
303
+ logger2.error("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043A\u043B\u0430\u0441\u0442\u0435\u0440\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C:", error);
304
+ process.exit(1);
305
+ });
306
+ };
307
+ const failed = (error) => {
308
+ if (closing) return;
309
+ if (!started) {
310
+ rejectReady(error);
311
+ } else {
312
+ logger2.error(error);
313
+ process.exitCode = 1;
314
+ shutdown();
315
+ }
316
+ };
317
+ cluster.on("message", onMessage);
318
+ if (options.gracefulShutdown !== false) {
319
+ for (const signal of SIGNALS) process.on(signal, shutdown);
320
+ }
321
+ try {
322
+ if (options.ops !== false) {
323
+ const supplied = options.ops;
324
+ ops = yield startOpsServer(__spreadProps(__spreadValues({}, supplied), {
325
+ collect: aggregation.collect,
326
+ contentType: aggregation.contentType,
327
+ isLive: () => {
328
+ var _a;
329
+ return [...children].some((worker) => {
330
+ var _a2;
331
+ return worker.isConnected() && Date.now() - ((_a2 = alive.get(worker.id)) != null ? _a2 : 0) < 1e4;
332
+ }) && ((_a = supplied == null ? void 0 : supplied.isLive) == null ? void 0 : _a.call(supplied)) !== false;
333
+ }
334
+ }));
335
+ }
336
+ if (closing) {
337
+ if (ops) yield closeHttpServer(ops);
338
+ yield closing;
339
+ return { close };
340
+ }
341
+ for (let i = 0; i < workers; i++) {
342
+ const worker = cluster.fork();
343
+ children.add(worker);
344
+ worker.once("error", failed);
345
+ worker.once("exit", (code, signal) => {
346
+ alive.delete(worker.id);
347
+ failed(new Error(`Den worker ${String(worker.process.pid)} exited (${String(code != null ? code : signal)})`));
348
+ });
349
+ }
350
+ yield ready;
351
+ started = true;
352
+ clearTimeout(timeout);
353
+ if (!closing) logger2.info(`\u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u0432\u043E\u0440\u043A\u0435\u0440\u043E\u0432: ${String(workers)}`);
354
+ return { close };
355
+ } catch (error) {
356
+ yield close();
357
+ throw error;
358
+ }
359
+ });
360
+ }
361
+ function stopWorker(worker) {
362
+ return __async(this, null, function* () {
363
+ if (worker.isDead()) return;
364
+ yield new Promise((resolve) => {
365
+ const timeout = setTimeout(() => worker.kill("SIGKILL"), SHUTDOWN_MS);
366
+ worker.once("exit", () => {
367
+ clearTimeout(timeout);
368
+ resolve();
369
+ });
370
+ worker.kill("SIGTERM");
371
+ });
372
+ });
373
+ }
374
+
180
375
  // src/serve/index.ts
376
+ function serveAppProcess(app, options = {}) {
377
+ return startAppProcess(app.name, options, (serveOptions) => serveApp(app, serveOptions));
378
+ }
181
379
  var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
182
380
  function serveApp(_0) {
183
381
  return __async(this, arguments, function* (app, options = {}) {
184
382
  var _a, _b, _c, _d, _e;
185
- const appLog = (_a = log2.getLogger(app.name)) != null ? _a : logger;
383
+ const appLog = (_a = log4.getLogger(app.name)) != null ? _a : logger;
186
384
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
187
385
  const instance = Fastify({ logger: false });
188
386
  const metrics = getProcessMetrics();
@@ -248,5 +446,6 @@ export {
248
446
  appPlugin,
249
447
  loadAppEnv,
250
448
  registerAssets,
251
- serveApp
449
+ serveApp,
450
+ serveAppProcess
252
451
  };