diff --git a/lib/ddp.js b/lib/ddp.js index 267f98e..a715dbf 100644 --- a/lib/ddp.js +++ b/lib/ddp.js @@ -138,6 +138,8 @@ class DDP extends EventEmitter { // Default `autoConnect` and `autoReconnect` to true this.autoConnect = options.autoConnect !== false; this.autoReconnect = options.autoReconnect !== false; + // save the initial value of autoReconnect to be able to reset it + this._autoReconnectInitital = options.autoReconnect !== false; this.reconnectInterval = options.reconnectInterval || DEFAULT_RECONNECT_INTERVAL; @@ -203,7 +205,7 @@ class DDP extends EventEmitter { } /** - * Emits a new event. + * Emits a new event. Wraps emitting in a setTimeout (macrotask) * @override */ emit(...args) { @@ -213,8 +215,12 @@ class DDP extends EventEmitter { /** * Initiates the underlying websocket to open the connection + * If the connection was closed before eg. by calling `disconnect` + * from the client side, + * the value of `autoReconnect` will be reset to the initial value. */ connect() { + this.autoReconnect = this._autoReconnectInitital; this.socket.open(); } diff --git a/src/Meteor.js b/src/Meteor.js index 1c58421..299788d 100644 --- a/src/Meteor.js +++ b/src/Meteor.js @@ -127,9 +127,15 @@ const Meteor = { } } + if (!options.AsyncTokenStorage) { + options.AsyncTokenStorage = options.AsyncStorage; + } + Data._endpoint = endpoint; Data._options = options; + this._reactiveDict.set('_userReady', false); + const ddp = new DDP({ endpoint: endpoint, SocketConstructor: WebSocket, @@ -153,7 +159,7 @@ const Meteor = { if (this.isVerbose) { console.info('Connected to DDP server.'); } - this._loadInitialUser().then(() => { + User._loadInitialUser().then(() => { this._subscriptionsRestart(); }); this._reactiveDict.set('connected', true); @@ -179,6 +185,9 @@ const Meteor = { sub.readyDeps.changed(); } + // Mark user as not ready + this._reactiveDict.set('_userReady', false); + if (!Data.ddp.autoReconnect) return; if (!lastDisconnect || new Date() - lastDisconnect > 3000) { @@ -310,6 +319,7 @@ const Meteor = { } // Reconnect if we lose internet + NetInfo.addEventListener( ({ type, isConnected, isInternetReachable, isWifiEnabled }) => { if (isConnected && Data.ddp.autoReconnect) { @@ -321,7 +331,10 @@ const Meteor = { console.warn( 'Warning: NetInfo not installed, so DDP will not automatically reconnect' ); + Data.ddp.connect(); } + } else { + Data.ddp.connect(); } }, subscribe(name) { diff --git a/src/user/User.js b/src/user/User.js index 48262f2..be67ca6 100644 --- a/src/user/User.js +++ b/src/user/User.js @@ -32,6 +32,9 @@ const User = { const user = Users.findOne(user_id); return user && user._id; }, + userReady() { + return Meteor._reactiveDict.get('_userReady'); + }, _isLoggingIn: true, _isLoggingOut: false, loggingIn() { @@ -41,7 +44,7 @@ const User = { return User._isLoggingOut; }, logout(callback) { - this._isTokenLogin = false; + User._isTokenLogin = false; User._startLoggingOut(); Meteor.call('logout', (err) => { User.handleLogout(); @@ -51,15 +54,27 @@ const User = { }); }, handleLogout() { - Data._options.AsyncStorage.removeItem(TOKEN_KEY); + Data._options.AsyncTokenStorage.removeItem(TOKEN_KEY); Data._tokenIdSaved = null; + Meteor.isVerbose && + console.info('User.handleLogout()::: userId Saved: null'); this._reactiveDict.set('_userIdSaved', null); User._userIdSaved = null; User._endLoggingOut(); }, + loginWithCustomHandler(req, callback) { + User._isTokenLogin = false; + User._startLoggingIn(); + + Meteor.call('login', req, (err, result) => { + User._handleLoginCallback(err, result); + + typeof callback == 'function' && callback(err); + }); + }, loginWithPassword(selector, password, callback) { - this._isTokenLogin = false; + User._isTokenLogin = false; if (typeof selector === 'string') { if (selector.indexOf('@') === -1) selector = { username: selector }; else selector = { email: selector }; @@ -80,7 +95,7 @@ const User = { ); }, loginWithPasswordAnd2faCode(selector, password, code, callback) { - this._isTokenLogin = false; + User._isTokenLogin = false; if (typeof selector === 'string') { if (selector.indexOf('@') === -1) selector = { username: selector }; else selector = { email: selector }; @@ -129,6 +144,7 @@ const User = { Data.notify('loggingOut'); }, _endLoggingIn() { + Meteor._reactiveDict.set('_userReady', true); this._reactiveDict.set('_loggingIn', false); Data.notify('loggingIn'); }, @@ -145,17 +161,25 @@ const User = { 'id:', result.id ); - Data._options.AsyncStorage.setItem(TOKEN_KEY, result.token); + + Data._options.AsyncTokenStorage.setItem(TOKEN_KEY, result.token).catch( + (error) => { + console.warn( + 'AsyncStorage error: ' + error.message + ' while saving token' + ); + } + ); + Data._tokenIdSaved = result.token; this._reactiveDict.set('_userIdSaved', result.id); User._userIdSaved = result.id; User._endLoggingIn(); - this._isTokenLogin = false; + User._isTokenLogin = false; Data.notify('onLogin'); } else { Meteor.isVerbose && console.info('User._handleLoginCallback::: error:', err); - if (this._isTokenLogin) { + if (User._isTokenLogin) { setTimeout(() => { if (User._userIdSaved) { return; @@ -164,16 +188,33 @@ const User = { if (Meteor.user()) { return; } - User._loginWithToken(User._userIdSaved); + Meteor.isVerbose && + console.info( + 'User._handleLoginCallback::: trying again to login with ' + + Data._tokenIdSaved + + ' after ' + + this._timeout + + 'ms.' + ); + if (this._timeout > 10000) { + Meteor.isVerbose && + console.info( + 'User._handleLoginCallback::: 10000ms timeout exceeded, ending logging in.' + ); + User._endLoggingIn(); + Data.notify('onLoginFailure', err); + return; + } + User._loginWithToken(Data._tokenIdSaved); }, this._timeout); - } - // Signify we aren't logginging in any more after a few seconds - if (this._timeout > 2000) { + } else { + Meteor.isVerbose && + console.info( + 'User._handleLoginCallback::: not token login, ending logging in.' + ); User._endLoggingIn(); + Data.notify('onLoginFailure', err); } - User._endLoggingIn(); - // we delegate the error to enable better logging - Data.notify('onLoginFailure', err); } Data.notify('change'); }, @@ -192,11 +233,9 @@ const User = { } if (value !== null) { - this._isTokenLogin = true; + User._isTokenLogin = true; Meteor.isVerbose && console.info('User._loginWithToken::: token:', value); - if (this._isCallingLogin) { - return; - } + this._isCallingLogin = true; User._startLoggingIn(); Meteor.call('login', { resume: value }, (err, result) => { @@ -215,13 +254,30 @@ const User = { this._loadInitialUser(); }, time + 100); } else if (err?.error === 403) { + Meteor.isVerbose && + console.info( + 'User._handleLoginCallback::: token login failed, logging out.' + ); + Data.notify('onLoginFailure', err); + Data.notify('change'); User.logout(); } else { + Meteor.isVerbose && + console.info( + 'User._handleLoginCallback::: token login error and result:', + err, + result + ); User._handleLoginCallback(err, result); } }); } else { - Meteor.isVerbose && console.info('User._loginWithToken::: token is null'); + Meteor.isVerbose && + console.info( + 'User._loginWithToken::: token is null, ending logging in.' + ); + Data.notify('onLoginFailure', new Error("Token doesn't exist")); + Data.notify('change'); User._endLoggingIn(); } }, @@ -234,11 +290,22 @@ const User = { User._startLoggingIn(); var value = null; try { - value = await Data._options.AsyncStorage.getItem(TOKEN_KEY); - } catch (error) { - console.warn('AsyncStorage error: ' + error.message); - } finally { + value = await Data._options.AsyncTokenStorage.getItem(TOKEN_KEY).catch( + (error) => { + console.warn( + 'AsyncStorage error: ' + error.message + ' while loading token' + ); + } + ); + User._loginWithToken(value); + } catch (error) { + User._endLoggingIn(); + Data.notify('onLoginFailure', err); + Data.notify('change'); + console.warn( + 'AsyncStorage error: ' + error.message + ' while loading token' + ); } }, };