-
Notifications
You must be signed in to change notification settings - Fork 25
/
WindowController.js
272 lines (220 loc) · 8.11 KB
/
WindowController.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/*global window, document, localStorage */
import { Class } from '../core/Core.js';
import { Obj } from '../foundation/Object.js';
import { invokeInNextEventLoop } from '../foundation/RunLoop.js';
import '../core/Date.js'; // For Date#format
/* { on, invokeInRunLoop } from */
import '../foundation/Decorators.js';
/**
Class: O.WindowController
Extends: O.Object
If your application is open in multiple browser windows/tabs, you may want
to coordinate behaviour between them, but you can't get a direct reference
to a window you didn't explicitly open. This class allows you to broadcast
messages to other tabs open in the same domain, so you can still coordinate
behaviour. In particular, if you use push events in your application, you
probably want only one tab to actually hold a permanent connection to the
push server. Browsers limit the maximum number of simultaneous connections
to the same server, so if you have (say) 6 tabs open, all of the allowed
connections could be taken up with push connections which are never
released, so your application cannot perform any other I/O and will appear
to mysteriously fail.
The WindowController class automatically coordinates between windows to
elect a single tab to be "master". You can connect just this one to your
push server, then broadcast any push events received to the other tabs via
this controller as well. It also monitors whether the window currently has
focus or not.
*/
const WindowController = Class({
Name: 'WindowController',
Extends: Obj,
/**
Property: O.WindowController#broadcastKey
Type: String
Default: "owm:broadcast"
The key to use for the local storage property that will be set to
broadcast messages to other tabs.
*/
broadcastKey: 'owm:broadcast',
/**
Property: O.WindowController#isMaster
Type: Boolean
Is this tab/window the elected "master"? If multiple windows with the
application are open, they will coordinate between themselves so only
one has the isMaster property set to true. Note, in some circumstances,
this may not happen instantly and there may be a short while when there
is no master or more than one master. However, it will quickly resolve
itself.
*/
/**
Property: O.WindowController#isFocused
Type: Boolean
Is the tab/window currently focused?
*/
/**
Property: O.WindowController#id
Type: String
A unique id for the window, guaranteed to be different than for any
other open window.
*/
init: function (/* ...mixins */) {
this.id = new Date().format('%y%m%d%H%M%S') + Math.random();
this.isMaster = false;
this.isFocused = document.hasFocus ? document.hasFocus() : true;
this._seenWCs = {};
WindowController.parent.constructor.apply(this, arguments);
window.addEventListener('storage', this, false);
window.addEventListener('unload', this, false);
window.addEventListener('focus', this, false);
window.addEventListener('blur', this, false);
invokeInNextEventLoop(this.checkMaster, this);
this.start();
},
destroy() {
this.end(this.get('broadcastKey'));
window.removeEventListener('storage', this, false);
window.removeEventListener('unload', this, false);
window.removeEventListener('focus', this, false);
window.removeEventListener('blur', this, false);
WindowController.parent.destroy.call(this);
},
/**
Method: O.WindowController#start
Sends a message to let other windows know a new one has been created.
*/
start() {
this.broadcast('wc:hello');
},
/**
Method: O.WindowController#end
Sends a message to let other windows know this one has been destroyed.
Parameters:
broadcastKey - {String} The broadcast key.
*/
end(broadcastKey) {
this.broadcast('wc:bye', null, broadcastKey);
},
/**
Method: O.WindowController#broadcastKeyDidChange
Observes changes to the broadcastKey and synchronizes with other
windows.
Parameters:
oldBroadcastKey - {String} The previous broadcast key.
*/
broadcastKeyDidChange: function (_, __, oldBroadcastKey) {
this.end(oldBroadcastKey);
this.start();
}.observes('broadcastKey'),
/**
Method (protected): O.WindowController#handleEvent
Handles storage, unload, focus and blur events.
Parameters:
event - {Event} The event object.
*/
handleEvent: function (event) {
switch (event.type) {
case 'storage':
if (event.key === this.get('broadcastKey')) {
try {
const data = JSON.parse(event.newValue);
// IE fires events in the same window that set the
// property. Ignore these.
if (data.wcId !== this.id) {
this.fire(data.type, data);
}
} catch (error) {}
}
break;
case 'unload':
this.destroy();
break;
case 'focus':
this.set('isFocused', true);
break;
case 'blur':
this.set('isFocused', false);
break;
}
}.invokeInRunLoop(),
/**
Method (protected): O.WindowController#sendPing
Sends a ping to let other windows know about the existence of this one.
*/
sendPing() {
this.broadcast('wc:ping');
},
/**
Method (private): O.WindowController#_hello
Handles the arrival of a new window.
Parameters:
event - {Event} An event object containing the window id.
*/
_hello: function (event) {
this._ping(event);
this.sendPing();
}.on('wc:hello'),
/**
Method (private): O.WindowController#_ping
Handles a ping from another window.
Parameters:
event - {Event} An event object containing the window id.
*/
_ping: function (event) {
const wcId = event.wcId;
this._seenWCs[wcId] = true;
if (wcId < this.id) {
this.checkMaster();
}
}.on('wc:ping'),
/**
Method (private): O.WindowController#_bye
Handles the departure of another window.
Parameters:
event - {Event} An event object containing the window id.
*/
_bye: function (event) {
delete this._seenWCs[event.wcId];
this.checkMaster();
}.on('wc:bye'),
/**
Method: O.WindowController#checkMaster
Looks at the set of other windows it knows about and sets the isMaster
property based on whether this window has the lowest ordered id.
*/
checkMaster() {
let isMaster = true;
const ourId = this.id;
for (const id in this._seenWCs) {
if (id < ourId) {
isMaster = false;
}
}
this.set('isMaster', isMaster);
},
/**
Method: O.WindowController#broadcast
Broadcast an event with JSON-serialisable data to other tabs.
Parameters:
type - {String} The name of the event being broadcast.
data - {Object} (optional). The data to broadcast.
broadcastKey - {String} (optional). The key to use; otherwise the
key will be taken from the broadcastKey property.
*/
broadcast(type, data, broadcastKey) {
try {
localStorage.setItem(
broadcastKey || this.get('broadcastKey'),
JSON.stringify(
Object.assign(
{
wcId: this.id,
type,
},
data,
),
),
);
} catch (error) {}
},
});
export { WindowController };