aboutsummaryrefslogtreecommitdiff
path: root/extension/src/webext-bridge/internal/persistent-port.ts
blob: 2281c68d9cc84a1b37a926ffd7dee1dd7ec364c2 (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
import browser from 'webextension-polyfill'
import type { Runtime } from 'webextension-polyfill'
import type { InternalMessage } from '../types'
import { createFingerprint } from './endpoint-fingerprint'
import type { QueuedMessage } from './types'
import { encodeConnectionArgs } from './connection-args'
import { createDeliveryLogger } from './delivery-logger'
import type { StatusMessage } from './port-message'
import { PortMessage } from './port-message'

/**
 * Manfiest V3 extensions can have their service worker terminated at any point
 * by the browser. That termination of service worker also terminates any messaging
 * porta created by other parts of the extension. This class is a wrapper around the
 * built-in Port object that re-instantiates the port connection everytime it gets
 * suspended
 */
export const createPersistentPort = (name = '') => {
  const fingerprint = createFingerprint()
  let port: Runtime.Port
  let undeliveredQueue: ReadonlyArray<QueuedMessage> = []
  const pendingResponses = createDeliveryLogger()
  const onMessageListeners = new Set<
  (message: InternalMessage, port: Runtime.Port) => void
  >()
  const onFailureListeners = new Set<(message: InternalMessage) => void>()

  const handleMessage = (msg: StatusMessage, port: Runtime.Port) => {
    switch (msg.status) {
      case 'undeliverable':
        if (
          !undeliveredQueue.some(
            m => m.message.messageID === msg.message.messageID,
          )
        ) {
          undeliveredQueue = [
            ...undeliveredQueue,
            {
              message: msg.message,
              resolvedDestination: msg.resolvedDestination,
            },
          ]
        }

        return

      case 'deliverable':
        undeliveredQueue = undeliveredQueue.reduce((acc, queuedMsg) => {
          if (queuedMsg.resolvedDestination === msg.deliverableTo) {
            PortMessage.toBackground(port, {
              type: 'deliver',
              message: queuedMsg.message,
            })

            return acc
          }

          return [...acc, queuedMsg]
        }, [] as ReadonlyArray<QueuedMessage>)

        return

      case 'delivered':
        if (msg.receipt.message.messageType === 'message')
          pendingResponses.add(msg.receipt)

        return

      case 'incoming':
        if (msg.message.messageType === 'reply')
          pendingResponses.remove(msg.message.messageID)

        onMessageListeners.forEach(cb => cb(msg.message, port))

        return

      case 'terminated': {
        const rogueMsgs = pendingResponses
          .entries()
          .filter(receipt => msg.fingerprint === receipt.to)
        pendingResponses.remove(rogueMsgs)
        rogueMsgs.forEach(({ message }) =>
          onFailureListeners.forEach(cb => cb(message)),
        )
      }
    }
  }

  const connect = () => {
    port = browser.runtime.connect({
      name: encodeConnectionArgs({
        endpointName: name,
        fingerprint,
      }),
    })
    port.onMessage.addListener(handleMessage)
    port.onDisconnect.addListener(connect)

    PortMessage.toBackground(port, {
      type: 'sync',
      pendingResponses: pendingResponses.entries(),
      pendingDeliveries: [
        ...new Set(
          undeliveredQueue.map(({ resolvedDestination }) => resolvedDestination),
        ),
      ],
    })
  }

  connect()

  return {
    onFailure(cb: (message: InternalMessage) => void) {
      onFailureListeners.add(cb)
    },
    onMessage(cb: (message: InternalMessage) => void): void {
      onMessageListeners.add(cb)
    },
    postMessage(message: any): void {
      PortMessage.toBackground(port, {
        type: 'deliver',
        message,
      })
    },
  }
}