@torrent-tv/proxy 2.101.9 → 2.101.10
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 +1 -1
- package/server.js +1 -0
- package/services/encode/EncodeOrchestrator.js +35 -0
- package/services/encode/PauseRule.js +23 -0
- package/services/encode/quality/HostLoad.js +43 -0
- package/services/server/wire-outputs.js +49 -1
- package/services/torrent/download/registry.js +27 -1
- package/services/torrent/torrent-pool.js +15 -1
- package/services/torrent/worker/client.js +11 -0
- package/services/torrent/worker/pool-adapter.js +11 -0
- package/services/torrent/worker/protocol.js +6 -0
- package/services/torrent/worker/worker.js +5 -0
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1068,6 +1068,7 @@ export async function startProxyServer({
|
|
|
1068
1068
|
// seconds into bytes, what to ask the swarm for, what to keep in memory —
|
|
1069
1069
|
// is its own business.
|
|
1070
1070
|
setDownloadRate: (bytesPerSecond, because) => torrentPool.setDownloadRate?.(bytesPerSecond, because),
|
|
1071
|
+
setSpeculativeHold: (held, because) => torrentPool.setSpeculativeHold?.(held, because),
|
|
1071
1072
|
setPriorityMap: async ({ sourceKey, fileIndex, durationSeconds, zones }) => {
|
|
1072
1073
|
void sourcePreparation?.refresh();
|
|
1073
1074
|
const record = sourceRegistry.get(sourceKey);
|
|
@@ -152,6 +152,14 @@ export class EncodeOrchestrator {
|
|
|
152
152
|
*/
|
|
153
153
|
#pauseRule = new PauseRule();
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Whether the run a viewer needs first is short of the processor, from the
|
|
157
|
+
* last time who has the processor was decided (`processorShortfall`).
|
|
158
|
+
*
|
|
159
|
+
* @type {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number } | null}
|
|
160
|
+
*/
|
|
161
|
+
#shortfall = null;
|
|
162
|
+
|
|
155
163
|
/**
|
|
156
164
|
* What starting and stopping an encoder was measured to cost on this host,
|
|
157
165
|
* before any viewer existed. Told once; real runs replace it as they end.
|
|
@@ -238,6 +246,8 @@ export class EncodeOrchestrator {
|
|
|
238
246
|
* @param {{ freeCores: () => number | null, otherCores: () => number | null } | null} [params.processor] -
|
|
239
247
|
* The processor left free and the processor other work takes, in cores
|
|
240
248
|
* (`ProcessorBudget`); absent, both are unknown.
|
|
249
|
+
* @param {() => void} [params.shortfallNoted] - Told each time who has the
|
|
250
|
+
* processor was decided, so the download can read `processorShortfall`.
|
|
241
251
|
*/
|
|
242
252
|
constructor({
|
|
243
253
|
maxRunsFor,
|
|
@@ -253,6 +263,7 @@ export class EncodeOrchestrator {
|
|
|
253
263
|
describeWaits = null,
|
|
254
264
|
admission = null,
|
|
255
265
|
processor = null,
|
|
266
|
+
shortfallNoted = null,
|
|
256
267
|
logger,
|
|
257
268
|
now
|
|
258
269
|
}) {
|
|
@@ -262,6 +273,7 @@ export class EncodeOrchestrator {
|
|
|
262
273
|
// is about one output, and then only the per-output limit binds.
|
|
263
274
|
this.admission = admission && typeof admission.placesFor === "function" ? admission : null;
|
|
264
275
|
this.processor = processor && typeof processor.freeCores === "function" ? processor : null;
|
|
276
|
+
this.shortfallNoted = typeof shortfallNoted === "function" ? shortfallNoted : () => undefined;
|
|
265
277
|
this.#describeWaits = typeof describeWaits === "function" ? describeWaits : () => "";
|
|
266
278
|
// Only the segment facts and operations this planner needs cross the
|
|
267
279
|
// storage boundary; the concrete storage remains in the server assembly.
|
|
@@ -720,12 +732,16 @@ export class EncodeOrchestrator {
|
|
|
720
732
|
const leading = this.#leadingBand();
|
|
721
733
|
this.#leading = leading;
|
|
722
734
|
if (!leading) {
|
|
735
|
+
this.#noteShortfall(null);
|
|
723
736
|
this.#placeHeldBack(null);
|
|
724
737
|
return;
|
|
725
738
|
}
|
|
726
739
|
const now = this.now();
|
|
727
740
|
const running = this.#runningNow();
|
|
728
741
|
const need = this.#needOf(leading);
|
|
742
|
+
// Only a run in a band a viewer is in can be short for a viewer.
|
|
743
|
+
const short = leading.urgent === true ? this.#pauseRule.shortfall({ leader: leading.run, need }) : null;
|
|
744
|
+
this.#noteShortfall(short ? { address: leading.address, ...short } : null);
|
|
729
745
|
const free = this.#processorReading("freeCores");
|
|
730
746
|
const others = this.#processorReading("otherCores");
|
|
731
747
|
for (const [address, runs] of this.#runs) {
|
|
@@ -752,6 +768,25 @@ export class EncodeOrchestrator {
|
|
|
752
768
|
this.#placeHeldBack(leading);
|
|
753
769
|
}
|
|
754
770
|
|
|
771
|
+
/**
|
|
772
|
+
* Whether the run a viewer needs first is short of the processor, and by
|
|
773
|
+
* what: its output, the speed it made over its seconds kept (median and
|
|
774
|
+
* scatter) and the speed its deadlines need, infinite where a viewer waits
|
|
775
|
+
* for it (`PauseRule.shortfall`, torrent-tv/meta#180). Null where it is not
|
|
776
|
+
* short, or nothing measured it.
|
|
777
|
+
*
|
|
778
|
+
* @returns {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number } | null}
|
|
779
|
+
*/
|
|
780
|
+
processorShortfall() {
|
|
781
|
+
return this.#shortfall;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** @param {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number } | null} short */
|
|
785
|
+
#noteShortfall(short) {
|
|
786
|
+
this.#shortfall = short;
|
|
787
|
+
this.shortfallNoted();
|
|
788
|
+
}
|
|
789
|
+
|
|
755
790
|
/**
|
|
756
791
|
* One second of each encoder's processor and speed, by process
|
|
757
792
|
* (`storage/processor-report.js`): what `PauseRule` judges from. Who has the
|
|
@@ -198,6 +198,29 @@ export class PauseRule {
|
|
|
198
198
|
return { runs: true, because: measured };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Whether the leader is short of the processor: its last second made film
|
|
203
|
+
* without waiting for its input, and over the seconds kept it made less than
|
|
204
|
+
* its need — anything at all where a viewer waits for it. Null where it is
|
|
205
|
+
* not short, waits for its input, or has fewer seconds than a median is
|
|
206
|
+
* taken from (torrent-tv/meta#180, phase 5: what the download's speculative
|
|
207
|
+
* levels give way to).
|
|
208
|
+
*
|
|
209
|
+
* @param {object} params
|
|
210
|
+
* @param {object} params.leader
|
|
211
|
+
* @param {number} params.need - `s*`, as for `judge`.
|
|
212
|
+
* @returns {{ speed: { middle: number, scatter: number }, need: number, seconds: number } | null}
|
|
213
|
+
*/
|
|
214
|
+
shortfall({ leader, need }) {
|
|
215
|
+
const kept = this.#seconds.get(leader) ?? [];
|
|
216
|
+
if (!(need > 0) || kept.length === 0 || kept.at(-1).waiting) return null;
|
|
217
|
+
const lead = kept.filter((second) => !second.waiting && second.speed !== null);
|
|
218
|
+
if (lead.length < LEAST_SECONDS) return null;
|
|
219
|
+
const speed = middleAndScatter(lead.map((second) => second.speed));
|
|
220
|
+
if (Number.isFinite(need) && speed.middle >= need) return null;
|
|
221
|
+
return { speed, need, seconds: lead.length };
|
|
222
|
+
}
|
|
223
|
+
|
|
201
224
|
/**
|
|
202
225
|
* Whether a run not started yet, on `address` and less urgent than
|
|
203
226
|
* `leader`, would be let go at once: a start the rule would pause is not
|
|
@@ -123,6 +123,15 @@ export class HostLoad {
|
|
|
123
123
|
/** @type {number[]} Recent readings behind that median. */
|
|
124
124
|
#torrentCostReadings = [];
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* What every torrent carried per interval, in bytes per second: in all, and
|
|
128
|
+
* over the time our selection held no connection back (`SelectionLimit`).
|
|
129
|
+
* Kept as readings for `swarmRate`.
|
|
130
|
+
*
|
|
131
|
+
* @type {{ carried: number[], unheld: number[] }}
|
|
132
|
+
*/
|
|
133
|
+
#swarmReadings = { carried: [], unheld: [] };
|
|
134
|
+
|
|
126
135
|
/**
|
|
127
136
|
* The share of the machine this process draws with nothing encoding and the
|
|
128
137
|
* torrents moving nothing — the spending that would have happened anyway.
|
|
@@ -264,6 +273,7 @@ export class HostLoad {
|
|
|
264
273
|
// worker starts its clock again: such an interval says nothing.
|
|
265
274
|
if (!(elapsedSec > 0) || megabytes < 0 || now.threadCpuSeconds < previous.threadCpuSeconds) return;
|
|
266
275
|
const cpuSeconds = (now.threadCpuSeconds - previous.threadCpuSeconds) / Math.max(1, os.cpus().length);
|
|
276
|
+
this.#noteSwarm(now, previous, megabytes * 1e6 / elapsedSec);
|
|
267
277
|
const limit = this.downloadLimit;
|
|
268
278
|
// What else the thread moved over the same interval, across every torrent:
|
|
269
279
|
// sent back to the swarm, and handed to readers (encoder inputs, container
|
|
@@ -311,6 +321,39 @@ export class HostLoad {
|
|
|
311
321
|
);
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
/**
|
|
325
|
+
* What the swarm can give every torrent, in bytes per second: the median of
|
|
326
|
+
* what it carried while our selection held no connection back, but never
|
|
327
|
+
* less than the median of what it carried in all — the same choice the
|
|
328
|
+
* start forecast makes for one source (`ViewerRequests` `sourceService`).
|
|
329
|
+
* Time our selection held a connection back measures our asking, not the
|
|
330
|
+
* swarm, so holding the speculative levels back does not lower it
|
|
331
|
+
* (torrent-tv/meta#180). Null before any interval was read.
|
|
332
|
+
*
|
|
333
|
+
* @returns {number | null}
|
|
334
|
+
*/
|
|
335
|
+
swarmRate() {
|
|
336
|
+
const carried = medianOf(this.#swarmReadings.carried);
|
|
337
|
+
const unheld = medianOf(this.#swarmReadings.unheld);
|
|
338
|
+
if (carried === null && unheld === null) return null;
|
|
339
|
+
return Math.max(carried ?? 0, unheld ?? 0);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {{ selection: { freeBytes?: number, freeMs?: number } | null }} now
|
|
344
|
+
* @param {{ selection: { freeBytes?: number, freeMs?: number } | null }} previous
|
|
345
|
+
* @param {number} carried - Bytes per second over the interval, every torrent.
|
|
346
|
+
*/
|
|
347
|
+
#noteSwarm(now, previous, carried) {
|
|
348
|
+
this.#swarmReadings.carried = [...this.#swarmReadings.carried, carried].slice(-READINGS_KEPT);
|
|
349
|
+
const freeMs = (now.selection?.freeMs ?? 0) - (previous.selection?.freeMs ?? 0);
|
|
350
|
+
const freeBytes = (now.selection?.freeBytes ?? 0) - (previous.selection?.freeBytes ?? 0);
|
|
351
|
+
// Totals that went back belong to a torrent that left: nothing to read.
|
|
352
|
+
if (freeMs > 0 && freeBytes >= 0) {
|
|
353
|
+
this.#swarmReadings.unheld = [...this.#swarmReadings.unheld, freeBytes * 1000 / freeMs].slice(-READINGS_KEPT);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
314
357
|
/**
|
|
315
358
|
* What the torrent thread draws in an interval that downloaded nothing.
|
|
316
359
|
*
|
|
@@ -205,6 +205,7 @@ export function wireOutputs({
|
|
|
205
205
|
getSourceStats = null,
|
|
206
206
|
setPriorityMap = null,
|
|
207
207
|
setDownloadRate = null,
|
|
208
|
+
setSpeculativeHold = null,
|
|
208
209
|
// What a second job costs on this host, measured at startup. Null when it
|
|
209
210
|
// could not be measured, and then nothing is corrected — the alternative
|
|
210
211
|
// is inventing a penalty, which is the same fault as inventing a fill rate.
|
|
@@ -753,6 +754,48 @@ export function wireOutputs({
|
|
|
753
754
|
void Promise.resolve(parts.setDownloadRate(share.bytesPerSecond, `film speed ${share.filmSpeed.toFixed(2)}x ` +
|
|
754
755
|
`over ${watched.length} file(s), ${Math.round(share.capacity * 100)}% of the machine for the proxy`)).catch(() => {});
|
|
755
756
|
};
|
|
757
|
+
parts.setSpeculativeHold = typeof setSpeculativeHold === "function" ? setSpeculativeHold : null;
|
|
758
|
+
// THE DOWNLOAD GIVES WAY TO THE ENCODER A VIEWER NEEDS FIRST
|
|
759
|
+
// (torrent-tv/meta#180, phase 5). While that encoder makes film on the
|
|
760
|
+
// processor and less than its deadlines need, what nobody needs yet — the
|
|
761
|
+
// film's tail, the gap behind a seek — is not downloaded, so the torrent
|
|
762
|
+
// thread's processor goes to it. Only where the swarm gives more than the
|
|
763
|
+
// film needs at the speed asked of it (its need, or with a viewer waiting
|
|
764
|
+
// what it makes up to its scatter): then every byte an encoder will reach
|
|
765
|
+
// still comes first and in time, and nothing is lost by not fetching ahead.
|
|
766
|
+
// Where the swarm is slower than that, the bytes fetched ahead are what the
|
|
767
|
+
// film will play from, and they are fetched. Decided each time who has the
|
|
768
|
+
// processor is decided and each time the machine is read.
|
|
769
|
+
parts.speculativeHeld = false;
|
|
770
|
+
parts.applySpeculativeHold = () => {
|
|
771
|
+
if (!parts.setSpeculativeHold || !parts.hostLoad || !parts.encodeOrchestrator) return;
|
|
772
|
+
const short = parts.encodeOrchestrator.processorShortfall();
|
|
773
|
+
let held = false;
|
|
774
|
+
let because = "the encoder a viewer needs first is not short of the processor";
|
|
775
|
+
if (short) {
|
|
776
|
+
const output = parts.outputs.outputsOn(short.address)[0] ?? null;
|
|
777
|
+
const length = Number(output?.file?.lengthBytes ?? parts.hostLoad.fileLengthByKey.get(output?.file?.key));
|
|
778
|
+
const bytesPerFilmSecond = length / Number(output?.file?.durationSeconds);
|
|
779
|
+
const asked = Number.isFinite(short.need) ? short.need : short.speed.middle + short.speed.scatter;
|
|
780
|
+
const needs = bytesPerFilmSecond * asked;
|
|
781
|
+
const swarm = parts.hostLoad.swarmRate();
|
|
782
|
+
const made = `the encoder a viewer needs first made ${short.speed.middle.toFixed(2)}x ` +
|
|
783
|
+
`(scatter ${short.speed.scatter.toFixed(2)}) over ${short.seconds}s` +
|
|
784
|
+
(Number.isFinite(short.need) ? `, needing ${short.need.toFixed(2)}x` : " with a viewer waiting");
|
|
785
|
+
if (!Number.isFinite(needs) || needs <= 0) {
|
|
786
|
+
because = `${made}; its film's bytes per second are unknown`;
|
|
787
|
+
} else if (swarm === null) {
|
|
788
|
+
because = `${made}; the swarm's rate is not measured yet`;
|
|
789
|
+
} else {
|
|
790
|
+
held = swarm > needs;
|
|
791
|
+
because = `${made}; the swarm gives ${(swarm / 1e6).toFixed(2)} MB/s, ` +
|
|
792
|
+
`${held ? "more than" : "not more than"} the ${(needs / 1e6).toFixed(2)} MB/s its film needs at ${asked.toFixed(2)}x`;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (held === parts.speculativeHeld) return;
|
|
796
|
+
parts.speculativeHeld = held;
|
|
797
|
+
void Promise.resolve(parts.setSpeculativeHold(held, because)).catch(() => {});
|
|
798
|
+
};
|
|
756
799
|
// The one owner of what the processor gives the encoders: the share of the
|
|
757
800
|
// machine other work leaves and the measured contention penalties, read by
|
|
758
801
|
// every place that decides on the processor (torrent-tv/meta#180).
|
|
@@ -1090,6 +1133,7 @@ export function wireOutputs({
|
|
|
1090
1133
|
// The processor left free and what other work takes, for the pause rule
|
|
1091
1134
|
// (torrent-tv/meta#180, phase 4).
|
|
1092
1135
|
processor: parts.processor,
|
|
1136
|
+
shortfallNoted: () => parts.applySpeculativeHold?.(),
|
|
1093
1137
|
startingSpeedFor: (address) => parts.encodeCost.speedForOutput(address),
|
|
1094
1138
|
segmentCoverage,
|
|
1095
1139
|
// HOW IT ASKS TO DECIDE AGAIN. A plan that refuses to place anything
|
|
@@ -1257,7 +1301,11 @@ export function wireOutputs({
|
|
|
1257
1301
|
liveConsumers: (...args) => parts.renditions.liveConsumers(...args),
|
|
1258
1302
|
liveRunsOf: (...args) => parts.encodeRuns.liveRunsOf(...args),
|
|
1259
1303
|
producedNumbers: (...args) => parts.serving.producedNumbers(...args),
|
|
1260
|
-
reportHostLoad: async (...args) => {
|
|
1304
|
+
reportHostLoad: async (...args) => {
|
|
1305
|
+
await parts.hostLoad.reportHostLoad(...args);
|
|
1306
|
+
parts.applyDownloadShare?.();
|
|
1307
|
+
parts.applySpeculativeHold?.();
|
|
1308
|
+
},
|
|
1261
1309
|
runStateOf: (...args) => parts.encodeRuns.runStateOf(...args),
|
|
1262
1310
|
sampleDownloadRates: (...args) => parts.hostLoad.sampleDownloadRates(...args),
|
|
1263
1311
|
get encodeCost() { return parts.encodeCost; },
|
|
@@ -30,6 +30,12 @@ let futurePending = null;
|
|
|
30
30
|
let publishingSelections = false;
|
|
31
31
|
/** What the last reconciliation told the swarm to fetch first, for the pool to say. */
|
|
32
32
|
let firstClassStated = "none";
|
|
33
|
+
/**
|
|
34
|
+
* Whether the speculative levels are held back for the processor: the encoder
|
|
35
|
+
* a viewer needs first is short of it, and the swarm gives more than its film
|
|
36
|
+
* needs (`holdSpeculative`, torrent-tv/meta#180).
|
|
37
|
+
*/
|
|
38
|
+
let speculativeHeld = false;
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
41
|
* The last forecast that finished on the maps it was begun on: its result, the
|
|
@@ -222,6 +228,26 @@ export function firstClassStatement() {
|
|
|
222
228
|
return firstClassStated;
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Hold the speculative levels back, or let them go again, on every torrent.
|
|
233
|
+
*
|
|
234
|
+
* Downloading what nobody needs yet takes the torrent thread's processor
|
|
235
|
+
* (Home Assistant r84: 9-19 % of the machine while it downloaded 7-14 MB/s
|
|
236
|
+
* and handed readers about 0.5 MB/s in most intervals, with 0-6 % of the
|
|
237
|
+
* machine free). The side that measures the encoders decides when that processor is
|
|
238
|
+
* worth more to them (torrent-tv/meta#180, phase 5); the urgent levels are
|
|
239
|
+
* never held, so every byte an encoder will reach is still asked for first.
|
|
240
|
+
*
|
|
241
|
+
* @param {boolean} held
|
|
242
|
+
* @returns {boolean} Whether this changed anything.
|
|
243
|
+
*/
|
|
244
|
+
export function holdSpeculative(held) {
|
|
245
|
+
const next = held === true;
|
|
246
|
+
if (next === speculativeHeld) return false;
|
|
247
|
+
speculativeHeld = next;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
225
251
|
/**
|
|
226
252
|
* Bring every torrent's download set into line with what is stated.
|
|
227
253
|
*
|
|
@@ -235,7 +261,7 @@ export function firstClassStatement() {
|
|
|
235
261
|
export function reconcileAll() {
|
|
236
262
|
if (publishingSelections) return { torrents: live.size, speculativeAllowed: false, stated: 0, withdrawn: 0 };
|
|
237
263
|
const entries = [...live];
|
|
238
|
-
const speculativeAllowed = !entries.some((entry) => entry.selection.hasUrgentMissing());
|
|
264
|
+
const speculativeAllowed = !speculativeHeld && !entries.some((entry) => entry.selection.hasUrgentMissing());
|
|
239
265
|
// The first class across every torrent: the most urgent missing level and
|
|
240
266
|
// its highest priority. Lower classes stay out of every torrent's selection.
|
|
241
267
|
const bands = entries.map(entry => entry.selection.missingBand()).filter(Boolean);
|
|
@@ -18,7 +18,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
18
18
|
import { pieceStoreOf } from "./piece-store-of.js";
|
|
19
19
|
import { demandProgress } from "./demand-progress.js";
|
|
20
20
|
import { Urgency, urgencyName } from "./demand/index.js";
|
|
21
|
-
import { demandFor, firstClassStatement, forgetTorrent, reconcileAll, hasUnmetDemand, selectionLimitOf } from "./download/registry.js";
|
|
21
|
+
import { demandFor, firstClassStatement, forgetTorrent, holdSpeculative, reconcileAll, hasUnmetDemand, selectionLimitOf } from "./download/registry.js";
|
|
22
22
|
import { applyDownloadRate } from "./download/throttle.js";
|
|
23
23
|
import { withdrawClaim } from "./download/withdraw-claim.js";
|
|
24
24
|
import { deadlineOfMapZone, levelOfMapZone, priorityOfMapZone } from "./map-zones.js";
|
|
@@ -1309,6 +1309,20 @@ export class TorrentPool {
|
|
|
1309
1309
|
/** The power of two the last limit said was nearest, null for none. */
|
|
1310
1310
|
#downloadLimitScale = null;
|
|
1311
1311
|
|
|
1312
|
+
/**
|
|
1313
|
+
* Hold back, or let go, what every torrent downloads speculatively: the
|
|
1314
|
+
* film's tail and the gap behind a seek (`download/registry.js`
|
|
1315
|
+
* `holdSpeculative`, torrent-tv/meta#180). Said when it changes.
|
|
1316
|
+
*
|
|
1317
|
+
* @param {boolean} held
|
|
1318
|
+
* @param {string} [because]
|
|
1319
|
+
*/
|
|
1320
|
+
setSpeculativeHold(held, because = "") {
|
|
1321
|
+
if (!holdSpeculative(held)) return;
|
|
1322
|
+
logger.info(`torrent-pool: speculative download ${held === true ? "held" : "let go"}${because ? ` (${because})` : ""}`);
|
|
1323
|
+
reconcileAll();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1312
1326
|
applyPriorityMap(torrent, fileIndex, zones, durationSeconds) {
|
|
1313
1327
|
// A MAP WITH NOTHING IN IT IS A STATEMENT, and it is answered before
|
|
1314
1328
|
// anything else is looked at. It says nobody wants any of this file, and
|
|
@@ -511,6 +511,17 @@ export class TorrentWorkerClient {
|
|
|
511
511
|
return this.#caller.call(Command.DOWNLOAD_RATE, { bytesPerSecond: Number.isFinite(bytesPerSecond) ? bytesPerSecond : null, because });
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Whether the speculative levels are held back (`Command.SPECULATIVE_HOLD`).
|
|
516
|
+
*
|
|
517
|
+
* @param {boolean} held
|
|
518
|
+
* @param {string} [because]
|
|
519
|
+
* @returns {Promise<void>}
|
|
520
|
+
*/
|
|
521
|
+
async setSpeculativeHold(held, because = "") {
|
|
522
|
+
return this.#caller.call(Command.SPECULATIVE_HOLD, { held: held === true, because });
|
|
523
|
+
}
|
|
524
|
+
|
|
514
525
|
async setPriorityMap({ sourceKey, fileIndex, zones, durationSeconds, postedAt }) {
|
|
515
526
|
return this.#caller.call(Command.PRIORITY_MAP, {
|
|
516
527
|
sourceKey,
|
|
@@ -179,6 +179,17 @@ export class WorkerTorrentPool {
|
|
|
179
179
|
return this.#client.setDownloadRate(bytesPerSecond, because);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Whether the speculative levels are held back for the encoders.
|
|
184
|
+
*
|
|
185
|
+
* @param {boolean} held
|
|
186
|
+
* @param {string} [because]
|
|
187
|
+
* @returns {Promise<void>}
|
|
188
|
+
*/
|
|
189
|
+
async setSpeculativeHold(held, because = "") {
|
|
190
|
+
return this.#client.setSpeculativeHold(held, because);
|
|
191
|
+
}
|
|
192
|
+
|
|
182
193
|
async setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones, postedAt }) {
|
|
183
194
|
if (!sourceKey) {
|
|
184
195
|
return;
|
|
@@ -72,6 +72,12 @@ export const Command = {
|
|
|
72
72
|
* (`storage/processor-share.js`, torrent-tv/meta#178).
|
|
73
73
|
*/
|
|
74
74
|
DOWNLOAD_RATE: "download-rate",
|
|
75
|
+
/**
|
|
76
|
+
* Whether the speculative levels are held back for the processor the
|
|
77
|
+
* encoders need (`download/registry.js` `holdSpeculative`,
|
|
78
|
+
* torrent-tv/meta#180).
|
|
79
|
+
*/
|
|
80
|
+
SPECULATIVE_HOLD: "speculative-hold",
|
|
75
81
|
/** Read a byte range; the body arrives as CHUNK messages. */
|
|
76
82
|
READ_RANGE: "read-range",
|
|
77
83
|
/** Abandon an in-flight READ_RANGE (viewer gone, seek superseded). */
|
|
@@ -525,6 +525,11 @@ async function runCommand(command, params, id) {
|
|
|
525
525
|
return true;
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
+
case Command.SPECULATIVE_HOLD: {
|
|
529
|
+
pool.setSpeculativeHold(params.held === true, params.because);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
528
533
|
case Command.READ_RANGE: {
|
|
529
534
|
// Streams its own reply; the caller's promise resolves once the body has
|
|
530
535
|
// been fully sent, which is what lets the client await completion.
|