Sfoglia il codice sorgente

Implement chunk caching and explicit acknowledgement requests

Add explicit marking of messages that should be retransmitted after connection loss
Add explicit ack requests in case the chunk cache exceeds 2 MiB
Add processing of ack updates
Add a size method to the chunk cache
Remove messageSerial (we now simply use the current chunk sequence number)
Remove sending of the outgoingMessageQueue (WIP: needs to be removed entirely)
Lennart Grahl 7 anni fa
parent
commit
6104b285b1
3 ha cambiato i file con 159 aggiunte e 80 eliminazioni
  1. 8 2
      src/partials/messenger.ts
  2. 13 3
      src/protocol/cache.ts
  3. 138 75
      src/services/webclient.ts

+ 8 - 2
src/partials/messenger.ts

@@ -585,6 +585,9 @@ class ConversationController {
                         `,
                         `,
                         // tslint:enable:max-line-length
                         // tslint:enable:max-line-length
                     }).then((data) => {
                     }).then((data) => {
+                        // TODO: This should probably be moved into the
+                        //       WebClientService as a specific method for the
+                        //       type.
                         const caption = data.caption;
                         const caption = data.caption;
                         const sendAsFile = data.sendAsFile;
                         const sendAsFile = data.sendAsFile;
                         contents.forEach((msg: threema.FileMessageData, index: number) => {
                         contents.forEach((msg: threema.FileMessageData, index: number) => {
@@ -592,7 +595,7 @@ class ConversationController {
                                 msg.caption = caption;
                                 msg.caption = caption;
                             }
                             }
                             msg.sendAsFile = sendAsFile;
                             msg.sendAsFile = sendAsFile;
-                            this.webClientService.sendMessage(this.$stateParams, type, msg)
+                            this.webClientService.sendMessage(this.$stateParams, type, true, msg)
                                 .then(() => {
                                 .then(() => {
                                     nextCallback(index);
                                     nextCallback(index);
                                 })
                                 })
@@ -612,7 +615,10 @@ class ConversationController {
                         // remove quote
                         // remove quote
                         this.webClientService.setQuote(this.receiver);
                         this.webClientService.setQuote(this.receiver);
                         // send message
                         // send message
-                        this.webClientService.sendMessage(this.$stateParams, type, msg)
+                        // TODO: This should probably be moved into the
+                        //       WebClientService as a specific method for the
+                        //       type.
+                        this.webClientService.sendMessage(this.$stateParams, type, true, msg)
                             .then(() => {
                             .then(() => {
                                 nextCallback(index);
                                 nextCallback(index);
                             })
                             })

+ 13 - 3
src/protocol/cache.ts

@@ -23,6 +23,7 @@ export type CachedChunk = Uint8Array | null;
 export class ChunkCache {
 export class ChunkCache {
     private readonly sequenceNumberMax: number;
     private readonly sequenceNumberMax: number;
     private _sequenceNumber = 0;
     private _sequenceNumber = 0;
+    private _size = 0;
     private cache: CachedChunk[] = [];
     private cache: CachedChunk[] = [];
 
 
     constructor(sequenceNumberMax: number) {
     constructor(sequenceNumberMax: number) {
@@ -36,6 +37,13 @@ export class ChunkCache {
         return this._sequenceNumber;
         return this._sequenceNumber;
     }
     }
 
 
+    /**
+     * Get the size of currently stored chunks.
+     */
+    public get size(): number {
+        return this._size;
+    }
+
     /**
     /**
      * Get the currently cached chunks.
      * Get the currently cached chunks.
      */
      */
@@ -47,7 +55,7 @@ export class ChunkCache {
      * Transfer an array of cached chunks to this cache instance.
      * Transfer an array of cached chunks to this cache instance.
      */
      */
     public transfer(cache: CachedChunk[]): void {
     public transfer(cache: CachedChunk[]): void {
-        // Add chunks but remove all which are blacklisted
+        // Add chunks but remove all which should not be retransmitted
         for (const chunk of cache) {
         for (const chunk of cache) {
             if (chunk !== null) {
             if (chunk !== null) {
                 this.append(chunk);
                 this.append(chunk);
@@ -64,8 +72,9 @@ export class ChunkCache {
             throw Error('Sequence number overflow');
             throw Error('Sequence number overflow');
         }
         }
 
 
-        // Update sequence number & append chunk
+        // Update sequence number, update size & append chunk
         ++this._sequenceNumber;
         ++this._sequenceNumber;
+        this._size += chunk.byteLength;
         this.cache.push(chunk);
         this.cache.push(chunk);
     }
     }
 
 
@@ -86,7 +95,8 @@ export class ChunkCache {
             throw new Error('Remote travelled back in time and acknowledged a chunk it has already acknowledged');
             throw new Error('Remote travelled back in time and acknowledged a chunk it has already acknowledged');
         }
         }
 
 
-        // Slice our cache
+        // Slice our cache & recalculate size
         this.cache = endOffset === 0 ? [] : this.cache.slice(endOffset);
         this.cache = endOffset === 0 ? [] : this.cache.slice(endOffset);
+        this._size = this.cache.reduce((sum, chunk) => sum + chunk.byteLength, 0);
     }
     }
 }
 }

+ 138 - 75
src/services/webclient.ts

@@ -51,6 +51,7 @@ import DisconnectReason = threema.DisconnectReason;
 export class WebClientService {
 export class WebClientService {
     private static CHUNK_SIZE = 64 * 1024;
     private static CHUNK_SIZE = 64 * 1024;
     private static SEQUENCE_NUMBER_MAX = (2 ** 32) - 1;
     private static SEQUENCE_NUMBER_MAX = (2 ** 32) - 1;
+    private static CHUNK_CACHE_SIZE_MAX = 2 * 1024 * 1024;
     private static AVATAR_LOW_MAX_SIZE = 48;
     private static AVATAR_LOW_MAX_SIZE = 48;
     private static MAX_TEXT_LENGTH = 3500;
     private static MAX_TEXT_LENGTH = 3500;
     private static MAX_FILE_SIZE_WEBRTC = 15 * 1024 * 1024;
     private static MAX_FILE_SIZE_WEBRTC = 15 * 1024 * 1024;
@@ -167,13 +168,13 @@ export class WebClientService {
     private relayedDataTask: saltyrtc.tasks.relayed_data.RelayedDataTask = null;
     private relayedDataTask: saltyrtc.tasks.relayed_data.RelayedDataTask = null;
     private secureDataChannel: saltyrtc.tasks.webrtc.SecureDataChannel = null;
     private secureDataChannel: saltyrtc.tasks.webrtc.SecureDataChannel = null;
     public chosenTask: threema.ChosenTask = threema.ChosenTask.None;
     public chosenTask: threema.ChosenTask = threema.ChosenTask.None;
+    private pendingAckRequest: number | null = null;
     private previousConnectionId: Uint8Array = null;
     private previousConnectionId: Uint8Array = null;
     private currentConnectionId: Uint8Array = null;
     private currentConnectionId: Uint8Array = null;
     private previousChunkCache: ChunkCache = null;
     private previousChunkCache: ChunkCache = null;
     private currentChunkCache: ChunkCache = null;
     private currentChunkCache: ChunkCache = null;
 
 
     // Message chunking
     // Message chunking
-    private messageSerial = 0;
     private unchunker: chunkedDc.Unchunker = null;
     private unchunker: chunkedDc.Unchunker = null;
 
 
     // Messenger data
     // Messenger data
@@ -394,6 +395,13 @@ export class WebClientService {
             this.$log.debug('Auth token:', this.salty.authTokenHex);
             this.$log.debug('Auth token:', this.salty.authTokenHex);
         }
         }
 
 
+        // Reset ack request
+        this.pendingAckRequest = null;
+
+        // Create unchunker
+        this.unchunker = new chunkedDc.Unchunker();
+        this.unchunker.onMessage = this.handleIncomingMessageBytes.bind(this);
+
         // Create chunk cache
         // Create chunk cache
         this.currentChunkCache = new ChunkCache(WebClientService.SEQUENCE_NUMBER_MAX);
         this.currentChunkCache = new ChunkCache(WebClientService.SEQUENCE_NUMBER_MAX);
 
 
@@ -593,7 +601,7 @@ export class WebClientService {
             return;
             return;
         }
         }
 
 
-        // Transfer the cache (filters blacklisted chunks)
+        // Transfer the cache (filters chunks which should not be retransmitted)
         this.currentChunkCache.transfer(this.previousChunkCache.chunks);
         this.currentChunkCache.transfer(this.previousChunkCache.chunks);
 
 
         // Resend chunks
         // Resend chunks
@@ -695,19 +703,6 @@ export class WebClientService {
 
 
         // Notify state service about data loading
         // Notify state service about data loading
         this.stateService.updateConnectionBuildupState('loading');
         this.stateService.updateConnectionBuildupState('loading');
-
-        // Process pending messages
-        if (this.outgoingMessageQueue.length > 0) {
-            this.$log.debug(this.logTag, 'Sending', this.outgoingMessageQueue.length, 'pending messages');
-            while (true) {
-                const msg = this.outgoingMessageQueue.shift();
-                if (msg === undefined) {
-                    break;
-                } else {
-                    this.send(msg);
-                }
-            }
-        }
     }
     }
 
 
     /**
     /**
@@ -757,13 +752,17 @@ export class WebClientService {
                 this.$log.warn('Secure data channel: Closed');
                 this.$log.warn('Secure data channel: Closed');
             };
             };
         } else if (this.chosenTask === threema.ChosenTask.RelayedData) {
         } else if (this.chosenTask === threema.ChosenTask.RelayedData) {
+            // Note: This ensures that the 'data' event cannot leak into
+            //       unchunker instances of subsequent connections.
+            const unchunker = this.unchunker;
+
             // Handle messages directly
             // Handle messages directly
             this.relayedDataTask.on('data', (ev: saltyrtc.SaltyRTCEvent) => {
             this.relayedDataTask.on('data', (ev: saltyrtc.SaltyRTCEvent) => {
                 const chunk = new Uint8Array(ev.data);
                 const chunk = new Uint8Array(ev.data);
                 if (this.config.MSG_DEBUGGING && this.config.DEBUG) {
                 if (this.config.MSG_DEBUGGING && this.config.DEBUG) {
                     this.$log.debug('[Chunk] Received chunk:', chunk);
                     this.$log.debug('[Chunk] Received chunk:', chunk);
                 }
                 }
-                this.unchunker.add(chunk.buffer);
+                unchunker.add(chunk.buffer);
             });
             });
 
 
             // The communication channel is now open! Fetch initial data
             // The communication channel is now open! Fetch initial data
@@ -893,7 +892,7 @@ export class WebClientService {
 
 
         // Send disconnect reason to the remote peer if requested
         // Send disconnect reason to the remote peer if requested
         if (send && this.stateService.state === threema.GlobalConnectionState.Ok) {
         if (send && this.stateService.state === threema.GlobalConnectionState.Ok) {
-            this._sendUpdate(WebClientService.SUB_TYPE_CONNECTION_DISCONNECT, undefined, {reason: reason});
+            this._sendUpdate(WebClientService.SUB_TYPE_CONNECTION_DISCONNECT, false, undefined, {reason: reason});
         }
         }
 
 
         // Reset states
         // Reset states
@@ -1031,7 +1030,14 @@ export class WebClientService {
                 sequenceNumber: sequenceNumber,
                 sequenceNumber: sequenceNumber,
             };
             };
         }
         }
-        this._sendUpdate(WebClientService.SUB_TYPE_CONNECTION_INFO, args, data);
+        this._sendUpdate(WebClientService.SUB_TYPE_CONNECTION_INFO, false, args, data);
+    }
+
+    /**
+     * Request a connection ack update.
+     */
+    private _requestConnectionAck(): void {
+        this._sendRequest(WebClientService.SUB_TYPE_CONNECTION_ACK, false);
     }
     }
 
 
     /**
     /**
@@ -1049,7 +1055,7 @@ export class WebClientService {
         if (browser.version) {
         if (browser.version) {
             data[WebClientService.ARGUMENT_BROWSER_VERSION] = browser.version;
             data[WebClientService.ARGUMENT_BROWSER_VERSION] = browser.version;
         }
         }
-        this._sendRequest(WebClientService.SUB_TYPE_CLIENT_INFO, undefined, data);
+        this._sendRequest(WebClientService.SUB_TYPE_CLIENT_INFO, true, undefined, data);
     }
     }
 
 
     /**
     /**
@@ -1057,7 +1063,7 @@ export class WebClientService {
      */
      */
     public requestReceivers(): void {
     public requestReceivers(): void {
         this.$log.debug('Sending receivers request');
         this.$log.debug('Sending receivers request');
-        this._sendRequest(WebClientService.SUB_TYPE_RECEIVERS);
+        this._sendRequest(WebClientService.SUB_TYPE_RECEIVERS, true);
     }
     }
 
 
     /**
     /**
@@ -1065,7 +1071,7 @@ export class WebClientService {
      */
      */
     public requestConversations(): void {
     public requestConversations(): void {
         this.$log.debug('Sending conversation request');
         this.$log.debug('Sending conversation request');
-        this._sendRequest(WebClientService.SUB_TYPE_CONVERSATIONS, {
+        this._sendRequest(WebClientService.SUB_TYPE_CONVERSATIONS, true, {
             [WebClientService.ARGUMENT_MAX_SIZE]: WebClientService.AVATAR_LOW_MAX_SIZE,
             [WebClientService.ARGUMENT_MAX_SIZE]: WebClientService.AVATAR_LOW_MAX_SIZE,
         });
         });
     }
     }
@@ -1075,7 +1081,7 @@ export class WebClientService {
      */
      */
     public requestBatteryStatus(): void {
     public requestBatteryStatus(): void {
         this.$log.debug('Sending battery status request');
         this.$log.debug('Sending battery status request');
-        this._sendRequest(WebClientService.SUB_TYPE_BATTERY_STATUS);
+        this._sendRequest(WebClientService.SUB_TYPE_BATTERY_STATUS, true);
     }
     }
 
 
     /**
     /**
@@ -1083,7 +1089,7 @@ export class WebClientService {
      */
      */
     public requestProfile(): void {
     public requestProfile(): void {
         this.$log.debug('Sending profile request');
         this.$log.debug('Sending profile request');
-        this._sendRequest(WebClientService.SUB_TYPE_PROFILE);
+        this._sendRequest(WebClientService.SUB_TYPE_PROFILE, true);
     }
     }
 
 
     /**
     /**
@@ -1132,7 +1138,7 @@ export class WebClientService {
         this.$log.debug('Sending message request for', receiver.type, receiver.id,
         this.$log.debug('Sending message request for', receiver.type, receiver.id,
             'with message id', msgId);
             'with message id', msgId);
 
 
-        this._sendRequest(WebClientService.SUB_TYPE_MESSAGES, args);
+        this._sendRequest(WebClientService.SUB_TYPE_MESSAGES, true, args);
 
 
         return refMsgId;
         return refMsgId;
     }
     }
@@ -1167,7 +1173,7 @@ export class WebClientService {
         }
         }
 
 
         this.$log.debug('Sending', resolution, 'res avatar request for', receiver.type, receiver.id);
         this.$log.debug('Sending', resolution, 'res avatar request for', receiver.type, receiver.id);
-        return this._sendRequestPromise(WebClientService.SUB_TYPE_AVATAR, args, 10000);
+        return this._sendRequestPromise(WebClientService.SUB_TYPE_AVATAR, true, args, 10000);
     }
     }
 
 
     /**
     /**
@@ -1189,7 +1195,7 @@ export class WebClientService {
         };
         };
 
 
         this.$log.debug('Sending', 'thumbnail request for', receiver.type, message.id);
         this.$log.debug('Sending', 'thumbnail request for', receiver.type, message.id);
-        return this._sendRequestPromise(WebClientService.SUB_TYPE_THUMBNAIL, args, 10000);
+        return this._sendRequestPromise(WebClientService.SUB_TYPE_THUMBNAIL, true, args, 10000);
     }
     }
 
 
     /**
     /**
@@ -1211,7 +1217,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_MESSAGE_ID]: msgId,
             [WebClientService.ARGUMENT_MESSAGE_ID]: msgId,
         };
         };
         this.$log.debug('Sending blob request for message', msgId);
         this.$log.debug('Sending blob request for message', msgId);
-        return this._sendRequestPromise(WebClientService.SUB_TYPE_BLOB, args);
+        return this._sendRequestPromise(WebClientService.SUB_TYPE_BLOB, true, args);
     }
     }
 
 
     /**
     /**
@@ -1235,11 +1241,11 @@ export class WebClientService {
             [WebClientService.ARGUMENT_MESSAGE_ID]: newestMessage.id.toString(),
             [WebClientService.ARGUMENT_MESSAGE_ID]: newestMessage.id.toString(),
         };
         };
         this.$log.debug('Sending read request for', receiver.type, receiver.id, '(msg ' + newestMessage.id + ')');
         this.$log.debug('Sending read request for', receiver.type, receiver.id, '(msg ' + newestMessage.id + ')');
-        this._sendRequest(WebClientService.SUB_TYPE_READ, args);
+        this._sendRequest(WebClientService.SUB_TYPE_READ, true, args);
     }
     }
 
 
     public requestContactDetail(contactReceiver: threema.ContactReceiver): Promise<any> {
     public requestContactDetail(contactReceiver: threema.ContactReceiver): Promise<any> {
-        return this._sendRequestPromise(WebClientService.SUB_TYPE_CONTACT_DETAIL, {
+        return this._sendRequestPromise(WebClientService.SUB_TYPE_CONTACT_DETAIL, true, {
             [WebClientService.ARGUMENT_IDENTITY]: contactReceiver.id,
             [WebClientService.ARGUMENT_IDENTITY]: contactReceiver.id,
         });
         });
     }
     }
@@ -1247,9 +1253,12 @@ export class WebClientService {
     /**
     /**
      * Send a message to the specified receiver.
      * Send a message to the specified receiver.
      */
      */
-    public sendMessage(receiver,
-                       type: threema.MessageContentType,
-                       message: threema.MessageData): Promise<Promise<any>> {
+    public sendMessage(
+        receiver,
+        type: threema.MessageContentType,
+        retransmit: boolean,
+        message: threema.MessageData,
+    ): Promise<Promise<any>> {
         return new Promise<any> (
         return new Promise<any> (
             (resolve, reject) => {
             (resolve, reject) => {
                 // Try to load receiver object
                 // Try to load receiver object
@@ -1372,7 +1381,7 @@ export class WebClientService {
                 };
                 };
 
 
                 // Send message and handling error promise
                 // Send message and handling error promise
-                this._sendCreatePromise(subType, args, message).catch((error) => {
+                this._sendCreatePromise(subType, retransmit, args, message).catch((error) => {
                     this.$log.error('Error sending message:', error);
                     this.$log.error('Error sending message:', error);
 
 
                     // Remove temporary message
                     // Remove temporary message
@@ -1420,7 +1429,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
             [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
             [WebClientService.ARGUMENT_MESSAGE_ACKNOWLEDGED]: acknowledged,
             [WebClientService.ARGUMENT_MESSAGE_ACKNOWLEDGED]: acknowledged,
         };
         };
-        this._sendRequest(WebClientService.SUB_TYPE_ACK, args);
+        this._sendRequest(WebClientService.SUB_TYPE_ACK, true, args);
     }
     }
 
 
     /**
     /**
@@ -1437,17 +1446,17 @@ export class WebClientService {
             [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
             [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
             [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
             [WebClientService.ARGUMENT_MESSAGE_ID]: message.id.toString(),
         };
         };
-        this._sendDeletePromise(WebClientService.SUB_TYPE_MESSAGE, args);
+        this._sendDeletePromise(WebClientService.SUB_TYPE_MESSAGE, true, args);
     }
     }
 
 
     public sendMeIsTyping(receiver: threema.ContactReceiver, isTyping: boolean): void {
     public sendMeIsTyping(receiver: threema.ContactReceiver, isTyping: boolean): void {
         const args = {[WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id};
         const args = {[WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id};
         const data = {[WebClientService.ARGUMENT_IS_TYPING]: isTyping};
         const data = {[WebClientService.ARGUMENT_IS_TYPING]: isTyping};
-        this._sendUpdate(WebClientService.SUB_TYPE_TYPING, args, data);
+        this._sendUpdate(WebClientService.SUB_TYPE_TYPING, false, args, data);
     }
     }
 
 
     public sendKeyPersisted(): void {
     public sendKeyPersisted(): void {
-        this._sendRequest(WebClientService.SUB_TYPE_KEY_PERSISTED);
+        this._sendRequest(WebClientService.SUB_TYPE_KEY_PERSISTED, true);
     }
     }
 
 
     /**
     /**
@@ -1458,7 +1467,7 @@ export class WebClientService {
         const data = {
         const data = {
             [WebClientService.ARGUMENT_IDENTITY]: threemaId,
             [WebClientService.ARGUMENT_IDENTITY]: threemaId,
         };
         };
-        return this._sendCreatePromise(WebClientService.SUB_TYPE_CONTACT, args, data);
+        return this._sendCreatePromise(WebClientService.SUB_TYPE_CONTACT, true, args, data);
     }
     }
 
 
     /**
     /**
@@ -1493,7 +1502,7 @@ export class WebClientService {
         const args = {
         const args = {
             [WebClientService.ARGUMENT_IDENTITY]: threemaId,
             [WebClientService.ARGUMENT_IDENTITY]: threemaId,
         };
         };
-        const promise = this._sendUpdatePromise(WebClientService.SUB_TYPE_CONTACT, args, data);
+        const promise = this._sendUpdatePromise(WebClientService.SUB_TYPE_CONTACT, true, args, data);
 
 
         // If necessary, force an avatar reload
         // If necessary, force an avatar reload
         if (avatar !== undefined) {
         if (avatar !== undefined) {
@@ -1522,7 +1531,7 @@ export class WebClientService {
             data[WebClientService.ARGUMENT_AVATAR] = avatar;
             data[WebClientService.ARGUMENT_AVATAR] = avatar;
         }
         }
 
 
-        return this._sendCreatePromise(WebClientService.SUB_TYPE_GROUP, args, data);
+        return this._sendCreatePromise(WebClientService.SUB_TYPE_GROUP, true, args, data);
     }
     }
 
 
     /**
     /**
@@ -1547,7 +1556,7 @@ export class WebClientService {
         const args = {
         const args = {
             [WebClientService.ARGUMENT_RECEIVER_ID]: id,
             [WebClientService.ARGUMENT_RECEIVER_ID]: id,
         };
         };
-        const promise = this._sendUpdatePromise(WebClientService.SUB_TYPE_GROUP, args, data);
+        const promise = this._sendUpdatePromise(WebClientService.SUB_TYPE_GROUP, true, args, data);
 
 
         // If necessary, reset avatar to force a avatar reload
         // If necessary, reset avatar to force a avatar reload
         if (avatar !== undefined) {
         if (avatar !== undefined) {
@@ -1566,7 +1575,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_DELETE_TYPE]: WebClientService.DELETE_GROUP_TYPE_LEAVE,
             [WebClientService.ARGUMENT_DELETE_TYPE]: WebClientService.DELETE_GROUP_TYPE_LEAVE,
         };
         };
 
 
-        return this._sendDeletePromise(WebClientService.SUB_TYPE_GROUP, args);
+        return this._sendDeletePromise(WebClientService.SUB_TYPE_GROUP, true, args);
     }
     }
 
 
     public deleteGroup(group: threema.GroupReceiver): Promise<any> {
     public deleteGroup(group: threema.GroupReceiver): Promise<any> {
@@ -1582,7 +1591,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_DELETE_TYPE]: WebClientService.DELETE_GROUP_TYPE_DELETE,
             [WebClientService.ARGUMENT_DELETE_TYPE]: WebClientService.DELETE_GROUP_TYPE_DELETE,
         };
         };
 
 
-        return this._sendDeletePromise(WebClientService.SUB_TYPE_GROUP, args);
+        return this._sendDeletePromise(WebClientService.SUB_TYPE_GROUP, true, args);
     }
     }
 
 
     /**
     /**
@@ -1597,7 +1606,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_RECEIVER_ID]: group.id,
             [WebClientService.ARGUMENT_RECEIVER_ID]: group.id,
         };
         };
 
 
-        return this._sendRequestPromise(WebClientService.SUB_TYPE_GROUP_SYNC, args, 10000);
+        return this._sendRequestPromise(WebClientService.SUB_TYPE_GROUP_SYNC, true, args, 10000);
     }
     }
 
 
     /**
     /**
@@ -1613,7 +1622,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_NAME]: name,
             [WebClientService.ARGUMENT_NAME]: name,
         };
         };
 
 
-        return this._sendCreatePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, args, data);
+        return this._sendCreatePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, true, args, data);
     }
     }
 
 
     public modifyDistributionList(id: string,
     public modifyDistributionList(id: string,
@@ -1624,7 +1633,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_NAME]: name,
             [WebClientService.ARGUMENT_NAME]: name,
         } as any;
         } as any;
 
 
-        return this._sendUpdatePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, {
+        return this._sendUpdatePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, true, {
             [WebClientService.ARGUMENT_RECEIVER_ID]: id,
             [WebClientService.ARGUMENT_RECEIVER_ID]: id,
         }, data);
         }, data);
     }
     }
@@ -1638,7 +1647,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_RECEIVER_ID]: distributionList.id,
             [WebClientService.ARGUMENT_RECEIVER_ID]: distributionList.id,
         };
         };
 
 
-        return this._sendDeletePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, args);
+        return this._sendDeletePromise(WebClientService.SUB_TYPE_DISTRIBUTION_LIST, true, args);
     }
     }
 
 
     /**
     /**
@@ -1656,7 +1665,7 @@ export class WebClientService {
             [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
             [WebClientService.ARGUMENT_RECEIVER_ID]: receiver.id,
         };
         };
 
 
-        return this._sendDeletePromise(WebClientService.SUB_TYPE_CLEAN_RECEIVER_CONVERSATION, args);
+        return this._sendDeletePromise(WebClientService.SUB_TYPE_CLEAN_RECEIVER_CONVERSATION, true, args);
     }
     }
 
 
     /**
     /**
@@ -1681,7 +1690,7 @@ export class WebClientService {
         }
         }
 
 
         // Send update, get back promise
         // Send update, get back promise
-        return this._sendUpdatePromise(WebClientService.SUB_TYPE_PROFILE, null, data);
+        return this._sendUpdatePromise(WebClientService.SUB_TYPE_PROFILE, true, null, data);
     }
     }
 
 
     /**
     /**
@@ -1758,10 +1767,6 @@ export class WebClientService {
         // Reset initialization data
         // Reset initialization data
         this._resetInitializationSteps();
         this._resetInitializationSteps();
 
 
-        // Initialize unchunker
-        this.unchunker = new chunkedDc.Unchunker();
-        this.unchunker.onMessage = this.handleIncomingMessageBytes.bind(this);
-
         // Create container instances
         // Create container instances
         this.receivers = this.container.createReceivers();
         this.receivers = this.container.createReceivers();
         this.conversations = this.container.createConversations();
         this.conversations = this.container.createConversations();
@@ -1877,6 +1882,37 @@ export class WebClientService {
 
 
     }
     }
 
 
+    /**
+     * A connectionAck message arrived.
+     */
+    private _receiveConnectionAck(message: threema.WireMessage) {
+        if (!hasValue(message.data)) {
+            this.$log.warn(this.logTag, 'Invalid connectionAck message: data missing');
+            return;
+        }
+        if (!hasValue(message.data.sequenceNumber)) {
+            this.$log.warn(this.logTag, 'Invalid connectionAck message: sequenceNumber missing');
+            return;
+        }
+        const sequenceNumber = message.data.sequenceNumber;
+
+        // Acknowledge chunks
+        const size = this.currentChunkCache.size;
+        try {
+            this.currentChunkCache.acknowledge(sequenceNumber);
+        } catch (error) {
+            this.$log.error(this.logTag, error);
+            this.failSession();
+            return;
+        }
+        this.$log.debug(`Chunk cache size ${size} -> ${this.currentChunkCache.size}`);
+
+        // Clear pending ack requests
+        if (this.pendingAckRequest !== null && sequenceNumber >= this.pendingAckRequest) {
+            this.pendingAckRequest = null;
+        }
+    }
+
     /**
     /**
      * A connectionDisconnect message arrived.
      * A connectionDisconnect message arrived.
      */
      */
@@ -2897,7 +2933,7 @@ export class WebClientService {
         this.pushTokenType = tokenType;
         this.pushTokenType = tokenType;
     }
     }
 
 
-    private _sendRequest(type, args?: object, data?: object): void {
+    private _sendRequest(type, retransmit: boolean, args?: object, data?: object): void {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_REQUEST,
             type: WebClientService.TYPE_REQUEST,
             subType: type,
             subType: type,
@@ -2908,10 +2944,10 @@ export class WebClientService {
         if (data !== undefined) {
         if (data !== undefined) {
             message.data = data;
             message.data = data;
         }
         }
-        this.send(message);
+        this.send(message, retransmit);
     }
     }
 
 
-    private _sendUpdate(type, args?: object, data?: object): void {
+    private _sendUpdate(type, retransmit: boolean, args?: object, data?: object): void {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_UPDATE,
             type: WebClientService.TYPE_UPDATE,
             subType: type,
             subType: type,
@@ -2922,10 +2958,10 @@ export class WebClientService {
         if (data !== undefined) {
         if (data !== undefined) {
             message.data = data;
             message.data = data;
         }
         }
-        this.send(message);
+        this.send(message, retransmit);
     }
     }
 
 
-    private _sendPromiseMessage(message: threema.WireMessage, timeout: number = null): Promise<any> {
+    private _sendPromiseMessage(message: threema.WireMessage, retransmit: boolean, timeout: number = null): Promise<any> {
         // Create arguments on wired message
         // Create arguments on wired message
         if (message.args === undefined || message.args === null) {
         if (message.args === undefined || message.args === null) {
             message.args = {};
             message.args = {};
@@ -2952,7 +2988,7 @@ export class WebClientService {
                     }, timeout);
                     }, timeout);
                 }
                 }
 
 
-                this.send(message);
+                this.send(message, retransmit);
             },
             },
         );
         );
     }
     }
@@ -2964,36 +3000,36 @@ export class WebClientService {
      *
      *
      * @param timeout Optional request timeout in ms
      * @param timeout Optional request timeout in ms
      */
      */
-    private _sendRequestPromise(type, args = null, timeout: number = null): Promise<any> {
+    private _sendRequestPromise(type, retransmit: boolean, args = null, timeout: number = null): Promise<any> {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_REQUEST,
             type: WebClientService.TYPE_REQUEST,
             subType: type,
             subType: type,
             args: args,
             args: args,
         };
         };
-        return this._sendPromiseMessage(message, timeout);
+        return this._sendPromiseMessage(message, retransmit, timeout);
     }
     }
 
 
-    private _sendCreatePromise(type, args = null, data: any = null, timeout: number = null): Promise<any> {
+    private _sendCreatePromise(type, retransmit: boolean, args = null, data: any = null, timeout: number = null): Promise<any> {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_CREATE,
             type: WebClientService.TYPE_CREATE,
             subType: type,
             subType: type,
             args: args,
             args: args,
             data: data,
             data: data,
         };
         };
-        return this._sendPromiseMessage(message, timeout);
+        return this._sendPromiseMessage(message, retransmit, timeout);
     }
     }
 
 
-    private _sendUpdatePromise(type, args = null, data: any = null, timeout: number = null): Promise<any> {
+    private _sendUpdatePromise(type, retransmit: boolean, args = null, data: any = null, timeout: number = null): Promise<any> {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_UPDATE,
             type: WebClientService.TYPE_UPDATE,
             subType: type,
             subType: type,
             data: data,
             data: data,
             args: args,
             args: args,
         };
         };
-        return this._sendPromiseMessage(message, timeout);
+        return this._sendPromiseMessage(message, retransmit, timeout);
     }
     }
 
 
-    private _sendCreate(type, data, args = null): void {
+    private _sendCreate(type, retransmit: boolean, data, args = null): void {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_CREATE,
             type: WebClientService.TYPE_CREATE,
             subType: type,
             subType: type,
@@ -3002,27 +3038,32 @@ export class WebClientService {
         if (args) {
         if (args) {
             message.args = args;
             message.args = args;
         }
         }
-        this.send(message);
+        this.send(message, retransmit);
     }
     }
 
 
-    private _sendDelete(type, args, data = null): void {
+    private _sendDelete(type, retransmit: boolean, args, data = null): void {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_DELETE,
             type: WebClientService.TYPE_DELETE,
             subType: type,
             subType: type,
             data: data,
             data: data,
             args: args,
             args: args,
         };
         };
-        this.send(message);
+        this.send(message, retransmit);
     }
     }
 
 
-    private _sendDeletePromise(type, args, data: any = null, timeout: number = null): Promise<any> {
+    private _sendDeletePromise(
+        type, retransmit: boolean,
+        args,
+        data: any = null,
+        timeout: number = null,
+    ): Promise<any> {
         const message: threema.WireMessage = {
         const message: threema.WireMessage = {
             type: WebClientService.TYPE_DELETE,
             type: WebClientService.TYPE_DELETE,
             subType: type,
             subType: type,
             data: data,
             data: data,
             args: args,
             args: args,
         };
         };
-        return this._sendPromiseMessage(message, timeout);
+        return this._sendPromiseMessage(message, retransmit, timeout);
     }
     }
 
 
     private _receiveRequest(type, message): void {
     private _receiveRequest(type, message): void {
@@ -3136,6 +3177,9 @@ export class WebClientService {
             case WebClientService.SUB_TYPE_ALERT:
             case WebClientService.SUB_TYPE_ALERT:
                 this._receiveAlert(message);
                 this._receiveAlert(message);
                 break;
                 break;
+            case WebClientService.SUB_TYPE_CONNECTION_ACK:
+                this._receiveConnectionAck(message);
+                break;
             case WebClientService.SUB_TYPE_CONNECTION_DISCONNECT:
             case WebClientService.SUB_TYPE_CONNECTION_DISCONNECT:
                 this._receiveConnectionDisconnect(message);
                 this._receiveConnectionDisconnect(message);
                 break;
                 break;
@@ -3202,11 +3246,12 @@ export class WebClientService {
     /**
     /**
      * Send a message via the underlying transport.
      * Send a message via the underlying transport.
      */
      */
-    private send(message: threema.WireMessage): void {
+    private send(message: threema.WireMessage, retransmit: boolean): void {
         this.$log.debug('Sending', message.type + '/' + message.subType, 'message');
         this.$log.debug('Sending', message.type + '/' + message.subType, 'message');
         if (this.config.MSG_DEBUGGING) {
         if (this.config.MSG_DEBUGGING) {
             this.$log.debug('[Message] Outgoing:', message.type, '/', message.subType, message);
             this.$log.debug('[Message] Outgoing:', message.type, '/', message.subType, message);
         }
         }
+
         switch (this.chosenTask) {
         switch (this.chosenTask) {
             case threema.ChosenTask.WebRTC:
             case threema.ChosenTask.WebRTC:
                 {
                 {
@@ -3229,12 +3274,30 @@ export class WebClientService {
                         }
                         }
                     } else {
                     } else {
                         const bytes: Uint8Array = this.msgpackEncode(message);
                         const bytes: Uint8Array = this.msgpackEncode(message);
-                        const chunker = new chunkedDc.Chunker(this.messageSerial, bytes, WebClientService.CHUNK_SIZE);
+                        // Note: We use the sequence number of the chunk cache here to avoid having another counter
+                        const chunker = new chunkedDc.Chunker(
+                            this.currentChunkCache.sequenceNumber, bytes, WebClientService.CHUNK_SIZE);
                         for (const chunk of chunker) {
                         for (const chunk of chunker) {
-                            // TODO: Add to chunk cache!
+                            // Add to chunk cache
+                            try {
+                                this.currentChunkCache.append(retransmit ? null : chunk);
+                            } catch (error) {
+                                this.$log.error(this.logTag, error);
+                                this.failSession();
+                                return;
+                            }
+
+                            // Send
                             this.sendChunk(chunk);
                             this.sendChunk(chunk);
                         }
                         }
-                        this.messageSerial += 1;
+
+                        // Check if we need to request an acknowledgement
+                        // Note: We only request if none is pending
+                        if (this.pendingAckRequest === null &&
+                            this.currentChunkCache.size > WebClientService.CHUNK_CACHE_SIZE_MAX) {
+                            this._requestConnectionAck();
+                            this.pendingAckRequest = this.currentChunkCache.sequenceNumber;
+                        }
                     }
                     }
                 }
                 }
                 break;
                 break;