@torrent-tv/proxy 2.107.27 → 2.107.29

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.107.27",
3
+ "version": "2.107.29",
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
@@ -1023,7 +1023,7 @@ export async function startProxyServer({
1023
1023
  },
1024
1024
  // `timing`, when given, is told how long each read of a file waited behind
1025
1025
  // the reads asked before it and how long it took (torrent-tv/meta#166).
1026
- resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null) => {
1026
+ resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null, signal = null) => {
1027
1027
  const grid = output.timeline?.published ?? output.timeline?.boundaries;
1028
1028
  const from = grid?.[fromIndex], to = grid?.[toIndex + 1];
1029
1029
  if (!Number.isFinite(from) || !(to > from)) return { kind: "terminal", reason: "output-interval-not-declared" };
@@ -1043,12 +1043,17 @@ export async function startProxyServer({
1043
1043
  for (const [fileIndex, choices] of selected) {
1044
1044
  const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex });
1045
1045
  if (!params) return { kind: "needs-source" };
1046
+ if (isCurrent) params.isCurrent = isCurrent;
1047
+ if (signal) params.signal = signal;
1046
1048
  if (timing) {
1047
1049
  params.walkAccount = timing.walk ??= { lockMs: 0, reads: 0, bytes: 0 };
1048
- params.onTimed = (_statement, waitedMs, readMs) => {
1050
+ params.onTimed = (statement, waitedMs, readMs) => {
1049
1051
  timing.reads += 1;
1050
1052
  timing.queuedMs += waitedMs;
1051
1053
  timing.readMs += readMs;
1054
+ const measured = timing.readsByStatement[statement] ??= { queuedMs: 0, readMs: 0 };
1055
+ measured.queuedMs += waitedMs;
1056
+ measured.readMs += readMs;
1052
1057
  };
1053
1058
  }
1054
1059
  const tracks = await containerOrchestrator.inspect(params, "tracks");
@@ -1070,7 +1075,6 @@ export async function startProxyServer({
1070
1075
  if (media.kind !== "result") return media;
1071
1076
  const timeShiftSeconds = Number(media.value?.startTimeSeconds) || 0;
1072
1077
  const sourceInterval = output.timeline.sourceInterval(fromIndex, toIndex, timeShiftSeconds);
1073
- if (isCurrent) params.isCurrent = isCurrent;
1074
1078
  params.packetInterval = { ...sourceInterval,
1075
1079
  trackIds: wanted.map(track => track.trackNumber),
1076
1080
  modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
@@ -115,10 +115,10 @@ export class EncodeInputs {
115
115
  }
116
116
 
117
117
  /** The container's statement of an interval, its time added to the request's account. */
118
- async #resolveTimed(timing, output, from, to, isCurrent = null) {
118
+ async #resolveTimed(timing, output, from, to, isCurrent = null, signal = null) {
119
119
  const startedAt = this.#now();
120
120
  try {
121
- return await this.#resolve(output, from, to, timing, isCurrent);
121
+ return await this.#resolve(output, from, to, timing, isCurrent, signal);
122
122
  } finally {
123
123
  timing.resolutions += 1;
124
124
  timing.resolveMs += this.#now() - startedAt;
@@ -208,6 +208,7 @@ export class EncodeInputs {
208
208
  : "withdrawn: no wanted window covers it");
209
209
  this.#requests.delete(key);
210
210
  this.#wanted.delete(key);
211
+ request.controller?.abort();
211
212
  request.result?.release?.();
212
213
  }
213
214
  this.#retryMemory();
@@ -233,6 +234,7 @@ export class EncodeInputs {
233
234
  this.#note(request, "withdrawn: the plan places no run there");
234
235
  this.#requests.delete(key);
235
236
  this.#wanted.delete(key);
237
+ request.controller?.abort();
236
238
  // A copy already answered may still be copying its stretch; released,
237
239
  // the stretch is dropped when its read returns.
238
240
  request.result?.release?.();
@@ -335,6 +337,7 @@ export class EncodeInputs {
335
337
  if (request.output !== output) continue;
336
338
  this.#requests.delete(key);
337
339
  this.#wanted.delete(key);
340
+ request.controller?.abort();
338
341
  request.result?.release?.();
339
342
  }
340
343
  this.#shared.forget(output);
@@ -404,6 +407,7 @@ export class EncodeInputs {
404
407
  }
405
408
 
406
409
  #prepare(request) {
410
+ request.controller ??= new AbortController();
407
411
  request.pending = true;
408
412
  request.revision = this.#revision;
409
413
  request.memoryRevision = this.#memoryRevision;
@@ -413,7 +417,7 @@ export class EncodeInputs {
413
417
  // field 2026-10-10, a picture's request took 38 s before its copy began and
414
418
  // nothing said on what (torrent-tv/meta#166).
415
419
  request.timing ??= { askedAt: this.#now(), attempts: 0, resolutions: 0, resolveMs: 0,
416
- reads: 0, queuedMs: 0, readMs: 0, heldMs: 0, budgetMs: 0 };
420
+ reads: 0, queuedMs: 0, readMs: 0, readsByStatement: {}, heldMs: 0, budgetMs: 0 };
417
421
  request.timing.attempts += 1;
418
422
  this.#observe(request, "attempt", { request: request.key, attempt: request.timing.attempts });
419
423
  request.promise = (async () => {
@@ -421,7 +425,7 @@ export class EncodeInputs {
421
425
  // packet walk, left to run, held the file's reads and the piece the
422
426
  // viewer had seeked to waited 16.9 s behind it (r59, torrent-tv/meta#178).
423
427
  const resolved = await this.#resolveTimed(request.timing, request.output, request.from, request.from,
424
- () => this.#requests.get(request.key) === request);
428
+ () => this.#requests.get(request.key) === request && !request.controller.signal.aborted, request.controller.signal);
425
429
  if (resolved.kind === "result") this.#original.set(request.output, resolved.sources?.every(source => source.input.original === true) === true);
426
430
  if (this.#requests.get(request.key) !== request) return;
427
431
  let result;
@@ -742,9 +746,12 @@ function awaitedIn(waiting, arrived) {
742
746
  */
743
747
  function describeTiming(timing, now) {
744
748
  if (!timing) return "";
749
+ const statements = Object.entries(timing.readsByStatement ?? {}).map(([statement, measured]) =>
750
+ `${statement} queued ${measured.queuedMs}ms/read ${measured.readMs}ms`).join(", ");
745
751
  return `; asked ${Math.max(0, now - timing.askedAt)}ms ago over ${timing.attempts} attempt(s): ` +
746
752
  `${timing.resolutions} resolution(s) ${timing.resolveMs}ms ` +
747
753
  `(${timing.reads} file read(s): queued ${timing.queuedMs}ms, reading ${timing.readMs}ms), ` +
754
+ (statements ? `by statement: ${statements}, ` : "") +
748
755
  `held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms` +
749
756
  // Where a packet input's walk spent its read: waiting for the same
750
757
  // track's earlier walks, or reading the file (r62, torrent-tv/meta#178).
@@ -146,7 +146,7 @@ export class ContainerOrchestrator {
146
146
  const key = `${params.sourceKey}:${params.fileIndex}`;
147
147
  let lifetime = this.#lifetimes.get(key);
148
148
  if (!lifetime) this.#lifetimes.set(key, lifetime = {});
149
- const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false;
149
+ const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false && params.signal?.aborted !== true;
150
150
  // Reads of one file run one after another, so an answer can wait for every
151
151
  // read asked before it. Told to whoever asked, apart from the read itself,
152
152
  // because the two call for different remedies (torrent-tv/meta#166).
@@ -237,9 +237,10 @@ export class ContainerOrchestrator {
237
237
  value = await container.readSourceRanges(params.packetInterval);
238
238
  if (value.kind !== "result") return { ...value, requestId };
239
239
  } else if (statement === "packets") {
240
- value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, account: params.walkAccount });
240
+ value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, signal: params.signal, account: params.walkAccount });
241
241
  if (value === null) return { kind: "terminal", reason: "request-obsolete", requestId };
242
- await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
242
+ const readRange = (start, end) => container.readRange(start, end, { signal: params.signal });
243
+ await value?.prepareAudioDependencies?.(params.packetInterval, readRange);
243
244
  } else if (statement === "chapters") {
244
245
  value = await container.readChapters({ read: params.readHeld ?? params.readRange,
245
246
  isCurrent: params.isCurrent,
@@ -252,7 +253,7 @@ export class ContainerOrchestrator {
252
253
  }
253
254
  return { kind: "result", value, requestId };
254
255
  } catch (error) {
255
- if (statement === "chapters" && params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
256
+ if (params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
256
257
  if (error instanceof ChapterUnsupported) return { kind: "terminal", reason: "chapter-not-supported", chapterReason: error.message, requestId };
257
258
  if (error instanceof IndexMemoryUnavailable) {
258
259
  const result = { kind: "needs-memory", bytes: error.bytes, requestId };
@@ -42,11 +42,11 @@ export class AsfContainer extends Container {
42
42
  }
43
43
  static detect(head) { return head.length >= 16 && head.subarray(0, 16).toString("hex") === HEADER; }
44
44
 
45
- async #readHeader() {
45
+ async #readHeader(readRange = this.readRange) {
46
46
  if (this.#info) return;
47
47
  this.#declarations ??= new RetainedReads(this.packetMemory);
48
48
  if (!this.#header) {
49
- const bytes = await this.readRange(0, 29);
49
+ const bytes = await readRange(0, 29);
50
50
  if (!AsfContainer.detect(bytes) || bytes[28] !== 1 || bytes[29] !== 2) throw new Error("ASF header is invalid.");
51
51
  const size = Number(bytes.readBigUInt64LE(16));
52
52
  const count = bytes.readUInt32LE(24);
@@ -56,7 +56,7 @@ export class AsfContainer extends Container {
56
56
  this.#header = { size, count };
57
57
  }
58
58
  while (this.#objects.length < this.#header.count) {
59
- const head = await this.readRange(this.#offset, this.#offset + 23);
59
+ const head = await readRange(this.#offset, this.#offset + 23);
60
60
  const size = Number(head.readBigUInt64LE(16));
61
61
  if (!Number.isSafeInteger(size) || size < 24 || this.#offset + size > this.#header.size) {
62
62
  throw new Error("ASF child object exceeds its header.");
@@ -64,7 +64,7 @@ export class AsfContainer extends Container {
64
64
  const guid = head.subarray(0, 16).toString("hex");
65
65
  if (ENCRYPTION.has(guid)) throw new Error("ASF media is encrypted.");
66
66
  const bytes = [FILE, STREAM, EXTENSION, EXTENDED_STREAM, LANGUAGES].includes(guid)
67
- ? await this.#declarations.read(this.#offset + 24, this.#offset + size - 1, this.readRange) : null;
67
+ ? await this.#declarations.read(this.#offset + 24, this.#offset + size - 1, readRange) : null;
68
68
  this.#objects.push({ guid, bytes });
69
69
  this.#offset += size;
70
70
  }
@@ -212,15 +212,21 @@ export class AsfContainer extends Container {
212
212
  const index = await this.readPacketIndex();
213
213
  return { times: index.keyframesOf(track.trackNumber), tolerance: 0 };
214
214
  }
215
- async readPacketIndex(interval) {
216
- await this.#readHeader();
215
+ async readPacketIndex(interval, { isCurrent, signal } = {}) {
216
+ const readRange = signal ? (start, end) => this.readRange(start, end, { signal }) : this.readRange;
217
+ if (isCurrent?.() === false) return null;
218
+ await this.#readHeader(readRange);
219
+ if (isCurrent?.() === false) return null;
217
220
  if (!this.#packets && Number.isFinite(interval?.from) && Number.isFinite(interval?.to) && interval.to > interval.from) {
218
- const window = await this.#readWindow(interval);
221
+ const window = await this.#readWindow(interval, isCurrent, readRange);
222
+ if (isCurrent?.() === false) return null;
219
223
  if (window) return window;
220
224
  }
225
+ if (isCurrent?.() === false) return null;
221
226
  if (!this.#packets) {
222
- const index = await readAsfPackets({ readRange: this.readRange, fileSize: this.fileSize, tracks: this.#tracks,
223
- info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory });
227
+ const index = await readAsfPackets({ readRange, fileSize: this.fileSize, tracks: this.#tracks,
228
+ info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory, isCurrent });
229
+ if (index === null) return null;
224
230
  const timeline = this.#tracks.find(track => track.type === "video") ?? this.#tracks.find(track => track.type === "audio");
225
231
  const bounds = timeline ? index.boundsOf(timeline.trackNumber) : null;
226
232
  if (bounds) {
@@ -246,17 +252,21 @@ export class AsfContainer extends Container {
246
252
  * interval, is read again from twice as far back, and from the first packet
247
253
  * at the latest; which it reaches is checked on what it read, not assumed.
248
254
  */
249
- async #readWindow(interval) {
250
- const data = await this.#dataLayout();
255
+ async #readWindow(interval, isCurrent, readRange = this.readRange) {
256
+ const data = await this.#dataLayout(readRange);
257
+ if (isCurrent?.() === false) return null;
251
258
  if (!data) return null;
252
259
  const requested = this.#tracks.filter(track => ["video", "audio"].includes(track.type) &&
253
260
  (!interval.trackIds?.length || interval.trackIds.includes(track.trackNumber)));
254
261
  if (!requested.length) return null;
255
262
  const info = { ...this.#info, durationSeconds: this.#duration };
256
263
  for (let lead = this.#info.prerollSeconds + WINDOW_LEAD_SECONDS; ; lead *= 2) {
264
+ if (isCurrent?.() === false) return null;
257
265
  const sentFrom = interval.from - lead;
258
- const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS);
259
- const window = await readAsfWindow({ readRange: this.readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom });
266
+ const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent, readRange);
267
+ if (isCurrent?.() === false) return null;
268
+ const window = await readAsfWindow({ readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom, isCurrent });
269
+ if (isCurrent?.() === false) return null;
260
270
  if (window.startCovered || first === 0) return window.index;
261
271
  }
262
272
  }
@@ -266,9 +276,9 @@ export class AsfContainer extends Container {
266
276
  * the format requires it (the File Properties state one length as both the
267
277
  * least and the most), and a packet is then found by its number.
268
278
  */
269
- async #dataLayout() {
279
+ async #dataLayout(readRange = this.readRange) {
270
280
  if (this.#data === null) {
271
- const data = await readAsfData({ readRange: this.readRange, fileSize: this.fileSize, info: this.#info });
281
+ const data = await readAsfData({ readRange, fileSize: this.fileSize, info: this.#info });
272
282
  const length = this.#info.packetLength;
273
283
  const fixed = length > 0 && this.#info.minimumPacketLength === length;
274
284
  const whole = fixed ? Math.floor((data.end - data.start) / length) : 0;
@@ -291,14 +301,16 @@ export class AsfContainer extends Container {
291
301
  * bytes the window from it to `until` is estimated to take, so the swarm is
292
302
  * asked for the packet and the window it begins at once.
293
303
  */
294
- async #packetSentBefore(seconds, data, until) {
295
- let low = 0, high = data.count, lowSent = await this.#sentAt(0, data), highSent = this.#duration;
304
+ async #packetSentBefore(seconds, data, until, isCurrent, readRange = this.readRange) {
305
+ if (isCurrent?.() === false) return null;
306
+ let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent, readRange), highSent = this.#duration;
307
+ if (isCurrent?.() === false) return null;
296
308
  if (lowSent > seconds) return 0;
297
309
  // Past the last packet nothing is sent before the film ends; the last
298
310
  // packet itself is read only when the search needs it.
299
311
  if (!(highSent > seconds)) {
300
312
  high = data.count - 1;
301
- highSent = await this.#sentAt(high, data);
313
+ highSent = await this.#sentAt(high, data, 1, isCurrent, readRange);
302
314
  if (highSent <= seconds) return high;
303
315
  }
304
316
  for (const [number, at] of this.#sent) {
@@ -307,12 +319,14 @@ export class AsfContainer extends Container {
307
319
  else { high = number; highSent = at; }
308
320
  }
309
321
  for (let guesses = 0; high - low > 1 && seconds - lowSent > WINDOW_SEARCH_SECONDS; guesses++) {
322
+ if (isCurrent?.() === false) return null;
310
323
  const perSecond = (high - low) / (highSent - lowSent);
311
324
  const aim = seconds - WINDOW_SEARCH_SECONDS / 2;
312
325
  const guess = guesses >= 3 || !(perSecond > 0) ? Math.floor((low + high) / 2) : low + Math.floor((aim - lowSent) * perSecond);
313
326
  const probe = Math.min(high - 1, Math.max(low + 1, guess));
314
327
  const window = perSecond > 0 ? Math.ceil((until - aim) * perSecond) : 1;
315
- const at = await this.#sentAt(probe, data, Math.max(1, window));
328
+ const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent, readRange);
329
+ if (isCurrent?.() === false) return null;
316
330
  if (at <= seconds) { low = probe; lowSent = at; }
317
331
  else { high = probe; highSent = at; }
318
332
  }
@@ -323,16 +337,17 @@ export class AsfContainer extends Container {
323
337
  * When packet `number` is sent. A packet that is not held is said as
324
338
  * missing with the `packets` packets from it.
325
339
  */
326
- async #sentAt(number, data, packets = 1) {
340
+ async #sentAt(number, data, packets = 1, isCurrent, readRange = this.readRange) {
341
+ if (isCurrent?.() === false) return null;
327
342
  if (!this.#sent.has(number)) {
328
343
  const at = data.start + number * data.length;
329
344
  try {
330
- this.#sent.set(number, await readAsfPacketTime({ readRange: this.readRange, at, end: data.end, info: this.#info }));
345
+ this.#sent.set(number, await readAsfPacketTime({ readRange, at, end: data.end, info: this.#info }));
331
346
  } catch (error) {
332
347
  if (!isUnavailable(error) || packets <= 1) throw error;
333
348
  throw new BytesUnavailable(at, Math.min(data.start + data.count * data.length, at + packets * data.length) - 1, 0);
334
349
  }
335
350
  }
336
- return this.#sent.get(number);
351
+ return isCurrent?.() === false ? null : this.#sent.get(number);
337
352
  }
338
353
  }
@@ -54,7 +54,7 @@ export class Container {
54
54
  * the medium — a torrent's piece length. Absent, an element is read in one go.
55
55
  *
56
56
  * @param {object} params
57
- * @param {(start:number,end:number)=>Promise<Buffer|null>} [params.readRange]
57
+ * @param {(start:number,end:number,options?:{signal?:AbortSignal})=>Promise<Buffer|null>} [params.readRange]
58
58
  * @param {number} [params.fileSize]
59
59
  * @param {string} [params.label]
60
60
  * @param {number} [params.portionBytes]
@@ -20,7 +20,7 @@ export async function readAsfData({ readRange, fileSize, info }) {
20
20
  }
21
21
 
22
22
  /** ASF payload addresses, including fragmented and compressed media objects. */
23
- export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {} }) {
23
+ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {}, isCurrent }) {
24
24
  if (!state.initialized) {
25
25
  const data = await readAsfData({ readRange, fileSize, info });
26
26
  Object.assign(state, { initialized: true, at: data.start, end: data.end, count: data.count, packets: 0,
@@ -30,6 +30,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
30
30
  const { end, count, frames, assembling } = state;
31
31
  const declared = new Map(tracks.map(track => [track.trackNumber, track]));
32
32
  while (at < end && (!count || packets < count)) {
33
+ if (isCurrent?.() === false) return null;
33
34
  const previousObjects = new Map([...assembling].map(([id, object]) => [id, { ...object, ranges: [...object.ranges] }]));
34
35
  const previousLengths = new Map([...frames].map(([id, stream]) => [id, stream.length]));
35
36
  try {
@@ -37,6 +38,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
37
38
  packets++;
38
39
  state.at = at;
39
40
  state.packets = packets;
41
+ if (isCurrent?.() === false) return null;
40
42
  if (firstPicture) {
41
43
  const video = tracks.find(track => track.type === "video");
42
44
  const stream = video && frames.get(video.trackNumber);
@@ -105,7 +107,7 @@ export async function readAsfPacketTime({ readRange, at, end, info }) {
105
107
  * @param {number} params.sentFrom - When packet `first` is sent, as far as the caller knows.
106
108
  * @returns {Promise<{ index: PacketIndex, startCovered: boolean }>}
107
109
  */
108
- export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom }) {
110
+ export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom, isCurrent }) {
109
111
  const declared = new Map(tracks.map(track => [track.trackNumber, track]));
110
112
  // Not admitted to IndexMemory: one segment's records, a few hundred, held
111
113
  // by the request that asked and dropped with it. Nothing owns a window to
@@ -125,23 +127,30 @@ export async function readAsfWindow({ readRange, data, tracks, requested, info,
125
127
  const held = (start, end) => buffer !== null && start >= bufferAt && end < bufferAt + buffer.length;
126
128
  const read = (start, end) => held(start, end) ? buffer.subarray(start - bufferAt, end - bufferAt + 1) : readRange(start, end);
127
129
  const load = async () => {
130
+ if (isCurrent?.() === false) return false;
128
131
  try {
129
132
  buffer = await readRange(at, Math.min(dataEnd - 1, at + WINDOW_PORTION_PACKETS * data.length - 1));
130
133
  } catch (error) {
134
+ if (isCurrent?.() === false) return false;
131
135
  if (!isUnavailable(error)) throw error;
132
136
  try {
133
137
  buffer = await readRange(at, Math.min(dataEnd - 1, at + data.length - 1));
134
138
  } catch (missing) {
139
+ if (isCurrent?.() === false) return false;
135
140
  if (!isUnavailable(missing)) throw missing;
136
141
  const ahead = Math.ceil(Math.max(0, interval.to + WINDOW_LEAD_SECONDS - sent) * rate);
137
142
  throw new BytesUnavailable(at, Math.min(dataEnd - 1, Math.max(at + data.length, at + ahead) - 1), 0);
138
143
  }
139
144
  }
145
+ if (isCurrent?.() === false) return false;
140
146
  bufferAt = at;
147
+ return true;
141
148
  };
142
149
  for (;;) {
150
+ if (isCurrent?.() === false) return null;
143
151
  if (at >= dataEnd) { reachedEnd = true; break; }
144
- if (!held(at, Math.min(dataEnd, at + data.length) - 1)) await load();
152
+ if (!held(at, Math.min(dataEnd, at + data.length) - 1) && !(await load())) return null;
153
+ if (isCurrent?.() === false) return null;
145
154
  ({ next: at, sent } = await readPacket({ readRange: read, at, end: data.end, info, declared, frames, assembling, started }));
146
155
  for (const id of ids) {
147
156
  const stream = frames.get(id), times = after.get(id);
@@ -56,17 +56,37 @@ export function isUnavailable(error) {
56
56
  * the clamped range is a read that did not finish: a stream that ended early,
57
57
  * a piece that is not downloaded, a store that could not serve it.
58
58
  *
59
- * @param {(start: number, end: number) => Promise<Buffer | null>} read
59
+ * @param {(start: number, end: number, options?: { signal?: AbortSignal }) => Promise<Buffer | null>} read
60
60
  * @param {number} fileSize
61
- * @returns {(start: number, end: number) => Promise<Buffer>}
61
+ * @returns {(start: number, end: number, options?: { signal?: AbortSignal }) => Promise<Buffer>}
62
62
  */
63
63
  export function strictReader(read, fileSize) {
64
- return async (start, end) => {
64
+ return async (start, end, { signal } = {}) => {
65
65
  const last = Math.min(end, fileSize - 1);
66
66
  if (!(start >= 0) || last < start) {
67
67
  return Buffer.alloc(0);
68
68
  }
69
- const bytes = await read(start, last);
69
+ let bytes;
70
+ if (!signal) {
71
+ bytes = await read(start, last);
72
+ } else {
73
+ const abort = () => {
74
+ const error = new Error("Read cancelled.");
75
+ error.name = "AbortError";
76
+ return error;
77
+ };
78
+ if (signal.aborted) throw abort();
79
+ let onAbort;
80
+ const cancelled = new Promise((_, reject) => {
81
+ onAbort = () => reject(abort());
82
+ signal.addEventListener("abort", onAbort, { once: true });
83
+ });
84
+ try {
85
+ bytes = await Promise.race([read(start, last, { signal }), cancelled]);
86
+ } finally {
87
+ signal.removeEventListener("abort", onAbort);
88
+ }
89
+ }
70
90
  const wanted = last - start + 1;
71
91
  const received = bytes ? bytes.length : 0;
72
92
  if (received < wanted) {