@meonode/canvas 1.7.2 → 2.0.1

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.
Files changed (85) hide show
  1. package/README.md +324 -154
  2. package/dist/cjs/canvas/canvas.type.d.ts +72 -9
  3. package/dist/cjs/canvas/canvas.type.d.ts.map +1 -1
  4. package/dist/cjs/canvas/{chart.canvas.util.d.ts → chart.canvas.d.ts} +2 -2
  5. package/dist/cjs/canvas/chart.canvas.d.ts.map +1 -0
  6. package/dist/cjs/canvas/{chart.canvas.util.js → chart.canvas.js} +23 -23
  7. package/dist/cjs/canvas/chart.canvas.js.map +1 -0
  8. package/dist/{esm/canvas/grid.canvas.util.d.ts → cjs/canvas/grid.canvas.d.ts} +2 -2
  9. package/dist/cjs/canvas/grid.canvas.d.ts.map +1 -0
  10. package/dist/cjs/canvas/{grid.canvas.util.js → grid.canvas.js} +4 -4
  11. package/dist/cjs/canvas/grid.canvas.js.map +1 -0
  12. package/dist/cjs/canvas/image.canvas.d.ts +54 -0
  13. package/dist/cjs/canvas/image.canvas.d.ts.map +1 -0
  14. package/dist/cjs/canvas/{image.canvas.util.js → image.canvas.js} +40 -140
  15. package/dist/cjs/canvas/image.canvas.js.map +1 -0
  16. package/dist/{esm/canvas/layout.canvas.util.d.ts → cjs/canvas/layout.canvas.d.ts} +1 -1
  17. package/dist/cjs/canvas/layout.canvas.d.ts.map +1 -0
  18. package/dist/cjs/canvas/{layout.canvas.util.js → layout.canvas.js} +1 -1
  19. package/dist/cjs/canvas/layout.canvas.js.map +1 -0
  20. package/dist/{esm/canvas/root.canvas.util.d.ts → cjs/canvas/root.canvas.d.ts} +29 -9
  21. package/dist/cjs/canvas/root.canvas.d.ts.map +1 -0
  22. package/dist/cjs/canvas/{root.canvas.util.js → root.canvas.js} +75 -55
  23. package/dist/cjs/canvas/root.canvas.js.map +1 -0
  24. package/dist/cjs/canvas/{text.canvas.util.d.ts → text.canvas.d.ts} +2 -2
  25. package/dist/cjs/canvas/text.canvas.d.ts.map +1 -0
  26. package/dist/cjs/canvas/{text.canvas.util.js → text.canvas.js} +3 -3
  27. package/dist/cjs/canvas/text.canvas.js.map +1 -0
  28. package/dist/cjs/index.d.ts +8 -7
  29. package/dist/cjs/index.d.ts.map +1 -1
  30. package/dist/cjs/index.js +19 -17
  31. package/dist/cjs/index.js.map +1 -1
  32. package/dist/cjs/util/disk.cache.d.ts +6 -0
  33. package/dist/cjs/util/disk.cache.d.ts.map +1 -1
  34. package/dist/cjs/util/disk.cache.js +39 -0
  35. package/dist/cjs/util/disk.cache.js.map +1 -1
  36. package/dist/cjs/worker/render.worker.js +2 -2
  37. package/dist/cjs/worker/render.worker.js.map +1 -1
  38. package/dist/esm/canvas/canvas.type.d.ts +72 -9
  39. package/dist/esm/canvas/canvas.type.d.ts.map +1 -1
  40. package/dist/esm/canvas/{chart.canvas.util.d.ts → chart.canvas.d.ts} +2 -2
  41. package/dist/esm/canvas/chart.canvas.d.ts.map +1 -0
  42. package/dist/esm/canvas/{chart.canvas.util.js → chart.canvas.js} +4 -4
  43. package/dist/{cjs/canvas/grid.canvas.util.d.ts → esm/canvas/grid.canvas.d.ts} +2 -2
  44. package/dist/esm/canvas/grid.canvas.d.ts.map +1 -0
  45. package/dist/esm/canvas/{grid.canvas.util.js → grid.canvas.js} +1 -1
  46. package/dist/esm/canvas/image.canvas.d.ts +54 -0
  47. package/dist/esm/canvas/image.canvas.d.ts.map +1 -0
  48. package/dist/esm/canvas/{image.canvas.util.js → image.canvas.js} +39 -136
  49. package/dist/{cjs/canvas/layout.canvas.util.d.ts → esm/canvas/layout.canvas.d.ts} +1 -1
  50. package/dist/esm/canvas/layout.canvas.d.ts.map +1 -0
  51. package/dist/{cjs/canvas/root.canvas.util.d.ts → esm/canvas/root.canvas.d.ts} +29 -9
  52. package/dist/esm/canvas/root.canvas.d.ts.map +1 -0
  53. package/dist/esm/canvas/{root.canvas.util.js → root.canvas.js} +63 -44
  54. package/dist/esm/canvas/{text.canvas.util.d.ts → text.canvas.d.ts} +2 -2
  55. package/dist/esm/canvas/text.canvas.d.ts.map +1 -0
  56. package/dist/esm/canvas/{text.canvas.util.js → text.canvas.js} +1 -1
  57. package/dist/esm/index.d.ts +8 -7
  58. package/dist/esm/index.d.ts.map +1 -1
  59. package/dist/esm/index.js +7 -6
  60. package/dist/esm/util/disk.cache.d.ts +6 -0
  61. package/dist/esm/util/disk.cache.d.ts.map +1 -1
  62. package/dist/esm/util/disk.cache.js +38 -1
  63. package/dist/esm/worker/render.worker.js +1 -1
  64. package/package.json +3 -1
  65. package/dist/cjs/canvas/chart.canvas.util.d.ts.map +0 -1
  66. package/dist/cjs/canvas/chart.canvas.util.js.map +0 -1
  67. package/dist/cjs/canvas/grid.canvas.util.d.ts.map +0 -1
  68. package/dist/cjs/canvas/grid.canvas.util.js.map +0 -1
  69. package/dist/cjs/canvas/image.canvas.util.d.ts +0 -82
  70. package/dist/cjs/canvas/image.canvas.util.d.ts.map +0 -1
  71. package/dist/cjs/canvas/image.canvas.util.js.map +0 -1
  72. package/dist/cjs/canvas/layout.canvas.util.d.ts.map +0 -1
  73. package/dist/cjs/canvas/layout.canvas.util.js.map +0 -1
  74. package/dist/cjs/canvas/root.canvas.util.d.ts.map +0 -1
  75. package/dist/cjs/canvas/root.canvas.util.js.map +0 -1
  76. package/dist/cjs/canvas/text.canvas.util.d.ts.map +0 -1
  77. package/dist/cjs/canvas/text.canvas.util.js.map +0 -1
  78. package/dist/esm/canvas/chart.canvas.util.d.ts.map +0 -1
  79. package/dist/esm/canvas/grid.canvas.util.d.ts.map +0 -1
  80. package/dist/esm/canvas/image.canvas.util.d.ts +0 -82
  81. package/dist/esm/canvas/image.canvas.util.d.ts.map +0 -1
  82. package/dist/esm/canvas/layout.canvas.util.d.ts.map +0 -1
  83. package/dist/esm/canvas/root.canvas.util.d.ts.map +0 -1
  84. package/dist/esm/canvas/text.canvas.util.d.ts.map +0 -1
  85. /package/dist/esm/canvas/{layout.canvas.util.js → layout.canvas.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { loadImage } from 'skia-canvas';
2
- import { BoxNode } from './layout.canvas.util.js';
2
+ import { BoxNode } from './layout.canvas.js';
3
3
  import { parseBorderRadius, drawRoundedRectPath } from './canvas.helper.js';
4
4
  import { promises } from 'fs';
5
5
  import { Style } from '../constant/common.const.js';
@@ -21,86 +21,6 @@ function calculateOffsetFromValue(positionValue, availableSpace) {
21
21
  console.warn(`[ImageNode] Invalid objectPosition value format: ${value}. Defaulting to 50%.`);
22
22
  return availableSpace * 0.5;
23
23
  }
24
- /**
25
- * A simple LRU cache for resolved `CanvasImage` objects.
26
- *
27
- * - Persists across render passes so repeated renders of the same tree don't
28
- * re-fetch every image.
29
- * - Bounded by `maxSize` entries; least-recently-used entries are evicted first.
30
- * - Call `dispose()` to eagerly release all held images, or rely on the
31
- * automatic `process.on('exit')` hook that clears the singleton.
32
- */
33
- class ImageLRUCache {
34
- map = new Map();
35
- maxSize;
36
- constructor(maxSize) {
37
- this.maxSize = maxSize;
38
- }
39
- get(key) {
40
- const entry = this.map.get(key);
41
- if (!entry)
42
- return undefined;
43
- // Move to end (most-recently used)
44
- this.map.delete(key);
45
- this.map.set(key, entry);
46
- return entry.image;
47
- }
48
- set(key, image) {
49
- // If key already exists, refresh it
50
- if (this.map.has(key)) {
51
- this.map.delete(key);
52
- }
53
- // Evict oldest if at capacity
54
- while (this.map.size >= this.maxSize) {
55
- const oldest = this.map.keys().next().value;
56
- this.map.delete(oldest);
57
- }
58
- this.map.set(key, { image, key });
59
- }
60
- has(key) {
61
- return this.map.has(key);
62
- }
63
- get size() {
64
- return this.map.size;
65
- }
66
- dispose() {
67
- this.map.clear();
68
- }
69
- }
70
- /** Module-level singleton — lazily created on first render. */
71
- let _globalImageCache = null;
72
- const DEFAULT_CACHE_SIZE = 128;
73
- // Symbol key on process to track hook registration across module reloads (e.g. Jest resetModules)
74
- const HOOK_KEY = Symbol.for('__meonode_canvas_image_cache_hook__');
75
- /**
76
- * Returns the singleton `ImageLRUCache`, creating it on first access.
77
- * Registers a one-time process cleanup hook to clear the cache
78
- * so native image buffers are freed when the process shuts down.
79
- */
80
- function getImageCache(maxSize = DEFAULT_CACHE_SIZE) {
81
- if (!_globalImageCache) {
82
- _globalImageCache = new ImageLRUCache(maxSize);
83
- }
84
- if (!globalThis[HOOK_KEY]) {
85
- globalThis[HOOK_KEY] = true;
86
- const cleanup = () => {
87
- _globalImageCache?.dispose();
88
- _globalImageCache = null;
89
- };
90
- process.once('exit', cleanup);
91
- process.once('SIGINT', cleanup);
92
- process.once('SIGTERM', cleanup);
93
- }
94
- return _globalImageCache;
95
- }
96
- /**
97
- * Explicitly disposes the global image cache.
98
- * Useful in tests or when tearing down the rendering engine.
99
- */
100
- function disposeImageCache() {
101
- _globalImageCache?.dispose();
102
- _globalImageCache = null;
103
- }
104
24
  /**
105
25
  * Renders images with configurable sizing, positioning, and effects.
106
26
  * Supports object-fit modes, positioning, border radius, and saturation filters.
@@ -120,9 +40,9 @@ class ImageNode extends BoxNode {
120
40
  ...props,
121
41
  };
122
42
  }
123
- load(cache) {
43
+ load(cache, diskCacheKeys) {
124
44
  if (!this.loadingPromise) {
125
- this.loadingPromise = this._loadImage(cache);
45
+ this.loadingPromise = this._loadImage(cache, diskCacheKeys);
126
46
  }
127
47
  return this.loadingPromise;
128
48
  }
@@ -130,10 +50,10 @@ class ImageNode extends BoxNode {
130
50
  * Fetches and processes the image source into a CanvasImage.
131
51
  * Does not touch node state — pure fetch logic.
132
52
  *
133
- * If `diskCacheKey` is provided, the resolved image buffer is written to the
134
- * disk cache at `.cache/files/<diskCacheKey>` (best-effort, fire-and-forget).
53
+ * If `diskCacheKey` and `diskCacheKeys` are provided, the resolved image buffer
54
+ * is written to disk and the key is recorded so the caller can clean it up later.
135
55
  */
136
- async _fetchCanvasImage(diskCacheKey) {
56
+ async _fetchCanvasImage(diskCacheKey, diskCacheKeys) {
137
57
  const { fileTypeFromBuffer, fileTypeFromFile } = await import('file-type');
138
58
  let finalSource = this.props.src;
139
59
  let isSvg = false;
@@ -192,11 +112,13 @@ class ImageNode extends BoxNode {
192
112
  const modifiedSvgString = svgString.replace(/fill="[^"]*"/g, `fill="${this.props.color}"`);
193
113
  finalSource = modifiedSvgString !== svgString ? Buffer.from(modifiedSvgString) : contentBuffer;
194
114
  }
195
- // Write resolved buffer to disk cache (fire-and-forget, non-fatal)
196
- if (diskCacheKey) {
115
+ // Write to disk and track the key so the render owner can clean it up
116
+ if (diskCacheKey && diskCacheKeys) {
197
117
  const cacheBuffer = Buffer.isBuffer(finalSource) ? finalSource : contentBuffer;
198
- if (cacheBuffer)
199
- writeDiskCache(diskCacheKey, cacheBuffer);
118
+ if (cacheBuffer) {
119
+ await writeDiskCache(diskCacheKey, cacheBuffer);
120
+ diskCacheKeys.add(diskCacheKey);
121
+ }
200
122
  }
201
123
  return loadImage(finalSource);
202
124
  }
@@ -204,14 +126,15 @@ class ImageNode extends BoxNode {
204
126
  * Loads and processes an image.
205
127
  *
206
128
  * Resolution order:
207
- * 1. Persistent LRU cache (cross-render) instant hit, no I/O.
208
- * 2. Disk cache at `.cache/files/<hash>`survives process restarts.
209
- * 3. Per-render dedup cache avoids duplicate in-flight fetches within a single render.
210
- * 4. Fresh fetch via `_fetchCanvasImage()` — writes buffer to disk cache after fetch.
129
+ * 1. Disk cache at `.cache/files/<hash>`survives process restarts.
130
+ * 2. Per-render dedup cache — avoids duplicate in-flight fetches when
131
+ * multiple ImageNodes share the same src within one render pass.
132
+ * 3. Fresh fetch via `_fetchCanvasImage()` — writes buffer to disk cache.
211
133
  *
212
134
  * Buffer sources use a SHA-256 hash as their cache key (same as string sources).
135
+ * All resolved images are released when the render completes (no cross-render retention).
213
136
  */
214
- _loadImage(cache) {
137
+ _loadImage(cache, diskCacheKeys) {
215
138
  if (!this.props.src) {
216
139
  const aspectRatioFromProps = typeof this.props.aspectRatio === 'number' && this.props.aspectRatio > 0 ? this.props.aspectRatio : undefined;
217
140
  this.node.setAspectRatio(aspectRatioFromProps);
@@ -222,56 +145,36 @@ class ImageNode extends BoxNode {
222
145
  return new Promise(resolve => {
223
146
  const load = async () => {
224
147
  try {
225
- const lru = getImageCache();
226
- const cacheKey = typeof this.props.src === 'string'
227
- ? this.props.color
228
- ? `${this.props.src}|${this.props.color}`
229
- : this.props.src
230
- : this.props.color
231
- ? `${hashBuffer(this.props.src)}|${this.props.color}`
232
- : hashBuffer(this.props.src);
233
- // 1. Check persistent LRU cache
234
- const cached = lru.get(cacheKey);
235
- if (cached) {
236
- this.loadedImage = cached;
237
- this.naturalWidth = cached.width;
238
- this.naturalHeight = cached.height;
239
- const calculatedAspectRatio = cached.width > 0 && cached.height > 0 ? cached.width / cached.height : undefined;
240
- const finalAspectRatio = typeof this.props.aspectRatio === 'number' && this.props.aspectRatio > 0 ? this.props.aspectRatio : calculatedAspectRatio;
241
- this.node.setAspectRatio(finalAspectRatio);
242
- this.props.onLoad?.();
243
- resolve();
244
- return;
245
- }
246
- // 2. Check disk cache (persists across process restarts)
247
- const diskBuffer = await readDiskCache(cacheKey);
248
- if (diskBuffer) {
249
- const img = await loadImage(diskBuffer);
250
- lru.set(cacheKey, img);
251
- this.loadedImage = img;
252
- this.naturalWidth = img.width;
253
- this.naturalHeight = img.height;
254
- const calculatedAspectRatio = img.width > 0 && img.height > 0 ? img.width / img.height : undefined;
255
- const finalAspectRatio = typeof this.props.aspectRatio === 'number' && this.props.aspectRatio > 0 ? this.props.aspectRatio : calculatedAspectRatio;
256
- this.node.setAspectRatio(finalAspectRatio);
257
- this.props.onLoad?.();
258
- resolve();
259
- return;
148
+ const srcHash = typeof this.props.src === 'string' ? hashBuffer(Buffer.from(this.props.src)) : hashBuffer(this.props.src);
149
+ const cacheKey = this.props.color ? `${srcHash}|${this.props.color}` : srcHash;
150
+ // 1. Disk cache read — only when disk caching is enabled for this render
151
+ if (diskCacheKeys) {
152
+ const diskBuffer = await readDiskCache(cacheKey);
153
+ if (diskBuffer) {
154
+ const img = await loadImage(diskBuffer);
155
+ this.loadedImage = img;
156
+ this.naturalWidth = img.width;
157
+ this.naturalHeight = img.height;
158
+ const calculatedAspectRatio = img.width > 0 && img.height > 0 ? img.width / img.height : undefined;
159
+ const finalAspectRatio = typeof this.props.aspectRatio === 'number' && this.props.aspectRatio > 0 ? this.props.aspectRatio : calculatedAspectRatio;
160
+ this.node.setAspectRatio(finalAspectRatio);
161
+ this.props.onLoad?.();
162
+ resolve();
163
+ return;
164
+ }
260
165
  }
261
- // 3. Per-render dedup cache or fresh fetch (writes to disk internally)
166
+ // 2. Per-render memory dedup cache or fresh fetch
262
167
  let imagePromise;
263
168
  if (cache) {
264
169
  if (!cache.has(cacheKey)) {
265
- cache.set(cacheKey, this._fetchCanvasImage(cacheKey));
170
+ cache.set(cacheKey, this._fetchCanvasImage(diskCacheKeys ? cacheKey : undefined, diskCacheKeys));
266
171
  }
267
172
  imagePromise = cache.get(cacheKey);
268
173
  }
269
174
  else {
270
- imagePromise = this._fetchCanvasImage(cacheKey);
175
+ imagePromise = this._fetchCanvasImage(diskCacheKeys ? cacheKey : undefined, diskCacheKeys);
271
176
  }
272
177
  const img = await imagePromise;
273
- // 4. Store in persistent LRU cache
274
- lru.set(cacheKey, img);
275
178
  this.loadedImage = img;
276
179
  this.naturalWidth = img.width;
277
180
  this.naturalHeight = img.height;
@@ -452,4 +355,4 @@ const Image = (props) => ({
452
355
  props: props,
453
356
  });
454
357
 
455
- export { Image, ImageLRUCache, ImageNode, disposeImageCache, getImageCache };
358
+ export { Image, ImageNode };
@@ -119,4 +119,4 @@ export declare class RowNode extends BoxNode {
119
119
  * @returns {RowNode} New RowNode instance.
120
120
  */
121
121
  export declare const Row: ({ children, ...rest }: BoxProps) => CanvasElement;
122
- //# sourceMappingURL=layout.canvas.util.d.ts.map
122
+ //# sourceMappingURL=layout.canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.canvas.d.ts","sourceRoot":"","sources":["../../../src/canvas/layout.canvas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAkB,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAGjG,OAAa,EAAS,IAAI,EAAE,MAAM,4BAA4B,CAAA;AAE9D;;;;GAIG;AACH,qBAAa,OAAO;IAClB;;OAEG;IACH,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAE/B;;OAEG;IACH,IAAI,EAAE,IAAI,CAAA;IAEV;;OAEG;IACH,QAAQ,EAAE,OAAO,EAAE,CAAA;IAEnB;;OAEG;IACH,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IAEtB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IAEZ;;;OAGG;gBACS,KAAK,GAAE,QAAQ,GAAG,SAAc;IAqB5C;;OAEG;IACI,sBAAsB;IAW7B;;;OAGG;IACH,SAAS,CAAC,sBAAsB,CAAC,WAAW,EAAE,QAAQ,GAAG,SAAS;IAkClE;;OAEG;IACH,SAAS,CAAC,aAAa,IAAI,IAAI;IAI/B;;;;OAIG;IACH,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM;IAanD;;;OAGG;IACI,cAAc,IAAI,OAAO;IAiBhC;;OAEG;IACH,SAAS,CAAC,+BAA+B;IAIzC;;;OAGG;IACH,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ;IAqInC;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,wBAAwB,EAAE,OAAO,GAAE,MAAU,EAAE,OAAO,GAAE,MAAU;IA+J9E;;;;;;;;OAQG;IACH,SAAS,CAAC,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAoR5G;AAWD;;;;GAIG;AACH,eAAO,MAAM,GAAG,GAAI,uBAAuB,QAAQ,KAAG,aAIpD,CAAA;AAEF;;;GAGG;AACH,qBAAa,UAAW,SAAQ,OAAO;gBACzB,KAAK,GAAE,QAAQ,GAAG,SAAc;CAS7C;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,GAAI,uBAAuB,QAAQ,KAAG,aAIvD,CAAA;AAEF;;;GAGG;AACH,qBAAa,OAAQ,SAAQ,OAAO;gBACtB,KAAK,GAAE,QAAQ,GAAG,SAAc;CAS7C;AAED;;;;GAIG;AACH,eAAO,MAAM,GAAG,GAAI,uBAAuB,QAAQ,KAAG,aAIpD,CAAA"}
@@ -1,20 +1,25 @@
1
1
  import { Canvas } from 'skia-canvas';
2
- import { ColumnNode, BoxNode } from '../canvas/layout.canvas.util.js';
3
- import type { BaseProps, RootProps, CanvasElement } from '../canvas/canvas.type.js';
2
+ import { ColumnNode, BoxNode } from '../canvas/layout.canvas.js';
3
+ import type { BaseProps, RootProps, CanvasElement, RootPropsWithWorker, RootPropsWithoutWorker } from '../canvas/canvas.type.js';
4
4
  export declare const _clearRegisteredFonts: () => void;
5
5
  export interface CanvasEngineConfig {
6
6
  /** Run rendering in worker threads to avoid blocking the event loop (default: true) */
7
7
  workerMode?: boolean;
8
8
  /** Number of worker threads in the pool (default: os.cpus().length - 1) */
9
9
  workers?: number;
10
- /** Maximum number of resolved images to keep in the persistent LRU cache (default: 128) */
11
- imageCacheSize?: number;
12
10
  }
13
11
  /**
14
12
  * Configure the canvas rendering engine.
15
13
  * Call this once at application startup before rendering.
14
+ * @deprecated Pass workerMode and workers directly to Root() props instead.
16
15
  */
17
16
  export declare function configure(options: CanvasEngineConfig): void;
17
+ /**
18
+ * Terminate all worker pools and free worker thread resources.
19
+ * Call this when shutting down a long-running server to clean up immediately.
20
+ * After calling, you must call configure() again before rendering.
21
+ */
22
+ export declare function terminate(): void;
18
23
  /**
19
24
  * Converts a CanvasElement tree into actual BoxNode instances.
20
25
  * Used both for non-worker rendering (inline tree building) and inside
@@ -26,6 +31,7 @@ export declare function buildTree(descriptor: CanvasElement): BoxNode;
26
31
  * Inherits from ColumnNode to provide vertical layout capabilities.
27
32
  */
28
33
  export declare class RootNode extends ColumnNode {
34
+ props: RootProps & BaseProps;
29
35
  /** The canvas instance used for rendering */
30
36
  private canvas;
31
37
  /** The 2D rendering context for the canvas */
@@ -56,10 +62,24 @@ export declare class RootNode extends ColumnNode {
56
62
  }
57
63
  /**
58
64
  * Creates and renders a new root node with the given properties.
59
- * When worker mode is enabled via configure(), rendering runs in a worker thread
60
- * and the returned object implements the same toBuffer/toBufferSync interface.
65
+ * Rendering runs in worker threads by default for non-blocking operation.
66
+ * @example
67
+ * // Worker mode (default) - .release() available
68
+ * const canvas = await Root({ width: 400, children: [...] })
69
+ * canvas.release() // ✓ OK
70
+ * @example
71
+ * // Worker mode explicit - .release() available
72
+ * const canvas = await Root({ width: 400, workerMode: true, workers: 2 })
73
+ * canvas.release() // ✓ OK
74
+ * @example
75
+ * // Non-worker mode - .release() NOT available, workers not allowed
76
+ * const canvas = await Root({ width: 400, workerMode: false })
77
+ * canvas.release() // ✗ TypeScript error
61
78
  * @param props Configuration properties for the root node
62
- * @returns Promise resolving to the rendered Canvas (or WorkerCanvas in worker mode)
79
+ * @returns Canvas with .release() in worker mode, plain Canvas otherwise
63
80
  */
64
- export declare const Root: (props: RootProps) => Promise<Canvas>;
65
- //# sourceMappingURL=root.canvas.util.d.ts.map
81
+ export declare function Root(props: RootPropsWithWorker): Promise<Canvas & {
82
+ release(): void;
83
+ }>;
84
+ export declare function Root(props: RootPropsWithoutWorker): Promise<Canvas>;
85
+ //# sourceMappingURL=root.canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.canvas.d.ts","sourceRoot":"","sources":["../../../src/canvas/root.canvas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA8C,MAAM,aAAa,CAAA;AAEhF,OAAO,EAAE,UAAU,EAAE,OAAO,EAAW,MAAM,2BAA2B,CAAA;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAmB/H,eAAO,MAAM,qBAAqB,YAEjC,CAAA;AAwBD,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,kBAAkB,QAGpD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,SAKxB;AAgMD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAmB5D;AAED;;;GAGG;AACH,qBAAa,QAAS,SAAQ,UAAU;IAC9B,KAAK,EAAE,SAAS,GAAG,SAAS,CAAA;IACpC,6CAA6C;IAC7C,OAAO,CAAC,MAAM,CAAoB;IAClC,8CAA8C;IAC9C,OAAO,CAAC,GAAG,CAAwC;IACnD,4CAA4C;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAE9B;;;;OAIG;gBACS,KAAK,EAAE,SAAS,GAAG,SAAS;IAoDxC;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAazB;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;CAqDhC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,CAAC,CAAA;AACvF,wBAAgB,IAAI,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA"}
@@ -1,9 +1,10 @@
1
1
  import { FontLibrary, Canvas } from 'skia-canvas';
2
- import { ColumnNode, RowNode, BoxNode } from './layout.canvas.util.js';
3
- import { ImageNode, disposeImageCache, getImageCache } from './image.canvas.util.js';
4
- import { TextNode } from './text.canvas.util.js';
5
- import { ChartNode } from './chart.canvas.util.js';
6
- import { GridItemNode, GridNode } from './grid.canvas.util.js';
2
+ import { ColumnNode, RowNode, BoxNode } from './layout.canvas.js';
3
+ import { ImageNode } from './image.canvas.js';
4
+ import { deleteDiskCache } from '../util/disk.cache.js';
5
+ import { TextNode } from './text.canvas.js';
6
+ import { ChartNode } from './chart.canvas.js';
7
+ import { GridItemNode, GridNode } from './grid.canvas.js';
7
8
  import { Style } from '../constant/common.const.js';
8
9
  import { WorkerPreProcessor } from './canvas.helper.js';
9
10
  import * as path from 'node:path';
@@ -14,26 +15,48 @@ import { fileURLToPath } from 'node:url';
14
15
 
15
16
  /** Registry to track fonts that have already been loaded */
16
17
  const registeredFonts = new Map();
17
- /** Engine configuration */
18
- let _workerMode = true;
19
- let _workerPoolSize = Math.max(1, cpus().length - 1);
18
+ /**
19
+ * FinalizationRegistry to clean up WorkerCanvas instances that were not explicitly released.
20
+ * This is a safety net — users should still call .release() explicitly.
21
+ */
22
+ const canvasRegistry = new FinalizationRegistry(heldValue => {
23
+ // Best-effort cleanup — worker may already be terminated
24
+ try {
25
+ // Access workers via a public method or make it accessible
26
+ // For now, just try to send the message and let errors be caught
27
+ if (_workerPool) {
28
+ ;
29
+ _workerPool.workers?.[heldValue.workerIdx]?.postMessage({ type: 'release', canvasId: heldValue.canvasId });
30
+ }
31
+ }
32
+ catch {
33
+ // Worker already gone — nothing to clean up
34
+ }
35
+ });
36
+ /** Engine configuration — legacy support for configure() */
37
+ let _defaultWorkerMode = true;
38
+ let _defaultWorkerPoolSize = Math.max(1, cpus().length - 1);
20
39
  let _workerPool = null;
21
40
  /**
22
41
  * Configure the canvas rendering engine.
23
42
  * Call this once at application startup before rendering.
43
+ * @deprecated Pass workerMode and workers directly to Root() props instead.
24
44
  */
25
45
  function configure(options) {
26
46
  if (options.workerMode !== undefined)
27
- _workerMode = options.workerMode;
47
+ _defaultWorkerMode = options.workerMode;
28
48
  if (options.workers !== undefined)
29
- _workerPoolSize = options.workers;
30
- if (options.imageCacheSize !== undefined) {
31
- // Dispose existing cache and create a new one with the specified size
32
- disposeImageCache();
33
- getImageCache(options.imageCacheSize);
34
- }
35
- if (_workerMode) {
36
- _workerPool = new WorkerPool(_workerPoolSize);
49
+ _defaultWorkerPoolSize = options.workers;
50
+ }
51
+ /**
52
+ * Terminate all worker pools and free worker thread resources.
53
+ * Call this when shutting down a long-running server to clean up immediately.
54
+ * After calling, you must call configure() again before rendering.
55
+ */
56
+ function terminate() {
57
+ if (_workerPool) {
58
+ _workerPool.terminate();
59
+ _workerPool = null;
37
60
  }
38
61
  }
39
62
  /**
@@ -55,6 +78,8 @@ class WorkerCanvas {
55
78
  this._pool = opts.pool;
56
79
  this._workerIdx = opts.workerIdx;
57
80
  this._canvasId = opts.canvasId;
81
+ // Register for finalizer-based cleanup if user forgets to call .release()
82
+ canvasRegistry.register(this, { workerIdx: opts.workerIdx, canvasId: opts.canvasId }, this);
58
83
  }
59
84
  _call(method, ...args) {
60
85
  return this._pool.callOnCanvas(this._workerIdx, this._canvasId, method, args);
@@ -105,6 +130,8 @@ class WorkerCanvas {
105
130
  /** Release the Canvas from worker memory. Call when done with this object. */
106
131
  release() {
107
132
  this._pool.releaseCanvas(this._workerIdx, this._canvasId);
133
+ // Unregister from finalizer since we're explicitly cleaning up
134
+ canvasRegistry.unregister(this);
108
135
  }
109
136
  }
110
137
  /** Worker thread pool — routes render and canvas-call messages */
@@ -295,6 +322,7 @@ class RootNode extends ColumnNode {
295
322
  * @returns Promise resolving to the rendered Canvas instance
296
323
  */
297
324
  async render() {
325
+ const diskCacheKeys = this.props.useDiskCache ? new Set() : undefined;
298
326
  try {
299
327
  // Step 1: Load all images with a concurrency limit to avoid overwhelming remote sources.
300
328
  // A per-render cache deduplicates identical src+color combinations within this render pass.
@@ -306,7 +334,7 @@ class RootNode extends ColumnNode {
306
334
  const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
307
335
  while (queue.length > 0) {
308
336
  const node = queue.shift();
309
- await node.load(imageCache);
337
+ await node.load(imageCache, diskCacheKeys);
310
338
  }
311
339
  });
312
340
  await Promise.allSettled(workers);
@@ -334,35 +362,26 @@ class RootNode extends ColumnNode {
334
362
  return this.canvas;
335
363
  }
336
364
  finally {
337
- // Always clear the persistent image cache after render (success or error)
338
- // so resolved CanvasImage references don't outlive the render pass.
339
- disposeImageCache();
365
+ if (diskCacheKeys?.size) {
366
+ await Promise.allSettled([...diskCacheKeys].map(key => deleteDiskCache(key)));
367
+ }
340
368
  }
341
369
  }
342
370
  }
343
- /**
344
- * Creates and renders a new root node with the given properties.
345
- * When worker mode is enabled via configure(), rendering runs in a worker thread
346
- * and the returned object implements the same toBuffer/toBufferSync interface.
347
- * @param props Configuration properties for the root node
348
- * @returns Promise resolving to the rendered Canvas (or WorkerCanvas in worker mode)
349
- */
350
- const Root = async (props) => {
351
- try {
352
- if (_workerMode) {
353
- if (!_workerPool) {
354
- _workerPool = new WorkerPool(_workerPoolSize);
355
- }
356
- const result = await _workerPool.render(props);
357
- return new WorkerCanvas({ ...result, pool: _workerPool });
371
+ async function Root(props) {
372
+ // Determine worker mode: props override legacy configure()
373
+ const workerMode = props.workerMode ?? _defaultWorkerMode;
374
+ const workerPoolSize = props.workers ?? _defaultWorkerPoolSize;
375
+ if (workerMode) {
376
+ // Lazy initialize worker pool
377
+ if (!_workerPool) {
378
+ _workerPool = new WorkerPool(workerPoolSize);
358
379
  }
359
- return await new RootNode(props).render();
380
+ const result = await _workerPool.render(props);
381
+ return new WorkerCanvas({ ...result, pool: _workerPool });
360
382
  }
361
- catch (err) {
362
- // Ensure cache is cleared even if Root-level orchestration fails
363
- disposeImageCache();
364
- throw err;
365
- }
366
- };
383
+ // Non-worker mode — render directly and return Canvas
384
+ return new RootNode(props).render();
385
+ }
367
386
 
368
- export { Root, RootNode, buildTree, configure };
387
+ export { Root, RootNode, buildTree, configure, terminate };
@@ -1,6 +1,6 @@
1
1
  import type { TextProps, CanvasElement } from '../canvas/canvas.type.js';
2
2
  import { type CanvasRenderingContext2D } from 'skia-canvas';
3
- import { BoxNode } from '../canvas/layout.canvas.util.js';
3
+ import { BoxNode } from '../canvas/layout.canvas.js';
4
4
  /**
5
5
  * Node for rendering text content with rich text styling support
6
6
  * Supports color and weight variations through HTML-like tags
@@ -162,4 +162,4 @@ export declare class TextNode extends BoxNode {
162
162
  * Creates a new TextNode instance with rich text support
163
163
  */
164
164
  export declare const Text: (text: number | string, props?: TextProps) => CanvasElement;
165
- //# sourceMappingURL=text.canvas.util.d.ts.map
165
+ //# sourceMappingURL=text.canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.canvas.d.ts","sourceRoot":"","sources":["../../../src/canvas/text.canvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAe,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACpF,OAAO,EAAU,KAAK,wBAAwB,EAA2B,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGnD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,kBAAkB,CAAe;IAEjC,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAElC,IAAI,GAAE,MAAM,GAAG,MAAW,EAAE,KAAK,GAAE,SAAc;IAuB7D;;;;;;;;OAQG;WACW,gBAAgB,CAC5B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,GAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QACpC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAClC,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA;QACjD,YAAY,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;KACnD;cAwBW,aAAa,IAAI,IAAI;IAoDxC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAyBxB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,WAAW;IA+NnB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IA6KpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAmErB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;OAgBG;cACgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAkXrH;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,KAAG,aAI9D,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import { Canvas } from 'skia-canvas';
2
- import { BoxNode } from './layout.canvas.util.js';
2
+ import { BoxNode } from './layout.canvas.js';
3
3
  import { Style } from '../constant/common.const.js';
4
4
 
5
5
  /**
@@ -1,10 +1,11 @@
1
1
  export * from './constant/common.const.js';
2
2
  export * from './canvas/canvas.type.js';
3
- export { Box, Column, Row, type BoxNode } from './canvas/layout.canvas.util.js';
4
- export { Image, disposeImageCache } from './canvas/image.canvas.util.js';
5
- export { Text } from './canvas/text.canvas.util.js';
6
- export { Root, configure } from './canvas/root.canvas.util.js';
7
- export { GridItem } from './canvas/grid.canvas.util.js';
8
- export { Grid } from './canvas/grid.canvas.util.js';
9
- export { Chart } from './canvas/chart.canvas.util.js';
3
+ export { Box, Column, Row, type BoxNode } from './canvas/layout.canvas.js';
4
+ export { Image } from './canvas/image.canvas.js';
5
+ export { Text } from './canvas/text.canvas.js';
6
+ export { Root, configure, terminate } from './canvas/root.canvas.js';
7
+ export { GridItem } from './canvas/grid.canvas.js';
8
+ export { Grid } from './canvas/grid.canvas.js';
9
+ export { Chart } from './canvas/chart.canvas.js';
10
+ export { clearDiskCache } from './util/disk.cache.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAC/E,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
package/dist/esm/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export { Border, Style } from './constant/common.const.js';
2
- export { Box, Column, Row } from './canvas/layout.canvas.util.js';
3
- export { Image, disposeImageCache } from './canvas/image.canvas.util.js';
4
- export { Text } from './canvas/text.canvas.util.js';
5
- export { Root, configure } from './canvas/root.canvas.util.js';
6
- export { Grid, GridItem } from './canvas/grid.canvas.util.js';
7
- export { Chart } from './canvas/chart.canvas.util.js';
2
+ export { Box, Column, Row } from './canvas/layout.canvas.js';
3
+ export { Image } from './canvas/image.canvas.js';
4
+ export { Text } from './canvas/text.canvas.js';
5
+ export { Root, configure, terminate } from './canvas/root.canvas.js';
6
+ export { Grid, GridItem } from './canvas/grid.canvas.js';
7
+ export { Chart } from './canvas/chart.canvas.js';
8
+ export { clearDiskCache } from './util/disk.cache.js';
8
9
  export * from 'yoga-layout';
@@ -1,4 +1,10 @@
1
1
  export declare function hashBuffer(buf: Buffer): string;
2
2
  export declare function readDiskCache(key: string): Promise<Buffer | null>;
3
3
  export declare function writeDiskCache(key: string, data: Buffer): Promise<void>;
4
+ export declare function deleteDiskCache(key: string): Promise<void>;
5
+ /**
6
+ * Delete the entire disk cache directory.
7
+ * Called on process exit to clean up any orphaned cache files.
8
+ */
9
+ export declare function clearDiskCache(): Promise<void>;
4
10
  //# sourceMappingURL=disk.cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"disk.cache.d.ts","sourceRoot":"","sources":["../../../src/util/disk.cache.ts"],"names":[],"mappings":"AAaA,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOvE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7E"}
1
+ {"version":3,"file":"disk.cache.d.ts","sourceRoot":"","sources":["../../../src/util/disk.cache.ts"],"names":[],"mappings":"AAaA,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOvE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7E;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;AAED;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAOpD"}
@@ -31,5 +31,42 @@ async function writeDiskCache(key, data) {
31
31
  // best-effort — cache write failures are non-fatal
32
32
  }
33
33
  }
34
+ async function deleteDiskCache(key) {
35
+ try {
36
+ await promises.unlink(join(CACHE_DIR, key));
37
+ }
38
+ catch {
39
+ // non-fatal — file may not exist if write failed earlier
40
+ }
41
+ }
42
+ /**
43
+ * Delete the entire disk cache directory.
44
+ * Called on process exit to clean up any orphaned cache files.
45
+ */
46
+ async function clearDiskCache() {
47
+ _dirEnsured = false;
48
+ try {
49
+ await promises.rm(CACHE_DIR, { recursive: true, force: true });
50
+ }
51
+ catch {
52
+ // non-fatal — directory may not exist
53
+ }
54
+ }
55
+ // Clean up disk cache on process exit to handle crashes mid-render.
56
+ // Guard prevents re-entry: clearDiskCache() is async, so without this the
57
+ // beforeExit → schedule I/O → idle → beforeExit cycle loops forever.
58
+ let _exitCleanupStarted = false;
59
+ process.on('beforeExit', () => {
60
+ if (_exitCleanupStarted)
61
+ return;
62
+ _exitCleanupStarted = true;
63
+ void clearDiskCache();
64
+ });
65
+ // Also clean up on SIGINT/SIGTERM for graceful shutdowns
66
+ const cleanupOnExit = () => {
67
+ clearDiskCache().finally(() => process.exit(0));
68
+ };
69
+ process.on('SIGINT', cleanupOnExit);
70
+ process.on('SIGTERM', cleanupOnExit);
34
71
 
35
- export { hashBuffer, readDiskCache, writeDiskCache };
72
+ export { clearDiskCache, deleteDiskCache, hashBuffer, readDiskCache, writeDiskCache };
@@ -1,5 +1,5 @@
1
1
  import { parentPort } from 'worker_threads';
2
- import { RootNode } from '../canvas/root.canvas.util.js';
2
+ import { RootNode } from '../canvas/root.canvas.js';
3
3
 
4
4
  /**
5
5
  * Worker thread entry point for off-main-thread canvas rendering.