@torrent-tv/proxy 2.74.1 → 2.76.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/CHANGELOG.md +1504 -1453
- package/CLAUDE.md +19 -6
- package/biome.json +182 -1
- package/docs/container-architecture.md +27 -6
- package/docs/encode-run-state.md +1 -1
- package/knip.json +14 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +1 -1
- package/routes/api/transcode-sessions/post.js +185 -185
- package/routes/api/transcode-sessions/progress/get.js +5 -1
- package/routes/transcode/audio-file/get.js +11 -1
- package/routes/transcode/audio-warm/get.js +11 -1
- package/routes/transcode/session-file/get.js +1 -1
- package/routes/transcode/variant-file/get.js +10 -1
- package/scripts/render-run-graph.js +2 -2
- package/server.js +25 -0
- package/services/audio-inventory.js +9 -214
- package/services/container/AviContainer.js +266 -81
- package/services/container/Container.js +281 -80
- package/services/container/ContainerFactory.js +67 -0
- package/services/container/MatroskaContainer.js +327 -8
- package/services/container/Mp4Container.js +373 -27
- package/services/container/SubtitleFileContainer.js +0 -1
- package/services/controllers/SubtitleController.js +128 -128
- package/services/demand/index.js +7 -10
- package/services/download/registry.js +0 -14
- package/services/encode/CoverageMap.js +281 -0
- package/services/encode/EncodePlan.js +255 -0
- package/services/encode/EncodeRun.js +587 -0
- package/services/encode/Encoder.js +84 -0
- package/services/encode/NvencEncoder.js +45 -0
- package/services/encode/QsvEncoder.js +47 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +529 -0
- package/services/encode/SoftwareEncoder.js +111 -0
- package/services/encode/V4l2m2mEncoder.js +53 -0
- package/services/encode/VaapiEncoder.js +53 -0
- package/services/encode/args.js +200 -0
- package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
- package/services/encode/index.js +9 -0
- package/services/encode/run-command.js +647 -0
- package/services/hls-session-manager.js +11073 -10711
- package/services/hwaccel.js +1688 -1992
- package/services/orchestrators/EncodeOrchestrator.js +359 -0
- package/services/output/LiveOutputs.js +213 -0
- package/services/output/Output.js +94 -0
- package/services/output/OutputSpec.js +195 -0
- package/services/output/Timeline.js +220 -0
- package/services/output/index.js +1 -0
- package/services/output/ladder.js +26 -0
- package/services/playback-planner.js +806 -747
- package/services/produced-index.js +222 -300
- package/services/source/SourceFile.js +346 -0
- package/services/{sidecar-files.js → torrent/files.js} +107 -11
- package/services/torrent/naming.js +619 -0
- package/services/torrent-worker/client.js +10 -0
- package/services/torrent-worker/container-tracks.js +71 -43
- package/services/torrent-worker/pool-adapter.js +370 -333
- package/services/torrent-worker/protocol.js +7 -0
- package/services/torrent-worker/subtitle-cues.js +549 -549
- package/services/torrent-worker/worker.js +18 -0
- package/services/tracks/AudioTrack.js +131 -40
- package/services/tracks/TextSubtitleTrack.js +287 -287
- package/services/tracks/index.js +15 -14
- package/services/viewer/Viewer.js +145 -0
- package/services/viewer/Viewers.js +124 -0
- package/test/audio-inventory.test.js +176 -177
- package/test/auto-quality-step.test.js +508 -506
- package/test/behind-head-repair.test.js +17 -7
- package/test/coverage-map.test.js +153 -0
- package/test/cut-times-timeline.test.js +6 -5
- package/test/cuts-follow-published-grid.test.js +4 -4
- package/test/decode-cost.test.js +31 -12
- package/test/encode-exit.test.js +1 -1
- package/test/encode-orchestrator.test.js +196 -0
- package/test/encode-plan.test.js +245 -0
- package/test/encode-run-state.test.js +2 -2
- package/test/encode-run.test.js +168 -0
- package/test/encoder-kinds.test.js +122 -0
- package/test/held-request-width.test.js +9 -3
- package/test/helpers/encode-run.js +128 -0
- package/test/keyframe-index-accuracy.test.js +19 -12
- package/test/keyframes-belong-to-the-file.test.js +132 -0
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/orchestrator-wired.test.js +164 -0
- package/test/output-shape.test.js +68 -0
- package/test/output-spec.test.js +157 -0
- package/test/produced-copy-choice.test.js +58 -92
- package/test/produced-index.test.js +142 -188
- package/test/quality-variants.test.js +1079 -1075
- package/test/run-graph-drift.test.js +1 -1
- package/test/run-intervals.test.js +329 -0
- package/test/run-position-follows-published-grid.test.js +4 -4
- package/test/seek-landing.test.js +8 -8
- package/test/seek-target-not-superseded.test.js +21 -9
- package/test/segment-demand.test.js +82 -0
- package/test/segment-serve-wiring.test.js +47 -52
- package/test/segment-store.test.js +187 -0
- package/test/segments-are-shared.test.js +175 -0
- package/test/sidecar-naming.test.js +142 -0
- package/test/source-file.test.js +133 -0
- package/test/stale-request-after-seek.test.js +18 -12
- package/test/subtitle-language.test.js +252 -252
- package/test/timeline.test.js +95 -0
- package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
- package/test/torrent-naming.test.js +255 -0
- package/test/tracks-begin-together.test.js +44 -32
- package/test/two-viewers-one-picture.test.js +347 -0
- package/test/video-facts.test.js +102 -0
- package/test/viewer-outputs.test.js +273 -0
- package/test/viewer.test.js +91 -0
- package/utils/perf.js +1 -63
- package/services/container/index.js +0 -6
- package/services/container-index/avi.js +0 -167
- package/services/container-index/index.js +0 -118
- package/services/container-index/matroska.js +0 -336
- package/services/container-index/mp4.js +0 -358
- package/services/controllers/index.js +0 -2
- package/services/download/index.js +0 -8
- package/services/orchestrators/index.js +0 -2
- /package/services/{container-index → container}/ebml-reader.js +0 -0
- /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
- /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
|
@@ -1,300 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One statement of what
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Three places used to ask what
|
|
11
|
-
* itself: the look-ahead counted numbers, the serving path looked for a file,
|
|
12
|
-
* the header derivation listed names. Two of them held opposite beliefs about
|
|
13
|
-
* one file for ten minutes on 2026-09-03 — a segment a killed run had opened
|
|
14
|
-
* and never written was a NAME to one and an empty file to the other, so the
|
|
15
|
-
* encoder was stopped for having produced it while the request for it was
|
|
16
|
-
* refused for its being unwritten. That defect is fixed; what allowed it was
|
|
17
|
-
* three rules with no shared definition, and this is the shared definition.
|
|
18
|
-
*
|
|
19
|
-
* It is also what those three cost. Every one of them
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.#forgetDir(held);
|
|
224
|
-
}
|
|
225
|
-
this.#runsListedAt = -1;
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
this.#forgetDir(dir);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Read one run's directory into the index.
|
|
233
|
-
*
|
|
234
|
-
* @param {string} dir
|
|
235
|
-
* @param {number} changedAt
|
|
236
|
-
* @returns {void}
|
|
237
|
-
*/
|
|
238
|
-
#readDir(dir, changedAt) {
|
|
239
|
-
let names;
|
|
240
|
-
try {
|
|
241
|
-
names = readdirSync(dir, { withFileTypes: false });
|
|
242
|
-
this.#directoryReads += 1;
|
|
243
|
-
} catch {
|
|
244
|
-
this.#forgetDir(dir);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
/** @type {RunContents} */
|
|
248
|
-
const contents = { readAt: changedAt, byName: new Map(), byNumber: new Map() };
|
|
249
|
-
for (const name of names) {
|
|
250
|
-
const full = path.join(dir, name);
|
|
251
|
-
if (!this.#segmentFormat.isSegmentFileName(name)) {
|
|
252
|
-
contents.byName.set(name, full);
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
const index = this.#segmentFormat.segmentIndexFromName(name);
|
|
256
|
-
if (index < 0) {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (!this.#nonEmpty.has(full)) {
|
|
260
|
-
let size = 0;
|
|
261
|
-
try {
|
|
262
|
-
size = statSync(full).size;
|
|
263
|
-
} catch {
|
|
264
|
-
continue; // Vanished between the listing and the question.
|
|
265
|
-
}
|
|
266
|
-
if (size === 0) {
|
|
267
|
-
continue; // Opened, nothing written into it yet — or ever.
|
|
268
|
-
}
|
|
269
|
-
this.#nonEmpty.add(full);
|
|
270
|
-
}
|
|
271
|
-
contents.byName.set(name, full);
|
|
272
|
-
contents.byNumber.set(index, full);
|
|
273
|
-
}
|
|
274
|
-
const previous = this.#runs.get(dir);
|
|
275
|
-
if (previous) {
|
|
276
|
-
for (const held of previous.byName.values()) {
|
|
277
|
-
if (!contents.byName.has(path.basename(held))) {
|
|
278
|
-
this.#nonEmpty.delete(held);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
this.#runs.set(dir, contents);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Drop everything a directory answered for.
|
|
287
|
-
*
|
|
288
|
-
* @param {string} dir
|
|
289
|
-
* @returns {void}
|
|
290
|
-
*/
|
|
291
|
-
#forgetDir(dir) {
|
|
292
|
-
const contents = this.#runs.get(dir);
|
|
293
|
-
if (contents) {
|
|
294
|
-
for (const held of contents.byName.values()) {
|
|
295
|
-
this.#nonEmpty.delete(held);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
this.#runs.delete(dir);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file One statement of what an output has produced.
|
|
3
|
+
*
|
|
4
|
+
* Every run of an output writes into that output's own directory, and there is
|
|
5
|
+
* exactly one. Two runs cannot want the same name because they are given
|
|
6
|
+
* stretches that do not overlap — which is what replaced the directory-per-run
|
|
7
|
+
* scheme that once kept them apart, and with it the whole notion of a run
|
|
8
|
+
* having a place of its own.
|
|
9
|
+
*
|
|
10
|
+
* Three places used to ask what has been produced, and each answered for
|
|
11
|
+
* itself: the look-ahead counted numbers, the serving path looked for a file,
|
|
12
|
+
* the header derivation listed names. Two of them held opposite beliefs about
|
|
13
|
+
* one file for ten minutes on 2026-09-03 — a segment a killed run had opened
|
|
14
|
+
* and never written was a NAME to one and an empty file to the other, so the
|
|
15
|
+
* encoder was stopped for having produced it while the request for it was
|
|
16
|
+
* refused for its being unwritten. That defect is fixed; what allowed it was
|
|
17
|
+
* three rules with no shared definition, and this is the shared definition.
|
|
18
|
+
*
|
|
19
|
+
* It is also what those three cost. Every one of them listed the directory on
|
|
20
|
+
* the thread that carries the data channel — 1350 files for a 90-minute film,
|
|
21
|
+
* on every segment request. A directory's modification time changes when an
|
|
22
|
+
* entry is added or removed, so this asks THAT and re-reads only when it has
|
|
23
|
+
* moved. A quiet request costs one `stat` instead of a listing plus a `stat`
|
|
24
|
+
* per file.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { readdirSync, statSync } from "node:fs";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What an output's directory holds.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {object} DirectoryContents
|
|
34
|
+
* @property {number} readAt - The directory's modification time when it was read.
|
|
35
|
+
* @property {Map<string, string>} byName - File name to full path, every file.
|
|
36
|
+
* @property {Map<number, string>} byNumber - Segment number to full path, only
|
|
37
|
+
* for segments carrying bytes.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What an output has produced, and where.
|
|
42
|
+
*
|
|
43
|
+
* Not a cache of a truth kept elsewhere: this IS where the answer lives, and
|
|
44
|
+
* the disk is read only to build it and to notice that it has moved on.
|
|
45
|
+
*/
|
|
46
|
+
export class ProducedIndex {
|
|
47
|
+
/** @type {string} */
|
|
48
|
+
#dirPath;
|
|
49
|
+
|
|
50
|
+
/** @type {{ isSegmentFileName: (name: string) => boolean, segmentIndexFromName: (name: string) => number }} */
|
|
51
|
+
#segmentFormat;
|
|
52
|
+
|
|
53
|
+
/** @type {DirectoryContents | null} */
|
|
54
|
+
#contents = null;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Paths already seen carrying bytes. A piece that has bytes never loses them,
|
|
58
|
+
* so this answer never has to be taken back — which is what makes it worth
|
|
59
|
+
* keeping.
|
|
60
|
+
*
|
|
61
|
+
* @type {Set<string>}
|
|
62
|
+
*/
|
|
63
|
+
#nonEmpty = new Set();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* How many times the directory has been listed.
|
|
67
|
+
*
|
|
68
|
+
* The whole point of this class is that the answer is (nearly) one per change
|
|
69
|
+
* rather than one per request, and a claim like that is worth being able to
|
|
70
|
+
* check rather than believe.
|
|
71
|
+
*/
|
|
72
|
+
#directoryReads = 0;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} options
|
|
76
|
+
* @param {string} options.dirPath - The output's directory. Every run writes
|
|
77
|
+
* straight into it.
|
|
78
|
+
* @param {{ isSegmentFileName: (name: string) => boolean, segmentIndexFromName: (name: string) => number }} options.segmentFormat
|
|
79
|
+
*/
|
|
80
|
+
constructor({ dirPath, segmentFormat }) {
|
|
81
|
+
this.#dirPath = dirPath;
|
|
82
|
+
this.#segmentFormat = segmentFormat;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** How many times the directory has been listed. @returns {number} */
|
|
86
|
+
get directoryReads() {
|
|
87
|
+
return this.#directoryReads;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Where the output writes. @returns {string} */
|
|
91
|
+
get dirPath() {
|
|
92
|
+
return this.#dirPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bring the index up to date with the disk, reading only if it has moved.
|
|
97
|
+
*
|
|
98
|
+
* @returns {void}
|
|
99
|
+
*/
|
|
100
|
+
refresh() {
|
|
101
|
+
let changedAt;
|
|
102
|
+
try {
|
|
103
|
+
changedAt = statSync(this.#dirPath).mtimeMs;
|
|
104
|
+
} catch {
|
|
105
|
+
this.#forget();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (this.#contents?.readAt === changedAt) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.#read(changedAt);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Where a produced file is, or null when nothing has written it.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} fileName
|
|
118
|
+
* @returns {string | null}
|
|
119
|
+
*/
|
|
120
|
+
pathOf(fileName) {
|
|
121
|
+
this.refresh();
|
|
122
|
+
return this.#contents?.byName.get(fileName) ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Every segment number the output holds with bytes in it.
|
|
127
|
+
*
|
|
128
|
+
* @returns {Set<number>}
|
|
129
|
+
*/
|
|
130
|
+
segmentNumbers() {
|
|
131
|
+
this.refresh();
|
|
132
|
+
return new Set(this.#contents?.byNumber.keys() ?? []);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every produced file name.
|
|
137
|
+
*
|
|
138
|
+
* @returns {string[]}
|
|
139
|
+
*/
|
|
140
|
+
fileNames() {
|
|
141
|
+
this.refresh();
|
|
142
|
+
return [...(this.#contents?.byName.keys() ?? [])];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Forget what is held, so the next question re-reads the disk.
|
|
147
|
+
*
|
|
148
|
+
* Used where a file has just been removed on purpose: the directory's own
|
|
149
|
+
* time has moved, so a refresh would find it anyway, but a caller that
|
|
150
|
+
* deletes a file and asks in the same tick should not be told it is there.
|
|
151
|
+
*
|
|
152
|
+
* @returns {void}
|
|
153
|
+
*/
|
|
154
|
+
invalidate() {
|
|
155
|
+
this.#forget();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read the directory into the index.
|
|
160
|
+
*
|
|
161
|
+
* @param {number} changedAt
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
#read(changedAt) {
|
|
165
|
+
let names;
|
|
166
|
+
try {
|
|
167
|
+
names = readdirSync(this.#dirPath, { withFileTypes: false });
|
|
168
|
+
this.#directoryReads += 1;
|
|
169
|
+
} catch {
|
|
170
|
+
this.#forget();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
/** @type {DirectoryContents} */
|
|
174
|
+
const contents = { readAt: changedAt, byName: new Map(), byNumber: new Map() };
|
|
175
|
+
for (const name of names) {
|
|
176
|
+
const full = path.join(this.#dirPath, name);
|
|
177
|
+
if (!this.#segmentFormat.isSegmentFileName(name)) {
|
|
178
|
+
contents.byName.set(name, full);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const index = this.#segmentFormat.segmentIndexFromName(name);
|
|
182
|
+
if (index < 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!this.#nonEmpty.has(full)) {
|
|
186
|
+
let size = 0;
|
|
187
|
+
try {
|
|
188
|
+
size = statSync(full).size;
|
|
189
|
+
} catch {
|
|
190
|
+
continue; // Vanished between the listing and the question.
|
|
191
|
+
}
|
|
192
|
+
if (size === 0) {
|
|
193
|
+
continue; // Opened, nothing written into it yet — or ever.
|
|
194
|
+
}
|
|
195
|
+
this.#nonEmpty.add(full);
|
|
196
|
+
}
|
|
197
|
+
contents.byName.set(name, full);
|
|
198
|
+
contents.byNumber.set(index, full);
|
|
199
|
+
}
|
|
200
|
+
const previous = this.#contents;
|
|
201
|
+
if (previous) {
|
|
202
|
+
for (const held of previous.byName.values()) {
|
|
203
|
+
if (!contents.byName.has(path.basename(held))) {
|
|
204
|
+
this.#nonEmpty.delete(held);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
this.#contents = contents;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Drop everything the index answered for.
|
|
213
|
+
*
|
|
214
|
+
* @returns {void}
|
|
215
|
+
*/
|
|
216
|
+
#forget() {
|
|
217
|
+
for (const held of this.#contents?.byName.values() ?? []) {
|
|
218
|
+
this.#nonEmpty.delete(held);
|
|
219
|
+
}
|
|
220
|
+
this.#contents = null;
|
|
221
|
+
}
|
|
222
|
+
}
|