@meonode/canvas 1.7.2 → 2.0.0
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/README.md +161 -7
- package/dist/cjs/canvas/canvas.type.d.ts +68 -5
- package/dist/cjs/canvas/canvas.type.d.ts.map +1 -1
- package/dist/cjs/canvas/image.canvas.util.d.ts +9 -37
- package/dist/cjs/canvas/image.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/image.canvas.util.js +37 -137
- package/dist/cjs/canvas/image.canvas.util.js.map +1 -1
- package/dist/cjs/canvas/root.canvas.util.d.ts +27 -7
- package/dist/cjs/canvas/root.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/root.canvas.util.js +58 -38
- package/dist/cjs/canvas/root.canvas.util.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/util/disk.cache.d.ts +6 -0
- package/dist/cjs/util/disk.cache.d.ts.map +1 -1
- package/dist/cjs/util/disk.cache.js +33 -0
- package/dist/cjs/util/disk.cache.js.map +1 -1
- package/dist/esm/canvas/canvas.type.d.ts +68 -5
- package/dist/esm/canvas/canvas.type.d.ts.map +1 -1
- package/dist/esm/canvas/image.canvas.util.d.ts +9 -37
- package/dist/esm/canvas/image.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/image.canvas.util.js +38 -135
- package/dist/esm/canvas/root.canvas.util.d.ts +27 -7
- package/dist/esm/canvas/root.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/root.canvas.util.js +59 -40
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -2
- package/dist/esm/util/disk.cache.d.ts +6 -0
- package/dist/esm/util/disk.cache.d.ts.map +1 -1
- package/dist/esm/util/disk.cache.js +32 -1
- package/package.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FontLibrary, Canvas } from 'skia-canvas';
|
|
2
2
|
import { ColumnNode, RowNode, BoxNode } from './layout.canvas.util.js';
|
|
3
|
-
import { ImageNode
|
|
3
|
+
import { ImageNode } from './image.canvas.util.js';
|
|
4
|
+
import { deleteDiskCache } from '../util/disk.cache.js';
|
|
4
5
|
import { TextNode } from './text.canvas.util.js';
|
|
5
6
|
import { ChartNode } from './chart.canvas.util.js';
|
|
6
7
|
import { GridItemNode, GridNode } from './grid.canvas.util.js';
|
|
@@ -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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
47
|
+
_defaultWorkerMode = options.workerMode;
|
|
28
48
|
if (options.workers !== undefined)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
365
|
+
if (diskCacheKeys?.size) {
|
|
366
|
+
await Promise.allSettled([...diskCacheKeys].map(key => deleteDiskCache(key)));
|
|
367
|
+
}
|
|
340
368
|
}
|
|
341
369
|
}
|
|
342
370
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
380
|
+
const result = await _workerPool.render(props);
|
|
381
|
+
return new WorkerCanvas({ ...result, pool: _workerPool });
|
|
360
382
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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 };
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export * from './constant/common.const.js';
|
|
2
2
|
export * from './canvas/canvas.type.js';
|
|
3
3
|
export { Box, Column, Row, type BoxNode } from './canvas/layout.canvas.util.js';
|
|
4
|
-
export { Image
|
|
4
|
+
export { Image } from './canvas/image.canvas.util.js';
|
|
5
5
|
export { Text } from './canvas/text.canvas.util.js';
|
|
6
|
-
export { Root, configure } from './canvas/root.canvas.util.js';
|
|
6
|
+
export { Root, configure, terminate } from './canvas/root.canvas.util.js';
|
|
7
7
|
export { GridItem } from './canvas/grid.canvas.util.js';
|
|
8
8
|
export { Grid } from './canvas/grid.canvas.util.js';
|
|
9
9
|
export { Chart } from './canvas/chart.canvas.util.js';
|
|
10
|
+
export { clearDiskCache } from './util/disk.cache.js';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,MAAM,+BAA+B,CAAA;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAA;AACrD,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
2
|
export { Box, Column, Row } from './canvas/layout.canvas.util.js';
|
|
3
|
-
export { Image
|
|
3
|
+
export { Image } from './canvas/image.canvas.util.js';
|
|
4
4
|
export { Text } from './canvas/text.canvas.util.js';
|
|
5
|
-
export { Root, configure } from './canvas/root.canvas.util.js';
|
|
5
|
+
export { Root, configure, terminate } from './canvas/root.canvas.util.js';
|
|
6
6
|
export { Grid, GridItem } from './canvas/grid.canvas.util.js';
|
|
7
7
|
export { Chart } from './canvas/chart.canvas.util.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,CAMpD"}
|
|
@@ -31,5 +31,36 @@ 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
|
+
try {
|
|
48
|
+
await promises.rm(CACHE_DIR, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// non-fatal — directory may not exist
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Clean up disk cache on process exit to handle crashes mid-render
|
|
55
|
+
process.on('beforeExit', () => {
|
|
56
|
+
// Fire and forget — best effort cleanup
|
|
57
|
+
clearDiskCache();
|
|
58
|
+
});
|
|
59
|
+
// Also clean up on SIGINT/SIGTERM for graceful shutdowns
|
|
60
|
+
const cleanupOnExit = () => {
|
|
61
|
+
clearDiskCache().finally(() => process.exit(0));
|
|
62
|
+
};
|
|
63
|
+
process.on('SIGINT', cleanupOnExit);
|
|
64
|
+
process.on('SIGTERM', cleanupOnExit);
|
|
34
65
|
|
|
35
|
-
export { hashBuffer, readDiskCache, writeDiskCache };
|
|
66
|
+
export { clearDiskCache, deleteDiskCache, hashBuffer, readDiskCache, writeDiskCache };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meonode/canvas",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A declarative, component-based library for server-side canvas image generation. Write complex visuals with simple functions, similar to the composition style of @meonode/ui.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.mjs --fix",
|
|
43
43
|
"format": "prettier --write .",
|
|
44
44
|
"generate:samples": "npx tsx scripts/generate_sample_charts.ts && npx tsx scripts/generate_sample_grids.ts && npx tsx scripts/generate_sample_nested_grids.ts",
|
|
45
|
+
"check:memory": "npx tsx --expose-gc scripts/check_memory.ts",
|
|
45
46
|
"prepare": "husky"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|