summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources/server-sent-events.js
blob: 894bbc76bc7f8efdec6c662221c09a54da6dda5f (plain)
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
/* This Source Code Form is subject to the terms of the Mozilla Public
      +  * License, v. 2.0. If a copy of the MPL was not distributed with this
      +  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {
  LongStringActor,
} = require("resource://devtools/server/actors/string.js");

const {
  TYPES: { SERVER_SENT_EVENT },
} = require("resource://devtools/server/actors/resources/index.js");

const eventSourceEventService = Cc[
  "@mozilla.org/eventsourceevent/service;1"
].getService(Ci.nsIEventSourceEventService);

class ServerSentEventWatcher {
  constructor() {
    this.windowIds = new Set();
    // Register for backend events.
    this.onWindowReady = this.onWindowReady.bind(this);
    this.onWindowDestroy = this.onWindowDestroy.bind(this);
  }
  /**
   * Start watching for all server sent events related to a given Target Actor.
   *
   * @param TargetActor targetActor
   *        The target actor on which we should observe server sent events.
   * @param Object options
   *        Dictionary object with following attributes:
   *        - onAvailable: mandatory function
   *          This will be called for each resource.
   */
  watch(targetActor, { onAvailable }) {
    this.onAvailable = onAvailable;
    this.targetActor = targetActor;

    for (const window of this.targetActor.windows) {
      const { innerWindowId } = window.windowGlobalChild;
      this.startListening(innerWindowId);
    }

    // Listen for subsequent top-level-document reloads/navigations,
    // new iframe additions or current iframe reloads/navigation.
    this.targetActor.on("window-ready", this.onWindowReady);
    this.targetActor.on("window-destroyed", this.onWindowDestroy);
  }

  static createResource(messageType, eventParams) {
    return {
      resourceType: SERVER_SENT_EVENT,
      messageType,
      ...eventParams,
    };
  }

  static prepareFramePayload(targetActor, frame) {
    const payload = new LongStringActor(targetActor.conn, frame);
    targetActor.manage(payload);
    return payload.form();
  }

  onWindowReady({ window }) {
    const { innerWindowId } = window.windowGlobalChild;
    this.startListening(innerWindowId);
  }

  onWindowDestroy({ id }) {
    this.stopListening(id);
  }

  startListening(innerWindowId) {
    if (!this.windowIds.has(innerWindowId)) {
      this.windowIds.add(innerWindowId);
      eventSourceEventService.addListener(innerWindowId, this);
    }
  }

  stopListening(innerWindowId) {
    if (this.windowIds.has(innerWindowId)) {
      this.windowIds.delete(innerWindowId);
      // The listener might have already been cleaned up on `window-destroy`.
      if (!eventSourceEventService.hasListenerFor(innerWindowId)) {
        console.warn(
          "Already stopped listening to server sent events for this window."
        );
        return;
      }
      eventSourceEventService.removeListener(innerWindowId, this);
    }
  }

  destroy() {
    // cleanup any other listeners not removed on `window-destroy`
    for (const id of this.windowIds) {
      this.stopListening(id);
    }
    this.targetActor.off("window-ready", this.onWindowReady);
    this.targetActor.off("window-destroyed", this.onWindowDestroy);
  }

  // nsIEventSourceEventService specific functions
  eventSourceConnectionOpened() {}

  eventSourceConnectionClosed(httpChannelId) {
    const resource = ServerSentEventWatcher.createResource(
      "eventSourceConnectionClosed",
      { httpChannelId }
    );
    this.onAvailable([resource]);
  }

  eventReceived(httpChannelId, eventName, lastEventId, data, retry, timeStamp) {
    const payload = ServerSentEventWatcher.prepareFramePayload(
      this.targetActor,
      data
    );
    const resource = ServerSentEventWatcher.createResource("eventReceived", {
      httpChannelId,
      data: {
        payload,
        eventName,
        lastEventId,
        retry,
        timeStamp,
      },
    });

    this.onAvailable([resource]);
  }
}

module.exports = ServerSentEventWatcher;