@things-factory/shell 10.0.10 → 10.0.13

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.
@@ -15,8 +15,83 @@ exports.getAllPoolStats = getAllPoolStats;
15
15
  exports.cleanupAllPools = cleanupAllPools;
16
16
  exports.resetPool = resetPool;
17
17
  exports.removePool = removePool;
18
+ const env_1 = require("@things-factory/env");
18
19
  const pool_manager_1 = require("./pool-manager");
19
20
  const config_1 = require("./config");
21
+ /*
22
+ * ── 프로세스가 끝날 때 브라우저를 내린다 (2026-08-22) ────────────────────────
23
+ *
24
+ * 실측: `chrome-headless-shell` 프로세스 **549개가 3.4GB** 를 붙잡고 있었다. 나이 그룹이 90개 —
25
+ * 서버를 90번 올리는 동안 매번 12개가 남았다. 부모는 전부 launchd 였다(고아). 남은 시스템 여유
26
+ * 페이지가 4,931(약 19MB)이었고, 압축된 페이지가 1.3GB 였다.
27
+ *
28
+ * 자리가 **둘** 비어 있었다.
29
+ *
30
+ * ① `cleanupAllPools()` 은 있는데 **아무도 부르지 않았다.** 주석에 「보통 애플리케이션 종료 때
31
+ * 부른다」고 적혀 있었고, 저장소 전체에 호출처가 없었다. 정리 함수가 있다는 것이 정리가 된다는
32
+ * 뜻은 아니다.
33
+ * ② puppeteer 의 기본 신호 핸들러를 **꺼 두었다**(`handleSIGINT/SIGTERM/SIGHUP: false`,
34
+ * `browser-factory.ts`). 그것을 끈 것 자체는 뜻이 있다 — 라이브러리가 우리 프로세스의 신호 처리를
35
+ * 가로채면 정상 종료 순서를 우리가 정할 수 없다. 그런데 **대신 내리는 쪽을 두지 않았다.**
36
+ *
37
+ * 그래서 여기서 한 번만 건다. 풀을 만드는 길이 이 모듈이므로, 내리는 책임도 이 모듈에 있다.
38
+ *
39
+ * ── 무엇을 하지 않는가 ─────────────────────────────────────────────────────
40
+ * **`process.exit` 를 부르지 않는다.** 신호를 받았을 때 우리가 프로세스를 끝내면 다른 종료 절차(연결
41
+ * 종료·저널 flush)를 가로챈다. 우리는 우리 것만 내리고, 핸들러를 **한 번만** 쓰고 스스로 뗀다
42
+ * (`once`) — 그러지 않으면 두 번째 신호에 아무 일도 못 한다.
43
+ *
44
+ * `SIGKILL` 은 잡을 수 없다. `kill -9` 로 내린 프로세스의 브라우저는 여전히 남는다 — 그것은 이
45
+ * 고침의 한계이고, 지어내지 않고 적는다.
46
+ */
47
+ let teardownArmed = false;
48
+ function armTeardown() {
49
+ if (teardownArmed)
50
+ return;
51
+ teardownArmed = true;
52
+ /* 정상 종료 — 이벤트 루프가 빌 때. 여기서는 await 가 실제로 기다려진다. */
53
+ process.once('beforeExit', () => {
54
+ void pool_manager_1.HeadlessPoolManager.cleanupAll();
55
+ });
56
+ /*
57
+ * 신호 종료 — 내리고 나서 **같은 신호를 스스로에게 다시 보낸다.** 핸들러를 뗀 뒤 보내므로 이번에는
58
+ * 기본 거동(종료)이 일어난다. `process.exit` 보다 이 편이 낫다: 종료 코드가 신호의 것으로 남고,
59
+ * 다른 핸들러가 자기 몫을 먼저 끝낼 자리가 있다.
60
+ *
61
+ * ── **정리를 기다리다 종료를 막지 않는다** (2026-08-22 실측) ─────────────────
62
+ * 처음에는 `cleanupAll()` 을 끝까지 기다렸다. 그런데 그것이 매달렸다: 로그에 「Cleaning up all
63
+ * headless pools (3 pools)」는 찍히고 완료 줄이 없었고, **프로세스가 SIGTERM 을 받고도 13분 넘게
64
+ * 살아 있었다.** 풀 정리(`drain`)가 돌아오지 않으면 그렇게 된다.
65
+ *
66
+ * 종료를 막는 정리는 정리가 아니다 — 사람이 `kill -9` 를 쓰게 되고, 그러면 애초에 이 훅이 없는 것과
67
+ * 같아진다(그리고 브라우저가 고아로 남는다). 그래서 **상한을 둔다**: 그 안에 끝나면 깨끗하게 끝나고,
68
+ * 넘으면 그 사실을 말하고 종료로 넘어간다. 남은 브라우저는 다음 정리가 거둔다.
69
+ */
70
+ const TEARDOWN_BUDGET_MS = 5000;
71
+ for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
72
+ process.once(signal, () => {
73
+ let settled = false;
74
+ const proceed = (why) => {
75
+ if (settled)
76
+ return;
77
+ settled = true;
78
+ if (why)
79
+ env_1.logger.warn(`[headless-pool] ${why} — 종료를 계속한다(남은 브라우저는 다음 정리가 거둔다)`);
80
+ process.kill(process.pid, signal);
81
+ };
82
+ /* 상한을 넘겨도 종료가 막히지 않게 — 타이머가 프로세스를 붙잡지 않도록 `unref`. */
83
+ const budget = setTimeout(() => proceed(`풀 정리가 ${TEARDOWN_BUDGET_MS}ms 안에 끝나지 않았다`), TEARDOWN_BUDGET_MS);
84
+ if (typeof budget.unref === 'function')
85
+ budget.unref();
86
+ pool_manager_1.HeadlessPoolManager.cleanupAll()
87
+ .catch(err => env_1.logger.warn('[headless-pool] 정리 중 오류', err))
88
+ .finally(() => {
89
+ clearTimeout(budget);
90
+ proceed();
91
+ });
92
+ });
93
+ }
94
+ }
20
95
  // Re-export types and utilities
21
96
  var config_2 = require("./config");
22
97
  Object.defineProperty(exports, "HEADLESS_POOL_ARGUMENT_SETS", { enumerable: true, get: function () { return config_2.HEADLESS_POOL_ARGUMENT_SETS; } });
@@ -50,6 +125,8 @@ Object.defineProperty(exports, "BrowserFactory", { enumerable: true, get: functi
50
125
  * })
51
126
  */
52
127
  function createHeadlessPool(name, config = {}) {
128
+ /* 브라우저를 띄우는 프로세스에만 종료 훅을 건다 — 풀을 만들지 않는 프로세스에는 달지 않는다. */
129
+ armTeardown();
53
130
  return pool_manager_1.HeadlessPoolManager.createPool(name, config);
54
131
  }
55
132
  /**
@@ -71,6 +148,7 @@ function getHeadlessPool(name) {
71
148
  * @returns HeadlessPool instance
72
149
  */
73
150
  function getOrCreateHeadlessPool(name, config = {}) {
151
+ armTeardown();
74
152
  if (pool_manager_1.HeadlessPoolManager.hasPool(name)) {
75
153
  return pool_manager_1.HeadlessPoolManager.getPool(name);
76
154
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/utils/headless-pool/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAiCH,gDAEC;AASD,0CAEC;AAUD,0DAKC;AAQD,0CAEC;AAOD,0CAEC;AAMD,0CAEC;AAOD,8BAEC;AAOD,gCAEC;AAxGD,iDAAkE;AAClE,qCAA0E;AAE1E,gCAAgC;AAChC,mCAA0E;AAA7C,qHAAA,2BAA2B,OAAA;AACxD,+CAAkE;AAAzD,4GAAA,YAAY,OAAA;AAAE,mHAAA,mBAAmB,OAAA;AAC1C,2CAAwC;AAA/B,uGAAA,SAAS,OAAA;AAClB,qDAAkD;AAAzC,iHAAA,cAAc,OAAA;AAEvB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,SAAgB,kBAAkB,CAAC,IAAY,EAAE,SAA6B,EAAE;IAC9E,OAAO,kCAAmB,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACrD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,IAAY;IAC1C,OAAO,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,uBAAuB,CAAC,IAAY,EAAE,SAA6B,EAAE;IACnF,IAAI,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,OAAO,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,kCAAmB,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACrD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,IAAY;IAC1C,OAAO,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe;IAC7B,OAAO,kCAAmB,CAAC,eAAe,EAAE,CAAA;AAC9C,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe;IAC7B,OAAO,kCAAmB,CAAC,UAAU,EAAE,CAAA;AACzC,CAAC;AAED;;;;GAIG;AACH,SAAgB,SAAS,CAAC,IAAY;IACpC,OAAO,kCAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;AAC5C,CAAC;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAC,IAAY;IACrC,OAAO,kCAAmB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7C,CAAC;AAGD,iBAAiB;AACjB,kBAAe;IACb,kBAAkB;IAClB,eAAe;IACf,uBAAuB;IACvB,eAAe;IACf,eAAe;IACf,eAAe;IACf,SAAS;IACT,UAAU;IACV,2BAA2B,EAA3B,oCAA2B;CAC5B,CAAA","sourcesContent":["/**\n * Unified Headless Pool System for Things Factory\n * \n * This module provides a centralized headless browser pool system\n * that can be used across all Things Factory packages.\n */\n\nimport { HeadlessPoolManager, HeadlessPool } from './pool-manager'\nimport { HeadlessPoolConfig, HEADLESS_POOL_ARGUMENT_SETS } from './config'\n\n// Re-export types and utilities\nexport { HeadlessPoolConfig, HEADLESS_POOL_ARGUMENT_SETS } from './config'\nexport { HeadlessPool, HeadlessPoolManager } from './pool-manager'\nexport { PoolStats } from './pool-stats'\nexport { BrowserFactory } from './browser-factory'\n\n/**\n * Create a new headless browser pool\n * \n * @param name - Unique name for the pool\n * @param config - Pool configuration options\n * @returns HeadlessPool instance\n * \n * @example\n * // Create integration pool with advanced features\n * const integrationPool = createHeadlessPool('integration', {\n * min: 2, max: 20,\n * args: [...HEADLESS_POOL_ARGUMENT_SETS.basic, ...HEADLESS_POOL_ARGUMENT_SETS.keychain_safe],\n * enableStats: true,\n * enableRecovery: true\n * })\n * \n * @example \n * // Create simple pool\n * const myPool = createHeadlessPool('my-pool', {\n * min: 1, max: 5\n * })\n */\nexport function createHeadlessPool(name: string, config: HeadlessPoolConfig = {}): HeadlessPool {\n return HeadlessPoolManager.createPool(name, config)\n}\n\n/**\n * Get existing headless browser pool by name\n * \n * @param name - Pool name\n * @returns HeadlessPool instance\n * @throws Error if pool doesn't exist\n */\nexport function getHeadlessPool(name: string): HeadlessPool {\n return HeadlessPoolManager.getPool(name)\n}\n\n/**\n * Get or create headless browser pool\n * If pool exists, returns it. If not, creates with provided config.\n * \n * @param name - Pool name\n * @param config - Pool configuration (used only if creating new pool)\n * @returns HeadlessPool instance\n */\nexport function getOrCreateHeadlessPool(name: string, config: HeadlessPoolConfig = {}): HeadlessPool {\n if (HeadlessPoolManager.hasPool(name)) {\n return HeadlessPoolManager.getPool(name)\n }\n return HeadlessPoolManager.createPool(name, config)\n}\n\n/**\n * Check if pool exists\n * \n * @param name - Pool name\n * @returns boolean\n */\nexport function hasHeadlessPool(name: string): boolean {\n return HeadlessPoolManager.hasPool(name)\n}\n\n/**\n * Get statistics for all pools\n * \n * @returns Object containing stats for all pools\n */\nexport function getAllPoolStats() {\n return HeadlessPoolManager.getAllPoolStats()\n}\n\n/**\n * Clean up all headless browser pools\n * Typically called during application shutdown\n */\nexport function cleanupAllPools(): Promise<void> {\n return HeadlessPoolManager.cleanupAll()\n}\n\n/**\n * Reset a specific pool (cleanup and recreate)\n * \n * @param name - Pool name\n */\nexport function resetPool(name: string): Promise<void> {\n return HeadlessPoolManager.resetPool(name)\n}\n\n/**\n * Remove pool from registry\n * \n * @param name - Pool name\n */\nexport function removePool(name: string): void {\n return HeadlessPoolManager.removePool(name)\n}\n\n\n// Default export\nexport default {\n createHeadlessPool,\n getHeadlessPool,\n getOrCreateHeadlessPool,\n hasHeadlessPool,\n getAllPoolStats,\n cleanupAllPools,\n resetPool,\n removePool,\n HEADLESS_POOL_ARGUMENT_SETS\n}"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/utils/headless-pool/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AA8GH,gDAIC;AASD,0CAEC;AAUD,0DAMC;AAQD,0CAEC;AAOD,0CAEC;AAMD,0CAEC;AAOD,8BAEC;AAOD,gCAEC;AAxLD,6CAA4C;AAC5C,iDAAkE;AAClE,qCAA0E;AAE1E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,IAAI,aAAa,GAAG,KAAK,CAAA;AAEzB,SAAS,WAAW;IAClB,IAAI,aAAa;QAAE,OAAM;IACzB,aAAa,GAAG,IAAI,CAAA;IAEpB,kDAAkD;IAClD,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,EAAE;QAC9B,KAAK,kCAAmB,CAAC,UAAU,EAAE,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF;;;;;;;;;;;;;OAaG;IACH,MAAM,kBAAkB,GAAG,IAAI,CAAA;IAE/B,KAAK,MAAM,MAAM,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAU,EAAE,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;YACxB,IAAI,OAAO,GAAG,KAAK,CAAA;YACnB,MAAM,OAAO,GAAG,CAAC,GAAY,EAAE,EAAE;gBAC/B,IAAI,OAAO;oBAAE,OAAM;gBACnB,OAAO,GAAG,IAAI,CAAA;gBACd,IAAI,GAAG;oBAAE,YAAM,CAAC,IAAI,CAAC,mBAAmB,GAAG,kCAAkC,CAAC,CAAA;gBAC9E,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;YACnC,CAAC,CAAA;YACD,sDAAsD;YACtD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,kBAAkB,eAAe,CAAC,EAAE,kBAAkB,CAAC,CAAA;YACxG,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,UAAU;gBAAE,MAAM,CAAC,KAAK,EAAE,CAAA;YAEtD,kCAAmB,CAAC,UAAU,EAAE;iBAC7B,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,YAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;iBACzD,OAAO,CAAC,GAAG,EAAE;gBACZ,YAAY,CAAC,MAAM,CAAC,CAAA;gBACpB,OAAO,EAAE,CAAA;YACX,CAAC,CAAC,CAAA;QACN,CAAC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,gCAAgC;AAChC,mCAA0E;AAA7C,qHAAA,2BAA2B,OAAA;AACxD,+CAAkE;AAAzD,4GAAA,YAAY,OAAA;AAAE,mHAAA,mBAAmB,OAAA;AAC1C,2CAAwC;AAA/B,uGAAA,SAAS,OAAA;AAClB,qDAAkD;AAAzC,iHAAA,cAAc,OAAA;AAEvB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,SAAgB,kBAAkB,CAAC,IAAY,EAAE,SAA6B,EAAE;IAC9E,0DAA0D;IAC1D,WAAW,EAAE,CAAA;IACb,OAAO,kCAAmB,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACrD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,IAAY;IAC1C,OAAO,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,uBAAuB,CAAC,IAAY,EAAE,SAA6B,EAAE;IACnF,WAAW,EAAE,CAAA;IACb,IAAI,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,OAAO,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,kCAAmB,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AACrD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,IAAY;IAC1C,OAAO,kCAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe;IAC7B,OAAO,kCAAmB,CAAC,eAAe,EAAE,CAAA;AAC9C,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe;IAC7B,OAAO,kCAAmB,CAAC,UAAU,EAAE,CAAA;AACzC,CAAC;AAED;;;;GAIG;AACH,SAAgB,SAAS,CAAC,IAAY;IACpC,OAAO,kCAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;AAC5C,CAAC;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAC,IAAY;IACrC,OAAO,kCAAmB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7C,CAAC;AAGD,iBAAiB;AACjB,kBAAe;IACb,kBAAkB;IAClB,eAAe;IACf,uBAAuB;IACvB,eAAe;IACf,eAAe;IACf,eAAe;IACf,SAAS;IACT,UAAU;IACV,2BAA2B,EAA3B,oCAA2B;CAC5B,CAAA","sourcesContent":["/**\n * Unified Headless Pool System for Things Factory\n * \n * This module provides a centralized headless browser pool system\n * that can be used across all Things Factory packages.\n */\n\nimport { logger } from '@things-factory/env'\nimport { HeadlessPoolManager, HeadlessPool } from './pool-manager'\nimport { HeadlessPoolConfig, HEADLESS_POOL_ARGUMENT_SETS } from './config'\n\n/*\n * ── 프로세스가 끝날 때 브라우저를 내린다 (2026-08-22) ────────────────────────\n *\n * 실측: `chrome-headless-shell` 프로세스 **549개가 3.4GB** 를 붙잡고 있었다. 나이 그룹이 90개 —\n * 서버를 90번 올리는 동안 매번 12개가 남았다. 부모는 전부 launchd 였다(고아). 남은 시스템 여유\n * 페이지가 4,931(약 19MB)이었고, 압축된 페이지가 1.3GB 였다.\n *\n * 자리가 **둘** 비어 있었다.\n *\n * ① `cleanupAllPools()` 은 있는데 **아무도 부르지 않았다.** 주석에 「보통 애플리케이션 종료 때\n * 부른다」고 적혀 있었고, 저장소 전체에 호출처가 없었다. 정리 함수가 있다는 것이 정리가 된다는\n * 뜻은 아니다.\n * ② puppeteer 의 기본 신호 핸들러를 **꺼 두었다**(`handleSIGINT/SIGTERM/SIGHUP: false`,\n * `browser-factory.ts`). 그것을 끈 것 자체는 뜻이 있다 — 라이브러리가 우리 프로세스의 신호 처리를\n * 가로채면 정상 종료 순서를 우리가 정할 수 없다. 그런데 **대신 내리는 쪽을 두지 않았다.**\n *\n * 그래서 여기서 한 번만 건다. 풀을 만드는 길이 이 모듈이므로, 내리는 책임도 이 모듈에 있다.\n *\n * ── 무엇을 하지 않는가 ─────────────────────────────────────────────────────\n * **`process.exit` 를 부르지 않는다.** 신호를 받았을 때 우리가 프로세스를 끝내면 다른 종료 절차(연결\n * 종료·저널 flush)를 가로챈다. 우리는 우리 것만 내리고, 핸들러를 **한 번만** 쓰고 스스로 뗀다\n * (`once`) — 그러지 않으면 두 번째 신호에 아무 일도 못 한다.\n *\n * `SIGKILL` 은 잡을 수 없다. `kill -9` 로 내린 프로세스의 브라우저는 여전히 남는다 — 그것은 이\n * 고침의 한계이고, 지어내지 않고 적는다.\n */\nlet teardownArmed = false\n\nfunction armTeardown(): void {\n if (teardownArmed) return\n teardownArmed = true\n\n /* 정상 종료 — 이벤트 루프가 빌 때. 여기서는 await 가 실제로 기다려진다. */\n process.once('beforeExit', () => {\n void HeadlessPoolManager.cleanupAll()\n })\n\n /*\n * 신호 종료 — 내리고 나서 **같은 신호를 스스로에게 다시 보낸다.** 핸들러를 뗀 뒤 보내므로 이번에는\n * 기본 거동(종료)이 일어난다. `process.exit` 보다 이 편이 낫다: 종료 코드가 신호의 것으로 남고,\n * 다른 핸들러가 자기 몫을 먼저 끝낼 자리가 있다.\n *\n * ── **정리를 기다리다 종료를 막지 않는다** (2026-08-22 실측) ─────────────────\n * 처음에는 `cleanupAll()` 을 끝까지 기다렸다. 그런데 그것이 매달렸다: 로그에 「Cleaning up all\n * headless pools (3 pools)」는 찍히고 완료 줄이 없었고, **프로세스가 SIGTERM 을 받고도 13분 넘게\n * 살아 있었다.** 풀 정리(`drain`)가 돌아오지 않으면 그렇게 된다.\n *\n * 종료를 막는 정리는 정리가 아니다 — 사람이 `kill -9` 를 쓰게 되고, 그러면 애초에 이 훅이 없는 것과\n * 같아진다(그리고 브라우저가 고아로 남는다). 그래서 **상한을 둔다**: 그 안에 끝나면 깨끗하게 끝나고,\n * 넘으면 그 사실을 말하고 종료로 넘어간다. 남은 브라우저는 다음 정리가 거둔다.\n */\n const TEARDOWN_BUDGET_MS = 5000\n\n for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'] as const) {\n process.once(signal, () => {\n let settled = false\n const proceed = (why?: string) => {\n if (settled) return\n settled = true\n if (why) logger.warn(`[headless-pool] ${why} — 종료를 계속한다(남은 브라우저는 다음 정리가 거둔다)`)\n process.kill(process.pid, signal)\n }\n /* 상한을 넘겨도 종료가 막히지 않게 — 타이머가 프로세스를 붙잡지 않도록 `unref`. */\n const budget = setTimeout(() => proceed(`풀 정리가 ${TEARDOWN_BUDGET_MS}ms 안에 끝나지 않았다`), TEARDOWN_BUDGET_MS)\n if (typeof budget.unref === 'function') budget.unref()\n\n HeadlessPoolManager.cleanupAll()\n .catch(err => logger.warn('[headless-pool] 정리 중 오류', err))\n .finally(() => {\n clearTimeout(budget)\n proceed()\n })\n })\n }\n}\n\n// Re-export types and utilities\nexport { HeadlessPoolConfig, HEADLESS_POOL_ARGUMENT_SETS } from './config'\nexport { HeadlessPool, HeadlessPoolManager } from './pool-manager'\nexport { PoolStats } from './pool-stats'\nexport { BrowserFactory } from './browser-factory'\n\n/**\n * Create a new headless browser pool\n * \n * @param name - Unique name for the pool\n * @param config - Pool configuration options\n * @returns HeadlessPool instance\n * \n * @example\n * // Create integration pool with advanced features\n * const integrationPool = createHeadlessPool('integration', {\n * min: 2, max: 20,\n * args: [...HEADLESS_POOL_ARGUMENT_SETS.basic, ...HEADLESS_POOL_ARGUMENT_SETS.keychain_safe],\n * enableStats: true,\n * enableRecovery: true\n * })\n * \n * @example \n * // Create simple pool\n * const myPool = createHeadlessPool('my-pool', {\n * min: 1, max: 5\n * })\n */\nexport function createHeadlessPool(name: string, config: HeadlessPoolConfig = {}): HeadlessPool {\n /* 브라우저를 띄우는 프로세스에만 종료 훅을 건다 — 풀을 만들지 않는 프로세스에는 달지 않는다. */\n armTeardown()\n return HeadlessPoolManager.createPool(name, config)\n}\n\n/**\n * Get existing headless browser pool by name\n * \n * @param name - Pool name\n * @returns HeadlessPool instance\n * @throws Error if pool doesn't exist\n */\nexport function getHeadlessPool(name: string): HeadlessPool {\n return HeadlessPoolManager.getPool(name)\n}\n\n/**\n * Get or create headless browser pool\n * If pool exists, returns it. If not, creates with provided config.\n * \n * @param name - Pool name\n * @param config - Pool configuration (used only if creating new pool)\n * @returns HeadlessPool instance\n */\nexport function getOrCreateHeadlessPool(name: string, config: HeadlessPoolConfig = {}): HeadlessPool {\n armTeardown()\n if (HeadlessPoolManager.hasPool(name)) {\n return HeadlessPoolManager.getPool(name)\n }\n return HeadlessPoolManager.createPool(name, config)\n}\n\n/**\n * Check if pool exists\n * \n * @param name - Pool name\n * @returns boolean\n */\nexport function hasHeadlessPool(name: string): boolean {\n return HeadlessPoolManager.hasPool(name)\n}\n\n/**\n * Get statistics for all pools\n * \n * @returns Object containing stats for all pools\n */\nexport function getAllPoolStats() {\n return HeadlessPoolManager.getAllPoolStats()\n}\n\n/**\n * Clean up all headless browser pools\n * Typically called during application shutdown\n */\nexport function cleanupAllPools(): Promise<void> {\n return HeadlessPoolManager.cleanupAll()\n}\n\n/**\n * Reset a specific pool (cleanup and recreate)\n * \n * @param name - Pool name\n */\nexport function resetPool(name: string): Promise<void> {\n return HeadlessPoolManager.resetPool(name)\n}\n\n/**\n * Remove pool from registry\n * \n * @param name - Pool name\n */\nexport function removePool(name: string): void {\n return HeadlessPoolManager.removePool(name)\n}\n\n\n// Default export\nexport default {\n createHeadlessPool,\n getHeadlessPool,\n getOrCreateHeadlessPool,\n hasHeadlessPool,\n getAllPoolStats,\n cleanupAllPools,\n resetPool,\n removePool,\n HEADLESS_POOL_ARGUMENT_SETS\n}"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "10.0.10",
3
+ "version": "10.0.13",
4
4
  "description": "Core module for framework",
5
5
  "bin": {
6
6
  "things-factory": "bin/things-factory",
@@ -126,5 +126,5 @@
126
126
  "pg": "^8.7.3",
127
127
  "sqlite3": "^5.0.8"
128
128
  },
129
- "gitHead": "afb7bf76bb16e946c8e62d9e79199c3fa5e34174"
129
+ "gitHead": "808b3b9e3bf15c058316ec4c73455108df7722aa"
130
130
  }