forked from arnellebalane/hermes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
hermes.js
175 lines (150 loc) · 5.95 KB
/
hermes.js
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
(function(global, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
global.hermes = factory();
}
})(window, function() {
const callbacks = {};
function on(name, callback) {
if (!(name in callbacks)) {
callbacks[name] = [];
}
callbacks[name].push(callback);
}
function off(name, callback) {
if (name in callbacks) {
if (typeof callback === 'function') {
const index = callbacks[name].indexOf(callback);
callbacks[name].splice(index, 1);
}
if (typeof callback !== 'function' || callbacks[name].length === 0) {
delete callbacks[name];
}
}
}
function broadcast(name, data) {
if (name in callbacks) {
callbacks[name].forEach((callback) => callback(data));
}
}
function broadcastChannelApiFactory() {
/**
* The BroadcastChannel API allows simple communication between
* browsing contexts (including tabs), sort of like a PubSub that
* works across different tabs. This is the ideal solution for
* messaging between different tabs, but it is relatively new.
*
* Support table for BroadcastChannel: http://caniuse.com/#feat=broadcastchannel
**/
const channel = new BroadcastChannel('hermes');
channel.onmessage = (e) => broadcast(e.data.name, e.data.data);
function send(name, data, includeSelf=false) {
channel.postMessage({ name, data });
if (includeSelf) {
broadcast(name, data);
}
}
return { on, off, send };
}
function sharedWorkerApiFactory() {
/**
* A SharedWorker is a script that is run by the browser in the
* background. Different browsing contexts (including tabs) from the
* same origin have shared accesss to the SharedWorker instance and
* can communicate with it. We are taking advantage of these features
* to use it as a messaging channel which simply forwards messages it
* receives to the other connected tabs.
*
* Support table for SharedWorker: http://caniuse.com/#feat=sharedworkers
**/
const selector = '[src$="hermes.js"],[src$="hermes.min.js"]';
const script = document.querySelector(selector);
const scriptUrl = new URL(script.src);
const workerPath = scriptUrl.pathname
.replace(/hermes(\.min)?\.js/, 'hermes-worker$1.js');
const worker = new SharedWorker(workerPath, 'hermes');
worker.port.start();
worker.port.onmessage = (e) => broadcast(e.data.name, e.data.data);
function send(name, data, includeSelf=false) {
worker.port.postMessage({ name, data });
if (includeSelf) {
broadcast(name, data);
}
}
return { on, off, send };
}
function localStorageApiFactory() {
/**
* The localStorage is a key-value pair storage, and browser tabs from
* the same origin have shared access to it. Whenever something
* changes in the localStorage, the window object emits the `storage`
* event in the other tabs letting them know about the change.
*
* Support table for localStorage: http://caniuse.com/#search=webstorage
**/
const storage = window.localStorage;
const prefix = '__hermes:';
const queue = {};
window.addEventListener('storage', (e) => {
if (e.key.indexOf(prefix) === 0 && e.oldValue === null) {
const name = e.key.replace(prefix, '');
const data = JSON.parse(e.newValue);
broadcast(name, data);
}
});
window.addEventListener('storage', (e) => {
if (e.key.indexOf(prefix) === 0 && e.newValue === null) {
const name = e.key.replace(prefix, '');
if (name in queue) {
send(name, queue[name].shift());
if (queue[name].length === 0) {
delete queue[name];
}
}
}
});
function send(name, data, includeSelf=false) {
const key = prefix + name;
if (storage.getItem(key) === null) {
storage.setItem(key, JSON.stringify(data));
storage.removeItem(key);
if (includeSelf) {
broadcast(name, data);
}
} else {
// The queueing system ensures that multiple calls to the send
// function using the same name does not override each other's
// values and makes sure that the next value is sent only when
// the previous one has already been deleted from the storage.
// NOTE: This could just be trying to solve a problem that is
// very unlikely to occur.
if (!(key) in queue) {
queue[key] = [];
}
queue[key].push(data);
}
}
return { on, off, send };
}
function emptyApiFactory() {
/**
* When the browser does not support any of the APIs that we're using
* for messaging, just present an empty api that does just gives
* warnings regarding the lack of support.
**/
function noop() {
console.warn('Hermes messaging is not supported.');
}
return { on: noop, off: noop, send: noop };
}
if ('BroadcastChannel' in window) {
return broadcastChannelApiFactory();
} else if ('SharedWorker' in window) {
return sharedWorkerApiFactory();
} else if ('localStorage' in window) {
return localStorageApiFactory();
} else {
return emptyApiFactory();
}
});