1 | /* eslint-env browser */
|
2 |
|
3 | /**
|
4 | * Helpers for cross-tab communication using broadcastchannel with LocalStorage fallback.
|
5 | *
|
6 | * ```js
|
7 | * // In browser window A:
|
8 | * broadcastchannel.subscribe('my events', data => console.log(data))
|
9 | * broadcastchannel.publish('my events', 'Hello world!') // => A: 'Hello world!' fires synchronously in same tab
|
10 | *
|
11 | * // In browser window B:
|
12 | * broadcastchannel.publish('my events', 'hello from tab B') // => A: 'hello from tab B'
|
13 | * ```
|
14 | *
|
15 | * @module broadcastchannel
|
16 | */
|
17 |
|
18 | // @todo before next major: use Uint8Array instead as buffer object
|
19 |
|
20 | import * as map from './map.js'
|
21 | import * as buffer from './buffer.js'
|
22 | import * as storage from './storage.js'
|
23 |
|
24 | /**
|
25 | * @typedef {Object} Channel
|
26 | * @property {Set<function(any, any):any>} Channel.subs
|
27 | * @property {any} Channel.bc
|
28 | */
|
29 |
|
30 | /**
|
31 | * @type {Map<string, Channel>}
|
32 | */
|
33 | const channels = new Map()
|
34 |
|
35 | class LocalStoragePolyfill {
|
36 | /**
|
37 | * @param {string} room
|
38 | */
|
39 | constructor (room) {
|
40 | this.room = room
|
41 | /**
|
42 | * @type {null|function({data:ArrayBuffer}):void}
|
43 | */
|
44 | this.onmessage = null
|
45 | storage.onChange(e => e.key === room && this.onmessage !== null && this.onmessage({ data: buffer.fromBase64(e.newValue || '') }))
|
46 | }
|
47 |
|
48 | /**
|
49 | * @param {ArrayBuffer} buf
|
50 | */
|
51 | postMessage (buf) {
|
52 | storage.varStorage.setItem(this.room, buffer.toBase64(buffer.createUint8ArrayFromArrayBuffer(buf)))
|
53 | }
|
54 | }
|
55 |
|
56 | // Use BroadcastChannel or Polyfill
|
57 | const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel
|
58 |
|
59 | /**
|
60 | * @param {string} room
|
61 | * @return {Channel}
|
62 | */
|
63 | const getChannel = room =>
|
64 | map.setIfUndefined(channels, room, () => {
|
65 | const subs = new Set()
|
66 | const bc = new BC(room)
|
67 | /**
|
68 | * @param {{data:ArrayBuffer}} e
|
69 | */
|
70 | bc.onmessage = e => subs.forEach(sub => sub(e.data, 'broadcastchannel'))
|
71 | return {
|
72 | bc, subs
|
73 | }
|
74 | })
|
75 |
|
76 | /**
|
77 | * Subscribe to global `publish` events.
|
78 | *
|
79 | * @function
|
80 | * @param {string} room
|
81 | * @param {function(any, any):any} f
|
82 | */
|
83 | export const subscribe = (room, f) => getChannel(room).subs.add(f)
|
84 |
|
85 | /**
|
86 | * Unsubscribe from `publish` global events.
|
87 | *
|
88 | * @function
|
89 | * @param {string} room
|
90 | * @param {function(any, any):any} f
|
91 | */
|
92 | export const unsubscribe = (room, f) => getChannel(room).subs.delete(f)
|
93 |
|
94 | /**
|
95 | * Publish data to all subscribers (including subscribers on this tab)
|
96 | *
|
97 | * @function
|
98 | * @param {string} room
|
99 | * @param {any} data
|
100 | * @param {any} [origin]
|
101 | */
|
102 | export const publish = (room, data, origin = null) => {
|
103 | const c = getChannel(room)
|
104 | c.bc.postMessage(data)
|
105 | c.subs.forEach(sub => sub(data, origin))
|
106 | }
|