@torrent-tv/proxy 2.12.1 → 2.12.2
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 +6 -0
- package/package.json +1 -1
- package/services/data-channel-handler.js +95 -19
- package/host-timings.json +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.12.2
|
|
2
|
+
|
|
3
|
+
- **Fix**: The transport heartbeat is written once per connection, with each channel's queue beside it. The counters it reports belong to the peer connection, not to a channel, so printing the line per channel produced two byte-for-byte identical readings — `sent=5153491` under both "proxy" and "proxy-control" on 2026-08-14 — which read as two independent measurements agreeing. The one figure that IS per channel, its queue depth, was the only real difference and was buried in a line that looked like a duplicate, leaving the second channel unobservable in the log.
|
|
4
|
+
- **Fix**: A channel watch ends when the transport stops knowing about its session, not only when the channel reports itself closed. The close callback is the ordinary way it ends and it does not always arrive — a peer connection can die without one — leaving a timer sampling a session that no longer exists for the life of the process.
|
|
5
|
+
- **Chore**: `host-timings.json` is no longer under version control, and no longer ships in the package. It is runtime state the proxy rewrites every session, so it arrived in every diff, would have carried one developer machine's medians into every published version, and would have conflicted on every release.
|
|
6
|
+
|
|
1
7
|
## 2.12.1
|
|
2
8
|
|
|
3
9
|
- **Fix**: The grid a copied stream is cut on now describes the FILE, not the container's index. A copy can only be cut where a keyframe already is, and nothing cheaper than the index can say where that is before a byte is encoded — but an index can be wrong. Reproduced 2026-08-12 against one file, both ways: with an honest index every produced segment started exactly where declared; with the index moved 1.8 s, every segment started 1.8 s early and matched no boundary at all. The field showed the second shape, so the mechanism was never at fault and the data was. The truth arrives anyway, one segment at a time — a produced piece states where it really begins — and it is now written back into the grid, which the whole family shares. That is what lets a re-encoded rung be cut to match a copied one: it is forced onto times the copy really uses. A correction that would cross its neighbours is refused, since that is a reading from a run that began somewhere else.
|
package/package.json
CHANGED
|
@@ -103,10 +103,73 @@
|
|
|
103
103
|
* @returns {() => void} Stops the watch.
|
|
104
104
|
*/
|
|
105
105
|
function makeSendQueueWatcher({ log, getTransportSnapshot }) {
|
|
106
|
+
// Every channel of one connection reads the SAME transport counters — the
|
|
107
|
+
// snapshot describes the peer connection, not the channel — so the heartbeat
|
|
108
|
+
// belongs to the connection and is printed once for it. Printed per channel
|
|
109
|
+
// it produced two byte-for-byte identical lines (measured 2026-08-14:
|
|
110
|
+
// `sent=5153491` under both "proxy" and "proxy-control"), which read as two
|
|
111
|
+
// independent readings agreeing and made the second channel invisible: the
|
|
112
|
+
// one thing that IS per channel, its queue depth, was the only real
|
|
113
|
+
// difference and it was buried in a line that looked like a duplicate.
|
|
114
|
+
//
|
|
115
|
+
// sessionId → the channels currently open on that connection, and when it was
|
|
116
|
+
// last reported. Channels are keyed by the channel OBJECT, not by its label:
|
|
117
|
+
// a label is whatever the peer chose and two channels can carry the same one
|
|
118
|
+
// (or none, where `getLabel` is missing and both fall back to "?"), and a
|
|
119
|
+
// Map keyed on that would let one channel evict the other and then, on
|
|
120
|
+
// closing, delete the survivor's entry.
|
|
121
|
+
/** @type {Map<string, { channels: Map<DataChannel, string>, at: number, previous: object | null, unknown: number }>} */
|
|
122
|
+
const connections = new Map();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What each channel of a connection is holding, right now.
|
|
126
|
+
*
|
|
127
|
+
* @param {Map<DataChannel, string>} channels
|
|
128
|
+
* @returns {string} `label:NB` per channel, in the order they opened.
|
|
129
|
+
*/
|
|
130
|
+
const queueDepths = (channels) => {
|
|
131
|
+
const parts = [];
|
|
132
|
+
for (const [openChannel, channelLabel] of channels) {
|
|
133
|
+
let depth = -1;
|
|
134
|
+
try {
|
|
135
|
+
depth = typeof openChannel.bufferedAmount === "function" ? openChannel.bufferedAmount() : 0;
|
|
136
|
+
} catch {
|
|
137
|
+
depth = -1;
|
|
138
|
+
}
|
|
139
|
+
parts.push(`${channelLabel}:${depth}B`);
|
|
140
|
+
}
|
|
141
|
+
return parts.join(" ");
|
|
142
|
+
};
|
|
143
|
+
|
|
106
144
|
return function watchSendQueue(sessionId, tag, label, channel) {
|
|
107
145
|
let lowestSinceDrain = Number.POSITIVE_INFINITY;
|
|
108
146
|
let stuckSince = 0;
|
|
109
147
|
let previous = null;
|
|
148
|
+
let connection = connections.get(sessionId);
|
|
149
|
+
if (!connection) {
|
|
150
|
+
connection = { channels: new Map(), at: 0, previous: null, unknown: 0 };
|
|
151
|
+
connections.set(sessionId, connection);
|
|
152
|
+
}
|
|
153
|
+
connection.channels.set(channel, label);
|
|
154
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
155
|
+
let timer = null;
|
|
156
|
+
/**
|
|
157
|
+
* End this channel's watch and let go of its entry.
|
|
158
|
+
*
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
const stop = () => {
|
|
162
|
+
if (timer) {
|
|
163
|
+
clearInterval(timer);
|
|
164
|
+
}
|
|
165
|
+
connection.channels.delete(channel);
|
|
166
|
+
// Only if the map still holds THIS record: a late stop, after the same
|
|
167
|
+
// session id has been reused and a new record made for it, must not evict
|
|
168
|
+
// the live one.
|
|
169
|
+
if (connection.channels.size === 0 && connections.get(sessionId) === connection) {
|
|
170
|
+
connections.delete(sessionId);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
110
173
|
// Independent of the queue: the transport's own counters, sampled for as
|
|
111
174
|
// long as the channel is open. The queue was the wrong thing to watch —
|
|
112
175
|
// field 2026-08-06, a 9.26 MB segment was accepted by the transport with
|
|
@@ -115,30 +178,38 @@ function makeSendQueueWatcher({ log, getTransportSnapshot }) {
|
|
|
115
178
|
// same way while requests kept coming the other direction. With nothing
|
|
116
179
|
// queued this watcher never woke, so the one question that matters — did
|
|
117
180
|
// those bytes leave the machine — has no answer in the log. It does now.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
181
|
+
// A connection the transport no longer knows about is gone, whatever the
|
|
182
|
+
// channel says. `onClosed` is the ordinary way this watch ends, and it does
|
|
183
|
+
// not always come — a peer connection can die without it, leaving the timer
|
|
184
|
+
// and this channel's entry behind for the life of the process.
|
|
185
|
+
//
|
|
186
|
+
// The count is kept on the CONNECTION: exactly one channel enters the
|
|
187
|
+
// heartbeat branch per interval, so a per-channel count would advance only
|
|
188
|
+
// on that channel's turn and the teardown would take three heartbeats per
|
|
189
|
+
// channel rather than three in total.
|
|
190
|
+
timer = setInterval(() => {
|
|
122
191
|
const sampledAt = Date.now();
|
|
123
|
-
|
|
124
|
-
|
|
192
|
+
// Whichever channel's timer arrives first past the interval reports for
|
|
193
|
+
// the whole connection; the others find the timestamp already moved and
|
|
194
|
+
// skip. So the line appears once however many channels are open.
|
|
195
|
+
if (sampledAt - connection.at >= TRANSPORT_HEARTBEAT_MS) {
|
|
196
|
+
connection.at = sampledAt;
|
|
125
197
|
const snapshot = getTransportSnapshot?.(sessionId) ?? null;
|
|
198
|
+
connection.unknown = snapshot ? 0 : connection.unknown + 1;
|
|
199
|
+
if (connection.unknown >= TRANSPORT_UNKNOWN_HEARTBEATS) {
|
|
200
|
+
stop();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
126
203
|
if (snapshot) {
|
|
127
|
-
const sent =
|
|
128
|
-
const received =
|
|
129
|
-
? snapshot.bytesReceived -
|
|
204
|
+
const sent = connection.previous ? snapshot.bytesSent - connection.previous.bytesSent : null;
|
|
205
|
+
const received = connection.previous
|
|
206
|
+
? snapshot.bytesReceived - connection.previous.bytesReceived
|
|
130
207
|
: null;
|
|
131
|
-
|
|
132
|
-
let depth = 0;
|
|
133
|
-
try {
|
|
134
|
-
depth = typeof channel.bufferedAmount === "function" ? channel.bufferedAmount() : 0;
|
|
135
|
-
} catch {
|
|
136
|
-
depth = -1;
|
|
137
|
-
}
|
|
208
|
+
connection.previous = snapshot;
|
|
138
209
|
log(
|
|
139
|
-
`[dc-transport] ${tag}
|
|
210
|
+
`[dc-transport] ${tag} sent=${snapshot.bytesSent}` +
|
|
140
211
|
`${sent === null ? "" : ` (+${sent})`} received=${snapshot.bytesReceived}` +
|
|
141
|
-
`${received === null ? "" : ` (+${received})`} queued
|
|
212
|
+
`${received === null ? "" : ` (+${received})`} queued[${queueDepths(connection.channels)}] ` +
|
|
142
213
|
`rtt=${snapshot.rtt}ms pc=${snapshot.state} ice=${snapshot.iceState} pair=${snapshot.pair}`
|
|
143
214
|
);
|
|
144
215
|
}
|
|
@@ -184,7 +255,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot }) {
|
|
|
184
255
|
if (typeof timer.unref === "function") {
|
|
185
256
|
timer.unref();
|
|
186
257
|
}
|
|
187
|
-
return
|
|
258
|
+
return stop;
|
|
188
259
|
};
|
|
189
260
|
}
|
|
190
261
|
|
|
@@ -668,6 +739,11 @@ const SEND_QUEUE_SAMPLE_MS = 1_000;
|
|
|
668
739
|
// send queue is doing. Frequent enough to place a loss within a few seconds,
|
|
669
740
|
// sparse enough that a two-hour film costs a few hundred lines.
|
|
670
741
|
const TRANSPORT_HEARTBEAT_MS = 5_000;
|
|
742
|
+
|
|
743
|
+
// How many heartbeats in a row may find no transport for this session before
|
|
744
|
+
// the watch gives up. Several rather than one, so a momentary gap in the
|
|
745
|
+
// registry does not end a healthy watch.
|
|
746
|
+
const TRANSPORT_UNKNOWN_HEARTBEATS = 3;
|
|
671
747
|
const SEND_QUEUE_STUCK_MS = 5_000;
|
|
672
748
|
const DC_BUFFER_HIGH_WATER = 8 * 1024 * 1024;
|
|
673
749
|
/** Resume sending once the channel buffer drains to this many bytes. */
|
package/host-timings.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"firstSegment":[3,1,4,11,3,1,3,17],"sessionCreate":[]}
|