Skip to content

Commit 86fea68

Browse files
Various auth fixes (#1141)
* Fixes exportPrototypeProperties for multiple properties where the last property overrides the previous properties. * Fixes fireauth.messagechannel.Sender. Currently, this does not allow sending multiple messages from the same sender, since the port can only be transferred one time. The implementation has been modified to open a new messagechannel on each message instead. Note this does not affect indexeddb usage of this utility since a new sender is created each time and one message sent. * Fixes service worker indexedDB on some slow browsers, which was detected in Edge. Unfortunately, some browsers (detected in Edge) are too slow to detect the first ack event and timeout quickly. Instead what we can do is assume the following: 1. one service worker activated at one time. 2. The service worker once subscribed to the keyChanged event will not unsubscribe. So on initialization, the client will ping the service worker when it's ready to get all subscribed events with a long timeout. If the service worker responds with the keyChanged event, we can deduce that the sw can handle this event. The next time a change event occurs, we can safely use a long ACK timeout.
1 parent 3e98afa commit 86fea68

File tree

9 files changed

+452
-140
lines changed

9 files changed

+452
-140
lines changed

packages/auth/src/exports_lib.js

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,26 +114,43 @@ fireauth.exportlib.exportPrototypeProperties = function(protObj, propMap) {
114114
if (unobfuscatedPropName === obfuscatedPropName) {
115115
continue;
116116
}
117+
/**
118+
* @this {!Object}
119+
* @param {string} obfuscatedPropName The obfuscated property name.
120+
* @return {*} The value of the property.
121+
*/
122+
var getter = function(obfuscatedPropName) {
123+
return this[obfuscatedPropName];
124+
};
125+
/**
126+
* @this {!Object}
127+
* @param {string} unobfuscatedPropName The unobfuscated property name.
128+
* @param {string} obfuscatedPropName The obfuscated property name.
129+
* @param {!fireauth.args.Argument} expectedArg The expected argument to the
130+
* setter of this property.
131+
* @param {*} value The new value of the property.
132+
*/
133+
var setter = function(unobfuscatedPropName, obfuscatedPropName,
134+
expectedArg, value) {
135+
// Validate the argument before setting it.
136+
fireauth.args.validate(
137+
unobfuscatedPropName, [expectedArg], [value], true);
138+
this[obfuscatedPropName] = value;
139+
};
117140
// Get the expected argument.
118141
var expectedArg = propMap[obfuscatedPropName].arg;
119142
Object.defineProperty(protObj, unobfuscatedPropName, {
120143
/**
121144
* @this {!Object}
122-
* @return {string} The value of the property.
145+
* @return {*} The value of the property.
123146
*/
124-
get: function() {
125-
return this[obfuscatedPropName];
126-
},
147+
get: goog.partial(getter, obfuscatedPropName),
127148
/**
128149
* @this {!Object}
129-
* @param {string} value The new value of the property.
150+
* @param {*} value The new value of the property.
130151
*/
131-
set: function(value) {
132-
// Validate the argument before setting it.
133-
fireauth.args.validate(
134-
unobfuscatedPropName, [expectedArg], [value], true);
135-
this[obfuscatedPropName] = value;
136-
},
152+
set: goog.partial(setter, unobfuscatedPropName, obfuscatedPropName,
153+
expectedArg),
137154
enumerable: true
138155
});
139156
}

packages/auth/src/messagechannel/defines.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ goog.provide('fireauth.messagechannel.utils');
3030
* @enum {string}
3131
*/
3232
fireauth.messagechannel.Error = {
33+
CONNECTION_CLOSED: 'connection_closed',
3334
CONNECTION_UNAVAILABLE: 'connection_unavailable',
3435
INVALID_RESPONSE: 'invalid_response',
3536
TIMEOUT: 'timeout',
@@ -53,8 +54,10 @@ fireauth.messagechannel.Status = {
5354
* @enum {number}
5455
*/
5556
fireauth.messagechannel.TimeoutDuration = {
56-
ACK: 20,
57-
COMPLETION: 500
57+
ACK: 50,
58+
COMPLETION: 3000,
59+
// Used when a handler is confirmed to be available on the other side.
60+
LONG_ACK: 800
5861
};
5962

6063

packages/auth/src/messagechannel/receiver.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ fireauth.messagechannel.Receiver = function(eventTarget) {
4343
this.eventTarget_ = eventTarget;
4444
/**
4545
* @const @private {!Object.<string,
46-
* !Array<function(string, *):!goog.Promise<*>|void>>}
46+
* !Array<function(string, *):!goog.Promise<?>|void>>}
4747
* This is the event type to handlers hash map. It is used to hold the
4848
* corresponding handlers for specified events.
4949
*/
5050
this.eventHandlers_ = {};
5151
/**
52-
* @const@private {function(!Event)} The internal 'message' event handler used
53-
* to reroute the request to corresponding subscribed handlers.
52+
* @const @private {function(!Event)} The internal 'message' event handler
53+
* used to reroute the request to corresponding subscribed handlers.
5454
*/
5555
this.messageEventHandler_ = goog.bind(this.handleEvent_, this);
5656
};
@@ -180,7 +180,7 @@ fireauth.messagechannel.Receiver.prototype.handleEvent_ = function(event) {
180180
/**
181181
* Subscribes to events of the specified type.
182182
* @param {string} eventType The event type to listen to.
183-
* @param {function(string, *):!goog.Promise<*>|void} handler The async callback
183+
* @param {function(string, *):!goog.Promise<?>|void} handler The async callback
184184
* function to run when the event is triggered.
185185
*/
186186
fireauth.messagechannel.Receiver.prototype.subscribe =
@@ -199,7 +199,7 @@ fireauth.messagechannel.Receiver.prototype.subscribe =
199199
* Unsubscribes the specified handler from the specified event. If no handler
200200
* is specified, all handlers are unsubscribed.
201201
* @param {string} eventType The event type to unsubscribe from.
202-
* @param {?function(string, *):!goog.Promise<*>|void=} opt_handler The
202+
* @param {?function(string, *):!goog.Promise<?>|void=} opt_handler The
203203
* callback function to unsubscribe from the specified event type. If none
204204
* is specified, all handlers are unsubscribed.
205205
*/

packages/auth/src/messagechannel/sender.js

Lines changed: 58 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ goog.require('goog.Promise');
3030
goog.require('goog.array');
3131

3232

33+
/**
34+
* This is the interface defining a MessageChannel/handler pair.
35+
*
36+
* @typedef {{
37+
* onMessage: function(!Event),
38+
* messageChannel: !MessageChannel
39+
* }}
40+
*/
41+
fireauth.messagechannel.MessageHandler;
42+
3343

3444
/**
3545
* Helper static function to create messageChannel errors.
@@ -60,17 +70,11 @@ fireauth.messagechannel.Sender = function(postMessager) {
6070
* PostMessager.
6171
*/
6272
this.postMessager_ = postMessager;
73+
/** @private {boolean} Whether the connection was closed. */
74+
this.closed_ = false;
6375
/**
64-
* @private {?MessageChannel} The messageChannel reference if
65-
* supported.
66-
*/
67-
this.messageChannel_ =
68-
fireauth.messagechannel.utils.initializeMessageChannel();
69-
/** @private {boolean} Whether the connection was started. */
70-
this.started_ = false;
71-
/**
72-
* @const @private {!Array<function(!Event)>} The list of subscribed message
73-
* handlers.
76+
* @const @private {!Array<!fireauth.messagechannel.MessageHandler>} The list
77+
* of subscribed message handlers and their corresponding MessageChannels.
7478
*/
7579
this.messageHandlers_ = [];
7680
};
@@ -94,24 +98,38 @@ fireauth.messagechannel.Sender = function(postMessager) {
9498
* @param {string} eventType The event type identifying the message. This is
9599
* used to help the receiver handle this message.
96100
* @param {?Object=} opt_data The optional data to send along the message.
101+
* @param {?boolean=} opt_useLongTimeout Whether long timeout should be used
102+
* for ACK responses.
97103
* @return {!goog.Promise<!Array<{fulfilled: boolean,
98104
* value: (*|undefined),
99105
* reason: (*|undefined)}>>} A promise that
100106
* resolves with the receiver responses.
101107
*/
102-
fireauth.messagechannel.Sender.prototype.send = function(eventType, opt_data) {
108+
fireauth.messagechannel.Sender.prototype.send = function(
109+
eventType, opt_data, opt_useLongTimeout) {
103110
var self = this;
104111
var eventId;
105112
var data = opt_data || {};
106113
var onMessage;
107114
var ackTimer;
108115
var completionTimer;
116+
var entry = null;
117+
if (this.closed_) {
118+
return goog.Promise.reject(fireauth.messagechannel.createError_(
119+
fireauth.messagechannel.Error.CONNECTION_UNAVAILABLE));
120+
}
121+
var ackTimeout =
122+
!!opt_useLongTimeout ?
123+
fireauth.messagechannel.TimeoutDuration.LONG_ACK :
124+
fireauth.messagechannel.TimeoutDuration.ACK;
125+
var messageChannel =
126+
fireauth.messagechannel.utils.initializeMessageChannel();
109127
return new goog.Promise(function(resolve, reject) {
110128
// Send message along with port for reply
111-
if (self.messageChannel_) {
129+
if (messageChannel) {
112130
eventId = fireauth.messagechannel.utils.generateEventId();
113131
// Start the connection if not already started.
114-
self.start();
132+
messageChannel.port1.start();
115133
// Handler for receiving message reply from receiver.
116134
// Blocks promise resolution until service worker detects the change.
117135
ackTimer = setTimeout(function() {
@@ -121,7 +139,7 @@ fireauth.messagechannel.Sender.prototype.send = function(eventType, opt_data) {
121139
// Timeout after some time.
122140
reject(fireauth.messagechannel.createError_(
123141
fireauth.messagechannel.Error.UNSUPPORTED_EVENT));
124-
}, fireauth.messagechannel.TimeoutDuration.ACK);
142+
}, ackTimeout);
125143
onMessage = function(event) {
126144
// Process only the expected events that match current event ID.
127145
if (event.data['eventId'] !== eventId) {
@@ -153,8 +171,12 @@ fireauth.messagechannel.Sender.prototype.send = function(eventType, opt_data) {
153171
fireauth.messagechannel.Error.INVALID_RESPONSE));
154172
}
155173
};
156-
self.messageHandlers_.push(onMessage);
157-
self.messageChannel_.port1.addEventListener('message', onMessage);
174+
entry = {
175+
'messageChannel': messageChannel,
176+
'onMessage': onMessage
177+
};
178+
self.messageHandlers_.push(entry);
179+
messageChannel.port1.addEventListener('message', onMessage);
158180
var request = {
159181
'eventType': eventType,
160182
'eventId': eventId,
@@ -165,7 +187,7 @@ fireauth.messagechannel.Sender.prototype.send = function(eventType, opt_data) {
165187
// receiver or using an outdated version.
166188
self.postMessager_.postMessage(
167189
request,
168-
[self.messageChannel_.port2]);
190+
[messageChannel.port2]);
169191
} else {
170192
// No connection available.
171193
reject(fireauth.messagechannel.createError_(
@@ -174,64 +196,45 @@ fireauth.messagechannel.Sender.prototype.send = function(eventType, opt_data) {
174196
}).then(function(result) {
175197
// On completion, remove the message handler. A new one is needed for a
176198
// new message.
177-
self.removeMessageHandler_(onMessage);
199+
self.removeMessageHandler_(entry);
178200
return result;
179201
}).thenCatch(function(error) {
180202
// On failure, remove the message handler. A new one is needed for a new
181203
// message.
182-
self.removeMessageHandler_(onMessage);
204+
self.removeMessageHandler_(entry);
183205
throw error;
184206
});
185207
};
186208

187209

188210
/**
189-
* @param {function(!Event)} onMessage The message handler to remove.
211+
* Removes the onMessage handler for the specified messageChannel.
212+
* @param {?fireauth.messagechannel.MessageHandler} messageHandler
190213
* @private
191214
*/
192215
fireauth.messagechannel.Sender.prototype.removeMessageHandler_ =
193-
function(onMessage) {
194-
if (this.messageChannel_) {
195-
goog.array.removeAllIf(this.messageHandlers_, function(ele) {
196-
return ele == onMessage;
197-
});
198-
this.messageChannel_.port1.removeEventListener('message', onMessage);
216+
function(messageHandler) {
217+
if (!messageHandler) {
218+
return;
199219
}
200-
};
201-
202-
203-
/**
204-
* Removing all subscribed message handlers.
205-
* @private
206-
*/
207-
fireauth.messagechannel.Sender.prototype.removeAllMessageHandlers_ =
208-
function() {
209-
while (this.messageHandlers_.length > 0) {
210-
var onMessage = this.messageHandlers_.pop();
211-
this.messageChannel_.port1.removeEventListener('message', onMessage);
220+
var messageChannel = messageHandler['messageChannel'];
221+
var onMessage = messageHandler['onMessage'];
222+
if (messageChannel) {
223+
messageChannel.port1.removeEventListener('message', onMessage);
224+
messageChannel.port1.close();
212225
}
226+
goog.array.removeAllIf(this.messageHandlers_, function(ele) {
227+
return ele == messageHandler;
228+
});
213229
};
214230

215231

216232
/** Closes the underlying MessageChannel connection. */
217233
fireauth.messagechannel.Sender.prototype.close = function() {
218-
if (this.messageChannel_) {
219-
// Any pending event will timeout.
220-
this.removeAllMessageHandlers_();
221-
// No new messages will be returned.
222-
this.messageChannel_.port1.close();
223-
this.messageChannel_ = null;
224-
this.started_ = false;
234+
// Any pending event will timeout.
235+
while (this.messageHandlers_.length > 0) {
236+
this.removeMessageHandler_(this.messageHandlers_[0]);
225237
}
238+
this.closed_ = true;
226239
};
227240

228-
229-
/** Starts the underlying MessageChannel connection if not already started. */
230-
fireauth.messagechannel.Sender.prototype.start = function() {
231-
// Note that re-connection is not supported. If a connection is closed, a new
232-
// sender has to be created.
233-
if (this.messageChannel_ && !this.started_) {
234-
this.messageChannel_.port1.start();
235-
this.started_ = true;
236-
}
237-
};

packages/auth/src/storage/indexeddb.js

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ fireauth.storage.IndexedDB = function(
104104
* running from a serviceworker.
105105
*/
106106
this.receiver_ = null;
107+
/**
108+
* @private {?fireauth.messagechannel.Sender} The messageChannel sender to
109+
* send keyChanged messages to the service worker from the client.
110+
*/
111+
this.sender_ = null;
112+
/**
113+
* @private {boolean} Whether the service worker has a receiver for the
114+
* keyChanged events.
115+
*/
116+
this.serviceWorkerReceiverAvailable_ = false;
117+
/** @private {?ServiceWorker} The current active service worker. */
118+
this.activeServiceWorker_ = null;
107119
var scope = this;
108120
if (fireauth.util.getWorkerGlobalScope()) {
109121
this.receiver_ = fireauth.messagechannel.Receiver.getInstance(
@@ -128,6 +140,34 @@ fireauth.storage.IndexedDB = function(
128140
};
129141
});
130142
});
143+
// Used to inform sender that service worker what events it supports.
144+
this.receiver_.subscribe('ping', function(origin, request) {
145+
return goog.Promise.resolve(['keyChanged']);
146+
});
147+
} else {
148+
// Get active service worker when its available.
149+
fireauth.util.getActiveServiceWorker()
150+
.then(function(sw) {
151+
scope.activeServiceWorker_ = sw;
152+
if (sw) {
153+
// Initialize the sender.
154+
scope.sender_ = new fireauth.messagechannel.Sender(
155+
new fireauth.messagechannel.WorkerClientPostMessager(sw));
156+
// Ping the service worker to check what events they can handle.
157+
// Use long timeout.
158+
scope.sender_.send('ping', null, true)
159+
.then(function(results) {
160+
// Check if keyChanged is supported.
161+
if (results[0]['fulfilled'] &&
162+
goog.array.contains(results[0]['value'], 'keyChanged')) {
163+
scope.serviceWorkerReceiverAvailable_ = true;
164+
}
165+
})
166+
.thenCatch(function(error) {
167+
// Ignore error.
168+
});
169+
}
170+
});
131171
}
132172
};
133173

@@ -413,12 +453,18 @@ fireauth.storage.IndexedDB.prototype.set = function(key, value) {
413453
* @private
414454
*/
415455
fireauth.storage.IndexedDB.prototype.notifySW_ = function(key) {
416-
if (fireauth.util.getServiceWorkerController()) {
417-
var sender = new fireauth.messagechannel.Sender(
418-
new fireauth.messagechannel.WorkerClientPostMessager(
419-
/** @type {!ServiceWorker} */ (
420-
fireauth.util.getServiceWorkerController())));
421-
return sender.send('keyChanged', {'key': key})
456+
// If sender is available.
457+
// Run some sanity check to confirm no sw change occurred.
458+
// For now, we support one service worker per page.
459+
if (this.sender_ &&
460+
this.activeServiceWorker_ &&
461+
fireauth.util.getServiceWorkerController() ===
462+
this.activeServiceWorker_) {
463+
return this.sender_.send(
464+
'keyChanged',
465+
{'key': key},
466+
// Use long timeout if receiver is known to be available.
467+
this.serviceWorkerReceiverAvailable_)
422468
.then(function(responses) {
423469
// Return nothing.
424470
})

0 commit comments

Comments
 (0)