@torrent-tv/proxy 2.9.73 → 2.9.74
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 +7 -0
- package/package.json +5 -2
- package/services/piece-store/disk-tier.js +151 -0
- package/services/piece-store/piece-lru.js +150 -0
- package/services/piece-store/shared-piece-store.js +315 -0
- package/services/torrent-pool.js +18 -2
- package/services/torrent-worker/channel.js +17 -13
- package/services/torrent-worker/install-webrtc-shim.js +32 -0
- package/services/torrent-worker/webrtc-shim.js +219 -0
- package/services/torrent-worker/worker.js +12 -1
- package/test/piece-lru.test.js +109 -0
- package/test/shared-piece-store.test.js +226 -0
- package/test/webrtc-shim.test.js +150 -0
- package/test/worker-channel.test.js +120 -0
package/services/torrent-pool.js
CHANGED
|
@@ -12,6 +12,7 @@ import path from "node:path";
|
|
|
12
12
|
import { rmSync, statfsSync } from "node:fs";
|
|
13
13
|
import WebTorrent from "webtorrent";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
|
+
import { SharedPieceStore } from "./piece-store/shared-piece-store.js";
|
|
15
16
|
|
|
16
17
|
// WebTorrent's default download root (see webtorrent lib/torrent.js: TMP =
|
|
17
18
|
// path.join(os.tmpdir(), 'webtorrent')). We use the default store, so all
|
|
@@ -308,6 +309,9 @@ export class TorrentPool {
|
|
|
308
309
|
/** Global disk cap in bytes (0 = disabled). */
|
|
309
310
|
#maxDiskBytes = 0;
|
|
310
311
|
|
|
312
|
+
/** Memory budget per torrent for resident pieces; undefined = store default. */
|
|
313
|
+
#memoryBytes;
|
|
314
|
+
|
|
311
315
|
/** Periodic disk-cap enforcement timer. */
|
|
312
316
|
#diskSweepTimer = null;
|
|
313
317
|
|
|
@@ -323,7 +327,9 @@ export class TorrentPool {
|
|
|
323
327
|
* default is computed from free disk (min(10 GB, half free)). Pass 0 to
|
|
324
328
|
* disable the cap.
|
|
325
329
|
*/
|
|
326
|
-
constructor({ maxDiskBytes } = {}) {
|
|
330
|
+
constructor({ maxDiskBytes, memoryBytes } = {}) {
|
|
331
|
+
this.#memoryBytes = Number.isFinite(memoryBytes) && memoryBytes > 0 ? memoryBytes : undefined;
|
|
332
|
+
|
|
327
333
|
// Sweep orphaned torrent data left by a previous hard kill (no graceful
|
|
328
334
|
// shutdown ran, so destroyAll never cleaned the store). Safe here: no
|
|
329
335
|
// torrents are loaded yet at construction. Best-effort, synchronous so it
|
|
@@ -558,7 +564,17 @@ export class TorrentPool {
|
|
|
558
564
|
reject(error);
|
|
559
565
|
};
|
|
560
566
|
this.client.once("error", onError);
|
|
561
|
-
|
|
567
|
+
// Our own store, and WebTorrent's piece cache switched off in front of it
|
|
568
|
+
// (`storeCacheSlots: 0`). That cache is what made the thread split fail:
|
|
569
|
+
// it hands out the buffer it keeps and re-slices it later, so moving a
|
|
570
|
+
// piece across threads detached memory still in use. Ours owns what it
|
|
571
|
+
// hands out, holds pieces in shared memory the main thread can read
|
|
572
|
+
// directly, and spills to disk instead of losing them.
|
|
573
|
+
this.client.add(torrentId, {
|
|
574
|
+
store: SharedPieceStore,
|
|
575
|
+
storeCacheSlots: 0,
|
|
576
|
+
storeOpts: { memoryBytes: this.#memoryBytes }
|
|
577
|
+
}, (readyTorrent) => {
|
|
562
578
|
this.client.off("error", onError);
|
|
563
579
|
this.torrents.set(key, readyTorrent);
|
|
564
580
|
this.#lastAccess.set(readyTorrent, Date.now());
|
|
@@ -183,20 +183,24 @@ export function createSendStream({ port, requestId }) {
|
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
inFlight += 1;
|
|
186
|
-
//
|
|
187
|
-
// of the design, and the difference between 4.8 ms and 37 ms per 10 MB.
|
|
186
|
+
// Copy into memory this transport allocated, then transfer THAT.
|
|
188
187
|
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
188
|
+
// Transferring the caller's buffer is faster and was what shipped, but it
|
|
189
|
+
// is only correct if the caller owns the memory outright — and no test at
|
|
190
|
+
// this boundary can establish that. 2.9.73 tried to decide it by
|
|
191
|
+
// inspection (`byteOffset === 0 && byteLength === buffer.byteLength`),
|
|
192
|
+
// which answers "does this view cover its region", not "did we allocate
|
|
193
|
+
// it". WebTorrent's piece cache returns a buffer covering its whole
|
|
194
|
+
// region and keeps using it, so the check passed and the transfer
|
|
195
|
+
// detached the cache: every later read failed with a detached
|
|
196
|
+
// ArrayBuffer, and because the error never reached the reader it looked
|
|
197
|
+
// like an empty file (`Stream ends prematurely at 0`).
|
|
198
|
+
//
|
|
199
|
+
// The copy costs 3.64 ms per 8 MB on the field host, against 37 ms for a
|
|
200
|
+
// structured clone. It disappears entirely for pieces read through the
|
|
201
|
+
// shared piece store, which the main thread reads by offset without any
|
|
202
|
+
// hand-over at all.
|
|
203
|
+
const payload = new Uint8Array(bytes);
|
|
200
204
|
port.postMessage(
|
|
201
205
|
{ type: Event.CHUNK, id: requestId, bytes: payload },
|
|
202
206
|
[payload.buffer]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Point `webrtc-polyfill` at the JavaScript WebRTC stack, in this thread only.
|
|
3
|
+
*
|
|
4
|
+
* Imported for its side effect, and imported FIRST by `worker.js` — ES module
|
|
5
|
+
* bodies run in import order, so registering the hook here happens before
|
|
6
|
+
* `torrent-pool.js` pulls in WebTorrent, which is what reaches
|
|
7
|
+
* `@thaunknown/simple-peer` and, through it, `webrtc-polyfill`.
|
|
8
|
+
*
|
|
9
|
+
* Scope is deliberately narrow. The hook lives in the worker's isolate, so the
|
|
10
|
+
* main thread keeps using node-datachannel directly for the browser's video
|
|
11
|
+
* channel — see `webrtc-shim.js` for why the two cannot share one process
|
|
12
|
+
* isolate at all.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { registerHooks } from "node:module";
|
|
16
|
+
|
|
17
|
+
const SHIM_URL = new URL("./webrtc-shim.js", import.meta.url).href;
|
|
18
|
+
|
|
19
|
+
registerHooks({
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} specifier
|
|
22
|
+
* @param {object} context
|
|
23
|
+
* @param {(specifier: string, context: object) => { url: string }} nextResolve
|
|
24
|
+
* @returns {{ url: string, shortCircuit?: boolean }}
|
|
25
|
+
*/
|
|
26
|
+
resolve(specifier, context, nextResolve) {
|
|
27
|
+
if (specifier === "webrtc-polyfill") {
|
|
28
|
+
return { url: SHIM_URL, shortCircuit: true };
|
|
29
|
+
}
|
|
30
|
+
return nextResolve(specifier, context);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `webrtc-polyfill`'s interface, served by a pure-JavaScript WebRTC stack.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists.** node-datachannel is native, and its native side is not
|
|
5
|
+
* safe to use from two V8 isolates at once: the process aborts with
|
|
6
|
+
* `HandleScope: Entering the V8 API without proper locking in place`. Measured
|
|
7
|
+
* on win32/x64 and linux/arm64 alike — one isolate is fine (either the main
|
|
8
|
+
* thread or a worker), two at the same time is fatal, and preloading in both
|
|
9
|
+
* does not help. The upstream issue about workers (#129) was closed in 0.4.0
|
|
10
|
+
* but only covers use from a worker ALONE, which does work.
|
|
11
|
+
*
|
|
12
|
+
* We need it twice: `webrtc-manager.js` runs the video channel to the browser on
|
|
13
|
+
* the main thread, and the torrent client — which is now on its own thread —
|
|
14
|
+
* creates peer connections of its own to announce on `wss://` trackers. Before
|
|
15
|
+
* the thread split both lived in one isolate and nothing was wrong; afterwards
|
|
16
|
+
* any torrent carrying a wss tracker took the whole proxy down.
|
|
17
|
+
*
|
|
18
|
+
* So the native stack stays where it earns its keep — the main thread, carrying
|
|
19
|
+
* video — and the torrent's trackers get a JavaScript implementation, where the
|
|
20
|
+
* traffic is a handful of signalling messages. `services/torrent-worker/worker.js`
|
|
21
|
+
* installs a module resolution hook that points `webrtc-polyfill` here; nothing
|
|
22
|
+
* outside the worker thread is affected, and no dependency is patched (the addon
|
|
23
|
+
* installs with `--ignore-scripts`, so a postinstall patch would never run).
|
|
24
|
+
*
|
|
25
|
+
* **What the shim has to fix.** werift's peer connection matches the interface
|
|
26
|
+
* `simple-peer` expects, but its data channel differs in two ways that matter:
|
|
27
|
+
*
|
|
28
|
+
* - it has no `binaryType`, and hands `onmessage` a `Buffer`. `simple-peer`
|
|
29
|
+
* only recognises `ArrayBuffer`; anything else goes through `text2arr`,
|
|
30
|
+
* which would corrupt every byte of torrent payload.
|
|
31
|
+
* - it has no `onbufferedamountlow`. `simple-peer` builds its backpressure on
|
|
32
|
+
* that event, so without it a send that hits the high-water mark never
|
|
33
|
+
* resumes.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
RTCPeerConnection as WeriftPeerConnection,
|
|
38
|
+
RTCIceCandidate
|
|
39
|
+
} from "werift";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A session description built the way browsers build it — from one object.
|
|
43
|
+
*
|
|
44
|
+
* werift's own class takes two positional arguments, `(sdp, type)`. Callers
|
|
45
|
+
* written against the browser pass `{ type, sdp }`, so with werift's class the
|
|
46
|
+
* type lands in the sdp slot and the description is rejected: field 2026-08-03,
|
|
47
|
+
* wss announces succeeded but every peer connection died with "Connection
|
|
48
|
+
* error: invalid sessionDescription".
|
|
49
|
+
*
|
|
50
|
+
* `RTCIceCandidate` needs no such treatment — werift already takes an object.
|
|
51
|
+
*/
|
|
52
|
+
class ShimSessionDescription {
|
|
53
|
+
/**
|
|
54
|
+
* @param {{ type?: string, sdp?: string } | string} init
|
|
55
|
+
* @param {string} [type] - Tolerated for callers using werift's own order.
|
|
56
|
+
*/
|
|
57
|
+
constructor(init, type) {
|
|
58
|
+
if (typeof init === "string") {
|
|
59
|
+
this.sdp = init;
|
|
60
|
+
this.type = type;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.type = init?.type;
|
|
64
|
+
this.sdp = init?.sdp;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
toJSON() {
|
|
68
|
+
return { type: this.type, sdp: this.sdp };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A werift data channel wearing the browser's interface.
|
|
74
|
+
*
|
|
75
|
+
* Only what `simple-peer` touches is implemented — promising more would be
|
|
76
|
+
* pretending, since anything else has no caller and would never be exercised.
|
|
77
|
+
*/
|
|
78
|
+
class ShimDataChannel {
|
|
79
|
+
#channel;
|
|
80
|
+
/** @type {"blob" | "arraybuffer"} */
|
|
81
|
+
binaryType = "blob";
|
|
82
|
+
/** @type {((event: { data: ArrayBuffer | Buffer }) => void) | null} */
|
|
83
|
+
onmessage = null;
|
|
84
|
+
/** @type {(() => void) | null} */
|
|
85
|
+
onopen = null;
|
|
86
|
+
/** @type {(() => void) | null} */
|
|
87
|
+
onclose = null;
|
|
88
|
+
/** @type {((event: { error?: Error }) => void) | null} */
|
|
89
|
+
onerror = null;
|
|
90
|
+
/** @type {(() => void) | null} */
|
|
91
|
+
onbufferedamountlow = null;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {object} channel - werift's `RTCDataChannel`.
|
|
95
|
+
*/
|
|
96
|
+
constructor(channel) {
|
|
97
|
+
this.#channel = channel;
|
|
98
|
+
|
|
99
|
+
channel.onmessage = (event) => {
|
|
100
|
+
const data = event?.data ?? event;
|
|
101
|
+
this.onmessage?.({ data: this.#toWireFormat(data) });
|
|
102
|
+
};
|
|
103
|
+
channel.onopen = () => this.onopen?.();
|
|
104
|
+
channel.onclose = () => this.onclose?.();
|
|
105
|
+
channel.onerror = (event) => this.onerror?.(event ?? {});
|
|
106
|
+
|
|
107
|
+
// werift reports this as an observable rather than a handler property.
|
|
108
|
+
channel.bufferedAmountLow?.subscribe?.(() => this.onbufferedamountlow?.());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Match what a browser would deliver for the requested `binaryType`.
|
|
113
|
+
*
|
|
114
|
+
* `simple-peer` checks `instanceof ArrayBuffer` and sends everything else
|
|
115
|
+
* through a text decoder, so a `Buffer` handed over unchanged arrives
|
|
116
|
+
* mangled.
|
|
117
|
+
*
|
|
118
|
+
* @param {unknown} data
|
|
119
|
+
* @returns {ArrayBuffer | unknown}
|
|
120
|
+
*/
|
|
121
|
+
#toWireFormat(data) {
|
|
122
|
+
if (this.binaryType !== "arraybuffer" || typeof data === "string") {
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
if (ArrayBuffer.isView(data)) {
|
|
126
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
127
|
+
}
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get label() {
|
|
132
|
+
return this.#channel.label;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get readyState() {
|
|
136
|
+
return this.#channel.readyState;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get bufferedAmount() {
|
|
140
|
+
return this.#channel.bufferedAmount;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get bufferedAmountLowThreshold() {
|
|
144
|
+
return this.#channel.bufferedAmountLowThreshold;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
set bufferedAmountLowThreshold(value) {
|
|
148
|
+
this.#channel.bufferedAmountLowThreshold = value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string | ArrayBuffer | ArrayBufferView} data
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
send(data) {
|
|
156
|
+
if (typeof data === "string") {
|
|
157
|
+
this.#channel.send(data);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#channel.send(ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
close() {
|
|
164
|
+
this.#channel.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* werift's peer connection, handing out data channels that carry the browser's
|
|
170
|
+
* interface. Everything else is inherited unchanged — the peer connection side
|
|
171
|
+
* already matches.
|
|
172
|
+
*/
|
|
173
|
+
class ShimPeerConnection extends WeriftPeerConnection {
|
|
174
|
+
/**
|
|
175
|
+
* @param {...unknown} args - Passed through to werift.
|
|
176
|
+
*/
|
|
177
|
+
constructor(...args) {
|
|
178
|
+
super(...args);
|
|
179
|
+
|
|
180
|
+
// `ondatachannel` has to be redefined on the instance, not declared as an
|
|
181
|
+
// accessor on this class: werift assigns it as an own field in its own
|
|
182
|
+
// constructor, and an own property shadows a prototype accessor — so a
|
|
183
|
+
// subclass setter is simply never called, and the caller receives werift's
|
|
184
|
+
// bare channel instead of the wrapped one.
|
|
185
|
+
let handler = null;
|
|
186
|
+
const deliver = (event) => {
|
|
187
|
+
handler?.({ ...event, channel: new ShimDataChannel(event?.channel ?? event) });
|
|
188
|
+
};
|
|
189
|
+
Object.defineProperty(this, "ondatachannel", {
|
|
190
|
+
configurable: true,
|
|
191
|
+
// werift reads this property to dispatch, so it must hand back the
|
|
192
|
+
// wrapper rather than what the caller assigned.
|
|
193
|
+
get: () => (handler ? deliver : null),
|
|
194
|
+
set: (value) => {
|
|
195
|
+
handler = value;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} label
|
|
202
|
+
* @param {object} [options]
|
|
203
|
+
* @returns {ShimDataChannel}
|
|
204
|
+
*/
|
|
205
|
+
createDataChannel(label, options) {
|
|
206
|
+
return new ShimDataChannel(super.createDataChannel(label, options));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
ShimPeerConnection as RTCPeerConnection,
|
|
212
|
+
ShimSessionDescription as RTCSessionDescription,
|
|
213
|
+
RTCIceCandidate
|
|
214
|
+
};
|
|
215
|
+
export default {
|
|
216
|
+
RTCPeerConnection: ShimPeerConnection,
|
|
217
|
+
RTCSessionDescription: ShimSessionDescription,
|
|
218
|
+
RTCIceCandidate
|
|
219
|
+
};
|
|
@@ -17,11 +17,22 @@
|
|
|
17
17
|
* threads.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
// MUST stay first: it redirects `webrtc-polyfill` to a JavaScript WebRTC stack
|
|
21
|
+
// before WebTorrent can reach the native one. Two isolates using
|
|
22
|
+
// node-datachannel at once abort the process, and the torrent's wss trackers
|
|
23
|
+
// create peer connections of their own.
|
|
24
|
+
import "./install-webrtc-shim.js";
|
|
20
25
|
import { parentPort, workerData } from "node:worker_threads";
|
|
21
|
-
import { TorrentPool } from "../torrent-pool.js";
|
|
22
26
|
import { createSendStream } from "./channel.js";
|
|
23
27
|
import { Command, Event, STREAM_CHUNK_BYTES } from "./protocol.js";
|
|
24
28
|
|
|
29
|
+
// Imported dynamically, and that is load-bearing: static imports are RESOLVED
|
|
30
|
+
// during linking, before any module body runs, so a statically imported pool
|
|
31
|
+
// would drag in WebTorrent — and with it the real `webrtc-polyfill` — before
|
|
32
|
+
// the hook above had a chance to register. Verified the hard way: with a static
|
|
33
|
+
// import the process still aborted, and the stack named the genuine polyfill.
|
|
34
|
+
const { TorrentPool } = await import("../torrent-pool.js");
|
|
35
|
+
|
|
25
36
|
const pool = new TorrentPool({ maxDiskBytes: workerData?.maxDiskBytes });
|
|
26
37
|
|
|
27
38
|
/** Torrents by sourceKey — the main thread names them, this thread owns them. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Recency and pinning for resident pieces.
|
|
3
|
+
*
|
|
4
|
+
* The pinning cases matter more than the ordering ones: proxy 2.9.71 removed a
|
|
5
|
+
* torrent's data while a reader was mid-read, and every later read hung. Here
|
|
6
|
+
* that is meant to be impossible by construction, so it is worth stating in
|
|
7
|
+
* tests rather than trusting to eviction order.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { PieceLru } from "../services/piece-store/piece-lru.js";
|
|
13
|
+
|
|
14
|
+
test("evicts the least recently used piece", () => {
|
|
15
|
+
const lru = new PieceLru(3);
|
|
16
|
+
lru.touch(1);
|
|
17
|
+
lru.touch(2);
|
|
18
|
+
lru.touch(3);
|
|
19
|
+
|
|
20
|
+
assert.equal(lru.evictionCandidate(), 1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("using a piece again moves it out of the firing line", () => {
|
|
24
|
+
const lru = new PieceLru(3);
|
|
25
|
+
lru.touch(1);
|
|
26
|
+
lru.touch(2);
|
|
27
|
+
lru.touch(3);
|
|
28
|
+
lru.touch(1);
|
|
29
|
+
|
|
30
|
+
assert.equal(lru.evictionCandidate(), 2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("a pinned piece is never offered for eviction", () => {
|
|
34
|
+
const lru = new PieceLru(3);
|
|
35
|
+
lru.touch(1);
|
|
36
|
+
lru.touch(2);
|
|
37
|
+
lru.touch(3);
|
|
38
|
+
lru.pin(1);
|
|
39
|
+
|
|
40
|
+
assert.equal(lru.evictionCandidate(), 2, "the pinned piece was offered up");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("pins nest, so one reader leaving does not expose the piece", () => {
|
|
44
|
+
const lru = new PieceLru(2);
|
|
45
|
+
lru.touch(1);
|
|
46
|
+
lru.touch(2);
|
|
47
|
+
|
|
48
|
+
// Two sessions reading the same piece — the union-window case.
|
|
49
|
+
lru.pin(1);
|
|
50
|
+
lru.pin(1);
|
|
51
|
+
lru.unpin(1);
|
|
52
|
+
|
|
53
|
+
assert.ok(lru.isPinned(1), "the piece stopped being held while a reader still had it");
|
|
54
|
+
assert.equal(lru.evictionCandidate(), 2);
|
|
55
|
+
|
|
56
|
+
lru.unpin(1);
|
|
57
|
+
assert.equal(lru.isPinned(1), false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("reports no candidate rather than evicting a piece in use", () => {
|
|
61
|
+
const lru = new PieceLru(2);
|
|
62
|
+
lru.touch(1);
|
|
63
|
+
lru.touch(2);
|
|
64
|
+
lru.pin(1);
|
|
65
|
+
lru.pin(2);
|
|
66
|
+
|
|
67
|
+
assert.equal(
|
|
68
|
+
lru.evictionCandidate(),
|
|
69
|
+
null,
|
|
70
|
+
"with every piece held, the caller must wait — not have memory taken from under it"
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("unpinning something that was never pinned is harmless", () => {
|
|
75
|
+
const lru = new PieceLru(2);
|
|
76
|
+
lru.touch(1);
|
|
77
|
+
lru.unpin(1);
|
|
78
|
+
lru.unpin(1);
|
|
79
|
+
|
|
80
|
+
assert.equal(lru.isPinned(1), false);
|
|
81
|
+
assert.equal(lru.evictionCandidate(), 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("removal takes a piece out of the ordering", () => {
|
|
85
|
+
const lru = new PieceLru(3);
|
|
86
|
+
lru.touch(1);
|
|
87
|
+
lru.touch(2);
|
|
88
|
+
lru.remove(1);
|
|
89
|
+
|
|
90
|
+
assert.equal(lru.has(1), false);
|
|
91
|
+
assert.equal(lru.evictionCandidate(), 2);
|
|
92
|
+
assert.equal(lru.size, 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("fullness follows capacity", () => {
|
|
96
|
+
const lru = new PieceLru(2);
|
|
97
|
+
assert.equal(lru.isFull(), false);
|
|
98
|
+
lru.touch(1);
|
|
99
|
+
lru.touch(2);
|
|
100
|
+
assert.equal(lru.isFull(), true);
|
|
101
|
+
lru.remove(1);
|
|
102
|
+
assert.equal(lru.isFull(), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a nonsensical capacity is refused at construction", () => {
|
|
106
|
+
assert.throws(() => new PieceLru(0), /positive integer/);
|
|
107
|
+
assert.throws(() => new PieceLru(-1), /positive integer/);
|
|
108
|
+
assert.throws(() => new PieceLru(1.5), /positive integer/);
|
|
109
|
+
});
|