Skip to content

Commit 3299e1a

Browse files
committed
feat(worker): enhance cross-platform support for worker lifecycle and messaging
1 parent 9616a60 commit 3299e1a

File tree

3 files changed

+165
-45
lines changed

3 files changed

+165
-45
lines changed

dev/smoke-test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ArtoolkitPlugin } from '../src/plugin.js';
2+
3+
// minimal eventBus stub
4+
const eventBus = {
5+
_h: new Map(),
6+
on(e, h) { if (!this._h.has(e)) this._h.set(e, []); this._h.get(e).push(h); },
7+
off(e, h) { if (!this._h.has(e)) return; const a = this._h.get(e); this._h.set(e, a.filter(x=>x!==h)); },
8+
emit(e, p) { (this._h.get(e) || []).forEach(h => { try { h(p); } catch (err) { console.error(err); } }); }
9+
};
10+
11+
const core = { eventBus };
12+
13+
async function run() {
14+
const plugin = new ArtoolkitPlugin({ worker: true });
15+
await plugin.init(core);
16+
await plugin.enable();
17+
18+
// Listen to marker events:
19+
eventBus.on('ar:markerFound', d => console.log('FOUND', d));
20+
eventBus.on('ar:markerUpdated', d => console.log('UPDATED', d));
21+
eventBus.on('ar:markerLost', d => console.log('LOST', d));
22+
eventBus.on('ar:workerReady', () => console.log('Worker ready'));
23+
24+
// Emit engine:update frames periodically to trigger worker processing
25+
let id = 0;
26+
const iv = setInterval(() => {
27+
eventBus.emit('engine:update', { id: ++id, timestamp: Date.now() });
28+
if (id >= 10) {
29+
clearInterval(iv);
30+
// wait a bit and then stop plugin
31+
setTimeout(async () => {
32+
await plugin.disable();
33+
console.log('plugin disabled');
34+
}, 1000);
35+
}
36+
}, 100);
37+
}
38+
39+
run().catch(console.error);

src/plugin.js

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* - subscribes to engine:update to send frames (by id) to the worker
77
* - emits ar:markerFound / ar:markerUpdated / ar:markerLost on the engine eventBus
88
*
9-
* Note: the worker stub is intentionally simple and returns a fake detection.
9+
* Works both in browsers (global Worker) and in Node (worker_threads.Worker).
1010
*/
1111
export class ArtoolkitPlugin {
1212
constructor(options = {}) {
@@ -45,7 +45,7 @@ export class ArtoolkitPlugin {
4545

4646
// start worker if configured
4747
if (this.workerEnabled) {
48-
this._startWorker();
48+
await this._startWorker();
4949
}
5050

5151
// start a simple interval to sweep lost markers by frame count (optional)
@@ -84,6 +84,7 @@ export class ArtoolkitPlugin {
8484
// Send lightweight message to worker (worker may accept ImageBitmap later)
8585
if (this._worker) {
8686
try {
87+
// In Node worker_threads, worker.postMessage exists too
8788
this._worker.postMessage({ type: 'processFrame', payload: { frameId: frame.id } });
8889
} catch (err) {
8990
// worker may be terminated; ignore
@@ -94,28 +95,73 @@ export class ArtoolkitPlugin {
9495
}
9596
}
9697

97-
// Worker lifecycle
98-
_startWorker() {
98+
// Worker lifecycle (cross-platform)
99+
async _startWorker() {
99100
if (this._worker) return;
100-
// spawn worker relative to this module
101-
this._worker = new Worker(new URL('./worker/worker.js', import.meta.url));
102-
this._worker.addEventListener('message', this._onWorkerMessage);
103-
this._worker.postMessage({ type: 'init' });
101+
102+
// Browser environment: global Worker exists
103+
if (typeof Worker !== 'undefined') {
104+
// Works in browsers and bundlers that support new URL(...) for workers
105+
this._worker = new Worker(new URL('./worker/worker.js', import.meta.url));
106+
} else {
107+
// Node environment: use worker_threads.Worker
108+
// dynamically import to avoid bundling node-only module into browser builds
109+
const { Worker: NodeWorker } = await import('node:worker_threads');
110+
// Convert the worker module URL into a filesystem path suitable for worker_threads
111+
const workerUrl = new URL('./worker/worker.js', import.meta.url);
112+
113+
// Robust conversion to platform path:
114+
// fileURLToPath handles Windows correctly and avoids duplicate drive letters.
115+
const { fileURLToPath } = await import('node:url');
116+
const workerPath = fileURLToPath(workerUrl);
117+
118+
// Create worker as ES module
119+
this._worker = new NodeWorker(workerPath, { type: 'module' });
120+
}
121+
122+
// Attach message handler (same for both environments)
123+
if (this._worker.addEventListener) {
124+
this._worker.addEventListener('message', this._onWorkerMessage);
125+
} else if (this._worker.on) {
126+
this._worker.on('message', this._onWorkerMessage);
127+
}
128+
129+
// If worker supports postMessage init, send init
130+
try {
131+
this._worker.postMessage?.({ type: 'init' });
132+
} catch (e) {
133+
// ignore
134+
}
104135
}
105136

106137
_stopWorker() {
107138
if (!this._worker) return;
108-
this._worker.removeEventListener('message', this._onWorkerMessage);
139+
140+
// Remove handler
141+
if (this._worker.removeEventListener) {
142+
this._worker.removeEventListener('message', this._onWorkerMessage);
143+
} else if (this._worker.off) {
144+
this._worker.off('message', this._onWorkerMessage);
145+
}
146+
109147
try {
110-
this._worker.terminate();
148+
// terminate/close depending on environment
149+
if (typeof Worker !== 'undefined') {
150+
this._worker.terminate();
151+
} else {
152+
// Node worker_threads
153+
this._worker.terminate?.();
154+
}
111155
} catch (e) {
112156
// ignore
113157
}
114158
this._worker = null;
115159
}
116160

117161
_onWorkerMessage(ev) {
118-
const { type, payload } = ev.data || {};
162+
// worker_threads messages arrive as the raw payload; browser workers wrap in event.data
163+
const data = ev && ev.data !== undefined ? ev.data : ev;
164+
const { type, payload } = data || {};
119165
if (type === 'ready') {
120166
// Worker initialized
121167
this.core?.eventBus?.emit('ar:workerReady', {});
@@ -153,10 +199,7 @@ export class ArtoolkitPlugin {
153199
const now = Date.now();
154200
for (const [id, state] of this._markers.entries()) {
155201
const deltaMs = now - (state.lastSeen || 0);
156-
// converted threshold: if not seen within lostThreshold * frameInterval (~100ms here) mark lost
157-
// simple heuristic: if lastSeen is older than lostThreshold * 200ms mark lost
158202
if (deltaMs > (this.lostThreshold * 200)) {
159-
// emit lost
160203
this._markers.delete(id);
161204
this.core.eventBus.emit('ar:markerLost', { id, timestamp: now });
162205
}

src/worker/worker.js

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,73 @@
1-
self.addEventListener('message', async (ev) => {
2-
const { type, payload } = ev.data || {};
3-
try {
4-
if (type === 'init') {
5-
// Worker init hook. Load WASM or other heavy libraries here in future.
6-
// Respond ready to main thread.
7-
self.postMessage({ type: 'ready' });
8-
} else if (type === 'processFrame') {
9-
// payload: { imageBitmapTransferable?, width, height }
10-
// This stub simulates detection latency and returns a fake marker result.
11-
// In real implementation, run artoolkit detection and return detections.
12-
const { frameId } = payload || {};
13-
// Simulate async detection
14-
await new Promise((r) => setTimeout(r, 10));
1+
// Cross-platform worker stub (browser Worker and Node worker_threads)
2+
let isNodeWorker = false;
3+
let parent = null;
154

16-
// Fake detection result: one marker with id 'demo' and identity matrix
17-
const fakeMatrix = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
18-
const result = {
19-
detections: [
20-
{
21-
id: 'demo',
22-
confidence: 0.9,
23-
poseMatrix: Array.from(fakeMatrix), // structured-clonable
24-
corners: [ {x:0,y:0}, {x:1,y:0}, {x:1,y:1}, {x:0,y:1} ],
25-
frameId
26-
}
27-
]
28-
};
5+
try {
6+
// In Node worker_threads, require is available and worker_threads.parentPort exists.
7+
// Use dynamic require to avoid bundling issues in browser builds.
8+
// If this throws, assume browser worker environment.
9+
// eslint-disable-next-line no-global-assign
10+
const wt = await import('node:worker_threads').catch(() => null);
11+
if (wt && wt.parentPort) {
12+
isNodeWorker = true;
13+
parent = wt.parentPort;
14+
}
15+
} catch (e) {
16+
// Not running under Node worker_threads
17+
isNodeWorker = false;
18+
parent = null;
19+
}
20+
21+
// Helper abstractions
22+
function onMessage(fn) {
23+
if (isNodeWorker) {
24+
parent.on('message', (msg) => fn(msg));
25+
} else {
26+
// browser worker global is `self`
27+
self.addEventListener('message', (ev) => fn(ev.data));
28+
}
29+
}
30+
31+
function sendMessage(msg) {
32+
if (isNodeWorker) {
33+
parent.postMessage(msg);
34+
} else {
35+
self.postMessage(msg);
36+
}
37+
}
38+
39+
// Worker implementation (same logic as before)
40+
onMessage(async (ev) => {
41+
const { type, payload } = ev || {};
42+
try {
43+
if (type === 'init') {
44+
// Worker init hook. Load WASM or other heavy libraries here in future.
45+
// Respond ready to main thread.
46+
sendMessage({ type: 'ready' });
47+
} else if (type === 'processFrame') {
48+
// payload: { imageBitmapTransferable?, width, height, frameId }
49+
// This stub simulates detection latency and returns a fake marker result.
50+
const { frameId } = payload || {};
51+
// Simulate async detection
52+
await new Promise((r) => setTimeout(r, 10));
53+
54+
// Fake detection result: one marker "demo" and identity matrix
55+
const fakeMatrix = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]);
56+
const result = {
57+
detections: [
58+
{
59+
id: 'demo',
60+
confidence: 0.9,
61+
poseMatrix: Array.from(fakeMatrix), // structured-clonable for postMessage
62+
corners: [{x:0,y:0},{x:1,y:0},{x:1,y:1},{x:0,y:1}],
63+
frameId
64+
}
65+
]
66+
};
2967

30-
self.postMessage({ type: 'detectionResult', payload: result });
68+
sendMessage({ type: 'detectionResult', payload: result });
69+
}
70+
} catch (err) {
71+
sendMessage({ type: 'error', payload: { message: err?.message || String(err) } });
3172
}
32-
} catch (err) {
33-
self.postMessage({ type: 'error', payload: { message: err.message } });
34-
}
3573
});

0 commit comments

Comments
 (0)