@torrent-tv/proxy 2.80.19 → 2.81.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 +12 -0
- package/package.json +1 -1
- package/research/double-spawn-2026-09-10.md +171 -0
- package/services/disk/DiskSpace.js +146 -0
- package/services/disk/wire.js +60 -0
- package/services/encode/EncodeRun.js +25 -3
- package/services/encode/SegmentStore.js +137 -9
- package/services/hls-session-manager.js +26 -45
- package/services/orchestrators/EncodeOrchestrator.js +4 -1
- package/services/piece-store/allowance.js +107 -0
- package/services/piece-store/piece-disk-store.js +365 -0
- package/services/piece-store/shared-piece-store.js +1549 -1535
- package/services/torrent-worker/client.js +32 -0
- package/services/torrent-worker/pool-adapter.js +15 -0
- package/services/torrent-worker/protocol.js +9 -0
- package/services/torrent-worker/worker.js +8 -1
- package/services/viewer/positions.js +48 -0
- package/test/audio-inventory.test.js +176 -176
- package/test/auto-quality-step.test.js +514 -514
- package/test/concurrent-cost.test.js +138 -138
- package/test/coverage-follows-the-disk.test.js +191 -191
- package/test/coverage-map.test.js +195 -195
- package/test/declared-tracks.test.js +35 -35
- package/test/disk-space.test.js +138 -0
- package/test/encode-orchestrator.test.js +0 -3
- package/test/encode-run.test.js +5 -12
- package/test/held-request-width.test.js +155 -155
- package/test/helpers/encode-run.js +2 -2
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +281 -220
- package/test/orchestrator-wired.test.js +199 -199
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +267 -0
- package/test/piece-reader.test.js +4 -4
- package/test/piece-store-eviction.test.js +17 -17
- package/test/piece-store-reservations.test.js +20 -1
- package/test/piece-store-slow-disk.test.js +16 -1
- package/test/read-window.test.js +6 -6
- package/test/run-intervals.test.js +100 -100
- package/test/seek-landing.test.js +109 -109
- package/test/segment-store-eviction.test.js +232 -0
- package/test/segments-are-shared.test.js +1 -1
- package/test/shared-piece-store.test.js +12 -12
- package/test/sidecar-naming.test.js +142 -142
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-walk.test.js +369 -369
- package/test/subtitle-defaults.test.js +97 -97
- package/test/subtitle-track-numbering.test.js +370 -370
- package/test/tail-duplication.test.js +167 -167
- package/test/tracks-begin-together.test.js +195 -195
- package/test/two-viewers-one-picture.test.js +374 -374
- package/test/video-facts.test.js +102 -102
- package/test/wedge-certainty.test.js +131 -131
- package/services/piece-store/disk-tier.js +0 -151
|
@@ -27,7 +27,7 @@ async function makeStore(capacityPieces) {
|
|
|
27
27
|
const store = new SharedPieceStore(PIECE, {
|
|
28
28
|
length: PIECE * 64,
|
|
29
29
|
memoryBytes: PIECE * capacityPieces,
|
|
30
|
-
|
|
30
|
+
path: directory,
|
|
31
31
|
name: "eviction-test"
|
|
32
32
|
});
|
|
33
33
|
return { store, directory };
|
|
@@ -89,7 +89,7 @@ test("concurrent puts past capacity never hand two pieces the same slot", async
|
|
|
89
89
|
);
|
|
90
90
|
} finally {
|
|
91
91
|
await store.destroy();
|
|
92
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
92
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -108,7 +108,7 @@ test("a piece caught mid-spill is waited for, not reported missing", async () =>
|
|
|
108
108
|
assert.ok(bytes.equals(pieceOf(0)), "piece 0 came back wrong while being spilled");
|
|
109
109
|
} finally {
|
|
110
110
|
await store.destroy();
|
|
111
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
111
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -129,7 +129,7 @@ test("pinned pieces are never evicted, and the pin count is reported", async ()
|
|
|
129
129
|
assert.equal(store.stats().pinned, 0, "unpin is not reflected in the stats");
|
|
130
130
|
} finally {
|
|
131
131
|
await store.destroy();
|
|
132
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
132
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
133
133
|
}
|
|
134
134
|
});
|
|
135
135
|
|
|
@@ -188,7 +188,7 @@ test("the store says why it spills: what is asked of it, what it had to take, ho
|
|
|
188
188
|
assert.equal(store.stats().demand.readers, 0, "a reader that ends stops being counted");
|
|
189
189
|
} finally {
|
|
190
190
|
store.destroy(() => undefined);
|
|
191
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
191
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -226,7 +226,7 @@ test("a piece nobody has declared does not push out one that is being read", asy
|
|
|
226
226
|
}
|
|
227
227
|
} finally {
|
|
228
228
|
store.destroy(() => undefined);
|
|
229
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
229
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
230
230
|
}
|
|
231
231
|
});
|
|
232
232
|
|
|
@@ -245,7 +245,7 @@ test("before any reader has declared anything, an arriving piece still goes to m
|
|
|
245
245
|
assert.ok(stats.spills > 0, "the store filled and evicted, as it did before");
|
|
246
246
|
} finally {
|
|
247
247
|
store.destroy(() => undefined);
|
|
248
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
248
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
249
249
|
}
|
|
250
250
|
});
|
|
251
251
|
|
|
@@ -274,7 +274,7 @@ test("the store asks for what its readers declared, and for a whole window at le
|
|
|
274
274
|
assert.equal(store.wantedBytes, 50 * PIECE);
|
|
275
275
|
} finally {
|
|
276
276
|
store.destroy(() => undefined);
|
|
277
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
277
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
278
278
|
}
|
|
279
279
|
});
|
|
280
280
|
|
|
@@ -300,7 +300,7 @@ test("the floor the ceiling will not fall below is the same union, not the wides
|
|
|
300
300
|
);
|
|
301
301
|
} finally {
|
|
302
302
|
store.destroy(() => undefined);
|
|
303
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
303
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
304
304
|
}
|
|
305
305
|
});
|
|
306
306
|
|
|
@@ -333,7 +333,7 @@ test("a block is re-used instead of a new one being allocated for every piece",
|
|
|
333
333
|
}
|
|
334
334
|
} finally {
|
|
335
335
|
store.destroy(() => undefined);
|
|
336
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
336
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
337
337
|
}
|
|
338
338
|
});
|
|
339
339
|
|
|
@@ -360,7 +360,7 @@ test("a spare block is given up once it has sat longer than the store's own work
|
|
|
360
360
|
assert.equal(store.stats().blocksReleased, released);
|
|
361
361
|
} finally {
|
|
362
362
|
store.destroy(() => undefined);
|
|
363
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
363
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
364
364
|
}
|
|
365
365
|
});
|
|
366
366
|
|
|
@@ -388,7 +388,7 @@ test("evicting a piece the disk already holds costs no second write", async () =
|
|
|
388
388
|
assert.ok((await get(store, 0)).equals(pieceOf(0)));
|
|
389
389
|
} finally {
|
|
390
390
|
store.destroy(() => undefined);
|
|
391
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
391
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
392
392
|
}
|
|
393
393
|
});
|
|
394
394
|
|
|
@@ -411,7 +411,7 @@ test("a store whose readers have gone asks for nothing, one that never had them
|
|
|
411
411
|
assert.ok(store.wantedBytes < opening, "a store with no readers left asks for nothing");
|
|
412
412
|
} finally {
|
|
413
413
|
store.destroy(() => undefined);
|
|
414
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
414
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
415
415
|
}
|
|
416
416
|
});
|
|
417
417
|
|
|
@@ -434,7 +434,7 @@ test("the allowance is never cut below one reader's whole window", async () => {
|
|
|
434
434
|
assert.equal(obeyed.belowAWindow, false);
|
|
435
435
|
} finally {
|
|
436
436
|
store.destroy(() => undefined);
|
|
437
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
437
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
438
438
|
}
|
|
439
439
|
});
|
|
440
440
|
|
|
@@ -478,7 +478,7 @@ test("the pool stays the size it is allowed to be, however many pieces pass thro
|
|
|
478
478
|
}
|
|
479
479
|
} finally {
|
|
480
480
|
store.destroy(() => undefined);
|
|
481
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
481
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
482
482
|
}
|
|
483
483
|
});
|
|
484
484
|
|
|
@@ -512,7 +512,7 @@ test("a piece wanted later than the one it would displace goes to disk instead",
|
|
|
512
512
|
}
|
|
513
513
|
} finally {
|
|
514
514
|
store.destroy(() => undefined);
|
|
515
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
515
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -542,6 +542,6 @@ test("a piece the map wants more displaces one it wants less, however far away i
|
|
|
542
542
|
assert.ok((await get(store, 95)).equals(pieceOf(95)));
|
|
543
543
|
} finally {
|
|
544
544
|
store.destroy(() => undefined);
|
|
545
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
545
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
546
546
|
}
|
|
547
547
|
});
|
|
@@ -39,6 +39,25 @@ function makeDisk({ failWrite = false, failRead = false, holdWrites = false } =
|
|
|
39
39
|
get size() {
|
|
40
40
|
return stored.size;
|
|
41
41
|
},
|
|
42
|
+
get bytes() {
|
|
43
|
+
let total = 0;
|
|
44
|
+
for (const value of stored.values()) {
|
|
45
|
+
total += value.length;
|
|
46
|
+
}
|
|
47
|
+
return total;
|
|
48
|
+
},
|
|
49
|
+
get path() {
|
|
50
|
+
return ".";
|
|
51
|
+
},
|
|
52
|
+
get allowanceBytes() {
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
reviseAllowance() {
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
stats() {
|
|
59
|
+
return { pieces: stored.size, bytes: this.bytes, allowanceBytes: null, evictions: 0 };
|
|
60
|
+
},
|
|
42
61
|
has(index) {
|
|
43
62
|
return stored.has(index);
|
|
44
63
|
},
|
|
@@ -247,7 +266,7 @@ test("a piece written back to memory is not resurrected on disk by its own spill
|
|
|
247
266
|
|
|
248
267
|
// The swarm hands piece 0 back while that write is still going. The store
|
|
249
268
|
// must drop the disk copy AFTER the write has recorded it, not before —
|
|
250
|
-
// `
|
|
269
|
+
// `PieceDiskStore.write` adds the index on completion, so an early forget is
|
|
251
270
|
// undone and the next read of piece 0 comes back from before the rewrite.
|
|
252
271
|
let settled = false;
|
|
253
272
|
const rewritten = put(store, 0);
|
|
@@ -55,6 +55,21 @@ function heldDisk() {
|
|
|
55
55
|
get size() {
|
|
56
56
|
return stored.size;
|
|
57
57
|
},
|
|
58
|
+
get bytes() {
|
|
59
|
+
return 0;
|
|
60
|
+
},
|
|
61
|
+
get path() {
|
|
62
|
+
return ".";
|
|
63
|
+
},
|
|
64
|
+
get allowanceBytes() {
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
67
|
+
reviseAllowance() {
|
|
68
|
+
return null;
|
|
69
|
+
},
|
|
70
|
+
stats() {
|
|
71
|
+
return { pieces: stored.size, bytes: 0, allowanceBytes: null, evictions: 0 };
|
|
72
|
+
},
|
|
58
73
|
has: (index) => stored.has(index),
|
|
59
74
|
forget: (index) => stored.delete(index),
|
|
60
75
|
write(index) {
|
|
@@ -139,6 +154,6 @@ test("memory in use never runs past the allowance while the disk is behind", asy
|
|
|
139
154
|
assert.ok(store.stats().blocksInUse <= capacity + 1, "the pool did not settle");
|
|
140
155
|
} finally {
|
|
141
156
|
store.destroy(() => undefined);
|
|
142
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
157
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
143
158
|
}
|
|
144
159
|
});
|
package/test/read-window.test.js
CHANGED
|
@@ -154,7 +154,7 @@ test("a read that is not stopped claims nothing", async () => {
|
|
|
154
154
|
await iterator.return();
|
|
155
155
|
} finally {
|
|
156
156
|
store.destroy(() => undefined);
|
|
157
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
157
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
|
|
@@ -165,7 +165,7 @@ test("a finished read leaves nothing selected", async () => {
|
|
|
165
165
|
assert.deepEqual(torrent.held, [], "the read kept its claim after finishing");
|
|
166
166
|
} finally {
|
|
167
167
|
store.destroy(() => undefined);
|
|
168
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
168
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
171
|
|
|
@@ -188,7 +188,7 @@ test("an abandoned read leaves nothing selected", async () => {
|
|
|
188
188
|
assert.deepEqual(torrent.held, [], "an abandoned read kept its claim forever");
|
|
189
189
|
} finally {
|
|
190
190
|
store.destroy(() => undefined);
|
|
191
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
191
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -238,7 +238,7 @@ test("two stopped readers add up, and one leaving takes only its own", async ()
|
|
|
238
238
|
assert.deepEqual(torrent.held, [], "the last reader left something behind");
|
|
239
239
|
} finally {
|
|
240
240
|
store.destroy(() => undefined);
|
|
241
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
241
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
242
242
|
}
|
|
243
243
|
});
|
|
244
244
|
|
|
@@ -275,7 +275,7 @@ test("criticality marks the window being waited for, not the whole range", async
|
|
|
275
275
|
await iterator.return();
|
|
276
276
|
} finally {
|
|
277
277
|
store.destroy(() => undefined);
|
|
278
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
278
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
279
279
|
}
|
|
280
280
|
});
|
|
281
281
|
|
|
@@ -306,7 +306,7 @@ test("a reader that is abandoned mid-fragment does not keep the piece pinned", a
|
|
|
306
306
|
);
|
|
307
307
|
} finally {
|
|
308
308
|
store.destroy(() => undefined);
|
|
309
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
309
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
310
310
|
}
|
|
311
311
|
});
|
|
312
312
|
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How far a run may work, and who decides it.
|
|
3
|
-
*
|
|
4
|
-
* These cases used to be asked of `planRunInterval`, a second authority inside
|
|
5
|
-
* the session manager that answered by its own rules: it walked the whole track
|
|
6
|
-
* for the first free number, MOVED the start there, and counted every live run
|
|
7
|
-
* as claiming up to `head + look-ahead`. It contradicted the plan directly, and
|
|
8
|
-
* the two together produced the field oscillation of 2026-09-05 — the plan
|
|
9
|
-
* commanded a start at #46, this moved it to #78, the plan killed the run for
|
|
10
|
-
* standing outside the window it had asked for, and the same start was
|
|
11
|
-
* commanded again, 350-700ms per cycle, no segment ever produced, the viewer's
|
|
12
|
-
* picture stopped for 125 seconds.
|
|
13
|
-
*
|
|
14
|
-
* It is gone. WHERE a run starts is the plan's decision and nothing moves it;
|
|
15
|
-
* HOW FAR it may work is a fact of the one coverage map, and that is what these
|
|
16
|
-
* cases now ask. The map is the same object the plan reads, so there is no
|
|
17
|
-
* second set of rules for the two to disagree about.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
23
|
-
|
|
24
|
-
/** A stand-in for a run: the map identifies one by being it. */
|
|
25
|
-
function run(name) {
|
|
26
|
-
return { name };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("with nothing made, a run gets everything from where it was asked", () => {
|
|
30
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
31
|
-
|
|
32
|
-
assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
|
|
33
|
-
assert.equal(coverage.firstGapFrom(0), 0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("a run stops before material that is already made", () => {
|
|
37
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
38
|
-
coverage.setReady([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
|
39
|
-
|
|
40
|
-
assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("a run stops before a stretch another live run was given", () => {
|
|
44
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
45
|
-
const other = run("other");
|
|
46
|
-
coverage.claim(other, 40, 60);
|
|
47
|
-
|
|
48
|
-
assert.equal(coverage.freeRunFrom(0), 40, "0..39, and the other run's claim begins at 40");
|
|
49
|
-
assert.equal(coverage.firstGapFrom(45), 61, "the first thing nobody holds after it");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("a run's own claim does not hold it back", () => {
|
|
53
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
54
|
-
const mine = run("mine");
|
|
55
|
-
coverage.claim(mine, 40, 60);
|
|
56
|
-
|
|
57
|
-
assert.equal(
|
|
58
|
-
coverage.freeRunFrom(40, mine),
|
|
59
|
-
60,
|
|
60
|
-
"asked by the run that holds it, the stretch is its own to work through"
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("a run that has ended holds nothing back", () => {
|
|
65
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
66
|
-
const dead = run("dead");
|
|
67
|
-
coverage.claim(dead, 40, 60);
|
|
68
|
-
coverage.release(dead);
|
|
69
|
-
|
|
70
|
-
assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("what a dead run DID finish stays made", () => {
|
|
74
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
75
|
-
const dead = run("dead");
|
|
76
|
-
coverage.claim(dead, 40, 60);
|
|
77
|
-
coverage.setReady([40, 41, 42]);
|
|
78
|
-
coverage.release(dead);
|
|
79
|
-
|
|
80
|
-
assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("nothing left to make is answered with nothing", () => {
|
|
84
|
-
const coverage = new CoverageMap({ segmentCount: 5 });
|
|
85
|
-
coverage.setReady([0, 1, 2, 3, 4]);
|
|
86
|
-
|
|
87
|
-
assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("a run without an end does not take the film away from a viewer further in", () => {
|
|
91
|
-
// The case that used to need a second authority's `head + look-ahead` guess.
|
|
92
|
-
// A run's claim is now the stretch the plan GAVE it, so a viewer opening the
|
|
93
|
-
// same film in the middle finds their own position free.
|
|
94
|
-
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
95
|
-
const first = run("first");
|
|
96
|
-
coverage.claim(first, 0, 499);
|
|
97
|
-
|
|
98
|
-
assert.equal(coverage.firstGapFrom(500), 500, "the second viewer's own position is free");
|
|
99
|
-
assert.equal(coverage.freeRunFrom(500), 500, "and everything from there to the end");
|
|
100
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file How far a run may work, and who decides it.
|
|
3
|
+
*
|
|
4
|
+
* These cases used to be asked of `planRunInterval`, a second authority inside
|
|
5
|
+
* the session manager that answered by its own rules: it walked the whole track
|
|
6
|
+
* for the first free number, MOVED the start there, and counted every live run
|
|
7
|
+
* as claiming up to `head + look-ahead`. It contradicted the plan directly, and
|
|
8
|
+
* the two together produced the field oscillation of 2026-09-05 — the plan
|
|
9
|
+
* commanded a start at #46, this moved it to #78, the plan killed the run for
|
|
10
|
+
* standing outside the window it had asked for, and the same start was
|
|
11
|
+
* commanded again, 350-700ms per cycle, no segment ever produced, the viewer's
|
|
12
|
+
* picture stopped for 125 seconds.
|
|
13
|
+
*
|
|
14
|
+
* It is gone. WHERE a run starts is the plan's decision and nothing moves it;
|
|
15
|
+
* HOW FAR it may work is a fact of the one coverage map, and that is what these
|
|
16
|
+
* cases now ask. The map is the same object the plan reads, so there is no
|
|
17
|
+
* second set of rules for the two to disagree about.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
23
|
+
|
|
24
|
+
/** A stand-in for a run: the map identifies one by being it. */
|
|
25
|
+
function run(name) {
|
|
26
|
+
return { name };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("with nothing made, a run gets everything from where it was asked", () => {
|
|
30
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
31
|
+
|
|
32
|
+
assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
|
|
33
|
+
assert.equal(coverage.firstGapFrom(0), 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a run stops before material that is already made", () => {
|
|
37
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
38
|
+
coverage.setReady([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
|
39
|
+
|
|
40
|
+
assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a run stops before a stretch another live run was given", () => {
|
|
44
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
45
|
+
const other = run("other");
|
|
46
|
+
coverage.claim(other, 40, 60);
|
|
47
|
+
|
|
48
|
+
assert.equal(coverage.freeRunFrom(0), 40, "0..39, and the other run's claim begins at 40");
|
|
49
|
+
assert.equal(coverage.firstGapFrom(45), 61, "the first thing nobody holds after it");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a run's own claim does not hold it back", () => {
|
|
53
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
54
|
+
const mine = run("mine");
|
|
55
|
+
coverage.claim(mine, 40, 60);
|
|
56
|
+
|
|
57
|
+
assert.equal(
|
|
58
|
+
coverage.freeRunFrom(40, mine),
|
|
59
|
+
60,
|
|
60
|
+
"asked by the run that holds it, the stretch is its own to work through"
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a run that has ended holds nothing back", () => {
|
|
65
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
66
|
+
const dead = run("dead");
|
|
67
|
+
coverage.claim(dead, 40, 60);
|
|
68
|
+
coverage.release(dead);
|
|
69
|
+
|
|
70
|
+
assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("what a dead run DID finish stays made", () => {
|
|
74
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
75
|
+
const dead = run("dead");
|
|
76
|
+
coverage.claim(dead, 40, 60);
|
|
77
|
+
coverage.setReady([40, 41, 42]);
|
|
78
|
+
coverage.release(dead);
|
|
79
|
+
|
|
80
|
+
assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("nothing left to make is answered with nothing", () => {
|
|
84
|
+
const coverage = new CoverageMap({ segmentCount: 5 });
|
|
85
|
+
coverage.setReady([0, 1, 2, 3, 4]);
|
|
86
|
+
|
|
87
|
+
assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a run without an end does not take the film away from a viewer further in", () => {
|
|
91
|
+
// The case that used to need a second authority's `head + look-ahead` guess.
|
|
92
|
+
// A run's claim is now the stretch the plan GAVE it, so a viewer opening the
|
|
93
|
+
// same film in the middle finds their own position free.
|
|
94
|
+
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
95
|
+
const first = run("first");
|
|
96
|
+
coverage.claim(first, 0, 499);
|
|
97
|
+
|
|
98
|
+
assert.equal(coverage.firstGapFrom(500), 500, "the second viewer's own position is free");
|
|
99
|
+
assert.equal(coverage.freeRunFrom(500), 500, "and everything from there to the end");
|
|
100
|
+
});
|