@ryanstark24/sfgraph-core 1.2.0 → 1.4.0
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/analyze/__tests__/audit-graph.test.js +79 -0
- package/dist/analyze/__tests__/audit-graph.test.js.map +1 -1
- package/dist/analyze/__tests__/edge-filters.test.d.ts +2 -0
- package/dist/analyze/__tests__/edge-filters.test.d.ts.map +1 -0
- package/dist/analyze/__tests__/edge-filters.test.js +66 -0
- package/dist/analyze/__tests__/edge-filters.test.js.map +1 -0
- package/dist/analyze/__tests__/findings.test.js +19 -9
- package/dist/analyze/__tests__/findings.test.js.map +1 -1
- package/dist/analyze/__tests__/governor-risks-detection.test.js +90 -0
- package/dist/analyze/__tests__/governor-risks-detection.test.js.map +1 -1
- package/dist/analyze/__tests__/security-fls-filter.test.d.ts +2 -0
- package/dist/analyze/__tests__/security-fls-filter.test.d.ts.map +1 -0
- package/dist/analyze/__tests__/security-fls-filter.test.js +23 -0
- package/dist/analyze/__tests__/security-fls-filter.test.js.map +1 -0
- package/dist/analyze/audit-graph.d.ts +26 -2
- package/dist/analyze/audit-graph.d.ts.map +1 -1
- package/dist/analyze/audit-graph.js +52 -5
- package/dist/analyze/audit-graph.js.map +1 -1
- package/dist/analyze/dependencies.d.ts +2 -2
- package/dist/analyze/dependencies.d.ts.map +1 -1
- package/dist/analyze/dependencies.js +9 -3
- package/dist/analyze/dependencies.js.map +1 -1
- package/dist/analyze/dependents.d.ts +6 -1
- package/dist/analyze/dependents.d.ts.map +1 -1
- package/dist/analyze/dependents.js +12 -2
- package/dist/analyze/dependents.js.map +1 -1
- package/dist/analyze/edge-filters.d.ts +37 -0
- package/dist/analyze/edge-filters.d.ts.map +1 -0
- package/dist/analyze/edge-filters.js +51 -0
- package/dist/analyze/edge-filters.js.map +1 -0
- package/dist/analyze/find-nodes.d.ts.map +1 -1
- package/dist/analyze/find-nodes.js.map +1 -1
- package/dist/analyze/findings.d.ts +1 -1
- package/dist/analyze/findings.d.ts.map +1 -1
- package/dist/analyze/findings.js +1 -3
- package/dist/analyze/findings.js.map +1 -1
- package/dist/analyze/populate.d.ts.map +1 -1
- package/dist/analyze/populate.js +55 -10
- package/dist/analyze/populate.js.map +1 -1
- package/dist/analyze/security.d.ts +15 -0
- package/dist/analyze/security.d.ts.map +1 -1
- package/dist/analyze/security.js +59 -3
- package/dist/analyze/security.js.map +1 -1
- package/dist/analyze/wip-impact.d.ts.map +1 -1
- package/dist/analyze/wip-impact.js +12 -2
- package/dist/analyze/wip-impact.js.map +1 -1
- package/dist/domain/metadata-category.d.ts +1 -0
- package/dist/domain/metadata-category.d.ts.map +1 -1
- package/dist/domain/metadata-category.js +1 -0
- package/dist/domain/metadata-category.js.map +1 -1
- package/dist/embedding/embed.d.ts.map +1 -1
- package/dist/embedding/embed.js +14 -1
- package/dist/embedding/embed.js.map +1 -1
- package/dist/extractors/interfaces/metadata-source.d.ts +11 -0
- package/dist/extractors/interfaces/metadata-source.d.ts.map +1 -1
- package/dist/extractors/live-org/__tests__/bulk-retrieve.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/custom-settings.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/custom-settings.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/custom-settings.test.js +79 -0
- package/dist/extractors/live-org/__tests__/custom-settings.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/extractor-enrichment.test.js +70 -8
- package/dist/extractors/live-org/__tests__/extractor-enrichment.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.js +9 -6
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js +1 -1
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js +1 -1
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/pools.test.js +4 -1
- package/dist/extractors/live-org/__tests__/pools.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/rate-limit.test.js +1 -6
- package/dist/extractors/live-org/__tests__/rate-limit.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/soql-rebatch.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/stop-waiting.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/stop-waiting.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/stop-waiting.test.js +243 -0
- package/dist/extractors/live-org/__tests__/stop-waiting.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.js +40 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.js +270 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.js.map +1 -0
- package/dist/extractors/live-org/bulk-retrieve.d.ts +169 -0
- package/dist/extractors/live-org/bulk-retrieve.d.ts.map +1 -1
- package/dist/extractors/live-org/bulk-retrieve.js +499 -70
- package/dist/extractors/live-org/bulk-retrieve.js.map +1 -1
- package/dist/extractors/live-org/capabilities.d.ts.map +1 -1
- package/dist/extractors/live-org/capabilities.js.map +1 -1
- package/dist/extractors/live-org/extractors/__tests__/apex.test.d.ts +2 -0
- package/dist/extractors/live-org/extractors/__tests__/apex.test.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/__tests__/apex.test.js +73 -0
- package/dist/extractors/live-org/extractors/__tests__/apex.test.js.map +1 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.d.ts +2 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.js +183 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.js.map +1 -0
- package/dist/extractors/live-org/extractors/apex.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/apex.js +76 -51
- package/dist/extractors/live-org/extractors/apex.js.map +1 -1
- package/dist/extractors/live-org/extractors/custom-settings.d.ts +3 -0
- package/dist/extractors/live-org/extractors/custom-settings.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/custom-settings.js +66 -0
- package/dist/extractors/live-org/extractors/custom-settings.js.map +1 -0
- package/dist/extractors/live-org/extractors/flow.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/flow.js.map +1 -1
- package/dist/extractors/live-org/extractors/generic-metadata.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/generic-metadata.js.map +1 -1
- package/dist/extractors/live-org/extractors/generic-sobject.js.map +1 -1
- package/dist/extractors/live-org/extractors/lwc.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/lwc.js +81 -41
- package/dist/extractors/live-org/extractors/lwc.js.map +1 -1
- package/dist/extractors/live-org/extractors/mcd-baseline.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/mcd-baseline.js +2 -4
- package/dist/extractors/live-org/extractors/mcd-baseline.js.map +1 -1
- package/dist/extractors/live-org/extractors/object.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/object.js.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.js.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio.d.ts +8 -1
- package/dist/extractors/live-org/extractors/omnistudio.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio.js +59 -6
- package/dist/extractors/live-org/extractors/omnistudio.js.map +1 -1
- package/dist/extractors/live-org/rate-limit.d.ts +41 -15
- package/dist/extractors/live-org/rate-limit.d.ts.map +1 -1
- package/dist/extractors/live-org/rate-limit.js +185 -33
- package/dist/extractors/live-org/rate-limit.js.map +1 -1
- package/dist/extractors/live-org/vlocity/runner.d.ts.map +1 -1
- package/dist/extractors/live-org/vlocity/runner.js +30 -6
- package/dist/extractors/live-org/vlocity/runner.js.map +1 -1
- package/dist/ingest/__tests__/config-values.test.d.ts +2 -0
- package/dist/ingest/__tests__/config-values.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/config-values.test.js +65 -0
- package/dist/ingest/__tests__/config-values.test.js.map +1 -0
- package/dist/ingest/__tests__/full-sync-deletion.test.js +11 -4
- package/dist/ingest/__tests__/full-sync-deletion.test.js.map +1 -1
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.d.ts +10 -0
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.js +251 -0
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.js.map +1 -0
- package/dist/ingest/__tests__/live-ingest.test.js.map +1 -1
- package/dist/ingest/__tests__/parser-dispatch.test.d.ts +2 -0
- package/dist/ingest/__tests__/parser-dispatch.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/parser-dispatch.test.js +55 -0
- package/dist/ingest/__tests__/parser-dispatch.test.js.map +1 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.d.ts +2 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.js +134 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.js.map +1 -0
- package/dist/ingest/detect-deletions-guard.d.ts +45 -0
- package/dist/ingest/detect-deletions-guard.d.ts.map +1 -0
- package/dist/ingest/detect-deletions-guard.js +63 -0
- package/dist/ingest/detect-deletions-guard.js.map +1 -0
- package/dist/ingest/live-ingest.d.ts +16 -0
- package/dist/ingest/live-ingest.d.ts.map +1 -1
- package/dist/ingest/live-ingest.js +776 -549
- package/dist/ingest/live-ingest.js.map +1 -1
- package/dist/parsers/__tests__/datatransform-structured.test.d.ts +2 -0
- package/dist/parsers/__tests__/datatransform-structured.test.d.ts.map +1 -0
- package/dist/parsers/__tests__/datatransform-structured.test.js +147 -0
- package/dist/parsers/__tests__/datatransform-structured.test.js.map +1 -0
- package/dist/parsers/__tests__/edge-provenance.test.js +4 -1
- package/dist/parsers/__tests__/edge-provenance.test.js.map +1 -1
- package/dist/parsers/__tests__/lwc-bindings.test.js +6 -3
- package/dist/parsers/__tests__/lwc-bindings.test.js.map +1 -1
- package/dist/parsers/__tests__/object-inline-fields.test.js +2 -2
- package/dist/parsers/__tests__/object-inline-fields.test.js.map +1 -1
- package/dist/parsers/__tests__/omnistudio.golden.test.js +5 -6
- package/dist/parsers/__tests__/omnistudio.golden.test.js.map +1 -1
- package/dist/parsers/__tests__/vlocity.golden.test.js +9 -13
- package/dist/parsers/__tests__/vlocity.golden.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/ast-extractor.test.js +64 -4
- package/dist/parsers/apex/__tests__/ast-extractor.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/empty-body.test.d.ts +2 -0
- package/dist/parsers/apex/__tests__/empty-body.test.d.ts.map +1 -0
- package/dist/parsers/apex/__tests__/empty-body.test.js +40 -0
- package/dist/parsers/apex/__tests__/empty-body.test.js.map +1 -0
- package/dist/parsers/apex/__tests__/is-test-attribute.test.js +4 -2
- package/dist/parsers/apex/__tests__/is-test-attribute.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/regex-arg-count.test.js +1 -1
- package/dist/parsers/apex/__tests__/regex-arg-count.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/regex-inloop.test.d.ts +2 -0
- package/dist/parsers/apex/__tests__/regex-inloop.test.d.ts.map +1 -0
- package/dist/parsers/apex/__tests__/regex-inloop.test.js +70 -0
- package/dist/parsers/apex/__tests__/regex-inloop.test.js.map +1 -0
- package/dist/parsers/apex/arity-resolver.d.ts.map +1 -1
- package/dist/parsers/apex/arity-resolver.js.map +1 -1
- package/dist/parsers/apex/ast-extractor.d.ts.map +1 -1
- package/dist/parsers/apex/ast-extractor.js +33 -1
- package/dist/parsers/apex/ast-extractor.js.map +1 -1
- package/dist/parsers/apex/class.d.ts.map +1 -1
- package/dist/parsers/apex/class.js +62 -3
- package/dist/parsers/apex/class.js.map +1 -1
- package/dist/parsers/apex/trigger.d.ts.map +1 -1
- package/dist/parsers/apex/trigger.js +10 -0
- package/dist/parsers/apex/trigger.js.map +1 -1
- package/dist/parsers/flow/__tests__/invocable-resolver.test.js.map +1 -1
- package/dist/parsers/flow/invocable-resolver.d.ts.map +1 -1
- package/dist/parsers/flow/invocable-resolver.js.map +1 -1
- package/dist/parsers/generic/reflection-walker.d.ts.map +1 -1
- package/dist/parsers/generic/reflection-walker.js +1 -3
- package/dist/parsers/generic/reflection-walker.js.map +1 -1
- package/dist/parsers/lwc/html-visitor.d.ts.map +1 -1
- package/dist/parsers/lwc/html-visitor.js +3 -2
- package/dist/parsers/lwc/html-visitor.js.map +1 -1
- package/dist/parsers/lwc/js-visitor.d.ts.map +1 -1
- package/dist/parsers/lwc/js-visitor.js +6 -2
- package/dist/parsers/lwc/js-visitor.js.map +1 -1
- package/dist/parsers/object/object.d.ts.map +1 -1
- package/dist/parsers/object/object.js +1 -5
- package/dist/parsers/object/object.js.map +1 -1
- package/dist/parsers/omnistudio/data-transform.d.ts.map +1 -1
- package/dist/parsers/omnistudio/data-transform.js +39 -6
- package/dist/parsers/omnistudio/data-transform.js.map +1 -1
- package/dist/parsers/omnistudio/integration-procedure.d.ts.map +1 -1
- package/dist/parsers/omnistudio/integration-procedure.js +3 -2
- package/dist/parsers/omnistudio/integration-procedure.js.map +1 -1
- package/dist/parsers/omnistudio/overlap-detector.d.ts.map +1 -1
- package/dist/parsers/omnistudio/overlap-detector.js +2 -9
- package/dist/parsers/omnistudio/overlap-detector.js.map +1 -1
- package/dist/parsers/omnistudio/process.d.ts.map +1 -1
- package/dist/parsers/omnistudio/process.js +3 -2
- package/dist/parsers/omnistudio/process.js.map +1 -1
- package/dist/parsers/omnistudio/ui-card.d.ts.map +1 -1
- package/dist/parsers/omnistudio/ui-card.js +3 -2
- package/dist/parsers/omnistudio/ui-card.js.map +1 -1
- package/dist/parsers/rules/custom-metadata-type.yml +7 -0
- package/dist/parsers/rules/custom-setting.yml +24 -0
- package/dist/parsers/vlocity/common.d.ts +16 -0
- package/dist/parsers/vlocity/common.d.ts.map +1 -1
- package/dist/parsers/vlocity/common.js +62 -0
- package/dist/parsers/vlocity/common.js.map +1 -1
- package/dist/parsers/vlocity/data-raptor.d.ts.map +1 -1
- package/dist/parsers/vlocity/data-raptor.js +43 -14
- package/dist/parsers/vlocity/data-raptor.js.map +1 -1
- package/dist/render/__tests__/sarif.test.js +1 -4
- package/dist/render/__tests__/sarif.test.js.map +1 -1
- package/dist/render/sarif.d.ts.map +1 -1
- package/dist/render/sarif.js +1 -3
- package/dist/render/sarif.js.map +1 -1
- package/dist/storage/__tests__/analysis-tables-migration.test.js +1 -1
- package/dist/storage/__tests__/graph-store.test.js +13 -0
- package/dist/storage/__tests__/graph-store.test.js.map +1 -1
- package/dist/storage/__tests__/migrations.test.js +4 -4
- package/dist/storage/__tests__/rename-stability.test.js.map +1 -1
- package/dist/storage/__tests__/sync-generation.test.d.ts +2 -0
- package/dist/storage/__tests__/sync-generation.test.d.ts.map +1 -0
- package/dist/storage/__tests__/sync-generation.test.js +159 -0
- package/dist/storage/__tests__/sync-generation.test.js.map +1 -0
- package/dist/storage/interfaces.d.ts +57 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/sqlite/graph-store.d.ts +15 -1
- package/dist/storage/sqlite/graph-store.d.ts.map +1 -1
- package/dist/storage/sqlite/graph-store.js +78 -0
- package/dist/storage/sqlite/graph-store.js.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +19 -0
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/rename-stability.d.ts +1 -1
- package/dist/storage/sqlite/rename-stability.d.ts.map +1 -1
- package/dist/storage/sqlite/rename-stability.js +2 -6
- package/dist/storage/sqlite/rename-stability.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { discoverMetadataTypes } from "./discovery.js";
|
|
2
2
|
import { buildDispatchTable } from "./dispatch.js";
|
|
3
3
|
import { iterApex } from "./extractors/apex.js";
|
|
4
|
+
import { iterCustomSettings } from "./extractors/custom-settings.js";
|
|
4
5
|
import { iterFlow } from "./extractors/flow.js";
|
|
5
6
|
import { iterGenericMetadata } from "./extractors/generic-metadata.js";
|
|
6
7
|
import { iterIntegration } from "./extractors/integration.js";
|
|
@@ -9,6 +10,28 @@ import { iterObject } from "./extractors/object.js";
|
|
|
9
10
|
import { iterOmnistudio } from "./extractors/omnistudio.js";
|
|
10
11
|
import { iterSecurity } from "./extractors/security.js";
|
|
11
12
|
import { iterVlocity } from "./extractors/vlocity.js";
|
|
13
|
+
import { StopWaitingError, stopSignalContext } from "./rate-limit.js";
|
|
14
|
+
function createWedgeCoordinator(warnings = [], overrides) {
|
|
15
|
+
const capEnv = Number.parseInt(process.env.SFGRAPH_MAX_BACKGROUND_WEDGES ?? "", 10);
|
|
16
|
+
// Default 32 (was 4): a full --rebuild on a large org wedges many slow/empty
|
|
17
|
+
// generic-metadata sources at the 90s first-yield watchdog AND the heavy Apex
|
|
18
|
+
// source; a cap of 4 evicted most of them (their data lost). The drain loop
|
|
19
|
+
// skips already-resolved entries cheaply, so a high cap costs little.
|
|
20
|
+
const cap = Number.isFinite(capEnv) && capEnv > 0 ? capEnv : 32;
|
|
21
|
+
const drainEnv = Number.parseInt(process.env.SFGRAPH_LATE_DRAIN_BUDGET_MS ?? "", 10);
|
|
22
|
+
// Default 300s (was 60s): the per-entry budget is TOTAL drain wall-clock.
|
|
23
|
+
// Apex needs ~75s to stream all classes once the Tooling pool frees up after
|
|
24
|
+
// the main loop; 60s clipped it mid-drain. Empty/fast entries still return
|
|
25
|
+
// `done` immediately and cost nothing, so the larger budget only helps the
|
|
26
|
+
// genuinely-heavy entries finish.
|
|
27
|
+
const drain = Number.isFinite(drainEnv) && drainEnv >= 0 ? drainEnv : 300_000;
|
|
28
|
+
return {
|
|
29
|
+
warnings,
|
|
30
|
+
backgroundWedges: [],
|
|
31
|
+
maxBackgroundWedges: overrides?.maxBackgroundWedges ?? cap,
|
|
32
|
+
lateDrainBudgetMs: overrides?.lateDrainBudgetMs ?? drain,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
12
35
|
/** Best-effort classification so the end-of-run recommendation is targeted. */
|
|
13
36
|
function classifySkip(msg) {
|
|
14
37
|
const m = msg.toUpperCase();
|
|
@@ -34,27 +57,27 @@ export async function* mergeAsyncIterables(...iters) {
|
|
|
34
57
|
yield v;
|
|
35
58
|
}
|
|
36
59
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
export function mergeAsyncIterablesParallel(...args) {
|
|
61
|
+
// Disambiguate the two overloads. The 2-arg array+opts form is used by
|
|
62
|
+
// tests; the rest-args form is the production fan-out path.
|
|
63
|
+
let iters;
|
|
64
|
+
let opts;
|
|
65
|
+
if (args.length === 2 &&
|
|
66
|
+
Array.isArray(args[0]) &&
|
|
67
|
+
args[1] != null &&
|
|
68
|
+
typeof args[1] === "object" &&
|
|
69
|
+
!(args[1] instanceof Promise) &&
|
|
70
|
+
!(Symbol.asyncIterator in args[1])) {
|
|
71
|
+
iters = args[0];
|
|
72
|
+
opts = args[1];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
iters = args;
|
|
76
|
+
opts = undefined;
|
|
77
|
+
}
|
|
78
|
+
return mergeAsyncIterablesParallelImpl(iters, opts);
|
|
79
|
+
}
|
|
80
|
+
async function* mergeAsyncIterablesParallelImpl(iters, opts) {
|
|
58
81
|
const envConcurrency = Number.parseInt(process.env.SFGRAPH_SOURCE_CONCURRENCY ?? "", 10);
|
|
59
82
|
// Raised 8 -> 12: a wider window lets Tooling-backed sources (apex, lwc)
|
|
60
83
|
// start immediately rather than wait behind the cohort of metadata-pool
|
|
@@ -62,7 +85,11 @@ export async function* mergeAsyncIterablesParallel(...iters) {
|
|
|
62
85
|
// which spend most of their time queued in Bottleneck. Per-pool concurrency
|
|
63
86
|
// still caps total HTTP fan-out, so this only opens up parallelism that
|
|
64
87
|
// was being throttled at the wrong layer.
|
|
65
|
-
const concurrency =
|
|
88
|
+
const concurrency = opts?.concurrency != null && opts.concurrency > 0
|
|
89
|
+
? opts.concurrency
|
|
90
|
+
: Number.isFinite(envConcurrency) && envConcurrency > 0
|
|
91
|
+
? envConcurrency
|
|
92
|
+
: 12;
|
|
66
93
|
const iterators = iters.map((it) => it[Symbol.asyncIterator]());
|
|
67
94
|
const pending = new Map();
|
|
68
95
|
let nextIterIdx = 0;
|
|
@@ -97,60 +124,275 @@ export async function* mergeAsyncIterablesParallel(...iters) {
|
|
|
97
124
|
advance(idx);
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Per-source watchdog budgets. Centralized here so the failSoft wrapper and
|
|
129
|
+
* tests share the same constants.
|
|
130
|
+
*
|
|
131
|
+
* Phase 1.5 W1.5-01 contract: these are the budgets the watchdog uses, but
|
|
132
|
+
* the clock against which they measure starts at slot-acquisition time
|
|
133
|
+
* (i.e., on the FIRST `it.next()` call inside `failSoft`'s body), NOT at
|
|
134
|
+
* source registration. Queued-but-not-yet-executing sources accumulate
|
|
135
|
+
* ZERO clock time, so a wedge in one source can no longer cascade-kill
|
|
136
|
+
* its queued neighbors.
|
|
137
|
+
*
|
|
138
|
+
* Anti-feature reminder: do NOT raise these to "fix" a real wedge. The
|
|
139
|
+
* correct fix is per-source override (W1.5-future) or fixing the underlying
|
|
140
|
+
* pre-yield setup call. See `.planning/phase-1.5/PLAN.md` anti-features.
|
|
141
|
+
*/
|
|
142
|
+
const WATCHDOG_INACTIVITY_MS_DEFAULT = 5 * 60_000;
|
|
143
|
+
const WATCHDOG_FIRST_YIELD_MS_DEFAULT = 90_000;
|
|
144
|
+
/**
|
|
145
|
+
* Per-source watchdog overrides (the "per-source override" the anti-feature
|
|
146
|
+
* note above points to). Some sources legitimately need far longer than the
|
|
147
|
+
* 90s default to produce their FIRST record — most notably the Vlocity runner,
|
|
148
|
+
* which executes a long sequence of `vlocity_cmt` SObject queries and assembles
|
|
149
|
+
* DataPacks before yielding member #1, and can have multi-minute gaps between
|
|
150
|
+
* DataPack types once streaming. Raising the GLOBAL budget to accommodate it is
|
|
151
|
+
* wrong: it makes every other slow/empty source hold its merger slot 10x longer
|
|
152
|
+
* and starves the pool (observed: a 600s global first-yield stalled the whole
|
|
153
|
+
* fan-out behind a few empty generic sources). So scope the larger budget to
|
|
154
|
+
* the specific label only. Explicit env vars still win (debug/tests).
|
|
155
|
+
*/
|
|
156
|
+
const SOURCE_WATCHDOG_OVERRIDES = {
|
|
157
|
+
// Vlocity must stay INLINE: the post-merger background-drain cannot reliably
|
|
158
|
+
// recover it (it needs minutes to first-yield, longer than any sane drain
|
|
159
|
+
// budget, and competes for drain slots). Giving it a long inline budget lets
|
|
160
|
+
// it hold its DATA-pool slot and stream once the pool frees up. NB: do NOT
|
|
161
|
+
// do this for Tooling-pool sources like Apex — holding a scarce Tooling slot
|
|
162
|
+
// that long starves every other Tooling source and stalls the whole fan-out.
|
|
163
|
+
// Apex instead wedges fast (releasing its slot) and is recovered by the
|
|
164
|
+
// background-drain pass, which is sized generously in createWedgeCoordinator.
|
|
165
|
+
vlocity: { firstYieldMs: 20 * 60_000, inactivityMs: 10 * 60_000 },
|
|
166
|
+
};
|
|
167
|
+
/** Read watchdog budgets per-call so unit tests can override via env vars
|
|
168
|
+
* without restarting the process. Production env should leave these unset:
|
|
169
|
+
* ordinary sources get the documented 90s first-yield / 5min inactivity;
|
|
170
|
+
* a source in SOURCE_WATCHDOG_OVERRIDES gets its scoped budget instead. */
|
|
171
|
+
function readWatchdogBudgets(label) {
|
|
172
|
+
const fy = Number.parseInt(process.env.SFGRAPH_WATCHDOG_FIRST_YIELD_MS ?? "", 10);
|
|
173
|
+
const inact = Number.parseInt(process.env.SFGRAPH_WATCHDOG_INACTIVITY_MS ?? "", 10);
|
|
174
|
+
const override = label ? SOURCE_WATCHDOG_OVERRIDES[label] : undefined;
|
|
175
|
+
return {
|
|
176
|
+
firstYieldMs: Number.isFinite(fy) && fy > 0
|
|
177
|
+
? fy
|
|
178
|
+
: (override?.firstYieldMs ?? WATCHDOG_FIRST_YIELD_MS_DEFAULT),
|
|
179
|
+
inactivityMs: Number.isFinite(inact) && inact > 0
|
|
180
|
+
? inact
|
|
181
|
+
: (override?.inactivityMs ?? WATCHDOG_INACTIVITY_MS_DEFAULT),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** Build the namespaced wedge warning string per the schema documented in
|
|
185
|
+
* PLAN.md (`wedge:<source>:<stage>:<detail>`). Centralized here so the
|
|
186
|
+
* format is stable across emit sites + grep-friendly. */
|
|
187
|
+
function fmtWedgeWarning(source, stage, detail) {
|
|
188
|
+
const parts = [`wedge:${source}:${stage}`];
|
|
189
|
+
for (const [k, v] of Object.entries(detail)) {
|
|
190
|
+
parts.push(`${k}=${v}`);
|
|
191
|
+
}
|
|
192
|
+
return parts.join(":");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Enforce the background-wedge cap. When admitting a new entry would push
|
|
196
|
+
* the count above `maxBackgroundWedges`, mark the OLDEST entries' stop-
|
|
197
|
+
* waiting flags so their drain loops bail at the next iteration. Emits a
|
|
198
|
+
* `wedge:cap:backgroundWedgeAborted:...` warning per evicted entry. The
|
|
199
|
+
* iterator object itself is left in the set; the late-drain loop is what
|
|
200
|
+
* sees the flag and skips it. (We do NOT remove from `backgroundWedges`
|
|
201
|
+
* here so post-merger inspection still sees every wedge that ever fired
|
|
202
|
+
* during the run.)
|
|
203
|
+
*/
|
|
204
|
+
function enforceBackgroundWedgeCap(coordinator) {
|
|
205
|
+
const active = coordinator.backgroundWedges.filter((e) => !e.stopWaitingRef.value);
|
|
206
|
+
const overage = active.length - coordinator.maxBackgroundWedges;
|
|
207
|
+
if (overage <= 0)
|
|
208
|
+
return;
|
|
209
|
+
// Sort by registeredAt ascending; oldest first.
|
|
210
|
+
active.sort((a, b) => a.registeredAt - b.registeredAt);
|
|
211
|
+
for (let i = 0; i < overage; i += 1) {
|
|
212
|
+
const victim = active[i];
|
|
213
|
+
if (!victim)
|
|
214
|
+
continue;
|
|
215
|
+
victim.stopWaitingRef.value = true;
|
|
216
|
+
const ageMs = Date.now() - victim.registeredAt;
|
|
217
|
+
coordinator.warnings.push(fmtWedgeWarning("cap", "backgroundWedgeAborted", {
|
|
218
|
+
source: victim.label,
|
|
219
|
+
ageMs,
|
|
220
|
+
reason: "backgroundWedgeCapExceeded",
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Drain a single background-wedge iterator. Called once per wedged source,
|
|
226
|
+
* post-merger, with a wall-clock budget. Each iteration races the iterator's
|
|
227
|
+
* `it.next()` against the remaining budget. Records arriving inside the
|
|
228
|
+
* budget are yielded with `attributes.lateYield = true`. The pending HTTP
|
|
229
|
+
* request (if the wedge is on a network call) is NOT actively cancelled
|
|
230
|
+
* (W1.5-03's job); we just stop awaiting it.
|
|
231
|
+
*
|
|
232
|
+
* Bails immediately if `stopWaitingRef.value === true` (cap eviction).
|
|
233
|
+
*/
|
|
234
|
+
async function* drainBackgroundWedge(entry, budgetMs, warnings) {
|
|
235
|
+
const debug = process.env.SFGRAPH_DEBUG_INGEST === "1";
|
|
236
|
+
const startedAt = Date.now();
|
|
237
|
+
let lateCount = 0;
|
|
238
|
+
while (!entry.stopWaitingRef.value) {
|
|
239
|
+
const remaining = startedAt + budgetMs - Date.now();
|
|
240
|
+
if (remaining <= 0)
|
|
241
|
+
break;
|
|
242
|
+
// First iteration: consume the abandoned-but-still-pending `it.next()`
|
|
243
|
+
// promise from when the watchdog fired. Subsequent iterations call
|
|
244
|
+
// `iterator.next()` normally. Async-generator semantics queue
|
|
245
|
+
// concurrent `.next()` calls, so we MUST consume the pending one
|
|
246
|
+
// before issuing a new one — otherwise the late-yielded value goes
|
|
247
|
+
// to the abandoned promise and the drain sees the NEXT yield (or
|
|
248
|
+
// `done:true` if the generator is single-yield).
|
|
249
|
+
const nextPromise = entry.pendingFirstNext !== null
|
|
250
|
+
? entry.pendingFirstNext
|
|
251
|
+
: stopSignalContext.run(entry.stopWaitingRef, () => entry.iterator.next());
|
|
252
|
+
if (entry.pendingFirstNext !== null)
|
|
253
|
+
entry.pendingFirstNext = null;
|
|
254
|
+
let next;
|
|
255
|
+
try {
|
|
256
|
+
next = await Promise.race([
|
|
257
|
+
nextPromise,
|
|
258
|
+
new Promise((resolve) => {
|
|
259
|
+
setTimeout(
|
|
260
|
+
// Resolve (not reject) with a synthetic done sentinel — bailing
|
|
261
|
+
// is the normal exit path from a budget-exceeded drain, not an
|
|
262
|
+
// error condition.
|
|
263
|
+
() => resolve({ value: undefined, done: true }), remaining);
|
|
264
|
+
}),
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
// Late-arriving error from the wedged iterator. Don't crash the drain;
|
|
269
|
+
// surface as a warning and stop.
|
|
270
|
+
const err = e?.message ?? String(e);
|
|
271
|
+
warnings.push(fmtWedgeWarning(entry.label, "lateError", { error: err.slice(0, 120) }));
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
if (next.done)
|
|
275
|
+
break;
|
|
276
|
+
lateCount += 1;
|
|
277
|
+
const rec = next.value;
|
|
278
|
+
// Tag with lateYield so downstream consumers / tests can identify the
|
|
279
|
+
// record as having arrived after slot release.
|
|
280
|
+
const tagged = {
|
|
281
|
+
...rec,
|
|
282
|
+
attributes: { ...(rec.attributes ?? {}), lateYield: true },
|
|
283
|
+
};
|
|
284
|
+
yield tagged;
|
|
285
|
+
}
|
|
286
|
+
if (lateCount > 0) {
|
|
287
|
+
const totalMs = Date.now() - entry.registeredAt;
|
|
288
|
+
warnings.push(fmtWedgeWarning(entry.label, "resolvedLate", {
|
|
289
|
+
totalMs,
|
|
290
|
+
records: lateCount,
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
if (debug) {
|
|
294
|
+
console.log(`ingest: [debug] ${entry.label} background-wedge drain finished (${lateCount} late records, stopWaiting=${entry.stopWaitingRef.value})`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Wrap an iterable so a thrown error is captured + the stream ends cleanly
|
|
299
|
+
* instead of aborting the whole ingest. The error is recorded into a
|
|
300
|
+
* shared skip report (consumed at end-of-run) and a compact ✗ line is
|
|
301
|
+
* printed so the user sees something happened without the full error
|
|
302
|
+
* message scrolling past during the run.
|
|
303
|
+
*
|
|
304
|
+
* Phase 1.5 W1.5-01 + W1.5-02 contract:
|
|
305
|
+
*
|
|
306
|
+
* - The watchdog clock starts when this generator's BODY first executes
|
|
307
|
+
* (i.e., when the merger calls `.next()` on us for the first time —
|
|
308
|
+
* that's slot-acquisition time). Queued sources still inside
|
|
309
|
+
* `mergeAsyncIterablesParallel`'s waiting list accumulate ZERO clock
|
|
310
|
+
* time. Implemented via an internal `startedAtRef` that's written once
|
|
311
|
+
* on body entry; if the caller passes a `startedAtRef` we update it in
|
|
312
|
+
* place so the merger can observe the slot-acquisition moment.
|
|
313
|
+
*
|
|
314
|
+
* - On watchdog fire, we DO NOT throw (which would abort the merger's
|
|
315
|
+
* Promise.race for our slot AND lose the still-pending iterator).
|
|
316
|
+
* Instead we:
|
|
317
|
+
* 1. Emit a `wedge:<label>:firstYield:90s:...` (or `:inactivity:300s:`)
|
|
318
|
+
* warning into coordinator.warnings.
|
|
319
|
+
* 2. Register the still-pending iterator into coordinator.background
|
|
320
|
+
* Wedges with a stopWaitingRef. Enforce the cap (evict oldest).
|
|
321
|
+
* 3. RETURN from the generator. This cleanly releases the merger's
|
|
322
|
+
* slot — `pending.delete(idx)` runs in mergeAsyncIterablesParallel
|
|
323
|
+
* and the next queued source acquires the slot.
|
|
324
|
+
* Post-merger, the bulkRetrieve top-level fan-out drains the background
|
|
325
|
+
* wedge set; records arriving inside the late-drain budget are yielded
|
|
326
|
+
* with `attributes.lateYield = true`.
|
|
327
|
+
*/
|
|
328
|
+
async function* failSoft(label, factory, onError, coordinator,
|
|
329
|
+
/** Optional outer-visible clock cell. The merger does not currently read
|
|
330
|
+
* this, but the contract is "set by failSoft on body entry"; tests use
|
|
331
|
+
* it to assert slot-acquisition timing. */
|
|
332
|
+
startedAtRef) {
|
|
106
333
|
const debug = process.env.SFGRAPH_DEBUG_INGEST === "1";
|
|
334
|
+
// W1.5-01: the clock starts HERE — body-entry, which is slot-acquisition
|
|
335
|
+
// time. NOT at failSoft() invocation (that's source registration, which
|
|
336
|
+
// can be hundreds of seconds before the merger picks us out of the queue).
|
|
107
337
|
const startedAt = Date.now();
|
|
338
|
+
if (startedAtRef)
|
|
339
|
+
startedAtRef.value = startedAt;
|
|
108
340
|
let count = 0;
|
|
109
341
|
let started = false;
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
342
|
+
// Track last-yielded qualifiedName for diagnostic warnings — the smoking-
|
|
343
|
+
// gun field that names WHICH record was in flight when the wedge fired.
|
|
344
|
+
let lastYieldedQName = "<none>";
|
|
345
|
+
const { firstYieldMs, inactivityMs } = readWatchdogBudgets(label);
|
|
346
|
+
// W1.5-03: pre-allocate the per-source stop-signal at body entry and
|
|
347
|
+
// publish it into AsyncLocalStorage so any downstream rate-limit wrapper
|
|
348
|
+
// (`withTimeout`, `soqlWithTimeout`, `scheduleQuery`) inside the wedged
|
|
349
|
+
// iterator can observe `value = true` after cap-eviction flips it. Same
|
|
350
|
+
// ref is later attached to the BackgroundWedgeEntry so the keystone's
|
|
351
|
+
// `enforceBackgroundWedgeCap` flip propagates to in-flight calls without
|
|
352
|
+
// any per-extractor signature change. AsyncLocalStorage propagates across
|
|
353
|
+
// awaits within an async function started inside .run() — we factor the
|
|
354
|
+
// iterator into a helper started inside the .run() callback.
|
|
355
|
+
const stopWaitingRef = { value: false };
|
|
356
|
+
const it = stopSignalContext.run(stopWaitingRef, () => factory()[Symbol.asyncIterator]());
|
|
357
|
+
if (debug)
|
|
358
|
+
console.log(`ingest: [debug] ${label} ← invoked at ${startedAt}`);
|
|
126
359
|
try {
|
|
127
|
-
if (debug)
|
|
128
|
-
console.log(`ingest: [debug] ${label} ← invoked at ${startedAt}`);
|
|
129
|
-
const it = factory()[Symbol.asyncIterator]();
|
|
130
360
|
while (true) {
|
|
131
361
|
const remainingFirstYield = started
|
|
132
362
|
? Number.POSITIVE_INFINITY
|
|
133
363
|
: startedAt + firstYieldMs - Date.now();
|
|
134
364
|
const watchdogMs = Math.min(inactivityMs, remainingFirstYield);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
365
|
+
const stage = started ? "inactivity" : "firstYield";
|
|
366
|
+
const stageDetail = started ? `${inactivityMs / 1000}s` : `${firstYieldMs / 1000}s`;
|
|
367
|
+
// Run each `.next()` synchronously inside the ALS context so awaits
|
|
368
|
+
// inside the underlying generator's body that call into rate-limit
|
|
369
|
+
// wrappers can resolve the stop-signal via `getStore()`.
|
|
370
|
+
const pending = stopSignalContext.run(stopWaitingRef, () => it.next());
|
|
371
|
+
let timer;
|
|
372
|
+
const wedged = await Promise.race([
|
|
373
|
+
pending.then((result) => {
|
|
374
|
+
if (timer)
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
return { wedge: false, result };
|
|
377
|
+
}),
|
|
378
|
+
new Promise((resolve) => {
|
|
379
|
+
// watchdogMs may be <=0 if the budget is already burned (defensive
|
|
380
|
+
// — should be rare since the clock starts at body-entry). setTimeout
|
|
381
|
+
// with non-positive delay fires on the next event-loop tick, which
|
|
382
|
+
// is the correct wedge-immediately behavior.
|
|
383
|
+
timer = setTimeout(() => resolve({ wedge: true }), Math.max(0, watchdogMs));
|
|
145
384
|
}),
|
|
146
385
|
]);
|
|
386
|
+
if (wedged.wedge) {
|
|
387
|
+
// Hand off the still-pending it.next() so the drain can consume
|
|
388
|
+
// it directly — see BackgroundWedgeEntry.pendingFirstNext.
|
|
389
|
+
emitWedge(stage, stageDetail, pending);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const next = wedged.result;
|
|
147
393
|
if (next.done)
|
|
148
394
|
break;
|
|
149
|
-
|
|
150
|
-
started = true;
|
|
151
|
-
console.log(`ingest: ${label} → starting…`);
|
|
152
|
-
}
|
|
153
|
-
count += 1;
|
|
395
|
+
recordYield(next.value);
|
|
154
396
|
yield next.value;
|
|
155
397
|
}
|
|
156
398
|
if (started) {
|
|
@@ -175,6 +417,64 @@ async function* failSoft(label, factory, onError) {
|
|
|
175
417
|
console.log(`ingest: [debug] ${label} → finalised (${count} records, ${Date.now() - startedAt}ms)`);
|
|
176
418
|
}
|
|
177
419
|
}
|
|
420
|
+
/** Inline helper: extracts qualifiedName-ish text from a RawMember for the
|
|
421
|
+
* `lastYielded=...` diagnostic field. Safe against undefined. */
|
|
422
|
+
function recordYield(v) {
|
|
423
|
+
if (!started) {
|
|
424
|
+
started = true;
|
|
425
|
+
console.log(`ingest: ${label} → starting…`);
|
|
426
|
+
}
|
|
427
|
+
count += 1;
|
|
428
|
+
const ref = v.ref;
|
|
429
|
+
if (ref) {
|
|
430
|
+
lastYieldedQName = `${ref.memberType}/${ref.memberName}`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Slot-release + soft-isolate on watchdog fire. Two side effects:
|
|
435
|
+
* 1. Push a `wedge:<label>:<stage>:<detail>` warning into the shared sink.
|
|
436
|
+
* 2. Park the still-pending iterator into the background-wedge set so
|
|
437
|
+
* the post-merger late-drain pass can capture any records that
|
|
438
|
+
* eventually arrive. Enforce the cap (oldest evicted with a
|
|
439
|
+
* `backgroundWedgeAborted` warning).
|
|
440
|
+
* The generator then RETURNS (caller is `for await` of the merger; the
|
|
441
|
+
* merger sees `done:true` and releases the slot).
|
|
442
|
+
*/
|
|
443
|
+
function emitWedge(stage, stageDetail, pendingFirstNext) {
|
|
444
|
+
const wedgedFor = Date.now() - startedAt;
|
|
445
|
+
if (coordinator) {
|
|
446
|
+
// Schema (PLAN.md): `wedge:<label>:<stage>:<stageDetail>:<k=v>:<k=v>`.
|
|
447
|
+
// <stageDetail> is a positional value (e.g. "90s"), not a key=value.
|
|
448
|
+
const w = `wedge:${label}:${stage}:${stageDetail}` +
|
|
449
|
+
`:lastYielded=${lastYieldedQName}` +
|
|
450
|
+
`:wedgedForMs=${wedgedFor}`;
|
|
451
|
+
coordinator.warnings.push(w);
|
|
452
|
+
console.log(`ingest: ${label} ⚠ wedged (${stage} ${stageDetail}); slot released, draining in background`);
|
|
453
|
+
// W1.5-03: reuse the SAME stopWaitingRef that was published into ALS
|
|
454
|
+
// at body entry. When enforceBackgroundWedgeCap later flips this ref's
|
|
455
|
+
// `.value = true` on cap eviction, any in-flight rate-limit wrapper
|
|
456
|
+
// inside the wedged iterator's continuing background pumps will see
|
|
457
|
+
// the flag through `stopSignalContext.getStore()` and reject with
|
|
458
|
+
// `StopWaitingError` quickly. The underlying jsforce HTTP call still
|
|
459
|
+
// leaks until socket idle timeout — documented in rate-limit.ts.
|
|
460
|
+
coordinator.backgroundWedges.push({
|
|
461
|
+
label,
|
|
462
|
+
iterator: it,
|
|
463
|
+
pendingFirstNext,
|
|
464
|
+
registeredAt: Date.now(),
|
|
465
|
+
stopWaitingRef,
|
|
466
|
+
});
|
|
467
|
+
enforceBackgroundWedgeCap(coordinator);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// No coordinator (e.g. unit-test path that didn't wire one up, or
|
|
471
|
+
// back-compat caller): fall back to the old "log+skip" behavior so
|
|
472
|
+
// the caller still gets observable feedback.
|
|
473
|
+
const msg = `source watchdog (${stage} ${stageDetail}): no record yielded — pool jammed or call wedged`;
|
|
474
|
+
onError?.(label, new Error(msg));
|
|
475
|
+
console.log(`ingest: ${label} ✗ skipped (${msg.slice(0, 80)})`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
178
478
|
}
|
|
179
479
|
/** Typed-extractor ownership map: which XML type names a dedicated extractor covers. */
|
|
180
480
|
const APEX_TYPES = new Set(["ApexClass", "ApexTrigger"]);
|
|
@@ -291,10 +591,29 @@ const GENERIC_TYPE_WHITELIST = new Set([
|
|
|
291
591
|
"OmniUiCard",
|
|
292
592
|
"OmniProcess",
|
|
293
593
|
]);
|
|
594
|
+
/**
|
|
595
|
+
* Types whose data is fully captured by a CONTAINER type, so routing them
|
|
596
|
+
* individually only produces value-less opaque duplicates that clobber the
|
|
597
|
+
* container's richer nodes. `CustomLabel` (singular) is covered by the
|
|
598
|
+
* `CustomLabels` container — whose rule extracts each label's value/language;
|
|
599
|
+
* the singular path would emit `CustomLabel:Name` nodes with no value and,
|
|
600
|
+
* depending on merge order, overwrite the real ones. Skip them.
|
|
601
|
+
*/
|
|
602
|
+
const CONTAINER_COVERED_TYPES = new Set(["CustomLabel"]);
|
|
294
603
|
function shouldRouteGeneric(type) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
604
|
+
// Never route a type a container already covers with richer data.
|
|
605
|
+
if (CONTAINER_COVERED_TYPES.has(type))
|
|
606
|
+
return false;
|
|
607
|
+
// Default: route EVERY long-tail metadata type through the generic
|
|
608
|
+
// extractor so the graph — and therefore semantic search — covers the org
|
|
609
|
+
// completely. Anything without a dedicated extractor still becomes a typed
|
|
610
|
+
// node (the rule parsers key off the type name) or an opaque node, which is
|
|
611
|
+
// what search needs. The old curated whitelist silently dropped ~250
|
|
612
|
+
// discovered types (search returned nothing for them). Set
|
|
613
|
+
// SFGRAPH_GENERIC_WHITELIST_ONLY=1 to restore the restricted subset.
|
|
614
|
+
if (process.env.SFGRAPH_GENERIC_WHITELIST_ONLY === "1")
|
|
615
|
+
return GENERIC_TYPE_WHITELIST.has(type);
|
|
616
|
+
return true;
|
|
298
617
|
}
|
|
299
618
|
export async function* bulkRetrieve(conn, caps, orgId, opts = {}) {
|
|
300
619
|
// Back-compat: callers used to pass IngestSkipReport directly as the 4th arg.
|
|
@@ -304,6 +623,13 @@ export async function* bulkRetrieve(conn, caps, orgId, opts = {}) {
|
|
|
304
623
|
: opts;
|
|
305
624
|
const skipReport = normalized.skipReport;
|
|
306
625
|
const onlyLabels = normalized.onlyLabels;
|
|
626
|
+
// Phase 1.5 W1.5-02 coordinator. Built only when a warnings sink was
|
|
627
|
+
// supplied — without one, no place to push wedge strings, so the legacy
|
|
628
|
+
// "watchdog fires → skip entry" behavior is preserved. The coordinator
|
|
629
|
+
// owns the background-wedge cap + late-drain budget.
|
|
630
|
+
const coordinator = normalized.warnings
|
|
631
|
+
? createWedgeCoordinator(normalized.warnings)
|
|
632
|
+
: undefined;
|
|
307
633
|
// Discover the type list this org actually supports. If discovery fails or
|
|
308
634
|
// returns nothing usable, fall back to invoking every known extractor —
|
|
309
635
|
// preserves Commit-A behavior for mocks that don't implement describe.
|
|
@@ -336,7 +662,7 @@ export async function* bulkRetrieve(conn, caps, orgId, opts = {}) {
|
|
|
336
662
|
// ingest. The wrapper records the source label + error into skipReport
|
|
337
663
|
// (consumed at end of run for a consolidated summary) and ends the
|
|
338
664
|
// stream cleanly.
|
|
339
|
-
sources.push(failSoft(key, factory, onSkip));
|
|
665
|
+
sources.push(failSoft(key, factory, onSkip, coordinator));
|
|
340
666
|
};
|
|
341
667
|
if (types.length === 0) {
|
|
342
668
|
// Discovery unavailable: invoke every dedicated extractor once.
|
|
@@ -394,12 +720,54 @@ export async function* bulkRetrieve(conn, caps, orgId, opts = {}) {
|
|
|
394
720
|
break;
|
|
395
721
|
}
|
|
396
722
|
}
|
|
723
|
+
// Child metadata types (ValidationRule, ListView, CompactLayout, FieldSet,
|
|
724
|
+
// WebLink, BusinessProcess, ...) are returned by describe() only as
|
|
725
|
+
// `childXmlNames` of a parent (mostly CustomObject), never as top-level
|
|
726
|
+
// types — so the dispatch loop above never fetches them. Most ARE
|
|
727
|
+
// independently listable via metadata.list, so route each through the
|
|
728
|
+
// generic extractor. This is how validation rules / list views on STANDARD
|
|
729
|
+
// objects get captured — iterObject only sees children embedded in the
|
|
730
|
+
// custom objects it reads, missing the bulk that live on Account/Case/etc.
|
|
731
|
+
// Children that aren't independently listable fail-soft to nothing. Skip
|
|
732
|
+
// children already owned by a dedicated extractor or dispatched above.
|
|
733
|
+
if (process.env.SFGRAPH_GENERIC_WHITELIST_ONLY !== "1") {
|
|
734
|
+
const typedOwned = new Set([
|
|
735
|
+
...APEX_TYPES,
|
|
736
|
+
...LWC_TYPES,
|
|
737
|
+
...FLOW_TYPES,
|
|
738
|
+
...OBJECT_TYPES,
|
|
739
|
+
...SECURITY_TYPES,
|
|
740
|
+
...INTEGRATION_TYPES,
|
|
741
|
+
"CustomField", // fetched in bulk by iterObject; re-listing is wasteful
|
|
742
|
+
]);
|
|
743
|
+
const childTypes = new Set();
|
|
744
|
+
for (const t of types)
|
|
745
|
+
for (const c of t.childXmlNames)
|
|
746
|
+
childTypes.add(c);
|
|
747
|
+
for (const child of childTypes) {
|
|
748
|
+
if (dispatch.has(child) || typedOwned.has(child))
|
|
749
|
+
continue;
|
|
750
|
+
if (CONTAINER_COVERED_TYPES.has(child))
|
|
751
|
+
continue; // CustomLabel ⊂ CustomLabels
|
|
752
|
+
invoke(`generic:${child}`, () => iterGenericMetadata(conn, String(orgId), child));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
397
755
|
}
|
|
398
756
|
if (caps.vlocityLegacy) {
|
|
399
757
|
invoke("vlocity", () => iterVlocity(conn, caps, String(orgId), onSkip));
|
|
400
758
|
}
|
|
759
|
+
// Custom Setting ROW capture — self-guards (yields nothing if the org has no
|
|
760
|
+
// custom settings or EntityDefinition is unavailable), so invoke always.
|
|
761
|
+
invoke("custom-settings", () => iterCustomSettings(conn, String(orgId)));
|
|
401
762
|
if (caps.omnistudioOncore) {
|
|
402
|
-
|
|
763
|
+
// When the Metadata-API retrieve path is enabled it covers
|
|
764
|
+
// OmniDataTransform and OmniUiCard with higher fidelity — skip them on
|
|
765
|
+
// the SOQL path so the same component is not ingested twice (the
|
|
766
|
+
// retrieve XML would otherwise clobber the SOQL-enriched node, and
|
|
767
|
+
// vice versa, nondeterministically by merge order).
|
|
768
|
+
invoke("omnistudio", () => iterOmnistudio(conn, normalized.enableOmnistudioRetrieve
|
|
769
|
+
? { skipTypes: new Set(["OmniDataTransform", "OmniUiCard"]) }
|
|
770
|
+
: {}));
|
|
403
771
|
if (normalized.enableOmnistudioRetrieve) {
|
|
404
772
|
invoke("omnistudio-retrieve", async function* () {
|
|
405
773
|
const { iterOmnistudioRetrieve } = await import("./extractors/omnistudio-retrieve.js");
|
|
@@ -410,10 +778,16 @@ export async function* bulkRetrieve(conn, caps, orgId, opts = {}) {
|
|
|
410
778
|
});
|
|
411
779
|
}
|
|
412
780
|
}
|
|
413
|
-
// Log the generic-type
|
|
414
|
-
// skip-vs-route decision visible without --debug.
|
|
781
|
+
// Log the generic-type routing summary once at fan-out start — makes the
|
|
782
|
+
// skip-vs-route decision visible without --debug. The default is FULL
|
|
783
|
+
// coverage (every discovered type routed); the opt-out is
|
|
784
|
+
// SFGRAPH_GENERIC_WHITELIST_ONLY=1 (restricts to the curated subset).
|
|
415
785
|
if (types.length > 0) {
|
|
416
|
-
|
|
786
|
+
const whitelistOnly = process.env.SFGRAPH_GENERIC_WHITELIST_ONLY === "1";
|
|
787
|
+
const mode = whitelistOnly
|
|
788
|
+
? "curated whitelist only (SFGRAPH_GENERIC_WHITELIST_ONLY=1) — unset to route all"
|
|
789
|
+
: "full coverage (all discovered types) — set SFGRAPH_GENERIC_WHITELIST_ONLY=1 to restrict";
|
|
790
|
+
console.log(`ingest: dispatch routed=${sources.length} sources (${types.length} discovered metadata types; ${mode})`);
|
|
417
791
|
}
|
|
418
792
|
// Parallel by default: fan out across all source iterators so different
|
|
419
793
|
// pools (Tooling/Metadata/Data) saturate simultaneously. Escape hatch:
|
|
@@ -421,5 +795,60 @@ export async function* bulkRetrieve(conn, caps, orgId, opts = {}) {
|
|
|
421
795
|
// anyone who hits an ordering bug or wants the old log layout.
|
|
422
796
|
const sequential = process.env.SFGRAPH_SEQUENTIAL_SOURCES === "1";
|
|
423
797
|
yield* sequential ? mergeAsyncIterables(...sources) : mergeAsyncIterablesParallel(...sources);
|
|
798
|
+
// Phase 1.5 W1.5-02 late-yield drain. When the merger has finished its
|
|
799
|
+
// main loop (all live slots cleared), drain any background-wedge iterators
|
|
800
|
+
// for the configured budget. Records arriving inside the budget are
|
|
801
|
+
// yielded with `attributes.lateYield = true`. Iterators whose stopWaiting
|
|
802
|
+
// flag was set by the cap-eviction path skip immediately. Anything still
|
|
803
|
+
// unresolved after the budget is dropped on the floor — its underlying
|
|
804
|
+
// jsforce HTTP request continues running until libuv tears it down at
|
|
805
|
+
// process exit (W1.5-03 will surface a "stop-waiting" semantic for this).
|
|
806
|
+
if (coordinator && coordinator.backgroundWedges.length > 0 && coordinator.lateDrainBudgetMs > 0) {
|
|
807
|
+
if (process.env.SFGRAPH_DEBUG_INGEST === "1") {
|
|
808
|
+
console.log(`ingest: [debug] late-drain pass: ${coordinator.backgroundWedges.length} background wedge(s), budget=${coordinator.lateDrainBudgetMs}ms`);
|
|
809
|
+
}
|
|
810
|
+
for (const entry of coordinator.backgroundWedges) {
|
|
811
|
+
yield* drainBackgroundWedge(entry, coordinator.lateDrainBudgetMs, coordinator.warnings);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
424
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Internal-surface exports for unit tests in `__tests__/`. Not part of the
|
|
817
|
+
* public package API — these are intentionally namespaced under `__testing`
|
|
818
|
+
* so they don't appear in the package's exports surface to downstream
|
|
819
|
+
* consumers. (They're still importable, but the underscore convention
|
|
820
|
+
* signals "test only".)
|
|
821
|
+
*
|
|
822
|
+
* Exposed:
|
|
823
|
+
* - `failSoft`: wrap an iterable with the W1.5-01/02 watchdog + soft-isolate.
|
|
824
|
+
* - `createWedgeCoordinator`: build a coordinator + warnings sink with
|
|
825
|
+
* optional cap/drain overrides for deterministic tests.
|
|
826
|
+
* - `drainBackgroundWedge`: run the post-merger late-drain pass against
|
|
827
|
+
* a single background-wedge entry.
|
|
828
|
+
* - `mergeAsyncIterablesParallel`: re-export for symmetry; the (array,
|
|
829
|
+
* opts) overload is what tests use to fix concurrency.
|
|
830
|
+
* - `setTestWatchdogBudgets` / `resetTestWatchdogBudgets`: env-var-backed
|
|
831
|
+
* knobs to squeeze the watchdog budget for fast tests. Always paired
|
|
832
|
+
* via try/finally in tests so the environment is left clean.
|
|
833
|
+
*/
|
|
834
|
+
export const __testing = {
|
|
835
|
+
failSoft,
|
|
836
|
+
createWedgeCoordinator,
|
|
837
|
+
drainBackgroundWedge,
|
|
838
|
+
mergeAsyncIterablesParallel,
|
|
839
|
+
readWatchdogBudgets,
|
|
840
|
+
setTestWatchdogBudgets(opts) {
|
|
841
|
+
process.env.SFGRAPH_WATCHDOG_FIRST_YIELD_MS = String(opts.firstYieldMs);
|
|
842
|
+
process.env.SFGRAPH_WATCHDOG_INACTIVITY_MS = String(opts.inactivityMs);
|
|
843
|
+
},
|
|
844
|
+
resetTestWatchdogBudgets() {
|
|
845
|
+
// Note: assigning `undefined` to a `process.env.X` key in Node coerces to
|
|
846
|
+
// the string "undefined". That's fine here because readWatchdogBudgets()
|
|
847
|
+
// runs parseInt() on the value, parseInt("undefined") === NaN, and NaN
|
|
848
|
+
// falls through to the *_MS_DEFAULT branch. So semantically equivalent
|
|
849
|
+
// to `delete`, but satisfies biome's lint/performance/noDelete.
|
|
850
|
+
process.env.SFGRAPH_WATCHDOG_FIRST_YIELD_MS = undefined;
|
|
851
|
+
process.env.SFGRAPH_WATCHDOG_INACTIVITY_MS = undefined;
|
|
852
|
+
},
|
|
853
|
+
};
|
|
425
854
|
//# sourceMappingURL=bulk-retrieve.js.map
|