UNPKG

3.04 kBJavaScriptView Raw
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
20import * as map from './map.js'
21import * as set from './set.js'
22import * as buffer from './buffer.js'
23import * as storage from './storage.js'
24
25/**
26 * @typedef {Object} Channel
27 * @property {Set<function(any, any):any>} Channel.subs
28 * @property {any} Channel.bc
29 */
30
31/**
32 * @type {Map<string, Channel>}
33 */
34const channels = new Map()
35
36/* c8 ignore start */
37class LocalStoragePolyfill {
38 /**
39 * @param {string} room
40 */
41 constructor (room) {
42 this.room = room
43 /**
44 * @type {null|function({data:ArrayBuffer}):void}
45 */
46 this.onmessage = null
47 /**
48 * @param {any} e
49 */
50 this._onChange = e => e.key === room && this.onmessage !== null && this.onmessage({ data: buffer.fromBase64(e.newValue || '') })
51 storage.onChange(this._onChange)
52 }
53
54 /**
55 * @param {ArrayBuffer} buf
56 */
57 postMessage (buf) {
58 storage.varStorage.setItem(this.room, buffer.toBase64(buffer.createUint8ArrayFromArrayBuffer(buf)))
59 }
60
61 close () {
62 storage.offChange(this._onChange)
63 }
64}
65/* c8 ignore stop */
66
67// Use BroadcastChannel or Polyfill
68/* c8 ignore next */
69const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel
70
71/**
72 * @param {string} room
73 * @return {Channel}
74 */
75const getChannel = room =>
76 map.setIfUndefined(channels, room, () => {
77 const subs = set.create()
78 const bc = new BC(room)
79 /**
80 * @param {{data:ArrayBuffer}} e
81 */
82 /* c8 ignore next */
83 bc.onmessage = e => subs.forEach(sub => sub(e.data, 'broadcastchannel'))
84 return {
85 bc, subs
86 }
87 })
88
89/**
90 * Subscribe to global `publish` events.
91 *
92 * @function
93 * @param {string} room
94 * @param {function(any, any):any} f
95 */
96export const subscribe = (room, f) => {
97 getChannel(room).subs.add(f)
98 return f
99}
100
101/**
102 * Unsubscribe from `publish` global events.
103 *
104 * @function
105 * @param {string} room
106 * @param {function(any, any):any} f
107 */
108export const unsubscribe = (room, f) => {
109 const channel = getChannel(room)
110 const unsubscribed = channel.subs.delete(f)
111 if (unsubscribed && channel.subs.size === 0) {
112 channel.bc.close()
113 channels.delete(room)
114 }
115 return unsubscribed
116}
117
118/**
119 * Publish data to all subscribers (including subscribers on this tab)
120 *
121 * @function
122 * @param {string} room
123 * @param {any} data
124 * @param {any} [origin]
125 */
126export const publish = (room, data, origin = null) => {
127 const c = getChannel(room)
128 c.bc.postMessage(data)
129 c.subs.forEach(sub => sub(data, origin))
130}