diff --git a/docs/adr/0009-connection-local-nat-prediction-experiment.md b/docs/adr/0009-connection-local-nat-prediction-experiment.md index 6e234b7..b1b6583 100644 --- a/docs/adr/0009-connection-local-nat-prediction-experiment.md +++ b/docs/adr/0009-connection-local-nat-prediction-experiment.md @@ -35,10 +35,11 @@ state, or media path. candidates and cannot enter the arithmetic sequence. If three or more distinct survey candidates for one media section and public address form an arithmetic port sequence, the adapter appends at most eight bounded - candidates around the sequence's high endpoint. Every ordinary candidate - keeps normal Trickle ICE timing; no ordinary candidate is delayed, rejected, - or replaced. A Browser that does not expose candidate source URLs keeps - ordinary ICE but does not mix independent observations into prediction. + candidates outward from both sequence endpoints: four above the high endpoint + and four below the low endpoint. Every ordinary candidate keeps normal + Trickle ICE timing; no ordinary candidate is delayed, rejected, or replaced. + A Browser that does not expose candidate source URLs keeps ordinary ICE but + does not mix independent observations into prediction. 4. Candidate observations remain in memory for that connection only. No NAT label, raw address, port, score, hard candidate skip, periodic probe, or route-controller branch is introduced. Sanitized diagnostics record only diff --git a/src/client/webrtc/nat-prediction.ts b/src/client/webrtc/nat-prediction.ts index fee1382..e7d8c9c 100644 --- a/src/client/webrtc/nat-prediction.ts +++ b/src/client/webrtc/nat-prediction.ts @@ -244,23 +244,21 @@ function stableStep(observations: readonly SrflxObservation[]): number | null { return step; } -function predictedFrom( - anchor: SrflxObservation, +function predictedAround( + low: SrflxObservation, + high: SrflxObservation, step: number, - occupiedPorts: ReadonlySet, ): SignalCandidate[] { const predicted: SignalCandidate[] = []; - const seenPorts = new Set(occupiedPorts); - for (const direction of [1, -1] as const) { + for (const [anchor, direction] of [ + [high, 1], + [low, -1], + ] as const) { for (let index = 1; index <= NAT_PREDICTION_STEPS; index += 1) { const port = anchor.port + direction * step * index; if (port < MIN_PREDICTABLE_PORT || port > MAX_PREDICTABLE_PORT) { continue; } - if (seenPorts.has(port)) { - continue; - } - seenPorts.add(port); const fields = [...anchor.fields]; fields[0] = `candidate:s${direction > 0 ? "p" : "m"}${index}`; fields[5] = String(port); @@ -298,14 +296,11 @@ export function predictSrflxCandidates( if (step === null) { continue; } - const anchor = distinct.reduce((latest, observation) => - observation.port > latest.port ? observation : latest, + const ordered = [...distinct].sort((left, right) => left.port - right.port); + return predictedAround(ordered[0]!, ordered.at(-1)!, step).slice( + 0, + MAX_NAT_PREDICTION_CANDIDATES, ); - return predictedFrom( - anchor, - step, - new Set(distinct.map((observation) => observation.port)), - ).slice(0, MAX_NAT_PREDICTION_CANDIDATES); } return []; } diff --git a/tests/nat-prediction.test.ts b/tests/nat-prediction.test.ts index b3c2782..796ec5a 100644 --- a/tests/nat-prediction.test.ts +++ b/tests/nat-prediction.test.ts @@ -118,12 +118,17 @@ describe("NAT prediction ICE adapter", () => { candidate(40_006), ]); - expect(predictions.length).toBeLessThanOrEqual( - MAX_NAT_PREDICTION_CANDIDATES, - ); - expect(predictions.map(portOf)).toEqual( - expect.arrayContaining([40_009, 39_997]), - ); + expect(predictions).toHaveLength(MAX_NAT_PREDICTION_CANDIDATES); + expect(predictions.map(portOf)).toEqual([ + 40_009, + 40_012, + 40_015, + 40_018, + 39_997, + 39_994, + 39_991, + 39_988, + ]); expect(predictions.every((value) => value?.sdpMid === "0")).toBe(true); }); @@ -134,9 +139,16 @@ describe("NAT prediction ICE adapter", () => { candidate(40_000), ]); - expect(predictions.map(portOf)).toEqual( - expect.arrayContaining([40_009, 39_997]), - ); + expect(predictions.map(portOf)).toEqual([ + 40_009, + 40_012, + 40_015, + 40_018, + 39_997, + 39_994, + 39_991, + 39_988, + ]); }); it("does not predict an unstable or incomplete mapping shape", () => {