@torrent-tv/proxy 2.97.30 → 2.97.31

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.97.30",
3
+ "version": "2.97.31",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -543,7 +543,7 @@ export async function startProxyServer({
543
543
  // ranges closer than a piece hold the same pieces as one.
544
544
  const pieceLength = Number(torrentPool.knownTorrent(map.sourceKey)?.pieceLength);
545
545
  const passStartedAt = performance.now();
546
- let segments = 0;
546
+ let rangeCount = 0;
547
547
  const converted = [];
548
548
  const demands = [];
549
549
  // What each output's next run would copy, asked of the swarm as one group
@@ -629,12 +629,18 @@ export async function startProxyServer({
629
629
  const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
630
630
  const convert = ranges => {
631
631
  const joined = joinedRangesOf(ranges, pieceLength);
632
- segments += 1;
632
+ rangeCount += joined.length;
633
633
  // What the segment reads is kept for the forecast in a table of its
634
634
  // own, whatever form the torrent is told it in.
635
635
  segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
636
- for (const [byteStart, byteEnd] of joined) converted.push({ ...demand,
637
- downloadInterval: { from: zone.owner.from, to: zone.owner.to }, byteStart, byteEnd });
636
+ // ONE ZONE FOR THE SEGMENT, its demand stated once for all the
637
+ // ranges it reads. The demand — level, priority, deadline, the map
638
+ // zone it belongs to — is this pass's; the ranges are the list kept
639
+ // for the segment, unchanged from pass to pass. A zone for each
640
+ // range copied the demand into every one: ten thousand objects a
641
+ // pass over a two-hour AVI, made again 21-61 times a minute on Home
642
+ // Assistant (r26, torrent-tv/meta#178).
643
+ converted.push({ ...demand, downloadInterval: { from: zone.owner.from, to: zone.owner.to }, ranges: joined });
638
644
  };
639
645
  // The bytes one segment needs are a fact of the file and the output, so
640
646
  // they are worked out once. A whole film is thousands of segments and
@@ -694,7 +700,7 @@ export async function startProxyServer({
694
700
  convert(input.ranges);
695
701
  }
696
702
  const result = expanded();
697
- notePass({ complete, segments, ranges: converted.length, zones: result.length, ms: performance.now() - passStartedAt });
703
+ notePass({ complete, segments: converted.length, ranges: rangeCount, zones: result.length, ms: performance.now() - passStartedAt });
698
704
  if (complete) resolvedPasses.set(fileKey, { ...pass, result });
699
705
  else resolvedPasses.delete(fileKey);
700
706
  return result;
@@ -63,6 +63,18 @@ export function deadlineOfMapZone(zone, now = Date.now()) {
63
63
  Number.isFinite(zone.withinSeconds) ? now + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY;
64
64
  }
65
65
 
66
+ /**
67
+ * The byte ranges a zone names: a segment's zone names all the ranges its
68
+ * segment reads, and states its demand once for them (`ranges`); any other
69
+ * zone names one (`byteStart`-`byteEnd`). Inclusive.
70
+ *
71
+ * @param {{ ranges?: Array<[number, number]>, byteStart?: number, byteEnd?: number }} zone
72
+ * @returns {Array<[number, number]>}
73
+ */
74
+ export function rangesOfZone(zone) {
75
+ return Array.isArray(zone.ranges) ? zone.ranges : [[zone.byteStart, zone.byteEnd]];
76
+ }
77
+
66
78
  /**
67
79
  * The zones with every repeat of the same bytes left out but the one the swarm
68
80
  * would rank first.
@@ -80,14 +92,9 @@ export function deadlineOfMapZone(zone, now = Date.now()) {
80
92
  * @returns {Zone[]} In their order, without the repeats.
81
93
  */
82
94
  export function withoutRepeatedZones(zones, now = Date.now()) {
83
- const keys = zones.map(zone => `${zone.byteStart}:${zone.byteEnd}`);
84
- const best = new Map();
85
- zones.forEach((zone, index) => {
86
- const kept = best.get(keys[index]);
87
- const band = { urgency: levelOfMapZone(zone), priority: priorityOfMapZone(zone), deadlineAt: deadlineOfMapZone(zone, now) };
88
- if (!kept || compareBands(band, kept.band) < 0) best.set(keys[index], { index, band });
89
- });
90
- return zones.filter((_zone, index) => best.get(keys[index]).index === index);
95
+ const bands = zones.map(zone => bandOf(zone, now));
96
+ const best = bestOfRepeats(zones.length, index => zones[index].byteStart, index => zones[index].byteEnd, index => bands[index]);
97
+ return zones.filter((_zone, index) => best.get(zones[index].byteStart).get(zones[index].byteEnd) === index);
91
98
  }
92
99
 
93
100
  /**
@@ -105,45 +112,103 @@ export function withoutRepeatedZones(zones, now = Date.now()) {
105
112
  * consecutive segments are consecutive bytes. A zone a metadata read states
106
113
  * keeps its own request and order, and is left as it is.
107
114
  *
108
- * @template {{ byteStart: number, byteEnd: number }} Zone
109
- * @param {Zone[]} zones
115
+ * A segment's zone states its demand once for all its ranges
116
+ * ({@link rangesOfZone}); each range is a part of its own here, so a repeat is
117
+ * found range by range, and the band and deadline every range is packed with
118
+ * are the zone's current ones.
119
+ *
120
+ * @param {Array<{ byteStart?: number, byteEnd?: number, ranges?: Array<[number, number]> }>} zones
110
121
  * @param {number} [now]
111
122
  * @returns {object[]}
112
123
  */
113
124
  export function coalescedZones(zones, now = Date.now()) {
125
+ // Every range of every zone is a part, kept in numbers rather than objects:
126
+ // a film's map is ten thousand of them (r26, torrent-tv/meta#178).
127
+ let count = 0;
128
+ for (const zone of zones) count += Array.isArray(zone.ranges) ? zone.ranges.length : 1;
129
+ const starts = new Float64Array(count);
130
+ const ends = new Float64Array(count);
131
+ const owners = new Int32Array(count);
132
+ const bands = zones.map(zone => bandOf(zone, now));
133
+ let at = 0;
134
+ zones.forEach((zone, index) => {
135
+ for (const [start, end] of rangesOfZone(zone)) {
136
+ starts[at] = start;
137
+ ends[at] = end;
138
+ owners[at] = index;
139
+ at += 1;
140
+ }
141
+ });
142
+ const best = bestOfRepeats(count, part => starts[part], part => ends[part], part => bands[owners[part]]);
114
143
  const kept = [];
144
+ /** Band key → the band and its parts, in the order the bands first appear. */
115
145
  const groups = new Map();
116
- for (const zone of withoutRepeatedZones(zones, now)) {
146
+ for (let part = 0; part < count; part++) {
147
+ if (best.get(starts[part]).get(ends[part]) !== part) continue;
148
+ const zone = zones[owners[part]];
117
149
  if (zone.requestId !== undefined || zone.downloadOnly || (Number.isSafeInteger(zone.order) && zone.order > 0)) {
118
150
  kept.push(zone);
119
151
  continue;
120
152
  }
121
- const urgency = levelOfMapZone(zone);
122
- const priority = priorityOfMapZone(zone);
123
- const key = `${urgency}:${priority}`;
153
+ const band = bands[owners[part]];
154
+ const key = band.urgency * 1000 + band.priority;
124
155
  let group = groups.get(key);
125
- if (!group) groups.set(key, group = { urgency, priority, parts: [] });
126
- group.parts.push({ start: zone.byteStart, end: zone.byteEnd, deadlineAt: deadlineOfMapZone(zone, now) });
156
+ if (!group) groups.set(key, group = { band, parts: [] });
157
+ group.parts.push(part);
127
158
  }
128
- for (const { urgency, priority, parts } of groups.values()) {
129
- parts.sort((left, right) => left.start - right.start);
130
- let window = null;
159
+ for (const { band: { urgency, priority }, parts } of groups.values()) {
160
+ parts.sort((left, right) => starts[left] - starts[right]);
161
+ let windowStart = 0;
162
+ let windowEnd = -1;
163
+ let windowDeadline = Number.POSITIVE_INFINITY;
164
+ let open = false;
131
165
  const close = () => {
132
- if (!window) return;
133
- kept.push({ byteStart: window.start, byteEnd: window.end, priority, urgent: urgency === Urgency.NEAR,
166
+ if (!open) return;
167
+ kept.push({ byteStart: windowStart, byteEnd: windowEnd, priority, urgent: urgency === Urgency.NEAR,
134
168
  behind: urgency === Urgency.BEHIND, deferred: urgency === Urgency.TAIL,
135
- ...(Number.isFinite(window.deadlineAt) ? { deadlineAt: window.deadlineAt } : {}) });
169
+ ...(Number.isFinite(windowDeadline) ? { deadlineAt: windowDeadline } : {}) });
136
170
  };
137
171
  for (const part of parts) {
138
- if (window && part.start <= window.end + 1) {
139
- window.end = Math.max(window.end, part.end);
140
- window.deadlineAt = Math.min(window.deadlineAt, part.deadlineAt);
172
+ const deadlineAt = bands[owners[part]].deadlineAt;
173
+ if (open && starts[part] <= windowEnd + 1) {
174
+ windowEnd = Math.max(windowEnd, ends[part]);
175
+ windowDeadline = Math.min(windowDeadline, deadlineAt);
141
176
  } else {
142
177
  close();
143
- window = { ...part };
178
+ open = true;
179
+ windowStart = starts[part];
180
+ windowEnd = ends[part];
181
+ windowDeadline = deadlineAt;
144
182
  }
145
183
  }
146
184
  close();
147
185
  }
148
186
  return kept;
149
187
  }
188
+
189
+ /** The band a zone's bytes are fetched in. */
190
+ function bandOf(zone, now) {
191
+ return { urgency: levelOfMapZone(zone), priority: priorityOfMapZone(zone), deadlineAt: deadlineOfMapZone(zone, now) };
192
+ }
193
+
194
+ /**
195
+ * Which of the parts naming each exact byte range is the best: by band
196
+ * ({@link compareBands}), the first of equals. Kept by the range's two numbers
197
+ * rather than a string made of them: making a key of each of a film's ten
198
+ * thousand ranges was most of what packing its map cost (r26,
199
+ * torrent-tv/meta#178).
200
+ *
201
+ * @returns {Map<number, Map<number, number>>} start → end → the best part.
202
+ */
203
+ function bestOfRepeats(count, startOf, endOf, bandOfPart) {
204
+ const best = new Map();
205
+ for (let part = 0; part < count; part++) {
206
+ const start = startOf(part);
207
+ const end = endOf(part);
208
+ let byEnd = best.get(start);
209
+ if (!byEnd) best.set(start, byEnd = new Map());
210
+ const kept = byEnd.get(end);
211
+ if (kept === undefined || compareBands(bandOfPart(part), bandOfPart(kept)) < 0) byEnd.set(end, part);
212
+ }
213
+ return best;
214
+ }
@@ -102,8 +102,11 @@ function merged(ranges) {
102
102
  * The map's byte zones with each output's next run input raised to its first
103
103
  * segment's place.
104
104
  *
105
- * @template {{ outputKey: string, index: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean, byteStart: number, byteEnd: number }} Zone
106
- * @param {Zone[]} zones - One or more per segment, as the map's pass made them.
105
+ * @template {{ outputKey: string, index: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean,
106
+ * ranges?: Array<[number, number]>, byteStart?: number, byteEnd?: number }} Zone
107
+ * @param {Zone[]} zones - One or more per segment, as the map's pass made
108
+ * them: a segment's zone names all its ranges (`ranges`), any other one
109
+ * range (`byteStart`-`byteEnd`).
107
110
  * @param {(outputKey: string) => number | null} targetBytesOf - What the
108
111
  * output's next run would be given to copy; nothing is raised without it.
109
112
  * @returns {Zone[]} In their order; raised zones are copies.
@@ -138,7 +141,7 @@ export function expandToTargetInputs(zones, targetBytesOf) {
138
141
  if (zone.index === run.first.index) {
139
142
  let ranges = held.get(zone.outputKey);
140
143
  if (!ranges) held.set(zone.outputKey, ranges = []);
141
- ranges.push([zone.byteStart, zone.byteEnd]);
144
+ ranges.push(...rangesOf(zone));
142
145
  } else if (zone.index > run.first.index) {
143
146
  const list = run.after.get(zone.index);
144
147
  if (list) list.push(zone);
@@ -153,7 +156,7 @@ export function expandToTargetInputs(zones, targetBytesOf) {
153
156
  // or not yet worked out, ends it.
154
157
  for (let index = run.first.index + 1; run.after.has(index); index++) {
155
158
  const list = run.after.get(index);
156
- const ranges = list.map(zone => [zone.byteStart, zone.byteEnd]);
159
+ const ranges = list.flatMap(rangesOf);
157
160
  if (holding.total + holding.adds(ranges) > run.target) break;
158
161
  holding.add(ranges);
159
162
  for (const zone of list) {
@@ -164,3 +167,8 @@ export function expandToTargetInputs(zones, targetBytesOf) {
164
167
  }
165
168
  return raised.size === 0 ? zones : zones.map(zone => raised.get(zone) ?? zone);
166
169
  }
170
+
171
+ /** The ranges a zone names, inclusive. */
172
+ function rangesOf(zone) {
173
+ return Array.isArray(zone.ranges) ? zone.ranges : [[zone.byteStart, zone.byteEnd]];
174
+ }