@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.
@@ -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 findAllObjectArrayFields(value, path = []) {
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) => findAllObjectArrayFields(item, [...path, ARRAY_WILDCARD_SEGMENT]));
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]) => findAllObjectArrayFields(v, [...path, key]));
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
- return isObjectArrayItem(value) ? [...found, { path, items: [value] }] : found;
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
- function scanPrimaryCandidateGroups(actions, primaryIndex, globallyConsumedIndices) {
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
- for (let drillIndex = primaryIndex + 1; drillIndex < actions.length; drillIndex++) {
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 = scanPrimaryCandidateGroups(actions, primaryIndex, globallyConsumedIndices);
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 = scanPrimaryCandidateGroups(actions, laterIndex, globallyConsumedIndices);
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
- globallyConsumedIndices.add(freshestIndex);
5226
+ addConsumed(freshestIndex);
5062
5227
  for (const target of freshestGroup.targets) {
5063
5228
  for (const chainIndex of target.chain)
5064
- globallyConsumedIndices.add(chainIndex);
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
- function resolveSpecMatchedPrimaryItemIndexAlongChain(actions, primaryItems, joinFields, primaryStepIndex, drillStepIndex) {
5214
- for (let entryIndex = drillStepIndex; entryIndex > primaryStepIndex; entryIndex--) {
5215
- const matched = resolveSpecMatchedPrimaryItemIndex(primaryItems, joinFields, actions[entryIndex].capture);
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, primaryStepIndex, drillStepIndex);
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;