Server
- /api/download/:id answers Range with a strong ETag ("<id>.<gen>") and honours
If-Range; a stale partial gets the whole current file (streamed, so Bun does
not re-apply the Range itself). Uploads get the same treatment.
- GET /api/download/:id/prepare never blocks: ready {gen,size,sha256,etag,ext},
working (server still fetching), legacy (HEVC / too long / cache offline),
failed. Recently refused prepares are remembered for 10 minutes.
Browser
- The OPFS worker saves in 8 MiB ranges, writes at the byte offset, flushes
each chunk, retries each chunk 6 times with backoff (30 s idle timeout) and
keeps the .part plus a .part.json sidecar naming the server copy it belongs
to. A changed copy restarts cleanly; the finished file is hashed once and
checked against the server's SHA-256.
- SaveQueue remembers unfinished saves and resumes them on start, online,
return to the foreground and every 2 minutes while visible; one at a time.
- Downloads shows live MB progress, 'Preparing on server', 'Verifying', and
paused saves with Resume and Cancel (confirmed).
- listVideos ignores the sidecars; new listPartials/discardPartial helpers.
Verified in Chromium through a connection-dropping proxy: paused at 8 MiB,
auto-resumed after a reload from byte 8388608, final SHA-256 matched.
32 lines
1.5 KiB
JavaScript
32 lines
1.5 KiB
JavaScript
'use strict';
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const R = require('./resume-core');
|
|
|
|
test('nextRange walks the file in chunks and stops at the end', () => {
|
|
assert.deepStrictEqual(R.nextRange(0, 20, 8), { start: 0, end: 7 });
|
|
assert.deepStrictEqual(R.nextRange(16, 20, 8), { start: 16, end: 19 });
|
|
assert.strictEqual(R.nextRange(20, 20, 8), null);
|
|
assert.strictEqual(R.nextRange(0, 0, 8), null);
|
|
});
|
|
|
|
test('parseContentRange reads start, end and total', () => {
|
|
assert.deepStrictEqual(R.parseContentRange('bytes 8-15/20'), { start: 8, end: 15, total: 20 });
|
|
assert.deepStrictEqual(R.parseContentRange('bytes 0-1/*'), { start: 0, end: 1, total: null });
|
|
assert.strictEqual(R.parseContentRange('bytes */20'), null);
|
|
assert.strictEqual(R.parseContentRange(null), null);
|
|
});
|
|
|
|
test('backoff grows and caps at 30 s', () => {
|
|
assert.deepStrictEqual([0, 1, 2, 3, 4, 9].map(R.backoffMs), [2000, 4000, 8000, 16000, 30000, 30000]);
|
|
});
|
|
|
|
test('a partial is reused only for the same server copy', () => {
|
|
const t = { etag: '"a.1"', size: 100 };
|
|
assert.strictEqual(R.resumeFrom({ etag: '"a.1"', size: 100 }, 40, t), 40);
|
|
assert.strictEqual(R.resumeFrom({ etag: '"a.0"', size: 100 }, 40, t), 0, 'older generation');
|
|
assert.strictEqual(R.resumeFrom({ etag: '"a.1"', size: 90 }, 40, t), 0, 'size changed');
|
|
assert.strictEqual(R.resumeFrom(null, 40, t), 0, 'no sidecar');
|
|
assert.strictEqual(R.resumeFrom({ etag: '"a.1"', size: 100 }, 150, t), 100, 'never past the end');
|
|
});
|