1*61c4878aSAndroid Build Coastguard Worker// Copyright 2022 The Pigweed Authors 2*61c4878aSAndroid Build Coastguard Worker// 3*61c4878aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4*61c4878aSAndroid Build Coastguard Worker// use this file except in compliance with the License. You may obtain a copy of 5*61c4878aSAndroid Build Coastguard Worker// the License at 6*61c4878aSAndroid Build Coastguard Worker// 7*61c4878aSAndroid Build Coastguard Worker// https://www.apache.org/licenses/LICENSE-2.0 8*61c4878aSAndroid Build Coastguard Worker// 9*61c4878aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*61c4878aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11*61c4878aSAndroid Build Coastguard Worker// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12*61c4878aSAndroid Build Coastguard Worker// License for the specific language governing permissions and limitations under 13*61c4878aSAndroid Build Coastguard Worker// the License. 14*61c4878aSAndroid Build Coastguard Worker 15*61c4878aSAndroid Build Coastguard Workerimport { Status } from 'pigweedjs/pw_status'; 16*61c4878aSAndroid Build Coastguard Workerimport { Chunk } from 'pigweedjs/protos/pw_transfer/transfer_pb'; 17*61c4878aSAndroid Build Coastguard Worker 18*61c4878aSAndroid Build Coastguard Workerexport class ProgressStats { 19*61c4878aSAndroid Build Coastguard Worker constructor( 20*61c4878aSAndroid Build Coastguard Worker readonly bytesSent: number, 21*61c4878aSAndroid Build Coastguard Worker readonly bytesConfirmedReceived: number, 22*61c4878aSAndroid Build Coastguard Worker readonly totalSizeBytes?: number, 23*61c4878aSAndroid Build Coastguard Worker ) {} 24*61c4878aSAndroid Build Coastguard Worker 25*61c4878aSAndroid Build Coastguard Worker get percentReceived(): number { 26*61c4878aSAndroid Build Coastguard Worker if (this.totalSizeBytes === undefined) { 27*61c4878aSAndroid Build Coastguard Worker return NaN; 28*61c4878aSAndroid Build Coastguard Worker } 29*61c4878aSAndroid Build Coastguard Worker return (this.bytesConfirmedReceived / this.totalSizeBytes) * 100; 30*61c4878aSAndroid Build Coastguard Worker } 31*61c4878aSAndroid Build Coastguard Worker 32*61c4878aSAndroid Build Coastguard Worker toString(): string { 33*61c4878aSAndroid Build Coastguard Worker const total = 34*61c4878aSAndroid Build Coastguard Worker this.totalSizeBytes === undefined 35*61c4878aSAndroid Build Coastguard Worker ? 'undefined' 36*61c4878aSAndroid Build Coastguard Worker : this.totalSizeBytes.toString(); 37*61c4878aSAndroid Build Coastguard Worker const percent = this.percentReceived.toFixed(2); 38*61c4878aSAndroid Build Coastguard Worker return ( 39*61c4878aSAndroid Build Coastguard Worker `${percent}% (${this.bytesSent} B sent, ` + 40*61c4878aSAndroid Build Coastguard Worker `${this.bytesConfirmedReceived} B received of ${total} B)` 41*61c4878aSAndroid Build Coastguard Worker ); 42*61c4878aSAndroid Build Coastguard Worker } 43*61c4878aSAndroid Build Coastguard Worker} 44*61c4878aSAndroid Build Coastguard Worker 45*61c4878aSAndroid Build Coastguard Workerexport type ProgressCallback = (stats: ProgressStats) => void; 46*61c4878aSAndroid Build Coastguard Worker 47*61c4878aSAndroid Build Coastguard Worker/** A Timer which invokes a callback after a certain timeout. */ 48*61c4878aSAndroid Build Coastguard Workerclass Timer { 49*61c4878aSAndroid Build Coastguard Worker private task?: ReturnType<typeof setTimeout>; 50*61c4878aSAndroid Build Coastguard Worker 51*61c4878aSAndroid Build Coastguard Worker constructor( 52*61c4878aSAndroid Build Coastguard Worker readonly timeoutS: number, 53*61c4878aSAndroid Build Coastguard Worker private readonly callback: () => any, 54*61c4878aSAndroid Build Coastguard Worker ) {} 55*61c4878aSAndroid Build Coastguard Worker 56*61c4878aSAndroid Build Coastguard Worker /** 57*61c4878aSAndroid Build Coastguard Worker * Starts a new timer. 58*61c4878aSAndroid Build Coastguard Worker * 59*61c4878aSAndroid Build Coastguard Worker * If a timer is already running, it is stopped and a new timer started. 60*61c4878aSAndroid Build Coastguard Worker * This can be used to implement watchdog-like behavior, where a callback 61*61c4878aSAndroid Build Coastguard Worker * is invoked after some time without a kick. 62*61c4878aSAndroid Build Coastguard Worker */ 63*61c4878aSAndroid Build Coastguard Worker start() { 64*61c4878aSAndroid Build Coastguard Worker this.stop(); 65*61c4878aSAndroid Build Coastguard Worker this.task = setTimeout(this.callback, this.timeoutS * 1000); 66*61c4878aSAndroid Build Coastguard Worker } 67*61c4878aSAndroid Build Coastguard Worker 68*61c4878aSAndroid Build Coastguard Worker /** Terminates a running timer. */ 69*61c4878aSAndroid Build Coastguard Worker stop() { 70*61c4878aSAndroid Build Coastguard Worker if (this.task !== undefined) { 71*61c4878aSAndroid Build Coastguard Worker clearTimeout(this.task); 72*61c4878aSAndroid Build Coastguard Worker this.task = undefined; 73*61c4878aSAndroid Build Coastguard Worker } 74*61c4878aSAndroid Build Coastguard Worker } 75*61c4878aSAndroid Build Coastguard Worker} 76*61c4878aSAndroid Build Coastguard Worker 77*61c4878aSAndroid Build Coastguard Worker/** 78*61c4878aSAndroid Build Coastguard Worker * A client-side data transfer through a Manager. 79*61c4878aSAndroid Build Coastguard Worker * 80*61c4878aSAndroid Build Coastguard Worker * Subclasses are responsible for implementing all of the logic for their type 81*61c4878aSAndroid Build Coastguard Worker * of transfer, receiving messages from the server and sending the appropriate 82*61c4878aSAndroid Build Coastguard Worker * messages in response. 83*61c4878aSAndroid Build Coastguard Worker */ 84*61c4878aSAndroid Build Coastguard Workerexport abstract class Transfer { 85*61c4878aSAndroid Build Coastguard Worker status: Status = Status.OK; 86*61c4878aSAndroid Build Coastguard Worker done: Promise<Status>; 87*61c4878aSAndroid Build Coastguard Worker data = new Uint8Array(0); 88*61c4878aSAndroid Build Coastguard Worker 89*61c4878aSAndroid Build Coastguard Worker private retries = 0; 90*61c4878aSAndroid Build Coastguard Worker private responseTimer?: Timer; 91*61c4878aSAndroid Build Coastguard Worker private resolve?: (value: Status | PromiseLike<Status>) => void; 92*61c4878aSAndroid Build Coastguard Worker 93*61c4878aSAndroid Build Coastguard Worker constructor( 94*61c4878aSAndroid Build Coastguard Worker public id: number, 95*61c4878aSAndroid Build Coastguard Worker protected sendChunk: (chunk: Chunk) => void, 96*61c4878aSAndroid Build Coastguard Worker responseTimeoutS: number, 97*61c4878aSAndroid Build Coastguard Worker private maxRetries: number, 98*61c4878aSAndroid Build Coastguard Worker private progressCallback?: ProgressCallback, 99*61c4878aSAndroid Build Coastguard Worker ) { 100*61c4878aSAndroid Build Coastguard Worker this.responseTimer = new Timer(responseTimeoutS, this.onTimeout); 101*61c4878aSAndroid Build Coastguard Worker this.done = new Promise<Status>((resolve) => { 102*61c4878aSAndroid Build Coastguard Worker this.resolve = resolve!; 103*61c4878aSAndroid Build Coastguard Worker }); 104*61c4878aSAndroid Build Coastguard Worker } 105*61c4878aSAndroid Build Coastguard Worker 106*61c4878aSAndroid Build Coastguard Worker /** Returns the initial chunk to notify the server of the transfer. */ 107*61c4878aSAndroid Build Coastguard Worker protected abstract get initialChunk(): Chunk; 108*61c4878aSAndroid Build Coastguard Worker 109*61c4878aSAndroid Build Coastguard Worker /** Handles a chunk that contains or requests data. */ 110*61c4878aSAndroid Build Coastguard Worker protected abstract handleDataChunk(chunk: Chunk): void; 111*61c4878aSAndroid Build Coastguard Worker 112*61c4878aSAndroid Build Coastguard Worker /** Retries after a timeout occurs. */ 113*61c4878aSAndroid Build Coastguard Worker protected abstract retryAfterTimeout(): void; 114*61c4878aSAndroid Build Coastguard Worker 115*61c4878aSAndroid Build Coastguard Worker /** Handles a timeout while waiting for a chunk. */ 116*61c4878aSAndroid Build Coastguard Worker private onTimeout = () => { 117*61c4878aSAndroid Build Coastguard Worker this.retries += 1; 118*61c4878aSAndroid Build Coastguard Worker if (this.retries > this.maxRetries) { 119*61c4878aSAndroid Build Coastguard Worker this.finish(Status.DEADLINE_EXCEEDED); 120*61c4878aSAndroid Build Coastguard Worker return; 121*61c4878aSAndroid Build Coastguard Worker } 122*61c4878aSAndroid Build Coastguard Worker 123*61c4878aSAndroid Build Coastguard Worker console.debug( 124*61c4878aSAndroid Build Coastguard Worker `Received no responses for ${this.responseTimer?.timeoutS}; retrying ${this.retries}/${this.maxRetries}`, 125*61c4878aSAndroid Build Coastguard Worker ); 126*61c4878aSAndroid Build Coastguard Worker 127*61c4878aSAndroid Build Coastguard Worker this.retryAfterTimeout(); 128*61c4878aSAndroid Build Coastguard Worker this.responseTimer?.start(); 129*61c4878aSAndroid Build Coastguard Worker }; 130*61c4878aSAndroid Build Coastguard Worker 131*61c4878aSAndroid Build Coastguard Worker /** Sends the initial chunk of the transfer. */ 132*61c4878aSAndroid Build Coastguard Worker begin(): void { 133*61c4878aSAndroid Build Coastguard Worker this.sendChunk(this.initialChunk as any); 134*61c4878aSAndroid Build Coastguard Worker this.responseTimer?.start(); 135*61c4878aSAndroid Build Coastguard Worker } 136*61c4878aSAndroid Build Coastguard Worker 137*61c4878aSAndroid Build Coastguard Worker /** Ends the transfer with the specified status */ 138*61c4878aSAndroid Build Coastguard Worker protected finish(status: Status): void { 139*61c4878aSAndroid Build Coastguard Worker this.responseTimer?.stop(); 140*61c4878aSAndroid Build Coastguard Worker this.responseTimer = undefined; 141*61c4878aSAndroid Build Coastguard Worker this.status = status; 142*61c4878aSAndroid Build Coastguard Worker 143*61c4878aSAndroid Build Coastguard Worker if (status === Status.OK) { 144*61c4878aSAndroid Build Coastguard Worker const totalSize = this.data.length; 145*61c4878aSAndroid Build Coastguard Worker this.updateProgress(totalSize, totalSize, totalSize); 146*61c4878aSAndroid Build Coastguard Worker } 147*61c4878aSAndroid Build Coastguard Worker 148*61c4878aSAndroid Build Coastguard Worker this.resolve!(this.status); 149*61c4878aSAndroid Build Coastguard Worker } 150*61c4878aSAndroid Build Coastguard Worker 151*61c4878aSAndroid Build Coastguard Worker /** Ends the transfer without sending a completion chunk */ 152*61c4878aSAndroid Build Coastguard Worker abort(status: Status): void { 153*61c4878aSAndroid Build Coastguard Worker this.finish(status); 154*61c4878aSAndroid Build Coastguard Worker } 155*61c4878aSAndroid Build Coastguard Worker 156*61c4878aSAndroid Build Coastguard Worker /** Ends the transfer and sends a completion chunk */ 157*61c4878aSAndroid Build Coastguard Worker terminate(status: Status): void { 158*61c4878aSAndroid Build Coastguard Worker const chunk = new Chunk(); 159*61c4878aSAndroid Build Coastguard Worker chunk.setStatus(status); 160*61c4878aSAndroid Build Coastguard Worker chunk.setTransferId(this.id); 161*61c4878aSAndroid Build Coastguard Worker chunk.setType(Chunk.Type.COMPLETION); 162*61c4878aSAndroid Build Coastguard Worker this.sendChunk(chunk); 163*61c4878aSAndroid Build Coastguard Worker this.abort(status); 164*61c4878aSAndroid Build Coastguard Worker } 165*61c4878aSAndroid Build Coastguard Worker 166*61c4878aSAndroid Build Coastguard Worker /** Invokes the provided progress callback, if any, with the progress */ 167*61c4878aSAndroid Build Coastguard Worker updateProgress( 168*61c4878aSAndroid Build Coastguard Worker bytesSent: number, 169*61c4878aSAndroid Build Coastguard Worker bytesConfirmedReceived: number, 170*61c4878aSAndroid Build Coastguard Worker totalSizeBytes?: number, 171*61c4878aSAndroid Build Coastguard Worker ): void { 172*61c4878aSAndroid Build Coastguard Worker const stats = new ProgressStats( 173*61c4878aSAndroid Build Coastguard Worker bytesSent, 174*61c4878aSAndroid Build Coastguard Worker bytesConfirmedReceived, 175*61c4878aSAndroid Build Coastguard Worker totalSizeBytes, 176*61c4878aSAndroid Build Coastguard Worker ); 177*61c4878aSAndroid Build Coastguard Worker console.debug(`Transfer ${this.id} progress: ${stats}`); 178*61c4878aSAndroid Build Coastguard Worker 179*61c4878aSAndroid Build Coastguard Worker if (this.progressCallback !== undefined) { 180*61c4878aSAndroid Build Coastguard Worker this.progressCallback(stats); 181*61c4878aSAndroid Build Coastguard Worker } 182*61c4878aSAndroid Build Coastguard Worker } 183*61c4878aSAndroid Build Coastguard Worker 184*61c4878aSAndroid Build Coastguard Worker /** 185*61c4878aSAndroid Build Coastguard Worker * Processes an incoming chunk from the server. 186*61c4878aSAndroid Build Coastguard Worker * 187*61c4878aSAndroid Build Coastguard Worker * Handles terminating chunks (i.e. those with a status) and forwards 188*61c4878aSAndroid Build Coastguard Worker * non-terminating chunks to handle_data_chunk. 189*61c4878aSAndroid Build Coastguard Worker */ 190*61c4878aSAndroid Build Coastguard Worker handleChunk(chunk: Chunk): void { 191*61c4878aSAndroid Build Coastguard Worker this.responseTimer?.stop(); 192*61c4878aSAndroid Build Coastguard Worker this.retries = 0; // Received data from service, so reset the retries. 193*61c4878aSAndroid Build Coastguard Worker 194*61c4878aSAndroid Build Coastguard Worker console.debug(`Received chunk:(${chunk})`); 195*61c4878aSAndroid Build Coastguard Worker 196*61c4878aSAndroid Build Coastguard Worker // Status chunks are only used to terminate a transfer. They do not 197*61c4878aSAndroid Build Coastguard Worker // contain any data that requires processing. 198*61c4878aSAndroid Build Coastguard Worker if (chunk.hasStatus()) { 199*61c4878aSAndroid Build Coastguard Worker this.finish(chunk.getStatus()); 200*61c4878aSAndroid Build Coastguard Worker return; 201*61c4878aSAndroid Build Coastguard Worker } 202*61c4878aSAndroid Build Coastguard Worker 203*61c4878aSAndroid Build Coastguard Worker this.handleDataChunk(chunk); 204*61c4878aSAndroid Build Coastguard Worker 205*61c4878aSAndroid Build Coastguard Worker // Start the timeout for the server to send a chunk in response. 206*61c4878aSAndroid Build Coastguard Worker this.responseTimer?.start(); 207*61c4878aSAndroid Build Coastguard Worker } 208*61c4878aSAndroid Build Coastguard Worker} 209*61c4878aSAndroid Build Coastguard Worker 210*61c4878aSAndroid Build Coastguard Worker/** 211*61c4878aSAndroid Build Coastguard Worker * A client <= server read transfer. 212*61c4878aSAndroid Build Coastguard Worker * 213*61c4878aSAndroid Build Coastguard Worker * Although typescript can effectively handle an unlimited transfer window, this 214*61c4878aSAndroid Build Coastguard Worker * client sets a conservative window and chunk size to avoid overloading the 215*61c4878aSAndroid Build Coastguard Worker * device. These are configurable in the constructor. 216*61c4878aSAndroid Build Coastguard Worker */ 217*61c4878aSAndroid Build Coastguard Workerexport class ReadTransfer extends Transfer { 218*61c4878aSAndroid Build Coastguard Worker private maxBytesToReceive: number; 219*61c4878aSAndroid Build Coastguard Worker private maxChunkSize: number; 220*61c4878aSAndroid Build Coastguard Worker private chunkDelayMicroS?: number; // Microseconds 221*61c4878aSAndroid Build Coastguard Worker private remainingTransferSize?: number; 222*61c4878aSAndroid Build Coastguard Worker private offset = 0; 223*61c4878aSAndroid Build Coastguard Worker private pendingBytes: number; 224*61c4878aSAndroid Build Coastguard Worker private windowEndOffset: number; 225*61c4878aSAndroid Build Coastguard Worker 226*61c4878aSAndroid Build Coastguard Worker // The fractional position within a window at which a receive transfer should 227*61c4878aSAndroid Build Coastguard Worker // extend its window size to minimize the amount of time the transmitter 228*61c4878aSAndroid Build Coastguard Worker // spends blocked. 229*61c4878aSAndroid Build Coastguard Worker // 230*61c4878aSAndroid Build Coastguard Worker // For example, a divisor of 2 will extend the window when half of the 231*61c4878aSAndroid Build Coastguard Worker // requested data has been received, a divisor of three will extend at a third 232*61c4878aSAndroid Build Coastguard Worker // of the window, and so on. 233*61c4878aSAndroid Build Coastguard Worker private static EXTEND_WINDOW_DIVISOR = 2; 234*61c4878aSAndroid Build Coastguard Worker 235*61c4878aSAndroid Build Coastguard Worker constructor( 236*61c4878aSAndroid Build Coastguard Worker id: number, 237*61c4878aSAndroid Build Coastguard Worker sendChunk: (chunk: Chunk) => void, 238*61c4878aSAndroid Build Coastguard Worker responseTimeoutS: number, 239*61c4878aSAndroid Build Coastguard Worker maxRetries: number, 240*61c4878aSAndroid Build Coastguard Worker progressCallback?: ProgressCallback, 241*61c4878aSAndroid Build Coastguard Worker maxBytesToReceive = 8192, 242*61c4878aSAndroid Build Coastguard Worker maxChunkSize = 1024, 243*61c4878aSAndroid Build Coastguard Worker chunkDelayMicroS?: number, 244*61c4878aSAndroid Build Coastguard Worker ) { 245*61c4878aSAndroid Build Coastguard Worker super(id, sendChunk, responseTimeoutS, maxRetries, progressCallback); 246*61c4878aSAndroid Build Coastguard Worker this.maxBytesToReceive = maxBytesToReceive; 247*61c4878aSAndroid Build Coastguard Worker this.maxChunkSize = maxChunkSize; 248*61c4878aSAndroid Build Coastguard Worker this.chunkDelayMicroS = chunkDelayMicroS; 249*61c4878aSAndroid Build Coastguard Worker this.pendingBytes = maxBytesToReceive; 250*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset = maxBytesToReceive; 251*61c4878aSAndroid Build Coastguard Worker } 252*61c4878aSAndroid Build Coastguard Worker 253*61c4878aSAndroid Build Coastguard Worker protected get initialChunk(): any { 254*61c4878aSAndroid Build Coastguard Worker return this.transferParameters(Chunk.Type.START); 255*61c4878aSAndroid Build Coastguard Worker } 256*61c4878aSAndroid Build Coastguard Worker 257*61c4878aSAndroid Build Coastguard Worker /** Builds an updated transfer parameters chunk to send the server. */ 258*61c4878aSAndroid Build Coastguard Worker private transferParameters(type: any, update = true): Chunk { 259*61c4878aSAndroid Build Coastguard Worker if (update) { 260*61c4878aSAndroid Build Coastguard Worker this.pendingBytes = this.maxBytesToReceive; 261*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset = this.offset + this.maxBytesToReceive; 262*61c4878aSAndroid Build Coastguard Worker } 263*61c4878aSAndroid Build Coastguard Worker 264*61c4878aSAndroid Build Coastguard Worker const chunk = new Chunk(); 265*61c4878aSAndroid Build Coastguard Worker chunk.setTransferId(this.id); 266*61c4878aSAndroid Build Coastguard Worker chunk.setPendingBytes(this.pendingBytes); 267*61c4878aSAndroid Build Coastguard Worker chunk.setMaxChunkSizeBytes(this.maxChunkSize); 268*61c4878aSAndroid Build Coastguard Worker chunk.setOffset(this.offset); 269*61c4878aSAndroid Build Coastguard Worker chunk.setWindowEndOffset(this.windowEndOffset); 270*61c4878aSAndroid Build Coastguard Worker chunk.setType(type); 271*61c4878aSAndroid Build Coastguard Worker 272*61c4878aSAndroid Build Coastguard Worker if (this.chunkDelayMicroS !== 0) { 273*61c4878aSAndroid Build Coastguard Worker chunk.setMinDelayMicroseconds(this.chunkDelayMicroS!); 274*61c4878aSAndroid Build Coastguard Worker } 275*61c4878aSAndroid Build Coastguard Worker return chunk; 276*61c4878aSAndroid Build Coastguard Worker } 277*61c4878aSAndroid Build Coastguard Worker 278*61c4878aSAndroid Build Coastguard Worker /** 279*61c4878aSAndroid Build Coastguard Worker * Processes an incoming chunk from the server. 280*61c4878aSAndroid Build Coastguard Worker * 281*61c4878aSAndroid Build Coastguard Worker * In a read transfer, the client receives data chunks from the server. 282*61c4878aSAndroid Build Coastguard Worker * Once all pending data is received, the transfer parameters are updated. 283*61c4878aSAndroid Build Coastguard Worker */ 284*61c4878aSAndroid Build Coastguard Worker protected handleDataChunk(chunk: Chunk): void { 285*61c4878aSAndroid Build Coastguard Worker const chunkData = chunk.getData() as Uint8Array; 286*61c4878aSAndroid Build Coastguard Worker 287*61c4878aSAndroid Build Coastguard Worker if (chunk.getOffset() != this.offset) { 288*61c4878aSAndroid Build Coastguard Worker if (chunk.getOffset() + chunkData.length <= this.offset) { 289*61c4878aSAndroid Build Coastguard Worker // If the chunk's data has already been received, don't go through a full 290*61c4878aSAndroid Build Coastguard Worker // recovery cycle to avoid shrinking the window size and potentially 291*61c4878aSAndroid Build Coastguard Worker // thrashing. The expected data may already be in-flight, so just allow 292*61c4878aSAndroid Build Coastguard Worker // the transmitter to keep going with a CONTINUE parameters chunk. 293*61c4878aSAndroid Build Coastguard Worker this.sendChunk( 294*61c4878aSAndroid Build Coastguard Worker this.transferParameters( 295*61c4878aSAndroid Build Coastguard Worker Chunk.Type.PARAMETERS_CONTINUE, 296*61c4878aSAndroid Build Coastguard Worker /*update=*/ false, 297*61c4878aSAndroid Build Coastguard Worker ), 298*61c4878aSAndroid Build Coastguard Worker ); 299*61c4878aSAndroid Build Coastguard Worker } else { 300*61c4878aSAndroid Build Coastguard Worker // Initially, the transfer service only supports in-order transfers. 301*61c4878aSAndroid Build Coastguard Worker // If data is received out of order, request that the server 302*61c4878aSAndroid Build Coastguard Worker // retransmit from the previous offset. 303*61c4878aSAndroid Build Coastguard Worker this.sendChunk( 304*61c4878aSAndroid Build Coastguard Worker this.transferParameters(Chunk.Type.PARAMETERS_RETRANSMIT), 305*61c4878aSAndroid Build Coastguard Worker ); 306*61c4878aSAndroid Build Coastguard Worker } 307*61c4878aSAndroid Build Coastguard Worker return; 308*61c4878aSAndroid Build Coastguard Worker } 309*61c4878aSAndroid Build Coastguard Worker 310*61c4878aSAndroid Build Coastguard Worker const oldData = this.data; 311*61c4878aSAndroid Build Coastguard Worker this.data = new Uint8Array(chunkData.length + oldData.length); 312*61c4878aSAndroid Build Coastguard Worker this.data.set(oldData); 313*61c4878aSAndroid Build Coastguard Worker this.data.set(chunkData, oldData.length); 314*61c4878aSAndroid Build Coastguard Worker 315*61c4878aSAndroid Build Coastguard Worker this.pendingBytes -= chunk.getData().length; 316*61c4878aSAndroid Build Coastguard Worker this.offset += chunk.getData().length; 317*61c4878aSAndroid Build Coastguard Worker 318*61c4878aSAndroid Build Coastguard Worker if (chunk.hasRemainingBytes()) { 319*61c4878aSAndroid Build Coastguard Worker if (chunk.getRemainingBytes() === 0) { 320*61c4878aSAndroid Build Coastguard Worker // No more data to read. Acknowledge receipt and finish. 321*61c4878aSAndroid Build Coastguard Worker this.terminate(Status.OK); 322*61c4878aSAndroid Build Coastguard Worker return; 323*61c4878aSAndroid Build Coastguard Worker } 324*61c4878aSAndroid Build Coastguard Worker 325*61c4878aSAndroid Build Coastguard Worker this.remainingTransferSize = chunk.getRemainingBytes(); 326*61c4878aSAndroid Build Coastguard Worker } else if (this.remainingTransferSize !== undefined) { 327*61c4878aSAndroid Build Coastguard Worker // Update the remaining transfer size, if it is known. 328*61c4878aSAndroid Build Coastguard Worker this.remainingTransferSize -= chunk.getData().length; 329*61c4878aSAndroid Build Coastguard Worker 330*61c4878aSAndroid Build Coastguard Worker if (this.remainingTransferSize <= 0) { 331*61c4878aSAndroid Build Coastguard Worker this.remainingTransferSize = undefined; 332*61c4878aSAndroid Build Coastguard Worker } 333*61c4878aSAndroid Build Coastguard Worker } 334*61c4878aSAndroid Build Coastguard Worker 335*61c4878aSAndroid Build Coastguard Worker if (chunk.getWindowEndOffset() !== 0) { 336*61c4878aSAndroid Build Coastguard Worker if (chunk.getWindowEndOffset() < this.offset) { 337*61c4878aSAndroid Build Coastguard Worker console.error( 338*61c4878aSAndroid Build Coastguard Worker `Transfer ${ 339*61c4878aSAndroid Build Coastguard Worker this.id 340*61c4878aSAndroid Build Coastguard Worker }: transmitter sent invalid earlier end offset ${chunk.getWindowEndOffset()} (receiver offset ${ 341*61c4878aSAndroid Build Coastguard Worker this.offset 342*61c4878aSAndroid Build Coastguard Worker })`, 343*61c4878aSAndroid Build Coastguard Worker ); 344*61c4878aSAndroid Build Coastguard Worker this.terminate(Status.INTERNAL); 345*61c4878aSAndroid Build Coastguard Worker return; 346*61c4878aSAndroid Build Coastguard Worker } 347*61c4878aSAndroid Build Coastguard Worker 348*61c4878aSAndroid Build Coastguard Worker if (chunk.getWindowEndOffset() < this.offset) { 349*61c4878aSAndroid Build Coastguard Worker console.error( 350*61c4878aSAndroid Build Coastguard Worker `Transfer ${ 351*61c4878aSAndroid Build Coastguard Worker this.id 352*61c4878aSAndroid Build Coastguard Worker }: transmitter sent invalid later end offset ${chunk.getWindowEndOffset()} (receiver end offset ${ 353*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset 354*61c4878aSAndroid Build Coastguard Worker })`, 355*61c4878aSAndroid Build Coastguard Worker ); 356*61c4878aSAndroid Build Coastguard Worker this.terminate(Status.INTERNAL); 357*61c4878aSAndroid Build Coastguard Worker return; 358*61c4878aSAndroid Build Coastguard Worker } 359*61c4878aSAndroid Build Coastguard Worker 360*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset = chunk.getWindowEndOffset(); 361*61c4878aSAndroid Build Coastguard Worker this.pendingBytes -= chunk.getWindowEndOffset() - this.offset; 362*61c4878aSAndroid Build Coastguard Worker } 363*61c4878aSAndroid Build Coastguard Worker 364*61c4878aSAndroid Build Coastguard Worker const remainingWindowSize = this.windowEndOffset - this.offset; 365*61c4878aSAndroid Build Coastguard Worker const extendWindow = 366*61c4878aSAndroid Build Coastguard Worker remainingWindowSize <= 367*61c4878aSAndroid Build Coastguard Worker this.maxBytesToReceive / ReadTransfer.EXTEND_WINDOW_DIVISOR; 368*61c4878aSAndroid Build Coastguard Worker 369*61c4878aSAndroid Build Coastguard Worker const totalSize = 370*61c4878aSAndroid Build Coastguard Worker this.remainingTransferSize === undefined 371*61c4878aSAndroid Build Coastguard Worker ? undefined 372*61c4878aSAndroid Build Coastguard Worker : this.remainingTransferSize + this.offset; 373*61c4878aSAndroid Build Coastguard Worker this.updateProgress(this.offset, this.offset, totalSize); 374*61c4878aSAndroid Build Coastguard Worker 375*61c4878aSAndroid Build Coastguard Worker if (this.pendingBytes === 0) { 376*61c4878aSAndroid Build Coastguard Worker // All pending data was received. Send out a new parameters chunk 377*61c4878aSAndroid Build Coastguard Worker // for the next block. 378*61c4878aSAndroid Build Coastguard Worker this.sendChunk(this.transferParameters(Chunk.Type.PARAMETERS_RETRANSMIT)); 379*61c4878aSAndroid Build Coastguard Worker } else if (extendWindow) { 380*61c4878aSAndroid Build Coastguard Worker this.sendChunk(this.transferParameters(Chunk.Type.PARAMETERS_CONTINUE)); 381*61c4878aSAndroid Build Coastguard Worker } 382*61c4878aSAndroid Build Coastguard Worker } 383*61c4878aSAndroid Build Coastguard Worker 384*61c4878aSAndroid Build Coastguard Worker protected retryAfterTimeout(): void { 385*61c4878aSAndroid Build Coastguard Worker this.sendChunk(this.transferParameters(Chunk.Type.PARAMETERS_RETRANSMIT)); 386*61c4878aSAndroid Build Coastguard Worker } 387*61c4878aSAndroid Build Coastguard Worker} 388*61c4878aSAndroid Build Coastguard Worker 389*61c4878aSAndroid Build Coastguard Worker/** 390*61c4878aSAndroid Build Coastguard Worker * A client => server write transfer. 391*61c4878aSAndroid Build Coastguard Worker */ 392*61c4878aSAndroid Build Coastguard Workerexport class WriteTransfer extends Transfer { 393*61c4878aSAndroid Build Coastguard Worker private windowId = 0; 394*61c4878aSAndroid Build Coastguard Worker offset = 0; 395*61c4878aSAndroid Build Coastguard Worker maxChunkSize = 0; 396*61c4878aSAndroid Build Coastguard Worker chunkDelayMicroS?: number; 397*61c4878aSAndroid Build Coastguard Worker windowEndOffset = 0; 398*61c4878aSAndroid Build Coastguard Worker lastChunk: Chunk; 399*61c4878aSAndroid Build Coastguard Worker 400*61c4878aSAndroid Build Coastguard Worker constructor( 401*61c4878aSAndroid Build Coastguard Worker id: number, 402*61c4878aSAndroid Build Coastguard Worker data: Uint8Array, 403*61c4878aSAndroid Build Coastguard Worker sendChunk: (chunk: Chunk) => void, 404*61c4878aSAndroid Build Coastguard Worker responseTimeoutS: number, 405*61c4878aSAndroid Build Coastguard Worker initialResponseTimeoutS: number, 406*61c4878aSAndroid Build Coastguard Worker maxRetries: number, 407*61c4878aSAndroid Build Coastguard Worker progressCallback?: ProgressCallback, 408*61c4878aSAndroid Build Coastguard Worker ) { 409*61c4878aSAndroid Build Coastguard Worker super(id, sendChunk, responseTimeoutS, maxRetries, progressCallback); 410*61c4878aSAndroid Build Coastguard Worker this.data = data; 411*61c4878aSAndroid Build Coastguard Worker this.lastChunk = this.initialChunk; 412*61c4878aSAndroid Build Coastguard Worker } 413*61c4878aSAndroid Build Coastguard Worker 414*61c4878aSAndroid Build Coastguard Worker protected get initialChunk(): any { 415*61c4878aSAndroid Build Coastguard Worker // TODO(frolv): The session ID should not be set here but assigned by the 416*61c4878aSAndroid Build Coastguard Worker // server during an initial handshake. 417*61c4878aSAndroid Build Coastguard Worker const chunk = new Chunk(); 418*61c4878aSAndroid Build Coastguard Worker chunk.setTransferId(this.id); 419*61c4878aSAndroid Build Coastguard Worker chunk.setResourceId(this.id); 420*61c4878aSAndroid Build Coastguard Worker chunk.setType(Chunk.Type.START); 421*61c4878aSAndroid Build Coastguard Worker return chunk; 422*61c4878aSAndroid Build Coastguard Worker } 423*61c4878aSAndroid Build Coastguard Worker 424*61c4878aSAndroid Build Coastguard Worker /** 425*61c4878aSAndroid Build Coastguard Worker * Processes an incoming chunk from the server. 426*61c4878aSAndroid Build Coastguard Worker * 427*61c4878aSAndroid Build Coastguard Worker * In a write transfer, the server only sends transfer parameter updates 428*61c4878aSAndroid Build Coastguard Worker * to the client. When a message is received, update local parameters and 429*61c4878aSAndroid Build Coastguard Worker * send data accordingly. 430*61c4878aSAndroid Build Coastguard Worker */ 431*61c4878aSAndroid Build Coastguard Worker protected handleDataChunk(chunk: Chunk): void { 432*61c4878aSAndroid Build Coastguard Worker this.windowId += 1; 433*61c4878aSAndroid Build Coastguard Worker const initialWindowId = this.windowId; 434*61c4878aSAndroid Build Coastguard Worker 435*61c4878aSAndroid Build Coastguard Worker if (!this.handleParametersUpdate(chunk)) { 436*61c4878aSAndroid Build Coastguard Worker return; 437*61c4878aSAndroid Build Coastguard Worker } 438*61c4878aSAndroid Build Coastguard Worker 439*61c4878aSAndroid Build Coastguard Worker const bytesAknowledged = chunk.getOffset(); 440*61c4878aSAndroid Build Coastguard Worker 441*61c4878aSAndroid Build Coastguard Worker let writeChunk: Chunk; 442*61c4878aSAndroid Build Coastguard Worker // eslint-disable-next-line no-constant-condition 443*61c4878aSAndroid Build Coastguard Worker while (true) { 444*61c4878aSAndroid Build Coastguard Worker writeChunk = this.nextChunk(); 445*61c4878aSAndroid Build Coastguard Worker this.offset += writeChunk.getData().length; 446*61c4878aSAndroid Build Coastguard Worker const sentRequestedBytes = this.offset === this.windowEndOffset; 447*61c4878aSAndroid Build Coastguard Worker 448*61c4878aSAndroid Build Coastguard Worker this.updateProgress(this.offset, bytesAknowledged, this.data.length); 449*61c4878aSAndroid Build Coastguard Worker this.sendChunk(writeChunk); 450*61c4878aSAndroid Build Coastguard Worker 451*61c4878aSAndroid Build Coastguard Worker if (sentRequestedBytes) { 452*61c4878aSAndroid Build Coastguard Worker break; 453*61c4878aSAndroid Build Coastguard Worker } 454*61c4878aSAndroid Build Coastguard Worker } 455*61c4878aSAndroid Build Coastguard Worker 456*61c4878aSAndroid Build Coastguard Worker this.lastChunk = writeChunk; 457*61c4878aSAndroid Build Coastguard Worker } 458*61c4878aSAndroid Build Coastguard Worker 459*61c4878aSAndroid Build Coastguard Worker /** Updates transfer state base on a transfer parameters update. */ 460*61c4878aSAndroid Build Coastguard Worker private handleParametersUpdate(chunk: Chunk): boolean { 461*61c4878aSAndroid Build Coastguard Worker let retransmit = true; 462*61c4878aSAndroid Build Coastguard Worker if (chunk.hasType()) { 463*61c4878aSAndroid Build Coastguard Worker retransmit = chunk.getType() === Chunk.Type.PARAMETERS_RETRANSMIT; 464*61c4878aSAndroid Build Coastguard Worker } 465*61c4878aSAndroid Build Coastguard Worker 466*61c4878aSAndroid Build Coastguard Worker if (chunk.getOffset() > this.data.length) { 467*61c4878aSAndroid Build Coastguard Worker // Bad offset; terminate the transfer. 468*61c4878aSAndroid Build Coastguard Worker console.error( 469*61c4878aSAndroid Build Coastguard Worker `Transfer ${ 470*61c4878aSAndroid Build Coastguard Worker this.id 471*61c4878aSAndroid Build Coastguard Worker }: server requested invalid offset ${chunk.getOffset()} (size ${ 472*61c4878aSAndroid Build Coastguard Worker this.data.length 473*61c4878aSAndroid Build Coastguard Worker })`, 474*61c4878aSAndroid Build Coastguard Worker ); 475*61c4878aSAndroid Build Coastguard Worker 476*61c4878aSAndroid Build Coastguard Worker this.terminate(Status.OUT_OF_RANGE); 477*61c4878aSAndroid Build Coastguard Worker return false; 478*61c4878aSAndroid Build Coastguard Worker } 479*61c4878aSAndroid Build Coastguard Worker 480*61c4878aSAndroid Build Coastguard Worker if (chunk.getPendingBytes() === 0) { 481*61c4878aSAndroid Build Coastguard Worker console.error( 482*61c4878aSAndroid Build Coastguard Worker `Transfer ${this.id}: service requested 0 bytes (invalid); aborting`, 483*61c4878aSAndroid Build Coastguard Worker ); 484*61c4878aSAndroid Build Coastguard Worker this.terminate(Status.INTERNAL); 485*61c4878aSAndroid Build Coastguard Worker return false; 486*61c4878aSAndroid Build Coastguard Worker } 487*61c4878aSAndroid Build Coastguard Worker 488*61c4878aSAndroid Build Coastguard Worker if (retransmit) { 489*61c4878aSAndroid Build Coastguard Worker // Check whether the client has sent a previous data offset, which 490*61c4878aSAndroid Build Coastguard Worker // indicates that some chunks were lost in transmission. 491*61c4878aSAndroid Build Coastguard Worker if (chunk.getOffset() < this.offset) { 492*61c4878aSAndroid Build Coastguard Worker console.debug( 493*61c4878aSAndroid Build Coastguard Worker `Write transfer ${ 494*61c4878aSAndroid Build Coastguard Worker this.id 495*61c4878aSAndroid Build Coastguard Worker } rolling back to offset ${chunk.getOffset()} from ${this.offset}`, 496*61c4878aSAndroid Build Coastguard Worker ); 497*61c4878aSAndroid Build Coastguard Worker } 498*61c4878aSAndroid Build Coastguard Worker 499*61c4878aSAndroid Build Coastguard Worker this.offset = chunk.getOffset(); 500*61c4878aSAndroid Build Coastguard Worker 501*61c4878aSAndroid Build Coastguard Worker // Retransmit is the default behavior for older versions of the 502*61c4878aSAndroid Build Coastguard Worker // transfer protocol. The window_end_offset field is not guaranteed 503*61c4878aSAndroid Build Coastguard Worker // to be set in these version, so it must be calculated. 504*61c4878aSAndroid Build Coastguard Worker const maxBytesToSend = Math.min( 505*61c4878aSAndroid Build Coastguard Worker chunk.getPendingBytes(), 506*61c4878aSAndroid Build Coastguard Worker this.data.length - this.offset, 507*61c4878aSAndroid Build Coastguard Worker ); 508*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset = this.offset + maxBytesToSend; 509*61c4878aSAndroid Build Coastguard Worker } else { 510*61c4878aSAndroid Build Coastguard Worker // Extend the window to the new end offset specified by the server. 511*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset = Math.min( 512*61c4878aSAndroid Build Coastguard Worker chunk.getWindowEndOffset(), 513*61c4878aSAndroid Build Coastguard Worker this.data.length, 514*61c4878aSAndroid Build Coastguard Worker ); 515*61c4878aSAndroid Build Coastguard Worker } 516*61c4878aSAndroid Build Coastguard Worker 517*61c4878aSAndroid Build Coastguard Worker if (chunk.hasMaxChunkSizeBytes()) { 518*61c4878aSAndroid Build Coastguard Worker this.maxChunkSize = chunk.getMaxChunkSizeBytes(); 519*61c4878aSAndroid Build Coastguard Worker } 520*61c4878aSAndroid Build Coastguard Worker 521*61c4878aSAndroid Build Coastguard Worker if (chunk.hasMinDelayMicroseconds()) { 522*61c4878aSAndroid Build Coastguard Worker this.chunkDelayMicroS = chunk.getMinDelayMicroseconds(); 523*61c4878aSAndroid Build Coastguard Worker } 524*61c4878aSAndroid Build Coastguard Worker return true; 525*61c4878aSAndroid Build Coastguard Worker } 526*61c4878aSAndroid Build Coastguard Worker 527*61c4878aSAndroid Build Coastguard Worker /** Returns the next Chunk message to send in the data transfer. */ 528*61c4878aSAndroid Build Coastguard Worker private nextChunk(): Chunk { 529*61c4878aSAndroid Build Coastguard Worker const chunk = new Chunk(); 530*61c4878aSAndroid Build Coastguard Worker chunk.setTransferId(this.id); 531*61c4878aSAndroid Build Coastguard Worker chunk.setOffset(this.offset); 532*61c4878aSAndroid Build Coastguard Worker chunk.setType(Chunk.Type.DATA); 533*61c4878aSAndroid Build Coastguard Worker 534*61c4878aSAndroid Build Coastguard Worker const maxBytesInChunk = Math.min( 535*61c4878aSAndroid Build Coastguard Worker this.maxChunkSize, 536*61c4878aSAndroid Build Coastguard Worker this.windowEndOffset - this.offset, 537*61c4878aSAndroid Build Coastguard Worker ); 538*61c4878aSAndroid Build Coastguard Worker 539*61c4878aSAndroid Build Coastguard Worker chunk.setData(this.data.slice(this.offset, this.offset + maxBytesInChunk)); 540*61c4878aSAndroid Build Coastguard Worker 541*61c4878aSAndroid Build Coastguard Worker // Mark the final chunk of the transfer. 542*61c4878aSAndroid Build Coastguard Worker if (this.data.length - this.offset <= maxBytesInChunk) { 543*61c4878aSAndroid Build Coastguard Worker chunk.setRemainingBytes(0); 544*61c4878aSAndroid Build Coastguard Worker } 545*61c4878aSAndroid Build Coastguard Worker return chunk; 546*61c4878aSAndroid Build Coastguard Worker } 547*61c4878aSAndroid Build Coastguard Worker 548*61c4878aSAndroid Build Coastguard Worker protected retryAfterTimeout(): void { 549*61c4878aSAndroid Build Coastguard Worker this.sendChunk(this.lastChunk); 550*61c4878aSAndroid Build Coastguard Worker } 551*61c4878aSAndroid Build Coastguard Worker} 552