@meonode/canvas 2.0.4 → 2.0.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.
@@ -3,11 +3,22 @@ import type { RootProps } from '../canvas/canvas.type.js';
3
3
  export interface PoolRenderResult extends RenderResult {
4
4
  workerIdx: number;
5
5
  }
6
+ /** Sentinel embedded in serialized props to mark where a function was extracted. */
7
+ export declare const FN_MARKER = "__comlinkFnId";
6
8
  /**
7
- * Deeply walks an object tree, wraps any function values with Comlink.proxy(),
8
- * and tracks wrapped proxies for deterministic cleanup.
9
+ * Deeply walks an object tree, replaces function values with `{ [FN_MARKER]: id }` sentinels,
10
+ * and collects the original functions in a Map keyed by their assigned id.
11
+ * Returns the cleaned (function-free) tree that is safe for structured clone.
9
12
  */
10
- export declare function wrapFunctions<T>(obj: T, proxies: Set<unknown>): T;
13
+ export declare function extractFunctions<T>(obj: T, fnMap: Map<number, (...args: unknown[]) => unknown>, nextId: {
14
+ value: number;
15
+ }): T;
16
+ /**
17
+ * Deeply walks an object tree received on the worker side, replaces
18
+ * `{ [FN_MARKER]: id }` sentinels with async functions that delegate
19
+ * to the main-thread callback proxy.
20
+ */
21
+ export declare function restoreFunctions<T>(obj: T, callFn: (id: number, ...args: unknown[]) => Promise<unknown>): T;
11
22
  /**
12
23
  * Pool of Comlink-wrapped worker threads.
13
24
  * Manages idle/queue scheduling and proxy lifecycle.
@@ -1 +1 @@
1
- {"version":3,"file":"comlink.pool.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,SAAS,EAAE,MAAM,CAAA;CAClB;AAQD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAuBjE;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,KAAK,CAAmB;gBAEpB,IAAI,EAAE,MAAM;IAYxB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;YAQC,aAAa;IAWrB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0CzD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAInH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxD,SAAS;CAOV"}
1
+ {"version":3,"file":"comlink.pool.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAa,YAAY,EAAU,MAAM,0BAA0B,CAAA;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,SAAS,EAAE,MAAM,CAAA;CAClB;AASD,oFAAoF;AACpF,eAAO,MAAM,SAAS,kBAAkB,CAAA;AAExC;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,EAAE,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,CAuB7H;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAuB3G;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,KAAK,CAAmB;gBAEpB,IAAI,EAAE,MAAM;IAYxB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;YAQC,aAAa;IAiBrB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuDzD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAInH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxD,SAAS;CAOV"}
@@ -28,17 +28,20 @@ function _interopNamespaceDefault(e) {
28
28
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
29
29
  var Comlink__namespace = /*#__PURE__*/_interopNamespaceDefault(Comlink);
30
30
 
31
+ /** Sentinel embedded in serialized props to mark where a function was extracted. */
32
+ const FN_MARKER = '__comlinkFnId';
31
33
  /**
32
- * Deeply walks an object tree, wraps any function values with Comlink.proxy(),
33
- * and tracks wrapped proxies for deterministic cleanup.
34
+ * Deeply walks an object tree, replaces function values with `{ [FN_MARKER]: id }` sentinels,
35
+ * and collects the original functions in a Map keyed by their assigned id.
36
+ * Returns the cleaned (function-free) tree that is safe for structured clone.
34
37
  */
35
- function wrapFunctions(obj, proxies) {
38
+ function extractFunctions(obj, fnMap, nextId) {
36
39
  if (obj === null || obj === undefined)
37
40
  return obj;
38
41
  if (typeof obj === 'function') {
39
- const wrapped = Comlink__namespace.proxy(obj);
40
- proxies.add(wrapped);
41
- return wrapped;
42
+ const id = nextId.value++;
43
+ fnMap.set(id, obj);
44
+ return { [FN_MARKER]: id };
42
45
  }
43
46
  if (typeof obj !== 'object')
44
47
  return obj;
@@ -50,11 +53,41 @@ function wrapFunctions(obj, proxies) {
50
53
  if (ArrayBuffer.isView(obj))
51
54
  return obj;
52
55
  if (Array.isArray(obj)) {
53
- return obj.map(item => wrapFunctions(item, proxies));
56
+ return obj.map(item => extractFunctions(item, fnMap, nextId));
54
57
  }
55
58
  const result = {};
56
59
  for (const key of Object.keys(obj)) {
57
- result[key] = wrapFunctions(obj[key], proxies);
60
+ result[key] = extractFunctions(obj[key], fnMap, nextId);
61
+ }
62
+ return result;
63
+ }
64
+ /**
65
+ * Deeply walks an object tree received on the worker side, replaces
66
+ * `{ [FN_MARKER]: id }` sentinels with async functions that delegate
67
+ * to the main-thread callback proxy.
68
+ */
69
+ function restoreFunctions(obj, callFn) {
70
+ if (obj === null || obj === undefined)
71
+ return obj;
72
+ if (typeof obj !== 'object')
73
+ return obj;
74
+ if (Buffer.isBuffer(obj))
75
+ return obj;
76
+ if (obj instanceof ArrayBuffer)
77
+ return obj;
78
+ if (ArrayBuffer.isView(obj))
79
+ return obj;
80
+ // Check for sentinel
81
+ if (FN_MARKER in obj) {
82
+ const id = obj[FN_MARKER];
83
+ return ((...args) => callFn(id, ...args));
84
+ }
85
+ if (Array.isArray(obj)) {
86
+ return obj.map(item => restoreFunctions(item, callFn));
87
+ }
88
+ const result = {};
89
+ for (const key of Object.keys(obj)) {
90
+ result[key] = restoreFunctions(obj[key], callFn);
58
91
  }
59
92
  return result;
60
93
  }
@@ -88,12 +121,12 @@ class ComlinkPool {
88
121
  while (this.queue.length > 0 && this.idle.length > 0) {
89
122
  const task = this.queue.shift();
90
123
  const idx = this.idle.pop();
91
- void this.executeRender(idx, task.props, task.resolve, task.reject);
124
+ void this.executeRender(idx, task.props, task.callFn, task.resolve, task.reject);
92
125
  }
93
126
  }
94
- async executeRender(idx, props, resolve, reject) {
127
+ async executeRender(idx, props, callFn, resolve, reject) {
95
128
  try {
96
- const result = await this.endpoints[idx].render(props);
129
+ const result = await this.endpoints[idx].render(props, callFn);
97
130
  resolve({ ...result, workerIdx: idx });
98
131
  }
99
132
  catch (err) {
@@ -104,13 +137,25 @@ class ComlinkPool {
104
137
  }
105
138
  }
106
139
  async render(props) {
107
- const proxies = new Set();
108
- const wrapped = wrapFunctions(props, proxies);
140
+ // Extract functions from props, replacing them with serializable sentinels.
141
+ // A single Comlink.proxy() callback is created at the top level so Comlink
142
+ // can correctly transfer it via its proxy transfer handler.
143
+ const fnMap = new Map();
144
+ const cleaned = extractFunctions(props, fnMap, { value: 0 });
145
+ let callFnProxy;
146
+ if (fnMap.size > 0) {
147
+ callFnProxy = Comlink__namespace.proxy(async (id, ...args) => {
148
+ const fn = fnMap.get(id);
149
+ if (!fn)
150
+ throw new Error(`[ComlinkPool] Function #${id} not found`);
151
+ return fn(...args);
152
+ });
153
+ }
109
154
  const cleanup = () => {
110
- for (const p of proxies) {
155
+ if (callFnProxy) {
111
156
  try {
112
157
  ;
113
- p[Comlink__namespace.releaseProxy]?.();
158
+ callFnProxy[Comlink__namespace.releaseProxy]?.();
114
159
  }
115
160
  catch {
116
161
  // Proxy may already be released
@@ -121,7 +166,7 @@ class ComlinkPool {
121
166
  const idx = this.acquire();
122
167
  if (idx !== null) {
123
168
  try {
124
- const result = await this.endpoints[idx].render(wrapped);
169
+ const result = await this.endpoints[idx].render(cleaned, callFnProxy);
125
170
  return { ...result, workerIdx: idx };
126
171
  }
127
172
  finally {
@@ -132,7 +177,8 @@ class ComlinkPool {
132
177
  // Queued path — cleanup AFTER the queued task completes, not before
133
178
  return new Promise((resolve, reject) => {
134
179
  this.queue.push({
135
- props: wrapped,
180
+ props: cleaned,
181
+ callFn: callFnProxy,
136
182
  resolve: result => {
137
183
  cleanup();
138
184
  resolve(result);
@@ -160,5 +206,7 @@ class ComlinkPool {
160
206
  }
161
207
 
162
208
  exports.ComlinkPool = ComlinkPool;
163
- exports.wrapFunctions = wrapFunctions;
209
+ exports.FN_MARKER = FN_MARKER;
210
+ exports.extractFunctions = extractFunctions;
211
+ exports.restoreFunctions = restoreFunctions;
164
212
  //# sourceMappingURL=comlink.pool.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"comlink.pool.js","sources":["../../../../src/worker/comlink.pool.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads'\nimport { fileURLToPath } from 'node:url'\nimport * as path from 'node:path'\nimport { Comlink, nodeEndpoint } from '@/worker/comlink.setup.js'\nimport type { Remote } from 'comlink'\nimport type { WorkerAPI, RenderResult } from '@/worker/worker.types.js'\nimport type { RootProps } from '@/canvas/canvas.type.js'\n\nexport interface PoolRenderResult extends RenderResult {\n workerIdx: number\n}\n\ninterface QueuedTask {\n props: RootProps\n resolve: (result: PoolRenderResult) => void\n reject: (err: Error) => void\n}\n\n/**\n * Deeply walks an object tree, wraps any function values with Comlink.proxy(),\n * and tracks wrapped proxies for deterministic cleanup.\n */\nexport function wrapFunctions<T>(obj: T, proxies: Set<unknown>): T {\n if (obj === null || obj === undefined) return obj\n if (typeof obj === 'function') {\n const wrapped = Comlink.proxy(obj)\n proxies.add(wrapped)\n return wrapped as unknown as T\n }\n if (typeof obj !== 'object') return obj\n\n // Preserve binary data types — don't walk into them\n if (Buffer.isBuffer(obj)) return obj\n if (obj instanceof ArrayBuffer) return obj\n if (ArrayBuffer.isView(obj)) return obj\n\n if (Array.isArray(obj)) {\n return obj.map(item => wrapFunctions(item, proxies)) as unknown as T\n }\n\n const result: Record<string, unknown> = {}\n for (const key of Object.keys(obj as Record<string, unknown>)) {\n result[key] = wrapFunctions((obj as Record<string, unknown>)[key], proxies)\n }\n return result as T\n}\n\n/**\n * Pool of Comlink-wrapped worker threads.\n * Manages idle/queue scheduling and proxy lifecycle.\n */\nexport class ComlinkPool {\n private workers: Worker[] = []\n private endpoints: Remote<WorkerAPI>[] = []\n private idle: number[] = []\n private queue: QueuedTask[] = []\n\n constructor(size: number) {\n const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '../worker/render.worker.js')\n\n for (let i = 0; i < size; i++) {\n const worker = new Worker(workerFile)\n const endpoint = Comlink.wrap<WorkerAPI>(nodeEndpoint(worker))\n this.workers.push(worker)\n this.endpoints.push(endpoint)\n this.idle.push(i)\n }\n }\n\n private acquire(): number | null {\n return this.idle.pop() ?? null\n }\n\n private release(idx: number) {\n this.idle.push(idx)\n this.drain()\n }\n\n private drain() {\n while (this.queue.length > 0 && this.idle.length > 0) {\n const task = this.queue.shift()!\n const idx = this.idle.pop()!\n void this.executeRender(idx, task.props, task.resolve, task.reject)\n }\n }\n\n private async executeRender(idx: number, props: RootProps, resolve: (result: PoolRenderResult) => void, reject: (err: Error) => void) {\n try {\n const result = await this.endpoints[idx].render(props)\n resolve({ ...result, workerIdx: idx })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n } finally {\n this.release(idx)\n }\n }\n\n async render(props: RootProps): Promise<PoolRenderResult> {\n const proxies = new Set<unknown>()\n const wrapped = wrapFunctions(props, proxies)\n\n const cleanup = () => {\n for (const p of proxies) {\n try {\n ;(p as any)[Comlink.releaseProxy]?.()\n } catch {\n // Proxy may already be released\n }\n }\n }\n\n // Direct path — idle worker available\n const idx = this.acquire()\n if (idx !== null) {\n try {\n const result = await this.endpoints[idx].render(wrapped)\n return { ...result, workerIdx: idx }\n } finally {\n this.release(idx)\n cleanup()\n }\n }\n\n // Queued path — cleanup AFTER the queued task completes, not before\n return new Promise<PoolRenderResult>((resolve, reject) => {\n this.queue.push({\n props: wrapped,\n resolve: result => {\n cleanup()\n resolve(result)\n },\n reject: err => {\n cleanup()\n reject(err)\n },\n })\n })\n }\n\n callOnCanvas(workerIdx: number, canvasId: number, method: string, args: unknown[]): Promise<Buffer | string | void> {\n return this.endpoints[workerIdx].callOnCanvas(canvasId, method, args) as Promise<Buffer | string | void>\n }\n\n releaseCanvas(workerIdx: number, canvasId: number): void {\n this.endpoints[workerIdx].releaseCanvas(canvasId)\n }\n\n terminate() {\n this.workers.forEach(w => w.terminate())\n this.workers = []\n this.endpoints = []\n this.idle = []\n this.queue = []\n }\n}\n"],"names":["Comlink","path","fileURLToPath","Worker"],"mappings":"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"}
1
+ {"version":3,"file":"comlink.pool.js","sources":["../../../../src/worker/comlink.pool.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads'\nimport { fileURLToPath } from 'node:url'\nimport * as path from 'node:path'\nimport { Comlink, nodeEndpoint } from '@/worker/comlink.setup.js'\nimport type { Remote } from 'comlink'\nimport type { WorkerAPI, RenderResult, CallFn } from '@/worker/worker.types.js'\nimport type { RootProps } from '@/canvas/canvas.type.js'\n\nexport interface PoolRenderResult extends RenderResult {\n workerIdx: number\n}\n\ninterface QueuedTask {\n props: RootProps\n callFn: CallFn | undefined\n resolve: (result: PoolRenderResult) => void\n reject: (err: Error) => void\n}\n\n/** Sentinel embedded in serialized props to mark where a function was extracted. */\nexport const FN_MARKER = '__comlinkFnId'\n\n/**\n * Deeply walks an object tree, replaces function values with `{ [FN_MARKER]: id }` sentinels,\n * and collects the original functions in a Map keyed by their assigned id.\n * Returns the cleaned (function-free) tree that is safe for structured clone.\n */\nexport function extractFunctions<T>(obj: T, fnMap: Map<number, (...args: unknown[]) => unknown>, nextId: { value: number }): T {\n if (obj === null || obj === undefined) return obj\n if (typeof obj === 'function') {\n const id = nextId.value++\n fnMap.set(id, obj as (...args: unknown[]) => unknown)\n return { [FN_MARKER]: id } as unknown as T\n }\n if (typeof obj !== 'object') return obj\n\n // Preserve binary data types — don't walk into them\n if (Buffer.isBuffer(obj)) return obj\n if (obj instanceof ArrayBuffer) return obj\n if (ArrayBuffer.isView(obj)) return obj\n\n if (Array.isArray(obj)) {\n return obj.map(item => extractFunctions(item, fnMap, nextId)) as unknown as T\n }\n\n const result: Record<string, unknown> = {}\n for (const key of Object.keys(obj as Record<string, unknown>)) {\n result[key] = extractFunctions((obj as Record<string, unknown>)[key], fnMap, nextId)\n }\n return result as T\n}\n\n/**\n * Deeply walks an object tree received on the worker side, replaces\n * `{ [FN_MARKER]: id }` sentinels with async functions that delegate\n * to the main-thread callback proxy.\n */\nexport function restoreFunctions<T>(obj: T, callFn: (id: number, ...args: unknown[]) => Promise<unknown>): T {\n if (obj === null || obj === undefined) return obj\n if (typeof obj !== 'object') return obj\n\n if (Buffer.isBuffer(obj)) return obj\n if (obj instanceof ArrayBuffer) return obj\n if (ArrayBuffer.isView(obj)) return obj\n\n // Check for sentinel\n if (FN_MARKER in (obj as Record<string, unknown>)) {\n const id = (obj as Record<string, unknown>)[FN_MARKER] as number\n return ((...args: unknown[]) => callFn(id, ...args)) as unknown as T\n }\n\n if (Array.isArray(obj)) {\n return obj.map(item => restoreFunctions(item, callFn)) as unknown as T\n }\n\n const result: Record<string, unknown> = {}\n for (const key of Object.keys(obj as Record<string, unknown>)) {\n result[key] = restoreFunctions((obj as Record<string, unknown>)[key], callFn)\n }\n return result as T\n}\n\n/**\n * Pool of Comlink-wrapped worker threads.\n * Manages idle/queue scheduling and proxy lifecycle.\n */\nexport class ComlinkPool {\n private workers: Worker[] = []\n private endpoints: Remote<WorkerAPI>[] = []\n private idle: number[] = []\n private queue: QueuedTask[] = []\n\n constructor(size: number) {\n const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '../worker/render.worker.js')\n\n for (let i = 0; i < size; i++) {\n const worker = new Worker(workerFile)\n const endpoint = Comlink.wrap<WorkerAPI>(nodeEndpoint(worker))\n this.workers.push(worker)\n this.endpoints.push(endpoint)\n this.idle.push(i)\n }\n }\n\n private acquire(): number | null {\n return this.idle.pop() ?? null\n }\n\n private release(idx: number) {\n this.idle.push(idx)\n this.drain()\n }\n\n private drain() {\n while (this.queue.length > 0 && this.idle.length > 0) {\n const task = this.queue.shift()!\n const idx = this.idle.pop()!\n void this.executeRender(idx, task.props, task.callFn, task.resolve, task.reject)\n }\n }\n\n private async executeRender(\n idx: number,\n props: RootProps,\n callFn: CallFn | undefined,\n resolve: (result: PoolRenderResult) => void,\n reject: (err: Error) => void,\n ) {\n try {\n const result = await this.endpoints[idx].render(props, callFn)\n resolve({ ...result, workerIdx: idx })\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)))\n } finally {\n this.release(idx)\n }\n }\n\n async render(props: RootProps): Promise<PoolRenderResult> {\n // Extract functions from props, replacing them with serializable sentinels.\n // A single Comlink.proxy() callback is created at the top level so Comlink\n // can correctly transfer it via its proxy transfer handler.\n const fnMap = new Map<number, (...args: unknown[]) => unknown>()\n const cleaned = extractFunctions(props, fnMap, { value: 0 })\n\n let callFnProxy: CallFn | undefined\n if (fnMap.size > 0) {\n callFnProxy = Comlink.proxy(async (id: number, ...args: unknown[]) => {\n const fn = fnMap.get(id)\n if (!fn) throw new Error(`[ComlinkPool] Function #${id} not found`)\n return fn(...args)\n })\n }\n\n const cleanup = () => {\n if (callFnProxy) {\n try {\n ;(callFnProxy as any)[Comlink.releaseProxy]?.()\n } catch {\n // Proxy may already be released\n }\n }\n }\n\n // Direct path — idle worker available\n const idx = this.acquire()\n if (idx !== null) {\n try {\n const result = await this.endpoints[idx].render(cleaned, callFnProxy)\n return { ...result, workerIdx: idx }\n } finally {\n this.release(idx)\n cleanup()\n }\n }\n\n // Queued path — cleanup AFTER the queued task completes, not before\n return new Promise<PoolRenderResult>((resolve, reject) => {\n this.queue.push({\n props: cleaned,\n callFn: callFnProxy,\n resolve: result => {\n cleanup()\n resolve(result)\n },\n reject: err => {\n cleanup()\n reject(err)\n },\n })\n })\n }\n\n callOnCanvas(workerIdx: number, canvasId: number, method: string, args: unknown[]): Promise<Buffer | string | void> {\n return this.endpoints[workerIdx].callOnCanvas(canvasId, method, args) as Promise<Buffer | string | void>\n }\n\n releaseCanvas(workerIdx: number, canvasId: number): void {\n this.endpoints[workerIdx].releaseCanvas(canvasId)\n }\n\n terminate() {\n this.workers.forEach(w => w.terminate())\n this.workers = []\n this.endpoints = []\n this.idle = []\n this.queue = []\n }\n}\n"],"names":["path","fileURLToPath","Worker","Comlink"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"comlink.setup.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAKlC,OAAO,YAAY,MAAM,mCAAmC,CAAA;AA2B5D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"comlink.setup.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAKlC,OAAO,YAAY,MAAM,mCAAmC,CAAA;AA4B5D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -33,7 +33,7 @@ var Comlink__namespace = /*#__PURE__*/_interopNamespaceDefault(Comlink);
33
33
  */
34
34
  function installNodeProxyHandler() {
35
35
  Comlink__namespace.transferHandlers.set('proxy', {
36
- canHandle: (obj) => typeof obj === 'object' && obj !== null && Comlink__namespace.proxyMarker in obj,
36
+ canHandle: (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null && Comlink__namespace.proxyMarker in obj,
37
37
  serialize: (obj) => {
38
38
  const { port1, port2 } = new node_worker_threads.MessageChannel();
39
39
  Comlink__namespace.expose(obj, nodeEndpoint(port1));
@@ -1 +1 @@
1
- {"version":3,"file":"comlink.setup.js","sources":["../../../../src/worker/comlink.setup.ts"],"sourcesContent":["// src/worker/comlink.setup.ts\nimport * as Comlink from 'comlink'\nimport { MessageChannel } from 'node:worker_threads'\n\n// Use deep import path — Comlink has no `exports` field (issue #508)\n// @ts-expect-error — Comlink's node-adapter has no type declarations at this path\nimport nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs'\n\n/**\n * Fix Comlink.proxy() for Node.js worker_threads (issue #313).\n * The built-in proxy transfer handler uses browser MessageChannel.\n * This override uses Node's MessageChannel instead.\n *\n * Must be called on BOTH main thread and worker before any Comlink usage.\n */\nfunction installNodeProxyHandler() {\n Comlink.transferHandlers.set('proxy', {\n canHandle: (obj: unknown): obj is { [Comlink.proxyMarker]: true } => typeof obj === 'object' && obj !== null && Comlink.proxyMarker in obj,\n serialize: (obj: unknown) => {\n const { port1, port2 } = new MessageChannel()\n Comlink.expose(obj, nodeEndpoint(port1))\n return [port2, [port2]]\n },\n deserialize: (port: MessagePort) => {\n ;(port as any).start?.()\n return Comlink.wrap(nodeEndpoint(port))\n },\n })\n}\n\n// Install immediately on import\ninstallNodeProxyHandler()\n\nexport { Comlink, nodeEndpoint }\n"],"names":["Comlink","MessageChannel"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAQA;;;;;;AAMG;AACH,SAAS,uBAAuB,GAAA;AAC9B,IAAAA,kBAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE;AACpC,QAAA,SAAS,EAAE,CAAC,GAAY,KAA6C,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAIA,kBAAO,CAAC,WAAW,IAAI,GAAG;AAC1I,QAAA,SAAS,EAAE,CAAC,GAAY,KAAI;YAC1B,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAIC,kCAAc,EAAE;YAC7CD,kBAAO,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;AACxC,YAAA,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;AACD,QAAA,WAAW,EAAE,CAAC,IAAiB,KAAI;AAC/B,YAAA,IAAY,CAAC,KAAK,IAAI;YACxB,OAAOA,kBAAO,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;AACF,KAAA,CAAC;AACJ;AAEA;AACA,uBAAuB,EAAE;;;;;"}
1
+ {"version":3,"file":"comlink.setup.js","sources":["../../../../src/worker/comlink.setup.ts"],"sourcesContent":["// src/worker/comlink.setup.ts\nimport * as Comlink from 'comlink'\nimport { MessageChannel } from 'node:worker_threads'\n\n// Use deep import path — Comlink has no `exports` field (issue #508)\n// @ts-expect-error — Comlink's node-adapter has no type declarations at this path\nimport nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs'\n\n/**\n * Fix Comlink.proxy() for Node.js worker_threads (issue #313).\n * The built-in proxy transfer handler uses browser MessageChannel.\n * This override uses Node's MessageChannel instead.\n *\n * Must be called on BOTH main thread and worker before any Comlink usage.\n */\nfunction installNodeProxyHandler() {\n Comlink.transferHandlers.set('proxy', {\n canHandle: (obj: unknown): obj is { [Comlink.proxyMarker]: true } =>\n (typeof obj === 'object' || typeof obj === 'function') && obj !== null && Comlink.proxyMarker in obj,\n serialize: (obj: unknown) => {\n const { port1, port2 } = new MessageChannel()\n Comlink.expose(obj, nodeEndpoint(port1))\n return [port2, [port2]]\n },\n deserialize: (port: MessagePort) => {\n ;(port as any).start?.()\n return Comlink.wrap(nodeEndpoint(port))\n },\n })\n}\n\n// Install immediately on import\ninstallNodeProxyHandler()\n\nexport { Comlink, nodeEndpoint }\n"],"names":["Comlink","MessageChannel"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAQA;;;;;;AAMG;AACH,SAAS,uBAAuB,GAAA;AAC9B,IAAAA,kBAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE;QACpC,SAAS,EAAE,CAAC,GAAY,KACtB,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,UAAU,KAAK,GAAG,KAAK,IAAI,IAAIA,kBAAO,CAAC,WAAW,IAAI,GAAG;AACtG,QAAA,SAAS,EAAE,CAAC,GAAY,KAAI;YAC1B,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAIC,kCAAc,EAAE;YAC7CD,kBAAO,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;AACxC,YAAA,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;AACD,QAAA,WAAW,EAAE,CAAC,IAAiB,KAAI;AAC/B,YAAA,IAAY,CAAC,KAAK,IAAI;YACxB,OAAOA,kBAAO,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;AACF,KAAA,CAAC;AACJ;AAEA;AACA,uBAAuB,EAAE;;;;;"}
@@ -2,6 +2,7 @@
2
2
 
3
3
  var node_worker_threads = require('node:worker_threads');
4
4
  require('./comlink.setup.js');
5
+ var comlink_pool = require('./comlink.pool.js');
5
6
  var root_canvas = require('../canvas/root.canvas.js');
6
7
  var Comlink = require('comlink');
7
8
  var nodeEndpoint = require('comlink/dist/esm/node-adapter.mjs');
@@ -31,8 +32,9 @@ if (!node_worker_threads.parentPort) {
31
32
  const canvases = new Map();
32
33
  let nextCanvasId = 0;
33
34
  const api = {
34
- async render(props) {
35
- const canvas = await new root_canvas.RootNode(props).render();
35
+ async render(props, callFn) {
36
+ const resolved = callFn ? comlink_pool.restoreFunctions(props, callFn) : props;
37
+ const canvas = await new root_canvas.RootNode(resolved).render();
36
38
  const canvasId = nextCanvasId++;
37
39
  canvases.set(canvasId, canvas);
38
40
  const result = {
@@ -1 +1 @@
1
- {"version":3,"file":"render.worker.js","sources":["../../../../src/worker/render.worker.ts"],"sourcesContent":["import { parentPort } from 'node:worker_threads'\nimport { Comlink, nodeEndpoint } from '@/worker/comlink.setup.js'\nimport { RootNode } from '@/canvas/root.canvas.js'\nimport type { Canvas } from 'skia-canvas'\nimport type { WorkerAPI, RenderResult } from '@/worker/worker.types.js'\n\nif (!parentPort) {\n throw new Error('[render.worker] Must be run as a worker thread')\n}\n\nconst canvases = new Map<number, Canvas>()\nlet nextCanvasId = 0\n\nconst api: WorkerAPI = {\n async render(props) {\n const canvas = await new RootNode(props).render()\n const canvasId = nextCanvasId++\n canvases.set(canvasId, canvas)\n const result: RenderResult = {\n canvasId,\n buffer: canvas.toBufferSync('png'),\n width: canvas.width,\n height: canvas.height,\n }\n return result\n },\n\n async callOnCanvas(canvasId, method, args) {\n const canvas = canvases.get(canvasId)\n if (!canvas) {\n throw new Error(`[render.worker] Canvas ${canvasId} not found`)\n }\n switch (method) {\n case 'toBuffer':\n return canvas.toBuffer(...(args as [any, any?]))\n case 'toURL':\n return canvas.toURL(...(args as [any, any?]))\n case 'toFile':\n await canvas.toFile(...(args as [string, any?]))\n return\n case 'toSharp':\n return await canvas.toSharp(...(args as [any?])).toBuffer()\n default:\n throw new Error(`[render.worker] Unknown method: ${method}`)\n }\n },\n\n releaseCanvas(canvasId) {\n canvases.delete(canvasId)\n },\n}\n\nComlink.expose(api, nodeEndpoint(parentPort))\n"],"names":["parentPort","RootNode","Comlink"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,IAAI,CAACA,8BAAU,EAAE;AACf,IAAA,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC;AACnE;AAEA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB;AAC1C,IAAI,YAAY,GAAG,CAAC;AAEpB,MAAM,GAAG,GAAc;IACrB,MAAM,MAAM,CAAC,KAAK,EAAA;QAChB,MAAM,MAAM,GAAG,MAAM,IAAIC,oBAAQ,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE;AACjD,QAAA,MAAM,QAAQ,GAAG,YAAY,EAAE;AAC/B,QAAA,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC;AAC9B,QAAA,MAAM,MAAM,GAAiB;YAC3B,QAAQ;AACR,YAAA,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB;AACD,QAAA,OAAO,MAAM;IACf,CAAC;AAED,IAAA,MAAM,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAA;QACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;QACrC,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAA,UAAA,CAAY,CAAC;QACjE;QACA,QAAQ,MAAM;AACZ,YAAA,KAAK,UAAU;AACb,gBAAA,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAI,IAAoB,CAAC;AAClD,YAAA,KAAK,OAAO;AACV,gBAAA,OAAO,MAAM,CAAC,KAAK,CAAC,GAAI,IAAoB,CAAC;AAC/C,YAAA,KAAK,QAAQ;AACX,gBAAA,MAAM,MAAM,CAAC,MAAM,CAAC,GAAI,IAAuB,CAAC;gBAChD;AACF,YAAA,KAAK,SAAS;gBACZ,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,GAAI,IAAe,CAAC,CAAC,QAAQ,EAAE;AAC7D,YAAA;AACE,gBAAA,MAAM,IAAI,KAAK,CAAC,mCAAmC,MAAM,CAAA,CAAE,CAAC;;IAElE,CAAC;AAED,IAAA,aAAa,CAAC,QAAQ,EAAA;AACpB,QAAA,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC3B,CAAC;CACF;AAEDC,kBAAO,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAACF,8BAAU,CAAC,CAAC;;"}
1
+ {"version":3,"file":"render.worker.js","sources":["../../../../src/worker/render.worker.ts"],"sourcesContent":["import { parentPort } from 'node:worker_threads'\nimport { Comlink, nodeEndpoint } from '@/worker/comlink.setup.js'\nimport { restoreFunctions } from '@/worker/comlink.pool.js'\nimport { RootNode } from '@/canvas/root.canvas.js'\nimport type { Canvas } from 'skia-canvas'\nimport type { WorkerAPI, RenderResult, CallFn } from '@/worker/worker.types.js'\n\nif (!parentPort) {\n throw new Error('[render.worker] Must be run as a worker thread')\n}\n\nconst canvases = new Map<number, Canvas>()\nlet nextCanvasId = 0\n\nconst api: WorkerAPI = {\n async render(props, callFn?: CallFn) {\n const resolved = callFn ? restoreFunctions(props, callFn) : props\n const canvas = await new RootNode(resolved).render()\n const canvasId = nextCanvasId++\n canvases.set(canvasId, canvas)\n const result: RenderResult = {\n canvasId,\n buffer: canvas.toBufferSync('png'),\n width: canvas.width,\n height: canvas.height,\n }\n return result\n },\n\n async callOnCanvas(canvasId, method, args) {\n const canvas = canvases.get(canvasId)\n if (!canvas) {\n throw new Error(`[render.worker] Canvas ${canvasId} not found`)\n }\n switch (method) {\n case 'toBuffer':\n return canvas.toBuffer(...(args as [any, any?]))\n case 'toURL':\n return canvas.toURL(...(args as [any, any?]))\n case 'toFile':\n await canvas.toFile(...(args as [string, any?]))\n return\n case 'toSharp':\n return await canvas.toSharp(...(args as [any?])).toBuffer()\n default:\n throw new Error(`[render.worker] Unknown method: ${method}`)\n }\n },\n\n releaseCanvas(canvasId) {\n canvases.delete(canvasId)\n },\n}\n\nComlink.expose(api, nodeEndpoint(parentPort))\n"],"names":["parentPort","restoreFunctions","RootNode","Comlink"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOA,IAAI,CAACA,8BAAU,EAAE;AACf,IAAA,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC;AACnE;AAEA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB;AAC1C,IAAI,YAAY,GAAG,CAAC;AAEpB,MAAM,GAAG,GAAc;AACrB,IAAA,MAAM,MAAM,CAAC,KAAK,EAAE,MAAe,EAAA;AACjC,QAAA,MAAM,QAAQ,GAAG,MAAM,GAAGC,6BAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,KAAK;QACjE,MAAM,MAAM,GAAG,MAAM,IAAIC,oBAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE;AACpD,QAAA,MAAM,QAAQ,GAAG,YAAY,EAAE;AAC/B,QAAA,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC;AAC9B,QAAA,MAAM,MAAM,GAAiB;YAC3B,QAAQ;AACR,YAAA,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB;AACD,QAAA,OAAO,MAAM;IACf,CAAC;AAED,IAAA,MAAM,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAA;QACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;QACrC,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAA,UAAA,CAAY,CAAC;QACjE;QACA,QAAQ,MAAM;AACZ,YAAA,KAAK,UAAU;AACb,gBAAA,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAI,IAAoB,CAAC;AAClD,YAAA,KAAK,OAAO;AACV,gBAAA,OAAO,MAAM,CAAC,KAAK,CAAC,GAAI,IAAoB,CAAC;AAC/C,YAAA,KAAK,QAAQ;AACX,gBAAA,MAAM,MAAM,CAAC,MAAM,CAAC,GAAI,IAAuB,CAAC;gBAChD;AACF,YAAA,KAAK,SAAS;gBACZ,OAAO,MAAM,MAAM,CAAC,OAAO,CAAC,GAAI,IAAe,CAAC,CAAC,QAAQ,EAAE;AAC7D,YAAA;AACE,gBAAA,MAAM,IAAI,KAAK,CAAC,mCAAmC,MAAM,CAAA,CAAE,CAAC;;IAElE,CAAC;AAED,IAAA,aAAa,CAAC,QAAQ,EAAA;AACpB,QAAA,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC3B,CAAC;CACF;AAEDC,kBAAO,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CAACH,8BAAU,CAAC,CAAC;;"}
@@ -1,4 +1,5 @@
1
1
  import type { RootProps } from '../canvas/canvas.type.js';
2
+ export type CallFn = (id: number, ...args: unknown[]) => Promise<unknown>;
2
3
  export interface RenderResult {
3
4
  canvasId: number;
4
5
  buffer: Buffer;
@@ -6,7 +7,7 @@ export interface RenderResult {
6
7
  height: number;
7
8
  }
8
9
  export interface WorkerAPI {
9
- render(props: RootProps): Promise<RenderResult>;
10
+ render(props: RootProps, callFn?: CallFn): Promise<RenderResult>;
10
11
  callOnCanvas(canvasId: number, method: string, args: unknown[]): Promise<Buffer | string | void>;
11
12
  releaseCanvas(canvasId: number): void;
12
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"worker.types.d.ts","sourceRoot":"","sources":["../../../src/worker/worker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,CAAA;IAChG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC"}
1
+ {"version":3,"file":"worker.types.d.ts","sourceRoot":"","sources":["../../../src/worker/worker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,MAAM,MAAM,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEzE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAChE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,CAAA;IAChG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC"}
@@ -3,11 +3,22 @@ import type { RootProps } from '../canvas/canvas.type.js';
3
3
  export interface PoolRenderResult extends RenderResult {
4
4
  workerIdx: number;
5
5
  }
6
+ /** Sentinel embedded in serialized props to mark where a function was extracted. */
7
+ export declare const FN_MARKER = "__comlinkFnId";
6
8
  /**
7
- * Deeply walks an object tree, wraps any function values with Comlink.proxy(),
8
- * and tracks wrapped proxies for deterministic cleanup.
9
+ * Deeply walks an object tree, replaces function values with `{ [FN_MARKER]: id }` sentinels,
10
+ * and collects the original functions in a Map keyed by their assigned id.
11
+ * Returns the cleaned (function-free) tree that is safe for structured clone.
9
12
  */
10
- export declare function wrapFunctions<T>(obj: T, proxies: Set<unknown>): T;
13
+ export declare function extractFunctions<T>(obj: T, fnMap: Map<number, (...args: unknown[]) => unknown>, nextId: {
14
+ value: number;
15
+ }): T;
16
+ /**
17
+ * Deeply walks an object tree received on the worker side, replaces
18
+ * `{ [FN_MARKER]: id }` sentinels with async functions that delegate
19
+ * to the main-thread callback proxy.
20
+ */
21
+ export declare function restoreFunctions<T>(obj: T, callFn: (id: number, ...args: unknown[]) => Promise<unknown>): T;
11
22
  /**
12
23
  * Pool of Comlink-wrapped worker threads.
13
24
  * Manages idle/queue scheduling and proxy lifecycle.
@@ -1 +1 @@
1
- {"version":3,"file":"comlink.pool.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,SAAS,EAAE,MAAM,CAAA;CAClB;AAQD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAuBjE;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,KAAK,CAAmB;gBAEpB,IAAI,EAAE,MAAM;IAYxB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;YAQC,aAAa;IAWrB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0CzD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAInH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxD,SAAS;CAOV"}
1
+ {"version":3,"file":"comlink.pool.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAa,YAAY,EAAU,MAAM,0BAA0B,CAAA;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,SAAS,EAAE,MAAM,CAAA;CAClB;AASD,oFAAoF;AACpF,eAAO,MAAM,SAAS,kBAAkB,CAAA;AAExC;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,EAAE,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,CAuB7H;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAuB3G;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,KAAK,CAAmB;gBAEpB,IAAI,EAAE,MAAM;IAYxB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;YAQC,aAAa;IAiBrB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuDzD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAInH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIxD,SAAS;CAOV"}
@@ -5,17 +5,20 @@ import './comlink.setup.js';
5
5
  import * as Comlink from 'comlink';
6
6
  import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
7
7
 
8
+ /** Sentinel embedded in serialized props to mark where a function was extracted. */
9
+ const FN_MARKER = '__comlinkFnId';
8
10
  /**
9
- * Deeply walks an object tree, wraps any function values with Comlink.proxy(),
10
- * and tracks wrapped proxies for deterministic cleanup.
11
+ * Deeply walks an object tree, replaces function values with `{ [FN_MARKER]: id }` sentinels,
12
+ * and collects the original functions in a Map keyed by their assigned id.
13
+ * Returns the cleaned (function-free) tree that is safe for structured clone.
11
14
  */
12
- function wrapFunctions(obj, proxies) {
15
+ function extractFunctions(obj, fnMap, nextId) {
13
16
  if (obj === null || obj === undefined)
14
17
  return obj;
15
18
  if (typeof obj === 'function') {
16
- const wrapped = Comlink.proxy(obj);
17
- proxies.add(wrapped);
18
- return wrapped;
19
+ const id = nextId.value++;
20
+ fnMap.set(id, obj);
21
+ return { [FN_MARKER]: id };
19
22
  }
20
23
  if (typeof obj !== 'object')
21
24
  return obj;
@@ -27,11 +30,41 @@ function wrapFunctions(obj, proxies) {
27
30
  if (ArrayBuffer.isView(obj))
28
31
  return obj;
29
32
  if (Array.isArray(obj)) {
30
- return obj.map(item => wrapFunctions(item, proxies));
33
+ return obj.map(item => extractFunctions(item, fnMap, nextId));
31
34
  }
32
35
  const result = {};
33
36
  for (const key of Object.keys(obj)) {
34
- result[key] = wrapFunctions(obj[key], proxies);
37
+ result[key] = extractFunctions(obj[key], fnMap, nextId);
38
+ }
39
+ return result;
40
+ }
41
+ /**
42
+ * Deeply walks an object tree received on the worker side, replaces
43
+ * `{ [FN_MARKER]: id }` sentinels with async functions that delegate
44
+ * to the main-thread callback proxy.
45
+ */
46
+ function restoreFunctions(obj, callFn) {
47
+ if (obj === null || obj === undefined)
48
+ return obj;
49
+ if (typeof obj !== 'object')
50
+ return obj;
51
+ if (Buffer.isBuffer(obj))
52
+ return obj;
53
+ if (obj instanceof ArrayBuffer)
54
+ return obj;
55
+ if (ArrayBuffer.isView(obj))
56
+ return obj;
57
+ // Check for sentinel
58
+ if (FN_MARKER in obj) {
59
+ const id = obj[FN_MARKER];
60
+ return ((...args) => callFn(id, ...args));
61
+ }
62
+ if (Array.isArray(obj)) {
63
+ return obj.map(item => restoreFunctions(item, callFn));
64
+ }
65
+ const result = {};
66
+ for (const key of Object.keys(obj)) {
67
+ result[key] = restoreFunctions(obj[key], callFn);
35
68
  }
36
69
  return result;
37
70
  }
@@ -65,12 +98,12 @@ class ComlinkPool {
65
98
  while (this.queue.length > 0 && this.idle.length > 0) {
66
99
  const task = this.queue.shift();
67
100
  const idx = this.idle.pop();
68
- void this.executeRender(idx, task.props, task.resolve, task.reject);
101
+ void this.executeRender(idx, task.props, task.callFn, task.resolve, task.reject);
69
102
  }
70
103
  }
71
- async executeRender(idx, props, resolve, reject) {
104
+ async executeRender(idx, props, callFn, resolve, reject) {
72
105
  try {
73
- const result = await this.endpoints[idx].render(props);
106
+ const result = await this.endpoints[idx].render(props, callFn);
74
107
  resolve({ ...result, workerIdx: idx });
75
108
  }
76
109
  catch (err) {
@@ -81,13 +114,25 @@ class ComlinkPool {
81
114
  }
82
115
  }
83
116
  async render(props) {
84
- const proxies = new Set();
85
- const wrapped = wrapFunctions(props, proxies);
117
+ // Extract functions from props, replacing them with serializable sentinels.
118
+ // A single Comlink.proxy() callback is created at the top level so Comlink
119
+ // can correctly transfer it via its proxy transfer handler.
120
+ const fnMap = new Map();
121
+ const cleaned = extractFunctions(props, fnMap, { value: 0 });
122
+ let callFnProxy;
123
+ if (fnMap.size > 0) {
124
+ callFnProxy = Comlink.proxy(async (id, ...args) => {
125
+ const fn = fnMap.get(id);
126
+ if (!fn)
127
+ throw new Error(`[ComlinkPool] Function #${id} not found`);
128
+ return fn(...args);
129
+ });
130
+ }
86
131
  const cleanup = () => {
87
- for (const p of proxies) {
132
+ if (callFnProxy) {
88
133
  try {
89
134
  ;
90
- p[Comlink.releaseProxy]?.();
135
+ callFnProxy[Comlink.releaseProxy]?.();
91
136
  }
92
137
  catch {
93
138
  // Proxy may already be released
@@ -98,7 +143,7 @@ class ComlinkPool {
98
143
  const idx = this.acquire();
99
144
  if (idx !== null) {
100
145
  try {
101
- const result = await this.endpoints[idx].render(wrapped);
146
+ const result = await this.endpoints[idx].render(cleaned, callFnProxy);
102
147
  return { ...result, workerIdx: idx };
103
148
  }
104
149
  finally {
@@ -109,7 +154,8 @@ class ComlinkPool {
109
154
  // Queued path — cleanup AFTER the queued task completes, not before
110
155
  return new Promise((resolve, reject) => {
111
156
  this.queue.push({
112
- props: wrapped,
157
+ props: cleaned,
158
+ callFn: callFnProxy,
113
159
  resolve: result => {
114
160
  cleanup();
115
161
  resolve(result);
@@ -136,4 +182,4 @@ class ComlinkPool {
136
182
  }
137
183
  }
138
184
 
139
- export { ComlinkPool, wrapFunctions };
185
+ export { ComlinkPool, FN_MARKER, extractFunctions, restoreFunctions };
@@ -1 +1 @@
1
- {"version":3,"file":"comlink.setup.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAKlC,OAAO,YAAY,MAAM,mCAAmC,CAAA;AA2B5D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"comlink.setup.d.ts","sourceRoot":"","sources":["../../../src/worker/comlink.setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAA;AAKlC,OAAO,YAAY,MAAM,mCAAmC,CAAA;AA4B5D,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -14,7 +14,7 @@ export { default as nodeEndpoint } from 'comlink/dist/esm/node-adapter.mjs';
14
14
  */
15
15
  function installNodeProxyHandler() {
16
16
  Comlink.transferHandlers.set('proxy', {
17
- canHandle: (obj) => typeof obj === 'object' && obj !== null && Comlink.proxyMarker in obj,
17
+ canHandle: (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null && Comlink.proxyMarker in obj,
18
18
  serialize: (obj) => {
19
19
  const { port1, port2 } = new MessageChannel();
20
20
  Comlink.expose(obj, nodeEndpoint(port1));
@@ -1,5 +1,6 @@
1
1
  import { parentPort } from 'node:worker_threads';
2
2
  import './comlink.setup.js';
3
+ import { restoreFunctions } from './comlink.pool.js';
3
4
  import { RootNode } from '../canvas/root.canvas.js';
4
5
  import * as Comlink from 'comlink';
5
6
  import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
@@ -10,8 +11,9 @@ if (!parentPort) {
10
11
  const canvases = new Map();
11
12
  let nextCanvasId = 0;
12
13
  const api = {
13
- async render(props) {
14
- const canvas = await new RootNode(props).render();
14
+ async render(props, callFn) {
15
+ const resolved = callFn ? restoreFunctions(props, callFn) : props;
16
+ const canvas = await new RootNode(resolved).render();
15
17
  const canvasId = nextCanvasId++;
16
18
  canvases.set(canvasId, canvas);
17
19
  const result = {
@@ -1,4 +1,5 @@
1
1
  import type { RootProps } from '../canvas/canvas.type.js';
2
+ export type CallFn = (id: number, ...args: unknown[]) => Promise<unknown>;
2
3
  export interface RenderResult {
3
4
  canvasId: number;
4
5
  buffer: Buffer;
@@ -6,7 +7,7 @@ export interface RenderResult {
6
7
  height: number;
7
8
  }
8
9
  export interface WorkerAPI {
9
- render(props: RootProps): Promise<RenderResult>;
10
+ render(props: RootProps, callFn?: CallFn): Promise<RenderResult>;
10
11
  callOnCanvas(canvasId: number, method: string, args: unknown[]): Promise<Buffer | string | void>;
11
12
  releaseCanvas(canvasId: number): void;
12
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"worker.types.d.ts","sourceRoot":"","sources":["../../../src/worker/worker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,CAAA;IAChG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC"}
1
+ {"version":3,"file":"worker.types.d.ts","sourceRoot":"","sources":["../../../src/worker/worker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD,MAAM,MAAM,MAAM,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEzE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAChE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,CAAA;IAChG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meonode/canvas",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
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",