summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js23
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/file-utils.js111
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transfers.js90
13 files changed, 149 insertions, 85 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index ff066cf..6f4bb92 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -157,9 +157,11 @@ function TransferWidget() {
}, [open]);
const running = items.filter(i => i.status === 'running');
- const waiting = items.filter(i => i.status === 'queued');
+ const waiting = items.filter(
+ i => i.status === 'queued' || i.status === 'preparing');
const finished = items.filter(
- i => i.status !== 'running' && i.status !== 'queued');
+ i => i.status !== 'running' && i.status !== 'queued'
+ && i.status !== 'preparing');
const active = running.length + waiting.length;
// Grouped, and in this order: what is moving, what is waiting, what is over.
@@ -240,7 +242,8 @@ function TransferRow({ it }) {
? html`<a class="transfer-name" href="#" title=${it.name}
onClick=${(e) => { e.preventDefault(); transfers.open(it.id); }}>${it.name}</a>`
: html`<span class="transfer-name" title=${it.name}>${it.name}</span>`}
- ${(it.status === 'running' || it.status === 'queued') && html`
+ ${(it.status === 'running' || it.status === 'queued'
+ || it.status === 'preparing') && html`
<button class="transfer-cancel"
aria-label=${t('transfers.cancel_one', { name: it.name })}
title=${t('transfers.cancel')}
@@ -249,7 +252,19 @@ function TransferRow({ it }) {
</button>
`}
</div>
- ${it.status === 'queued'
+ ${it.status === 'preparing'
+ ? html`
+ ${/* Not a progress bar at 0%: nothing is wrong and nothing is
+ stalled, the download is still finding somewhere to write. The
+ row exists from the click precisely so this state is visible
+ instead of being an empty panel. */''}
+ <div class="dl-progress dl-waiting"></div>
+ <div class="transfer-meta">
+ <span>${t('transfers.preparing')}</span>
+ <span>${formatSize(it.total)}</span>
+ </div>
+ `
+ : it.status === 'queued'
? html`
<div class="dl-progress dl-waiting"></div>
<div class="transfer-meta">
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
index 61002d6..3475f81 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
@@ -321,47 +321,34 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk
* download button — both just want "get this entry to disk".
*/
async function downloadEntry(transfers, transport, gek, entry) {
- // The target FIRST, then the slot — and that order is load-bearing.
- //
- // Asking for the slot first looks better (the widget could draw a row while
- // the target is being chosen) and is wrong: a granted slot has to be taken up
- // within the node's acceptance deadline, and opening a target can take thirty
- // seconds of streamed-download timeouts, or as long as somebody leaves a Save
- // As dialog open. The node then revokes the grant and passes it to the next
- // in the queue — `transfer: reclaimed … (not_taken_up)` in its log — and this
- // download starts fetching under a `tr` that is no longer granted.
- //
- // Measured, not reasoned: three downloads started, one arrived, and the
- // node's log named the reason. Do not move this again without moving the
- // deadline, and the deadline exists so a client that dies between asking and
- // starting does not hold a slot nobody can use.
- let target;
- try {
- target = await _openDownloadTarget(entry.name, entry.size);
- } catch (err) {
- // The refusal belongs in the transfers panel, not in a console nobody
- // opens: that is where someone who just clicked Download is looking, and a
- // failed row naming the reason is the whole point of refusing rather than
- // filling the tab. Started only to be failed, deliberately.
- transfers.start({
- kind: 'download', name: entry.name, total: entry.size, transport,
- run: async () => { throw err; },
- });
- return;
- }
- if (target === false) return; // the picker was dismissed
-
- const openRef = { url: null };
const totalChunks = Math.ceil(entry.size / CHUNK_SIZE);
+ const openRef = { url: null };
+ let target = null;
+
transfers.start({
- kind: 'download', name: (target && target.name) || entry.name,
- total: entry.size, transport,
- // Asked for here, once there is somewhere to write: see the note above.
- lease: transport.openTransfer({
+ kind: 'download', name: entry.name, total: entry.size, transport,
+
+ // The row exists from the click. Opening a target is what takes the time —
+ // the streamed path waits for the worker (twice), a Save As dialog waits
+ // for a person — and doing it before the row meant three clicks produced no
+ // panel at all and then several rows at once.
+ prepare: async () => {
+ target = await _openDownloadTarget(entry.name, entry.size);
+ // Dismissed: nothing was started, so nothing is left on screen.
+ if (target === false) return false;
+ return target ? { name: target.name } : true;
+ },
+
+ // After the target, never before: a granted slot has to be taken up within
+ // the node's deadline, and opening a target can outlast it. See §8.1 of
+ // ~/next/improve-downloads.md — the other order was tried and cost two of
+ // three downloads.
+ makeLease: () => transport.openTransfer({
kind: 'download', bytes: entry.size, chunks: totalChunks }),
- open: target
- ? (target.open || null)
- : () => { if (openRef.url) window.open(openRef.url, '_blank'); },
+
+ open: () => (target && target.open) ? target.open()
+ : (openRef.url ? window.open(openRef.url, '_blank') : undefined),
+
run: async ({ signal, onProgress, lease }) => {
let done = 0;
const onChunk = (bytes) => { done += bytes; onProgress(done, entry.size); };
@@ -435,38 +422,36 @@ async function downloadDirectory(transfers, transport, gek, entries, dir, { setE
// totalBytes decides how this is delivered, but it is not the archive's
// size — headers and the central directory come on top — so it is not
// announced as a Content-Length that the download would then miss.
- let target;
- try {
- target = await _openDownloadTarget(suggested, totalBytes, {
- types: [{ description: 'ZIP archive',
- accept: { 'application/zip': ['.zip'] } }],
- }, 0);
- } catch (err) {
- // Reported beside the folder that was clicked, like zip_too_large just
- // above — this function is called in a loop over a selection, and the
- // sibling folders must still download.
- setError(err.message);
- return;
- }
- if (target === false) return;
- if (!target && !confirm(t('group.zip_no_stream', {
- size: formatSize(totalBytes), name: suggested,
- }))) {
- return;
- }
const zipOpenRef = { url: null };
+ let target = null;
transfers.start({
- kind: 'download', name: (target && target.name) || suggested,
- total: totalBytes, transport,
+ kind: 'download', name: suggested, total: totalBytes, transport,
+
+ // Same order as downloadEntry: the row first, then the target, then the
+ // slot. A folder of forty files is exactly where the wait is longest.
+ prepare: async () => {
+ target = await _openDownloadTarget(suggested, totalBytes, {
+ types: [{ description: 'ZIP archive',
+ accept: { 'application/zip': ['.zip'] } }],
+ }, 0);
+ if (target === false) return false;
+ if (!target && !confirm(t('group.zip_no_stream', {
+ size: formatSize(totalBytes), name: suggested,
+ }))) {
+ return false;
+ }
+ return target ? { name: target.name } : true;
+ },
+
// **One** lease for the archive, not one per file. Dozens of leases for a
// folder would deadlock against the member's own cap: the job cannot finish
// until it holds them all, and it can never hold more than two.
- lease: transport.openTransfer({
+ makeLease: () => transport.openTransfer({
kind: 'download', bytes: totalBytes, chunks: files.length }),
- open: target
- ? (target.open || null)
- : () => { if (zipOpenRef.url) window.open(zipOpenRef.url, '_blank'); },
+
+ open: () => (target && target.open) ? target.open()
+ : (zipOpenRef.url ? window.open(zipOpenRef.url, '_blank') : undefined),
run: async ({ signal, onProgress, lease }) => {
const writable = target ? target.writable : null;
const parts = writable ? null : [];
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index f5c0c78..1458cf2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -578,6 +578,7 @@ export default {
'transfers.open': 'Öffnen',
'transfers.done': 'Abgeschlossen',
'transfers.cancelled': 'Abgebrochen',
+ 'transfers.preparing': 'Wird vorbereitet…',
'transfers.waiting_own_slots': 'Wartet — Ihre Plätze sind belegt',
'transfers.waiting_node': 'Wartet — {n} davor',
'transfers.summary': '{running} laufend · {waiting} wartend',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index e3ed844..b0430c0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -694,6 +694,7 @@ export default {
'transfers.open': 'Open',
'transfers.done': 'Finished',
'transfers.cancelled': 'Cancelled',
+ 'transfers.preparing': 'Preparing…',
'transfers.waiting_own_slots': 'Waiting — your slots are busy',
'transfers.waiting_node': 'Waiting — {n} ahead',
'transfers.summary': '{running} running · {waiting} waiting',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index c509599..adfb1cc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -574,6 +574,7 @@ export default {
'transfers.open': 'Abrir',
'transfers.done': 'Terminada',
'transfers.cancelled': 'Cancelada',
+ 'transfers.preparing': 'Preparando…',
'transfers.waiting_own_slots': 'En espera — sus espacios están ocupados',
'transfers.waiting_node': 'En espera — {n} por delante',
'transfers.summary': '{running} en curso · {waiting} en espera',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 49a1071..b409927 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -577,6 +577,7 @@ export default {
'transfers.open': 'Ouvrir',
'transfers.done': 'Terminé',
'transfers.cancelled': 'Annulé',
+ 'transfers.preparing': 'Préparation…',
'transfers.waiting_own_slots': 'En attente — vos slots sont occupés',
'transfers.waiting_node': 'En attente — {n} devant',
'transfers.summary': '{running} en cours · {waiting} en attente',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 8f1ef1e..7e6246e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -577,6 +577,7 @@ export default {
'transfers.open': 'Apri',
'transfers.done': 'Completato',
'transfers.cancelled': 'Annullato',
+ 'transfers.preparing': 'Preparazione…',
'transfers.waiting_own_slots': 'In attesa — i suoi posti sono occupati',
'transfers.waiting_node': 'In attesa — {n} prima',
'transfers.summary': '{running} in corso · {waiting} in attesa',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 7ea9789..a74eec9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -569,6 +569,7 @@ export default {
'transfers.open': '開く',
'transfers.done': '完了',
'transfers.cancelled': 'キャンセル済み',
+ 'transfers.preparing': '準備中…',
'transfers.waiting_own_slots': '待機中 — 自分の枠がすべて使用中です',
'transfers.waiting_node': '待機中 — 前に {n} 件',
'transfers.summary': '実行中 {running} · 待機中 {waiting}',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 74cd269..a7d64f7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -578,6 +578,7 @@ export default {
'transfers.open': 'Openen',
'transfers.done': 'Voltooid',
'transfers.cancelled': 'Geannuleerd',
+ 'transfers.preparing': 'Voorbereiden…',
'transfers.waiting_own_slots': 'Wacht — uw plaatsen zijn bezet',
'transfers.waiting_node': 'Wacht — {n} ervoor',
'transfers.summary': '{running} bezig · {waiting} wachtend',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 584e5f2..5bd34fb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -590,6 +590,7 @@ export default {
'transfers.open': 'Otwórz',
'transfers.done': 'Zakończony',
'transfers.cancelled': 'Anulowany',
+ 'transfers.preparing': 'Przygotowywanie…',
'transfers.waiting_own_slots': 'Oczekiwanie — Twoje miejsca są zajęte',
'transfers.waiting_node': 'Oczekiwanie — {n} przed',
'transfers.summary': '{running} w toku · {waiting} oczekuje',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index b034117..102ec92 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -576,6 +576,7 @@ export default {
'transfers.open': 'Abrir',
'transfers.done': 'Concluída',
'transfers.cancelled': 'Cancelada',
+ 'transfers.preparing': 'Preparando…',
'transfers.waiting_own_slots': 'Aguardando — seus espaços estão ocupados',
'transfers.waiting_node': 'Aguardando — {n} na frente',
'transfers.summary': '{running} em andamento · {waiting} aguardando',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index ffe2cb0..6b64785 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -557,6 +557,7 @@ export default {
'transfers.open': '打开',
'transfers.done': '已完成',
'transfers.cancelled': '已取消',
+ 'transfers.preparing': '准备中…',
'transfers.waiting_own_slots': '等待中 — 您的通道已占满',
'transfers.waiting_node': '等待中 — 前面还有 {n} 个',
'transfers.summary': '进行中 {running} · 等待中 {waiting}',
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));
}
/**