@torrent-tv/proxy 2.102.0 → 2.102.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/package.json +1 -1
- package/server.js +9 -1
- package/services/encode/EncodeRuns.js +5 -0
- package/services/encode/LocalObservations.js +15 -1
- package/services/media/ContainerOrchestrator.js +7 -3
- package/services/server/ViewerRequests.js +38 -10
- package/services/server/wire-outputs.js +3 -1
- package/services/torrent/worker/pool-adapter.js +10 -2
- package/utils/wait-trace.js +68 -0
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import fastifyHelmet from "@fastify/helmet";
|
|
|
12
12
|
import getPort from "get-port";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { performance } from "node:perf_hooks";
|
|
15
|
+
import { SourceWaitTrace } from "./utils/wait-trace.js";
|
|
15
16
|
import { handleHealthGet } from "./routes/health/get.js";
|
|
16
17
|
import { handleMediaGet } from "./routes/media/get.js";
|
|
17
18
|
import { ProbeRequests } from "./services/media/ProbeRequests.js";
|
|
@@ -157,6 +158,8 @@ export async function startProxyServer({
|
|
|
157
158
|
bodyLimit: 256 * 1024 * 1024 // 256 MB
|
|
158
159
|
});
|
|
159
160
|
const forwardedSessionResponseStarts = new WeakMap();
|
|
161
|
+
const waitTrace = new SourceWaitTrace({ emit: value => logger.info(`[wait-trace] ${JSON.stringify(value)}`) });
|
|
162
|
+
waitTrace.install(app);
|
|
160
163
|
const logForwardedSessionResponseStage = (stage, req) => {
|
|
161
164
|
const startedAt = forwardedSessionResponseStarts.get(req);
|
|
162
165
|
const requestId = req.headers["x-ttv-forwarded-request-id"];
|
|
@@ -175,7 +178,7 @@ export async function startProxyServer({
|
|
|
175
178
|
await app.register(fastifyCors, {
|
|
176
179
|
origin: true,
|
|
177
180
|
methods: ["GET", "POST", "OPTIONS"],
|
|
178
|
-
allowedHeaders: ["Content-Type", "Range"]
|
|
181
|
+
allowedHeaders: ["Content-Type", "Range", "X-Ttv-Wait-Trace", "X-Ttv-Wait-Attempt", "X-Ttv-Wait-Operation"]
|
|
179
182
|
});
|
|
180
183
|
|
|
181
184
|
// Allow browser requests from an HTTPS page to this private-network proxy
|
|
@@ -220,6 +223,7 @@ export async function startProxyServer({
|
|
|
220
223
|
// 10 MB file took 12-23 s against 125 ms to hand it to the channel. The
|
|
221
224
|
// adapter keeps TorrentPool's interface, so nothing downstream changed.
|
|
222
225
|
const torrentPool = new WorkerTorrentPool({
|
|
226
|
+
onObserved: fields => waitTrace.source(fields.sourceKey, "torrent-ready", fields),
|
|
223
227
|
memoryBytes,
|
|
224
228
|
stateDir,
|
|
225
229
|
heapSnapshots,
|
|
@@ -250,6 +254,8 @@ export async function startProxyServer({
|
|
|
250
254
|
// only once the worker thread is up and a piece has verified, which is long
|
|
251
255
|
// after the declarations below have run.
|
|
252
256
|
onPiecesArrived: ({ sourceKey, fileIndexes, arrived }) => {
|
|
257
|
+
waitTrace.source(sourceKey, "held-bytes-changed", { fileIndexes, arrived: arrived?.slice?.(0, 32),
|
|
258
|
+
arrivedCount: arrived?.length });
|
|
253
259
|
for (const fileIndex of fileIndexes) {
|
|
254
260
|
mediaReads.bytesChanged(sourceKey, fileIndex);
|
|
255
261
|
probeReads.bytesChanged(sourceKey, fileIndex);
|
|
@@ -871,6 +877,7 @@ export async function startProxyServer({
|
|
|
871
877
|
} }),
|
|
872
878
|
label: String(file.name ?? ""),
|
|
873
879
|
fileOffset: file.offset,
|
|
880
|
+
onObserved: fields => waitTrace.source(sourceKey, "container-read", { fileIndex, requestId, ...fields }),
|
|
874
881
|
onTracks: (tracks) => contentsOf(torrent).noteVideo(fileIndex, tracks.some((track) => track.type === "video")),
|
|
875
882
|
onReadStart: () => ({ storage: mediaReads.revision(sourceKey, fileIndex), memory: mediaReads.memoryRevision(), demand: downloadMaps.epoch(sourceKey, fileIndex) }),
|
|
876
883
|
onReadResult: async (statement, result, revision) => {
|
|
@@ -918,6 +925,7 @@ export async function startProxyServer({
|
|
|
918
925
|
});
|
|
919
926
|
let sourcePreparation;
|
|
920
927
|
const outputParts = wireOutputs({
|
|
928
|
+
observeWait: (sourceKey, event, fields) => waitTrace.source(sourceKey, event, fields),
|
|
921
929
|
pinnedPreset,
|
|
922
930
|
indexMemory,
|
|
923
931
|
serviceShare,
|
|
@@ -1106,6 +1106,7 @@ export class EncodeRuns {
|
|
|
1106
1106
|
// proven, and it is cleared up when the run ends.
|
|
1107
1107
|
// Told when this run closes its first piece; see the next stretch below.
|
|
1108
1108
|
let atFirstPiece = null;
|
|
1109
|
+
const observedStart = performance.now();
|
|
1109
1110
|
const run = new EncodeRun({
|
|
1110
1111
|
address: session.outputKey ?? session.id,
|
|
1111
1112
|
encoder: this.#host.videoEncoder,
|
|
@@ -1196,6 +1197,10 @@ export class EncodeRuns {
|
|
|
1196
1197
|
{ code: "ERR_INCOMPLETE_MEDIA" });
|
|
1197
1198
|
const published = this.#host.segmentFiles.publish(address, name, session.segmentFormat, { mediaRanges, bytes });
|
|
1198
1199
|
if (!published) throw new Error(`Closed piece #${index} could not be stored.`);
|
|
1200
|
+
if (index === safeIndex) this.#host.observeWait?.(session.file.sourceKey, "first-piece-published", {
|
|
1201
|
+
output: address, segment: index, startedAt: observedStart, endedAt: performance.now(),
|
|
1202
|
+
encoderWrittenAfterMs: Number.isFinite(writtenAt) ? writtenAt - run.startedAt : null,
|
|
1203
|
+
namedAfterMs: Number.isFinite(namedAt) ? namedAt - run.startedAt : null });
|
|
1199
1204
|
this.#leadingByOutput.get(address)?.delete(index);
|
|
1200
1205
|
// WHERE A LATE PIECE'S TIME WENT: the encoder writing it, this process
|
|
1201
1206
|
// getting to its name, or its publication waiting for the file after
|
|
@@ -305,6 +305,16 @@ export class LocalObservations {
|
|
|
305
305
|
* @returns {number | null}
|
|
306
306
|
*/
|
|
307
307
|
firstFrameSeconds(features) {
|
|
308
|
+
return this.firstFrameEstimate(features)?.seconds ?? null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Return the current stage-median forecast and the measurements that formed it.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} features
|
|
315
|
+
* @returns {{ seconds: number, stagesMs: object, observations: number } | null}
|
|
316
|
+
*/
|
|
317
|
+
firstFrameEstimate(features) {
|
|
308
318
|
if (!features || typeof features !== "object") return null;
|
|
309
319
|
const stages = ["transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
|
|
310
320
|
const candidates = [];
|
|
@@ -319,7 +329,11 @@ export class LocalObservations {
|
|
|
319
329
|
const prices = stages.map(stage => median(candidates.map(reading => reading[stage])
|
|
320
330
|
.filter(Number.isFinite)));
|
|
321
331
|
if (prices.some(value => value === null)) return null;
|
|
322
|
-
return
|
|
332
|
+
return {
|
|
333
|
+
seconds: prices.reduce((sum, value) => sum + value, 0) / 1000,
|
|
334
|
+
stagesMs: Object.fromEntries(stages.map((stage, index) => [stage, prices[index]])),
|
|
335
|
+
observations: candidates.length
|
|
336
|
+
};
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
#firstFrameKey() {
|
|
@@ -156,13 +156,17 @@ export class ContainerOrchestrator {
|
|
|
156
156
|
// table of the whole film it does not use (r38, torrent-tv/meta#178).
|
|
157
157
|
const aside = this.#answersWithoutReading(key, statement);
|
|
158
158
|
const previous = aside ? Promise.resolve() : this.#reads.get(key) ?? Promise.resolve();
|
|
159
|
-
const askedAt =
|
|
159
|
+
const askedAt = performance.now();
|
|
160
160
|
const reading = previous.catch(() => undefined).then(async () => {
|
|
161
161
|
if (!isCurrent()) return { result: { kind: "terminal", reason: "request-obsolete", requestId: params.requestId } };
|
|
162
|
-
const startedAt =
|
|
162
|
+
const startedAt = performance.now();
|
|
163
163
|
const revision = params.onReadStart?.(statement);
|
|
164
164
|
const result = await this.#inspectRead({ ...params, isCurrent }, statement);
|
|
165
|
-
|
|
165
|
+
const endedAt = performance.now();
|
|
166
|
+
params.onTimed?.(statement, startedAt - askedAt, endedAt - startedAt);
|
|
167
|
+
if (statement !== "source-ranges" || endedAt - askedAt >= 10) params.onObserved?.({ statement, askedAt,
|
|
168
|
+
startedAt, endedAt, queuedMs: startedAt - askedAt, readMs: endedAt - startedAt, cached: aside,
|
|
169
|
+
result: result.kind, reason: result.reason, ranges: result.ranges });
|
|
166
170
|
return { result, revision };
|
|
167
171
|
});
|
|
168
172
|
if (!aside) this.#reads.set(key, reading);
|
|
@@ -125,11 +125,13 @@ export class ViewerRequests {
|
|
|
125
125
|
const changed = this.#recordEarlyWaitSnapshot(context, context.features);
|
|
126
126
|
if (changed) context.revision += 1;
|
|
127
127
|
this.#pruneEarlyWaits();
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const seconds = earlyWaitRemainingSeconds(totalSeconds,
|
|
128
|
+
const elapsedMs = waitElapsedSince(context, waitElapsedMs);
|
|
129
|
+
const historical = Number.isFinite(context.activeTotalSeconds) ? null : this.#historicalFirstFrame(context.features);
|
|
130
|
+
const totalSeconds = Number.isFinite(context.activeTotalSeconds) ? context.activeTotalSeconds : historical?.seconds ?? null;
|
|
131
|
+
const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
|
|
132
132
|
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
133
|
+
this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
|
|
134
|
+
firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
|
|
133
135
|
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
134
136
|
}
|
|
135
137
|
const sameWait = current?.waitId === waitId;
|
|
@@ -139,11 +141,13 @@ export class ViewerRequests {
|
|
|
139
141
|
const changed = this.#recordEarlyWaitSnapshot(context, context.features);
|
|
140
142
|
if (sameWait && changed) context.revision += 1;
|
|
141
143
|
this.#pruneEarlyWaits();
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const seconds = earlyWaitRemainingSeconds(totalSeconds,
|
|
144
|
+
const elapsedMs = waitElapsedSince(context, waitElapsedMs);
|
|
145
|
+
const historical = Number.isFinite(context.activeTotalSeconds) ? null : this.#historicalFirstFrame(context.features);
|
|
146
|
+
const totalSeconds = Number.isFinite(context.activeTotalSeconds) ? context.activeTotalSeconds : historical?.seconds ?? null;
|
|
147
|
+
const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
|
|
146
148
|
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
149
|
+
this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
|
|
150
|
+
firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
|
|
147
151
|
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
148
152
|
}
|
|
149
153
|
|
|
@@ -192,20 +196,44 @@ export class ViewerRequests {
|
|
|
192
196
|
? elapsedMs / 1000 + measuredWorkSeconds
|
|
193
197
|
: context.activeTotalSeconds;
|
|
194
198
|
const activeEstimateChanged = hasActiveEstimate && activeTotalSeconds !== context.activeTotalSeconds;
|
|
195
|
-
if (hasActiveEstimate)
|
|
199
|
+
if (hasActiveEstimate) {
|
|
200
|
+
context.activeTotalSeconds = activeTotalSeconds;
|
|
201
|
+
context.activeFirstRequiredSegmentsSeconds = measuredWorkSeconds;
|
|
202
|
+
}
|
|
196
203
|
const changed = featuresChanged || activeEstimateChanged;
|
|
197
204
|
if (sameWait && changed) context.revision += 1;
|
|
198
205
|
this.#pruneEarlyWaits();
|
|
199
206
|
const progressHasComponentForecast = Object.hasOwn(processing, "firstRequiredSegmentsSeconds");
|
|
200
207
|
let totalSeconds = activeTotalSeconds;
|
|
208
|
+
let historical = null;
|
|
201
209
|
if (!Number.isFinite(totalSeconds) && !progressHasComponentForecast) {
|
|
202
|
-
|
|
210
|
+
historical = this.#historicalFirstFrame(context.features);
|
|
211
|
+
totalSeconds = historical?.seconds ?? null;
|
|
203
212
|
}
|
|
204
213
|
const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
|
|
205
214
|
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
215
|
+
this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
|
|
216
|
+
firstRequiredSegmentsSeconds: hasActiveEstimate ? measuredWorkSeconds : context.activeFirstRequiredSegmentsSeconds });
|
|
206
217
|
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
207
218
|
}
|
|
208
219
|
|
|
220
|
+
#historicalFirstFrame(features) {
|
|
221
|
+
const estimate = this.#host.firstFrameSecondsOf?.(features) ?? null;
|
|
222
|
+
return Number.isFinite(estimate) ? { seconds: estimate, stagesMs: null, observations: null } : estimate;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical, firstRequiredSegmentsSeconds }) {
|
|
226
|
+
if (context.loggedForecastRevision === context.revision) return;
|
|
227
|
+
const basis = Number.isFinite(firstRequiredSegmentsSeconds) ? "first-required" : "historical-stages";
|
|
228
|
+
const estimate = basis === "first-required"
|
|
229
|
+
? { firstRequiredSegmentsSeconds }
|
|
230
|
+
: { stagesMs: historical?.stagesMs ?? null, observations: historical?.observations ?? null };
|
|
231
|
+
logger.info(`early wait forecast ${JSON.stringify({ waitId: context.waitId, consumer: context.consumerId,
|
|
232
|
+
generation: context.generation, features: context.features, elapsedMs, revision: context.revision,
|
|
233
|
+
predicted: { remainingSeconds: seconds, totalSeconds, basis, ...estimate } })}`);
|
|
234
|
+
context.loggedForecastRevision = context.revision;
|
|
235
|
+
}
|
|
236
|
+
|
|
209
237
|
/**
|
|
210
238
|
* Learn from the browser's presented-frame measurement for the current
|
|
211
239
|
* viewing. The saved source and link readings are the ones available at
|
|
@@ -177,6 +177,7 @@ function buildHttpBaseUrl(host, port) {
|
|
|
177
177
|
* @returns {object} The components, and the settings they read.
|
|
178
178
|
*/
|
|
179
179
|
export function wireOutputs({
|
|
180
|
+
observeWait = null,
|
|
180
181
|
enabled,
|
|
181
182
|
ffmpegBin,
|
|
182
183
|
localBindHost,
|
|
@@ -363,6 +364,7 @@ export function wireOutputs({
|
|
|
363
364
|
get segmentDurationSec() { return parts.segmentDurationSec; },
|
|
364
365
|
readsThroughCache: () => parts.inputCacheAvailable === true,
|
|
365
366
|
segmentFiles,
|
|
367
|
+
observeWait,
|
|
366
368
|
get videoEncoder() { return parts.encoders.current; },
|
|
367
369
|
get encoders() { return parts.encoders; },
|
|
368
370
|
invalidateWaits: (output) => parts.serving.invalidateWaits(output),
|
|
@@ -649,7 +651,7 @@ export function wireOutputs({
|
|
|
649
651
|
const predicted = Number(qualityStateOf(output).predictedSpeedWhenOffered);
|
|
650
652
|
return Number.isFinite(predicted) && predicted > 0 ? predicted : null;
|
|
651
653
|
},
|
|
652
|
-
firstFrameSecondsOf: (features) => parts.localObservations?.
|
|
654
|
+
firstFrameSecondsOf: (features) => parts.localObservations?.firstFrameEstimate(features) ?? null,
|
|
653
655
|
noteFirstFrame: (observation) => parts.localObservations?.noteFirstFrame(observation),
|
|
654
656
|
audioDescriptionOf,
|
|
655
657
|
openSecondsOf: (output) => runStartSecondsOf(output),
|
|
@@ -31,12 +31,14 @@ export class WorkerTorrentPool {
|
|
|
31
31
|
/** Stand-ins by source key, so repeat calls return the same object. */
|
|
32
32
|
#torrents = new Map();
|
|
33
33
|
#pending = new Map();
|
|
34
|
+
#observed;
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* @param {{ memoryBytes?: number, stateDir?: string }} [options]
|
|
37
38
|
* @param {object | null} [client] - The worker interface; tests supply a fake.
|
|
38
39
|
*/
|
|
39
40
|
constructor(options = {}, client = null) {
|
|
41
|
+
this.#observed = options.onObserved;
|
|
40
42
|
this.#client = client ?? new TorrentWorkerClient({ ...options, onSourceForgotten: event => {
|
|
41
43
|
this.forget(event.sourceKey);
|
|
42
44
|
options.onSourceForgotten?.(event);
|
|
@@ -51,13 +53,16 @@ export class WorkerTorrentPool {
|
|
|
51
53
|
* @returns {Promise<object>}
|
|
52
54
|
*/
|
|
53
55
|
async getTorrent(sourceType, source) {
|
|
56
|
+
const startedAt = performance.now();
|
|
54
57
|
const sourceKey = await deriveSourceKey(sourceType, source);
|
|
58
|
+
const observed = outcome => this.#observed?.({ sourceKey, startedAt, endedAt: performance.now(), outcome });
|
|
55
59
|
const existing = this.#torrents.get(sourceKey);
|
|
56
60
|
if (existing) {
|
|
61
|
+
observed("cached");
|
|
57
62
|
return existing;
|
|
58
63
|
}
|
|
59
64
|
const joined = this.#pending.get(sourceKey);
|
|
60
|
-
if (joined) return joined;
|
|
65
|
+
if (joined) return joined.finally(() => observed("joined"));
|
|
61
66
|
const pending = this.#client.getTorrent({ sourceKey, sourceType, source }).then(torrent => {
|
|
62
67
|
if (this.#pending.get(sourceKey) !== pending) {
|
|
63
68
|
const error = new Error("Source was forgotten while its metadata was being read.");
|
|
@@ -66,7 +71,10 @@ export class WorkerTorrentPool {
|
|
|
66
71
|
}
|
|
67
72
|
this.#torrents.set(sourceKey, torrent);
|
|
68
73
|
return torrent;
|
|
69
|
-
}).finally(() => {
|
|
74
|
+
}).finally(() => {
|
|
75
|
+
observed(this.#torrents.has(sourceKey) ? "ready" : "error");
|
|
76
|
+
if (this.#pending.get(sourceKey) === pending) this.#pending.delete(sourceKey);
|
|
77
|
+
});
|
|
70
78
|
this.#pending.set(sourceKey, pending);
|
|
71
79
|
return pending;
|
|
72
80
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Bounded, opt-in startup observations. No torrent or application imports. */
|
|
2
|
+
export class SourceWaitTrace {
|
|
3
|
+
constructor({ emit, now = () => performance.now() }) {
|
|
4
|
+
Object.assign(this, { emit, now, sources: new Map(), requests: new WeakMap() });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
write(context, event, fields = {}) {
|
|
8
|
+
this.emit({ version: 1, clock: "proxy-main", wait: context.wait, attempt: context.attempt,
|
|
9
|
+
operation: context.operation, event, at: this.now(), ...fields });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
remember(sourceKey, context) {
|
|
13
|
+
if (typeof sourceKey !== "string" || !sourceKey) return;
|
|
14
|
+
for (const [key, value] of this.sources) if (this.now() - value.seen > 60_000) this.sources.delete(key);
|
|
15
|
+
if (this.sources.size >= 64 && !this.sources.has(sourceKey)) this.sources.delete(this.sources.keys().next().value);
|
|
16
|
+
this.sources.set(sourceKey, { ...context, seen: this.now() });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
source(sourceKey, event, fields = {}) {
|
|
20
|
+
const context = this.sources.get(sourceKey);
|
|
21
|
+
if (!context || this.now() - context.seen > 60_000) return;
|
|
22
|
+
this.write({ ...context, operation: null }, event, { sourceKey, ...fields });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
install(app) {
|
|
26
|
+
app.addHook("onRequest", async (req, reply) => {
|
|
27
|
+
const wait = req.headers["x-ttv-wait-trace"];
|
|
28
|
+
const operation = req.headers["x-ttv-wait-operation"];
|
|
29
|
+
const attempt = Number(req.headers["x-ttv-wait-attempt"]);
|
|
30
|
+
if (typeof wait !== "string" || !/^[a-zA-Z0-9.-]{1,80}$/.test(wait) ||
|
|
31
|
+
typeof operation !== "string" || !/^b[0-9]{1,12}$/.test(operation) ||
|
|
32
|
+
!Number.isSafeInteger(attempt) || attempt < 0) return;
|
|
33
|
+
const context = { wait, attempt, operation, startedAt: this.now() };
|
|
34
|
+
this.requests.set(req, context);
|
|
35
|
+
this.write(context, "http-start", { method: req.method, path: req.url,
|
|
36
|
+
requestId: req.headers["x-ttv-forwarded-request-id"] ?? req.id });
|
|
37
|
+
let finished = false;
|
|
38
|
+
const end = outcome => {
|
|
39
|
+
if (finished) return;
|
|
40
|
+
finished = true;
|
|
41
|
+
this.write(context, "http-end", { outcome, status: reply.statusCode, duration: this.now() - context.startedAt });
|
|
42
|
+
};
|
|
43
|
+
reply.raw.once("finish", () => end("finished"));
|
|
44
|
+
reply.raw.once("close", () => end(reply.raw.writableFinished ? "finished" : "closed"));
|
|
45
|
+
});
|
|
46
|
+
app.addHook("preHandler", async req => {
|
|
47
|
+
const context = this.requests.get(req);
|
|
48
|
+
if (!context) return;
|
|
49
|
+
const match = /^\/api\/sources\/([^/?]+)/.exec(req.url);
|
|
50
|
+
const sourceKey = req.body?.sourceKey ?? req.query?.sourceKey ?? (match ? decodeURIComponent(match[1]) : null);
|
|
51
|
+
this.remember(sourceKey, context);
|
|
52
|
+
this.write(context, "http-handler", { sourceKey });
|
|
53
|
+
});
|
|
54
|
+
app.addHook("preSerialization", async (req, _reply, payload) => {
|
|
55
|
+
const context = this.requests.get(req);
|
|
56
|
+
if (context) {
|
|
57
|
+
this.remember(payload?.sourceKey, context);
|
|
58
|
+
this.write(context, "http-serialize");
|
|
59
|
+
}
|
|
60
|
+
return payload;
|
|
61
|
+
});
|
|
62
|
+
app.addHook("onSend", async (req, _reply, payload) => {
|
|
63
|
+
const context = this.requests.get(req);
|
|
64
|
+
if (context) this.write(context, "http-send");
|
|
65
|
+
return payload;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|