aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transfers.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transfers.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transfers.js90
1 files changed, 72 insertions, 18 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transfers.js b/packages/meshbay-hub/src/meshbay_hub/static/transfers.js
index 46f75a2..d3b164d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transfers.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transfers.js
@@ -22,6 +22,12 @@
const SPEED_WINDOW_MS = 5000;
+/** Not finished: still preparing, waiting for a slot, or transferring. One
+ * definition, because six places ask and they were drifting apart. */
+function _live(status) {
+ return status === 'preparing' || status === 'queued' || status === 'running';
+}
+
function _abortError() {
const err = new Error('Cancelled');
err.name = 'AbortError';
@@ -84,8 +90,7 @@ export class TransferStore {
/** Running or waiting for a slot — what the nav badge counts. */
get pending() {
- return this._items.filter(
- it => it.status === 'running' || it.status === 'queued').length;
+ return this._items.filter(it => _live(it.status)).length;
}
_speed(it) {
@@ -104,8 +109,28 @@ export class TransferStore {
* `run` receives `{ signal, onProgress }`. It must poll `signal.aborted` — a
* cancel that only sets a flag nobody reads is a button that lies.
*/
+ /**
+ * Start a transfer.
+ *
+ * `run` receives `{ signal, onProgress, lease }`. It must poll
+ * `signal.aborted` — a cancel that only sets a flag nobody reads is a button
+ * that lies.
+ *
+ * `prepare` is optional and runs before anything else, with the row already
+ * on screen. It is where a download opens its target, which can take tens of
+ * seconds — the streamed path waits for the worker, twice, and a Save As
+ * dialog waits for a person. Doing that *before* creating the row meant three
+ * clicks produced no panel at all, not even the icon, and then several rows
+ * at once. Returning `false` drops the row again, which is what a dismissed
+ * dialog should look like: nothing, rather than a cancelled transfer nobody
+ * started.
+ *
+ * `makeLease` is called after `prepare` succeeds, never before. A granted
+ * slot must be taken up within the node's deadline, so it is asked for once
+ * there is somewhere to write — see file-utils.js's downloadEntry.
+ */
start({ kind, name, total = 0, transport = null, run, open = null,
- lease = null }) {
+ lease = null, prepare = null, makeLease = null }) {
const item = {
id: _nextId++,
kind, name, total, transport, open, lease,
@@ -114,7 +139,8 @@ export class TransferStore {
// 'running'. Two different things are true of it — nothing is moving, and
// nothing is wrong — and a status that conflates them is what makes a
// queue look like a hang.
- status: lease && lease.state !== 'granted' ? 'queued' : 'running',
+ status: prepare ? 'preparing'
+ : (lease && lease.state !== 'granted' ? 'queued' : 'running'),
ahead: (lease && lease.ahead) || 0,
error: '',
samples: [{ t: this._now(), done: 0 }],
@@ -155,17 +181,32 @@ export class TransferStore {
// appear to move — the widget showing "3 ahead" for ever while the node
// quietly worked through the queue. Nothing about that looks wrong from
// either side, which is why it needs a test rather than a reading.
- if (item.lease) {
- item.lease._onState = (lease) => {
- if (item.status !== 'queued' && item.status !== 'running') return;
- item.ahead = lease.ahead;
- item.status = lease.state === 'granted' ? 'running' : 'queued';
- this._emit();
- };
- }
+ if (item.lease) this._watchLease(item);
const promise = Promise.resolve()
.then(async () => {
+ if (prepare) {
+ const ready = await prepare();
+ if (item.signal.aborted) throw _abortError();
+ if (ready === false) {
+ // Dismissed. Not a failure and not a cancellation: nothing was ever
+ // started, so nothing should be left on screen to explain.
+ this._drop(item.id);
+ return undefined;
+ }
+ if (ready && ready.name) item.name = ready.name;
+ item.status = 'running';
+ this._emit();
+ }
+ if (makeLease && !item.lease) {
+ item.lease = makeLease();
+ this._watchLease(item);
+ if (item.lease.state !== 'granted') {
+ item.status = 'queued';
+ item.ahead = item.lease.ahead || 0;
+ this._emit();
+ }
+ }
if (item.lease) {
await item.lease.acquire();
if (item.signal.aborted) throw _abortError();
@@ -191,6 +232,21 @@ export class TransferStore {
return item.id;
}
+ _watchLease(item) {
+ item.lease._onState = (lease) => {
+ if (item.status !== 'queued' && item.status !== 'running') return;
+ item.ahead = lease.ahead;
+ item.status = lease.state === 'granted' ? 'running' : 'queued';
+ this._emit();
+ };
+ }
+
+ /** Remove a row entirely. Only for a transfer that never started. */
+ _drop(id) {
+ this._items = this._items.filter(it => it.id !== id);
+ this._emit();
+ }
+
/**
* Hand a finished download to the browser to display.
*
@@ -209,7 +265,7 @@ export class TransferStore {
// 'queued' too: a transfer waiting for a slot is exactly the one somebody
// is most likely to give up on, and its queue entry has to go with it or
// the node grants a slot to a transfer that will never use it.
- if (!item || (item.status !== 'running' && item.status !== 'queued')) return;
+ if (!item || !_live(item.status)) return;
item.signal.aborted = true;
if (item.lease) item.lease.release('cancelled');
// Marked at once. The work stops when it next looks, but a cancelled
@@ -221,14 +277,13 @@ export class TransferStore {
cancelAll() {
for (const it of this._items) {
- if (it.status === 'running' || it.status === 'queued') this.cancel(it.id);
+ if (_live(it.status)) this.cancel(it.id);
}
}
/** Drop everything finished, keeping what is still running or waiting. */
clearFinished() {
- this._items = this._items.filter(
- it => it.status === 'running' || it.status === 'queued');
+ this._items = this._items.filter(it => _live(it.status));
this._emit();
}
@@ -237,8 +292,7 @@ export class TransferStore {
// slot can never be granted one, and the transfer would sit at "waiting"
// for ever with nothing left to answer it.
return this._items.some(
- it => it.transport === transport
- && (it.status === 'running' || it.status === 'queued'));
+ it => it.transport === transport && _live(it.status));
}
/**