@torrent-tv/proxy 2.80.19 → 2.81.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/research/double-spawn-2026-09-10.md +171 -0
  4. package/services/disk/DiskSpace.js +150 -0
  5. package/services/disk/wire.js +60 -0
  6. package/services/encode/EncodeRun.js +25 -3
  7. package/services/encode/SegmentStore.js +137 -9
  8. package/services/hls-session-manager.js +26 -45
  9. package/services/orchestrators/EncodeOrchestrator.js +4 -1
  10. package/services/piece-store/allowance.js +107 -0
  11. package/services/piece-store/piece-disk-store.js +365 -0
  12. package/services/piece-store/shared-piece-store.js +1549 -1535
  13. package/services/torrent-worker/client.js +32 -0
  14. package/services/torrent-worker/pool-adapter.js +15 -0
  15. package/services/torrent-worker/protocol.js +9 -0
  16. package/services/torrent-worker/worker.js +8 -1
  17. package/services/viewer/positions.js +48 -0
  18. package/test/audio-inventory.test.js +176 -176
  19. package/test/auto-quality-step.test.js +514 -514
  20. package/test/concurrent-cost.test.js +138 -138
  21. package/test/coverage-follows-the-disk.test.js +191 -191
  22. package/test/coverage-map.test.js +195 -195
  23. package/test/declared-tracks.test.js +35 -35
  24. package/test/disk-space.test.js +150 -0
  25. package/test/encode-orchestrator.test.js +0 -3
  26. package/test/encode-run.test.js +5 -12
  27. package/test/held-request-width.test.js +155 -155
  28. package/test/helpers/encode-run.js +2 -2
  29. package/test/matroska-blocks.test.js +0 -0
  30. package/test/matroska-cues-track.test.js +192 -192
  31. package/test/mp4-composition-times.test.js +0 -0
  32. package/test/mp4-subtitles.test.js +173 -173
  33. package/test/one-authority.test.js +281 -220
  34. package/test/orchestrator-wired.test.js +199 -199
  35. package/test/packet-witness-ring.test.js +236 -236
  36. package/test/packet-witness.test.js +148 -148
  37. package/test/piece-disk-store.test.js +267 -0
  38. package/test/piece-reader.test.js +4 -4
  39. package/test/piece-store-eviction.test.js +17 -17
  40. package/test/piece-store-reservations.test.js +20 -1
  41. package/test/piece-store-slow-disk.test.js +16 -1
  42. package/test/read-window.test.js +6 -6
  43. package/test/run-intervals.test.js +100 -100
  44. package/test/seek-landing.test.js +109 -109
  45. package/test/segment-store-eviction.test.js +232 -0
  46. package/test/segments-are-shared.test.js +1 -1
  47. package/test/shared-piece-store.test.js +12 -12
  48. package/test/sidecar-naming.test.js +142 -142
  49. package/test/subtitle-cue-framing.test.js +200 -200
  50. package/test/subtitle-cue-walk.test.js +369 -369
  51. package/test/subtitle-defaults.test.js +97 -97
  52. package/test/subtitle-track-numbering.test.js +370 -370
  53. package/test/tail-duplication.test.js +167 -167
  54. package/test/tracks-begin-together.test.js +195 -195
  55. package/test/two-viewers-one-picture.test.js +374 -374
  56. package/test/video-facts.test.js +102 -102
  57. package/test/wedge-certainty.test.js +131 -131
  58. 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
- spillDirectory: directory,
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
- // `DiskTier.write` adds the index on completion, so an early forget is
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
  });
@@ -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
+ });