server-sent-events.js (3872B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 + * License, v. 2.0. If a copy of the MPL was not distributed with this 3 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 LongStringActor, 9 } = require("resource://devtools/server/actors/string.js"); 10 11 const eventSourceEventService = Cc[ 12 "@mozilla.org/eventsourceevent/service;1" 13 ].getService(Ci.nsIEventSourceEventService); 14 15 class ServerSentEventWatcher { 16 constructor() { 17 this.windowIds = new Set(); 18 // Register for backend events. 19 this.onWindowReady = this.onWindowReady.bind(this); 20 this.onWindowDestroy = this.onWindowDestroy.bind(this); 21 } 22 /** 23 * Start watching for all server sent events related to a given Target Actor. 24 * 25 * @param TargetActor targetActor 26 * The target actor on which we should observe server sent events. 27 * @param Object options 28 * Dictionary object with following attributes: 29 * - onAvailable: mandatory function 30 * This will be called for each resource. 31 */ 32 watch(targetActor, { onAvailable }) { 33 this.onAvailable = onAvailable; 34 this.targetActor = targetActor; 35 36 for (const window of this.targetActor.windows) { 37 const { innerWindowId } = window.windowGlobalChild; 38 this.startListening(innerWindowId); 39 } 40 41 // Listen for subsequent top-level-document reloads/navigations, 42 // new iframe additions or current iframe reloads/navigation. 43 this.targetActor.on("window-ready", this.onWindowReady); 44 this.targetActor.on("window-destroyed", this.onWindowDestroy); 45 } 46 47 static createResource(messageType, eventParams) { 48 return { 49 messageType, 50 ...eventParams, 51 }; 52 } 53 54 static prepareFramePayload(targetActor, frame) { 55 const payload = new LongStringActor(targetActor.conn, frame); 56 targetActor.manage(payload); 57 return payload.form(); 58 } 59 60 onWindowReady({ window }) { 61 const { innerWindowId } = window.windowGlobalChild; 62 this.startListening(innerWindowId); 63 } 64 65 onWindowDestroy({ id }) { 66 this.stopListening(id); 67 } 68 69 startListening(innerWindowId) { 70 if (!this.windowIds.has(innerWindowId)) { 71 this.windowIds.add(innerWindowId); 72 eventSourceEventService.addListener(innerWindowId, this); 73 } 74 } 75 76 stopListening(innerWindowId) { 77 if (this.windowIds.has(innerWindowId)) { 78 this.windowIds.delete(innerWindowId); 79 // The listener might have already been cleaned up on `window-destroy`. 80 if (!eventSourceEventService.hasListenerFor(innerWindowId)) { 81 console.warn( 82 "Already stopped listening to server sent events for this window." 83 ); 84 return; 85 } 86 eventSourceEventService.removeListener(innerWindowId, this); 87 } 88 } 89 90 destroy() { 91 // cleanup any other listeners not removed on `window-destroy` 92 for (const id of this.windowIds) { 93 this.stopListening(id); 94 } 95 this.targetActor.off("window-ready", this.onWindowReady); 96 this.targetActor.off("window-destroyed", this.onWindowDestroy); 97 } 98 99 // nsIEventSourceEventService specific functions 100 eventSourceConnectionOpened() {} 101 102 eventSourceConnectionClosed(httpChannelId) { 103 const resource = ServerSentEventWatcher.createResource( 104 "eventSourceConnectionClosed", 105 { httpChannelId } 106 ); 107 this.onAvailable([resource]); 108 } 109 110 eventReceived(httpChannelId, eventName, lastEventId, data, retry, timeStamp) { 111 const payload = ServerSentEventWatcher.prepareFramePayload( 112 this.targetActor, 113 data 114 ); 115 const resource = ServerSentEventWatcher.createResource("eventReceived", { 116 httpChannelId, 117 data: { 118 payload, 119 eventName, 120 lastEventId, 121 retry, 122 timeStamp, 123 }, 124 }); 125 126 this.onAvailable([resource]); 127 } 128 } 129 130 module.exports = ServerSentEventWatcher;