@things-factory/board-service 10.1.39 → 10.1.40

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.
@@ -6,7 +6,6 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.getHeadlessPool = getHeadlessPool;
8
8
  const shell_1 = require("@things-factory/shell");
9
- const shell_2 = require("@things-factory/shell");
10
9
  const fonts_js_1 = require("./fonts.js");
11
10
  // Custom setup function for label page
12
11
  async function setupLabelPage(browser) {
@@ -70,25 +69,40 @@ async function setupLabelPage(browser) {
70
69
  */
71
70
  const LABEL_POOL_MIN = parseInt(process.env.HEADLESS_POOL_LABEL_MIN ?? '1', 10);
72
71
  const LABEL_POOL_MAX = parseInt(process.env.HEADLESS_POOL_LABEL_MAX ?? '10', 10);
73
- // Create the label pool instance with custom setup
74
- const labelPool = (0, shell_1.getOrCreateHeadlessPool)('label', {
75
- min: LABEL_POOL_MIN,
76
- max: LABEL_POOL_MAX,
77
- args: [
78
- ...shell_1.HEADLESS_POOL_ARGUMENT_SETS.basic,
79
- ...shell_1.HEADLESS_POOL_ARGUMENT_SETS.keychain_safe
80
- ],
81
- acquireTimeoutMillis: 15000,
82
- testOnBorrow: true,
83
- enableCleanup: true,
84
- customSetup: setupLabelPage
72
+ /*
73
+ * The pool is made when the server has started, not when this module is loaded.
74
+ *
75
+ * With min=1 the pool builds a browser as soon as it exists, and the page setup reads fonts from the database and
76
+ * loads a page from this same server. At module load neither is ready: shell's databaseInitializer is still running
77
+ * synchronize(), and on sqlite every query shares the one query runner. The fonts query's release() cleared the
78
+ * runner's loaded-table list mid-synchronize, which then created env_vars again and died on 'already exists' —
79
+ * plant did not start after 10.1.38 (seen 2026-09-24; it showed then because ops-master made synchronize rebuild
80
+ * five tables, a longer window). 'bootstrap-module-start' comes after the database and after the server listens.
81
+ *
82
+ * A caller that needs a label before that — none should, since routes are served only after it — still gets a pool.
83
+ */
84
+ let labelPool;
85
+ function pool() {
86
+ labelPool ??= (0, shell_1.getOrCreateHeadlessPool)('label', {
87
+ min: LABEL_POOL_MIN,
88
+ max: LABEL_POOL_MAX,
89
+ args: [...shell_1.HEADLESS_POOL_ARGUMENT_SETS.basic, ...shell_1.HEADLESS_POOL_ARGUMENT_SETS.keychain_safe],
90
+ acquireTimeoutMillis: 15000,
91
+ testOnBorrow: true,
92
+ enableCleanup: true,
93
+ customSetup: setupLabelPage
94
+ });
95
+ return labelPool;
96
+ }
97
+ (0, shell_1.onHook)('bootstrap-module-start', () => {
98
+ pool();
85
99
  });
86
100
  // Font change event subscription
87
101
  setTimeout(async () => {
88
- const eventSource = shell_2.pubsub.subscribe('notify-font-changed');
102
+ const eventSource = shell_1.pubsub.subscribe('notify-font-changed');
89
103
  for await (const data of eventSource) {
90
- // Reset the pool when fonts change
91
- await labelPool.reset();
104
+ // Reset the pool when fonts change; a pool not made yet will read the new fonts when it is.
105
+ await labelPool?.reset();
92
106
  }
93
107
  });
94
108
  /**
@@ -97,15 +111,15 @@ setTimeout(async () => {
97
111
  */
98
112
  function getHeadlessPool() {
99
113
  return {
100
- acquire: () => labelPool.acquire(),
101
- release: (resource) => labelPool.release(resource),
114
+ acquire: () => pool().acquire(),
115
+ release: (resource) => pool().release(resource),
102
116
  size: 0, // These will be dynamically calculated if needed
103
117
  available: 0,
104
118
  borrowed: 0,
105
119
  pending: 0,
106
120
  max: LABEL_POOL_MAX,
107
121
  min: LABEL_POOL_MIN,
108
- clear: () => labelPool.reset() // For font change compatibility
122
+ clear: () => labelPool?.reset() // For font change compatibility
109
123
  };
110
124
  }
111
125
  //# sourceMappingURL=headless-pool-for-label.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless-pool-for-label.js","sourceRoot":"","sources":["../../server/controllers/headless-pool-for-label.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAwGH,0CAYC;AAlHD,iDAA4F;AAC5F,iDAA8C;AAC9C,yCAAkC;AAElC,uCAAuC;AACvC,KAAK,UAAU,cAAc,CAAC,OAAY;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,IAAI,GAAG,WAAW,CAAA;IACxB,MAAM,IAAI,GAAG,8BAA8B,CAAA;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAA;IAC7B,MAAM,GAAG,GAAG,GAAG,QAAQ,MAAM,IAAI,IAAI,IAAI,GAAG,IAAI,EAAE,CAAA;IAElD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IACpC,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,GAAG,MAAM,IAAA,gBAAK,GAAE,CAAA;IAE9C,MAAM,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAA;IAEvC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAC,GAAG,EAAC,EAAE;QAC7B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;QAE7F,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,EAAE,CAAC;YACpC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAA;QAC/E,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;YACvB,IAAI,IAAI;gBAAE,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;QACzD,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE;QAC3B,OAAO,CAAC,KAAK,CAAC,wBAAwB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;QACtD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;QACvB,OAAO,CAAC,KAAK,CAAC,oBAAoB,KAAK,EAAE,CAAC,CAAA;QAC1C,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE;QACjC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE;QAC3B,IAAI,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,EAAE,CAAC;YAC1B,OAAO,CAAC,QAAQ,CAAC;gBACf,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;iBACnC;gBACD,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC;oBACvB,KAAK,EAAE,UAAU;oBACjB,UAAU;iBACX,CAAC;aACH,CAAC,CAAA;QACJ,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,QAAQ,EAAE,CAAA;QACpB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;IAEvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,GAAG,EAAE,EAAE,CAAC,CAAA;AAC/E,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,IAAI,EAAE,EAAE,CAAC,CAAA;AAEhF,mDAAmD;AACnD,MAAM,SAAS,GAAG,IAAA,+BAAuB,EAAC,OAAO,EAAE;IACjD,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,cAAc;IACnB,IAAI,EAAE;QACJ,GAAG,mCAA2B,CAAC,KAAK;QACpC,GAAG,mCAA2B,CAAC,aAAa;KAC7C;IACD,oBAAoB,EAAE,KAAK;IAC3B,YAAY,EAAE,IAAI;IAClB,aAAa,EAAE,IAAI;IACnB,WAAW,EAAE,cAAc;CAC5B,CAAC,CAAA;AAEF,iCAAiC;AACjC,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,WAAW,GAAG,cAAM,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAA;IAC3D,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QACrC,mCAAmC;QACnC,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;IACzB,CAAC;AACH,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,SAAgB,eAAe;IAC7B,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE;QAClC,OAAO,EAAE,CAAC,QAAa,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC;QACvD,IAAI,EAAE,CAAC,EAAE,iDAAiD;QAC1D,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;QACV,GAAG,EAAE,cAAc;QACnB,GAAG,EAAE,cAAc;QACnB,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,gCAAgC;KAChE,CAAA;AACH,CAAC","sourcesContent":["/**\n * Label Service Headless Pool\n * Using the unified headless pool system from @things-factory/shell\n */\n\nimport { getOrCreateHeadlessPool, HEADLESS_POOL_ARGUMENT_SETS } from '@things-factory/shell'\nimport { pubsub } from '@things-factory/shell'\nimport { fonts } from './fonts.js'\n\n// Custom setup function for label page\nasync function setupLabelPage(browser: any) {\n const protocol = 'http'\n const host = 'localhost'\n const path = '/internal-label-command-view'\n const port = process.env.PORT\n const url = `${protocol}://${host}:${port}${path}`\n\n const page = await browser.newPage()\n const [fontsToUse, fontStyles] = await fonts()\n\n await page.setRequestInterception(true)\n\n page.on('console', async msg => {\n const args = await Promise.all(msg.args().map(arg => arg.jsonValue().catch(() => undefined)))\n\n if (args.some(a => a !== undefined)) {\n console.log(`[headless ${msg.type()}]`, ...args.filter(a => a !== undefined))\n } else {\n const text = msg.text()\n if (text) console.log(`[headless ${msg.type()}]`, text)\n }\n })\n\n page.on('pageerror', error => {\n console.error(`[headless pageerror] ${error.message}`)\n console.error(error.stack)\n })\n\n page.on('error', error => {\n console.error(`[headless fault] ${error}`)\n console.error(error.stack)\n })\n\n page.on('requestfailed', request => {\n console.error('Request failed:', request.url())\n })\n\n page.on('request', request => {\n if (request.url() === url) {\n request.continue({\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n postData: JSON.stringify({\n fonts: fontsToUse,\n fontStyles\n })\n })\n } else {\n request.continue()\n }\n })\n\n await page.goto(url, { timeout: 0, waitUntil: 'load' })\n\n return { browser, page }\n}\n\n/**\n * 라벨 출력은 (자동 출고 워크플로우 등) 즉시성 요구 — cold start 비용 (browser+page\n * setup ~2-3 초) 이 사용자 경험에 직결. min=1 로 최소 1 개 warm 유지가 기본.\n * 사용 빈도가 매우 낮은 사이트는 환경 변수로 0 까지 낮출 수 있다.\n *\n * HEADLESS_POOL_LABEL_MIN (default 1)\n * HEADLESS_POOL_LABEL_MAX (default 10)\n */\nconst LABEL_POOL_MIN = parseInt(process.env.HEADLESS_POOL_LABEL_MIN ?? '1', 10)\nconst LABEL_POOL_MAX = parseInt(process.env.HEADLESS_POOL_LABEL_MAX ?? '10', 10)\n\n// Create the label pool instance with custom setup\nconst labelPool = getOrCreateHeadlessPool('label', {\n min: LABEL_POOL_MIN,\n max: LABEL_POOL_MAX,\n args: [\n ...HEADLESS_POOL_ARGUMENT_SETS.basic,\n ...HEADLESS_POOL_ARGUMENT_SETS.keychain_safe\n ],\n acquireTimeoutMillis: 15000,\n testOnBorrow: true,\n enableCleanup: true,\n customSetup: setupLabelPage\n})\n\n// Font change event subscription\nsetTimeout(async () => {\n const eventSource = pubsub.subscribe('notify-font-changed')\n for await (const data of eventSource) {\n // Reset the pool when fonts change\n await labelPool.reset()\n }\n})\n\n/**\n * Get the label headless pool\n * @returns Pool instance with acquire/release methods\n */\nexport function getHeadlessPool() {\n return {\n acquire: () => labelPool.acquire(),\n release: (resource: any) => labelPool.release(resource),\n size: 0, // These will be dynamically calculated if needed\n available: 0,\n borrowed: 0,\n pending: 0,\n max: LABEL_POOL_MAX,\n min: LABEL_POOL_MIN,\n clear: () => labelPool.reset() // For font change compatibility\n }\n}"]}
1
+ {"version":3,"file":"headless-pool-for-label.js","sourceRoot":"","sources":["../../server/controllers/headless-pool-for-label.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Label Service Headless Pool\n * Using the unified headless pool system from @things-factory/shell\n */\n\nimport { getOrCreateHeadlessPool, HEADLESS_POOL_ARGUMENT_SETS, onHook, pubsub } from '@things-factory/shell'\nimport { fonts } from './fonts.js'\n\n// Custom setup function for label page\nasync function setupLabelPage(browser: any) {\n const protocol = 'http'\n const host = 'localhost'\n const path = '/internal-label-command-view'\n const port = process.env.PORT\n const url = `${protocol}://${host}:${port}${path}`\n\n const page = await browser.newPage()\n const [fontsToUse, fontStyles] = await fonts()\n\n await page.setRequestInterception(true)\n\n page.on('console', async msg => {\n const args = await Promise.all(msg.args().map(arg => arg.jsonValue().catch(() => undefined)))\n\n if (args.some(a => a !== undefined)) {\n console.log(`[headless ${msg.type()}]`, ...args.filter(a => a !== undefined))\n } else {\n const text = msg.text()\n if (text) console.log(`[headless ${msg.type()}]`, text)\n }\n })\n\n page.on('pageerror', error => {\n console.error(`[headless pageerror] ${error.message}`)\n console.error(error.stack)\n })\n\n page.on('error', error => {\n console.error(`[headless fault] ${error}`)\n console.error(error.stack)\n })\n\n page.on('requestfailed', request => {\n console.error('Request failed:', request.url())\n })\n\n page.on('request', request => {\n if (request.url() === url) {\n request.continue({\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n postData: JSON.stringify({\n fonts: fontsToUse,\n fontStyles\n })\n })\n } else {\n request.continue()\n }\n })\n\n await page.goto(url, { timeout: 0, waitUntil: 'load' })\n\n return { browser, page }\n}\n\n/**\n * 라벨 출력은 (자동 출고 워크플로우 등) 즉시성 요구 — cold start 비용 (browser+page\n * setup ~2-3 초) 이 사용자 경험에 직결. min=1 로 최소 1 개 warm 유지가 기본.\n * 사용 빈도가 매우 낮은 사이트는 환경 변수로 0 까지 낮출 수 있다.\n *\n * HEADLESS_POOL_LABEL_MIN (default 1)\n * HEADLESS_POOL_LABEL_MAX (default 10)\n */\nconst LABEL_POOL_MIN = parseInt(process.env.HEADLESS_POOL_LABEL_MIN ?? '1', 10)\nconst LABEL_POOL_MAX = parseInt(process.env.HEADLESS_POOL_LABEL_MAX ?? '10', 10)\n\n/*\n * The pool is made when the server has started, not when this module is loaded.\n *\n * With min=1 the pool builds a browser as soon as it exists, and the page setup reads fonts from the database and\n * loads a page from this same server. At module load neither is ready: shell's databaseInitializer is still running\n * synchronize(), and on sqlite every query shares the one query runner. The fonts query's release() cleared the\n * runner's loaded-table list mid-synchronize, which then created env_vars again and died on 'already exists' —\n * plant did not start after 10.1.38 (seen 2026-09-24; it showed then because ops-master made synchronize rebuild\n * five tables, a longer window). 'bootstrap-module-start' comes after the database and after the server listens.\n *\n * A caller that needs a label before that — none should, since routes are served only after it — still gets a pool.\n */\nlet labelPool: ReturnType<typeof getOrCreateHeadlessPool> | undefined\n\nfunction pool() {\n labelPool ??= getOrCreateHeadlessPool('label', {\n min: LABEL_POOL_MIN,\n max: LABEL_POOL_MAX,\n args: [...HEADLESS_POOL_ARGUMENT_SETS.basic, ...HEADLESS_POOL_ARGUMENT_SETS.keychain_safe],\n acquireTimeoutMillis: 15000,\n testOnBorrow: true,\n enableCleanup: true,\n customSetup: setupLabelPage\n })\n return labelPool\n}\n\nonHook('bootstrap-module-start', () => {\n pool()\n})\n\n// Font change event subscription\nsetTimeout(async () => {\n const eventSource = pubsub.subscribe('notify-font-changed')\n for await (const data of eventSource) {\n // Reset the pool when fonts change; a pool not made yet will read the new fonts when it is.\n await labelPool?.reset()\n }\n})\n\n/**\n * Get the label headless pool\n * @returns Pool instance with acquire/release methods\n */\nexport function getHeadlessPool() {\n return {\n acquire: () => pool().acquire(),\n release: (resource: any) => pool().release(resource),\n size: 0, // These will be dynamically calculated if needed\n available: 0,\n borrowed: 0,\n pending: 0,\n max: LABEL_POOL_MAX,\n min: LABEL_POOL_MIN,\n clear: () => labelPool?.reset() // For font change compatibility\n }\n}"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The label pool is made after the server starts, not when the module loads.
3
+ *
4
+ * With min=1 a pool builds a browser as soon as it exists, and that setup reads fonts from the database. At module
5
+ * load the database is still being synchronised; on sqlite that query shares synchronize()'s one query runner and
6
+ * broke it — plant did not start after 10.1.38 (2026-09-24). 'bootstrap-module-start' follows the database.
7
+ */
8
+ declare const created: any[];
9
+ declare const hooks: Record<string, Function[]>;
10
+ declare const load: () => typeof import("./headless-pool-for-label");
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The label pool is made after the server starts, not when the module loads.
3
+ *
4
+ * With min=1 a pool builds a browser as soon as it exists, and that setup reads fonts from the database. At module
5
+ * load the database is still being synchronised; on sqlite that query shares synchronize()'s one query runner and
6
+ * broke it — plant did not start after 10.1.38 (2026-09-24). 'bootstrap-module-start' follows the database.
7
+ */
8
+ const created = [];
9
+ const hooks = {};
10
+ jest.mock('@things-factory/shell', () => ({
11
+ HEADLESS_POOL_ARGUMENT_SETS: { basic: [], keychain_safe: [] },
12
+ getOrCreateHeadlessPool: jest.fn((name, config) => {
13
+ const pool = { name, config, acquire: jest.fn(async () => 'page'), release: jest.fn(), reset: jest.fn() };
14
+ created.push(pool);
15
+ return pool;
16
+ }),
17
+ onHook: (name, handler) => (hooks[name] ??= []).push(handler),
18
+ pubsub: { subscribe: () => ({ [Symbol.asyncIterator]: () => ({ next: () => new Promise(() => { }) }) }) }
19
+ }));
20
+ jest.mock('./fonts', () => ({ fonts: jest.fn(async () => [[], '']) }));
21
+ beforeEach(() => {
22
+ jest.resetModules();
23
+ created.length = 0;
24
+ for (const key of Object.keys(hooks))
25
+ delete hooks[key];
26
+ });
27
+ const load = () => require('./headless-pool-for-label');
28
+ test('loading the module makes no pool — nothing touches the database while it is being synchronised', () => {
29
+ load();
30
+ expect(created).toHaveLength(0);
31
+ });
32
+ test('the pool is made when the server has started', () => {
33
+ load();
34
+ expect(hooks['bootstrap-module-start']).toHaveLength(1);
35
+ hooks['bootstrap-module-start'][0]({});
36
+ expect(created).toHaveLength(1);
37
+ expect(created[0].name).toBe('label');
38
+ });
39
+ test('one pool, however it is first reached', async () => {
40
+ const { getHeadlessPool } = load();
41
+ await getHeadlessPool().acquire();
42
+ hooks['bootstrap-module-start'][0]({});
43
+ await getHeadlessPool().acquire();
44
+ expect(created).toHaveLength(1);
45
+ expect(created[0].acquire).toHaveBeenCalledTimes(2);
46
+ });
47
+ test('clearing before the pool exists does nothing and does not make one', async () => {
48
+ const { getHeadlessPool } = load();
49
+ await getHeadlessPool().clear();
50
+ expect(created).toHaveLength(0);
51
+ });
52
+ //# sourceMappingURL=headless-pool-for-label.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-pool-for-label.test.js","sourceRoot":"","sources":["../../server/controllers/headless-pool-for-label.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The label pool is made after the server starts, not when the module loads.\n *\n * With min=1 a pool builds a browser as soon as it exists, and that setup reads fonts from the database. At module\n * load the database is still being synchronised; on sqlite that query shares synchronize()'s one query runner and\n * broke it — plant did not start after 10.1.38 (2026-09-24). 'bootstrap-module-start' follows the database.\n */\nconst created: any[] = []\nconst hooks: Record<string, Function[]> = {}\n\njest.mock('@things-factory/shell', () => ({\n HEADLESS_POOL_ARGUMENT_SETS: { basic: [], keychain_safe: [] },\n getOrCreateHeadlessPool: jest.fn((name: string, config: any) => {\n const pool = { name, config, acquire: jest.fn(async () => 'page'), release: jest.fn(), reset: jest.fn() }\n created.push(pool)\n return pool\n }),\n onHook: (name: string, handler: Function) => (hooks[name] ??= []).push(handler),\n pubsub: { subscribe: () => ({ [Symbol.asyncIterator]: () => ({ next: () => new Promise(() => {}) }) }) }\n}))\njest.mock('./fonts', () => ({ fonts: jest.fn(async () => [[], '']) }))\n\nbeforeEach(() => {\n jest.resetModules()\n created.length = 0\n for (const key of Object.keys(hooks)) delete hooks[key]\n})\n\nconst load = () => require('./headless-pool-for-label') as typeof import('./headless-pool-for-label')\n\ntest('loading the module makes no pool — nothing touches the database while it is being synchronised', () => {\n load()\n expect(created).toHaveLength(0)\n})\n\ntest('the pool is made when the server has started', () => {\n load()\n expect(hooks['bootstrap-module-start']).toHaveLength(1)\n\n hooks['bootstrap-module-start'][0]({})\n\n expect(created).toHaveLength(1)\n expect(created[0].name).toBe('label')\n})\n\ntest('one pool, however it is first reached', async () => {\n const { getHeadlessPool } = load()\n\n await getHeadlessPool().acquire()\n hooks['bootstrap-module-start'][0]({})\n await getHeadlessPool().acquire()\n\n expect(created).toHaveLength(1)\n expect(created[0].acquire).toHaveBeenCalledTimes(2)\n})\n\ntest('clearing before the pool exists does nothing and does not make one', async () => {\n const { getHeadlessPool } = load()\n await getHeadlessPool().clear()\n expect(created).toHaveLength(0)\n})\n"]}