mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
perf(environment): decode only selected replay query values
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user