@torrent-tv/proxy 2.95.3 → 2.95.5

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.
@@ -1,166 +1,114 @@
1
1
  import { setImmediate as nextTurn } from "node:timers/promises";
2
- import { downloadCandidates, dispatchDownloadCandidates, peerCanServe, peerRequestCapacity, copyRequestArrival } from "./DeadlineScheduler.js";
2
+ import { piecesOf } from "../demand/pieces.js";
3
+ import { pieceStoreOf } from "../piece-store-of.js";
4
+ import { compareBands } from "./bands.js";
3
5
 
4
6
  const BLOCK_BYTES = 16 * 1024;
5
7
 
6
- /** Replay the actual request scheduler against copied protocol state only. */
7
- export async function futureDownload(entries, { now = Date.now(), findStore, replayOnly = false, isCurrent = () => true } = {}) {
8
+ /** Estimate completion from observed queues and the public selection bands.
9
+ * The native picker decides the order within a band, so every unqueued piece
10
+ * uses that band's completion time rather than an invented request order.
11
+ * Pieces without a measured supplier remain unknown. No requests are changed.
12
+ * Times are conditional on the measured rate and successful verification.
13
+ */
14
+ export async function futureDownload(entries, { now = Date.now(), findStore = pieceStoreOf, isCurrent = () => true } = {}) {
8
15
  if (!isCurrent()) return new Map();
9
- const candidates = downloadCandidates(entries, findStore);
10
- const copies = new Map();
11
- let clock = now;
12
- for (const candidate of candidates) {
13
- const source = candidate.torrent;
14
- let copy = copies.get(source);
15
- if (!copy) {
16
- copy = { pieceLength: source.pieceLength, maxWebConns: source.maxWebConns, states: new Map(), wires: [], arrivals: new Map() };
17
- copies.set(source, copy);
18
- for (const peer of source.wires ?? []) {
19
- if (peer.destroyed || (!peer.requests?.length && peer.peerChoking && !peer.hasFast)) continue;
20
- const speed = Math.max(0, Number(peer.downloadSpeed?.()) || 0);
21
- const available = new Set(candidates.filter(one => one.torrent === source && peer.peerPieces?.get(one.piece)).map(one => one.piece));
22
- if (!available.size && !peer.requests?.length) continue;
23
- const wire = { type: peer.type, destroyed: peer.destroyed, peerChoking: peer.peerChoking,
24
- hasFast: peer.hasFast, peerAllowedFastSet: [...(peer.peerAllowedFastSet ?? [])], peerPieces: { get: piece => available.has(piece) },
25
- peerExtendedHandshake: { reqq: peer.peerExtendedHandshake?.reqq }, downloadSpeed: () => speed,
26
- requests: (peer.requests ?? []).map(request => copyRequestArrival(request,
27
- { piece: request.piece, offset: request.offset, length: request.length, remaining: request.length })) };
28
- wire.cancel = (piece, offset, length) => {
29
- const index = wire.requests.findIndex(request => request.piece === piece && request.offset === offset && request.length === length);
30
- if (index < 0) return;
31
- wire.requests.splice(index, 1);
32
- const state = copy.states.get(piece);
33
- for (let block = offset / BLOCK_BYTES; state && block * BLOCK_BYTES < offset + length; block++) {
34
- state.reserved.delete(block);
35
- state.cursor = Math.min(state.cursor, block);
36
- }
37
- };
38
- copy.wires.push(wire);
16
+ const forecasts = new Map();
17
+ const pending = [];
18
+ let steps = 0;
19
+ for (const { torrent, register } of entries) {
20
+ if (torrent.destroyed || torrent.ready === false) continue;
21
+ const wanted = new Map();
22
+ for (const window of register.windows()) {
23
+ const file = torrent.files?.[window.fileIndex];
24
+ if (!file) continue;
25
+ const range = piecesOf({ fileOffset: file.offset, byteStart: window.byteStart,
26
+ byteEnd: Math.min(window.byteEnd, file.length - 1), pieceLength: torrent.pieceLength });
27
+ if (!range) continue;
28
+ for (let piece = range.from; piece <= range.to; piece++) {
29
+ const previous = wanted.get(piece);
30
+ if (!previous || compareBands(window, previous) < 0) {
31
+ wanted.set(piece, { urgency: window.urgency, priority: window.priority, deadlineAt: window.deadlineAt });
32
+ }
33
+ if (++steps % 256 === 0) {
34
+ await nextTurn();
35
+ if (!isCurrent()) return new Map();
36
+ }
39
37
  }
40
- copy._request = (wire, piece) => {
41
- const state = copy.states.get(piece);
42
- if (!state || state.complete) return false;
43
- let block = state.cursor;
44
- while (block < state.count && (state.present.has(block) || state.reserved.has(block))) block++;
45
- if (block === state.count) return false;
46
- const offset = block * BLOCK_BYTES;
47
- const length = wire.type === "webSeed" ? state.length - offset : Math.min(BLOCK_BYTES, state.length - offset);
48
- for (let index = block; index * BLOCK_BYTES < offset + length; index++) state.reserved.add(index);
49
- state.cursor = Math.ceil((offset + length) / BLOCK_BYTES);
50
- wire.requests.push({ piece, offset, length, remaining: length });
51
- return true;
52
- };
53
- }
54
- const piece = source.pieces?.[candidate.piece];
55
- if (!piece || !Number.isSafeInteger(piece.length) || piece.length <= 0 ||
56
- (piece.missing !== piece.length && !Array.isArray(piece._buffer))) {
57
- copy.arrivals.set(candidate.piece, null);
58
- continue;
59
- }
60
- const count = Math.ceil(piece.length / BLOCK_BYTES);
61
- const present = new Set();
62
- for (let block = 0; block < count; block++) if (piece._buffer?.[block]) present.add(block);
63
- const reserved = new Set();
64
- for (const wire of copy.wires) for (const request of wire.requests) {
65
- if (request.piece !== candidate.piece) continue;
66
- for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) reserved.add(block);
67
- }
68
- copy.states.set(candidate.piece, { length: piece.length, count, present, reserved, cursor: 0, complete: false });
69
- }
70
- const ordered = candidates.filter(candidate => copies.get(candidate.torrent).states.has(candidate.piece))
71
- .map(candidate => ({ ...candidate, torrent: copies.get(candidate.torrent) }));
72
- dispatchDownloadCandidates(ordered, clock);
73
- const solved = new Set();
74
- if (!replayOnly) for (const copy of copies.values()) {
75
- if (copy.wires.length === 1 && !copy.wires[0].destroyed) {
76
- finishSinglePeer(copy, ordered.filter(candidate => candidate.torrent === copy), now);
77
- solved.add(copy);
78
38
  }
79
- }
80
- const replay = ordered.filter(candidate => !solved.has(candidate.torrent));
81
- const pendingCandidates = new Set(replay);
82
- const candidateByCopy = new Map();
83
- for (const candidate of replay) {
84
- let pieces = candidateByCopy.get(candidate.torrent);
85
- if (!pieces) candidateByCopy.set(candidate.torrent, pieces = new Map());
86
- pieces.set(candidate.piece, candidate);
87
- }
88
- const wires = [...copies.values()].filter(copy => !solved.has(copy)).flatMap(copy => copy.wires.map(wire => ({ copy, wire })));
89
- let steps = 0;
90
- while (pendingCandidates.size) {
91
- dispatchDownloadCandidates([...pendingCandidates], clock);
92
- let next = null, delay = Infinity;
93
- for (const entry of wires) {
94
- const { wire } = entry;
95
- if (!wire.requests.length || !(wire.downloadSpeed() > 0)) continue;
96
- const arrivesIn = wire.requests[0].remaining / wire.downloadSpeed() * 1000;
97
- if (arrivesIn < delay) { delay = arrivesIn; next = entry; }
39
+ const queued = new Map();
40
+ for (const wire of torrent.wires ?? []) {
41
+ if (wire.destroyed) continue;
42
+ const speed = Number(wire.downloadSpeed?.());
43
+ if (!Number.isFinite(speed) || speed <= 0) continue;
44
+ let bytes = 0;
45
+ for (const request of wire.requests ?? []) {
46
+ if (!Number.isSafeInteger(request.length) || request.length <= 0) continue;
47
+ bytes += request.length;
48
+ const piece = torrent.pieces?.[request.piece];
49
+ if (!wanted.has(request.piece) || !piece || !Number.isSafeInteger(request.offset) ||
50
+ request.offset < 0 || request.offset % BLOCK_BYTES !== 0 ||
51
+ request.offset + request.length > piece.length ||
52
+ ((request.offset + request.length) % BLOCK_BYTES !== 0 && request.offset + request.length !== piece.length)) continue;
53
+ let blocks = queued.get(request.piece);
54
+ if (!blocks) queued.set(request.piece, blocks = new Map());
55
+ const at = now + bytes / speed * 1000;
56
+ for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
57
+ blocks.set(block, Math.min(blocks.get(block) ?? Infinity, at));
58
+ }
59
+ }
98
60
  }
99
- if (!next || !Number.isFinite(delay)) break;
100
- clock += delay;
101
- for (const { copy, wire } of wires) {
102
- const request = wire.requests[0];
103
- if (!request) continue;
104
- request.remaining = Math.max(0, request.remaining - delay / 1000 * wire.downloadSpeed());
105
- if (wire === next.wire) request.remaining = 0;
106
- if (request.remaining > 0) continue;
107
- wire.requests.shift();
108
- const state = copy.states.get(request.piece);
109
- if (!state || state.complete) continue;
110
- for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
111
- state.present.add(block);
112
- state.reserved.delete(block);
61
+ const store = findStore(torrent);
62
+ const arrivals = new Map();
63
+ for (const [index, band] of wanted) {
64
+ const present = store?.locationOf ? store.locationOf(index) !== "missing" : torrent.bitfield?.get(index);
65
+ if (present) {
66
+ arrivals.set(index, now);
67
+ continue;
113
68
  }
114
- if (state.present.size === state.count) {
115
- state.complete = true;
116
- state.present.clear();
117
- state.reserved.clear();
118
- copy.arrivals.set(request.piece, clock);
119
- pendingCandidates.delete(candidateByCopy.get(copy)?.get(request.piece));
69
+ const piece = torrent.pieces?.[index];
70
+ let at = now;
71
+ if (!piece || !Number.isSafeInteger(piece.length) || piece.length <= 0 ||
72
+ (piece.missing !== piece.length && !Array.isArray(piece._buffer))) at = null;
73
+ else for (let block = 0; block * BLOCK_BYTES < piece.length; block++) {
74
+ const length = Math.min(BLOCK_BYTES, piece.length - block * BLOCK_BYTES);
75
+ if (piece._buffer?.[block]?.length === length) continue;
76
+ const arrival = queued.get(index)?.get(block);
77
+ if (!Number.isFinite(arrival)) { at = null; break; }
78
+ at = Math.max(at, arrival);
79
+ }
80
+ arrivals.set(index, at);
81
+ const remaining = piece?.missing;
82
+ const speed = (torrent.wires ?? []).reduce((sum, wire) => {
83
+ if (wire.destroyed || !wire.peerPieces?.get(index)) return sum;
84
+ const rate = Number(wire.downloadSpeed?.());
85
+ return Number.isFinite(rate) && rate > 0 ? sum + rate : sum;
86
+ }, 0);
87
+ pending.push({ arrivals, index, ...band, seconds:
88
+ Number.isSafeInteger(remaining) && remaining > 0 && remaining <= piece.length && speed > 0
89
+ ? remaining / speed : null });
90
+ if (++steps % 256 === 0) {
91
+ await nextTurn();
92
+ if (!isCurrent()) return new Map();
120
93
  }
121
94
  }
122
- if (++steps % 256 === 0) {
123
- await nextTurn();
124
- if (!isCurrent()) return new Map();
125
- }
95
+ forecasts.set(torrent, { measuredAt: now, arrivals });
126
96
  }
127
- return new Map([...copies].map(([torrent, copy]) => {
128
- for (const piece of copy.states.keys()) if (!copy.arrivals.has(piece)) copy.arrivals.set(piece, null);
129
- return [torrent, { measuredAt: now, arrivals: copy.arrivals }];
130
- }));
131
- }
132
-
133
- /** With one peer there is no reassignment: its FIFO consumes the common map
134
- * order. Sum that unchanged order instead of replaying every block arrival. */
135
- function finishSinglePeer(copy, candidates, now) {
136
- const wire = copy.wires[0];
137
- const speed = wire.downloadSpeed();
138
- if (!(speed > 0) || !(peerRequestCapacity(copy, wire) > 0)) return;
139
- let at = now;
140
- for (const request of wire.requests) {
141
- at += request.length / speed * 1000;
142
- const state = copy.states.get(request.piece);
143
- if (!state || state.complete) continue;
144
- for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
145
- state.present.add(block);
146
- state.reserved.delete(block);
147
- }
148
- if (state.present.size === state.count) {
149
- state.complete = true;
150
- state.present.clear();
151
- state.reserved.clear();
152
- copy.arrivals.set(request.piece, at);
97
+ pending.sort(compareBands);
98
+ let finish = now;
99
+ for (let start = 0; start < pending.length;) {
100
+ let end = start;
101
+ const first = pending[start];
102
+ while (end < pending.length && compareBands(pending[end], first) === 0) {
103
+ const seconds = pending[end++].seconds;
104
+ finish = finish === null || seconds === null ? null : finish + seconds * 1000;
153
105
  }
154
- }
155
- for (const { piece } of candidates) {
156
- const state = copy.states.get(piece);
157
- if (state.complete || !peerCanServe(wire, piece)) continue;
158
- let missing = 0;
159
- for (let block = 0; block < state.count; block++) {
160
- if (!state.present.has(block)) missing += Math.min(BLOCK_BYTES, state.length - block * BLOCK_BYTES);
106
+ for (let i = start; i < end; i++) {
107
+ const { arrivals, index } = pending[i];
108
+ const queuedAt = arrivals.get(index);
109
+ if (finish !== null) arrivals.set(index, queuedAt === null ? finish : Math.min(queuedAt, finish));
161
110
  }
162
- at += missing / speed * 1000;
163
- state.complete = true;
164
- copy.arrivals.set(piece, at);
111
+ start = end;
165
112
  }
113
+ return isCurrent() ? forecasts : new Map();
166
114
  }
@@ -29,6 +29,7 @@ import {
29
29
  urgencyName
30
30
  } from "../demand/index.js";
31
31
  import { pieceStoreOf } from "../piece-store-of.js";
32
+ import { compareBands } from "./bands.js";
32
33
 
33
34
  export class SwarmSelection {
34
35
  #torrent;
@@ -69,23 +70,37 @@ export class SwarmSelection {
69
70
  * shared and the question is not a per-torrent one.
70
71
  * @returns {{ stated: number, withdrawn: number }}
71
72
  */
72
- reconcile() {
73
- const levels = [...new Set(this.#register.windows().map(window => window.urgency))];
73
+ reconcile({ speculativeAllowed = true, maximumUrgency = Infinity, minimumPriority = 1, latestDeadlineAt = Infinity } = {}) {
74
+ const windows=this.#register.windows();
75
+ const missing=windows.filter(window=>!this.#isSatisfied(window));
76
+ const urgency=missing.length?Math.min(...missing.map(window=>window.urgency)):null;
77
+ const eligible=missing.filter(window=>window.urgency===urgency);
78
+ const priority=eligible.length?Math.max(...eligible.map(window=>window.priority)):null;
79
+ const ranked=eligible.filter(window=>window.priority===priority);
80
+ const deadlineAt=ranked.length?Math.min(...ranked.map(window=>window.deadlineAt)):Infinity;
81
+ const active=urgency===null||urgency>maximumUrgency||priority<minimumPriority||deadlineAt>latestDeadlineAt||(!speculativeAllowed&&urgency>=Urgency.TAIL)?[]:
82
+ ranked.filter(window=>window.deadlineAt===deadlineAt);
74
83
  /** @type {Map<string, { from: number, to: number, priority: number }>} */
75
84
  const wanted = new Map();
76
- for (const urgency of levels) {
77
- for (const window of this.#register.at(urgency)) {
85
+ for (const window of active) {
78
86
  const range = this.#piecesFor(window);
79
87
  if (!range) {
80
88
  continue;
81
89
  }
82
90
  // Merged by range and priority, not by claimant: two readers wanting
83
91
  // the same pieces are one instruction to the swarm.
84
- const key = `${range.from}-${range.to}-${selectionPriority(urgency)}`;
85
- wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(urgency) });
86
- }
92
+ const key = `${range.from}-${range.to}-${selectionPriority(window.urgency)}`;
93
+ wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(window.urgency) });
87
94
  }
88
95
 
96
+ // Public selections merge adjacent ranges. Publish their union so an
97
+ // unchanged map does not repeatedly replace the library's merged range.
98
+ const ordered=[...wanted.values()].sort((a,b)=>a.from-b.from);
99
+ wanted.clear();
100
+ const union=[];
101
+ for(const range of ordered){const previous=union.at(-1);if(previous&&range.from<=previous.to+1)previous.to=Math.max(previous.to,range.to);else union.push({...range});}
102
+ for(const range of union)wanted.set(`${range.from}-${range.to}-${range.priority}`,range);
103
+
89
104
  let withdrawn = 0;
90
105
  for (const [key, range] of [...this.#stated]) {
91
106
  if (wanted.has(key)) {
@@ -111,6 +126,14 @@ export class SwarmSelection {
111
126
  }
112
127
 
113
128
  this.#projectIntoMemory();
129
+ for (const window of this.#register.at(Urgency.BLOCKED)) {
130
+ if (this.#isSatisfied(window)) continue;
131
+ const range = this.#piecesFor(window);
132
+ if (range) {
133
+ try { this.#torrent.critical?.(range.from, range.to); }
134
+ catch { /* A closing torrent cannot accept a critical selection. */ }
135
+ }
136
+ }
114
137
  return { stated, withdrawn };
115
138
  }
116
139
 
@@ -177,16 +200,15 @@ export class SwarmSelection {
177
200
  return false;
178
201
  }
179
202
 
180
- /**
181
- * Permission to take a block from a slow peer, for the level being waited on
182
- * and nothing else.
183
- *
184
- * Cleared before it is set again, because WebTorrent never clears the flag
185
- * itself: a reader walking a film would otherwise leave every piece of it
186
- * marked, and the mark would mean nothing anywhere.
187
- *
188
- * @returns {void}
189
- */
203
+ /** The highest missing map band, without choosing protocol blocks. */
204
+ missingBand() {
205
+ let band=null;
206
+ for(const window of this.#register.windows()) {
207
+ if(this.#isSatisfied(window))continue;
208
+ if(!band||compareBands(window,band)<0)band={urgency:window.urgency,priority:window.priority,deadlineAt:window.deadlineAt};
209
+ }
210
+ return band;
211
+ }
190
212
 
191
213
  /**
192
214
  * Tell the piece store which bytes will be read soon, from the same stated
@@ -281,8 +303,10 @@ export class SwarmSelection {
281
303
  if (!range) {
282
304
  return true;
283
305
  }
306
+ const store = this.#findStore(this.#torrent);
284
307
  for (let index = range.from; index <= range.to; index += 1) {
285
- if (!this.#torrent.bitfield?.get(index)) {
308
+ const present = store?.locationOf ? store.locationOf(index) !== "missing" : this.#torrent.bitfield?.get(index);
309
+ if (!present) {
286
310
  return false;
287
311
  }
288
312
  }
@@ -311,15 +335,7 @@ export class SwarmSelection {
311
335
  */
312
336
  #select({ from, to, priority }) {
313
337
  try {
314
- // The private form takes the stream flag, which makes a selection several
315
- // claimants can hold at the same bounds; the public one merges and
316
- // subtracts intervals and cannot express "one of several wants this". The
317
- // public call is the fallback if a future version drops the private one.
318
- if (typeof this.#torrent._select === "function") {
319
- this.#torrent._select(from, to, priority, null, true);
320
- } else if (typeof this.#torrent.select === "function") {
321
- this.#torrent.select(from, to, priority);
322
- }
338
+ this.#torrent.select?.(from, to, priority);
323
339
  } catch {
324
340
  // silent-ok: never fail a read because the download set refused.
325
341
  }
@@ -331,11 +347,7 @@ export class SwarmSelection {
331
347
  */
332
348
  #deselect({ from, to }) {
333
349
  try {
334
- if (typeof this.#torrent._deselect === "function") {
335
- this.#torrent._deselect(from, to, true);
336
- } else if (typeof this.#torrent.deselect === "function") {
337
- this.#torrent.deselect(from, to);
338
- }
350
+ this.#torrent.deselect?.(from, to);
339
351
  } catch {
340
352
  // silent-ok.
341
353
  }
@@ -353,7 +365,7 @@ export class SwarmSelection {
353
365
  return (
354
366
  `download: ${this.#stated.size} instruction(s) to the swarm from ` +
355
367
  `${this.#register.size} stated need(s) [${needs.join(" ")}]` +
356
- "; peer requests ordered by deadline and priority"
368
+ "; peer requests issued by WebTorrent"
357
369
  );
358
370
  }
359
371
  }
@@ -0,0 +1,5 @@
1
+ /** Public selection groups retain both the map's rank and its required time. */
2
+ export function compareBands(left, right) {
3
+ return left.urgency - right.urgency || right.priority - left.priority ||
4
+ (left.deadlineAt === right.deadlineAt ? 0 : left.deadlineAt - right.deadlineAt);
5
+ }
@@ -17,14 +17,12 @@
17
17
 
18
18
  import { DemandRegister } from "../demand/DemandRegister.js";
19
19
  import { SwarmSelection } from "./SwarmSelection.js";
20
- import { DeadlineScheduler } from "./DeadlineScheduler.js";
21
20
  import { futureDownload } from "./FutureDownload.js";
22
21
 
23
22
  /** @type {WeakMap<object, { register: DemandRegister, selection: SwarmSelection }>} */
24
23
  const byTorrent = new WeakMap();
25
24
  /** @type {Set<{ register: DemandRegister, selection: SwarmSelection }>} */
26
25
  const live = new Set();
27
- const scheduler = new DeadlineScheduler({ entries: () => live });
28
26
  let futurePending = null;
29
27
  let publishingSelections = false;
30
28
 
@@ -56,12 +54,6 @@ export function demandFor(torrent) {
56
54
  }
57
55
  const register = new DemandRegister();
58
56
  const entry = { torrent, register, selection: new SwarmSelection({ torrent, register }), withdrawalRevision: 0 };
59
- if (typeof torrent._request === "function" && typeof torrent._updateWire === "function") {
60
- entry.previousWireUpdate = torrent._updateWire;
61
- torrent._updateWire = wire => {
62
- if (!publishingSelections) scheduler.reconcile(Date.now(), { torrent, wire });
63
- };
64
- }
65
57
  byTorrent.set(torrent, entry);
66
58
  live.add(entry);
67
59
  entry.onBytesChanged = () => reconcileAll();
@@ -106,7 +98,6 @@ export function forgetTorrent(torrent) {
106
98
  held.register.clear();
107
99
  torrent.removeListener?.("verified", held.onBytesChanged);
108
100
  torrent.removeListener?.("piece-withdrawn", held.onWithdrawn);
109
- if (held.previousWireUpdate) torrent._updateWire = held.previousWireUpdate;
110
101
  byTorrent.delete(torrent);
111
102
  live.delete(held);
112
103
  }
@@ -125,18 +116,21 @@ export function reconcileAll() {
125
116
  if (publishingSelections) return { torrents: live.size, speculativeAllowed: false, stated: 0, withdrawn: 0 };
126
117
  const entries = [...live];
127
118
  const speculativeAllowed = !entries.some((entry) => entry.selection.hasUrgentMissing());
119
+ const bands=entries.map(entry=>entry.selection.missingBand()).filter(Boolean);
120
+ const maximumUrgency=bands.length?Math.min(...bands.map(band=>band.urgency)):Infinity;
121
+ const minimumPriority=bands.length?Math.max(...bands.filter(band=>band.urgency===maximumUrgency).map(band=>band.priority)):1;
122
+ const latestDeadlineAt=bands.length?Math.min(...bands.filter(band=>band.urgency===maximumUrgency&&band.priority===minimumPriority).map(band=>band.deadlineAt)):Infinity;
128
123
  let stated = 0;
129
124
  let withdrawn = 0;
130
125
  publishingSelections = true;
131
126
  try {
132
127
  for (const entry of entries) {
133
- const result = entry.selection.reconcile({ speculativeAllowed, deadlineDriven: !!entry.previousWireUpdate });
128
+ const result = entry.selection.reconcile({ speculativeAllowed, maximumUrgency, minimumPriority, latestDeadlineAt });
134
129
  stated += result.stated;
135
130
  withdrawn += result.withdrawn;
136
131
  }
137
132
  } finally {
138
133
  publishingSelections = false;
139
134
  }
140
- scheduler.reconcile();
141
135
  return { torrents: entries.length, speculativeAllowed, stated, withdrawn };
142
136
  }
@@ -23,7 +23,7 @@ export class DownloadMaps {
23
23
  const interval = zone.downloadInterval ?? zone;
24
24
  const current = file.sourceZones.find(candidate => candidate.from === interval.from && candidate.to === interval.to);
25
25
  const deadlineAt = current && Number.isFinite(current.deadlineAt)
26
- ? current.deadlineAt - (zone.leadSeconds ?? 0) * 1000 : current?.deadlineAt;
26
+ ? current.deadlineAt + (Math.max(0, zone.from - current.from) - (zone.leadSeconds ?? 0)) * 1000 : current?.deadlineAt;
27
27
  if (!current || Object.keys(current).some(key => key !== "from" && key !== "to" && !Object.is(zone[key], key === "deadlineAt" ? deadlineAt : current[key]))) changed = true;
28
28
  return current ? [{ ...zone, ...current, from: zone.from, to: zone.to, deadlineAt }] : [];
29
29
  });
@@ -1,5 +1,30 @@
1
1
  import { mapForViewer, mergeMaps, runsOf } from "./PriorityMap.js";
2
2
 
3
+ /** Price original-file cluster ranges without requiring a packet index. */
4
+ export async function nativeOriginalSourceMap({ container, viewers, durationSeconds, startTimeSeconds = 0,
5
+ allowanceSeconds, urgentReadyFor, now = Date.now() }) {
6
+ if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return [];
7
+ const maps = viewers.map(viewer => mapForViewer({
8
+ atSeconds: viewer.positionSeconds(now) ?? 0, durationSeconds, allowanceSeconds,
9
+ playing: viewer.playing || viewer.waiting, viewerCount: viewers.length,
10
+ pauseSeconds: viewer.pausedAt === null ? 0 : (now - viewer.pausedAt) / 1000,
11
+ urgentReady: urgentReadyFor(viewer)
12
+ }));
13
+ const zones = [];
14
+ for (const zone of runsOf(mergeMaps(maps))) {
15
+ const input = await container.readMappedSourceRanges({ from: zone.from + startTimeSeconds,
16
+ to: Math.min(durationSeconds, zone.to) + startTimeSeconds });
17
+ if (input.kind !== "result") continue;
18
+ const links = viewers.filter((_viewer, index) => maps[index].priority[zone.from] === zone.priority)
19
+ .map(viewer => viewer.linkReading()?.linkMbps).filter(rate => Number.isFinite(rate) && rate > 0);
20
+ const bytes = input.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0);
21
+ const deliverySeconds = links.length ? bytes * 8 / (Math.min(...links) * 1_000_000) : 0;
22
+ for (const [byteStart, byteEnd] of input.ranges) zones.push({ ...zone, byteStart, byteEnd,
23
+ deadlineAt: Number.isFinite(zone.withinSeconds) ? now + (zone.withinSeconds - deliverySeconds) * 1000 : Infinity });
24
+ }
25
+ return zones;
26
+ }
27
+
3
28
  /** Price native source packets with the same viewer map as encoded outputs. */
4
29
  export function nativeSourceMap({ index, tracks, viewers, durationSeconds, startTimeSeconds = 0,
5
30
  allowanceSeconds, urgentReadyFor, now = Date.now() }) {
@@ -83,6 +83,7 @@
83
83
  * @property {number} to - Last second, exclusive.
84
84
  * @property {number} priority
85
85
  * @property {number} withinSeconds
86
+ * @property {boolean} urgent - Required preparation before playback can continue.
86
87
  * @property {boolean} behind - Whether this lies behind every viewer.
87
88
  */
88
89