@hanzogui/static-worker 2.0.0-rc.41-hanzoai.5

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/src/index.ts ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * @hanzogui/static-worker
3
+ *
4
+ * Pure worker-based API for Hanzogui 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
+
11
+ import type { HanzoguiOptions } from '@hanzogui/types'
12
+ import { fileURLToPath } from 'node:url'
13
+ import Piscina from 'piscina'
14
+
15
+ export type { ExtractedResponse, HanzoguiProjectInfo } from '@hanzogui/static'
16
+ export type { HanzoguiOptions } from '@hanzogui/types'
17
+
18
+ export const getPragmaOptions = async (props: { source: string; path: string }) => {
19
+ const { default: Static } = await import('@hanzogui/static')
20
+ return Static.getPragmaOptions(props)
21
+ }
22
+
23
+ // Resolve worker path - works for both CJS and ESM
24
+ const getWorkerPath = () => {
25
+ // Piscina needs the actual file path, not the module resolution
26
+ // Use the CommonJS .js version which works for piscina
27
+ if (typeof import.meta !== 'undefined' && import.meta.url) {
28
+ const workerPath = fileURLToPath(import.meta.resolve('@hanzogui/static/worker'))
29
+ // Replace .mjs with .js for CommonJS compatibility
30
+ return workerPath.replace(/\.mjs$/, '.js')
31
+ }
32
+
33
+ // Fallback for CJS
34
+ return require.resolve('@hanzogui/static/worker').replace(/\.mjs$/, '.js')
35
+ }
36
+
37
+ // Use globalThis to share pool across module instances (Vite environments)
38
+ const POOL_KEY = '__hanzogui_piscina_pool__'
39
+ const CLOSING_KEY = '__hanzogui_piscina_closing__'
40
+ const TASK_COUNT_KEY = '__hanzogui_piscina_task_count__'
41
+ const RECYCLING_KEY = '__hanzogui_piscina_recycling__'
42
+
43
+ // recycle worker after this many tasks to prevent RSS bloat from V8 memory fragmentation
44
+ // Node.js worker threads don't release memory properly - see https://github.com/nodejs/node/issues/51868
45
+ // set high enough that builds (typically 200-400 files) never trigger a recycle,
46
+ // but long-running dev servers still get memory relief eventually
47
+ const MAX_TASKS_BEFORE_RECYCLE = 1000
48
+
49
+ function getSharedPool(): Piscina | null {
50
+ return (globalThis as any)[POOL_KEY] ?? null
51
+ }
52
+
53
+ function setSharedPool(pool: Piscina | null) {
54
+ ;(globalThis as any)[POOL_KEY] = pool
55
+ }
56
+
57
+ function isClosing(): boolean {
58
+ return (globalThis as any)[CLOSING_KEY] === true
59
+ }
60
+
61
+ function setClosing(value: boolean) {
62
+ ;(globalThis as any)[CLOSING_KEY] = value
63
+ }
64
+
65
+ function isRecycling(): boolean {
66
+ return (globalThis as any)[RECYCLING_KEY] === true
67
+ }
68
+
69
+ function setRecycling(value: boolean) {
70
+ ;(globalThis as any)[RECYCLING_KEY] = value
71
+ }
72
+
73
+ function getTaskCount(): number {
74
+ return (globalThis as any)[TASK_COUNT_KEY] ?? 0
75
+ }
76
+
77
+ function incrementTaskCount(): number {
78
+ const count = getTaskCount() + 1
79
+ ;(globalThis as any)[TASK_COUNT_KEY] = count
80
+ return count
81
+ }
82
+
83
+ function resetTaskCount() {
84
+ ;(globalThis as any)[TASK_COUNT_KEY] = 0
85
+ }
86
+
87
+ /**
88
+ * Create a new Piscina pool instance
89
+ */
90
+ function createPool(): Piscina {
91
+ const pool = new Piscina({
92
+ filename: getWorkerPath(),
93
+ // each worker loads and caches config independently
94
+ minThreads: 2,
95
+ maxThreads: 2,
96
+ // Never terminate due to idle - worker stays alive until close() or process exit
97
+ // This prevents "Terminating worker thread" errors from Piscina during idle
98
+ idleTimeout: Number.POSITIVE_INFINITY,
99
+ // no resourceLimits - we rely on task-based recycling instead
100
+ // V8 resourceLimits cause "Terminating worker thread" messages when hit
101
+ })
102
+
103
+ // Handle error events to prevent uncaught exceptions during pool destruction
104
+ pool.on('error', (err) => {
105
+ if (isClosing() || isRecycling()) return
106
+ const message =
107
+ err && typeof err === 'object' && 'message' in err ? String(err.message) : ''
108
+ // Suppress termination errors (can still occur during explicit close/destroy)
109
+ if (message.includes('Terminating worker thread')) return
110
+ console.error('[hanzogui] Worker pool error:', err)
111
+ })
112
+
113
+ return pool
114
+ }
115
+
116
+ /**
117
+ * Get or create the Piscina worker pool
118
+ */
119
+ function getPool(): Piscina {
120
+ let pool = getSharedPool()
121
+ if (!pool) {
122
+ pool = createPool()
123
+ setSharedPool(pool)
124
+ }
125
+ return pool
126
+ }
127
+
128
+ /**
129
+ * Load Hanzogui configuration in worker
130
+ * Sends a warmup task to trigger config loading
131
+ * bundleConfig auto-detects if files exist and skips rebuild
132
+ */
133
+ export async function loadHanzogui(options: Partial<HanzoguiOptions>): Promise<any> {
134
+ const pool = getPool()
135
+
136
+ // use extractToClassNames with a dummy request to trigger config loading
137
+ // the worker will cache the config for subsequent requests
138
+ const task = {
139
+ type: 'extractToClassNames',
140
+ source: '// dummy',
141
+ sourcePath: '__dummy__.tsx',
142
+ options: {
143
+ components: ['hanzogui'],
144
+ ...options,
145
+ },
146
+ shouldPrintDebug: false,
147
+ }
148
+
149
+ try {
150
+ await pool.run(task, { name: 'runTask' })
151
+ return { success: true }
152
+ } catch (error) {
153
+ console.error('[static-worker] Error loading Hanzogui config:', error)
154
+ throw error
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Recycle the worker pool to release RSS memory
160
+ * Creates new pool, swaps immediately, then destroys old pool
161
+ * V8 doesn't return memory to OS, so we need to restart the worker periodically
162
+ */
163
+ async function recyclePool(options: HanzoguiOptions): Promise<void> {
164
+ if (isClosing() || isRecycling()) return
165
+
166
+ const oldPool = getSharedPool()
167
+ if (!oldPool) return
168
+
169
+ setRecycling(true)
170
+
171
+ const start = Date.now()
172
+
173
+ try {
174
+ // suppress "Terminating worker thread" messages during recycle
175
+ const originalStderr = process.stderr.write.bind(process.stderr)
176
+ const originalStdout = process.stdout.write.bind(process.stdout)
177
+ const filter = (chunk: any, ...args: any[]) => {
178
+ const str = typeof chunk === 'string' ? chunk : chunk?.toString?.() || ''
179
+ if (str.includes('Terminating worker thread')) return true
180
+ return false
181
+ }
182
+ process.stderr.write = ((chunk: any, ...args: any[]) => {
183
+ if (filter(chunk)) return true
184
+ return originalStderr(chunk, ...args)
185
+ }) as any
186
+ process.stdout.write = ((chunk: any, ...args: any[]) => {
187
+ if (filter(chunk)) return true
188
+ return originalStdout(chunk, ...args)
189
+ }) as any
190
+
191
+ // create new pool and swap immediately
192
+ const newPool = createPool()
193
+ setSharedPool(newPool)
194
+
195
+ // warm up new pool with config (this caches it in the new worker)
196
+ const warmupTask = {
197
+ type: 'extractToClassNames',
198
+ source: '// warmup',
199
+ sourcePath: '__warmup__.tsx',
200
+ options: {
201
+ ...options,
202
+ // skip the "built config" log on warmup since it's a recycle
203
+ _skipBuildLog: true,
204
+ },
205
+ shouldPrintDebug: false,
206
+ }
207
+
208
+ await newPool.run(warmupTask, { name: 'runTask' })
209
+
210
+ // destroy old pool - pending tasks will be rejected
211
+ oldPool.removeAllListeners()
212
+ oldPool.destroy().catch(() => {})
213
+
214
+ // restore stderr/stdout after a delay
215
+ setTimeout(() => {
216
+ process.stderr.write = originalStderr
217
+ process.stdout.write = originalStdout
218
+ })
219
+
220
+ console.log(` ♻️ [hanzogui] recycled worker pool (${Date.now() - start}ms)`)
221
+ } finally {
222
+ setRecycling(false)
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Load Hanzogui build configuration asynchronously
228
+ * Uses esbuild-wasm to avoid EPIPE errors from native esbuild service lifecycle
229
+ */
230
+ export async function loadHanzoguiBuildConfig(
231
+ hanzoguiOptions: Partial<HanzoguiOptions> | undefined
232
+ ): Promise<HanzoguiOptions> {
233
+ const { default: Static } = await import('@hanzogui/static')
234
+
235
+ return Static.loadHanzoguiBuildConfigAsync(hanzoguiOptions)
236
+ }
237
+
238
+ /**
239
+ * Extract Hanzogui components to className-based CSS for web
240
+ */
241
+ export async function extractToClassNames(params: {
242
+ source: string | Buffer
243
+ sourcePath?: string
244
+ options: HanzoguiOptions
245
+ shouldPrintDebug?: boolean | 'verbose'
246
+ }): Promise<any> {
247
+ const { source, sourcePath = '', options, shouldPrintDebug = false } = params
248
+
249
+ if (typeof source !== 'string') {
250
+ throw new Error('`source` must be a string of javascript')
251
+ }
252
+
253
+ const task = {
254
+ type: 'extractToClassNames',
255
+ source,
256
+ sourcePath,
257
+ options,
258
+ shouldPrintDebug,
259
+ }
260
+
261
+ const pool = getPool()
262
+ const result = (await pool.run(task, { name: 'runTask' })) as any
263
+
264
+ if (!result.success) {
265
+ const errorMessage = [
266
+ `[hanzogui-extract] Error processing file: ${sourcePath || '(unknown)'}`,
267
+ ``,
268
+ result.error,
269
+ result.stack ? `\n${result.stack}` : '',
270
+ ]
271
+ .filter(Boolean)
272
+ .join('\n')
273
+
274
+ throw new Error(errorMessage)
275
+ }
276
+
277
+ // check if we need to recycle the worker to prevent RSS bloat
278
+ const count = incrementTaskCount()
279
+ if (count >= MAX_TASKS_BEFORE_RECYCLE) {
280
+ resetTaskCount()
281
+ // recycle asynchronously with hot-swap to not block current request
282
+ recyclePool(options).catch(() => {})
283
+ }
284
+
285
+ return result.data
286
+ }
287
+
288
+ /**
289
+ * Extract Hanzogui components to React Native StyleSheet format
290
+ */
291
+ export async function extractToNative(
292
+ sourceFileName: string,
293
+ sourceCode: string,
294
+ options: HanzoguiOptions
295
+ ): Promise<any> {
296
+ const task = {
297
+ type: 'extractToNative',
298
+ sourceFileName,
299
+ sourceCode,
300
+ options,
301
+ }
302
+
303
+ const pool = getPool()
304
+ const result = (await pool.run(task, { name: 'runTask' })) as any
305
+
306
+ if (!result.success) {
307
+ const errorMessage = [
308
+ `[hanzogui-extract] Error processing file: ${sourceFileName || '(unknown)'}`,
309
+ ``,
310
+ result.error,
311
+ result.stack ? `\n${result.stack}` : '',
312
+ ]
313
+ .filter(Boolean)
314
+ .join('\n')
315
+
316
+ throw new Error(errorMessage)
317
+ }
318
+
319
+ // check if we need to recycle the worker to prevent RSS bloat
320
+ const count = incrementTaskCount()
321
+ if (count >= MAX_TASKS_BEFORE_RECYCLE) {
322
+ resetTaskCount()
323
+ // recycle asynchronously with hot-swap to not block current request
324
+ recyclePool(options).catch(() => {})
325
+ }
326
+
327
+ return result.data
328
+ }
329
+
330
+ /**
331
+ * Watch Hanzogui config for changes and reload when it changes
332
+ */
333
+ export async function watchHanzoguiConfig(
334
+ options: HanzoguiOptions
335
+ ): Promise<{ dispose: () => void } | undefined> {
336
+ // For now, we'll use the static package's watcher directly
337
+ // This could be improved to use worker-based watching
338
+ const { default: Static } = await import('@hanzogui/static')
339
+ const watcher = await Static.watchHanzoguiConfig(options)
340
+
341
+ if (!watcher) {
342
+ return
343
+ }
344
+
345
+ // Wrap the dispose to also clear worker cache
346
+ const originalDispose = watcher.dispose
347
+ return {
348
+ dispose: () => {
349
+ originalDispose()
350
+ if (getSharedPool()) {
351
+ // Fire and forget - errors are handled internally
352
+ clearWorkerCache()
353
+ }
354
+ },
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Clear the worker's config cache
360
+ * Call this when config files change
361
+ */
362
+ export async function clearWorkerCache(): Promise<void> {
363
+ const pool = getSharedPool()
364
+ if (!pool || isClosing()) return
365
+
366
+ const task = { type: 'clearCache' }
367
+ await pool.run(task, { name: 'runTask' })
368
+ }
369
+
370
+ /**
371
+ * Clean up the worker pool on exit
372
+ * Should be called when the build process completes
373
+ */
374
+ export async function destroyPool(): Promise<void> {
375
+ const pool = getSharedPool()
376
+ if (pool) {
377
+ setClosing(true)
378
+ try {
379
+ await pool.close()
380
+ } finally {
381
+ setSharedPool(null)
382
+ setClosing(false)
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Get pool statistics for debugging
389
+ */
390
+ export function getPoolStats() {
391
+ const pool = getSharedPool()
392
+ if (!pool) {
393
+ return null
394
+ }
395
+ return {
396
+ threads: pool.threads.length,
397
+ queueSize: pool.queueSize,
398
+ completed: pool.completed,
399
+ duration: pool.duration,
400
+ utilization: pool.utilization,
401
+ }
402
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @hanzogui/static-worker
3
+ *
4
+ * Pure worker-based API for Hanzogui 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 type { HanzoguiOptions } from '@hanzogui/types';
11
+ export type { ExtractedResponse, HanzoguiProjectInfo } from '@hanzogui/static';
12
+ export type { HanzoguiOptions } from '@hanzogui/types';
13
+ export declare const getPragmaOptions: (props: {
14
+ source: string;
15
+ path: string;
16
+ }) => Promise<{
17
+ shouldPrintDebug: boolean | "verbose";
18
+ shouldDisable: boolean;
19
+ }>;
20
+ /**
21
+ * Load Hanzogui configuration in worker
22
+ * Sends a warmup task to trigger config loading
23
+ * bundleConfig auto-detects if files exist and skips rebuild
24
+ */
25
+ export declare function loadHanzogui(options: Partial<HanzoguiOptions>): Promise<any>;
26
+ /**
27
+ * Load Hanzogui build configuration asynchronously
28
+ * Uses esbuild-wasm to avoid EPIPE errors from native esbuild service lifecycle
29
+ */
30
+ export declare function loadHanzoguiBuildConfig(hanzoguiOptions: Partial<HanzoguiOptions> | undefined): Promise<HanzoguiOptions>;
31
+ /**
32
+ * Extract Hanzogui components to className-based CSS for web
33
+ */
34
+ export declare function extractToClassNames(params: {
35
+ source: string | Buffer;
36
+ sourcePath?: string;
37
+ options: HanzoguiOptions;
38
+ shouldPrintDebug?: boolean | 'verbose';
39
+ }): Promise<any>;
40
+ /**
41
+ * Extract Hanzogui components to React Native StyleSheet format
42
+ */
43
+ export declare function extractToNative(sourceFileName: string, sourceCode: string, options: HanzoguiOptions): Promise<any>;
44
+ /**
45
+ * Watch Hanzogui config for changes and reload when it changes
46
+ */
47
+ export declare function watchHanzoguiConfig(options: HanzoguiOptions): Promise<{
48
+ dispose: () => void;
49
+ } | undefined>;
50
+ /**
51
+ * Clear the worker's config cache
52
+ * Call this when config files change
53
+ */
54
+ export declare function clearWorkerCache(): Promise<void>;
55
+ /**
56
+ * Clean up the worker pool on exit
57
+ * Should be called when the build process completes
58
+ */
59
+ export declare function destroyPool(): Promise<void>;
60
+ /**
61
+ * Get pool statistics for debugging
62
+ */
63
+ export declare function getPoolStats(): {
64
+ threads: number;
65
+ queueSize: number;
66
+ completed: number;
67
+ duration: number;
68
+ utilization: number;
69
+ } | null;
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAItD,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAC9E,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,gBAAgB,GAAU,OAAO;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;;;EAG7E,CAAA;AA2GD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAuBlF;AAsED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,eAAe,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,GACpD,OAAO,CAAC,eAAe,CAAC,CAI1B;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,eAAe,CAAA;IACxB,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACvC,GAAG,OAAO,CAAC,GAAG,CAAC,CAwCf;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,GAAG,CAAC,CAiCd;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,SAAS,CAAC,CAqB9C;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMtD;AAED;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAWjD;AAED;;GAEG;AACH,wBAAgB,YAAY;;;;;;SAY3B"}