@torrent-tv/proxy 2.101.21 → 2.101.23
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
|
@@ -219,7 +219,7 @@ export class OutputLifecycle {
|
|
|
219
219
|
const maxBytes = this.#host.machineBudget.segmentBytes();
|
|
220
220
|
const inventory = this.#host.segmentStore.inventory();
|
|
221
221
|
let held = inventory.reduce((bytes, output) => bytes + output.bytes, 0);
|
|
222
|
-
if (!Number.isFinite(maxBytes) || maxBytes
|
|
222
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 0 || held <= maxBytes) return;
|
|
223
223
|
const candidates = leastNeededSegments(inventory.map(output => ({
|
|
224
224
|
...output,
|
|
225
225
|
positions: this.#host.viewerSegmentsOn(output.key),
|
|
@@ -54,8 +54,13 @@ export class CompletedFiles {
|
|
|
54
54
|
|
|
55
55
|
/** Assemblies in flight, so two passes cannot write one file at once. @type {Set<string>} */
|
|
56
56
|
#writing = new Set();
|
|
57
|
+
/** Bytes promised to assemblies that have not yet become held files. @type {Map<string, number>} */
|
|
58
|
+
#reservations = new Map();
|
|
59
|
+
/** Serialize allowance changes and admission decisions. @type {Promise<unknown>} */
|
|
60
|
+
#admission = Promise.resolve();
|
|
57
61
|
#readers = new Map();
|
|
58
62
|
#removeAfterRead = new Set();
|
|
63
|
+
#removing = new Set();
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
66
|
* What this store may hold, handed to it by the owner of the disk.
|
|
@@ -76,6 +81,8 @@ export class CompletedFiles {
|
|
|
76
81
|
|
|
77
82
|
/** When each held file was last asked for. @type {Map<string, number>} */
|
|
78
83
|
#lastRead = new Map();
|
|
84
|
+
/** Monotonic access order, so reads in the same clock tick remain ordered. */
|
|
85
|
+
#accessOrder = 0;
|
|
79
86
|
|
|
80
87
|
/**
|
|
81
88
|
* @param {object} params
|
|
@@ -142,17 +149,22 @@ export class CompletedFiles {
|
|
|
142
149
|
*/
|
|
143
150
|
find(infoHash, fileIndex) {
|
|
144
151
|
const key = this.#keyOf(infoHash, fileIndex);
|
|
145
|
-
if (this.#removeAfterRead.has(key)) return null;
|
|
152
|
+
if (this.#removeAfterRead.has(key) || this.#removing.has(key)) return null;
|
|
146
153
|
const file = this.#held.get(key) ?? null;
|
|
147
154
|
if (file) {
|
|
148
155
|
// WHEN IT WAS LAST WANTED, which is the order it leaves in. A whole file
|
|
149
156
|
// nobody has asked for in the longest is the one whose loss costs least:
|
|
150
157
|
// it can be downloaded again, and a read of it falls back to the pieces.
|
|
151
|
-
this.#
|
|
158
|
+
this.#touch(key);
|
|
152
159
|
}
|
|
153
160
|
return file;
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
/** Mark a file as more recently wanted than every earlier access. */
|
|
164
|
+
#touch(key) {
|
|
165
|
+
this.#lastRead.set(key, ++this.#accessOrder);
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
/** Pin a completed file against eviction until all its offset reads finish. */
|
|
157
169
|
acquire(infoHash, fileIndex) {
|
|
158
170
|
const key = this.#keyOf(infoHash, fileIndex);
|
|
@@ -186,20 +198,38 @@ export class CompletedFiles {
|
|
|
186
198
|
* many files went.
|
|
187
199
|
*/
|
|
188
200
|
async allow(bytes) {
|
|
189
|
-
this.#
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
return this.#withAdmission(async () => {
|
|
202
|
+
this.#allowanceBytes = Number.isFinite(bytes) && bytes >= 0 ? bytes : null;
|
|
203
|
+
const removed = await this.#trimToAllowance();
|
|
192
204
|
return { bytes: this.bytes, removed };
|
|
193
|
-
}
|
|
194
|
-
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Run one storage decision at a time. @param {() => Promise<unknown> | unknown} action */
|
|
209
|
+
#withAdmission(action) {
|
|
210
|
+
const result = this.#admission.then(action, action);
|
|
211
|
+
this.#admission = result.catch(() => undefined);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Include bytes currently being assembled when enforcing the allowance. */
|
|
216
|
+
#reservedBytes() {
|
|
217
|
+
let total = 0;
|
|
218
|
+
for (const length of this.#reservations.values()) total += length;
|
|
219
|
+
return total;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Remove eligible unused files until held and reserved bytes fit. */
|
|
223
|
+
async #trimToAllowance() {
|
|
224
|
+
let removed = 0;
|
|
225
|
+
if (this.#allowanceBytes === null) return removed;
|
|
226
|
+
while (this.bytes + this.#reservedBytes() > this.#allowanceBytes) {
|
|
195
227
|
const victim = this.#longestUnread();
|
|
196
|
-
if (!victim)
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
await this.#remove(victim);
|
|
228
|
+
if (!victim) break;
|
|
229
|
+
if (!await this.#remove(victim)) break;
|
|
200
230
|
removed += 1;
|
|
201
231
|
}
|
|
202
|
-
return
|
|
232
|
+
return removed;
|
|
203
233
|
}
|
|
204
234
|
|
|
205
235
|
/**
|
|
@@ -215,7 +245,7 @@ export class CompletedFiles {
|
|
|
215
245
|
if (this.#allowanceBytes === null) {
|
|
216
246
|
return true;
|
|
217
247
|
}
|
|
218
|
-
return this.bytes + Math.max(0, Number(length) || 0) <= this.#allowanceBytes;
|
|
248
|
+
return this.bytes + this.#reservedBytes() + Math.max(0, Number(length) || 0) <= this.#allowanceBytes;
|
|
219
249
|
}
|
|
220
250
|
|
|
221
251
|
/**
|
|
@@ -227,7 +257,7 @@ export class CompletedFiles {
|
|
|
227
257
|
let oldest = null;
|
|
228
258
|
let oldestAt = Number.POSITIVE_INFINITY;
|
|
229
259
|
for (const key of this.#held.keys()) {
|
|
230
|
-
if (this.#writing.has(key) || this.#readers.has(key)) {
|
|
260
|
+
if (this.#writing.has(key) || this.#readers.has(key) || this.#removing.has(key)) {
|
|
231
261
|
continue;
|
|
232
262
|
}
|
|
233
263
|
const at = this.#lastRead.get(key) ?? 0;
|
|
@@ -243,17 +273,24 @@ export class CompletedFiles {
|
|
|
243
273
|
* Take one whole file off the disk.
|
|
244
274
|
*
|
|
245
275
|
* @param {string} key
|
|
246
|
-
* @returns {Promise<
|
|
276
|
+
* @returns {Promise<boolean>} Whether the file was removed.
|
|
247
277
|
*/
|
|
248
278
|
async #remove(key) {
|
|
249
|
-
if (this.#
|
|
279
|
+
if (this.#removing.has(key)) return false;
|
|
280
|
+
if (this.#readers.has(key)) { this.#removeAfterRead.add(key); return false; }
|
|
250
281
|
const file = this.#held.get(key);
|
|
282
|
+
if (!file) return true;
|
|
283
|
+
this.#removing.add(key);
|
|
284
|
+
try {
|
|
285
|
+
await fs.rm(file.path, REMOVAL);
|
|
286
|
+
} catch {
|
|
287
|
+
this.#removing.delete(key);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
251
290
|
this.#held.delete(key);
|
|
252
291
|
this.#lastRead.delete(key);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
await fs.rm(file.path, REMOVAL).catch(() => undefined);
|
|
292
|
+
this.#removing.delete(key);
|
|
293
|
+
return true;
|
|
257
294
|
}
|
|
258
295
|
|
|
259
296
|
/**
|
|
@@ -344,18 +381,30 @@ export class CompletedFiles {
|
|
|
344
381
|
async keep({ infoHash, fileIndex, length, name, open }) {
|
|
345
382
|
const key = this.#keyOf(infoHash, fileIndex);
|
|
346
383
|
const held = this.#held.get(key);
|
|
347
|
-
if (held) {
|
|
384
|
+
if (held && !this.#removing.has(key)) {
|
|
348
385
|
return held;
|
|
349
386
|
}
|
|
387
|
+
if (this.#removing.has(key)) return null;
|
|
350
388
|
if (this.#writing.has(key)) {
|
|
351
389
|
return null;
|
|
352
390
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
391
|
+
this.#writing.add(key);
|
|
392
|
+
const admitted = await this.#withAdmission(async () => {
|
|
393
|
+
if (this.#held.has(key)) return false;
|
|
394
|
+
if (this.#allowanceBytes !== null && length > this.#allowanceBytes) return false;
|
|
395
|
+
while (this.#allowanceBytes !== null &&
|
|
396
|
+
this.bytes + this.#reservedBytes() + length > this.#allowanceBytes) {
|
|
397
|
+
const victim = this.#longestUnread();
|
|
398
|
+
if (!victim) return false;
|
|
399
|
+
if (!await this.#remove(victim)) return false;
|
|
400
|
+
}
|
|
401
|
+
this.#reservations.set(key, length);
|
|
402
|
+
return true;
|
|
403
|
+
});
|
|
404
|
+
if (!admitted) {
|
|
405
|
+
await this.#withAdmission(() => { this.#writing.delete(key); });
|
|
356
406
|
return null;
|
|
357
407
|
}
|
|
358
|
-
this.#writing.add(key);
|
|
359
408
|
const directory = path.join(this.#root, String(infoHash));
|
|
360
409
|
const where = path.join(directory, String(fileIndex));
|
|
361
410
|
const partial = `${where}.partial`;
|
|
@@ -367,6 +416,9 @@ export class CompletedFiles {
|
|
|
367
416
|
let written = 0;
|
|
368
417
|
try {
|
|
369
418
|
for await (const chunk of source) {
|
|
419
|
+
if (written + chunk.length > length) {
|
|
420
|
+
throw new Error("Whole-file read exceeded its declared length");
|
|
421
|
+
}
|
|
370
422
|
await handle.write(chunk);
|
|
371
423
|
written += chunk.length;
|
|
372
424
|
}
|
|
@@ -382,14 +434,24 @@ export class CompletedFiles {
|
|
|
382
434
|
}
|
|
383
435
|
await fs.rename(partial, where);
|
|
384
436
|
const file = { path: where, length, name: String(name ?? fileIndex) };
|
|
385
|
-
this.#
|
|
437
|
+
await this.#withAdmission(async () => {
|
|
438
|
+
this.#reservations.delete(key);
|
|
439
|
+
this.#held.set(key, file);
|
|
440
|
+
this.#touch(key);
|
|
441
|
+
this.#writing.delete(key);
|
|
442
|
+
await this.#trimToAllowance();
|
|
443
|
+
});
|
|
386
444
|
await this.#writeManifest(infoHash);
|
|
387
|
-
return
|
|
445
|
+
return this.#held.get(key) ?? null;
|
|
388
446
|
} catch {
|
|
389
447
|
await fs.rm(partial, REMOVAL).catch(() => undefined);
|
|
390
448
|
return null;
|
|
391
449
|
} finally {
|
|
392
|
-
this.#
|
|
450
|
+
await this.#withAdmission(async () => {
|
|
451
|
+
this.#reservations.delete(key);
|
|
452
|
+
this.#writing.delete(key);
|
|
453
|
+
await this.#trimToAllowance();
|
|
454
|
+
});
|
|
393
455
|
}
|
|
394
456
|
}
|
|
395
457
|
|
package/services/storage/wire.js
CHANGED
|
@@ -56,9 +56,9 @@ import { availableMemoryBytes } from "./machine-memory.js";
|
|
|
56
56
|
* @param {import("./MachineBudget.js").BudgetPolicy} [params.policy] - What the
|
|
57
57
|
* operator has said this proxy may take. Absent, the measured default.
|
|
58
58
|
* @param {{ info: Function, warn?: Function }} [params.logger]
|
|
59
|
-
* @returns {{ revise: () => Promise<unknown>, segmentBytes: () => number, diagnostics: object | null, describe: () => string }}
|
|
60
|
-
* What the segments may hold is asked for rather than pushed:
|
|
61
|
-
* first revision,
|
|
59
|
+
* @returns {{ revise: () => Promise<unknown>, segmentBytes: () => number | null, diagnostics: object | null, describe: () => string }}
|
|
60
|
+
* What the segments may hold is asked for rather than pushed: null until the
|
|
61
|
+
* first revision, then the measured allowance, including a measured zero.
|
|
62
62
|
*/
|
|
63
63
|
export function wireMachineBudget({
|
|
64
64
|
segmentStore,
|
|
@@ -101,7 +101,7 @@ export function wireMachineBudget({
|
|
|
101
101
|
const segmentsOn = deviceOf(segmentStore.root);
|
|
102
102
|
defineDisk(segmentsOn, segmentStore.root);
|
|
103
103
|
|
|
104
|
-
let segmentBytes =
|
|
104
|
+
let segmentBytes = null;
|
|
105
105
|
budget.register({
|
|
106
106
|
name: "segments",
|
|
107
107
|
resource: segmentsOn,
|