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.js118
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';