-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmessageHandler.mjs
896 lines (769 loc) · 33.2 KB
/
messageHandler.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
const VERBOSE = false; // this is chatty
/*
* Relays messages between contexts in a consistent way
*
* Contexts
* - content: content script
* - inject: inject script
* - background: background script
* - dash: drop-down dash iFrame
* - worker: worker script // Todo: implement worker contexts - that will be a lot of work
*
* Relays
* - content relays between background, dash, and inject
* - TODO: inject relays to workers
*/
export class MessageHandler {
static instance; // singleton instance
#listeners = [];
/**
* @constructor - follows the singleton pattern
* @param {context} context - the context of the instance use one of [content, inject, background, dash, worker]
* @returns {*} - returns the instance of the MessageHandler
*/
constructor(context) {
// Set up debug logging based on context
const contextSymbols = {
'content': "🕵",
'inject': "💉",
'background': "🫥",
'dash': "📈️",
// 'worker': "👷",
};
const debugSymbol = contextSymbols[context] || "";
if (process.env.NODE_ENV)
this.debug = Function.prototype.bind.call(console.debug, console, `vch ${debugSymbol} messageHandler[${context}] `);
else
this.debug = () => {
};
// Singleton pattern
if (MessageHandler.instance) {
if (VERBOSE) this.debug(`instance already exists for ${context}`);
return MessageHandler.instance;
} else {
MessageHandler.instance = this;
this.debug(`creating new MessageHandler for ${context}`);
}
// Setup listeners
if (context === CONTEXT.CONTENT) {
this.#documentListener(); // from inject
this.#runtimeListener(); // from background
this.#iFrameListener(); // from dash
} else if (context === CONTEXT.INJECT) {
this.#documentListener(); // from content
} else if (context === CONTEXT.DASH) {
this.#runtimeListener(); // from background
} else if (context === CONTEXT.BACKGROUND) {
this.#runtimeListener(); // from content
} else
this.debug(`invalid context for listener ${context}`);
// Handle pings from background
if (context === CONTEXT.CONTENT) {
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message === MESSAGE.PING) {
sendResponse({message: MESSAGE.PONG});
}
});
}
this.context = context;
}
/**
* Sends a message to another extension context
* @param {context} to - the context to send the message to
* @param {string} message - the message to send
* @param {object} data - the data to send with the message
* @param {context} origin - the original context the message is coming from
* @returns {void}
*
* Communication Methods Table:
* from col to row
* | | background | content | inject | dash |
* |------------|-------------------------------|------------------------------|---------------------------|--------------------------------|
* | background | // n/a | chrome.runtime.sendMessage | // relay via content | chrome.runtime.sendMessage |
* | content | chrome.tabs.sendMessage | // n/a | document.dispatchEvent | window.parent.postMessage |
* | inject | // relay via content | document.dispatchEvent | // n/a | // relay via content |
* | dash | chrome.tabs.sendMessage | chrome.runtime.sendMessage | // relay via content | // n/a |
*
*/
sendMessage = (to, message, data = {}, origin = this.context) => {
// ignore messages to self
if (this.context === to || !to || !message)
return;
// Can't send messages to background when disconnected - i.e. "extension context is invalidated"
if (this.disconnected) {
if (VERBOSE) this.debug(`disconnected from background: ignoring message to "${to}" from "${this.context}" with data ${JSON.stringify(data)}`);
return;
}
try {
let messageToSend = {
from: this.context,
origin: origin,
to: to,
message: message,
timestamp: (new Date()).toLocaleString(),
data: data
};
// Logging for debug
if (VERBOSE)
if (this.context !== origin)
this.debug(`sending "${message}" from "${origin}" via "${this.context}" to "${to}" with data ${JSON.stringify(data)}`);
else
this.debug(`sending "${message}" from "${this.context}" to "${to}" with data ${JSON.stringify(data)}`);
switch (this.context) {
case CONTEXT.BACKGROUND:
if (to === CONTEXT.CONTENT || to === CONTEXT.INJECT) {
// background->content requires that you specify the tab. This needs to be specified in the message.
// It would be better if you could specify the tab in the method and then use c.CONTEXT to send to all tabs
// ToDo: handle individual communication to tabs better
this.tabId = data.tabId;
// this.debug(`target tabId: ${this.tabId}`);
try {
chrome.tabs.sendMessage(this.tabId, {...messageToSend});
} catch (err) {
this.debug(`ERROR: failed to send ${message} from ${this.context} to tab ${this.tabId} - tab disconnected: `, err.message)
}
} else if (to === CONTEXT.DASH) {
chrome.runtime.sendMessage({...messageToSend});
}
break;
case CONTEXT.CONTENT:
if (to === CONTEXT.BACKGROUND || to === CONTEXT.DASH) {
try {
chrome.runtime.sendMessage(messageToSend, {}, response => {
if (chrome.runtime.lastError)
this.debug("Disconnected from background script:", chrome.runtime.lastError.message);
});
} catch (err) {
this.debug("Error sending message to background: ", err.message);
if (err.message.match(/context invalidated/i)) {
this.#handleDisconnect();
}
}
} else if (to === CONTEXT.INJECT) {
const toInjectEvent = new CustomEvent('vch', {detail: messageToSend});
document.dispatchEvent(toInjectEvent);
}
break;
case CONTEXT.INJECT:
messageToSend.data = JSON.stringify(messageToSend.data);
const toContentEvent = new CustomEvent('vch', {detail: messageToSend});
document.dispatchEvent(toContentEvent);
break;
case CONTEXT.DASH:
this.debug(`sending "${message}" from "${this.context}" to "${to}" with data ${JSON.stringify(data)}`);
if (to === CONTEXT.BACKGROUND) {
chrome.runtime.sendMessage({...messageToSend});
} else {
window.parent.postMessage({...messageToSend}, "*"); // parent origin is the user page
}
break;
default:
this.debug(`unhandled message from "${this.context}" to "${to}" with data ${JSON.stringify(data)}`);
break;
}
} catch (err) {
this.debug(`ERROR on "${message}"`, err);
}
}
/**
* Relays a message to another extension context
* @param {context} from - the original context the message is coming from
* @param {context} to - the context to send the message to
* @param {string} message - the message to send
* @param {object} data - the data to send with the message
* @returns {void}
*/
#relayHandler(from, to, message, data) {
// Relay scenarios
switch (`${from}→${to}`) {
case 'background→inject':
case 'inject→background':
case 'dash→inject':
if (VERBOSE)
this.debug(`relayHandler for "${from}→${to}" via ${this.context} for ${message} with data ${JSON.stringify(data)}`);
this.sendMessage(to, message, data, from);
break;
default:
return
}
}
/**
* Adds a listener for messages between content and background
*/
#runtimeListener() {
chrome.runtime.onMessage.addListener(
async (request, sender, sendResponse) => {
const {to, from, message} = request;
// Todo: something is sending data as a string: "{}"
let data = typeof request?.data === 'string' ? JSON.parse(request.data) : request?.data || {};
// ignore prerender and cached tabs since we cannot respond to them(?)
if (sender.documentLifecycle === 'prerender' || sender.documentLifecycle === 'cached') {
if (VERBOSE)
this.debug(`ignoring ${sender.documentLifecycle} tab message "${message}" on tab ${sender.tab?.id}. Request: `, request);
return;
}
// background doesn't its own tabId in sender
// We need it in cases when background is responding to a request from content,
// so it is appended as `data.tabId` there
const tabId = sender?.tab?.id || data?.tabId;
if (tabId) {
data.tabId = tabId;
this.tabId = tabId;
}
// skip messages not sent to this listener's context and ignore messages to self
if (from === this.context)
return
if (to !== this.context) {
this.#relayHandler(from, to, message, data);
return;
}
if (VERBOSE)
this.debug(`runtimeListener receiving "${message}" from ${from} ${tabId ? "on tab #" + tabId : ""} to ${to} in context ${this.context}`, request, sender);
this.#listeners.forEach(listener => {
if (message === listener.message) { //&& (from === null || from === listener.from)){
// ToDo: listener.arguments doesn't exist - should I make that?
if (this.context === CONTEXT.BACKGROUND)
listener.callback.call(listener.callback, data, listener.arguments);
else
listener.callback.call(listener.callback, data, listener.arguments);
}
});
if (sendResponse)
sendResponse(true);
})
}
/**
* Adds a listener for messages from inject to content and content to inject
*/
#documentListener() {
document.addEventListener('vch', async e => {
if (!e.detail) {
this.debug('ERROR: no e.detail', e)
return
}
const {to, from, message, data} = e.detail;
// ignore messages to self
if (from === this.context)
return
if (to !== this.context) {
this.#relayHandler(from, to, message, data);
return;
}
if (VERBOSE)
this.debug(`documentListener receiving "${message}" from ${from} to ${to} in context ${this.context}`, e.detail);
this.#listeners.forEach(listener => {
if (message === listener.message) {
let dataObj = typeof data === 'string' ? JSON.parse(data) : data;
// ToDo: listener.arguments doesn't exist - should I make that?
listener.callback.call(listener.callback, dataObj, listener.arguments);
}
});
});
}
/**
* Adds a listener for messages from dash to content
*/
#iFrameListener() {
const extensionOrigin = new URL(chrome.runtime.getURL('/')).origin;
window.addEventListener('message', e => {
const {to, from, message, data} = e.data;
if (e.origin !== extensionOrigin || from !== CONTEXT.DASH) return; // only dash should use this
if (from === this.context)
return
if (to !== this.context) {
this.#relayHandler(from, to, message, data);
return;
}
if (VERBOSE)
this.debug(`content iFrame listener receiving "${message}" from "${from}" to "${to}" in context "${this.context}"`, e.data);
this.#listeners.forEach(listener => {
if (message === listener.message) {
let dataObj = typeof data === 'string' ? JSON.parse(data) : data;
listener.callback.call(listener.callback, dataObj, listener.arguments);
}
});
});
}
/**
* Adds a listener for messages from worker to content
*
* @param {string} message - the message to listen for
* @param {function} callback - the function to call when the message is received
* @param {number} tabId - the tabId to listen for messages from
* @returns {void}
*/
addListener = (message = "", callback = null, tabId) => {
// Check if the listener already exists
const exists = this.#listeners.some(listener =>
listener.message === message && listener.callback === callback && listener.tabId === tabId
);
if (!exists) {
this.#listeners.push({message, callback, tabId});
if (VERBOSE) this.debug(`added listener "${message}" ` + `${tabId ? " for " + tabId : ""}`);
} else {
if (VERBOSE) this.debug(`listener "${message}" already exists`);
}
}
// ToDo: untested - all copilot
/**
* Removes a listener for messages from worker to content
*
* @param {string} message - the message to listen for
* @param {function} callback - the function to call when the message is received
* @param {string} tabId - the tabId to listen for messages from
*/
removeListener = (message = "", callback = null, tabId) => {
this.#listeners = this.#listeners.filter(listener => {
return listener.message !== message || listener.callback !== callback || listener.tabId !== tabId;
});
this.debug(`removed listener "${message}" from "${this.context}"` + `${tabId ? " for " + tabId : ""}`);
}
/**
* Disconnect logic
*/
/**
* Pings content scripts to check if they are loaded
* - designed for use in the background context
* - pong handled for content context in the constructor
* @param {number} tabId - the tabId to ping
* @returns {Promise<boolean>} - returns if the content script is loaded, otherwise rejects
*/
async ping(tabId) {
return new Promise((resolve, reject) => {
if (this.context !== CONTEXT.BACKGROUND) {
reject(new Error("ping only for the background context"));
}
chrome.tabs.sendMessage(tabId, {message: MESSAGE.PING}, response => {
if (chrome.runtime.lastError) {
if (VERBOSE) this.debug(`ping error: `, chrome.runtime.lastError.message);
reject(chrome.runtime.lastError);
}
if (response && response.message === MESSAGE.PONG) {
resolve();
} else {
const error = new Error(`unhandled ping failure on tab ${tabId}`);
if (VERBOSE) this.debug(error.message, response);
reject(error);
}
});
setTimeout(() => {
reject(false);
}, 1000); // 1 second timeout
});
}
// Keep a map of functions to call when disconnected from the background script
disconnectedCallbackMap = new Map();
disconnected = false;
/**
* Runs a callback when the messageHandler detects the background script is disconnected
* @private
* @returns {void}
*/
#handleDisconnect() {
this.disconnected = true;
if (this.disconnectedCallbackMap.size > 0) {
this.debug("running disconnect callbacks: ", this.disconnectedCallbackMap.keys());
this.disconnectedCallbackMap.forEach((cb) => {
cb();
});
}
}
/**
* Adds a callback to run when the messageHandler detects the background script is disconnected
* - multiple callbacks allowed per instance
* - must have a unique name
* @param {string} name - a name used to identify the callback
* @param {function} callback
*/
onDisconnectedHandler(name = 'default', callback) {
this.disconnectedCallbackMap.set(name, callback);
}
/**
* Sets a default callback to run when the messageHandler detects the background script is disconnected
* Only one allowed per instance
* @param callback
*/
set onDisconnected(callback) {
this.onDisconnectedHandler('default', callback);
}
/**
* Remove the disconnect handler
* @param {string} name
*/
removeDisconnectHandler(name = 'default') {
this.disconnectedCallbackMap.delete(name);
}
/**
* Streaming logic
* - only from dash to content for now
*/
/**
* Streams data using chrome.tabs.connect
* Currently only for use in the dash context
* @param {context} to - the context to send the message to. Currently only supports CONTEXT.CONTENT
* @param {ArrayBuffer} buffer - the data to send with the message. Must be an ArrayBuffer
* @param {number} chunkSize - the size of the chunks to send in bytes. Default is 512KB
* @returns {Promise<void>} - returns when the entire buffer has been sent
*/
async dataTransfer(to, buffer, chunkSize = 1024/2 * 1024) {
return new Promise(async (resolve, reject) => {
if (this.context !== CONTEXT.DASH || to !== CONTEXT.CONTENT) {
const reason = `stream not supported from ${this.context} to ${to}`;
this.debug(reason);
reject(new Error(reason));
return;
}
// Get the current tab
async function getActiveTabId() {
let tabs = await chrome.tabs.query({active: true, currentWindow: true});
return tabs[0].id;
}
const activeTabId = await getActiveTabId();
// this.debug(`activeTabId: ${activeTabId}`);
// try {
const port = chrome.tabs.connect(activeTabId, {name: "dash-stream"});
if (!port) {
this.debug(`Error connecting to stream: `, err.message);
reject(err);
return;
}
if (chrome.runtime.lastError) {
this.debug(`Error connecting to stream: `, chrome.runtime.lastError.message);
reject(new Error(chrome.runtime.lastError.message));
return;
}
port.onDisconnect.addListener(() => {
if (chrome.runtime.lastError) {
this.debug(`Port disconnected with error: `, chrome.runtime.lastError.message);
reject(new Error(chrome.runtime.lastError.message));
} else {
this.debug(`Port disconnected`);
resolve();
}
});
port.onMessage.addListener(msg => {
if (msg.type === MESSAGE.STREAM_COMPLETE) {
this.debug(`Stream transfer complete`);
resolve();
}
});
// Send chunks
for (let i = 0; i < buffer.byteLength; i += chunkSize) {
const chunk = new Uint8Array(buffer.slice(i, i + chunkSize));
const chunkArray = Array.from(chunk); // Convert to a normal array
port.postMessage({type: MESSAGE.STREAM_CHUNK, chunk: chunkArray});
if (VERBOSE)
this.debug(`sending data: ${i} / ${buffer.byteLength}`);
}
this.debug(`sending stream complete. ${buffer.byteLength} bytes sent`);
try {
port.postMessage({type: MESSAGE.STREAM_COMPLETE});
} catch (err) {
this.debug(`Error sending stream complete: `, err.message, port);
reject(err);
}
});
}
/**
* Listens for data from dataTransfer
* Only for use on the content context for now
* uses runtime.onConnect
* @param {function(Blob):void} callback - the function to call when all data is received
* @returns {void}
*/
onDataTransfer(callback) {
if (this.context !== CONTEXT.CONTENT) {
this.debug(`stream not supported in ${this.context}`);
return;
}
chrome.runtime.onConnect.addListener(port => {
if (chrome.runtime.lastError) {
this.debug(`Error connecting to stream: `, chrome.runtime.lastError.message);
}
if (port.name === 'dash-stream') {
let receivedParts = [];
let totalLength = 0;
port.onMessage.addListener(msg => {
if (msg.type === MESSAGE.STREAM_CHUNK) {
// msg.chunk comes in as a normal Array of numbers
const chunk = new Uint8Array(msg.chunk);
receivedParts.push(chunk);
totalLength += chunk.length;
} else if (msg.type === MESSAGE.STREAM_COMPLETE) {
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const part of receivedParts) {
combined.set(part, offset);
offset += part.length;
}
// this.debug("combined", combined);
// Convert to blob
const blob = new Blob([combined]);
callback(blob);
}
});
}
});
}
}
// ToDo: update this class
// inject->worker work for all worker instances
// Context class needs to be initialized before there are any workers
// workers should then be registered against it after
// sendMessage needs to go to all the workers
// given the divergence in functionality between workers and other contexts,
// it may be better to have a separate class for workers
// Used inside a Worker to communicate with only its parent
/**
* Used inside a Worker to communicate with its parent
*/
export class WorkerMessageHandler {
static instance; // singleton instance
listeners = [];
/**
* @constructor - wrapper for onmessage and postMessage to facilitate communication between a worker and its host script
* @singleton
*/
constructor() {
if (WorkerMessageHandler.instance)
return WorkerMessageHandler.instance;
else
WorkerMessageHandler.instance = this;
this.debug = Function.prototype.bind.call(console.debug, console, `vch 👷WorkerMessageHandler[${self.name}] `);
this.debug(`created new WorkerMessageHandler`);
onmessage = async (event) => {
const command = event?.data?.command || null;
// ToDo: this is getting messages for other workers
if (!command) {
// this.debug(`Error - Worker onmessage missing command`, event);
return;
}
if (VERBOSE) this.debug(`onmessage command ${command}`, event.data);
this.listeners.forEach(listener => {
if (command === listener.command) {
this.debug(`calling listener for ${command}`);
listener.callback(event.data);
}
})
}
}
/**
* Wrapper for postMessage to look like MessageHandler
* @param {string} command - the message command
* @param {object} data - the data to send with the message
* @param {array} transferable - the transferable objects to send with the message in an array
*/
sendMessage(command, data = {}, transferable = []) {
const message = {
command,
...data,
}
this.debug(`sending message ${message.command}`, message, transferable);
postMessage(message, transferable);
}
/**
* Add a listener for messages from the parent
* @param {string} command - the message command
* @param {function} callback - the function to call when the message is received
*/
addListener(command, callback) {
this.listeners.push({command, callback});
if (VERBOSE) this.debug(`added listener "${command}"`);
}
}
/**
* Used inside the Inject context to communicate with all workers or a specific worker
*/
export class InjectToWorkerMessageHandler { // extends MessageHandler {
static instance; // singleton instance when used in INJECT
static workers = []; // keep track of all the workers
#listeners = [];
/**
* @constructor - follows the singleton pattern
* @singleton
*/
constructor() {
// Singleton pattern
if (InjectToWorkerMessageHandler.instance) {
return InjectToWorkerMessageHandler.instance;
} else {
InjectToWorkerMessageHandler.instance = this;
}
this.context = CONTEXT.INJECT;
this.debug = Function.prototype.bind.call(console.debug, console, `vch 💉WorkerMessageHandler `);
this.debug(`created new WorkerToInjectMessageHandler`);
// No logging for production
if (process.env.NODE_ENV === 'production')
this.debug = () => {
};
// this.debug(`creating new WorkerMessageHandler in context ${this.context}`, this.worker);
/**
* Handles incoming messages from workers
* @param event
* @returns {Promise<void>}
*/
onmessage = async (event) => {
const command = event?.data?.command || null;
// ToDo: this is getting messages for other workers
if (!command) {
// this.debug(`Error - InjectToWorker onmessage missing command`, event);
return;
}
this.debug(`InjectToWorkerMessageHandler onmessage command ${command}`, event.data);
this.#listeners.forEach(listener => {
if (command === listener.command) {
this.debug(`calling listener for ${command}`);
listener.callback(event.data);
}
})
}
}
/**
* Register a worker with the handler so it can send messages to it
* @param worker
*/
registerWorker(worker) {
InjectToWorkerMessageHandler.workers.push(worker);
this.debug(`registered worker ${worker.name}`);
}
/**
* Send a message to a worker or all workers
* @param {string} workerName - use 'all' to send to all workers
* @param {string} command - the message command
* @param {object} data - the data to send with the message
* @param {array} transferable - the transferable objects to send with the message in an array
*/
sendMessage(workerName, command, data = {}, transferable = []) {
const message = {
command,
...data,
}
if (workerName === "all" || !workerName) {
InjectToWorkerMessageHandler.workers.forEach(worker => {
this.debug(`sending message ${message.command} to ${worker.name}`, message, transferable);
worker.postMessage(message, transferable);
});
} else {
const worker = InjectToWorkerMessageHandler.workers.find(worker => worker.name === workerName);
if (worker) {
this.debug(`sending message ${message.command} to ${worker.name}`, message, transferable);
worker.postMessage(message, transferable);
} else {
this.debug(`Worker ${workerName} not found`);
}
}
}
/**
* Add a listener for messages from workers
* @param {string} command - the message command
* @param {function} callback - the function to call when the message is received
*/
addListener(command, callback) {
this.#listeners.push({command, callback});
if (VERBOSE) this.debug(`added listener "${command}"`);
}
}
/**
* Message typing
*/
/**
* @typedef {Object} context
* @property {context} CONTENT
* @property {context} INJECT
* @property {context} BACKGROUND
* @property {context} DASH
* @property {context} WORKER
*/
/**
* Message contexts for communication between extension contexts
* @type {context} */
export const CONTEXT = {
CONTENT: 'content',
INJECT: 'inject',
BACKGROUND: 'background',
DASH: 'dash',
WORKER: 'worker'
}
/**
* Message types for communication between extension contexts
*/
export const MESSAGE = {
PING: 'ping', // background -> content
PONG: 'pong', // content -> background
REQUEST_TAB_ID: 'request_tab_id', // content -> background for getting tab
TAB_ID: 'tab_id', // background -> content for sending tab
STREAM_CHUNK: 'stream_chunk', // background | dash -> content
STREAM_COMPLETE: 'stream_complete', // background | dash -> content
// used in inject.js
GET_ALL_SETTINGS: 'get_all_settings',
INJECT_LOADED: 'inject_loaded',
STREAM_TRANSFER_COMPLETE: 'stream_transfer_complete',
STREAM_TRANSFER_FAILED: 'stream_transfer_failed',
GUM_STREAM_START: 'gum_stream_start',
AUDIO_TRACK_ADDED: 'audio_track_added',
VIDEO_TRACK_ADDED: 'video_track_added',
LOCAL_AUDIO_LEVEL: 'local_audio_level',
REMOTE_AUDIO_LEVEL: 'remote_audio_level',
PEER_CONNECTION_OPEN: 'peer_connection_open',
PEER_CONNECTION_CLOSED: 'peer_connection_closed',
PEER_CONNECTION_LOCAL_ADD_TRACK: 'peer_connection_local_add_track',
PEER_CONNECTION_LOCAL_REPLACE_TRACK: 'peer_connection_local_replace_track',
PEER_CONNECTION_LOCAL_REMOVE_TRACK: 'peer_connection_local_remove_track',
// background.js
DASH_INIT: 'dash_init',
// DASH_OPEN: 'dash_open',
DASH_OPEN_NEXT: 'dash_open_next',
FRAME_CAPTURE: 'frame_cap',
// GUM_STREAM_START: 'gum_stream_start',
GUM_STREAM_STOP: 'gum_stream_stop',
UNLOAD: 'unload',
// NEW_TRACK: 'new_track',
// TRACK_ENDED: 'track_ended',
// TRACK_MUTE: 'track_mute',
// TRACK_UNMUTE: 'track_unmute',
SUSPEND: 'suspend',
// content.js
TOGGLE_DASH: 'toggle_dash',
ALL_SETTINGS: 'settings',
// GUM_STREAM_START: 'gum_stream_start',
// UNLOAD: 'unload',
// TRACK_TRANSFER_COMPLETE: 'track_transfer_complete', // should have been STREAM_TRANSFER_COMPLETE
NEW_TRACK: 'new_track',
TRACK_ENDED: 'track_ended',
TRACK_MUTE: 'track_mute',
TRACK_UNMUTE: 'track_unmute',
CLONE_TRACK: 'clone_track',
// dash.js
DASH_INIT_DATA: 'dash_init_data',
RELOAD: 'reload', // refresh the page
// self-view
// SELF_VIEW: 'self_view',
SELF_VIEW_SWITCH_ELEMENT: 'self_view_switch_element',
REMOTE_TRACK_ADDED: 'remote_track_added',
REMOTE_TRACK_REMOVED: 'remote_track_removed',
// device manager
GET_DEVICE_SETTINGS: 'get_device_settings',
UPDATE_DEVICE_SETTINGS: 'update_device_settings',
DEVICE_CHANGE: 'device_change',
// GET_STANDBY_STREAM: 'get_standby_stream',
// bad connection
GET_BAD_CONNECTION_SETTINGS: 'get_background_connection_settings',
UPDATE_BAD_CONNECTION_SETTINGS: 'update_bad_connection_settings',
IMPAIRMENT_SETUP: 'setup_impairment',
IMPAIRMENT_CHANGE: 'change_impairment', // maps to UPDATE_BAD_CONNECTION_SETTINGS
// player
PLAYER_START: 'player_start',
PLAYER_LOAD: 'player_load',
PLAYER_CANPLAY: 'player_canplay', // from content to dash when player can play
PLAYER_PAUSE: 'player_pause',
PLAYER_RESUME: 'player_resume', // used in the worker to skip reading
PLAYER_END: 'player_end', // used in the worker to end the transform
FRAME_STREAM: 'frame_stream',
// Inject->Worker
WORKER_SETUP: 'setup',
PAUSE: 'pause',
UNPAUSE: 'unpause',
STOP: 'stop',
// worker->inject
WORKER_START: 'worker_start',
}