瀏覽代碼

Merge pull request #167 from threema-ch/issue-106

Fix / improve soft reconnect behavior

Refs #106
Danilo Bargen 8 年之前
父節點
當前提交
208b4c6058
共有 10 個文件被更改,包括 152 次插入69 次删除
  1. 11 1
      LICENSE-3RD-PARTY.txt
  2. 1 0
      gather-licenses.sh
  3. 15 10
      npm-shrinkwrap.json
  4. 1 0
      package.json
  5. 8 0
      src/app.ts
  6. 41 14
      src/controllers/status.ts
  7. 5 0
      src/partials/welcome.ts
  8. 17 8
      src/services/state.ts
  9. 48 34
      src/services/webclient.ts
  10. 5 2
      src/threema.d.ts

+ 11 - 1
LICENSE-3RD-PARTY.txt

@@ -707,7 +707,7 @@ License for saltyrtc-client
 
 The MIT License (MIT)
 
-Copyright (c) 2016 Threema GmbH
+Copyright (c) 2016-2017 Threema GmbH
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -759,6 +759,16 @@ SOFTWARE.
 
 
 
+----------
+License for ts-events
+----------
+
+Copyright (c) 2015, Rogier Schouten <github@workingcode.ninja>
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
+
 ----------
 License for tsify
 ----------

+ 1 - 0
gather-licenses.sh

@@ -29,6 +29,7 @@ LICENSE_FILES=(
     'node-sass' 'node_modules/node-sass/LICENSE'
     'saltyrtc-client' 'node_modules/saltyrtc-client/LICENSE.md'
     'saltyrtc-task-webrtc' 'node_modules/saltyrtc-task-webrtc/LICENSE.md'
+    'ts-events' 'node_modules/ts-events/LICENSE'
     'tsify' '.licenses/tsify'
     'tweetnacl' 'node_modules/tweetnacl/LICENSE'
     'typescript' 'node_modules/typescript/LICENSE.txt'

+ 15 - 10
npm-shrinkwrap.json

@@ -52,11 +52,6 @@
       "from": "@types/webrtc@>=0.0.21 <0.1.0",
       "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.21.tgz"
     },
-    "JSONStream": {
-      "version": "1.3.0",
-      "from": "JSONStream@>=1.0.3 <2.0.0",
-      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.0.tgz"
-    },
     "abbrev": {
       "version": "1.0.9",
       "from": "abbrev@>=1.0.0 <2.0.0",
@@ -2274,6 +2269,11 @@
       "from": "jsonpointer@>=4.0.0 <5.0.0",
       "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz"
     },
+    "JSONStream": {
+      "version": "1.3.0",
+      "from": "JSONStream@>=1.0.3 <2.0.0",
+      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.0.tgz"
+    },
     "jsprim": {
       "version": "1.3.1",
       "from": "jsprim@>=1.2.2 <2.0.0",
@@ -3741,16 +3741,16 @@
         }
       }
     },
-    "string-width": {
-      "version": "1.0.2",
-      "from": "string-width@>=1.0.1 <2.0.0",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
-    },
     "string_decoder": {
       "version": "0.10.31",
       "from": "string_decoder@>=0.10.0 <0.11.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
     },
+    "string-width": {
+      "version": "1.0.2",
+      "from": "string-width@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
+    },
     "stringstream": {
       "version": "0.0.5",
       "from": "stringstream@>=0.0.4 <0.1.0",
@@ -3925,6 +3925,11 @@
       "from": "trim-newlines@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz"
     },
+    "ts-events": {
+      "version": "3.1.5",
+      "from": "ts-events@latest",
+      "resolved": "https://registry.npmjs.org/ts-events/-/ts-events-3.1.5.tgz"
+    },
     "tsconfig": {
       "version": "5.0.3",
       "from": "tsconfig@>=5.0.3 <6.0.0",

+ 1 - 0
package.json

@@ -60,6 +60,7 @@
     "saltyrtc-client": "~0.9.1",
     "saltyrtc-task-webrtc": "~0.9.1",
     "sdp": "~1.3.0",
+    "ts-events": "^3.1.5",
     "tsify": "~2.0.1",
     "tweetnacl": "~0.14.4",
     "typescript": "~2.1.0",

+ 8 - 0
src/app.ts

@@ -17,6 +17,8 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {AsyncEvent} from 'ts-events';
+
 import config from './config';
 import './controllers';
 import './directives';
@@ -26,6 +28,12 @@ import './partials/welcome';
 import './services';
 import './threema/container';
 
+// Configure asynchronous events
+AsyncEvent.setScheduler(function(callback) {
+    // Replace the default setImmediate() call by a setTimeout(, 0) call
+    setTimeout(callback, 0);
+});
+
 // Create app module and set dependencies
 angular.module('3ema', [
     // Angular

+ 41 - 14
src/controllers/status.ts

@@ -28,6 +28,8 @@ import {WebClientService} from '../services/webclient';
  */
 export class StatusController {
 
+    private logTag: string = '[StatusController]';
+
     // State variable
     private state: threema.GlobalConnectionState = 'error';
 
@@ -110,7 +112,7 @@ export class StatusController {
                 }
                 break;
             default:
-                this.$log.error('Invalid state change: From', oldValue, 'to', newValue);
+                this.$log.error(this.logTag, 'Invalid state change: From', oldValue, 'to', newValue);
         }
     }
 
@@ -137,12 +139,19 @@ export class StatusController {
      * Go back to the welcome screen and try to reconnect using the same keys as right now.
      */
     private reconnect(): void {
-        this.$log.debug('Connection lost. Attempting to reconnect...');
+        this.$log.warn(this.logTag, 'Connection lost. Attempting to reconnect...');
 
         // Get original keys
         let originalKeyStore = this.webClientService.salty.keyStore;
         let originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;
 
+        // Timeout durations
+        const TIMEOUT1 = 20 * 1000; // Duration per step for first reconnect
+        const TIMEOUT2 = 20 * 1000; // Duration per step for second reconnect
+
+        // Reconnect state
+        let reconnectTry: 1 | 2 = 1;
+
         // Handler for failed reconnection attempts
         let reconnectionFailed = () => {
             // Collapse status bar
@@ -160,6 +169,20 @@ export class StatusController {
             });
         };
 
+        // Handlers for reconnecting timeout
+        const reconnect2Timeout = () => {
+            // Give up
+            this.$log.error(this.logTag, 'Reconnect timeout 2. Going back to initial loading screen...');
+            reconnectionFailed();
+        };
+        const reconnect1Timeout = () => {
+            // Could not connect so far.
+            this.$log.error(this.logTag, 'Reconnect timeout 1. Retrying...');
+            reconnectTry = 2;
+            this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
+            doSoftReconnect();
+        };
+
         // Function to soft-reconnect. Does not reset the loaded data.
         let doSoftReconnect = () => {
             const deleteStoredData = false;
@@ -176,25 +199,29 @@ export class StatusController {
                     this.collapseStatusBar();
                 },
                 (error) => {
-                    this.$log.error('Error state:', error);
+                    this.$log.error(this.logTag, 'Error state:', error);
                     this.$timeout.cancel(this.reconnectTimeout);
                     reconnectionFailed();
                 },
+                (progress: threema.ConnectionBuildupStateChange) => {
+                    if (progress.state === 'peer_handshake' || progress.state === 'loading') {
+                        this.$log.debug(this.logTag, 'Connection buildup advanced, resetting timeout');
+                        // Restart timeout
+                        this.$timeout.cancel(this.reconnectTimeout);
+                        if (reconnectTry === 1) {
+                            this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
+                        } else if (reconnectTry === 2) {
+                            this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
+                        } else {
+                            throw new Error('Invalid reconnectTry value: ' + reconnectTry);
+                        }
+                    }
+                },
             );
         };
 
         // Start timeout
-        this.reconnectTimeout = this.$timeout(() => {
-            // Could not connect so far.
-            // Retry once, but increase reconnect timeout to 40 seconds
-            this.$log.error('Reconnect timeout 1. Retrying for 40 seconds...');
-            this.reconnectTimeout = this.$timeout(() => {
-                // Give up
-                this.$log.error('Reconnect timeout 2. Going back to initial loading screen...');
-                reconnectionFailed();
-            }, 40000);
-            doSoftReconnect();
-        }, 20000);
+        this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
 
         // Start reconnecting process
         doSoftReconnect();

+ 5 - 0
src/partials/welcome.ts

@@ -390,6 +390,11 @@ class WelcomeController {
                 // TODO: should probably show an error message instead
                 this.$timeout(() => this.$state.reload(), WelcomeController.REDIRECT_DELAY);
             },
+
+            // State updates
+            (progress: threema.ConnectionBuildupStateChange) => {
+                // Do nothing
+            },
         );
     }
 

+ 17 - 8
src/services/state.ts

@@ -15,12 +15,19 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {AsyncEvent} from 'ts-events';
+
 export class StateService {
 
+    private logTag: string = '[StateService]';
+
     // Angular services
     private $log: ng.ILogService;
     private $interval: ng.IIntervalService;
 
+    // Events
+    public evtConnectionBuildupStateChange = new AsyncEvent<threema.ConnectionBuildupStateChange>();
+
     // WebRTC states
     public signalingConnectionState: saltyrtc.SignalingState;
     public rtcConnectionState: threema.RTCConnectionState;
@@ -50,7 +57,7 @@ export class StateService {
         const prevState = this.signalingConnectionState;
         this.signalingConnectionState = state;
         if (this.stage === 'signaling') {
-            this.$log.debug('[StateService] Signaling connection state:', prevState, '=>', state);
+            this.$log.debug(this.logTag, 'Signaling connection state:', prevState, '=>', state);
             switch (state) {
                 case 'new':
                 case 'ws-connecting':
@@ -67,10 +74,10 @@ export class StateService {
                     this.state = 'error';
                     break;
                 default:
-                    this.$log.warn('[StateService] Ignored signaling connection state change to', state);
+                    this.$log.warn(this.logTag, 'Ignored signaling connection state change to', state);
             }
         } else {
-            this.$log.debug('[StateService] Ignored signaling connection state to "' + state + '"');
+            this.$log.debug(this.logTag, 'Ignored signaling connection state to "' + state + '"');
         }
     }
 
@@ -81,7 +88,7 @@ export class StateService {
         const prevState = this.rtcConnectionState;
         this.rtcConnectionState = state;
         if (this.stage === 'rtc') {
-            this.$log.debug('[StateService] RTC connection state:', prevState, '=>', state);
+            this.$log.debug(this.logTag, 'RTC connection state:', prevState, '=>', state);
             switch (state) {
                 case 'new':
                 case 'connecting':
@@ -95,10 +102,10 @@ export class StateService {
                     this.state = 'error';
                     break;
                 default:
-                    this.$log.warn('[StateService] Ignored RTC connection state change to', state);
+                    this.$log.warn(this.logTag, 'Ignored RTC connection state change to', state);
             }
         } else {
-            this.$log.debug('[StateService] Ignored RTC connection state change to "' + state + '"');
+            this.$log.debug(this.logTag, 'Ignored RTC connection state change to "' + state + '"');
         }
     }
 
@@ -109,10 +116,12 @@ export class StateService {
         if (this.connectionBuildupState === state) {
             return;
         }
-        this.$log.debug('[StateService] Connection buildup state:', this.connectionBuildupState, '=>', state);
+        const prevState = this.connectionBuildupState;
+        this.$log.debug(this.logTag, 'Connection buildup state:', prevState, '=>', state);
 
         // Update state
         this.connectionBuildupState = state;
+        this.evtConnectionBuildupStateChange.post({state: state, prevState: prevState});
 
         // Cancel progress interval if present
         if (this.progressInterval !== null) {
@@ -188,7 +197,7 @@ export class StateService {
      * Reset all states.
      */
     public reset(): void {
-        this.$log.debug('[StateService] Reset');
+        this.$log.debug(this.logTag, 'Reset');
 
         // Reset state
         this.signalingConnectionState = 'new';

+ 48 - 34
src/services/webclient.ts

@@ -145,12 +145,12 @@ export class WebClientService {
     private receiverService: ReceiverService;
 
     // State handling
-    private startupPromise: ng.IDeferred<{}>; // TODO: deferred type
+    private startupPromise: ng.IDeferred<{}> = null; // TODO: deferred type
     private startupDone: boolean = false;
     private pendingInitializationStepRoutines: threema.InitializationStepRoutine[] = [];
     private initialized: Set<threema.InitializationStep> = new Set();
     private initializedThreshold = 3;
-    private state: StateService;
+    private stateService: StateService;
 
     // SaltyRTC
     private saltyRtcHost: string = null;
@@ -243,7 +243,7 @@ export class WebClientService {
         this.config = CONFIG;
 
         // State
-        this.state = stateService;
+        this.stateService = stateService;
 
         // Other properties
         this.container = container;
@@ -257,6 +257,15 @@ export class WebClientService {
 
         // Setup fields
         this._resetFields();
+
+        // Register event handlers
+        this.stateService.evtConnectionBuildupStateChange.attach(
+            (stateChange: threema.ConnectionBuildupStateChange) => {
+                if (this.startupPromise !== null) {
+                    this.startupPromise.notify(stateChange);
+                }
+            },
+        );
     }
 
     get me(): threema.MeReceiver {
@@ -295,7 +304,7 @@ export class WebClientService {
      */
     public init(keyStore?: saltyrtc.KeyStore, peerTrustedKey?: Uint8Array, resetFields = true): void {
         // Reset state
-        this.state.reset();
+        this.stateService.reset();
 
         // Create WebRTC task instance
         const maxPacketSize = this.browserService.getBrowser().firefox ? 16384 : 65536;
@@ -333,7 +342,7 @@ export class WebClientService {
         this.salty.on('new-responder', () => {
             if (!this.startupDone) {
                 // Peer handshake
-                this.state.updateConnectionBuildupState('peer_handshake');
+                this.stateService.updateConnectionBuildupState('peer_handshake');
             }
         });
 
@@ -347,16 +356,16 @@ export class WebClientService {
                         case 'new':
                         case 'ws-connecting':
                         case 'server-handshake':
-                            if (this.state.connectionBuildupState !== 'push'
-                                && this.state.connectionBuildupState !== 'manual_start') {
-                                this.state.updateConnectionBuildupState('connecting');
+                            if (this.stateService.connectionBuildupState !== 'push'
+                                && this.stateService.connectionBuildupState !== 'manual_start') {
+                                this.stateService.updateConnectionBuildupState('connecting');
                             }
                             break;
                         case 'peer-handshake':
                             // Waiting for peer
-                            if (this.state.connectionBuildupState !== 'push'
-                                && this.state.connectionBuildupState !== 'manual_start') {
-                                this.state.updateConnectionBuildupState('waiting');
+                            if (this.stateService.connectionBuildupState !== 'push'
+                                && this.stateService.connectionBuildupState !== 'manual_start') {
+                                this.stateService.updateConnectionBuildupState('waiting');
                             }
                             break;
                         case 'task':
@@ -364,13 +373,13 @@ export class WebClientService {
                             break;
                         case 'closing':
                         case 'closed':
-                            this.state.updateConnectionBuildupState('closed');
+                            this.stateService.updateConnectionBuildupState('closed');
                             break;
                         default:
                             this.$log.warn('Unknown signaling state:', state);
                     }
                 }
-                this.state.updateSignalingConnectionState(state);
+                this.stateService.updateSignalingConnectionState(state);
             }, 0);
         });
 
@@ -389,12 +398,12 @@ export class WebClientService {
 
             // On state changes in the PeerConnectionHelper class, let state service know about it
             this.pcHelper.onConnectionStateChange = (state: threema.RTCConnectionState) => {
-                if (state === 'connected' && this.state.wasConnected) {
+                if (state === 'connected' && this.stateService.wasConnected) {
                     // this happens if a lost connection could be restored
                     // without reset the peer connection
                     this._requestInitialData();
                 }
-                this.state.updateRtcConnectionState(state);
+                this.stateService.updateRtcConnectionState(state);
             };
 
             // Initiate handover
@@ -436,7 +445,7 @@ export class WebClientService {
                     this._requestInitialData();
 
                     // Notify state service about data loading
-                    this.state.updateConnectionBuildupState('loading');
+                    this.stateService.updateConnectionBuildupState('loading');
                 },
             );
 
@@ -505,12 +514,12 @@ export class WebClientService {
                 .then(() => {
                     this.$log.debug('Requested app wakeup');
                     this.$rootScope.$apply(() => {
-                        this.state.updateConnectionBuildupState('push');
+                        this.stateService.updateConnectionBuildupState('push');
                     });
                 });
         } else if (this.trustedKeyStore.hasTrustedKey()) {
             this.$log.debug('Push service not available');
-            this.state.updateConnectionBuildupState('manual_start');
+            this.stateService.updateConnectionBuildupState('manual_start');
         }
 
         return this.startupPromise.promise;
@@ -534,12 +543,12 @@ export class WebClientService {
                 redirect: boolean = false): void {
         this.$log.info('Disconnecting...');
 
-        if (requestedByUs && this.state.rtcConnectionState === 'connected') {
+        if (requestedByUs && this.stateService.rtcConnectionState === 'connected') {
             // Ask peer to disconnect too
             this.salty.sendApplicationMessage({type: 'disconnect', forget: deleteStoredData});
         }
 
-        this.state.reset();
+        this.stateService.reset();
 
         // Reset the unread count
         this.resetUnreadCount();
@@ -608,13 +617,16 @@ export class WebClientService {
         }
     }
 
-    // Mark a component as initialized
+    /**
+     * Mark a component as initialized
+     */
     public registerInitializationStep(name: threema.InitializationStep) {
         if (this.initialized.has(name) ) {
             this.$log.warn(this.logTag, 'initialization step', name, 'already registered');
             return;
         }
 
+        this.$log.debug(this.logTag, 'Initialization step', name, 'done');
         this.initialized.add(name);
 
         // check pending routines
@@ -634,9 +646,11 @@ export class WebClientService {
         });
 
         if (this.initialized.size >= this.initializedThreshold) {
-            this.state.updateConnectionBuildupState('done');
+            this.stateService.updateConnectionBuildupState('done');
             this.startupPromise.resolve();
+            this.startupPromise = null;
             this.startupDone = true;
+            this._resetInitializationSteps();
         }
     }
 
@@ -1234,13 +1248,20 @@ export class WebClientService {
     }
 
     /**
-     * Reset data fields.
+     * Reset data related to initialization.
      */
-    private _resetFields(): void {
-        // clear initialized steps
+    private _resetInitializationSteps(): void {
+        this.$log.debug(this.logTag, 'Reset initialization steps');
         this.initialized.clear();
-        // clear step routines
         this.pendingInitializationStepRoutines = [];
+    }
+
+    /**
+     * Reset data fields.
+     */
+    private _resetFields(): void {
+        // Reset initialization data
+        this._resetInitializationSteps();
 
         // Create container instances
         this.receivers = this.container.createReceivers();
@@ -1256,14 +1277,6 @@ export class WebClientService {
         this.conversations.setFilter(this.container.Filters.hasData(this.receivers));
     }
 
-    private _resetUI(): void {
-        this.$log.debug('UI Reset');
-        // Only reset if currently in the messenger state
-        if (this.$state.includes('messenger')) {
-            this.$state.go(this.$state.current, this.$state.params, {reload: true});
-        }
-    }
-
     private _requestInitialData(): void {
         // if all conversations are reloaded, clear the message cache
         // to get in sync (we dont know if a message was removed, updated etc..)
@@ -2371,4 +2384,5 @@ export class WebClientService {
     private resetUnreadCount(): void {
         this.titleService.updateUnreadCount(0);
     }
+
 }

+ 5 - 2
src/threema.d.ts

@@ -15,8 +15,6 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
-// Types used for the Threema Webclient.
-
 declare const angular: ng.IAngularStatic;
 
 declare namespace threema {
@@ -248,6 +246,11 @@ declare namespace threema {
     type ConnectionBuildupState = 'new' | 'connecting' | 'push' | 'manual_start' | 'waiting'
         | 'peer_handshake' | 'loading' | 'done' | 'closed';
 
+    interface ConnectionBuildupStateChange {
+        state: ConnectionBuildupState;
+        prevState: ConnectionBuildupState;
+    }
+
     /**
      * Connection state of the WebRTC peer connection.
      */