diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transfers.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transfers.js | 118 |
1 files changed, 104 insertions, 14 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transfers.js b/packages/meshbay-hub/src/meshbay_hub/static/transfers.js index d3b164d..797321c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transfers.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transfers.js @@ -25,7 +25,15 @@ 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'; + return status === 'preparing' || status === 'queued' || status === 'running' + || status === 'paused'; +} + +/** Raised by `run` when it stopped because the transfer was paused. */ +function _pausedError() { + const err = new Error('Paused'); + err.name = 'PausedError'; + return err; } function _abortError() { @@ -81,6 +89,9 @@ export class TransferStore { // Only for a file written into a folder the browser granted us: that is // the one case where the page can read its own download back. canOpen: it.status === 'done' && typeof it.open === 'function', + // Whether the target can be stopped and continued. False is the honest + // answer for a service-worker stream, and the button is not drawn. + pausable: Boolean(it.pausable), })); } @@ -144,7 +155,18 @@ export class TransferStore { ahead: (lease && lease.ahead) || 0, error: '', samples: [{ t: this._now(), done: 0 }], - signal: { aborted: false }, + signal: { aborted: false, paused: false }, + // Set from `prepare`: whether this target can be stopped and continued. + pausable: false, + // Where a resumed run picks up, in chunks. Zero until something pauses. + resumeFrom: 0, + // Resolved by resume(); awaited by the run loop while paused. + resumed: null, + _wake: null, + // Pausing gives the slot back, so resuming has to be able to ask for + // another one. A transfer handed a lease directly cannot, and must not + // be offered a button that would drop its slot for good. + _canRelease: Boolean(makeLease), }; this._items.push(item); this._emit(); @@ -195,25 +217,58 @@ export class TransferStore { return undefined; } if (ready && ready.name) item.name = ready.name; + // Only the target knows. A service-worker stream is already an HTTP + // response the browser is writing to its own download folder: not + // writing to it stalls that download outside our control, and an idle + // worker is terminated within seconds, taking the stream with it. So + // the button is offered where it works and nowhere else — a pause + // that silently restarts from zero is worse than no pause. + if (ready && ready.pausable) item.pausable = true; 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; + // Run, and be prepared to be stopped and started again. + // + // A paused transfer holds **nothing**: its slot goes back to the node + // and resuming rejoins the queue at the tail. Anything else lets one + // member close a node by pausing four downloads and going to lunch. + // So the lease is taken inside this loop, not before it. + for (;;) { + 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(); + item.status = 'running'; this._emit(); } - } - if (item.lease) { - await item.lease.acquire(); - if (item.signal.aborted) throw _abortError(); - item.status = 'running'; + try { + return await run({ signal: item.signal, onProgress, + lease: item.lease, from: item.resumeFrom || 0 }); + } catch (err) { + if (err.name !== 'PausedError') throw err; + } + // Where to pick up. `run` records it on the signal rather than + // returning it, because it has to survive being thrown past. + item.resumeFrom = item.signal.resumeFrom || 0; + if (item.lease) { + item.lease.release('paused'); + item.lease = null; + } + item.status = 'paused'; + item.ahead = 0; this._emit(); + this._maybeRelease(item.transport); + await item.resumed; + if (item.signal.aborted) throw _abortError(); } - return run({ signal: item.signal, onProgress, lease: item.lease }); }) .then(() => { if (item.signal.aborted) finish('cancelled'); @@ -260,6 +315,37 @@ export class TransferStore { if (item && typeof item.open === 'function') return item.open(); } + /** + * Stop a running transfer, keeping what it has already written. + * + * Only while running: a queued transfer is already stopped and holds no slot, + * and pausing it would only cost it its place. Only where the target can do + * it — see the note in `start`. + * + * The slot goes back to the node at once (§6.2 of the plan): a paused + * transfer holds nothing, and resuming rejoins the queue at the tail. + */ + pause(id) { + const item = this._items.find(it => it.id === id); + if (!item || !item.pausable || !item._canRelease + || item.status !== 'running') return; + item.signal.paused = true; + // Created here rather than in resume(): the run loop awaits it the moment + // `run` throws, which can be sooner than the next call into this store. + item.resumed = new Promise((resolve) => { item._wake = resolve; }); + this._emit(); + } + + /** Start it again, from where it stopped, behind whatever is waiting now. */ + resume(id) { + const item = this._items.find(it => it.id === id); + if (!item || item.status !== 'paused') return; + item.signal.paused = false; + item.status = 'queued'; + this._emit(); + if (item._wake) { item._wake(); item._wake = null; } + } + cancel(id) { const item = this._items.find(it => it.id === id); // 'queued' too: a transfer waiting for a slot is exactly the one somebody @@ -268,6 +354,10 @@ export class TransferStore { if (!item || !_live(item.status)) return; item.signal.aborted = true; if (item.lease) item.lease.release('cancelled'); + // A paused run is parked on `item.resumed`. Without this it stays parked + // for the life of the page, holding its target open, and the row says + // "cancelled" over a download that never stopped. + if (item._wake) { item._wake(); item._wake = null; } // Marked at once. The work stops when it next looks, but a cancelled // transfer should not keep reporting progress in the meantime. item.status = 'cancelled'; |