perf(environment): decode only selected replay query values

This commit is contained in:
morluto
2026-10-24 00:14:05 +08:00
parent b784912811
commit 96ea32b7e4
7 changed files with 78 additions and 36 deletions
@@ -11,8 +11,9 @@
2. Add `environmentStateDocument.ts` to validate `rea-state/v1` documents
against the served types and views, and `environmentStateStore.ts` for the
live store with reset, atomic replace and the `data/get` snapshot.
3. Export `selectsRecordedLine`, the body-taking `replayAnswer` and the path
rule `pathValueMatches` from `environmentReplay.ts`, export one recorded-body
3. Export `selectsRecordedLine` and the body-taking `replayAnswer` from
`environmentReplay.ts`, reuse `pathSegmentMatches` from the state rules,
export one recorded-body
predicate `stateBodyJson` from the state accumulator and `changesState` from
the state rules, and widen `collectionPathOf` to any value with a path, so
state answers reuse selection, matching, body and write rules.
@@ -17,7 +17,6 @@ import {
type RecordedExchange,
} from "./environmentSource.js";
import {
pathValueMatches,
replayEnvironmentTool,
selectReplayExchange,
} from "./environmentReplay.js";
@@ -115,14 +114,32 @@ describe("replay argument mapping and query admission", () => {
});
});
describe("pathValueMatches", () => {
it("matches one decode of either side and not a second decode of both", () => {
expect(pathValueMatches("2", "%32")).toBe(true);
expect(pathValueMatches("%32", "2")).toBe(true);
expect(pathValueMatches("book1", "book%2531")).toBe(false);
expect(pathValueMatches("book%2531", "book1")).toBe(false);
expect(pathValueMatches("book%31", "book%2531")).toBe(true);
});
describe("encoded replay path segments", () => {
it.each([
["2", "%32", true],
["%32", "2", true],
["book1", "book%2531", false],
["book%2531", "book1", false],
["book%31", "book%2531", true],
] as const)(
"matches %s against %s by at most one decode",
(segment, value, exact) => {
const users = tool("get_users_by_id");
const ordinal = users.record_ordinals[0]!;
const exchange = {
...source.exchanges.get(ordinal)!,
path: `/api/users/${segment}`,
};
expect(
selectReplayExchange(users, new Map([[ordinal, exchange]]), {
id: value,
}),
).toMatchObject({
exchange,
exact,
});
},
);
});
describe("replayEnvironmentTool selection", () => {
@@ -51,10 +51,6 @@ interface MatchScore {
readonly exact: boolean;
}
/** Replay matches verbatim segments or either spelling decoded once. */
export const pathValueMatches = (segment: string, value: string): boolean =>
pathSegmentMatches(segment, value);
/** Count how many supplied path and query values the recorded request line satisfies. */
const scoreExchange = (
tool: EnvironmentManifestTool,
@@ -83,7 +79,7 @@ const scoreExchange = (
for (const { index, value } of providedPaths) {
const segment = segments.get(index);
if (segment === value) verbatimMatches += 1;
if (segment !== undefined && pathValueMatches(segment, value))
if (segment !== undefined && pathSegmentMatches(segment, value))
pathMatches += 1;
}
const supplied = new Map<string, readonly string[]>();
@@ -99,9 +95,12 @@ const scoreExchange = (
const recorded = new Map<string, string[]>();
const inspectQuery = supplied.size > 0 || observedQuery !== undefined;
const recordedPairs = inspectQuery
? [...iterateUrlQueryParameters(exchange.query ?? "")].filter(([name]) =>
queryArguments.has(name),
)
? [
...iterateUrlQueryParameters(
exchange.query ?? "",
observedQuery === undefined ? supplied : queryArguments,
),
]
: [];
for (const [name, value] of recordedPairs) {
const values = recorded.get(name) ?? [];
@@ -124,9 +123,8 @@ const scoreExchange = (
const observedPairs =
observedQuery === undefined
? undefined
: [...iterateUrlQueryParameters(observedQuery ?? "")]
.filter(([name]) => queryArguments.has(name))
.map(([name, rawValue]) => {
: [...iterateUrlQueryParameters(observedQuery ?? "", queryArguments)].map(
([name, rawValue]) => {
const occurrence = observedOccurrences.get(name) ?? 0;
observedOccurrences.set(name, occurrence + 1);
const argument = queryArguments.get(name);
@@ -140,7 +138,8 @@ const scoreExchange = (
? value[occurrence]
: rawValue;
return [name, normalized] as const;
});
},
);
const observedOrderMatches =
observedPairs === undefined
? 0
@@ -208,9 +207,12 @@ export const selectReplayExchange = (
const queryWasSupplied = suppliedQueryNames.size > 0;
const canInspectQuery = observedQuery !== undefined || queryWasSupplied;
const bestPairs = canInspectQuery
? [...iterateUrlQueryParameters(best.exchange.query ?? "")].filter(
([name]) => suppliedQueryNames.has(name),
)
? [
...iterateUrlQueryParameters(
best.exchange.query ?? "",
suppliedQueryNames,
),
]
: [];
const orderAmbiguous =
observedQuery === undefined &&
@@ -237,8 +239,8 @@ export const selectReplayExchange = (
)
return false;
const pairs = [
...iterateUrlQueryParameters(candidate.query ?? ""),
].filter(([name]) => suppliedQueryNames.has(name));
...iterateUrlQueryParameters(candidate.query ?? "", suppliedQueryNames),
];
return (
pairs.length !== bestPairs.length ||
pairs.some(
@@ -52,3 +52,25 @@ it("bounds content-type display without changing body interpretation or retained
contentType,
);
});
it("keeps observed query order distinct from calls supplying only selected fields", () => {
const source = environmentServeSourceWithBody("{}", "application/json");
const original = source.manifest.tools.find(
(tool) => tool.name === "get_users_by_id",
)!;
const ordinal = original.record_ordinals[0]!;
const tool = { ...original, query_parameter_names: ["ignored", "q"] };
const retained = {
...source.exchanges.get(ordinal)!,
query: `?${"ignored=%FF+value&".repeat(20000)}%71=hi%74`,
};
const exchanges = new Map([[ordinal, retained]]);
const args = { id: "2", q: "hit" };
expect(selectReplayExchange(tool, exchanges, args)?.exact).toBe(true);
expect(selectReplayExchange(tool, exchanges, args, "?q=hit")?.exact).toBe(
false,
);
expect(
selectReplayExchange(tool, exchanges, args, retained.query)?.exact,
).toBe(true);
});
@@ -4,6 +4,7 @@ import {
} from "../unicodeCodePointOrder.js";
import {
iterateUrlQueryParameters,
parseUrlQueryParameterNames,
singleQueryValues,
} from "../recordedTransportUrl.js";
import {
@@ -141,9 +142,7 @@ export const servedLine = (
): ServedLine | undefined => {
const key = queryKey(record.query);
if (lines === undefined || key === undefined) return undefined;
const names = [...iterateUrlQueryParameters(record.query ?? "")].map(
([name]) => name,
);
const names = [...parseUrlQueryParameterNames(record.query ?? "")];
const named = bestLine(lines, lines.byQuery.get(key) ?? [], record);
if (named !== undefined) return { ...named, named: true };
const unfiltered =
@@ -165,7 +164,7 @@ const queryMatches = (
query: string | undefined,
): number => {
const matched = new Set<string>();
for (const [name, value] of iterateUrlQueryParameters(query ?? ""))
for (const [name, value] of iterateUrlQueryParameters(query ?? "", read))
if (read.get(name) === value) matched.add(name);
return matched.size;
};
+4 -3
View File
@@ -49,9 +49,10 @@ it.each([
["x", ""],
["", ""],
]);
expect([...iterateUrlQueryParameters(query, selected)]).toEqual(
[...expected].filter(([name]) => selected.has(name)),
);
for (const selection of [selected, new Set(selected.keys())])
expect([...iterateUrlQueryParameters(query, selection)]).toEqual(
[...expected].filter(([name]) => selection.has(name)),
);
});
it.each([
+1 -1
View File
@@ -122,7 +122,7 @@ function* queryFields(
/** Scan one field at a time; selection avoids decoding unrelated values. */
export function* iterateUrlQueryParameters(
query: string,
selected?: ReadonlyMap<string, string>,
selected?: ReadonlyMap<string, unknown> | ReadonlySet<string>,
): IterableIterator<[string, string]> {
for (const [start, nameEnd, end] of queryFields(query)) {
const name = formComponent(query.slice(start, nameEnd));