@torrent-tv/proxy 2.99.10 → 2.99.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import { SwarmSelection } from "./SwarmSelection.js";
|
|
|
20
20
|
import { SelectionLimit } from "./SelectionLimit.js";
|
|
21
21
|
import { futureDownload } from "./FutureDownload.js";
|
|
22
22
|
import { urgencyName } from "../demand/index.js";
|
|
23
|
+
import { pieceStoreOf } from "../piece-store-of.js";
|
|
23
24
|
|
|
24
25
|
/** @type {WeakMap<object, { register: DemandRegister, selection: SwarmSelection }>} */
|
|
25
26
|
const byTorrent = new WeakMap();
|
|
@@ -30,19 +31,117 @@ let publishingSelections = false;
|
|
|
30
31
|
/** What the last reconciliation told the swarm to fetch first, for the pool to say. */
|
|
31
32
|
let firstClassStated = "none";
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The last forecast that finished on the maps it was begun on: its result, the
|
|
36
|
+
* demand and withdrawal revisions of each torrent it was worked out on, and
|
|
37
|
+
* whether nothing has changed since; overdue once a missing piece outlived its
|
|
38
|
+
* forecast time.
|
|
39
|
+
*
|
|
40
|
+
* @type {{ result: Map<object, { measuredAt: number, arrivals: Map<number, number | null> }>,
|
|
41
|
+
* versions: Map<object, { demand: number, withdrawal: number }>, describes: () => boolean, overdue?: boolean } | null}
|
|
42
|
+
*/
|
|
43
|
+
let latestForecast = null;
|
|
44
|
+
/** Who is told that a forecast failed: this layer decides from demand and storage and writes no log of its own. */
|
|
45
|
+
let reportForecastFailure = () => {};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Say where a failed forecast is reported.
|
|
49
|
+
*
|
|
50
|
+
* @param {(error: unknown) => void} report
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
export function onForecastFailure(report) {
|
|
54
|
+
reportForecastFailure = report;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* When this torrent's wanted pieces are forecast to be held, as far as the last
|
|
59
|
+
* finished forecast says so for the map it now has; null when that forecast
|
|
60
|
+
* does not describe it.
|
|
61
|
+
*
|
|
62
|
+
* NEVER AWAITED. Awaited afresh on every report, the forecast held a waiting
|
|
63
|
+
* viewer's answer 3-11.6 s on Home Assistant while it took about a quarter of a
|
|
64
|
+
* second of work, the machine 83-94 % busy: the page asks again only once
|
|
65
|
+
* answered, so the hold after a seek to 100 s lasted 52.5 s (r51,
|
|
66
|
+
* torrent-tv/meta#178). A newer forecast is worked out beside the answer,
|
|
67
|
+
* one at a time, whenever the maps, the withdrawals or the held pieces have
|
|
68
|
+
* changed since the last one.
|
|
69
|
+
*
|
|
70
|
+
* ONLY FOR THE MAP IT WAS WORKED OUT ON. The order pieces come in follows the
|
|
71
|
+
* map, so a forecast made before the map changed says nothing of the new one,
|
|
72
|
+
* and neither does one made before a piece was withdrawn: that piece goes back
|
|
73
|
+
* into the queue ahead of others. Such a torrent gets null, and the caller
|
|
74
|
+
* estimates its missing bytes from the measured rate, as before any forecast.
|
|
75
|
+
*
|
|
76
|
+
* AND WHAT IS HELD IS READ NOW, FROM THE STORE. A piece held now is available
|
|
77
|
+
* now whatever was forecast. A piece still missing whose forecast time has
|
|
78
|
+
* passed did not come when forecast — its peer stopped, or it failed
|
|
79
|
+
* verification — and is unknown, not available: read as `max(0, at - now)`,
|
|
80
|
+
* 2.99.11's stale time made a missing piece count as held.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} torrent
|
|
83
|
+
* @param {{ now?: number, findStore?: (torrent: object) => object | null }} [options]
|
|
84
|
+
* @returns {{ measuredAt: number, arrivals: Map<number, number | null> } | null}
|
|
85
|
+
*/
|
|
86
|
+
export function downloadForecastOf(torrent, { now = Date.now(), findStore = pieceStoreOf } = {}) {
|
|
87
|
+
const entry = byTorrent.get(torrent);
|
|
88
|
+
if (!entry) return null;
|
|
89
|
+
refreshForecast();
|
|
90
|
+
const forecast = latestForecast?.result.get(torrent);
|
|
91
|
+
const version = latestForecast?.versions.get(entry);
|
|
92
|
+
if (!forecast || !version || version.demand !== entry.register.revision ||
|
|
93
|
+
version.withdrawal !== entry.withdrawalRevision) return null;
|
|
94
|
+
const store = findStore(torrent);
|
|
95
|
+
const arrivals = new Map();
|
|
96
|
+
let overdue = false;
|
|
97
|
+
for (const [piece, at] of forecast.arrivals) {
|
|
98
|
+
const held = store?.locationOf ? store.locationOf(piece) !== "missing" : torrent.bitfield?.get(piece) === true;
|
|
99
|
+
if (!held && Number.isFinite(at) && at <= now) overdue = true;
|
|
100
|
+
arrivals.set(piece, held ? now : Number.isFinite(at) && at > now ? at : null);
|
|
101
|
+
}
|
|
102
|
+
// A piece that did not come when forecast is a change no event announces:
|
|
103
|
+
// the next forecast is begun on it, as on one.
|
|
104
|
+
if (overdue) {
|
|
105
|
+
latestForecast.overdue = true;
|
|
106
|
+
refreshForecast();
|
|
44
107
|
}
|
|
45
|
-
return
|
|
108
|
+
return { measuredAt: forecast.measuredAt, arrivals };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Begin a forecast of every live map unless one is being worked out or the
|
|
113
|
+
* last one still describes them.
|
|
114
|
+
*
|
|
115
|
+
* A failed forecast is said and leaves the last one in place; the next report
|
|
116
|
+
* begins another. Left unhandled, its rejection would end nobody's wait and
|
|
117
|
+
* reach the thread's own handler as an unhandled rejection.
|
|
118
|
+
*
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
function refreshForecast() {
|
|
122
|
+
if (futurePending || (latestForecast?.describes() && !latestForecast.overdue)) return;
|
|
123
|
+
const entries = [...live];
|
|
124
|
+
const revisions = entries.map(entry => entry.register.revision);
|
|
125
|
+
const withdrawals = entries.map(entry => entry.withdrawalRevision);
|
|
126
|
+
const verified = entries.map(entry => entry.verifiedRevision);
|
|
127
|
+
const isCurrent = () => entries.length === live.size && !entries.some((entry, index) =>
|
|
128
|
+
!live.has(entry) || entry.register.revision !== revisions[index] ||
|
|
129
|
+
entry.withdrawalRevision !== withdrawals[index]);
|
|
130
|
+
// Worked out again once a piece has arrived as well: the pieces queued
|
|
131
|
+
// behind it now come sooner.
|
|
132
|
+
const describes = () => isCurrent() && entries.every((entry, index) => entry.verifiedRevision === verified[index]);
|
|
133
|
+
const versions = new Map(entries.map((entry, index) =>
|
|
134
|
+
[entry, { demand: revisions[index], withdrawal: withdrawals[index] }]));
|
|
135
|
+
futurePending = futureDownload(entries, { isCurrent }).then(result => {
|
|
136
|
+
if (isCurrent()) latestForecast = { result, versions, describes };
|
|
137
|
+
}, error => {
|
|
138
|
+
try { reportForecastFailure(error); } catch { /* a failing report must not end the next forecast */ }
|
|
139
|
+
}).finally(() => { futurePending = null; });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The forecast being worked out, for checks that wait for it to finish. */
|
|
143
|
+
export function forecastSettled() {
|
|
144
|
+
return futurePending ?? Promise.resolve();
|
|
46
145
|
}
|
|
47
146
|
|
|
48
147
|
/**
|
|
@@ -58,10 +157,11 @@ export function demandFor(torrent) {
|
|
|
58
157
|
}
|
|
59
158
|
const register = new DemandRegister();
|
|
60
159
|
const selection = new SwarmSelection({ torrent, register });
|
|
61
|
-
const entry = { torrent, register, selection, limit: new SelectionLimit({ torrent, register, selection }), withdrawalRevision: 0
|
|
160
|
+
const entry = { torrent, register, selection, limit: new SelectionLimit({ torrent, register, selection }), withdrawalRevision: 0,
|
|
161
|
+
verifiedRevision: 0 };
|
|
62
162
|
byTorrent.set(torrent, entry);
|
|
63
163
|
live.add(entry);
|
|
64
|
-
entry.onBytesChanged = () => reconcileAll();
|
|
164
|
+
entry.onBytesChanged = () => { entry.verifiedRevision++; reconcileAll(); };
|
|
65
165
|
entry.onWithdrawn = () => { entry.withdrawalRevision++; reconcileAll(); };
|
|
66
166
|
torrent.on?.("verified", entry.onBytesChanged);
|
|
67
167
|
torrent.on?.("piece-withdrawn", entry.onWithdrawn);
|
|
@@ -28,7 +28,7 @@ import { Readable } from "node:stream";
|
|
|
28
28
|
import { heldFileBytes } from "./held-file-bytes.js";
|
|
29
29
|
import { createSendStream } from "./channel.js";
|
|
30
30
|
import { readFragments, supplyFiguresFor } from "./piece-reader.js";
|
|
31
|
-
import { demandFor,
|
|
31
|
+
import { demandFor, downloadForecastOf, onForecastFailure, selectionLimitOf } from "../download/registry.js";
|
|
32
32
|
import { heldRangesOf, ownsItsMemory, readHeldBytes, readHeldRanges } from "./held-bytes.js";
|
|
33
33
|
import { CompletedFiles, completedFilesRoot } from "../../storage/files/CompletedFiles.js";
|
|
34
34
|
import { pieceFromWholeFiles, pieceIsInWholeFiles } from "../../storage/files/piece-from-whole-file.js";
|
|
@@ -79,6 +79,8 @@ const { SharedPieceStore, collectStoreStats, findSharedStore, machineReserveByte
|
|
|
79
79
|
// been asked for anything yet.
|
|
80
80
|
const dhtBootstrap = await resolveDhtBootstrap();
|
|
81
81
|
|
|
82
|
+
onForecastFailure(error => logger.warn(`download: the arrival forecast failed, the last one stands: ${error?.stack ?? error}`));
|
|
83
|
+
|
|
82
84
|
const pool = new TorrentPool({
|
|
83
85
|
memoryBytes: workerData?.memoryBytes,
|
|
84
86
|
dhtBootstrap,
|
|
@@ -473,7 +475,7 @@ async function runCommand(command, params, id) {
|
|
|
473
475
|
// interval invented from one point is exactly what this work removes.
|
|
474
476
|
const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
|
|
475
477
|
const forecastAt = performance.now();
|
|
476
|
-
const forecast = file && params.forecast === true ? (
|
|
478
|
+
const forecast = file && params.forecast === true ? downloadForecastOf(torrent) : null;
|
|
477
479
|
const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
|
|
478
480
|
const stats = pool.getFileStats(torrent, params.fileIndex);
|
|
479
481
|
const ranges = [];
|
|
@@ -350,7 +350,12 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
350
350
|
for (const range of source.downloadForecast.ranges ?? []) {
|
|
351
351
|
if (!Number.isFinite(range?.availableAt) || !Number.isSafeInteger(range.start) ||
|
|
352
352
|
!Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start) continue;
|
|
353
|
-
|
|
353
|
+
// A forecast time already past says nothing of when the bytes come:
|
|
354
|
+
// held bytes are in the residence, and missing ones are estimated
|
|
355
|
+
// below. Read as an arrival at zero, a piece forecast to come and
|
|
356
|
+
// still missing counted as held (torrent-tv/meta#178).
|
|
357
|
+
if (range.availableAt <= now) continue;
|
|
358
|
+
const arrival = (range.availableAt - now) / 1000;
|
|
354
359
|
// The entire protocol piece becomes readable after verification;
|
|
355
360
|
// bytes inside it never arrive proportionally to their file address.
|
|
356
361
|
sourceByteService.get(id).push({ start: range.start, end: range.end, begin: arrival, finish: arrival });
|