@hanzogui/static-worker 8.3.1 → 8.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -1,278 +1,349 @@
1
- import { fileURLToPath } from "node:url";
2
- import Piscina from "piscina";
3
- import * as extractablePathModule from "@hanzogui/static/extractablePath";
1
+ /**
2
+ * @hanzogui/static-worker
3
+ *
4
+ * Pure worker-based API for Gui static extraction.
5
+ * All operations run in a worker thread for better performance and isolation.
6
+ *
7
+ * This package provides a clean async API that wraps @hanzogui/static's worker
8
+ * implementation without exposing any sync/legacy APIs.
9
+ */
10
+ import Piscina from 'piscina';
11
+ import { worker } from './here.js';
12
+ /**
13
+ * Which files the compiler may read styles out of. Pure path logic, so it is
14
+ * handed straight across rather than run in the worker: a bundler plugin needs
15
+ * the answer before it decides whether a round-trip is worth making, and the
16
+ * extractor needs the same answer for real.
17
+ *
18
+ * @hanzogui/static is CJS-only, and Node's lexer does not find named exports
19
+ * through esbuild's `__export` wrapper, so an ESM `export { x } from` of the
20
+ * subpath throws at load. The namespace always carries `default` —
21
+ * module.exports — in both of this package's builds, so read the names off it
22
+ * once, here, instead of teaching every caller the same trick.
23
+ */
24
+ import * as extractablePathModule from '@hanzogui/static/extractablePath';
4
25
  const extractablePath = extractablePathModule.default ?? extractablePathModule;
5
- const {
6
- DEFAULT_EXTRACT_PACKAGES,
7
- installedPackageOf,
8
- isExtractable
9
- } = extractablePath;
10
- const getPragmaOptions = async props => {
11
- const {
12
- default: Static
13
- } = await import("@hanzogui/static");
14
- return Static.getPragmaOptions(props);
26
+ export const { DEFAULT_EXTRACT_PACKAGES, installedPackageOf, isExtractable } = extractablePath;
27
+ export const getPragmaOptions = async (props) => {
28
+ const { default: Static } = await import('@hanzogui/static');
29
+ return Static.getPragmaOptions(props);
15
30
  };
16
- const getWorkerPath = () => {
17
- if (typeof import.meta !== "undefined" && import.meta.url) {
18
- const workerPath = fileURLToPath(import.meta.resolve("@hanzogui/static/worker"));
19
- return workerPath.replace(/\.mjs$/, ".js");
20
- }
21
- return require.resolve("@hanzogui/static/worker").replace(/\.mjs$/, ".js");
22
- };
23
- const POOL_KEY = "__hanzogui_piscina_pool__";
24
- const CLOSING_KEY = "__hanzogui_piscina_closing__";
25
- const TASK_COUNT_KEY = "__hanzogui_piscina_task_count__";
26
- const RECYCLING_KEY = "__hanzogui_piscina_recycling__";
27
- const MAX_TASKS_BEFORE_RECYCLE = 1e3;
31
+ // The worker file, by this package's own resolution of it.
32
+ const getWorkerPath = () => worker;
33
+ // Use globalThis to share pool across module instances (Vite environments)
34
+ const POOL_KEY = '__hanzogui_piscina_pool__';
35
+ const CLOSING_KEY = '__hanzogui_piscina_closing__';
36
+ const TASK_COUNT_KEY = '__hanzogui_piscina_task_count__';
37
+ const RECYCLING_KEY = '__hanzogui_piscina_recycling__';
38
+ // recycle worker after this many tasks to prevent RSS bloat from V8 memory fragmentation
39
+ // Node.js worker threads don't release memory properly - see https://github.com/nodejs/node/issues/51868
40
+ // set high enough that builds (typically 200-400 files) never trigger a recycle,
41
+ // but long-running dev servers still get memory relief eventually
42
+ const MAX_TASKS_BEFORE_RECYCLE = 1000;
28
43
  function getSharedPool() {
29
- return globalThis[POOL_KEY] ?? null;
44
+ return globalThis[POOL_KEY] ?? null;
30
45
  }
31
46
  function setSharedPool(pool) {
32
- ;
33
- globalThis[POOL_KEY] = pool;
47
+ ;
48
+ globalThis[POOL_KEY] = pool;
34
49
  }
35
50
  function isClosing() {
36
- return globalThis[CLOSING_KEY] === true;
51
+ return globalThis[CLOSING_KEY] === true;
37
52
  }
38
53
  function setClosing(value) {
39
- ;
40
- globalThis[CLOSING_KEY] = value;
54
+ ;
55
+ globalThis[CLOSING_KEY] = value;
41
56
  }
42
57
  function isRecycling() {
43
- return globalThis[RECYCLING_KEY] === true;
58
+ return globalThis[RECYCLING_KEY] === true;
44
59
  }
45
60
  function setRecycling(value) {
46
- ;
47
- globalThis[RECYCLING_KEY] = value;
61
+ ;
62
+ globalThis[RECYCLING_KEY] = value;
48
63
  }
49
64
  function getTaskCount() {
50
- return globalThis[TASK_COUNT_KEY] ?? 0;
65
+ return globalThis[TASK_COUNT_KEY] ?? 0;
51
66
  }
52
67
  function incrementTaskCount() {
53
- const count = getTaskCount() + 1;
54
- globalThis[TASK_COUNT_KEY] = count;
55
- return count;
68
+ const count = getTaskCount() + 1;
69
+ globalThis[TASK_COUNT_KEY] = count;
70
+ return count;
56
71
  }
57
72
  function resetTaskCount() {
58
- ;
59
- globalThis[TASK_COUNT_KEY] = 0;
73
+ ;
74
+ globalThis[TASK_COUNT_KEY] = 0;
60
75
  }
76
+ /**
77
+ * Create a new Piscina pool instance
78
+ */
61
79
  function createPool() {
62
- const pool = new Piscina({
63
- filename: getWorkerPath(),
64
- // each worker loads and caches config independently
65
- minThreads: 2,
66
- maxThreads: 2,
67
- // Never terminate due to idle - worker stays alive until close() or process exit
68
- // This prevents "Terminating worker thread" errors from Piscina during idle
69
- idleTimeout: Number.POSITIVE_INFINITY
70
- // no resourceLimits - we rely on task-based recycling instead
71
- // V8 resourceLimits cause "Terminating worker thread" messages when hit
72
- });
73
- pool.on("error", err => {
74
- if (isClosing() || isRecycling()) return;
75
- const message = err && typeof err === "object" && "message" in err ? String(err.message) : "";
76
- if (message.includes("Terminating worker thread")) return;
77
- console.error("[hanzogui] Worker pool error:", err);
78
- });
79
- return pool;
80
+ const pool = new Piscina({
81
+ filename: getWorkerPath(),
82
+ // each worker loads and caches config independently
83
+ minThreads: 2,
84
+ maxThreads: 2,
85
+ // Never terminate due to idle - worker stays alive until close() or process exit
86
+ // This prevents "Terminating worker thread" errors from Piscina during idle
87
+ idleTimeout: Number.POSITIVE_INFINITY,
88
+ // no resourceLimits - we rely on task-based recycling instead
89
+ // V8 resourceLimits cause "Terminating worker thread" messages when hit
90
+ });
91
+ // Handle error events to prevent uncaught exceptions during pool destruction
92
+ pool.on('error', (err) => {
93
+ if (isClosing() || isRecycling())
94
+ return;
95
+ const message = err && typeof err === 'object' && 'message' in err ? String(err.message) : '';
96
+ // Suppress termination errors (can still occur during explicit close/destroy)
97
+ if (message.includes('Terminating worker thread'))
98
+ return;
99
+ console.error('[hanzogui] Worker pool error:', err);
100
+ });
101
+ return pool;
80
102
  }
103
+ /**
104
+ * Get or create the Piscina worker pool
105
+ */
81
106
  function getPool() {
82
- let pool = getSharedPool();
83
- if (!pool) {
84
- pool = createPool();
85
- setSharedPool(pool);
86
- }
87
- return pool;
107
+ let pool = getSharedPool();
108
+ if (!pool) {
109
+ pool = createPool();
110
+ setSharedPool(pool);
111
+ }
112
+ return pool;
88
113
  }
89
- async function loadGui(options) {
90
- const pool = getPool();
91
- const task = {
92
- type: "extractToClassNames",
93
- source: "// dummy",
94
- sourcePath: "__dummy__.tsx",
95
- options: {
96
- components: ["@hanzo/gui"],
97
- ...options
98
- },
99
- shouldPrintDebug: false
100
- };
101
- try {
102
- await pool.run(task, {
103
- name: "runTask"
104
- });
105
- return {
106
- success: true
114
+ /**
115
+ * Load Gui configuration in worker
116
+ * Sends a warmup task to trigger config loading
117
+ * bundleConfig auto-detects if files exist and skips rebuild
118
+ */
119
+ export async function loadGui(options) {
120
+ const pool = getPool();
121
+ // use extractToClassNames with a dummy request to trigger config loading
122
+ // the worker will cache the config for subsequent requests
123
+ const task = {
124
+ type: 'extractToClassNames',
125
+ source: '// dummy',
126
+ sourcePath: '__dummy__.tsx',
127
+ options: {
128
+ components: ['@hanzo/gui'],
129
+ ...options,
130
+ },
131
+ shouldPrintDebug: false,
107
132
  };
108
- } catch (error) {
109
- console.error("[static-worker] Error loading Gui config:", error);
110
- throw error;
111
- }
133
+ try {
134
+ await pool.run(task, { name: 'runTask' });
135
+ return { success: true };
136
+ }
137
+ catch (error) {
138
+ console.error('[static-worker] Error loading Gui config:', error);
139
+ throw error;
140
+ }
112
141
  }
142
+ /**
143
+ * Recycle the worker pool to release RSS memory
144
+ * Creates new pool, swaps immediately, then destroys old pool
145
+ * V8 doesn't return memory to OS, so we need to restart the worker periodically
146
+ */
113
147
  async function recyclePool(options) {
114
- if (isClosing() || isRecycling()) return;
115
- const oldPool = getSharedPool();
116
- if (!oldPool) return;
117
- setRecycling(true);
118
- const start = Date.now();
119
- try {
120
- const originalStderr = process.stderr.write.bind(process.stderr);
121
- const originalStdout = process.stdout.write.bind(process.stdout);
122
- const filter = (chunk, ...args) => {
123
- const str = typeof chunk === "string" ? chunk : chunk?.toString?.() || "";
124
- if (str.includes("Terminating worker thread")) return true;
125
- return false;
126
- };
127
- process.stderr.write = (chunk, ...args) => {
128
- if (filter(chunk)) return true;
129
- return originalStderr(chunk, ...args);
130
- };
131
- process.stdout.write = (chunk, ...args) => {
132
- if (filter(chunk)) return true;
133
- return originalStdout(chunk, ...args);
134
- };
135
- const newPool = createPool();
136
- setSharedPool(newPool);
137
- const warmupTask = {
138
- type: "extractToClassNames",
139
- source: "// warmup",
140
- sourcePath: "__warmup__.tsx",
141
- options: {
142
- ...options,
143
- // skip the "built config" log on warmup since it's a recycle
144
- _skipBuildLog: true
145
- },
146
- shouldPrintDebug: false
147
- };
148
- await newPool.run(warmupTask, {
149
- name: "runTask"
150
- });
151
- oldPool.removeAllListeners();
152
- oldPool.destroy().catch(() => {});
153
- setTimeout(() => {
154
- process.stderr.write = originalStderr;
155
- process.stdout.write = originalStdout;
156
- });
157
- console.log(` \u267B\uFE0F [hanzogui] recycled worker pool (${Date.now() - start}ms)`);
158
- } finally {
159
- setRecycling(false);
160
- }
148
+ if (isClosing() || isRecycling())
149
+ return;
150
+ const oldPool = getSharedPool();
151
+ if (!oldPool)
152
+ return;
153
+ setRecycling(true);
154
+ const start = Date.now();
155
+ try {
156
+ // suppress "Terminating worker thread" messages during recycle
157
+ const originalStderr = process.stderr.write.bind(process.stderr);
158
+ const originalStdout = process.stdout.write.bind(process.stdout);
159
+ const filter = (chunk, ...args) => {
160
+ const str = typeof chunk === 'string' ? chunk : chunk?.toString?.() || '';
161
+ if (str.includes('Terminating worker thread'))
162
+ return true;
163
+ return false;
164
+ };
165
+ process.stderr.write = ((chunk, ...args) => {
166
+ if (filter(chunk))
167
+ return true;
168
+ return originalStderr(chunk, ...args);
169
+ });
170
+ process.stdout.write = ((chunk, ...args) => {
171
+ if (filter(chunk))
172
+ return true;
173
+ return originalStdout(chunk, ...args);
174
+ });
175
+ // create new pool and swap immediately
176
+ const newPool = createPool();
177
+ setSharedPool(newPool);
178
+ // warm up new pool with config (this caches it in the new worker)
179
+ const warmupTask = {
180
+ type: 'extractToClassNames',
181
+ source: '// warmup',
182
+ sourcePath: '__warmup__.tsx',
183
+ options: {
184
+ ...options,
185
+ // skip the "built config" log on warmup since it's a recycle
186
+ _skipBuildLog: true,
187
+ },
188
+ shouldPrintDebug: false,
189
+ };
190
+ await newPool.run(warmupTask, { name: 'runTask' });
191
+ // destroy old pool - pending tasks will be rejected
192
+ oldPool.removeAllListeners();
193
+ oldPool.destroy().catch(() => { });
194
+ // restore stderr/stdout after a delay
195
+ setTimeout(() => {
196
+ process.stderr.write = originalStderr;
197
+ process.stdout.write = originalStdout;
198
+ });
199
+ console.log(` ♻️ [hanzogui] recycled worker pool (${Date.now() - start}ms)`);
200
+ }
201
+ finally {
202
+ setRecycling(false);
203
+ }
161
204
  }
162
- async function loadGuiBuildConfig(hanzoguiOptions) {
163
- const {
164
- default: Static
165
- } = await import("@hanzogui/static");
166
- return Static.loadGuiBuildConfigAsync(hanzoguiOptions);
205
+ /**
206
+ * Load Gui build configuration asynchronously
207
+ * Uses esbuild-wasm to avoid EPIPE errors from native esbuild service lifecycle
208
+ */
209
+ export async function loadGuiBuildConfig(hanzoguiOptions) {
210
+ const { default: Static } = await import('@hanzogui/static');
211
+ return Static.loadGuiBuildConfigAsync(hanzoguiOptions);
167
212
  }
168
- async function extractToClassNames(params) {
169
- const {
170
- source,
171
- sourcePath = "",
172
- options,
173
- shouldPrintDebug = false
174
- } = params;
175
- if (typeof source !== "string") {
176
- throw new Error("`source` must be a string of javascript");
177
- }
178
- const task = {
179
- type: "extractToClassNames",
180
- source,
181
- sourcePath,
182
- options,
183
- shouldPrintDebug
184
- };
185
- const pool = getPool();
186
- const result = await pool.run(task, {
187
- name: "runTask"
188
- });
189
- if (!result.success) {
190
- const errorMessage = [`[hanzogui-extract] Error processing file: ${sourcePath || "(unknown)"}`, ``, result.error, result.stack ? `
191
- ${result.stack}` : ""].filter(Boolean).join("\n");
192
- throw new Error(errorMessage);
193
- }
194
- const count = incrementTaskCount();
195
- if (count >= MAX_TASKS_BEFORE_RECYCLE) {
196
- resetTaskCount();
197
- recyclePool(options).catch(() => {});
198
- }
199
- return result.data;
213
+ /**
214
+ * Extract Gui components to className-based CSS for web
215
+ */
216
+ export async function extractToClassNames(params) {
217
+ const { source, sourcePath = '', options, shouldPrintDebug = false } = params;
218
+ if (typeof source !== 'string') {
219
+ throw new Error('`source` must be a string of javascript');
220
+ }
221
+ const task = {
222
+ type: 'extractToClassNames',
223
+ source,
224
+ sourcePath,
225
+ options,
226
+ shouldPrintDebug,
227
+ };
228
+ const pool = getPool();
229
+ const result = (await pool.run(task, { name: 'runTask' }));
230
+ if (!result.success) {
231
+ const errorMessage = [
232
+ `[hanzogui-extract] Error processing file: ${sourcePath || '(unknown)'}`,
233
+ ``,
234
+ result.error,
235
+ result.stack ? `\n${result.stack}` : '',
236
+ ]
237
+ .filter(Boolean)
238
+ .join('\n');
239
+ throw new Error(errorMessage);
240
+ }
241
+ // check if we need to recycle the worker to prevent RSS bloat
242
+ const count = incrementTaskCount();
243
+ if (count >= MAX_TASKS_BEFORE_RECYCLE) {
244
+ resetTaskCount();
245
+ // recycle asynchronously with hot-swap to not block current request
246
+ recyclePool(options).catch(() => { });
247
+ }
248
+ return result.data;
200
249
  }
201
- async function extractToNative(sourceFileName, sourceCode, options) {
202
- const task = {
203
- type: "extractToNative",
204
- sourceFileName,
205
- sourceCode,
206
- options
207
- };
208
- const pool = getPool();
209
- const result = await pool.run(task, {
210
- name: "runTask"
211
- });
212
- if (!result.success) {
213
- const errorMessage = [`[hanzogui-extract] Error processing file: ${sourceFileName || "(unknown)"}`, ``, result.error, result.stack ? `
214
- ${result.stack}` : ""].filter(Boolean).join("\n");
215
- throw new Error(errorMessage);
216
- }
217
- const count = incrementTaskCount();
218
- if (count >= MAX_TASKS_BEFORE_RECYCLE) {
219
- resetTaskCount();
220
- recyclePool(options).catch(() => {});
221
- }
222
- return result.data;
250
+ /**
251
+ * Extract Gui components to React Native StyleSheet format
252
+ */
253
+ export async function extractToNative(sourceFileName, sourceCode, options) {
254
+ const task = {
255
+ type: 'extractToNative',
256
+ sourceFileName,
257
+ sourceCode,
258
+ options,
259
+ };
260
+ const pool = getPool();
261
+ const result = (await pool.run(task, { name: 'runTask' }));
262
+ if (!result.success) {
263
+ const errorMessage = [
264
+ `[hanzogui-extract] Error processing file: ${sourceFileName || '(unknown)'}`,
265
+ ``,
266
+ result.error,
267
+ result.stack ? `\n${result.stack}` : '',
268
+ ]
269
+ .filter(Boolean)
270
+ .join('\n');
271
+ throw new Error(errorMessage);
272
+ }
273
+ // check if we need to recycle the worker to prevent RSS bloat
274
+ const count = incrementTaskCount();
275
+ if (count >= MAX_TASKS_BEFORE_RECYCLE) {
276
+ resetTaskCount();
277
+ // recycle asynchronously with hot-swap to not block current request
278
+ recyclePool(options).catch(() => { });
279
+ }
280
+ return result.data;
223
281
  }
224
- async function watchGuiConfig(options) {
225
- const {
226
- default: Static
227
- } = await import("@hanzogui/static");
228
- const watcher = await Static.watchGuiConfig(options);
229
- if (!watcher) {
230
- return;
231
- }
232
- const originalDispose = watcher.dispose;
233
- return {
234
- dispose: () => {
235
- originalDispose();
236
- if (getSharedPool()) {
237
- clearWorkerCache();
238
- }
282
+ /**
283
+ * Watch Gui config for changes and reload when it changes
284
+ */
285
+ export async function watchGuiConfig(options) {
286
+ // For now, we'll use the static package's watcher directly
287
+ // This could be improved to use worker-based watching
288
+ const { default: Static } = await import('@hanzogui/static');
289
+ const watcher = await Static.watchGuiConfig(options);
290
+ if (!watcher) {
291
+ return;
239
292
  }
240
- };
293
+ // Wrap the dispose to also clear worker cache
294
+ const originalDispose = watcher.dispose;
295
+ return {
296
+ dispose: () => {
297
+ originalDispose();
298
+ if (getSharedPool()) {
299
+ // Fire and forget - errors are handled internally
300
+ clearWorkerCache();
301
+ }
302
+ },
303
+ };
241
304
  }
242
- async function clearWorkerCache() {
243
- const pool = getSharedPool();
244
- if (!pool || isClosing()) return;
245
- const task = {
246
- type: "clearCache"
247
- };
248
- await pool.run(task, {
249
- name: "runTask"
250
- });
305
+ /**
306
+ * Clear the worker's config cache
307
+ * Call this when config files change
308
+ */
309
+ export async function clearWorkerCache() {
310
+ const pool = getSharedPool();
311
+ if (!pool || isClosing())
312
+ return;
313
+ const task = { type: 'clearCache' };
314
+ await pool.run(task, { name: 'runTask' });
251
315
  }
252
- async function destroyPool() {
253
- const pool = getSharedPool();
254
- if (pool) {
255
- setClosing(true);
256
- try {
257
- await pool.close();
258
- } finally {
259
- setSharedPool(null);
260
- setClosing(false);
316
+ /**
317
+ * Clean up the worker pool on exit
318
+ * Should be called when the build process completes
319
+ */
320
+ export async function destroyPool() {
321
+ const pool = getSharedPool();
322
+ if (pool) {
323
+ setClosing(true);
324
+ try {
325
+ await pool.close();
326
+ }
327
+ finally {
328
+ setSharedPool(null);
329
+ setClosing(false);
330
+ }
261
331
  }
262
- }
263
332
  }
264
- function getPoolStats() {
265
- const pool = getSharedPool();
266
- if (!pool) {
267
- return null;
268
- }
269
- return {
270
- threads: pool.threads.length,
271
- queueSize: pool.queueSize,
272
- completed: pool.completed,
273
- duration: pool.duration,
274
- utilization: pool.utilization
275
- };
333
+ /**
334
+ * Get pool statistics for debugging
335
+ */
336
+ export function getPoolStats() {
337
+ const pool = getSharedPool();
338
+ if (!pool) {
339
+ return null;
340
+ }
341
+ return {
342
+ threads: pool.threads.length,
343
+ queueSize: pool.queueSize,
344
+ completed: pool.completed,
345
+ duration: pool.duration,
346
+ utilization: pool.utilization,
347
+ };
276
348
  }
277
- export { DEFAULT_EXTRACT_PACKAGES, clearWorkerCache, destroyPool, extractToClassNames, extractToNative, getPoolStats, getPragmaOptions, installedPackageOf, isExtractable, loadGui, loadGuiBuildConfig, watchGuiConfig };
278
- //# sourceMappingURL=index.js.map
349
+ //# sourceMappingURL=index.js.map