@enricai/barnacle 1.12.21 → 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 +61 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +150 -0
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +40 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +324 -17
- 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;
|
|
@@ -4591,17 +4599,42 @@ const ARRAY_WILDCARD_SEGMENT = "*";
|
|
|
4591
4599
|
* A path segment for an array index the search descended through (to keep
|
|
4592
4600
|
* looking for a nested candidate array) is the {@link ARRAY_WILDCARD_SEGMENT}
|
|
4593
4601
|
* sentinel, never a literal index — see its docstring. */
|
|
4594
|
-
function
|
|
4602
|
+
function findAllObjectArrayFieldsUncached(value, path = []) {
|
|
4595
4603
|
if (value === null || typeof value !== "object")
|
|
4596
4604
|
return [];
|
|
4597
4605
|
if (Array.isArray(value)) {
|
|
4598
4606
|
const objectItems = value.filter(isObjectArrayItem);
|
|
4599
|
-
const nestedCandidates = objectItems.flatMap((item) =>
|
|
4607
|
+
const nestedCandidates = objectItems.flatMap((item) => findAllObjectArrayFieldsUncached(item, [...path, ARRAY_WILDCARD_SEGMENT]));
|
|
4600
4608
|
return objectItems.length > 0
|
|
4601
4609
|
? [{ path, items: objectItems }, ...nestedCandidates]
|
|
4602
4610
|
: nestedCandidates;
|
|
4603
4611
|
}
|
|
4604
|
-
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;
|
|
4605
4638
|
}
|
|
4606
4639
|
/** The first object-array field by DFS/key order — see
|
|
4607
4640
|
* {@link findAllObjectArrayFields}. Every call site that must disambiguate
|
|
@@ -4625,9 +4658,24 @@ function findObjectArrayField(value, path = []) {
|
|
|
4625
4658
|
* more genuine per-item data than a small real nested object-array; a
|
|
4626
4659
|
* caller that just wants the first real array (the common case) is
|
|
4627
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. */
|
|
4628
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;
|
|
4629
4675
|
const found = findAllObjectArrayFields(value, path);
|
|
4630
|
-
|
|
4676
|
+
const computed = isObjectArrayItem(value) ? [...found, { path, items: [value] }] : found;
|
|
4677
|
+
objectArrayFieldsOrWholeObjectCache.set(value, computed);
|
|
4678
|
+
return computed;
|
|
4631
4679
|
}
|
|
4632
4680
|
/** The first candidate from {@link findAllObjectArrayFieldsOrWholeObject} —
|
|
4633
4681
|
* the flat-object-aware counterpart of {@link findObjectArrayField}. */
|
|
@@ -4706,6 +4754,62 @@ function* walkItemFieldPaths(item, path = []) {
|
|
|
4706
4754
|
* order the primary response declares them, not sorted. Returns `[]` when no
|
|
4707
4755
|
* field of the item threads into the request at all.
|
|
4708
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
|
+
}
|
|
4709
4813
|
function findThreadedJoinFields(item, drillCapture) {
|
|
4710
4814
|
const requestValues = collectRequestStringValues(drillCapture);
|
|
4711
4815
|
if (requestValues.size === 0)
|
|
@@ -4724,8 +4828,22 @@ function findThreadedJoinFields(item, drillCapture) {
|
|
|
4724
4828
|
* join field. Also walks every response HEADER value, mirroring
|
|
4725
4829
|
* {@link collectRequestValuesIncludingHeaders} on the request side, since a
|
|
4726
4830
|
* chain hop can just as easily mint its join token in a response header
|
|
4727
|
-
* (e.g. a `Location` or custom correlation header) as in the body.
|
|
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();
|
|
4728
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) {
|
|
4729
4847
|
const values = new Set();
|
|
4730
4848
|
for (const { value } of walkAllPrimitiveLeaves(capture.responseBody)) {
|
|
4731
4849
|
if (typeof value === "string" && value.length > 0)
|
|
@@ -4885,7 +5003,20 @@ function directPrimitiveChildCountExcludingEchoed(obj, requestValues) {
|
|
|
4885
5003
|
* one array's target chain is never re-claimed as a fresh target thread of
|
|
4886
5004
|
* a different, independent array on the same primary response.
|
|
4887
5005
|
*/
|
|
4888
|
-
|
|
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++;
|
|
4889
5020
|
const primary = actions[primaryIndex];
|
|
4890
5021
|
const primaryCandidates = findAllObjectArrayFields(primary.capture.responseBody);
|
|
4891
5022
|
if (primaryCandidates.length === 0)
|
|
@@ -4899,7 +5030,12 @@ function scanPrimaryCandidateGroups(actions, primaryIndex, globallyConsumedIndic
|
|
|
4899
5030
|
const consumedIndices = new Set();
|
|
4900
5031
|
for (const primaryArray of primaryCandidates) {
|
|
4901
5032
|
const targets = [];
|
|
4902
|
-
|
|
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) {
|
|
4903
5039
|
const drill = actions[drillIndex];
|
|
4904
5040
|
if (drill === primary)
|
|
4905
5041
|
continue;
|
|
@@ -4989,11 +5125,40 @@ function detectDrillDownFoldPlan(actions) {
|
|
|
4989
5125
|
// primary's response, not on this later primary's array, so it must
|
|
4990
5126
|
// never be re-claimed as a fresh drill target for a subsequent primary.
|
|
4991
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
|
+
};
|
|
4992
5157
|
for (let primaryIndex = 0; primaryIndex < actions.length; primaryIndex++) {
|
|
4993
5158
|
if (globallyConsumedIndices.has(primaryIndex))
|
|
4994
5159
|
continue;
|
|
4995
5160
|
const primary = actions[primaryIndex];
|
|
4996
|
-
const groups =
|
|
5161
|
+
const groups = scanCached(primaryIndex);
|
|
4997
5162
|
if (groups.length === 0)
|
|
4998
5163
|
continue;
|
|
4999
5164
|
const primaryEndpointKey = endpointKey(primary.capture.url);
|
|
@@ -5020,7 +5185,7 @@ function detectDrillDownFoldPlan(actions) {
|
|
|
5020
5185
|
const laterAction = actions[laterIndex];
|
|
5021
5186
|
if (endpointKey(laterAction.capture.url) !== primaryEndpointKey)
|
|
5022
5187
|
continue;
|
|
5023
|
-
const laterGroups =
|
|
5188
|
+
const laterGroups = scanCached(laterIndex);
|
|
5024
5189
|
const laterGroup = laterGroups.find((g) => JSON.stringify(g.primaryArrayPath) === JSON.stringify(freshestGroup.primaryArrayPath));
|
|
5025
5190
|
if (laterGroup === undefined)
|
|
5026
5191
|
continue;
|
|
@@ -5058,10 +5223,10 @@ function detectDrillDownFoldPlan(actions) {
|
|
|
5058
5223
|
// itself is marked consumed once per group pushed (idempotent via
|
|
5059
5224
|
// Set.add), since the step itself is only visited once regardless of
|
|
5060
5225
|
// how many independent array groups it yields.
|
|
5061
|
-
|
|
5226
|
+
addConsumed(freshestIndex);
|
|
5062
5227
|
for (const target of freshestGroup.targets) {
|
|
5063
5228
|
for (const chainIndex of target.chain)
|
|
5064
|
-
|
|
5229
|
+
addConsumed(chainIndex);
|
|
5065
5230
|
}
|
|
5066
5231
|
}
|
|
5067
5232
|
}
|
|
@@ -5170,10 +5335,20 @@ function objectItemsAtPath(body, path) {
|
|
|
5170
5335
|
* through a request HEADER), so matching a spec's `joinFields` against the
|
|
5171
5336
|
* drill capture must search headers even though the structural heuristic
|
|
5172
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. */
|
|
5173
5344
|
function collectRequestValuesIncludingHeaders(capture) {
|
|
5345
|
+
const cached = requestValuesCache.get(capture);
|
|
5346
|
+
if (cached)
|
|
5347
|
+
return cached;
|
|
5174
5348
|
const values = collectRequestStringValues(capture);
|
|
5175
5349
|
for (const v of Object.values(capture.requestHeaders))
|
|
5176
5350
|
values.add(v);
|
|
5351
|
+
requestValuesCache.set(capture, values);
|
|
5177
5352
|
return values;
|
|
5178
5353
|
}
|
|
5179
5354
|
/** Finds which of `primaryItems` the drill call actually captured, by
|
|
@@ -5194,6 +5369,64 @@ function resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, drillCaptu
|
|
|
5194
5369
|
}));
|
|
5195
5370
|
return matchedIndex === -1 ? null : matchedIndex;
|
|
5196
5371
|
}
|
|
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
|
+
}
|
|
5197
5430
|
/** Like {@link resolveSpecMatchedPrimaryItemIndex}, but also looks upstream of
|
|
5198
5431
|
* `drillStepIndex` for the join key when `drillStepIndex`'s own request
|
|
5199
5432
|
* doesn't carry it — a `foldReturn` spec's `endpointPattern` naturally names
|
|
@@ -5210,9 +5443,49 @@ function resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, drillCaptu
|
|
|
5210
5443
|
* `drillStepIndex` alone would never include this upstream entry hop, so it
|
|
5211
5444
|
* would never be re-executed (header-parameterized) per primary item at
|
|
5212
5445
|
* runtime. */
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
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
|
+
})();
|
|
5216
5489
|
if (matched === null)
|
|
5217
5490
|
continue;
|
|
5218
5491
|
if (entryIndex === drillStepIndex)
|
|
@@ -5269,15 +5542,48 @@ function compileFoldReturnResultsMatcher(spec) {
|
|
|
5269
5542
|
function buildFoldPlanFromSpec(actions, spec) {
|
|
5270
5543
|
const primaryArrayPath = spec.resultsPath.split(".");
|
|
5271
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);
|
|
5272
5551
|
let freshestPlan = null;
|
|
5273
5552
|
for (let primaryStepIndex = 0; primaryStepIndex < actions.length; primaryStepIndex++) {
|
|
5274
5553
|
const primaryItems = objectItemsAtPath(actions[primaryStepIndex].capture.responseBody, primaryArrayPath);
|
|
5275
5554
|
if (!primaryItems)
|
|
5276
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 = [];
|
|
5277
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];
|
|
5278
5586
|
const drill = actions[drillStepIndex];
|
|
5279
|
-
if (!matchesFoldReturnEndpoint(drill.capture))
|
|
5280
|
-
continue;
|
|
5281
5587
|
// Widened to a flat (non-array) object response the same way the
|
|
5282
5588
|
// structural heuristic is (see findAllObjectArrayFieldsOrWholeObject):
|
|
5283
5589
|
// an explicit foldReturn declaration must be able to express a
|
|
@@ -5292,7 +5598,7 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5292
5598
|
// now does (see detectDrillDownFoldPlan). Validated after the chain
|
|
5293
5599
|
// resolves, below, since `[]` is a valid empty baseline for
|
|
5294
5600
|
// computeFoldChain but not a valid final drillArrayPath on its own.
|
|
5295
|
-
const matchResult = resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, spec.joinFields,
|
|
5601
|
+
const matchResult = resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, spec.joinFields, drillStepIndex, matchedItemIndexCache, candidateEntryIndicesDescending);
|
|
5296
5602
|
if (matchResult === null)
|
|
5297
5603
|
continue;
|
|
5298
5604
|
const { entryIndex, primaryMatchedItemIndex } = matchResult;
|
|
@@ -5339,6 +5645,7 @@ function buildFoldPlanFromSpec(actions, spec) {
|
|
|
5339
5645
|
},
|
|
5340
5646
|
],
|
|
5341
5647
|
};
|
|
5648
|
+
break;
|
|
5342
5649
|
}
|
|
5343
5650
|
}
|
|
5344
5651
|
return freshestPlan;
|