Skip to content

Commit bb64939

Browse files
authored
feat: Implement Triangular Communication for OffscreenCanvas transfers to bypass Firefox Nightly SharedWorker bugs (#9479) (#9480)
1 parent f0eca95 commit bb64939

4 files changed

Lines changed: 122 additions & 61 deletions

File tree

src/component/Canvas.mjs

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -66,44 +66,19 @@ class Canvas extends Component {
6666
}
6767

6868
if (offscreen) {
69-
let data,
70-
delay = 50;
71-
72-
while (me.mounted && !me.offscreenRegistered && !me.isDestroyed) {
73-
data = await Neo.main.DomAccess.getOffscreenCanvas({
74-
nodeId: id,
75-
windowId
76-
});
77-
78-
if (data.offscreen) {
79-
await Neo.worker.Canvas.registerCanvas({
80-
node : data.offscreen,
81-
nodeId: id,
82-
windowId
83-
}, [data.offscreen]);
84-
85-
me.offscreenRegistered = true;
86-
break
87-
} else if (data.transferred) {
88-
if (Neo.config.useSharedWorkers) {
89-
let retrieveData = await Neo.worker.Canvas.retrieveCanvas({
90-
nodeId: id,
91-
windowId
92-
});
93-
94-
if (retrieveData.hasCanvas) {
95-
me.offscreenRegistered = true;
96-
break
97-
}
98-
}
99-
}
100-
101-
await me.timeout(delay);
102-
103-
if (delay < 1000) {
104-
delay *= 2
105-
}
106-
}
69+
me.registerCanvasCallbacks ??= {};
70+
71+
let promise = new Promise(resolve => {
72+
me.registerCanvasCallbacks[id] = resolve;
73+
});
74+
75+
Neo.main.DomAccess.transferCanvasToWorker({
76+
nodeId: id,
77+
windowId
78+
});
79+
80+
await promise;
81+
me.offscreenRegistered = true;
10782
}
10883
} else if (offscreen) {
10984
if (me.offscreenRegistered) {

src/main/DomAccess.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class DomAccess extends Base {
8484
'setStyle',
8585
'startViewTransition',
8686
'syncModalMask',
87+
'transferCanvasToWorker',
8788
'trapFocus',
8889
'windowScrollTo'
8990
]
@@ -1120,6 +1121,36 @@ class DomAccess extends Base {
11201121
}
11211122
}
11221123

1124+
/**
1125+
* @summary Extracts an OffscreenCanvas and transfers it directly to the Canvas Worker.
1126+
*
1127+
* This method implements a "Triangular Communication" pattern required to bypass a core limitation in Firefox Nightly (and potentially other browsers) regarding SharedWorkers.
1128+
* Firefox fails silently when attempting to transfer an `OffscreenCanvas` from the Main Thread to the App Worker (SharedWorker), and then again from the App Worker to the Canvas Worker.
1129+
* By calling this method, the Main Thread extracts the canvas and sends it directly to the Canvas Worker, bypassing the App Worker entirely for the buffer transfer.
1130+
*
1131+
* @param {Object} data
1132+
* @param {String} data.id
1133+
* @param {String} data.nodeId
1134+
*/
1135+
transferCanvasToWorker({nodeId}) {
1136+
let me = this,
1137+
node = me.getElement(nodeId);
1138+
1139+
if (node) {
1140+
try {
1141+
let offscreen = node.transferControlToOffscreen();
1142+
1143+
Neo.worker.Manager.sendMessage('canvas', {
1144+
action: 'registerCanvasDirect',
1145+
node : offscreen,
1146+
nodeId
1147+
}, [offscreen])
1148+
} catch (e) {
1149+
// Ignore, means the canvas was already transferred or we do not support it
1150+
}
1151+
}
1152+
}
1153+
11231154
/**
11241155
* Traps (or stops trapping) focus within a Component
11251156
* @param {Object} data

src/worker/App.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,27 @@ class App extends Base {
539539
}
540540
}
541541

542+
/**
543+
* @summary Receives the ping from the Canvas Worker confirming a direct canvas transfer.
544+
*
545+
* This method resolves the promise created in `Neo.component.Canvas#afterSetMounted`.
546+
* It is the final step in the "Triangular Communication" pattern where the Main Thread sends the
547+
* `OffscreenCanvas` directly to the Canvas Worker, bypassing the App Worker's standard message payload,
548+
* to avoid transfer restrictions in Firefox SharedWorkers.
549+
*
550+
* @param {Object} msg
551+
* @param {String} msg.nodeId
552+
* @protected
553+
*/
554+
onCanvasRegistered({nodeId}) {
555+
let instance = Neo.get(nodeId);
556+
557+
if (instance?.registerCanvasCallbacks?.[nodeId]) {
558+
instance.registerCanvasCallbacks[nodeId]();
559+
delete instance.registerCanvasCallbacks[nodeId]
560+
}
561+
}
562+
542563
/**
543564
* @param {Object} data
544565
*/

src/worker/Canvas.mjs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,43 @@ class Canvas extends Base {
9090
}
9191
}
9292

93+
/**
94+
* Overrides worker/Base to handle specific messages like registerCanvasDirect
95+
* @param {MessageEvent} e
96+
*/
97+
onMessage(e) {
98+
let msg = e.data;
99+
100+
if (msg.action === 'registerCanvasDirect') {
101+
this.registerCanvasDirect(msg)
102+
} else {
103+
super.onMessage(e)
104+
}
105+
}
106+
107+
/**
108+
* @param {Object} msg
109+
*/
110+
onRegisterNeoConfig(msg) {
111+
super.onRegisterNeoConfig(msg);
112+
113+
if (Neo.config.useCanvasWorkerStartingPoint) {
114+
let path = Neo.config.appPath;
115+
116+
if (path.endsWith('.mjs')) {
117+
path = path.slice(0, -8); // removing "/app.mjs"
118+
}
119+
120+
import(
121+
/* webpackExclude: /(?:\/|\\)(buildScripts|dist|node_modules)/ */
122+
/* webpackMode: "lazy" */
123+
`../../${path}/canvas.mjs`
124+
).then(module => {
125+
module.onStart()
126+
})
127+
}
128+
}
129+
93130
/**
94131
* @param {Object} data
95132
*/
@@ -106,6 +143,26 @@ class Canvas extends Base {
106143
return true
107144
}
108145

146+
/**
147+
* @summary Receives an OffscreenCanvas directly from the Main Thread.
148+
*
149+
* This is the receiving end of the "Triangular Communication" pattern initiated by `Neo.main.DomAccess.transferCanvasToWorker`.
150+
* By receiving the canvas directly from Main, we avoid the `OffscreenCanvas` transfer restrictions inherent in Firefox's SharedWorker implementation.
151+
* Once the canvas is registered internally, this method pings the App Worker back over their direct `MessageChannel` to confirm receipt so the App Worker can proceed with rendering instructions.
152+
*
153+
* @param {Object} msg
154+
* @protected
155+
*/
156+
registerCanvasDirect(msg) {
157+
this.registerCanvas(msg);
158+
159+
// Ping App worker that canvas was received from main.
160+
this.sendMessage('app', {
161+
action: 'canvasRegistered',
162+
nodeId: msg.nodeId
163+
})
164+
}
165+
109166
/**
110167
* @param {Object} data
111168
* @param {String} data.nodeId
@@ -138,29 +195,6 @@ class Canvas extends Base {
138195
delete me.canvasWindowMap[data.nodeId]
139196
}
140197
}
141-
142-
/**
143-
* @param {Object} msg
144-
*/
145-
onRegisterNeoConfig(msg) {
146-
super.onRegisterNeoConfig(msg);
147-
148-
if (Neo.config.useCanvasWorkerStartingPoint) {
149-
let path = Neo.config.appPath;
150-
151-
if (path.endsWith('.mjs')) {
152-
path = path.slice(0, -8); // removing "/app.mjs"
153-
}
154-
155-
import(
156-
/* webpackExclude: /(?:\/|\\)(buildScripts|dist|node_modules)/ */
157-
/* webpackMode: "lazy" */
158-
`../../${path}/canvas.mjs`
159-
).then(module => {
160-
module.onStart()
161-
})
162-
}
163-
}
164198
}
165199

166200
export default Neo.setupClass(Canvas);

0 commit comments

Comments
 (0)