@enricai/barnacle 1.12.20 → 1.12.22
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/dist/scripts/recon-generate-multicall-fixture.d.ts +190 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +503 -2
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +71 -27
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +644 -55
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -52,8 +52,16 @@ exports.compileActionSteps = compileActionSteps;
|
|
|
52
52
|
exports.collectHeaderBindings = collectHeaderBindings;
|
|
53
53
|
exports.deriveProducerBoundaryBindings = deriveProducerBoundaryBindings;
|
|
54
54
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
55
|
+
exports.findAllObjectArrayFields = findAllObjectArrayFields;
|
|
56
|
+
exports.findAllObjectArrayFieldsOrWholeObject = findAllObjectArrayFieldsOrWholeObject;
|
|
57
|
+
exports.collectResponseLeafValues = collectResponseLeafValues;
|
|
58
|
+
exports.getScanPrimaryCandidateGroupsCallCountForTest = getScanPrimaryCandidateGroupsCallCountForTest;
|
|
59
|
+
exports.resetScanPrimaryCandidateGroupsCallCountForTest = resetScanPrimaryCandidateGroupsCallCountForTest;
|
|
55
60
|
exports.detectDrillDownFoldPlan = detectDrillDownFoldPlan;
|
|
56
61
|
exports.parseFoldReturnSpec = parseFoldReturnSpec;
|
|
62
|
+
exports.collectRequestValuesIncludingHeaders = collectRequestValuesIncludingHeaders;
|
|
63
|
+
exports.getFoldPlanResolutionCallCountForTests = getFoldPlanResolutionCallCountForTests;
|
|
64
|
+
exports.resetFoldPlanResolutionCallCountForTests = resetFoldPlanResolutionCallCountForTests;
|
|
57
65
|
exports.resolveFoldPlan = resolveFoldPlan;
|
|
58
66
|
exports.buildContractChecklist = buildContractChecklist;
|
|
59
67
|
exports.emitContractTs = emitContractTs;
|
|
@@ -1414,16 +1422,24 @@ function resolveManifestActionSequence(runRoot, captures) {
|
|
|
1414
1422
|
* JWT refresh, reference-lookup) that a browser fires incidentally. Absent
|
|
1415
1423
|
* patterns preserve the noise heuristic exactly.
|
|
1416
1424
|
*
|
|
1425
|
+
* When the flow declares a `foldReturnSpec`, a GET whose URL matches its
|
|
1426
|
+
* `endpointPattern` is admitted despite the GET drop above — the same scoped
|
|
1427
|
+
* rule `buildFoldPlanFromSpec` later uses to resolve the fold plan, so a
|
|
1428
|
+
* spec-declared GET drill-down survives to reach it instead of being
|
|
1429
|
+
* dropped before the fold pipeline ever sees it. Every other GET is still
|
|
1430
|
+
* dropped.
|
|
1431
|
+
*
|
|
1417
1432
|
* Exported for tests: this predicate decides what a generated plugin will POST
|
|
1418
1433
|
* at a live site, and it is the only gate between a browser's incidental
|
|
1419
1434
|
* chatter and the emitted hot path.
|
|
1420
1435
|
*/
|
|
1421
|
-
function extractActionSequence(captures, submitPatterns = null) {
|
|
1436
|
+
function extractActionSequence(captures, submitPatterns = null, foldReturnSpec = null) {
|
|
1422
1437
|
const matchesSubmit = compileSubmitMatcher(submitPatterns);
|
|
1438
|
+
const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
|
|
1423
1439
|
return captures
|
|
1424
1440
|
.map((capture, index) => ({ capture, index }))
|
|
1425
1441
|
.filter(({ capture }) => {
|
|
1426
|
-
if (capture.method === "GET")
|
|
1442
|
+
if (capture.method === "GET" && !matchesFoldReturn(capture))
|
|
1427
1443
|
return false;
|
|
1428
1444
|
if (capture.status < 200 || capture.status >= 300)
|
|
1429
1445
|
return false;
|
|
@@ -1445,11 +1461,23 @@ function extractActionSequence(captures, submitPatterns = null) {
|
|
|
1445
1461
|
* what let a chronologically-first fallback pick an unrelated query. Host is
|
|
1446
1462
|
* NOT a filter criterion, matching {@link extractActionSequence}.
|
|
1447
1463
|
*
|
|
1464
|
+
* When the flow declares a `foldReturnSpec`, a non-mutation capture whose
|
|
1465
|
+
* URL matches its `endpointPattern` is admitted despite the query drop
|
|
1466
|
+
* above, mirroring {@link extractActionSequence}'s GET admission. So is a
|
|
1467
|
+
* non-mutation capture whose response resolves the spec's own `resultsPath`
|
|
1468
|
+
* — the GraphQL-primary read op the drill-down folds onto, which
|
|
1469
|
+
* `endpointPattern` (naming the drill, not the primary) never matches on its
|
|
1470
|
+
* own; without this a declared spec would admit the drill-down but leave
|
|
1471
|
+
* `resolveFoldPlan` with no primary capture to resolve `resultsPath`
|
|
1472
|
+
* against. Every other non-mutation capture is still dropped.
|
|
1473
|
+
*
|
|
1448
1474
|
* Exported for tests: this predicate decides what a generated GraphQL plugin
|
|
1449
1475
|
* will send at a live site.
|
|
1450
1476
|
*/
|
|
1451
|
-
function extractGraphQLActionSequence(captures, submitPatterns = null) {
|
|
1477
|
+
function extractGraphQLActionSequence(captures, submitPatterns = null, foldReturnSpec = null) {
|
|
1452
1478
|
const matchesSubmit = compileSubmitMatcher(submitPatterns);
|
|
1479
|
+
const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
|
|
1480
|
+
const matchesFoldReturnResults = compileFoldReturnResultsMatcher(foldReturnSpec);
|
|
1453
1481
|
return captures
|
|
1454
1482
|
.map((capture, index) => ({ capture, index }))
|
|
1455
1483
|
.filter(({ capture }) => {
|
|
@@ -1459,7 +1487,9 @@ function extractGraphQLActionSequence(captures, submitPatterns = null) {
|
|
|
1459
1487
|
return false;
|
|
1460
1488
|
if (!matchesSubmit(capture))
|
|
1461
1489
|
return false;
|
|
1462
|
-
|
|
1490
|
+
if (capture.query !== null && /^\s*mutation\b/.test(capture.query))
|
|
1491
|
+
return true;
|
|
1492
|
+
return matchesFoldReturn(capture) || matchesFoldReturnResults(capture);
|
|
1463
1493
|
});
|
|
1464
1494
|
}
|
|
1465
1495
|
/**
|
|
@@ -1530,8 +1560,10 @@ function jsonBodyLeafValues(requestPostData) {
|
|
|
1530
1560
|
if (parsed === undefined)
|
|
1531
1561
|
return null;
|
|
1532
1562
|
const values = [];
|
|
1533
|
-
for (const { value } of
|
|
1534
|
-
|
|
1563
|
+
for (const { value } of walkAllPrimitiveLeaves(parsed)) {
|
|
1564
|
+
if (value !== null)
|
|
1565
|
+
values.push(String(value));
|
|
1566
|
+
}
|
|
1535
1567
|
return values;
|
|
1536
1568
|
}
|
|
1537
1569
|
/**
|
|
@@ -2622,11 +2654,29 @@ function* walkSetCookiePairs(rawSetCookie) {
|
|
|
2622
2654
|
* Exception: values in `PLACEHOLDER_STATE_VALUES` are skipped entirely so
|
|
2623
2655
|
* the LATER non-placeholder occurrence at the same JSON path becomes the
|
|
2624
2656
|
* canonical binding instead.
|
|
2657
|
+
*
|
|
2658
|
+
* `forceIncludeValues` (see {@link collectDependentDrillDownChainValues})
|
|
2659
|
+
* bypasses `MIN_STATE_VALUE_LENGTH` for the specific values it names — a
|
|
2660
|
+
* value already confirmed, by the fold-chain detector itself, to be threaded
|
|
2661
|
+
* from one dependent-drill-down chain hop's response into the next hop's
|
|
2662
|
+
* request is exactly as legitimate a produced state value as a long one; a
|
|
2663
|
+
* length floor exists to keep an UNRELATED short value (an enum code, a page
|
|
2664
|
+
* number) from being mistaken for reused state by blind substring/value
|
|
2665
|
+
* matching, and a value the chain detector already confirmed is threaded
|
|
2666
|
+
* carries no such ambiguity. Every other filter (MAX length, placeholder,
|
|
2667
|
+
* shielded UUID, GET-non-UUID) still applies.
|
|
2625
2668
|
*/
|
|
2626
2669
|
/** Exported for unit testing — lets tests exercise the produces[] walk (body
|
|
2627
2670
|
* AND header/cookie origins) directly against synthetic Capture sequences. */
|
|
2628
|
-
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set()) {
|
|
2671
|
+
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set(), forceIncludeValues = new Set()) {
|
|
2629
2672
|
const index = new Map();
|
|
2673
|
+
// Computed structurally off the SAME captures being indexed (no
|
|
2674
|
+
// foldReturnSpec available at this layer) — a spec-declared fold's own
|
|
2675
|
+
// chain values reach here via the caller-supplied `forceIncludeValues`
|
|
2676
|
+
// (see recon-generate's top-level `collectDependentDrillDownChainValues`
|
|
2677
|
+
// call), so this indexes a chain-produced value regardless of whether the
|
|
2678
|
+
// fold plan that confirmed it is structural or spec-declared.
|
|
2679
|
+
const chainForceIncludeValues = collectDependentDrillDownChainValues(captures.map((capture) => ({ capture })), null);
|
|
2630
2680
|
// First pass: identify the earliest origin among ACTION captures for each
|
|
2631
2681
|
// value. Action-only earliest-origin tracking is what compileActionSteps'
|
|
2632
2682
|
// produces[] check needs — it ignores non-action captures (telemetry GETs,
|
|
@@ -2645,7 +2695,13 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2645
2695
|
const rawSetCookie = Object.entries(c.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
|
|
2646
2696
|
if (rawSetCookie !== undefined) {
|
|
2647
2697
|
for (const { name, value } of walkSetCookiePairs(rawSetCookie)) {
|
|
2648
|
-
|
|
2698
|
+
// Same chain/force exemption as the body-value MIN_STATE_VALUE_LENGTH
|
|
2699
|
+
// floor below: a cookie-sourced value the fold-chain detector already
|
|
2700
|
+
// confirmed is threaded into a later hop's request is exactly as
|
|
2701
|
+
// legitimate as a long one, so it must not be dropped for being short.
|
|
2702
|
+
if (value.length < MIN_STATE_VALUE_LENGTH &&
|
|
2703
|
+
!chainForceIncludeValues.has(value) &&
|
|
2704
|
+
!forceIncludeValues.has(value))
|
|
2649
2705
|
continue;
|
|
2650
2706
|
if (value.length > MAX_COOKIE_STATE_VALUE_LENGTH)
|
|
2651
2707
|
continue;
|
|
@@ -2661,6 +2717,31 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2661
2717
|
}
|
|
2662
2718
|
}
|
|
2663
2719
|
}
|
|
2720
|
+
// Non-cookie response headers are only indexed for values the fold-chain
|
|
2721
|
+
// detector already confirmed are threaded from this hop's response into a
|
|
2722
|
+
// later hop's request (`chainForceIncludeValues`) — unlike Set-Cookie,
|
|
2723
|
+
// which is always a plausible token mint, an arbitrary header (e.g.
|
|
2724
|
+
// `X-Conversation-Id`) is indexed as producible state only when chain
|
|
2725
|
+
// detection itself has already established that reuse, so this never
|
|
2726
|
+
// sweeps every header value as noise.
|
|
2727
|
+
for (const [headerName, headerValue] of Object.entries(c.responseHeaders)) {
|
|
2728
|
+
if (headerName.toLowerCase() === "set-cookie")
|
|
2729
|
+
continue;
|
|
2730
|
+
if (!chainForceIncludeValues.has(headerValue))
|
|
2731
|
+
continue;
|
|
2732
|
+
if (headerValue.length > MAX_COOKIE_STATE_VALUE_LENGTH)
|
|
2733
|
+
continue;
|
|
2734
|
+
if (PLACEHOLDER_STATE_VALUES.has(headerValue))
|
|
2735
|
+
continue;
|
|
2736
|
+
if (!index.has(headerValue)) {
|
|
2737
|
+
index.set(headerValue, {
|
|
2738
|
+
value: headerValue,
|
|
2739
|
+
originIndex: i,
|
|
2740
|
+
path: [],
|
|
2741
|
+
headerOrigin: { sourceHeader: headerName },
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2664
2745
|
if (c.responseBody === undefined || c.responseBody === null)
|
|
2665
2746
|
continue;
|
|
2666
2747
|
// For GET captures, only index UUID-shaped strings. GET captures (today,
|
|
@@ -2671,8 +2752,13 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2671
2752
|
// e.g. "candidate" as a state value gets substituted INSIDE an already-
|
|
2672
2753
|
// emitted ${candidateId} interpolation, producing ${${entityTypeCode}Id}.
|
|
2673
2754
|
const isGet = c.method === "GET";
|
|
2674
|
-
for (const { value, path } of
|
|
2675
|
-
if (
|
|
2755
|
+
for (const { value: rawValue, path } of walkAllPrimitiveLeaves(c.responseBody)) {
|
|
2756
|
+
if (rawValue === null)
|
|
2757
|
+
continue;
|
|
2758
|
+
const value = String(rawValue);
|
|
2759
|
+
if (value.length < MIN_STATE_VALUE_LENGTH &&
|
|
2760
|
+
!chainForceIncludeValues.has(value) &&
|
|
2761
|
+
!forceIncludeValues.has(value))
|
|
2676
2762
|
continue;
|
|
2677
2763
|
if (value.length > MAX_STATE_VALUE_LENGTH)
|
|
2678
2764
|
continue;
|
|
@@ -2685,7 +2771,16 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
2685
2771
|
// corrupt T2/T3's already-substituted Values.
|
|
2686
2772
|
if (shieldedUuids.has(value))
|
|
2687
2773
|
continue;
|
|
2688
|
-
|
|
2774
|
+
// Same chain/force exemption as the MIN_STATE_VALUE_LENGTH floor above:
|
|
2775
|
+
// a value the fold-chain detector already confirmed is threaded from
|
|
2776
|
+
// this GET hop's response into a later hop's request is exactly as
|
|
2777
|
+
// legitimate as a UUID anchor, so it must not be dropped just because
|
|
2778
|
+
// this hop happens to be a GET rather than every existing fixture's
|
|
2779
|
+
// POST.
|
|
2780
|
+
if (isGet &&
|
|
2781
|
+
!UUID_REGEX.test(value) &&
|
|
2782
|
+
!chainForceIncludeValues.has(value) &&
|
|
2783
|
+
!forceIncludeValues.has(value))
|
|
2689
2784
|
continue;
|
|
2690
2785
|
if (!index.has(value)) {
|
|
2691
2786
|
index.set(value, { value, originIndex: i, path });
|
|
@@ -2891,8 +2986,48 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
2891
2986
|
});
|
|
2892
2987
|
}
|
|
2893
2988
|
}
|
|
2989
|
+
// Non-cookie response-header-origin produces — mirrors the Set-Cookie
|
|
2990
|
+
// block above but for a plain header (e.g. `X-Price-Token`) whose value
|
|
2991
|
+
// `indexStateValues` indexed with `headerOrigin.sourceHeader` set to the
|
|
2992
|
+
// real header name. Only emitted when the value is actually consumed as
|
|
2993
|
+
// a REQUEST HEADER downstream (`usedValueTargetHeader`) — `createHttpClient`'s
|
|
2994
|
+
// `bind` option (see http-client.ts) is the only mechanism that can
|
|
2995
|
+
// thread a header-origin value forward, since the emitted response
|
|
2996
|
+
// variable never exposes response headers to the rest of the generated
|
|
2997
|
+
// code the way it exposes the parsed body.
|
|
2998
|
+
for (const [headerName, headerValue] of Object.entries(capture.responseHeaders)) {
|
|
2999
|
+
if (headerName.toLowerCase() === "set-cookie")
|
|
3000
|
+
continue;
|
|
3001
|
+
if (!usedValues.has(headerValue))
|
|
3002
|
+
continue;
|
|
3003
|
+
const sv = stateIndex.get(headerValue);
|
|
3004
|
+
if (!sv || sv.originIndex !== index || !sv.headerOrigin)
|
|
3005
|
+
continue;
|
|
3006
|
+
const targetHeader = usedValueTargetHeader.get(headerValue);
|
|
3007
|
+
if (!targetHeader)
|
|
3008
|
+
continue;
|
|
3009
|
+
let name = `${headerName.replace(/[^A-Za-z0-9]/g, "")}Header`;
|
|
3010
|
+
if (!/^[A-Za-z_$]/.test(name))
|
|
3011
|
+
name = `_${name}`;
|
|
3012
|
+
let suffix = 1;
|
|
3013
|
+
while (seenNames.has(name)) {
|
|
3014
|
+
suffix++;
|
|
3015
|
+
name = `${headerName.replace(/[^A-Za-z0-9]/g, "")}Header${suffix}`;
|
|
3016
|
+
}
|
|
3017
|
+
seenNames.add(name);
|
|
3018
|
+
produces.push({
|
|
3019
|
+
kind: "header",
|
|
3020
|
+
name,
|
|
3021
|
+
sourceHeader: sv.headerOrigin.sourceHeader,
|
|
3022
|
+
cookieName: sv.headerOrigin.cookieName,
|
|
3023
|
+
targetHeader,
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
2894
3026
|
if (capture.responseBody !== undefined && capture.responseBody !== null) {
|
|
2895
|
-
for (const { value, path } of
|
|
3027
|
+
for (const { value: rawValue, path } of walkAllPrimitiveLeaves(capture.responseBody)) {
|
|
3028
|
+
if (rawValue === null)
|
|
3029
|
+
continue;
|
|
3030
|
+
const value = String(rawValue);
|
|
2896
3031
|
if (!usedValues.has(value))
|
|
2897
3032
|
continue;
|
|
2898
3033
|
const sv = stateIndex.get(value);
|
|
@@ -2977,7 +3112,9 @@ function resolveResponsePathValue(responseBody, path) {
|
|
|
2977
3112
|
return null;
|
|
2978
3113
|
}
|
|
2979
3114
|
}
|
|
2980
|
-
return typeof cursor === "string"
|
|
3115
|
+
return typeof cursor === "string" || typeof cursor === "number" || typeof cursor === "boolean"
|
|
3116
|
+
? String(cursor)
|
|
3117
|
+
: null;
|
|
2981
3118
|
}
|
|
2982
3119
|
/**
|
|
2983
3120
|
* Replaces occurrences of state values in `template` with `${varName}`
|
|
@@ -3738,7 +3875,8 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
3738
3875
|
const matchesJoinField = target.joinFields.some((field) => {
|
|
3739
3876
|
const value = readValueAtPath(firstItem, field.split("."));
|
|
3740
3877
|
return ((typeof value === "string" && value.length > 0 && value === headerValue) ||
|
|
3741
|
-
(typeof value === "number"
|
|
3878
|
+
((typeof value === "number" || typeof value === "boolean") &&
|
|
3879
|
+
String(value) === headerValue));
|
|
3742
3880
|
});
|
|
3743
3881
|
if (matchesJoinField)
|
|
3744
3882
|
headerNames.add(headerName);
|
|
@@ -3750,7 +3888,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
3750
3888
|
const value = readValueAtPath(firstItem, field.split("."));
|
|
3751
3889
|
if (typeof value === "string" && value.length > 0)
|
|
3752
3890
|
joinValues.add(value);
|
|
3753
|
-
else if (typeof value === "number")
|
|
3891
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
3754
3892
|
joinValues.add(String(value));
|
|
3755
3893
|
}
|
|
3756
3894
|
if (joinValues.size > 0)
|
|
@@ -4056,7 +4194,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
4056
4194
|
const value = readValueAtPath(firstItem, field.split("."));
|
|
4057
4195
|
const stringValue = typeof value === "string" && value.length > 0
|
|
4058
4196
|
? value
|
|
4059
|
-
: typeof value === "number"
|
|
4197
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
4060
4198
|
? String(value)
|
|
4061
4199
|
: null;
|
|
4062
4200
|
return stringValue !== null
|
|
@@ -4461,17 +4599,42 @@ const ARRAY_WILDCARD_SEGMENT = "*";
|
|
|
4461
4599
|
* A path segment for an array index the search descended through (to keep
|
|
4462
4600
|
* looking for a nested candidate array) is the {@link ARRAY_WILDCARD_SEGMENT}
|
|
4463
4601
|
* sentinel, never a literal index — see its docstring. */
|
|
4464
|
-
function
|
|
4602
|
+
function findAllObjectArrayFieldsUncached(value, path = []) {
|
|
4465
4603
|
if (value === null || typeof value !== "object")
|
|
4466
4604
|
return [];
|
|
4467
4605
|
if (Array.isArray(value)) {
|
|
4468
4606
|
const objectItems = value.filter(isObjectArrayItem);
|
|
4469
|
-
const nestedCandidates = objectItems.flatMap((item) =>
|
|
4607
|
+
const nestedCandidates = objectItems.flatMap((item) => findAllObjectArrayFieldsUncached(item, [...path, ARRAY_WILDCARD_SEGMENT]));
|
|
4470
4608
|
return objectItems.length > 0
|
|
4471
4609
|
? [{ path, items: objectItems }, ...nestedCandidates]
|
|
4472
4610
|
: nestedCandidates;
|
|
4473
4611
|
}
|
|
4474
|
-
return Object.entries(value).flatMap(([key, v]) =>
|
|
4612
|
+
return Object.entries(value).flatMap(([key, v]) => findAllObjectArrayFieldsUncached(v, [...path, key]));
|
|
4613
|
+
}
|
|
4614
|
+
/** Every distinct top-level `value` object {@link findAllObjectArrayFields}
|
|
4615
|
+
* is invoked on is scanned repeatedly — once per fold-chain candidate/join
|
|
4616
|
+
* disambiguation that re-derives the same response body — so a per-run,
|
|
4617
|
+
* identity-keyed cache lets a given body's whole-tree scan run at most once
|
|
4618
|
+
* regardless of how many callers re-derive it. Keyed on object identity
|
|
4619
|
+
* (never a serialized path/value pair) because captures/response bodies are
|
|
4620
|
+
* never mutated once produced (see this module's fold-plan investigation
|
|
4621
|
+
* notes), so identity alone is a safe, unconditionally correct cache key. */
|
|
4622
|
+
const objectArrayFieldsCache = new WeakMap();
|
|
4623
|
+
/** Memoized entry point for {@link findAllObjectArrayFieldsUncached} — see
|
|
4624
|
+
* {@link objectArrayFieldsCache}. Only the default top-level `path` is
|
|
4625
|
+
* cached (every real call site invokes with the default); a caller passing
|
|
4626
|
+
* an explicit `path` — recursion within the uncached walk itself — bypasses
|
|
4627
|
+
* the cache and hits the underlying scan directly. */
|
|
4628
|
+
function findAllObjectArrayFields(value, path = []) {
|
|
4629
|
+
if (path.length > 0 || value === null || typeof value !== "object") {
|
|
4630
|
+
return findAllObjectArrayFieldsUncached(value, path);
|
|
4631
|
+
}
|
|
4632
|
+
const cached = objectArrayFieldsCache.get(value);
|
|
4633
|
+
if (cached)
|
|
4634
|
+
return cached;
|
|
4635
|
+
const computed = findAllObjectArrayFieldsUncached(value, path);
|
|
4636
|
+
objectArrayFieldsCache.set(value, computed);
|
|
4637
|
+
return computed;
|
|
4475
4638
|
}
|
|
4476
4639
|
/** The first object-array field by DFS/key order — see
|
|
4477
4640
|
* {@link findAllObjectArrayFields}. Every call site that must disambiguate
|
|
@@ -4495,9 +4658,24 @@ function findObjectArrayField(value, path = []) {
|
|
|
4495
4658
|
* more genuine per-item data than a small real nested object-array; a
|
|
4496
4659
|
* caller that just wants the first real array (the common case) is
|
|
4497
4660
|
* unaffected since it still comes first. */
|
|
4661
|
+
const objectArrayFieldsOrWholeObjectCache = new WeakMap();
|
|
4662
|
+
/** Memoized the same way as {@link findAllObjectArrayFields} (see
|
|
4663
|
+
* {@link objectArrayFieldsCache}) — this is the candidate list
|
|
4664
|
+
* {@link selectDisambiguatedCandidate} and {@link buildFoldPlanFromSpec}
|
|
4665
|
+
* re-derive off the SAME responseBody on every chain hop/disambiguation, so
|
|
4666
|
+
* it is exactly as hot a redundant-recompute site as the underlying scan. */
|
|
4498
4667
|
function findAllObjectArrayFieldsOrWholeObject(value, path = []) {
|
|
4668
|
+
if (path.length > 0 || value === null || typeof value !== "object") {
|
|
4669
|
+
const found = findAllObjectArrayFields(value, path);
|
|
4670
|
+
return isObjectArrayItem(value) ? [...found, { path, items: [value] }] : found;
|
|
4671
|
+
}
|
|
4672
|
+
const cached = objectArrayFieldsOrWholeObjectCache.get(value);
|
|
4673
|
+
if (cached)
|
|
4674
|
+
return cached;
|
|
4499
4675
|
const found = findAllObjectArrayFields(value, path);
|
|
4500
|
-
|
|
4676
|
+
const computed = isObjectArrayItem(value) ? [...found, { path, items: [value] }] : found;
|
|
4677
|
+
objectArrayFieldsOrWholeObjectCache.set(value, computed);
|
|
4678
|
+
return computed;
|
|
4501
4679
|
}
|
|
4502
4680
|
/** The first candidate from {@link findAllObjectArrayFieldsOrWholeObject} —
|
|
4503
4681
|
* the flat-object-aware counterpart of {@link findObjectArrayField}. */
|
|
@@ -4539,14 +4717,14 @@ function collectRequestStringValues(capture) {
|
|
|
4539
4717
|
})();
|
|
4540
4718
|
if (parsedBody !== undefined) {
|
|
4541
4719
|
for (const { value } of walkAllPrimitiveLeaves(parsedBody)) {
|
|
4542
|
-
if (typeof value === "number")
|
|
4720
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
4543
4721
|
values.add(String(value));
|
|
4544
4722
|
}
|
|
4545
4723
|
}
|
|
4546
4724
|
return values;
|
|
4547
4725
|
}
|
|
4548
4726
|
/**
|
|
4549
|
-
* Yields every string/numeric leaf reachable from `item` by walking nested
|
|
4727
|
+
* Yields every string/numeric/boolean leaf reachable from `item` by walking nested
|
|
4550
4728
|
* plain objects only (not arrays — a join key is a scalar field of the item
|
|
4551
4729
|
* or one of its nested objects, never an element drawn from a nested array),
|
|
4552
4730
|
* paired with its dot-separated path from `item`'s root. A bare top-level
|
|
@@ -4565,8 +4743,8 @@ function* walkItemFieldPaths(item, path = []) {
|
|
|
4565
4743
|
}
|
|
4566
4744
|
}
|
|
4567
4745
|
/**
|
|
4568
|
-
* Finds the ordered list of an array item's string/numeric field
|
|
4569
|
-
* values are threaded into `drillCapture`'s outbound request — the join key a
|
|
4746
|
+
* Finds the ordered list of an array item's string/numeric/boolean field
|
|
4747
|
+
* paths whose values are threaded into `drillCapture`'s outbound request — the join key a
|
|
4570
4748
|
* dependent drill-down call was built from. Each entry is a dot-separated
|
|
4571
4749
|
* path (see {@link readValueAtPath} / {@link pathToAccessor}), so a bare
|
|
4572
4750
|
* top-level field stays a single segment (e.g. `"sku"`) and a field nested
|
|
@@ -4576,29 +4754,107 @@ function* walkItemFieldPaths(item, path = []) {
|
|
|
4576
4754
|
* order the primary response declares them, not sorted. Returns `[]` when no
|
|
4577
4755
|
* field of the item threads into the request at all.
|
|
4578
4756
|
*/
|
|
4757
|
+
/**
|
|
4758
|
+
* Maps every string/numeric/boolean value seen in any action's request URL
|
|
4759
|
+
* or body (see {@link collectRequestStringValues} — deliberately headers-
|
|
4760
|
+
* excluded, unlike {@link buildRequestValueIndex}, matching the structural
|
|
4761
|
+
* heuristic's own header-blind scan) to the ascending list of action indices
|
|
4762
|
+
* whose request carries it. Built once per {@link detectDrillDownFoldPlan}
|
|
4763
|
+
* call and shared across every primary candidate's scan, so
|
|
4764
|
+
* {@link scanPrimaryCandidateGroups} can jump straight to the indices that
|
|
4765
|
+
* could possibly thread one of a primary array's own item values instead of
|
|
4766
|
+
* walking every later action — the O(actions)-per-primary forward scan that
|
|
4767
|
+
* makes the structural heuristic itself O(actions^2) on a large capture set
|
|
4768
|
+
* dominated by same-shaped primary candidates.
|
|
4769
|
+
*/
|
|
4770
|
+
function buildRequestStringValueIndex(actions) {
|
|
4771
|
+
const index = new Map();
|
|
4772
|
+
for (let i = 0; i < actions.length; i++) {
|
|
4773
|
+
for (const value of collectRequestStringValues(actions[i].capture)) {
|
|
4774
|
+
const indices = index.get(value);
|
|
4775
|
+
if (indices)
|
|
4776
|
+
indices.push(i);
|
|
4777
|
+
else
|
|
4778
|
+
index.set(value, [i]);
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
return index;
|
|
4782
|
+
}
|
|
4783
|
+
/** Every string/numeric/boolean field value present anywhere across `items`
|
|
4784
|
+
* — the set of values whose {@link buildRequestStringValueIndex} entries can
|
|
4785
|
+
* possibly thread out of this primary array, used to prune the candidate
|
|
4786
|
+
* drill indices {@link scanPrimaryCandidateGroups} walks instead of
|
|
4787
|
+
* considering every later action index. */
|
|
4788
|
+
function collectItemsFieldValues(items) {
|
|
4789
|
+
const values = new Set();
|
|
4790
|
+
for (const item of items) {
|
|
4791
|
+
for (const { value } of walkItemFieldPaths(item)) {
|
|
4792
|
+
if (typeof value === "string" && value.length > 0)
|
|
4793
|
+
values.add(value);
|
|
4794
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
4795
|
+
values.add(String(value));
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
return values;
|
|
4799
|
+
}
|
|
4800
|
+
/** Ascending, deduped action indices strictly greater than `afterIndex`
|
|
4801
|
+
* whose request carries at least one of `values` — see
|
|
4802
|
+
* {@link collectItemsFieldValues} / {@link buildRequestStringValueIndex}. */
|
|
4803
|
+
function collectCandidateIndicesAscending(requestStringValueIndex, values, afterIndex) {
|
|
4804
|
+
const candidates = new Set();
|
|
4805
|
+
for (const value of values) {
|
|
4806
|
+
for (const index of requestStringValueIndex.get(value) ?? []) {
|
|
4807
|
+
if (index > afterIndex)
|
|
4808
|
+
candidates.add(index);
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
return [...candidates].sort((a, b) => a - b);
|
|
4812
|
+
}
|
|
4579
4813
|
function findThreadedJoinFields(item, drillCapture) {
|
|
4580
4814
|
const requestValues = collectRequestStringValues(drillCapture);
|
|
4581
4815
|
if (requestValues.size === 0)
|
|
4582
4816
|
return [];
|
|
4583
4817
|
return [...walkItemFieldPaths(item)]
|
|
4584
4818
|
.filter(({ value: v }) => (typeof v === "string" && v.length > 0 && requestValues.has(v)) ||
|
|
4585
|
-
(typeof v === "number" && requestValues.has(String(v)))
|
|
4819
|
+
(typeof v === "number" && requestValues.has(String(v))) ||
|
|
4820
|
+
(typeof v === "boolean" && requestValues.has(String(v))))
|
|
4586
4821
|
.map(({ path }) => path.join("."));
|
|
4587
4822
|
}
|
|
4588
|
-
/** Every string and
|
|
4823
|
+
/** Every string, numeric, and boolean leaf value present anywhere in a response —
|
|
4589
4824
|
* the set a chained drill-down step's request must overlap with for that
|
|
4590
4825
|
* step to count as depending on this response. Deliberately walks the WHOLE
|
|
4591
4826
|
* body (not just object-array items, unlike {@link findThreadedJoinFields})
|
|
4592
4827
|
* since a chained step can thread any response value, not only a per-item
|
|
4593
|
-
* join field.
|
|
4594
|
-
|
|
4828
|
+
* join field. Also walks every response HEADER value, mirroring
|
|
4829
|
+
* {@link collectRequestValuesIncludingHeaders} on the request side, since a
|
|
4830
|
+
* chain hop can just as easily mint its join token in a response header
|
|
4831
|
+
* (e.g. a `Location` or custom correlation header) as in the body.
|
|
4832
|
+
* Memoized like {@link objectArrayFieldsCache} — {@link computeFoldChain}'s
|
|
4833
|
+
* inner `dependsOnChain` check re-derives this for every chain member on
|
|
4834
|
+
* every outer loop iteration, making it the single hottest redundant-
|
|
4835
|
+
* recompute site in fold-chain resolution (see this module's fold-plan
|
|
4836
|
+
* investigation notes). */
|
|
4837
|
+
const responseLeafValuesCache = new WeakMap();
|
|
4838
|
+
function collectResponseLeafValues(capture) {
|
|
4839
|
+
const cached = responseLeafValuesCache.get(capture);
|
|
4840
|
+
if (cached)
|
|
4841
|
+
return cached;
|
|
4842
|
+
const computed = collectResponseLeafValuesUncached(capture);
|
|
4843
|
+
responseLeafValuesCache.set(capture, computed);
|
|
4844
|
+
return computed;
|
|
4845
|
+
}
|
|
4846
|
+
function collectResponseLeafValuesUncached(capture) {
|
|
4595
4847
|
const values = new Set();
|
|
4596
|
-
for (const { value } of walkAllPrimitiveLeaves(responseBody)) {
|
|
4848
|
+
for (const { value } of walkAllPrimitiveLeaves(capture.responseBody)) {
|
|
4597
4849
|
if (typeof value === "string" && value.length > 0)
|
|
4598
4850
|
values.add(value);
|
|
4599
4851
|
if (typeof value === "number")
|
|
4600
4852
|
values.add(String(value));
|
|
4853
|
+
if (typeof value === "boolean")
|
|
4854
|
+
values.add(String(value));
|
|
4601
4855
|
}
|
|
4856
|
+
for (const v of Object.values(capture.responseHeaders))
|
|
4857
|
+
values.add(v);
|
|
4602
4858
|
return values;
|
|
4603
4859
|
}
|
|
4604
4860
|
/**
|
|
@@ -4657,7 +4913,7 @@ function computeFoldChain(actions, drillStepIndex, drillArrayPath) {
|
|
|
4657
4913
|
const requestValues = collectRequestValuesIncludingHeaders(candidate.capture);
|
|
4658
4914
|
const dependsOnChain = chain.some((chainIndex) => {
|
|
4659
4915
|
const chainStepCapture = actions[chainIndex].capture;
|
|
4660
|
-
const responseValues = collectResponseLeafValues(chainStepCapture
|
|
4916
|
+
const responseValues = collectResponseLeafValues(chainStepCapture);
|
|
4661
4917
|
const echoedValues = collectRequestValuesIncludingHeaders(chainStepCapture);
|
|
4662
4918
|
return [...responseValues].some((v) => !echoedValues.has(v) && requestValues.has(v));
|
|
4663
4919
|
});
|
|
@@ -4725,7 +4981,7 @@ function directPrimitiveChildCountExcludingEchoed(obj, requestValues) {
|
|
|
4725
4981
|
let n = 0;
|
|
4726
4982
|
for (const v of Object.values(obj)) {
|
|
4727
4983
|
if (v === null || (typeof v !== "object" && typeof v !== "function")) {
|
|
4728
|
-
if (typeof v === "string" || typeof v === "number") {
|
|
4984
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
4729
4985
|
if (requestValues.has(String(v)))
|
|
4730
4986
|
continue;
|
|
4731
4987
|
}
|
|
@@ -4747,7 +5003,20 @@ function directPrimitiveChildCountExcludingEchoed(obj, requestValues) {
|
|
|
4747
5003
|
* one array's target chain is never re-claimed as a fresh target thread of
|
|
4748
5004
|
* a different, independent array on the same primary response.
|
|
4749
5005
|
*/
|
|
4750
|
-
|
|
5006
|
+
let scanPrimaryCandidateGroupsCallCount = 0;
|
|
5007
|
+
/** Test-only instrumentation for asserting `scanPrimaryCandidateGroups`'s
|
|
5008
|
+
* O(actions.length) scan is reused across repeat queries of the same index
|
|
5009
|
+
* (via `detectDrillDownFoldPlan`'s per-index cache) rather than re-run.
|
|
5010
|
+
* Not read by any production path. */
|
|
5011
|
+
function getScanPrimaryCandidateGroupsCallCountForTest() {
|
|
5012
|
+
return scanPrimaryCandidateGroupsCallCount;
|
|
5013
|
+
}
|
|
5014
|
+
/** Test-only counterpart to {@link getScanPrimaryCandidateGroupsCallCountForTest}. */
|
|
5015
|
+
function resetScanPrimaryCandidateGroupsCallCountForTest() {
|
|
5016
|
+
scanPrimaryCandidateGroupsCallCount = 0;
|
|
5017
|
+
}
|
|
5018
|
+
function scanPrimaryCandidateGroups(actions, primaryIndex, globallyConsumedIndices, requestStringValueIndex) {
|
|
5019
|
+
scanPrimaryCandidateGroupsCallCount++;
|
|
4751
5020
|
const primary = actions[primaryIndex];
|
|
4752
5021
|
const primaryCandidates = findAllObjectArrayFields(primary.capture.responseBody);
|
|
4753
5022
|
if (primaryCandidates.length === 0)
|
|
@@ -4761,7 +5030,12 @@ function scanPrimaryCandidateGroups(actions, primaryIndex, globallyConsumedIndic
|
|
|
4761
5030
|
const consumedIndices = new Set();
|
|
4762
5031
|
for (const primaryArray of primaryCandidates) {
|
|
4763
5032
|
const targets = [];
|
|
4764
|
-
|
|
5033
|
+
// Pruned to the (typically tiny) set of later action indices whose
|
|
5034
|
+
// request could possibly thread one of this array's own item values —
|
|
5035
|
+
// see buildRequestStringValueIndex's docstring — instead of every
|
|
5036
|
+
// index from primaryIndex+1 to the end of actions.
|
|
5037
|
+
const candidateDrillIndices = collectCandidateIndicesAscending(requestStringValueIndex, collectItemsFieldValues(primaryArray.items), primaryIndex);
|
|
5038
|
+
for (const drillIndex of candidateDrillIndices) {
|
|
4765
5039
|
const drill = actions[drillIndex];
|
|
4766
5040
|
if (drill === primary)
|
|
4767
5041
|
continue;
|
|
@@ -4851,11 +5125,40 @@ function detectDrillDownFoldPlan(actions) {
|
|
|
4851
5125
|
// primary's response, not on this later primary's array, so it must
|
|
4852
5126
|
// never be re-claimed as a fresh drill target for a subsequent primary.
|
|
4853
5127
|
const globallyConsumedIndices = new Set();
|
|
5128
|
+
// scanPrimaryCandidateGroups's result for a given index only depends on
|
|
5129
|
+
// `actions` (fixed) and the current contents of `globallyConsumedIndices`,
|
|
5130
|
+
// so it is safe to reuse across repeat queries of the SAME index as long
|
|
5131
|
+
// as the consumed set hasn't grown since it was computed. The freshest-wins
|
|
5132
|
+
// loop below re-queries the same laterIndex once per sibling group on a
|
|
5133
|
+
// re-queried primary, and the outer loop often reaches that very index as
|
|
5134
|
+
// its own primaryIndex shortly after — both hit this cache instead of
|
|
5135
|
+
// repeating the O(actions.length) scan.
|
|
5136
|
+
let consumedVersion = 0;
|
|
5137
|
+
const scanCache = new Map();
|
|
5138
|
+
const scanCacheVersion = new Map();
|
|
5139
|
+
// Built once for the whole detectDrillDownFoldPlan call — see
|
|
5140
|
+
// buildRequestStringValueIndex's docstring.
|
|
5141
|
+
const requestStringValueIndex = buildRequestStringValueIndex(actions);
|
|
5142
|
+
const scanCached = (index) => {
|
|
5143
|
+
const cachedVersion = scanCacheVersion.get(index);
|
|
5144
|
+
if (cachedVersion === consumedVersion)
|
|
5145
|
+
return scanCache.get(index);
|
|
5146
|
+
const result = scanPrimaryCandidateGroups(actions, index, globallyConsumedIndices, requestStringValueIndex);
|
|
5147
|
+
scanCache.set(index, result);
|
|
5148
|
+
scanCacheVersion.set(index, consumedVersion);
|
|
5149
|
+
return result;
|
|
5150
|
+
};
|
|
5151
|
+
const addConsumed = (index) => {
|
|
5152
|
+
if (globallyConsumedIndices.has(index))
|
|
5153
|
+
return;
|
|
5154
|
+
globallyConsumedIndices.add(index);
|
|
5155
|
+
consumedVersion++;
|
|
5156
|
+
};
|
|
4854
5157
|
for (let primaryIndex = 0; primaryIndex < actions.length; primaryIndex++) {
|
|
4855
5158
|
if (globallyConsumedIndices.has(primaryIndex))
|
|
4856
5159
|
continue;
|
|
4857
5160
|
const primary = actions[primaryIndex];
|
|
4858
|
-
const groups =
|
|
5161
|
+
const groups = scanCached(primaryIndex);
|
|
4859
5162
|
if (groups.length === 0)
|
|
4860
5163
|
continue;
|
|
4861
5164
|
const primaryEndpointKey = endpointKey(primary.capture.url);
|
|
@@ -4882,7 +5185,7 @@ function detectDrillDownFoldPlan(actions) {
|
|
|
4882
5185
|
const laterAction = actions[laterIndex];
|
|
4883
5186
|
if (endpointKey(laterAction.capture.url) !== primaryEndpointKey)
|
|
4884
5187
|
continue;
|
|
4885
|
-
const laterGroups =
|
|
5188
|
+
const laterGroups = scanCached(laterIndex);
|
|
4886
5189
|
const laterGroup = laterGroups.find((g) => JSON.stringify(g.primaryArrayPath) === JSON.stringify(freshestGroup.primaryArrayPath));
|
|
4887
5190
|
if (laterGroup === undefined)
|
|
4888
5191
|
continue;
|
|
@@ -4920,10 +5223,10 @@ function detectDrillDownFoldPlan(actions) {
|
|
|
4920
5223
|
// itself is marked consumed once per group pushed (idempotent via
|
|
4921
5224
|
// Set.add), since the step itself is only visited once regardless of
|
|
4922
5225
|
// how many independent array groups it yields.
|
|
4923
|
-
|
|
5226
|
+
addConsumed(freshestIndex);
|
|
4924
5227
|
for (const target of freshestGroup.targets) {
|
|
4925
5228
|
for (const chainIndex of target.chain)
|
|
4926
|
-
|
|
5229
|
+
addConsumed(chainIndex);
|
|
4927
5230
|
}
|
|
4928
5231
|
}
|
|
4929
5232
|
}
|
|
@@ -5032,10 +5335,20 @@ function objectItemsAtPath(body, path) {
|
|
|
5032
5335
|
* through a request HEADER), so matching a spec's `joinFields` against the
|
|
5033
5336
|
* drill capture must search headers even though the structural heuristic
|
|
5034
5337
|
* deliberately doesn't (see {@link collectRequestStringValues}'s docstring). */
|
|
5338
|
+
const requestValuesCache = new WeakMap();
|
|
5339
|
+
/** Memoized like {@link objectArrayFieldsCache} — the same capture's
|
|
5340
|
+
* request/header values are re-derived on every fold-chain candidate that
|
|
5341
|
+
* threads through it. Keyed on `Capture` identity rather than
|
|
5342
|
+
* `responseBody`, since this walks the capture's request side (URL, body,
|
|
5343
|
+
* headers), not its response. */
|
|
5035
5344
|
function collectRequestValuesIncludingHeaders(capture) {
|
|
5345
|
+
const cached = requestValuesCache.get(capture);
|
|
5346
|
+
if (cached)
|
|
5347
|
+
return cached;
|
|
5036
5348
|
const values = collectRequestStringValues(capture);
|
|
5037
5349
|
for (const v of Object.values(capture.requestHeaders))
|
|
5038
5350
|
values.add(v);
|
|
5351
|
+
requestValuesCache.set(capture, values);
|
|
5039
5352
|
return values;
|
|
5040
5353
|
}
|
|
5041
5354
|
/** Finds which of `primaryItems` the drill call actually captured, by
|
|
@@ -5051,12 +5364,150 @@ function resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, drillCaptu
|
|
|
5051
5364
|
const matchedIndex = primaryItems.findIndex((item) => joinFields.every((field) => {
|
|
5052
5365
|
const value = readValueAtPath(item, field.split("."));
|
|
5053
5366
|
return ((typeof value === "string" && value.length > 0 && requestValues.has(value)) ||
|
|
5054
|
-
(typeof value === "number" && requestValues.has(String(value)))
|
|
5367
|
+
(typeof value === "number" && requestValues.has(String(value))) ||
|
|
5368
|
+
(typeof value === "boolean" && requestValues.has(String(value))));
|
|
5055
5369
|
}));
|
|
5056
5370
|
return matchedIndex === -1 ? null : matchedIndex;
|
|
5057
5371
|
}
|
|
5058
|
-
|
|
5059
|
-
|
|
5372
|
+
/**
|
|
5373
|
+
* Maps every string/numeric/boolean value seen in any action's request (URL,
|
|
5374
|
+
* body, or headers — see {@link collectRequestValuesIncludingHeaders}) to the
|
|
5375
|
+
* ascending list of action indices whose request carries it. Built once per
|
|
5376
|
+
* {@link buildFoldPlanFromSpec} call and shared across every primary/drill
|
|
5377
|
+
* candidate pair, so {@link resolveSpecMatchedPrimaryItemIndexAlongChain}'s
|
|
5378
|
+
* upstream search can jump straight to the (typically tiny) set of indices
|
|
5379
|
+
* that could possibly carry a given primary item's join value instead of
|
|
5380
|
+
* scanning every index between the primary and the drill — the O(actions)-
|
|
5381
|
+
* per-candidate backward walk the reported large-capture-set hang traced to.
|
|
5382
|
+
*/
|
|
5383
|
+
function buildRequestValueIndex(actions) {
|
|
5384
|
+
const index = new Map();
|
|
5385
|
+
for (let i = 0; i < actions.length; i++) {
|
|
5386
|
+
for (const value of collectRequestValuesIncludingHeaders(actions[i].capture)) {
|
|
5387
|
+
const indices = index.get(value);
|
|
5388
|
+
if (indices)
|
|
5389
|
+
indices.push(i);
|
|
5390
|
+
else
|
|
5391
|
+
index.set(value, [i]);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
return index;
|
|
5395
|
+
}
|
|
5396
|
+
/** Every string/numeric/boolean {@link FoldReturnSpec.joinFields} value
|
|
5397
|
+
* present on any of `primaryItems`, stringified exactly as
|
|
5398
|
+
* {@link resolveSpecMatchedPrimaryItemIndex} compares them — the set of
|
|
5399
|
+
* values whose {@link buildRequestValueIndex} entries can possibly resolve
|
|
5400
|
+
* this primary's join, used to prune the candidate entry indices
|
|
5401
|
+
* {@link resolveSpecMatchedPrimaryItemIndexAlongChain} walks instead of
|
|
5402
|
+
* considering every action index in range. */
|
|
5403
|
+
function collectPrimaryJoinValues(primaryItems, joinFields) {
|
|
5404
|
+
const values = new Set();
|
|
5405
|
+
for (const item of primaryItems) {
|
|
5406
|
+
for (const field of joinFields) {
|
|
5407
|
+
const value = readValueAtPath(item, field.split("."));
|
|
5408
|
+
if (typeof value === "string" && value.length > 0)
|
|
5409
|
+
values.add(value);
|
|
5410
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
5411
|
+
values.add(String(value));
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
return values;
|
|
5415
|
+
}
|
|
5416
|
+
/** Descending, deduped action indices strictly greater than
|
|
5417
|
+
* `primaryStepIndex` whose request carries at least one of `joinValues` —
|
|
5418
|
+
* the pruned candidate set {@link resolveSpecMatchedPrimaryItemIndexAlongChain}
|
|
5419
|
+
* walks instead of every index in `(primaryStepIndex, actions.length)`. */
|
|
5420
|
+
function collectCandidateEntryIndicesDescending(requestValueIndex, joinValues, primaryStepIndex) {
|
|
5421
|
+
const candidates = new Set();
|
|
5422
|
+
for (const value of joinValues) {
|
|
5423
|
+
for (const index of requestValueIndex.get(value) ?? []) {
|
|
5424
|
+
if (index > primaryStepIndex)
|
|
5425
|
+
candidates.add(index);
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
return [...candidates].sort((a, b) => b - a);
|
|
5429
|
+
}
|
|
5430
|
+
/** Like {@link resolveSpecMatchedPrimaryItemIndex}, but also looks upstream of
|
|
5431
|
+
* `drillStepIndex` for the join key when `drillStepIndex`'s own request
|
|
5432
|
+
* doesn't carry it — a `foldReturn` spec's `endpointPattern` naturally names
|
|
5433
|
+
* the chain TERMINAL (the response actually holding the data an author wants
|
|
5434
|
+
* folded), not the opaque entry hop that carries the join key onward (e.g.
|
|
5435
|
+
* as a header-threaded token). Walks every earlier step back to
|
|
5436
|
+
* `primaryStepIndex`, in reverse so the closest (least ambiguous) candidate
|
|
5437
|
+
* wins first, and accepts one only once {@link computeFoldChain} confirms it
|
|
5438
|
+
* actually chains FORWARD to `drillStepIndex` — otherwise an unrelated
|
|
5439
|
+
* earlier step matching the join key by coincidence could hijack the fold.
|
|
5440
|
+
* Returns the resolved `entryIndex` alongside the matched item index so the
|
|
5441
|
+
* caller can build the fold's `chain` starting from the step that ACTUALLY
|
|
5442
|
+
* carries the join key, not from `drillStepIndex` — a chain built from
|
|
5443
|
+
* `drillStepIndex` alone would never include this upstream entry hop, so it
|
|
5444
|
+
* would never be re-executed (header-parameterized) per primary item at
|
|
5445
|
+
* runtime. */
|
|
5446
|
+
let resolveSpecMatchedPrimaryItemIndexAlongChainCallCount = 0;
|
|
5447
|
+
/** Test-only instrumentation: how many times
|
|
5448
|
+
* {@link resolveSpecMatchedPrimaryItemIndexAlongChain} — the expensive
|
|
5449
|
+
* backward-walk + {@link computeFoldChain} resolution — has actually run
|
|
5450
|
+
* since the last {@link resetFoldPlanResolutionCallCountForTests} call, so a
|
|
5451
|
+
* test can assert `buildFoldPlanFromSpec` pays for it only once per primary
|
|
5452
|
+
* rather than once per matching drill occurrence. */
|
|
5453
|
+
function getFoldPlanResolutionCallCountForTests() {
|
|
5454
|
+
return resolveSpecMatchedPrimaryItemIndexAlongChainCallCount;
|
|
5455
|
+
}
|
|
5456
|
+
function resetFoldPlanResolutionCallCountForTests() {
|
|
5457
|
+
resolveSpecMatchedPrimaryItemIndexAlongChainCallCount = 0;
|
|
5458
|
+
}
|
|
5459
|
+
function resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, joinFields, drillStepIndex,
|
|
5460
|
+
// Keyed by entryIndex alone: valid for every call sharing the same
|
|
5461
|
+
// primaryStepIndex/primaryItems/joinFields, since resolveSpecMatchedPrimaryItemIndex's
|
|
5462
|
+
// result for a given entryIndex depends on nothing else. Without this,
|
|
5463
|
+
// buildFoldPlanFromSpec's per-primary candidate loop re-walks the SAME
|
|
5464
|
+
// overlapping entryIndex range from scratch for every matching drill
|
|
5465
|
+
// occurrence it tries — O(candidates * chain length) per primary instead
|
|
5466
|
+
// of O(chain length) total, the combinatorial blowup the reported hang
|
|
5467
|
+
// traced to at large capture-set sizes.
|
|
5468
|
+
matchedItemIndexCache,
|
|
5469
|
+
// Descending, deduped, computed once per primaryStepIndex by
|
|
5470
|
+
// {@link collectCandidateEntryIndicesDescending} — every index whose
|
|
5471
|
+
// request carries at least one of this primary's join values, i.e. a
|
|
5472
|
+
// strict superset of the indices `resolveSpecMatchedPrimaryItemIndex`
|
|
5473
|
+
// could ever match. Walking this instead of every index down to
|
|
5474
|
+
// `primaryStepIndex` is what collapses the backward search from
|
|
5475
|
+
// O(actions) to O(occurrences of the actual join value) per candidate.
|
|
5476
|
+
candidateEntryIndicesDescending) {
|
|
5477
|
+
resolveSpecMatchedPrimaryItemIndexAlongChainCallCount++;
|
|
5478
|
+
for (const entryIndex of candidateEntryIndicesDescending) {
|
|
5479
|
+
if (entryIndex > drillStepIndex)
|
|
5480
|
+
continue;
|
|
5481
|
+
const cached = matchedItemIndexCache.get(entryIndex);
|
|
5482
|
+
const matched = cached !== undefined
|
|
5483
|
+
? cached
|
|
5484
|
+
: (() => {
|
|
5485
|
+
const resolved = resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, actions[entryIndex].capture);
|
|
5486
|
+
matchedItemIndexCache.set(entryIndex, resolved);
|
|
5487
|
+
return resolved;
|
|
5488
|
+
})();
|
|
5489
|
+
if (matched === null)
|
|
5490
|
+
continue;
|
|
5491
|
+
if (entryIndex === drillStepIndex)
|
|
5492
|
+
return { entryIndex, primaryMatchedItemIndex: matched };
|
|
5493
|
+
const { chain } = computeFoldChain(actions, entryIndex, []);
|
|
5494
|
+
if (chain.includes(drillStepIndex))
|
|
5495
|
+
return { entryIndex, primaryMatchedItemIndex: matched };
|
|
5496
|
+
}
|
|
5497
|
+
return null;
|
|
5498
|
+
}
|
|
5499
|
+
/**
|
|
5500
|
+
* Compiles a flow-declared {@link FoldReturnSpec.endpointPattern} into a
|
|
5501
|
+
* capture predicate, or a predicate that always returns `false` when `spec`
|
|
5502
|
+
* is `null` or its pattern isn't a valid regex — the same null-safe
|
|
5503
|
+
* try/catch shape {@link buildFoldPlanFromSpec} already applied inline,
|
|
5504
|
+
* shared here so the action-sequence extractors can admit a spec-matched
|
|
5505
|
+
* drill-down capture under the identical rule that later resolves its fold
|
|
5506
|
+
* plan, instead of dropping it before the fold pipeline ever sees it.
|
|
5507
|
+
*/
|
|
5508
|
+
function compileFoldReturnEndpointMatcher(spec) {
|
|
5509
|
+
if (spec === null)
|
|
5510
|
+
return () => false;
|
|
5060
5511
|
const endpointRx = (() => {
|
|
5061
5512
|
try {
|
|
5062
5513
|
return new RegExp(spec.endpointPattern);
|
|
@@ -5066,16 +5517,73 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5066
5517
|
}
|
|
5067
5518
|
})();
|
|
5068
5519
|
if (endpointRx === null)
|
|
5069
|
-
return
|
|
5520
|
+
return () => false;
|
|
5521
|
+
return (capture) => endpointRx.test(capture.url);
|
|
5522
|
+
}
|
|
5523
|
+
/**
|
|
5524
|
+
* A capture predicate matching whichever capture actually holds
|
|
5525
|
+
* {@link FoldReturnSpec.resultsPath}'s object array — the flow's own PRIMARY
|
|
5526
|
+
* results source, as opposed to {@link compileFoldReturnEndpointMatcher}'s
|
|
5527
|
+
* `endpointPattern` match (the drill-down). REST's `extractActionSequence`
|
|
5528
|
+
* never needs this: a REST primary is a POST/non-GET and is admitted
|
|
5529
|
+
* unconditionally regardless of `foldReturnSpec`. GraphQL's primary is
|
|
5530
|
+
* always a `query`, and `extractGraphQLActionSequence` drops every
|
|
5531
|
+
* non-mutation capture by default, so without this predicate a declared
|
|
5532
|
+
* `foldReturnSpec` would admit only the drill-down capture and never the
|
|
5533
|
+
* read op whose response the drill-down folds onto — leaving
|
|
5534
|
+
* `buildFoldPlanFromSpec` with no `primaryStepIndex` to resolve against.
|
|
5535
|
+
*/
|
|
5536
|
+
function compileFoldReturnResultsMatcher(spec) {
|
|
5537
|
+
if (spec === null)
|
|
5538
|
+
return () => false;
|
|
5539
|
+
const resultsPath = spec.resultsPath.split(".");
|
|
5540
|
+
return (capture) => objectItemsAtPath(capture.responseBody, resultsPath) !== null;
|
|
5541
|
+
}
|
|
5542
|
+
function buildFoldPlanFromSpec(actions, spec) {
|
|
5543
|
+
const primaryArrayPath = spec.resultsPath.split(".");
|
|
5544
|
+
const matchesFoldReturnEndpoint = compileFoldReturnEndpointMatcher(spec);
|
|
5545
|
+
// Built once and shared across every primaryStepIndex — see
|
|
5546
|
+
// buildRequestValueIndex's docstring. Lets each primary immediately tell
|
|
5547
|
+
// whether ANY action anywhere carries one of its own join values before
|
|
5548
|
+
// paying for anything else, instead of scanning its own drill candidates
|
|
5549
|
+
// one by one only to discover none of them can ever match.
|
|
5550
|
+
const requestValueIndex = buildRequestValueIndex(actions);
|
|
5070
5551
|
let freshestPlan = null;
|
|
5071
5552
|
for (let primaryStepIndex = 0; primaryStepIndex < actions.length; primaryStepIndex++) {
|
|
5072
5553
|
const primaryItems = objectItemsAtPath(actions[primaryStepIndex].capture.responseBody, primaryArrayPath);
|
|
5073
5554
|
if (!primaryItems)
|
|
5074
5555
|
continue;
|
|
5556
|
+
const joinValues = collectPrimaryJoinValues(primaryItems, spec.joinFields);
|
|
5557
|
+
const candidateEntryIndicesDescending = collectCandidateEntryIndicesDescending(requestValueIndex, joinValues, primaryStepIndex);
|
|
5558
|
+
// No action anywhere carries any of this primary's join values, so no
|
|
5559
|
+
// drillStepIndex candidate could ever resolve — skip straight to the
|
|
5560
|
+
// next primary instead of scanning this one's drill candidates (each of
|
|
5561
|
+
// which would only rediscover the same dead end).
|
|
5562
|
+
if (candidateEntryIndicesDescending.length === 0)
|
|
5563
|
+
continue;
|
|
5564
|
+
// Cheap regex-only pass first: collects every endpointPattern-matching
|
|
5565
|
+
// drillStepIndex without paying for the expensive backward-walk +
|
|
5566
|
+
// computeFoldChain resolution below. Scanned from the freshest (highest)
|
|
5567
|
+
// index down so the expensive resolution — tried only on the entries
|
|
5568
|
+
// this loop actually visits — runs on the candidate that would win
|
|
5569
|
+
// ties in the original last-write-wins scan first, falling through to
|
|
5570
|
+
// the next-freshest only when a candidate fails to resolve, instead of
|
|
5571
|
+
// resolving every earlier occurrence just to have it overwritten.
|
|
5572
|
+
const matchingDrillStepIndices = [];
|
|
5075
5573
|
for (let drillStepIndex = primaryStepIndex + 1; drillStepIndex < actions.length; drillStepIndex++) {
|
|
5574
|
+
if (matchesFoldReturnEndpoint(actions[drillStepIndex].capture)) {
|
|
5575
|
+
matchingDrillStepIndices.push(drillStepIndex);
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
// Shared across every matching-drill candidate tried below for THIS
|
|
5579
|
+
// primaryStepIndex — see resolveSpecMatchedPrimaryItemIndexAlongChain's
|
|
5580
|
+
// docstring on why a fresh cache per primary (not per drill candidate)
|
|
5581
|
+
// is what collapses the backward-walk from O(candidates * chain length)
|
|
5582
|
+
// to O(chain length).
|
|
5583
|
+
const matchedItemIndexCache = new Map();
|
|
5584
|
+
for (let i = matchingDrillStepIndices.length - 1; i >= 0; i--) {
|
|
5585
|
+
const drillStepIndex = matchingDrillStepIndices[i];
|
|
5076
5586
|
const drill = actions[drillStepIndex];
|
|
5077
|
-
if (!endpointRx.test(drill.capture.url))
|
|
5078
|
-
continue;
|
|
5079
5587
|
// Widened to a flat (non-array) object response the same way the
|
|
5080
5588
|
// structural heuristic is (see findAllObjectArrayFieldsOrWholeObject):
|
|
5081
5589
|
// an explicit foldReturn declaration must be able to express a
|
|
@@ -5090,17 +5598,31 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5090
5598
|
// now does (see detectDrillDownFoldPlan). Validated after the chain
|
|
5091
5599
|
// resolves, below, since `[]` is a valid empty baseline for
|
|
5092
5600
|
// computeFoldChain but not a valid final drillArrayPath on its own.
|
|
5601
|
+
const matchResult = resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, spec.joinFields, drillStepIndex, matchedItemIndexCache, candidateEntryIndicesDescending);
|
|
5602
|
+
if (matchResult === null)
|
|
5603
|
+
continue;
|
|
5604
|
+
const { entryIndex, primaryMatchedItemIndex } = matchResult;
|
|
5605
|
+
// Rooted at `entryIndex` — the step whose request ACTUALLY carries the
|
|
5606
|
+
// join key — not `drillStepIndex`, so an upstream entry hop the join
|
|
5607
|
+
// key was only resolvable through (e.g. a header-threaded token) is
|
|
5608
|
+
// itself part of `chain` and gets re-executed (join-parameterized) per
|
|
5609
|
+
// primary item at runtime, same as every structurally-detected chain.
|
|
5610
|
+
// `drillResultsPath`, when given, still targets `drillStepIndex`'s own
|
|
5611
|
+
// response specifically (the endpoint the spec names); it is otherwise
|
|
5612
|
+
// left for computeFoldChain's own forward richness walk to resolve,
|
|
5613
|
+
// exactly as it does for every step beyond the chain's entry.
|
|
5093
5614
|
const drillArrayPath = (() => {
|
|
5615
|
+
if (entryIndex !== drillStepIndex) {
|
|
5616
|
+
return (findObjectArrayFieldOrWholeObject(actions[entryIndex].capture.responseBody)?.path ??
|
|
5617
|
+
null);
|
|
5618
|
+
}
|
|
5094
5619
|
if (spec.drillResultsPath === undefined) {
|
|
5095
5620
|
return findObjectArrayFieldOrWholeObject(drill.capture.responseBody)?.path ?? null;
|
|
5096
5621
|
}
|
|
5097
5622
|
const path = spec.drillResultsPath.split(".");
|
|
5098
5623
|
return objectItemsAtPath(drill.capture.responseBody, path) ? path : null;
|
|
5099
5624
|
})();
|
|
5100
|
-
const
|
|
5101
|
-
if (primaryMatchedItemIndex === null)
|
|
5102
|
-
continue;
|
|
5103
|
-
const { chain, chainArrayPath, chainTerminalIndex } = computeFoldChain(actions, drillStepIndex, drillArrayPath ?? []);
|
|
5625
|
+
const { chain, chainArrayPath, chainTerminalIndex } = computeFoldChain(actions, entryIndex, drillArrayPath ?? []);
|
|
5104
5626
|
// The chain's resolved terminal must actually hold foldable data —
|
|
5105
5627
|
// an intermediate drill step with neither its own candidate NOR a
|
|
5106
5628
|
// later chained step that resolves one has nothing to merge, so it
|
|
@@ -5114,7 +5636,7 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5114
5636
|
targets: [
|
|
5115
5637
|
{
|
|
5116
5638
|
joinFields: spec.joinFields,
|
|
5117
|
-
drillStepIndex,
|
|
5639
|
+
drillStepIndex: entryIndex,
|
|
5118
5640
|
drillArrayPath: drillArrayPath ?? [],
|
|
5119
5641
|
primaryMatchedItemIndex,
|
|
5120
5642
|
chain,
|
|
@@ -5123,6 +5645,7 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5123
5645
|
},
|
|
5124
5646
|
],
|
|
5125
5647
|
};
|
|
5648
|
+
break;
|
|
5126
5649
|
}
|
|
5127
5650
|
}
|
|
5128
5651
|
return freshestPlan;
|
|
@@ -5207,6 +5730,62 @@ function mergeSpecPlanOntoSamePrimary(structuralPlans, actions, foldReturnSpec)
|
|
|
5207
5730
|
* plan that survives multipart disqualification, letting every downstream
|
|
5208
5731
|
* emitter/shape-inference caller fold each of them.
|
|
5209
5732
|
*/
|
|
5733
|
+
/**
|
|
5734
|
+
* Every value ACTUALLY threaded from one dependent-drill-down chain hop's
|
|
5735
|
+
* response into a LATER chain hop's own request/headers — the same overlap
|
|
5736
|
+
* `computeFoldChain`'s `dependsOnChain` check already computes to decide a
|
|
5737
|
+
* step belongs in the chain at all — across every fold target
|
|
5738
|
+
* `detectDrillDownFoldPlan` / `buildFoldPlanFromSpec` resolve. These are the
|
|
5739
|
+
* values `indexStateValues` must index as producible state regardless of
|
|
5740
|
+
* `MIN_STATE_VALUE_LENGTH`, so `compileActionSteps` can thread a
|
|
5741
|
+
* chain-produced join value into the next hop's request even when that
|
|
5742
|
+
* value is a short one (e.g. a bare numeric status token).
|
|
5743
|
+
*
|
|
5744
|
+
* Deliberately NOT "every leaf value on a chain hop's response" — a chain
|
|
5745
|
+
* terminal's response commonly ECHOES the primary item's own join field
|
|
5746
|
+
* back (e.g. `sku` on the folded record), and that echoed value is a
|
|
5747
|
+
* PRIMARY-ITEM field the fold loop's own per-item render already threads
|
|
5748
|
+
* literally (see {@link findThreadedJoinFields}'s docstring); sweeping it
|
|
5749
|
+
* into state-threading's single-earliest-origin index would collapse every
|
|
5750
|
+
* item's distinct value onto whichever item's capture was indexed first.
|
|
5751
|
+
* Restricting to values that themselves reappear in a STRICTLY LATER chain
|
|
5752
|
+
* hop's own request — real cross-call threading, not an echo sitting still
|
|
5753
|
+
* in one response — keeps this exact to the shape state-threading is
|
|
5754
|
+
* actually needed for.
|
|
5755
|
+
*
|
|
5756
|
+
* Runs directly off raw actions (not `resolveFoldPlan`, which needs
|
|
5757
|
+
* `isMultipart` — unavailable before `compileActionSteps` has run) since
|
|
5758
|
+
* fold-plan DETECTION depends only on each action's `capture`.
|
|
5759
|
+
*/
|
|
5760
|
+
function collectDependentDrillDownChainValues(actions, foldReturnSpec) {
|
|
5761
|
+
const structuralPlans = detectDrillDownFoldPlan(actions);
|
|
5762
|
+
const specPlan = foldReturnSpec === null ? null : buildFoldPlanFromSpec(actions, foldReturnSpec);
|
|
5763
|
+
const plans = structuralPlans.length > 0 ? structuralPlans : specPlan === null ? [] : [specPlan];
|
|
5764
|
+
const values = new Set();
|
|
5765
|
+
for (const plan of plans) {
|
|
5766
|
+
for (const target of plan.targets) {
|
|
5767
|
+
for (let j = 0; j < target.chain.length; j++) {
|
|
5768
|
+
const priorIndex = target.chain[j];
|
|
5769
|
+
const priorCapture = actions[priorIndex]?.capture;
|
|
5770
|
+
if (!priorCapture)
|
|
5771
|
+
continue;
|
|
5772
|
+
const responseValues = collectResponseLeafValues(priorCapture);
|
|
5773
|
+
const echoedValues = collectRequestValuesIncludingHeaders(priorCapture);
|
|
5774
|
+
for (let k = j + 1; k < target.chain.length; k++) {
|
|
5775
|
+
const laterCapture = actions[target.chain[k]]?.capture;
|
|
5776
|
+
if (!laterCapture)
|
|
5777
|
+
continue;
|
|
5778
|
+
const laterRequestValues = collectRequestValuesIncludingHeaders(laterCapture);
|
|
5779
|
+
for (const v of responseValues) {
|
|
5780
|
+
if (!echoedValues.has(v) && laterRequestValues.has(v))
|
|
5781
|
+
values.add(v);
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
return values;
|
|
5788
|
+
}
|
|
5210
5789
|
function resolveFoldPlan(actions, foldReturnSpec = null) {
|
|
5211
5790
|
const structuralPlans = detectDrillDownFoldPlan(actions);
|
|
5212
5791
|
const plans = structuralPlans.length > 0
|
|
@@ -6573,7 +7152,9 @@ async function main() {
|
|
|
6573
7152
|
// Hoisted so both the primary-operation gate below and rawActionCaptures
|
|
6574
7153
|
// (further down) read the same computed sequence instead of calling the
|
|
6575
7154
|
// extractor twice.
|
|
6576
|
-
const graphqlActionSequence = gql
|
|
7155
|
+
const graphqlActionSequence = gql
|
|
7156
|
+
? extractGraphQLActionSequence(captures, submitPatterns, foldReturnSpec)
|
|
7157
|
+
: [];
|
|
6577
7158
|
const primaryGraphQLOperation = gql && graphqlActionSequence.length === 0
|
|
6578
7159
|
? selectPrimaryGraphQLOperation(captures, flowSteps, vocabulary, process.env, ownBackendHostnames, fallbackDomain)
|
|
6579
7160
|
: null;
|
|
@@ -6624,7 +7205,7 @@ async function main() {
|
|
|
6624
7205
|
// heuristic extraction finds.
|
|
6625
7206
|
const patternedHeuristicActionCaptures = gql
|
|
6626
7207
|
? graphqlActionSequence
|
|
6627
|
-
: collapseRedundantPatches(extractActionSequence(captures, submitPatterns));
|
|
7208
|
+
: collapseRedundantPatches(extractActionSequence(captures, submitPatterns, foldReturnSpec));
|
|
6628
7209
|
// The same undercount hazard applies one layer below the manifest: a
|
|
6629
7210
|
// flow-declared submitEndpointPattern that matches only one section's URL
|
|
6630
7211
|
// (the natural way to describe "the button that finishes the wizard")
|
|
@@ -6637,8 +7218,8 @@ async function main() {
|
|
|
6637
7218
|
const unfilteredHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
|
|
6638
7219
|
? patternedHeuristicActionCaptures
|
|
6639
7220
|
: gql
|
|
6640
|
-
? extractGraphQLActionSequence(captures, null)
|
|
6641
|
-
: collapseRedundantPatches(extractActionSequence(captures, null));
|
|
7221
|
+
? extractGraphQLActionSequence(captures, null, foldReturnSpec)
|
|
7222
|
+
: collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec));
|
|
6642
7223
|
const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
|
|
6643
7224
|
if (patternUndercounts) {
|
|
6644
7225
|
logger.info(`submission selection: ignoring submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) as an undercount of the unfiltered heuristic action sequence (${unfilteredHeuristicActionCaptures.length} capture(s))`);
|
|
@@ -6669,8 +7250,8 @@ async function main() {
|
|
|
6669
7250
|
const rawUnfilteredActionCaptures = submitEndpointPattern === null
|
|
6670
7251
|
? null
|
|
6671
7252
|
: gql
|
|
6672
|
-
? extractGraphQLActionSequence(captures, null)
|
|
6673
|
-
: collapseRedundantPatches(extractActionSequence(captures, null));
|
|
7253
|
+
? extractGraphQLActionSequence(captures, null, foldReturnSpec)
|
|
7254
|
+
: collapseRedundantPatches(extractActionSequence(captures, null, foldReturnSpec));
|
|
6674
7255
|
// Form-schema detection runs BEFORE state-indexing so the field-id/option-id
|
|
6675
7256
|
// UUIDs can be shielded from indexing — those UUIDs are stable schema
|
|
6676
7257
|
// anchors that T2/T3 substitution depends on remaining literal in body
|
|
@@ -6718,8 +7299,16 @@ async function main() {
|
|
|
6718
7299
|
];
|
|
6719
7300
|
})();
|
|
6720
7301
|
const actionCaptureIndices = new Set(actionCaptures.map((a) => a.index));
|
|
7302
|
+
// Resolved off raw actionCaptures — fold-plan DETECTION depends only on
|
|
7303
|
+
// each action's capture, so this runs before compileActionSteps/
|
|
7304
|
+
// indexStateValues even exist — so a short numeric join value threaded
|
|
7305
|
+
// through a dependent-drill-down chain hop still gets indexed as
|
|
7306
|
+
// producible state (see collectDependentDrillDownChainValues).
|
|
7307
|
+
const dependentDrillDownChainValues = actionCaptures.length > 1
|
|
7308
|
+
? collectDependentDrillDownChainValues(actionCaptures, foldReturnSpec)
|
|
7309
|
+
: new Set();
|
|
6721
7310
|
const stateIndex = actionCaptures.length > 1
|
|
6722
|
-
? indexStateValues(captures, shieldedUuids, actionCaptureIndices)
|
|
7311
|
+
? indexStateValues(captures, shieldedUuids, actionCaptureIndices, dependentDrillDownChainValues)
|
|
6723
7312
|
: new Map();
|
|
6724
7313
|
const actionSteps = actionCaptures.length > 1 ? compileActionSteps(actionCaptures, stateIndex) : [];
|
|
6725
7314
|
const isSubmissionFlow = actionSteps.length > 1;
|