xref: /aosp_15_r20/external/pigweed/pw_transfer/ts/transfer.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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