@torrent-tv/proxy 2.90.0 → 2.92.0

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.
Files changed (146) hide show
  1. package/bin/cli.js +5 -2
  2. package/package.json +18 -2
  3. package/routes/api/playback-plan/post.js +21 -4
  4. package/routes/api/sources/files/get.js +20 -28
  5. package/routes/api/sources/post.js +4 -2
  6. package/routes/api/sources/stats/get.js +4 -11
  7. package/routes/api/sources/viewer/post.js +16 -0
  8. package/routes/api/sources/warm/post.js +6 -215
  9. package/routes/api/subtitles/get.js +22 -125
  10. package/routes/api/subtitles/post.js +26 -0
  11. package/routes/api/transcode-sessions/post.js +24 -2
  12. package/routes/media/get.js +80 -0
  13. package/routes/stream/get.js +26 -94
  14. package/routes/transcode/audio-warm/get.js +9 -21
  15. package/routes/transcode/variant-warm/get.js +8 -14
  16. package/server.js +410 -46
  17. package/services/encode/AdmittedInput.js +136 -0
  18. package/services/encode/CushionReport.js +1 -76
  19. package/services/encode/EncodeInputs.js +142 -0
  20. package/services/encode/EncodeOrchestrator.js +4 -1
  21. package/services/encode/EncodeRun.js +13 -4
  22. package/services/encode/EncodeRuns.js +132 -52
  23. package/services/encode/InputFailures.js +37 -0
  24. package/services/encode/MatroskaInput.js +158 -0
  25. package/services/encode/OutputOpening.js +16 -64
  26. package/services/encode/Renditions.js +1 -17
  27. package/services/encode/admitted-command.js +60 -0
  28. package/services/encode/annex-b-input.js +37 -0
  29. package/services/encode/bit-payload.js +29 -0
  30. package/services/encode/output/OutputCatalog.js +1 -1
  31. package/services/encode/output/Timeline.js +11 -0
  32. package/services/encode/pcm-normalization.js +30 -0
  33. package/services/encode/piece-completeness.js +54 -1
  34. package/services/encode/quality/HostLoad.js +19 -73
  35. package/services/encode/quality/QualityOffer.js +1 -1
  36. package/services/encode/run-command.js +1 -589
  37. package/services/encode/segment-formats/mp4-boxes.js +3 -1
  38. package/services/encode/segment-formats/presentation-segment.js +114 -0
  39. package/services/media/ContainerOrchestrator.js +118 -48
  40. package/services/media/KeyframeTables.js +16 -115
  41. package/services/media/MediaReadRequests.js +120 -0
  42. package/services/media/ProbeRequests.js +135 -0
  43. package/services/media/SegmentInputs.js +48 -0
  44. package/services/media/SourceFile.js +6 -0
  45. package/services/media/SubtitleOrchestrator.js +190 -4
  46. package/services/media/await-plan.js +47 -0
  47. package/services/media/container/AsfContainer.js +217 -0
  48. package/services/media/container/AviContainer.js +343 -34
  49. package/services/media/container/CadencedPictureOrder.js +45 -0
  50. package/services/media/container/Container.js +7 -3
  51. package/services/media/container/ContainerFactory.js +10 -3
  52. package/services/media/container/FfprobeContainer.js +222 -0
  53. package/services/media/container/MatroskaContainer.js +72 -46
  54. package/services/media/container/Mp4Container.js +472 -139
  55. package/services/media/container/MpegPsContainer.js +281 -0
  56. package/services/media/container/MpegTsContainer.js +277 -0
  57. package/services/media/container/PacketIndex.js +270 -0
  58. package/services/media/container/PacketRecords.js +237 -0
  59. package/services/media/container/RetainedBytes.js +39 -0
  60. package/services/media/container/RetainedReads.js +51 -0
  61. package/services/media/container/ac3-frame.js +46 -0
  62. package/services/media/container/asf-packets.js +153 -0
  63. package/services/media/container/avc-picture-order.js +50 -0
  64. package/services/media/container/avi-open-dml.js +47 -0
  65. package/services/media/container/dts-frame.js +51 -0
  66. package/services/media/container/ebml-stream.js +12 -1
  67. package/services/media/container/ffprobe-record.js +45 -0
  68. package/services/media/container/flv-packet-ranges.js +27 -0
  69. package/services/media/container/following-packet-time.js +37 -0
  70. package/services/media/container/h264-configuration.js +183 -0
  71. package/services/media/container/h264-elementary-index.js +245 -0
  72. package/services/media/container/hevc-configuration.js +165 -0
  73. package/services/media/container/hevc-elementary-index.js +132 -0
  74. package/services/media/container/hevc-picture-order.js +66 -0
  75. package/services/media/container/latm-frame.js +99 -0
  76. package/services/media/container/matroska-packets.js +207 -0
  77. package/services/media/container/memory-unavailable.js +8 -0
  78. package/services/media/container/mp3-packet-dependencies.js +41 -0
  79. package/services/media/container/mp4-fragments.js +148 -0
  80. package/services/media/container/mpeg-audio-frame.js +19 -0
  81. package/services/media/container/mpeg-audio-reservoir.js +27 -0
  82. package/services/media/container/mpeg-elementary-index.js +196 -0
  83. package/services/media/container/mpeg-sequence-reader.js +69 -0
  84. package/services/media/container/mpeg-ts-packets.js +93 -0
  85. package/services/media/container/mpeg-video-configuration.js +50 -0
  86. package/services/media/container/mpeg4-picture-timing.js +108 -0
  87. package/services/media/container/pes-header.js +34 -0
  88. package/services/media/playback-declarations.js +57 -0
  89. package/services/media/playback-planner.js +72 -230
  90. package/services/media/probe-packets.js +61 -0
  91. package/services/media/read-playback-declarations.js +18 -0
  92. package/services/media/tracks/AudioTrack.js +9 -0
  93. package/services/media/tracks/TextSubtitleTrack.js +4 -1
  94. package/services/server/OutputLifecycle.js +2 -0
  95. package/services/server/SegmentServing.js +40 -33
  96. package/services/server/ViewerRequests.js +22 -41
  97. package/services/server/controllers/PlaybackController.js +4 -0
  98. package/services/server/controllers/SubtitleController.js +8 -29
  99. package/services/server/transcode-session-files.js +58 -99
  100. package/services/server/wire-outputs.js +58 -15
  101. package/services/storage/IndexMemory.js +85 -0
  102. package/services/storage/MachineBudget.js +27 -3
  103. package/services/storage/allowance.js +8 -1
  104. package/services/storage/piece-store/piece-disk-store.js +18 -3
  105. package/services/storage/piece-store/piece-lru.js +8 -4
  106. package/services/storage/piece-store/shared-piece-store.js +26 -11
  107. package/services/storage/segment-store/SegmentStore.js +49 -18
  108. package/services/storage/wire.js +7 -0
  109. package/services/torrent/Contents.js +52 -20
  110. package/services/torrent/demand/DemandRegister.js +13 -4
  111. package/services/torrent/demand/Window.js +13 -2
  112. package/services/torrent/demand-progress.js +25 -0
  113. package/services/torrent/download/DeadlineScheduler.js +167 -0
  114. package/services/torrent/download/FutureDownload.js +166 -0
  115. package/services/torrent/download/SwarmSelection.js +30 -59
  116. package/services/torrent/download/registry.js +45 -5
  117. package/services/torrent/download/withdraw-claim.js +2 -0
  118. package/services/torrent/torrent-pool.js +39 -600
  119. package/services/torrent/worker/channel.js +8 -1
  120. package/services/torrent/worker/client.js +74 -72
  121. package/services/torrent/worker/files-in-use.js +9 -0
  122. package/services/torrent/worker/handle-state.js +12 -0
  123. package/services/torrent/worker/held-bytes.js +82 -99
  124. package/services/torrent/worker/held-file-bytes.js +17 -0
  125. package/services/torrent/worker/piece-reader.js +67 -683
  126. package/services/torrent/worker/pool-adapter.js +37 -123
  127. package/services/torrent/worker/protocol.js +6 -17
  128. package/services/torrent/worker/whole-file-stats.js +2 -0
  129. package/services/torrent/worker/worker.js +86 -156
  130. package/services/transport/data-channel-handler.js +56 -13
  131. package/services/transport/tunnel-client.js +111 -20
  132. package/services/viewer/DownloadMaps.js +168 -0
  133. package/services/viewer/NativeSourceMap.js +33 -0
  134. package/services/viewer/PriorityMap.js +49 -40
  135. package/services/viewer/PriorityOrchestrator.js +42 -13
  136. package/services/viewer/SourcePreparation.js +278 -0
  137. package/services/viewer/Viewer.js +11 -0
  138. package/services/viewer/Viewers.js +109 -2
  139. package/services/viewer/playback-readiness.js +50 -63
  140. package/services/viewer/segment-demands.js +37 -0
  141. package/services/viewer/urgent-output-ready.js +26 -0
  142. package/services/encode/run-inputs.js +0 -53
  143. package/services/media/keyframe-probe.js +0 -157
  144. package/services/media/media-info-probe.js +0 -93
  145. package/services/torrent/worker/background-fill.js +0 -213
  146. package/services/torrent/worker/resume-warm.js +0 -107
@@ -0,0 +1,166 @@
1
+ import { setImmediate as nextTurn } from "node:timers/promises";
2
+ import { downloadCandidates, dispatchDownloadCandidates, peerCanServe, peerRequestCapacity, copyRequestArrival } from "./DeadlineScheduler.js";
3
+
4
+ const BLOCK_BYTES = 16 * 1024;
5
+
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
+ 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);
39
+ }
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
+ }
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; }
98
+ }
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);
113
+ }
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));
120
+ }
121
+ }
122
+ if (++steps % 256 === 0) {
123
+ await nextTurn();
124
+ if (!isCurrent()) return new Map();
125
+ }
126
+ }
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);
153
+ }
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);
161
+ }
162
+ at += missing / speed * 1000;
163
+ state.complete = true;
164
+ copy.arrivals.set(piece, at);
165
+ }
166
+ }
@@ -23,7 +23,6 @@
23
23
  */
24
24
 
25
25
  import {
26
- isConditional,
27
26
  piecesOf,
28
27
  selectionPriority,
29
28
  Urgency,
@@ -36,8 +35,6 @@ export class SwarmSelection {
36
35
  #register;
37
36
  /** What was last stated to the library, so a restatement can be a no-op. */
38
37
  #stated = new Map();
39
- /** Pieces this instance marked for displacement, so it clears only its own. */
40
- #displacing = null;
41
38
  /** Claimants whose windows are currently protected in memory. */
42
39
  #protectedInMemory = new Set();
43
40
  #findStore;
@@ -72,11 +69,8 @@ export class SwarmSelection {
72
69
  * shared and the question is not a per-torrent one.
73
70
  * @returns {{ stated: number, withdrawn: number }}
74
71
  */
75
- reconcile({ speculativeAllowed = true } = {}) {
76
- const levels = this.#register.levelsToState(
77
- (window) => this.#isSatisfied(window),
78
- speculativeAllowed
79
- );
72
+ reconcile() {
73
+ const levels = [...new Set(this.#register.windows().map(window => window.urgency))];
80
74
  /** @type {Map<string, { from: number, to: number, priority: number }>} */
81
75
  const wanted = new Map();
82
76
  for (const urgency of levels) {
@@ -102,6 +96,8 @@ export class SwarmSelection {
102
96
  withdrawn += 1;
103
97
  }
104
98
 
99
+ this.#cancelUnwanted(wanted.values());
100
+
105
101
  let stated = 0;
106
102
  for (const [key, range] of wanted) {
107
103
  // Re-stated when the library has dropped it, even though this instance
@@ -114,7 +110,6 @@ export class SwarmSelection {
114
110
  stated += 1;
115
111
  }
116
112
 
117
- this.#markDisplacement();
118
113
  this.#projectIntoMemory();
119
114
  return { stated, withdrawn };
120
115
  }
@@ -125,7 +120,7 @@ export class SwarmSelection {
125
120
  this.#deselect(range);
126
121
  }
127
122
  this.#stated.clear();
128
- this.#clearDisplacement();
123
+ this.#cancelUnwanted([]);
129
124
  const store = this.#findStore(this.#torrent);
130
125
  for (const claimant of this.#protectedInMemory) {
131
126
  store?.releaseProtection?.(claimant);
@@ -133,6 +128,27 @@ export class SwarmSelection {
133
128
  this.#protectedInMemory.clear();
134
129
  }
135
130
 
131
+ /** Cancel removed blocks in this pass; the wire callback releases reservations. */
132
+ #cancelUnwanted(ranges) {
133
+ const wanted = [...ranges];
134
+ for (const wire of this.#torrent.wires ?? []) {
135
+ for (const request of [...(wire.requests ?? [])]) {
136
+ if (wanted.some((range) => request.piece >= range.from && request.piece <= range.to)) continue;
137
+ if (typeof wire.cancel !== "function") continue;
138
+ try {
139
+ wire.cancel(request.piece, request.offset, request.length);
140
+ this.#torrent.emit?.("download-request-cancelled", {
141
+ piece: request.piece, offset: request.offset, length: request.length, reason: "demand-withdrawn"
142
+ });
143
+ } catch (error) {
144
+ this.#torrent.emit?.("download-request-cancel-failed", {
145
+ piece: request.piece, offset: request.offset, length: request.length, error: error?.message ?? String(error)
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+
136
152
  /**
137
153
  * What is stated right now.
138
154
  *
@@ -171,30 +187,6 @@ export class SwarmSelection {
171
187
  *
172
188
  * @returns {void}
173
189
  */
174
- #markDisplacement() {
175
- const blocked = this.#register
176
- .at(Urgency.BLOCKED)
177
- .map((window) => this.#piecesFor(window))
178
- .filter((range) => range !== null);
179
- if (blocked.length === 0) {
180
- this.#clearDisplacement();
181
- return;
182
- }
183
- const nearest = Math.min(...blocked.map((range) => range.from));
184
- const furthest = Math.max(...blocked.map((range) => range.to));
185
- if (this.#displacing && this.#displacing.from === nearest && this.#displacing.to === furthest) {
186
- return;
187
- }
188
- this.#clearDisplacement();
189
- try {
190
- this.#torrent.critical?.(nearest, furthest);
191
- this.#displacing = { from: nearest, to: furthest };
192
- } catch {
193
- // silent-ok: displacement is an optimisation, and a torrent being torn
194
- // down is not worth failing a read over.
195
- this.#displacing = null;
196
- }
197
- }
198
190
 
199
191
  /**
200
192
  * Tell the piece store which bytes will be read soon, from the same stated
@@ -234,7 +226,7 @@ export class SwarmSelection {
234
226
  return;
235
227
  }
236
228
  const holding = new Set();
237
- for (const urgency of [Urgency.BLOCKED, Urgency.NEAR]) {
229
+ for (const urgency of [...new Set(this.#register.windows().map(window => window.urgency))]) {
238
230
  for (const window of this.#register.at(urgency)) {
239
231
  const range = this.#piecesFor(window);
240
232
  if (!range) {
@@ -243,7 +235,7 @@ export class SwarmSelection {
243
235
  // The level goes with the range: it is what eviction compares when
244
236
  // everything resident is wanted by somebody, and dropping it here is
245
237
  // what left the store choosing by recency alone.
246
- store.protectRange(window.claimant, range.from, range.to, window.urgency);
238
+ store.protectRange(window.claimant, range.from, range.to, window.urgency, window.deadlineAt, window.priority);
247
239
  holding.add(window.claimant);
248
240
  }
249
241
  }
@@ -255,17 +247,6 @@ export class SwarmSelection {
255
247
  this.#protectedInMemory = holding;
256
248
  }
257
249
 
258
- /** @returns {void} */
259
- #clearDisplacement() {
260
- if (!this.#displacing || !Array.isArray(this.#torrent._critical)) {
261
- this.#displacing = null;
262
- return;
263
- }
264
- for (let index = this.#displacing.from; index <= this.#displacing.to; index += 1) {
265
- this.#torrent._critical[index] = false;
266
- }
267
- this.#displacing = null;
268
- }
269
250
 
270
251
  /**
271
252
  * The pieces a window covers, or null when the file or the torrent cannot
@@ -363,24 +344,14 @@ export class SwarmSelection {
363
344
  *
364
345
  * @returns {string}
365
346
  */
366
- describe(speculativeAllowed = true) {
367
- const stating = this.#register.levelsToState(
368
- (window) => this.#isSatisfied(window),
369
- speculativeAllowed
370
- );
371
- const speculative = stating.filter((urgency) => isConditional(urgency)).map(urgencyName);
347
+ describe() {
372
348
  const needs = this.#register
373
349
  .windows()
374
350
  .map((window) => `${urgencyName(window.urgency)}:${window.claimant}`);
375
351
  return (
376
352
  `download: ${this.#stated.size} instruction(s) to the swarm from ` +
377
353
  `${this.#register.size} stated need(s) [${needs.join(" ")}]` +
378
- (speculative.length > 0
379
- ? `; ${speculative.join(" and ")} also stated — nothing urgent is missing`
380
- : "; nothing speculative is stated — something urgent is still missing") +
381
- (this.#displacing
382
- ? `; pieces ${this.#displacing.from}-${this.#displacing.to} may be taken from slow peers`
383
- : "")
354
+ "; peer requests ordered by deadline and priority"
384
355
  );
385
356
  }
386
357
  }
@@ -17,11 +17,31 @@
17
17
 
18
18
  import { DemandRegister } from "../demand/DemandRegister.js";
19
19
  import { SwarmSelection } from "./SwarmSelection.js";
20
+ import { DeadlineScheduler } from "./DeadlineScheduler.js";
21
+ import { futureDownload } from "./FutureDownload.js";
20
22
 
21
23
  /** @type {WeakMap<object, { register: DemandRegister, selection: SwarmSelection }>} */
22
24
  const byTorrent = new WeakMap();
23
25
  /** @type {Set<{ register: DemandRegister, selection: SwarmSelection }>} */
24
26
  const live = new Set();
27
+ const scheduler = new DeadlineScheduler({ entries: () => live });
28
+ let futurePending = null;
29
+ let publishingSelections = false;
30
+
31
+ /** Concurrent file reports share one snapshot of every live download map. */
32
+ export function forecastDownloads() {
33
+ if (!futurePending) {
34
+ const entries = [...live];
35
+ const revisions = entries.map(entry => entry.register.revision);
36
+ const withdrawals = entries.map(entry => entry.withdrawalRevision);
37
+ const isCurrent = () => entries.length === live.size && !entries.some((entry, index) =>
38
+ !live.has(entry) || entry.register.revision !== revisions[index] ||
39
+ entry.withdrawalRevision !== withdrawals[index]);
40
+ futurePending = futureDownload(entries, { isCurrent }).then(result =>
41
+ isCurrent() ? result : new Map()).finally(() => { futurePending = null; });
42
+ }
43
+ return futurePending;
44
+ }
25
45
 
26
46
  /**
27
47
  * The register and selection for a torrent, made on first use.
@@ -35,9 +55,19 @@ export function demandFor(torrent) {
35
55
  return held;
36
56
  }
37
57
  const register = new DemandRegister();
38
- const entry = { register, selection: new SwarmSelection({ torrent, register }) };
58
+ 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 = () => {
62
+ if (!publishingSelections) scheduler.reconcile();
63
+ };
64
+ }
39
65
  byTorrent.set(torrent, entry);
40
66
  live.add(entry);
67
+ entry.onBytesChanged = () => reconcileAll();
68
+ entry.onWithdrawn = () => { entry.withdrawalRevision++; reconcileAll(); };
69
+ torrent.on?.("verified", entry.onBytesChanged);
70
+ torrent.on?.("piece-withdrawn", entry.onWithdrawn);
41
71
  return entry;
42
72
  }
43
73
 
@@ -74,6 +104,9 @@ export function forgetTorrent(torrent) {
74
104
  }
75
105
  held.selection.releaseAll();
76
106
  held.register.clear();
107
+ torrent.removeListener?.("verified", held.onBytesChanged);
108
+ torrent.removeListener?.("piece-withdrawn", held.onWithdrawn);
109
+ if (held.previousWireUpdate) torrent._updateWire = held.previousWireUpdate;
77
110
  byTorrent.delete(torrent);
78
111
  live.delete(held);
79
112
  }
@@ -89,14 +122,21 @@ export function forgetTorrent(torrent) {
89
122
  * @returns {{ torrents: number, speculativeAllowed: boolean, stated: number, withdrawn: number }}
90
123
  */
91
124
  export function reconcileAll() {
125
+ if (publishingSelections) return { torrents: live.size, speculativeAllowed: false, stated: 0, withdrawn: 0 };
92
126
  const entries = [...live];
93
127
  const speculativeAllowed = !entries.some((entry) => entry.selection.hasUrgentMissing());
94
128
  let stated = 0;
95
129
  let withdrawn = 0;
96
- for (const entry of entries) {
97
- const result = entry.selection.reconcile({ speculativeAllowed });
98
- stated += result.stated;
99
- withdrawn += result.withdrawn;
130
+ publishingSelections = true;
131
+ try {
132
+ for (const entry of entries) {
133
+ const result = entry.selection.reconcile({ speculativeAllowed, deadlineDriven: !!entry.previousWireUpdate });
134
+ stated += result.stated;
135
+ withdrawn += result.withdrawn;
136
+ }
137
+ } finally {
138
+ publishingSelections = false;
100
139
  }
140
+ scheduler.reconcile();
101
141
  return { torrents: entries.length, speculativeAllowed, stated, withdrawn };
102
142
  }
@@ -76,5 +76,7 @@ export function withdrawClaim({ index, files, torrent, warn = () => undefined })
76
76
  );
77
77
  return "refused";
78
78
  }
79
+ try { owner.emit?.("piece-withdrawn", index); }
80
+ catch (error) { warn(`piece withdrawal listener failed: ${error?.message ?? error}`); }
79
81
  return "withdrawn";
80
82
  }