@torrent-tv/proxy 2.91.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.
- package/bin/cli.js +3 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readTrackEdits, walkBoxes } from "./mp4-boxes.js";
|
|
2
|
+
|
|
3
|
+
/** Repartition closed muxer output by presentation interval, preserving decode order. */
|
|
4
|
+
export function presentationSegment(current, following, { from, to }) {
|
|
5
|
+
const own = packetsOf(current);
|
|
6
|
+
const next = following ? packetsOf(following) : null;
|
|
7
|
+
const tracks = [];
|
|
8
|
+
for (const [id, track] of own.tracks) {
|
|
9
|
+
const edit = own.edits.get(id);
|
|
10
|
+
if (!edit) throw new Error("A segment track has no declared clock.");
|
|
11
|
+
const samples = [...track.samples];
|
|
12
|
+
if (track.kind === "soun" && next?.tracks.has(id)) {
|
|
13
|
+
const nextEdit = next.edits.get(id);
|
|
14
|
+
if (!nextEdit || nextEdit.timescale !== edit.timescale) throw new Error("Adjacent segment clocks differ.");
|
|
15
|
+
for (const sample of next.tracks.get(id).samples) {
|
|
16
|
+
samples.push({ ...sample, dts: sample.dts + nextEdit.offset - edit.offset });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const start = BigInt(Math.round(from * Number(edit.timescale)));
|
|
20
|
+
const end = BigInt(Math.round(to * Number(edit.timescale)));
|
|
21
|
+
const selected = samples.filter(sample => {
|
|
22
|
+
const pts = sample.dts + sample.composition + edit.offset;
|
|
23
|
+
return pts < end && pts + sample.duration > start;
|
|
24
|
+
});
|
|
25
|
+
selected.sort((left, right) => left.dts < right.dts ? -1 : left.dts > right.dts ? 1 : 0);
|
|
26
|
+
if (!selected.length) throw new Error("A presentation segment lacks required media.");
|
|
27
|
+
tracks.push({ id, samples: selected });
|
|
28
|
+
}
|
|
29
|
+
const makeMoof = offset => box("moof", full("mfhd", 0, 0, u32(1)), ...tracks.map(track => {
|
|
30
|
+
const entries = track.samples.map(sample => Buffer.concat([
|
|
31
|
+
u32(Number(sample.duration)), u32(sample.bytes.length), u32(sample.flags), i32(Number(sample.composition))
|
|
32
|
+
]));
|
|
33
|
+
const payload = Buffer.concat([u32(track.samples.length), i32(offset), ...entries]);
|
|
34
|
+
offset += track.samples.reduce((bytes, sample) => bytes + sample.bytes.length, 0);
|
|
35
|
+
const clock = Buffer.alloc(8);
|
|
36
|
+
clock.writeBigUInt64BE(track.samples[0].dts);
|
|
37
|
+
return box("traf", full("tfhd", 0, 0x20000, u32(track.id)), full("tfdt", 1, 0, clock), full("trun", 1, 0xf01, payload));
|
|
38
|
+
}));
|
|
39
|
+
const size = makeMoof(0).length;
|
|
40
|
+
return Buffer.concat([own.init, makeMoof(size + 8), box("mdat", ...tracks.flatMap(track => track.samples.map(sample => sample.bytes)))]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function packetsOf(raw) {
|
|
44
|
+
const edits = readTrackEdits(raw);
|
|
45
|
+
const tracks = new Map();
|
|
46
|
+
const defaults = new Map();
|
|
47
|
+
const payloads = [];
|
|
48
|
+
walkBoxes(raw, (type, start, end) => { if (type === "mdat") payloads.push([start, end]); }, 0, raw.length, () => false);
|
|
49
|
+
let id = null, firstMoof = null;
|
|
50
|
+
walkBoxes(raw, (type, start) => {
|
|
51
|
+
if (type === "tkhd") id = raw.readUInt32BE(start + (raw[start] === 1 ? 20 : 12));
|
|
52
|
+
if (type === "hdlr" && id !== null) tracks.set(id, { kind: raw.toString("latin1", start + 8, start + 12), samples: [] });
|
|
53
|
+
if (type === "trex") defaults.set(raw.readUInt32BE(start + 4), {
|
|
54
|
+
duration: raw.readUInt32BE(start + 12), size: raw.readUInt32BE(start + 16), flags: raw.readUInt32BE(start + 20)
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
walkBoxes(raw, (type, start, end) => {
|
|
58
|
+
if (type !== "moof") return;
|
|
59
|
+
const moof = start - 8;
|
|
60
|
+
firstMoof ??= moof;
|
|
61
|
+
walkBoxes(raw, (type, trafStart, trafEnd) => {
|
|
62
|
+
if (type !== "traf") return;
|
|
63
|
+
let trackId, clock = 0n, base = moof, data = null, values;
|
|
64
|
+
walkBoxes(raw, (type, at, boxEnd) => {
|
|
65
|
+
if (type === "tfhd") {
|
|
66
|
+
const flags = raw.readUIntBE(at + 1, 3);
|
|
67
|
+
trackId = raw.readUInt32BE(at + 4);
|
|
68
|
+
values = { ...defaults.get(trackId) };
|
|
69
|
+
let cursor = at + 8;
|
|
70
|
+
if (flags & 1) { base = Number(raw.readBigUInt64BE(cursor)); cursor += 8; }
|
|
71
|
+
if (flags & 2) cursor += 4;
|
|
72
|
+
for (const [flag, field] of [[8, "duration"], [16, "size"], [32, "flags"]]) {
|
|
73
|
+
if (flags & flag) { values[field] = raw.readUInt32BE(cursor); cursor += 4; }
|
|
74
|
+
}
|
|
75
|
+
} else if (type === "tfdt") {
|
|
76
|
+
clock = raw[at] === 1 ? raw.readBigUInt64BE(at + 4) : BigInt(raw.readUInt32BE(at + 4));
|
|
77
|
+
} else if (type === "trun") {
|
|
78
|
+
if (!tracks.has(trackId) || !values) throw new Error("Fragment refers to an undeclared track.");
|
|
79
|
+
const flags = raw.readUIntBE(at + 1, 3), count = raw.readUInt32BE(at + 4);
|
|
80
|
+
let cursor = at + 8, firstFlags = null;
|
|
81
|
+
if (flags & 1) { data = base + raw.readInt32BE(cursor); cursor += 4; }
|
|
82
|
+
if (flags & 4) { firstFlags = raw.readUInt32BE(cursor); cursor += 4; }
|
|
83
|
+
if (data === null) throw new Error("Fragment has no explicit payload location.");
|
|
84
|
+
for (let index = 0; index < count; index++) {
|
|
85
|
+
const sample = { ...values, composition: 0n, dts: clock };
|
|
86
|
+
if (index === 0 && firstFlags !== null) sample.flags = firstFlags;
|
|
87
|
+
for (const [flag, field] of [[0x100, "duration"], [0x200, "size"], [0x400, "flags"], [0x800, "composition"]]) {
|
|
88
|
+
if (flags & flag) {
|
|
89
|
+
if (cursor + 4 > boxEnd) throw new Error("Fragment sample declaration is truncated.");
|
|
90
|
+
sample[field] = field === "composition" ? BigInt(raw[at] === 1 ? raw.readInt32BE(cursor) : raw.readUInt32BE(cursor)) : raw.readUInt32BE(cursor);
|
|
91
|
+
cursor += 4;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!(sample.duration > 0) || !(sample.size > 0) || !Number.isSafeInteger(data) ||
|
|
95
|
+
!payloads.some(([start, end]) => data >= start && data + sample.size <= end)) throw new Error("Fragment sample bytes are incomplete.");
|
|
96
|
+
sample.duration = BigInt(sample.duration);
|
|
97
|
+
sample.bytes = raw.subarray(data, data + sample.size);
|
|
98
|
+
tracks.get(trackId).samples.push(sample);
|
|
99
|
+
data += sample.size;
|
|
100
|
+
clock += sample.duration;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, trafStart, trafEnd, () => false);
|
|
104
|
+
}, start, end, () => false);
|
|
105
|
+
}, 0, raw.length, () => false);
|
|
106
|
+
if (firstMoof === null) throw new Error("A closed segment has no fragments.");
|
|
107
|
+
if (tracks.size === 0 || [...tracks.values()].some(track => track.samples.length === 0)) throw new Error("A closed segment lacks declared track samples.");
|
|
108
|
+
return { init: raw.subarray(0, firstMoof), edits, tracks };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function box(type, ...parts) { const payload = Buffer.concat(parts); return Buffer.concat([u32(payload.length + 8), Buffer.from(type), payload]); }
|
|
112
|
+
function full(type, version, flags, payload) { const header = Buffer.alloc(4); header[0] = version; header.writeUIntBE(flags, 1, 3); return box(type, header, payload); }
|
|
113
|
+
function u32(value) { const bytes = Buffer.alloc(4); bytes.writeUInt32BE(value); return bytes; }
|
|
114
|
+
function i32(value) { const bytes = Buffer.alloc(4); bytes.writeInt32BE(value); return bytes; }
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
19
|
-
import { isUnavailable } from "./container/unavailable.js";
|
|
19
|
+
import { BytesUnavailable, isUnavailable } from "./container/unavailable.js";
|
|
20
|
+
import { IndexMemoryUnavailable } from "./container/memory-unavailable.js";
|
|
20
21
|
import { logger } from "../../utils/logger.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -29,6 +30,12 @@ import { logger } from "../../utils/logger.js";
|
|
|
29
30
|
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
30
31
|
|
|
31
32
|
export class ContainerOrchestrator {
|
|
33
|
+
#reads = new Map();
|
|
34
|
+
#lifetimes = new Map();
|
|
35
|
+
#activityEpoch = 0;
|
|
36
|
+
#activeReads = 0;
|
|
37
|
+
|
|
38
|
+
activity() { return { epoch: this.#activityEpoch, active: this.#activeReads > 0 }; }
|
|
32
39
|
constructor() {
|
|
33
40
|
/** @type {Map<string, import("./container/Container.js").Container|null>} */
|
|
34
41
|
this.cache = new Map();
|
|
@@ -60,6 +67,93 @@ export class ContainerOrchestrator {
|
|
|
60
67
|
return this.cache.get(`${sourceKey}:${fileIndex}`);
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
packetIndexBytes() {
|
|
71
|
+
return [...new Set(this.cache.values())].reduce((bytes, container) => bytes + (container?.packetIndexBytes?.() ?? 0), 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read one statement without converting missing bytes into an empty answer. */
|
|
75
|
+
async inspect(params, statement = "tracks") {
|
|
76
|
+
if (!["container", "tracks", "media-info", "keyframes", "packets"].includes(statement)) {
|
|
77
|
+
throw new TypeError(`Unknown media statement: ${statement}`);
|
|
78
|
+
}
|
|
79
|
+
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
80
|
+
let lifetime = this.#lifetimes.get(key);
|
|
81
|
+
if (!lifetime) this.#lifetimes.set(key, lifetime = {});
|
|
82
|
+
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false;
|
|
83
|
+
const previous = this.#reads.get(key) ?? Promise.resolve();
|
|
84
|
+
const reading = previous.catch(() => undefined).then(async () => {
|
|
85
|
+
if (!isCurrent()) return { result: { kind: "terminal", reason: "request-obsolete", requestId: params.requestId } };
|
|
86
|
+
const revision = params.onReadStart?.(statement);
|
|
87
|
+
const result = await this.#inspectRead({ ...params, isCurrent }, statement);
|
|
88
|
+
return { result, revision };
|
|
89
|
+
});
|
|
90
|
+
this.#reads.set(key, reading);
|
|
91
|
+
try {
|
|
92
|
+
const { result, revision } = await reading;
|
|
93
|
+
if (!isCurrent()) return { kind: "terminal", reason: "request-obsolete", requestId: params.requestId };
|
|
94
|
+
if (result.kind === "needs-ranges") params.onNeedsRanges?.(result);
|
|
95
|
+
if (statement === "tracks" && result.kind === "result") params.onTracks?.(result.value);
|
|
96
|
+
await params.onReadResult?.(statement, result, revision);
|
|
97
|
+
return result;
|
|
98
|
+
} finally {
|
|
99
|
+
if (this.#reads.get(key) === reading) this.#reads.delete(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async #inspectRead(params, statement) {
|
|
104
|
+
const requestId = params.requestId ?? `${params.sourceKey}:${params.fileIndex}:${statement}`;
|
|
105
|
+
this.#activityEpoch++;
|
|
106
|
+
this.#activeReads++;
|
|
107
|
+
try {
|
|
108
|
+
const container = await this.containerFor(params);
|
|
109
|
+
if (!container) {
|
|
110
|
+
const result = { kind: "terminal", reason: "format-not-supported", requestId };
|
|
111
|
+
logger.info(`media request=${requestId} terminal=${result.reason}`);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
let value;
|
|
115
|
+
if (statement === "container") {
|
|
116
|
+
value = container;
|
|
117
|
+
} else if (statement === "tracks") {
|
|
118
|
+
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
119
|
+
value = this.tracks.get(key);
|
|
120
|
+
if (!value) {
|
|
121
|
+
value = await container.readTracks();
|
|
122
|
+
if (!Array.isArray(value)) throw new Error("Container did not return a track table.");
|
|
123
|
+
if (params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
124
|
+
this.tracks.set(key, value);
|
|
125
|
+
}
|
|
126
|
+
} else if (statement === "media-info") {
|
|
127
|
+
value = await container.readMediaInfo();
|
|
128
|
+
} else if (statement === "keyframes") {
|
|
129
|
+
value = await container.readKeyframeIndex();
|
|
130
|
+
} else if (statement === "packets") {
|
|
131
|
+
value = await container.readPacketIndex(params.packetInterval);
|
|
132
|
+
await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
|
|
133
|
+
} else {
|
|
134
|
+
throw new TypeError(`Unknown media statement: ${statement}`);
|
|
135
|
+
}
|
|
136
|
+
return { kind: "result", value, requestId };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof IndexMemoryUnavailable) {
|
|
139
|
+
const result = { kind: "needs-memory", bytes: error.bytes, requestId };
|
|
140
|
+
logger.info(`media request=${requestId} needs memory=${error.bytes}`);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
if (isUnavailable(error)) {
|
|
144
|
+
const result = { kind: "needs-ranges", ranges: [[error.start, error.end]], requestId };
|
|
145
|
+
logger.info(`media request=${requestId} needs bytes=${error.start}-${error.end}`);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
const result = { kind: "terminal", reason: "media-read-failed", message: error?.message ?? String(error), requestId };
|
|
149
|
+
logger.warn(`media request=${requestId} terminal=${result.reason}: ${result.message}`);
|
|
150
|
+
return result;
|
|
151
|
+
} finally {
|
|
152
|
+
this.#activeReads--;
|
|
153
|
+
this.#activityEpoch++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
63
157
|
/**
|
|
64
158
|
* The file's one container, built on first ask.
|
|
65
159
|
*
|
|
@@ -74,12 +168,13 @@ export class ContainerOrchestrator {
|
|
|
74
168
|
* @throws {import("./container/unavailable.js").BytesUnavailable} While the
|
|
75
169
|
* head has not arrived. Nothing is kept for it.
|
|
76
170
|
*/
|
|
77
|
-
async containerFor({ sourceKey, fileIndex, readRange, fileSize, label = "", portionBytes }) {
|
|
171
|
+
async containerFor({ sourceKey, fileIndex, readRange, fileSize, label = "", portionBytes, probe, packetMemory }) {
|
|
78
172
|
const key = `${sourceKey}:${fileIndex}`;
|
|
79
173
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
80
174
|
if (this.pending.has(key)) return this.pending.get(key);
|
|
81
|
-
const p = ContainerFactory.create({ readRange, fileSize, label, portionBytes })
|
|
175
|
+
const p = ContainerFactory.create({ readRange, fileSize, label, portionBytes, probe, packetMemory })
|
|
82
176
|
.then((c) => {
|
|
177
|
+
if (this.pending.get(key) !== p) throw new Error("Container request was withdrawn.");
|
|
83
178
|
this.cache.set(key, c);
|
|
84
179
|
if (c) logger.info(`container: ${c.formatName} for "${label}"`);
|
|
85
180
|
else logger.info(`container: unknown for "${label}"`);
|
|
@@ -92,7 +187,7 @@ export class ContainerOrchestrator {
|
|
|
92
187
|
throw e;
|
|
93
188
|
})
|
|
94
189
|
.finally(() => {
|
|
95
|
-
this.pending.delete(key);
|
|
190
|
+
if (this.pending.get(key) === p) this.pending.delete(key);
|
|
96
191
|
});
|
|
97
192
|
this.pending.set(key, p);
|
|
98
193
|
return p;
|
|
@@ -106,13 +201,8 @@ export class ContainerOrchestrator {
|
|
|
106
201
|
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
107
202
|
*/
|
|
108
203
|
async getContainer(params) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} catch {
|
|
112
|
-
// `containerFor` has already said what failed, where it was not a
|
|
113
|
-
// shortage of bytes; either way nothing was kept.
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
204
|
+
const result = await this.inspect(params, "container");
|
|
205
|
+
return result.kind === "result" ? result.value : null;
|
|
116
206
|
}
|
|
117
207
|
|
|
118
208
|
/**
|
|
@@ -121,26 +211,8 @@ export class ContainerOrchestrator {
|
|
|
121
211
|
* the bytes have not arrived; that empty answer is not kept.
|
|
122
212
|
*/
|
|
123
213
|
async getTracks(params) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
if (known) {
|
|
127
|
-
return known;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
const container = await this.containerFor(params);
|
|
131
|
-
if (!container) return [];
|
|
132
|
-
const tracks = await container.readTracks();
|
|
133
|
-
if (Array.isArray(tracks)) {
|
|
134
|
-
this.tracks.set(key, tracks);
|
|
135
|
-
return tracks;
|
|
136
|
-
}
|
|
137
|
-
return [];
|
|
138
|
-
} catch (e) {
|
|
139
|
-
if (!isUnavailable(e)) {
|
|
140
|
-
logger.warn(`container: readTracks failed for "${params.label}": ${e?.message ?? e}`);
|
|
141
|
-
}
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
214
|
+
const result = await this.inspect(params, "tracks");
|
|
215
|
+
return result.kind === "result" ? result.value : [];
|
|
144
216
|
}
|
|
145
217
|
|
|
146
218
|
/**
|
|
@@ -155,16 +227,8 @@ export class ContainerOrchestrator {
|
|
|
155
227
|
* @returns {Promise<import("./container/Container.js").ContainerMediaInfo|null>}
|
|
156
228
|
*/
|
|
157
229
|
async getMediaInfo(params) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!container) return null;
|
|
161
|
-
return await container.readMediaInfo();
|
|
162
|
-
} catch (e) {
|
|
163
|
-
if (!isUnavailable(e)) {
|
|
164
|
-
logger.warn(`container: readMediaInfo failed for "${params.label}": ${e?.message ?? e}`);
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
230
|
+
const result = await this.inspect(params, "media-info");
|
|
231
|
+
return result.kind === "result" ? result.value : null;
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
/**
|
|
@@ -176,17 +240,23 @@ export class ContainerOrchestrator {
|
|
|
176
240
|
* not record "no keyframes" for a file whose index is still downloading.
|
|
177
241
|
*/
|
|
178
242
|
async getKeyframeIndex(params) {
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (isUnavailable(e)) throw e;
|
|
185
|
-
return null;
|
|
243
|
+
const result = await this.inspect(params, "keyframes");
|
|
244
|
+
if (result.kind === "needs-memory") throw new IndexMemoryUnavailable(result.bytes);
|
|
245
|
+
if (result.kind === "needs-ranges") {
|
|
246
|
+
const [start, end] = result.ranges[0];
|
|
247
|
+
throw new BytesUnavailable(start, end, 0);
|
|
186
248
|
}
|
|
249
|
+
return result.kind === "result" ? result.value : null;
|
|
187
250
|
}
|
|
188
251
|
|
|
189
252
|
forget(sourceKey, fileIndex) {
|
|
253
|
+
const prefix = `${sourceKey}:`;
|
|
254
|
+
for (const key of this.#lifetimes.keys()) {
|
|
255
|
+
if (fileIndex === undefined ? key.startsWith(prefix) : key === `${sourceKey}:${fileIndex}`) {
|
|
256
|
+
this.#lifetimes.delete(key);
|
|
257
|
+
this.#reads.delete(key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
190
260
|
if (fileIndex === undefined) {
|
|
191
261
|
for (const k of [...this.cache.keys()]) if (k.startsWith(`${sourceKey}:`)) this.cache.delete(k);
|
|
192
262
|
for (const k of [...this.pending.keys()]) if (k.startsWith(`${sourceKey}:`)) this.pending.delete(k);
|
|
@@ -1,69 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Reading the keyframe table of a file: once, whoever asks, with a bound
|
|
3
|
-
* on how long anybody waits for it.
|
|
4
|
-
*
|
|
5
|
-
* The table itself is `container/KeyframeTable.js`, one object per file. This is
|
|
6
|
-
* the policy around filling it in, and it is a policy because three things had
|
|
7
|
-
* to be true at once and each of them was learned from a field session:
|
|
8
|
-
*
|
|
9
|
-
* 1. **once per file, and one WAIT per file.** Two sessions created in the same
|
|
10
|
-
* moment used to miss the cache together and read the table twice — which is
|
|
11
|
-
* what two viewers opening one film do, measured 13 ms apart on 2026-09-03.
|
|
12
|
-
* Whoever asks second joins the read already running;
|
|
13
|
-
* 2. **the wait is bounded, the READ is not.** A picture cannot be copied
|
|
14
|
-
* without the table, so the answer to "not yet" is to re-encode — but the
|
|
15
|
-
* read goes on, lands in the file's own table, and is there for whoever holds
|
|
16
|
-
* it. Cancelling it would make every later viewer pay the whole wait again;
|
|
17
|
-
* 3. **a read that THREW is not an answer.** "The head is not downloaded" says
|
|
18
|
-
* nothing about the file; turning it into "this file has no keyframes" makes
|
|
19
|
-
* every picture of it a re-encode for as long as the process lives. Bytes
|
|
20
|
-
* that have not arrived reach here as `BytesUnavailable` — until 2026-10-01
|
|
21
|
-
* they came back as a reading of no times, which `KeyframeTable.learn`
|
|
22
|
-
* records as answered, so point 3 held only for reads that crashed. When
|
|
23
|
-
* pieces of such a file arrive the read is made again
|
|
24
|
-
* (`readAgainIfUnanswered`), and its answer lands in the same table.
|
|
25
|
-
*
|
|
26
|
-
* **What it does NOT know.** Where the bytes come from. The reader is one
|
|
27
|
-
* function handed in at construction — on this proxy it crosses to the torrent
|
|
28
|
-
* thread, where one container per file answers the track table and the media
|
|
29
|
-
* info from the same header — so this can be exercised with plain values alone.
|
|
30
|
-
* It used to live in the session manager, which also carried a SECOND reader
|
|
31
|
-
* that fetched the file over the proxy's own HTTP and parsed it there: a
|
|
32
|
-
* transport inside a layer that must not have one, and a second answer to a
|
|
33
|
-
* question with one owner.
|
|
34
|
-
*
|
|
35
|
-
* **Why this is not `ContainerOrchestrator`.** Both live on the main thread
|
|
36
|
-
* (the parse moved there on 2026-09-15). That one holds the file's one
|
|
37
|
-
* container, which keeps its own reading of the Cues table; this keeps the
|
|
38
|
-
* ANSWER the sessions hold — the keyframe times taken from that reading, and
|
|
39
|
-
* the wait around it, which a container cannot have because it does not know
|
|
40
|
-
* there is a viewer.
|
|
41
|
-
*/
|
|
1
|
+
/** One shared keyframe table per source file. Reads use available bytes only. */
|
|
42
2
|
|
|
43
3
|
import { KeyframeTable } from "./container/KeyframeTable.js";
|
|
44
4
|
import { isUnavailable } from "./container/unavailable.js";
|
|
45
5
|
import { logger as defaultLogger } from "../../utils/logger.js";
|
|
46
6
|
|
|
47
|
-
/**
|
|
48
|
-
* How long anybody waits for the table before giving up on copying the picture.
|
|
49
|
-
*
|
|
50
|
-
* Measured on the addon host, 2026-09-04, over seventeen files from
|
|
51
|
-
* `Dropbox/trn` — four containers, pieces from 0.25 to 16 MB, files from 0.36 to
|
|
52
|
-
* 20 GB, each torrent registered fresh so nothing of it was downloaded
|
|
53
|
-
* (`research/keyframe-table-read-2026-09-04.md`). Every table that arrived did
|
|
54
|
-
* so within 24.8 s, most within half a second; the two files that answered
|
|
55
|
-
* nothing took 120.9 s and 120.5 s.
|
|
56
|
-
*
|
|
57
|
-
* Those two figures are not a coincidence and they are what fixes this one:
|
|
58
|
-
* they are TWO of the bound the read already has — `READ_ABANDON_MS` in
|
|
59
|
-
* `torrent/worker/resume-warm.js`, one for the wait on the file's edges and
|
|
60
|
-
* one for the read itself, in series. A caller waiting for two of them is the
|
|
61
|
-
* defect; waiting for one is the bound, and it leaves 2.4x over the slowest
|
|
62
|
-
* table that did arrive. The line printed when it fires names which case
|
|
63
|
-
* happened, so the field can move it rather than an argument.
|
|
64
|
-
*/
|
|
65
|
-
export const KEYFRAME_TABLE_BUDGET_MS = 60_000;
|
|
66
|
-
|
|
67
7
|
export class KeyframeTables {
|
|
68
8
|
/** One per file, handed out rather than copied. @type {Map<string, KeyframeTable>} */
|
|
69
9
|
#byFile = new Map();
|
|
@@ -74,9 +14,6 @@ export class KeyframeTables {
|
|
|
74
14
|
/** @type {((params: { sourceKey: string, fileIndex: number }) => Promise<object | null>) | null} */
|
|
75
15
|
#readTable;
|
|
76
16
|
|
|
77
|
-
/** @type {number} */
|
|
78
|
-
#budgetMs;
|
|
79
|
-
|
|
80
17
|
/** @type {{ info: Function, warn: Function }} */
|
|
81
18
|
#logger;
|
|
82
19
|
|
|
@@ -85,13 +22,10 @@ export class KeyframeTables {
|
|
|
85
22
|
* @param {(params: { sourceKey: string, fileIndex: number }) => Promise<object | null>} [params.readTable] -
|
|
86
23
|
* Whatever can answer what this file's container states. The whole of this
|
|
87
24
|
* object's outside world.
|
|
88
|
-
* @param {number} [params.budgetMs] - How long `within` waits.
|
|
89
25
|
* @param {{ info: Function, warn: Function }} [params.logger]
|
|
90
26
|
*/
|
|
91
|
-
constructor({ readTable = null,
|
|
27
|
+
constructor({ readTable = null, logger = defaultLogger } = {}) {
|
|
92
28
|
this.#readTable = typeof readTable === "function" ? readTable : null;
|
|
93
|
-
const declared = Number(budgetMs);
|
|
94
|
-
this.#budgetMs = Number.isFinite(declared) && declared > 0 ? declared : KEYFRAME_TABLE_BUDGET_MS;
|
|
95
29
|
this.#logger = logger;
|
|
96
30
|
}
|
|
97
31
|
|
|
@@ -104,11 +38,6 @@ export class KeyframeTables {
|
|
|
104
38
|
return `${sourceKey}:${fileIndex}`;
|
|
105
39
|
}
|
|
106
40
|
|
|
107
|
-
/** How long a caller of `within` waits. @returns {number} */
|
|
108
|
-
get budgetMs() {
|
|
109
|
-
return this.#budgetMs;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
41
|
/**
|
|
113
42
|
* This file's table — the one object everyone reading it holds.
|
|
114
43
|
*
|
|
@@ -173,6 +102,7 @@ export class KeyframeTables {
|
|
|
173
102
|
}
|
|
174
103
|
const work = started
|
|
175
104
|
.then((reading) => {
|
|
105
|
+
if (this.#byFile.get(key) !== table) throw new DOMException("Keyframe request was withdrawn.", "AbortError");
|
|
176
106
|
table.learn({
|
|
177
107
|
times: Array.isArray(reading?.times) ? reading.times : null,
|
|
178
108
|
tolerance: reading?.tolerance,
|
|
@@ -202,7 +132,7 @@ export class KeyframeTables {
|
|
|
202
132
|
throw error;
|
|
203
133
|
})
|
|
204
134
|
.finally(() => {
|
|
205
|
-
this.#reading.delete(key);
|
|
135
|
+
if (this.#reading.get(key) === work) this.#reading.delete(key);
|
|
206
136
|
});
|
|
207
137
|
this.#reading.set(key, work);
|
|
208
138
|
return work;
|
|
@@ -247,47 +177,15 @@ export class KeyframeTables {
|
|
|
247
177
|
void this.warm(params);
|
|
248
178
|
}
|
|
249
179
|
|
|
250
|
-
/**
|
|
251
|
-
* The same read, with a bound on how long THIS caller waits for it.
|
|
252
|
-
*
|
|
253
|
-
* The read is not cancelled when the bound is reached — it goes on, and it
|
|
254
|
-
* lands in the table this returns, which is the file's own. What the bound
|
|
255
|
-
* decides is only whether this caller waits: a copied picture cannot be cut
|
|
256
|
-
* without the table, so the answer to "not yet" is to re-encode, which needs
|
|
257
|
-
* no table because it places the keyframes itself.
|
|
258
|
-
*
|
|
259
|
-
* @param {{ sourceKey: string, fileIndex: number, logName?: string }} params
|
|
260
|
-
* @returns {Promise<{ table: KeyframeTable, arrived: boolean }>} `arrived` is
|
|
261
|
-
* about THIS wait. False with an unanswered table means the read is still
|
|
262
|
-
* running, or found the bytes not downloaded yet and will be made again
|
|
263
|
-
* when they arrive — neither is the same as a file with no keyframes.
|
|
264
|
-
*/
|
|
180
|
+
/** Available-byte reads do not replace copying after elapsed time. */
|
|
265
181
|
async within(params) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
// Bytes not downloaded yet are "not arrived" to this caller: the table
|
|
274
|
-
// stays unanswered and is read again when pieces of the file arrive.
|
|
275
|
-
// Thrown out of here, a shortage of bytes would fail the opening of an
|
|
276
|
-
// output. Any other failure is still the caller's to see.
|
|
277
|
-
const answer = await Promise.race([
|
|
278
|
-
read.then(
|
|
279
|
-
(table) => ({ table, arrived: true }),
|
|
280
|
-
(error) => {
|
|
281
|
-
if (isUnavailable(error)) {
|
|
282
|
-
return { table: this.of(params), arrived: false };
|
|
283
|
-
}
|
|
284
|
-
throw error;
|
|
285
|
-
}
|
|
286
|
-
),
|
|
287
|
-
budget
|
|
288
|
-
]);
|
|
289
|
-
clearTimeout(timer);
|
|
290
|
-
return answer ?? { table: this.of(params), arrived: false };
|
|
182
|
+
try {
|
|
183
|
+
const table = await this.read(params);
|
|
184
|
+
return { table, arrived: table.answered };
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (isUnavailable(error)) return { table: this.of(params), arrived: false };
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
291
189
|
}
|
|
292
190
|
|
|
293
191
|
/**
|
|
@@ -342,11 +240,14 @@ export class KeyframeTables {
|
|
|
342
240
|
for (const key of [...this.#byFile.keys()]) {
|
|
343
241
|
if (key.startsWith(`${sourceKey}:`)) {
|
|
344
242
|
this.#byFile.delete(key);
|
|
243
|
+
this.#reading.delete(key);
|
|
345
244
|
}
|
|
346
245
|
}
|
|
347
246
|
return;
|
|
348
247
|
}
|
|
349
|
-
|
|
248
|
+
const key = KeyframeTables.keyFor(sourceKey, fileIndex);
|
|
249
|
+
this.#byFile.delete(key);
|
|
250
|
+
this.#reading.delete(key);
|
|
350
251
|
}
|
|
351
252
|
|
|
352
253
|
/** @returns {number} */
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Retry unfinished statements when their missing resource changes. */
|
|
2
|
+
export class MediaReadRequests {
|
|
3
|
+
#requests = new Map();
|
|
4
|
+
#read;
|
|
5
|
+
#failed;
|
|
6
|
+
#versions = new Map();
|
|
7
|
+
#listeners = new Map();
|
|
8
|
+
#memoryVersion = 0;
|
|
9
|
+
|
|
10
|
+
memoryRevision() { return this.#memoryVersion; }
|
|
11
|
+
|
|
12
|
+
subscribe(sourceKey, fileIndex, listener) {
|
|
13
|
+
const key = `${sourceKey}:${fileIndex}`;
|
|
14
|
+
let listeners = this.#listeners.get(key);
|
|
15
|
+
if (!listeners) this.#listeners.set(key, listeners = new Set());
|
|
16
|
+
listeners.add(listener);
|
|
17
|
+
return () => {
|
|
18
|
+
listeners.delete(listener);
|
|
19
|
+
if (!listeners.size) this.#listeners.delete(key);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#notify(sourceKey, fileIndex, result) {
|
|
24
|
+
for (const listener of [...(this.#listeners.get(`${sourceKey}:${fileIndex}`) ?? [])]) listener(result);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor({ read, failed }) {
|
|
28
|
+
this.#read = read;
|
|
29
|
+
this.#failed = failed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
revision(sourceKey, fileIndex) {
|
|
33
|
+
return this.#versions.get(`${sourceKey}:${fileIndex}`) ?? 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
record(params, statement, result, revision = this.revision(params.sourceKey, params.fileIndex), memoryRevision = this.#memoryVersion) {
|
|
37
|
+
const key = requestKey(params, statement);
|
|
38
|
+
if (!["needs-ranges", "needs-memory"].includes(result.kind)) {
|
|
39
|
+
this.#notify(params.sourceKey, params.fileIndex, result);
|
|
40
|
+
return this.#requests.delete(key);
|
|
41
|
+
}
|
|
42
|
+
const existing = this.#requests.get(key);
|
|
43
|
+
if (existing) { existing.params = params; existing.kind = result.kind; }
|
|
44
|
+
else this.#requests.set(key, { params, statement, kind: result.kind, running: false, changed: false });
|
|
45
|
+
const request = this.#requests.get(key);
|
|
46
|
+
// A read can change from missing memory to missing bytes. An event for
|
|
47
|
+
// its previous resource cannot authorize another read of unchanged bytes.
|
|
48
|
+
request.changed = result.kind === "needs-memory"
|
|
49
|
+
? memoryRevision !== this.#memoryVersion : revision !== this.revision(params.sourceKey, params.fileIndex);
|
|
50
|
+
if (request.changed) {
|
|
51
|
+
if (!request.running) queueMicrotask(() => { if (!request.running) void this.#retry(request); });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
memoryChanged() {
|
|
56
|
+
this.#memoryVersion++;
|
|
57
|
+
for (const request of this.#requests.values()) {
|
|
58
|
+
if (request.kind !== "needs-memory") continue;
|
|
59
|
+
// The retried statement announces its result. A memory event is not a
|
|
60
|
+
// source change for another statement still waiting for missing bytes.
|
|
61
|
+
request.changed = true;
|
|
62
|
+
if (!request.running) void this.#retry(request);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
bytesChanged(sourceKey, fileIndex) {
|
|
67
|
+
const fileKey = `${sourceKey}:${fileIndex}`;
|
|
68
|
+
this.#versions.set(fileKey, this.revision(sourceKey, fileIndex) + 1);
|
|
69
|
+
this.#notify(sourceKey, fileIndex, { kind: "bytes-changed" });
|
|
70
|
+
for (const request of this.#requests.values()) {
|
|
71
|
+
if (request.params.sourceKey !== sourceKey || request.params.fileIndex !== fileIndex) continue;
|
|
72
|
+
if (request.kind !== "needs-ranges") continue;
|
|
73
|
+
request.changed = true;
|
|
74
|
+
if (!request.running) void this.#retry(request);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
forget(sourceKey, fileIndex) {
|
|
79
|
+
for (const key of [...this.#listeners.keys()]) {
|
|
80
|
+
if (fileIndex === undefined ? key.startsWith(`${sourceKey}:`) : key === `${sourceKey}:${fileIndex}`) {
|
|
81
|
+
for (const listener of [...this.#listeners.get(key)]) listener({ kind: "cancelled", reason: "source-forgotten" });
|
|
82
|
+
this.#listeners.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const [key, request] of this.#requests) {
|
|
86
|
+
if (request.params.sourceKey === sourceKey && (fileIndex === undefined || request.params.fileIndex === fileIndex)) {
|
|
87
|
+
this.#requests.delete(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const key of this.#versions.keys()) {
|
|
91
|
+
if (fileIndex === undefined ? key.startsWith(`${sourceKey}:`) : key === `${sourceKey}:${fileIndex}`) this.#versions.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
retain(sourceKey, fileIndex, wanted) {
|
|
96
|
+
for (const [key, request] of this.#requests) {
|
|
97
|
+
if (request.params.sourceKey === sourceKey && request.params.fileIndex === fileIndex &&
|
|
98
|
+
!wanted(request.params, request.statement)) this.#requests.delete(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async #retry(request) {
|
|
103
|
+
request.running = true;
|
|
104
|
+
const key = requestKey(request.params, request.statement);
|
|
105
|
+
try {
|
|
106
|
+
while (request.changed && this.#requests.get(key) === request) {
|
|
107
|
+
request.changed = false;
|
|
108
|
+
await this.#read(request.params, request.statement);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
this.#failed?.(request.params, request.statement, error);
|
|
112
|
+
} finally {
|
|
113
|
+
request.running = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requestKey(params, statement) {
|
|
119
|
+
return JSON.stringify([params.sourceKey, params.fileIndex, statement, params.requestId ?? null]);
|
|
120
|
+
}
|