@droponair/sdk-js 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.0
4
+
5
+ ### Added
6
+
7
+ - **Downloads carry on where they stopped.** A download that breaks keeps what it
8
+ already had and asks the storage for the rest by range, retrying six times with a
9
+ growing wait. Nothing to change in an app: it happens inside `downloadAttachment`.
10
+
3
11
  ## 0.35.0
4
12
 
5
13
  ### Added
@@ -40,6 +40,9 @@ async function readWithProgress(resp, expectedBytes, onProgress) {
40
40
  }
41
41
  return out;
42
42
  }
43
+ /** How many times a broken download is tried again, and how long between tries. */
44
+ const DOWNLOAD_ATTEMPTS = 6;
45
+ const DOWNLOAD_BACKOFF_MS = [2000, 4000, 8000, 15000, 30000];
43
46
  async function sha256Hex(bytes) {
44
47
  const digest = await crypto.subtle.digest('SHA-256', asBufferSource(bytes));
45
48
  const hex = [];
@@ -225,18 +228,49 @@ class AttachmentClient {
225
228
  * current device, unwraps it, then decrypts the stored bytes.
226
229
  */
227
230
  async downloadAttachment(ref, opts = {}) {
228
- const info = await this.getDownloadUrl(ref.attachmentId);
229
- const resp = await this.deps.fetchFn(info.url, { method: info.method, headers: info.headers });
230
- if (!resp.ok) {
231
- const text = await resp.text().catch(() => '');
232
- const message = `Failed to download attachment bytes (HTTP ${resp.status}): ${text}`;
233
- // The link was good and the bytes are not there: deleted from storage
234
- // underneath a record that still existed.
235
- if (resp.status === 404)
236
- throw new attachment_types_1.AttachmentUnavailableError(ref.attachmentId, 404, message);
237
- throw new Error(message);
231
+ // What has already arrived is kept between attempts, and the rest asked for by
232
+ // range, so a download that breaks does not start the file again. A fresh link
233
+ // is asked for each attempt, since a link lasts minutes.
234
+ let had = new Uint8Array(0);
235
+ let info;
236
+ for (let attempt = 1;; attempt++) {
237
+ try {
238
+ info = await this.getDownloadUrl(ref.attachmentId);
239
+ const headers = { ...info.headers };
240
+ if (had.length > 0)
241
+ headers.Range = `bytes=${had.length}-`;
242
+ const resp = await this.deps.fetchFn(info.url, { method: info.method, headers });
243
+ if (resp.status === 416 && had.length > 0)
244
+ break;
245
+ if (!resp.ok) {
246
+ const text = await resp.text().catch(() => '');
247
+ const message = `Failed to download attachment bytes (HTTP ${resp.status}): ${text}`;
248
+ // The link was good and the bytes are not there: deleted from storage
249
+ // underneath a record that still existed.
250
+ if (resp.status === 404)
251
+ throw new attachment_types_1.AttachmentUnavailableError(ref.attachmentId, 404, message);
252
+ throw new Error(message);
253
+ }
254
+ const resuming = resp.status === 206 && had.length > 0;
255
+ const base = resuming ? had : new Uint8Array(0);
256
+ const rest = await readWithProgress(resp, Math.max(0, info.sizeBytes - base.length), opts.onProgress
257
+ ? (received, total) => opts.onProgress(base.length + received, base.length + total)
258
+ : undefined);
259
+ const joined = new Uint8Array(base.length + rest.length);
260
+ joined.set(base, 0);
261
+ joined.set(rest, base.length);
262
+ had = joined;
263
+ break;
264
+ }
265
+ catch (error) {
266
+ if (error instanceof attachment_types_1.AttachmentUnavailableError)
267
+ throw error;
268
+ if (attempt >= DOWNLOAD_ATTEMPTS)
269
+ throw error;
270
+ await new Promise((resolve) => setTimeout(resolve, DOWNLOAD_BACKOFF_MS[Math.min(attempt - 1, DOWNLOAD_BACKOFF_MS.length - 1)]));
271
+ }
238
272
  }
239
- let stored = await readWithProgress(resp, info.sizeBytes, opts.onProgress);
273
+ let stored = had;
240
274
  if (ref.encryptionType === 'CLEARTEXT') {
241
275
  await this.confirmReceived(ref.attachmentId);
242
276
  return { attachmentId: ref.attachmentId, mimeType: info.mimeType, sizeBytes: info.sizeBytes, sha256: info.sha256, bytes: stored };
package/dist/version.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * MINOR, additive feature (e.g. multi-device payloads, new call event type)
8
8
  * PATCH, bug-fix / perf improvement with no wire or API change
9
9
  */
10
- export declare const SDK_VERSION = "0.35.0";
10
+ export declare const SDK_VERSION = "0.36.0";
11
11
  /**
12
12
  * Binary encrypted-payload format version.
13
13
  * Included as the first byte of every encrypted payload so receivers can
package/dist/version.js CHANGED
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
10
10
  * MINOR, additive feature (e.g. multi-device payloads, new call event type)
11
11
  * PATCH, bug-fix / perf improvement with no wire or API change
12
12
  */
13
- exports.SDK_VERSION = '0.35.0';
13
+ exports.SDK_VERSION = '0.36.0';
14
14
  /**
15
15
  * Binary encrypted-payload format version.
16
16
  * Included as the first byte of every encrypted payload so receivers can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droponair/sdk-js",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "End-to-end encrypted messaging, voice and video calling SDK. The relay never sees your keys or message content.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",