@meonode/canvas 2.0.2 → 2.0.3
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/cjs/canvas/canvas.helper.js +0 -230
- package/dist/cjs/canvas/canvas.helper.js.map +1 -1
- package/dist/cjs/canvas/chart.canvas.js +70 -144
- package/dist/cjs/canvas/chart.canvas.js.map +1 -1
- package/dist/cjs/canvas/image.canvas.js +2 -2
- package/dist/cjs/canvas/image.canvas.js.map +1 -1
- package/dist/cjs/canvas/layout.canvas.js +6 -6
- package/dist/cjs/canvas/layout.canvas.js.map +1 -1
- package/dist/cjs/canvas/root.canvas.js +23 -117
- package/dist/cjs/canvas/root.canvas.js.map +1 -1
- package/dist/cjs/canvas/text.canvas.js +2 -2
- package/dist/cjs/canvas/text.canvas.js.map +1 -1
- package/dist/cjs/src/canvas/canvas.helper.d.ts +1 -20
- package/dist/cjs/src/canvas/canvas.helper.d.ts.map +1 -1
- package/dist/cjs/src/canvas/canvas.type.d.ts +1 -12
- package/dist/cjs/src/canvas/canvas.type.d.ts.map +1 -1
- package/dist/cjs/src/canvas/chart.canvas.d.ts +1 -1
- package/dist/cjs/src/canvas/chart.canvas.d.ts.map +1 -1
- package/dist/cjs/src/canvas/image.canvas.d.ts +1 -1
- package/dist/cjs/src/canvas/image.canvas.d.ts.map +1 -1
- package/dist/cjs/src/canvas/layout.canvas.d.ts +2 -2
- package/dist/cjs/src/canvas/layout.canvas.d.ts.map +1 -1
- package/dist/cjs/src/canvas/root.canvas.d.ts +3 -2
- package/dist/cjs/src/canvas/root.canvas.d.ts.map +1 -1
- package/dist/cjs/src/canvas/text.canvas.d.ts +1 -1
- package/dist/cjs/src/canvas/text.canvas.d.ts.map +1 -1
- package/dist/cjs/src/worker/comlink.pool.d.ts +30 -0
- package/dist/cjs/src/worker/comlink.pool.d.ts.map +1 -0
- package/dist/cjs/src/worker/comlink.setup.d.ts +4 -0
- package/dist/cjs/src/worker/comlink.setup.d.ts.map +1 -0
- package/dist/cjs/src/worker/worker.types.d.ts +5 -68
- package/dist/cjs/src/worker/worker.types.d.ts.map +1 -1
- package/dist/cjs/worker/comlink.pool.js +164 -0
- package/dist/cjs/worker/comlink.pool.js.map +1 -0
- package/dist/cjs/worker/comlink.setup.js +53 -0
- package/dist/cjs/worker/comlink.setup.js.map +1 -0
- package/dist/cjs/worker/render.worker.js +58 -61
- package/dist/cjs/worker/render.worker.js.map +1 -1
- package/dist/esm/canvas/canvas.helper.js +1 -230
- package/dist/esm/canvas/chart.canvas.js +71 -145
- package/dist/esm/canvas/image.canvas.js +2 -2
- package/dist/esm/canvas/layout.canvas.js +6 -6
- package/dist/esm/canvas/root.canvas.js +23 -116
- package/dist/esm/canvas/text.canvas.js +2 -2
- package/dist/esm/src/canvas/canvas.helper.d.ts +1 -20
- package/dist/esm/src/canvas/canvas.helper.d.ts.map +1 -1
- package/dist/esm/src/canvas/canvas.type.d.ts +1 -12
- package/dist/esm/src/canvas/canvas.type.d.ts.map +1 -1
- package/dist/esm/src/canvas/chart.canvas.d.ts +1 -1
- package/dist/esm/src/canvas/chart.canvas.d.ts.map +1 -1
- package/dist/esm/src/canvas/image.canvas.d.ts +1 -1
- package/dist/esm/src/canvas/image.canvas.d.ts.map +1 -1
- package/dist/esm/src/canvas/layout.canvas.d.ts +2 -2
- package/dist/esm/src/canvas/layout.canvas.d.ts.map +1 -1
- package/dist/esm/src/canvas/root.canvas.d.ts +3 -2
- package/dist/esm/src/canvas/root.canvas.d.ts.map +1 -1
- package/dist/esm/src/canvas/text.canvas.d.ts +1 -1
- package/dist/esm/src/canvas/text.canvas.d.ts.map +1 -1
- package/dist/esm/src/worker/comlink.pool.d.ts +30 -0
- package/dist/esm/src/worker/comlink.pool.d.ts.map +1 -0
- package/dist/esm/src/worker/comlink.setup.d.ts +4 -0
- package/dist/esm/src/worker/comlink.setup.d.ts.map +1 -0
- package/dist/esm/src/worker/worker.types.d.ts +5 -68
- package/dist/esm/src/worker/worker.types.d.ts.map +1 -1
- package/dist/esm/worker/comlink.pool.js +139 -0
- package/dist/esm/worker/comlink.setup.js +30 -0
- package/dist/esm/worker/render.worker.js +38 -60
- package/package.json +2 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import './comlink.setup.js';
|
|
5
|
+
import * as Comlink from 'comlink';
|
|
6
|
+
import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deeply walks an object tree, wraps any function values with Comlink.proxy(),
|
|
10
|
+
* and tracks wrapped proxies for deterministic cleanup.
|
|
11
|
+
*/
|
|
12
|
+
function wrapFunctions(obj, proxies) {
|
|
13
|
+
if (obj === null || obj === undefined)
|
|
14
|
+
return obj;
|
|
15
|
+
if (typeof obj === 'function') {
|
|
16
|
+
const wrapped = Comlink.proxy(obj);
|
|
17
|
+
proxies.add(wrapped);
|
|
18
|
+
return wrapped;
|
|
19
|
+
}
|
|
20
|
+
if (typeof obj !== 'object')
|
|
21
|
+
return obj;
|
|
22
|
+
// Preserve binary data types — don't walk into them
|
|
23
|
+
if (Buffer.isBuffer(obj))
|
|
24
|
+
return obj;
|
|
25
|
+
if (obj instanceof ArrayBuffer)
|
|
26
|
+
return obj;
|
|
27
|
+
if (ArrayBuffer.isView(obj))
|
|
28
|
+
return obj;
|
|
29
|
+
if (Array.isArray(obj)) {
|
|
30
|
+
return obj.map(item => wrapFunctions(item, proxies));
|
|
31
|
+
}
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const key of Object.keys(obj)) {
|
|
34
|
+
result[key] = wrapFunctions(obj[key], proxies);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pool of Comlink-wrapped worker threads.
|
|
40
|
+
* Manages idle/queue scheduling and proxy lifecycle.
|
|
41
|
+
*/
|
|
42
|
+
class ComlinkPool {
|
|
43
|
+
workers = [];
|
|
44
|
+
endpoints = [];
|
|
45
|
+
idle = [];
|
|
46
|
+
queue = [];
|
|
47
|
+
constructor(size) {
|
|
48
|
+
const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '../worker/render.worker.js');
|
|
49
|
+
for (let i = 0; i < size; i++) {
|
|
50
|
+
const worker = new Worker(workerFile);
|
|
51
|
+
const endpoint = Comlink.wrap(nodeEndpoint(worker));
|
|
52
|
+
this.workers.push(worker);
|
|
53
|
+
this.endpoints.push(endpoint);
|
|
54
|
+
this.idle.push(i);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
acquire() {
|
|
58
|
+
return this.idle.pop() ?? null;
|
|
59
|
+
}
|
|
60
|
+
release(idx) {
|
|
61
|
+
this.idle.push(idx);
|
|
62
|
+
this.drain();
|
|
63
|
+
}
|
|
64
|
+
drain() {
|
|
65
|
+
while (this.queue.length > 0 && this.idle.length > 0) {
|
|
66
|
+
const task = this.queue.shift();
|
|
67
|
+
const idx = this.idle.pop();
|
|
68
|
+
void this.executeRender(idx, task.props, task.resolve, task.reject);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async executeRender(idx, props, resolve, reject) {
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.endpoints[idx].render(props);
|
|
74
|
+
resolve({ ...result, workerIdx: idx });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.release(idx);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async render(props) {
|
|
84
|
+
const proxies = new Set();
|
|
85
|
+
const wrapped = wrapFunctions(props, proxies);
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
for (const p of proxies) {
|
|
88
|
+
try {
|
|
89
|
+
;
|
|
90
|
+
p[Comlink.releaseProxy]?.();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Proxy may already be released
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Direct path — idle worker available
|
|
98
|
+
const idx = this.acquire();
|
|
99
|
+
if (idx !== null) {
|
|
100
|
+
try {
|
|
101
|
+
const result = await this.endpoints[idx].render(wrapped);
|
|
102
|
+
return { ...result, workerIdx: idx };
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
this.release(idx);
|
|
106
|
+
cleanup();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Queued path — cleanup AFTER the queued task completes, not before
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
this.queue.push({
|
|
112
|
+
props: wrapped,
|
|
113
|
+
resolve: result => {
|
|
114
|
+
cleanup();
|
|
115
|
+
resolve(result);
|
|
116
|
+
},
|
|
117
|
+
reject: err => {
|
|
118
|
+
cleanup();
|
|
119
|
+
reject(err);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
callOnCanvas(workerIdx, canvasId, method, args) {
|
|
125
|
+
return this.endpoints[workerIdx].callOnCanvas(canvasId, method, args);
|
|
126
|
+
}
|
|
127
|
+
releaseCanvas(workerIdx, canvasId) {
|
|
128
|
+
this.endpoints[workerIdx].releaseCanvas(canvasId);
|
|
129
|
+
}
|
|
130
|
+
terminate() {
|
|
131
|
+
this.workers.forEach(w => w.terminate());
|
|
132
|
+
this.workers = [];
|
|
133
|
+
this.endpoints = [];
|
|
134
|
+
this.idle = [];
|
|
135
|
+
this.queue = [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { ComlinkPool, wrapFunctions };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
export { Comlink };
|
|
3
|
+
import { MessageChannel } from 'node:worker_threads';
|
|
4
|
+
import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
|
|
5
|
+
export { default as nodeEndpoint } from 'comlink/dist/esm/node-adapter.mjs';
|
|
6
|
+
|
|
7
|
+
// src/worker/comlink.setup.ts
|
|
8
|
+
/**
|
|
9
|
+
* Fix Comlink.proxy() for Node.js worker_threads (issue #313).
|
|
10
|
+
* The built-in proxy transfer handler uses browser MessageChannel.
|
|
11
|
+
* This override uses Node's MessageChannel instead.
|
|
12
|
+
*
|
|
13
|
+
* Must be called on BOTH main thread and worker before any Comlink usage.
|
|
14
|
+
*/
|
|
15
|
+
function installNodeProxyHandler() {
|
|
16
|
+
Comlink.transferHandlers.set('proxy', {
|
|
17
|
+
canHandle: (obj) => typeof obj === 'object' && obj !== null && Comlink.proxyMarker in obj,
|
|
18
|
+
serialize: (obj) => {
|
|
19
|
+
const { port1, port2 } = new MessageChannel();
|
|
20
|
+
Comlink.expose(obj, nodeEndpoint(port1));
|
|
21
|
+
return [port2, [port2]];
|
|
22
|
+
},
|
|
23
|
+
deserialize: (port) => {
|
|
24
|
+
port.start?.();
|
|
25
|
+
return Comlink.wrap(nodeEndpoint(port));
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Install immediately on import
|
|
30
|
+
installNodeProxyHandler();
|
|
@@ -1,70 +1,48 @@
|
|
|
1
|
-
import { parentPort } from 'worker_threads';
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
import './comlink.setup.js';
|
|
2
3
|
import { RootNode } from '../canvas/root.canvas.js';
|
|
4
|
+
import * as Comlink from 'comlink';
|
|
5
|
+
import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
|
|
3
6
|
|
|
4
|
-
/**
|
|
5
|
-
* Worker thread entry point for off-main-thread canvas rendering.
|
|
6
|
-
*
|
|
7
|
-
* Message protocol (main → worker):
|
|
8
|
-
* { type: 'render', taskId, props } — render and keep Canvas alive
|
|
9
|
-
* { type: 'call', taskId, canvasId, method, args } — call a method on a live Canvas
|
|
10
|
-
* { type: 'release', canvasId } — free Canvas from memory
|
|
11
|
-
*
|
|
12
|
-
* Responses (worker → main):
|
|
13
|
-
* WorkerRenderResponse — render complete (includes pre-encoded PNG buffer)
|
|
14
|
-
* WorkerCallResponse — method call result
|
|
15
|
-
* WorkerErrorResponse — any failure
|
|
16
|
-
*/
|
|
17
7
|
if (!parentPort) {
|
|
18
8
|
throw new Error('[render.worker] Must be run as a worker thread');
|
|
19
9
|
}
|
|
20
10
|
const canvases = new Map();
|
|
21
11
|
let nextCanvasId = 0;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else if (msg.type === 'call') {
|
|
38
|
-
const canvas = canvases.get(msg.canvasId);
|
|
12
|
+
const api = {
|
|
13
|
+
async render(props) {
|
|
14
|
+
const canvas = await new RootNode(props).render();
|
|
15
|
+
const canvasId = nextCanvasId++;
|
|
16
|
+
canvases.set(canvasId, canvas);
|
|
17
|
+
const result = {
|
|
18
|
+
canvasId,
|
|
19
|
+
buffer: canvas.toBufferSync('png'),
|
|
20
|
+
width: canvas.width,
|
|
21
|
+
height: canvas.height,
|
|
22
|
+
};
|
|
23
|
+
return result;
|
|
24
|
+
},
|
|
25
|
+
async callOnCanvas(canvasId, method, args) {
|
|
26
|
+
const canvas = canvases.get(canvasId);
|
|
39
27
|
if (!canvas) {
|
|
40
|
-
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
let result;
|
|
45
|
-
switch (msg.method) {
|
|
46
|
-
case 'toBuffer':
|
|
47
|
-
result = await canvas.toBuffer(...msg.args);
|
|
48
|
-
break;
|
|
49
|
-
case 'toURL':
|
|
50
|
-
result = await canvas.toURL(...msg.args);
|
|
51
|
-
break;
|
|
52
|
-
case 'toFile':
|
|
53
|
-
result = await canvas.toFile(...msg.args);
|
|
54
|
-
break;
|
|
55
|
-
case 'toSharp':
|
|
56
|
-
// Sharp instances can't be transferred across threads — serialize to buffer
|
|
57
|
-
result = await canvas.toSharp(...msg.args).toBuffer();
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
reply({ taskId: msg.taskId, result });
|
|
28
|
+
throw new Error(`[render.worker] Canvas ${canvasId} not found`);
|
|
61
29
|
}
|
|
62
|
-
|
|
63
|
-
|
|
30
|
+
switch (method) {
|
|
31
|
+
case 'toBuffer':
|
|
32
|
+
return canvas.toBuffer(...args);
|
|
33
|
+
case 'toURL':
|
|
34
|
+
return canvas.toURL(...args);
|
|
35
|
+
case 'toFile':
|
|
36
|
+
await canvas.toFile(...args);
|
|
37
|
+
return;
|
|
38
|
+
case 'toSharp':
|
|
39
|
+
return await canvas.toSharp(...args).toBuffer();
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`[render.worker] Unknown method: ${method}`);
|
|
64
42
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
43
|
+
},
|
|
44
|
+
releaseCanvas(canvasId) {
|
|
45
|
+
canvases.delete(canvasId);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
Comlink.expose(api, nodeEndpoint(parentPort));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meonode/canvas",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
},
|
|
82
82
|
"packageManager": "yarn@4.11.0",
|
|
83
83
|
"dependencies": {
|
|
84
|
+
"comlink": "^4.4.2",
|
|
84
85
|
"file-type": "^22.0.0",
|
|
85
86
|
"lodash-es": "^4.17.23",
|
|
86
87
|
"sharp": "^0.34.5",
|