@teambit/lanes 1.0.1100 → 1.0.1102
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/lanes.graphql.js +20 -2
- package/dist/lanes.graphql.js.map +1 -1
- package/dist/lanes.main.runtime.d.ts +73 -4
- package/dist/lanes.main.runtime.js +292 -74
- package/dist/lanes.main.runtime.js.map +1 -1
- package/dist/lanes.ui.runtime.d.ts +43 -4
- package/dist/lanes.ui.runtime.js +67 -6
- package/dist/lanes.ui.runtime.js.map +1 -1
- package/dist/{preview-1785267101230.js → preview-1785362864561.js} +2 -2
- package/lanes.ui.runtime.tsx +70 -4
- package/package.json +49 -48
|
@@ -17,16 +17,16 @@ function _cli() {
|
|
|
17
17
|
};
|
|
18
18
|
return data;
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
const data = _interopRequireDefault(require("p-map
|
|
22
|
-
|
|
20
|
+
function _pMap() {
|
|
21
|
+
const data = _interopRequireDefault(require("p-map"));
|
|
22
|
+
_pMap = function () {
|
|
23
23
|
return data;
|
|
24
24
|
};
|
|
25
25
|
return data;
|
|
26
26
|
}
|
|
27
|
-
function
|
|
28
|
-
const data = _interopRequireDefault(require("p-
|
|
29
|
-
|
|
27
|
+
function _pLimit() {
|
|
28
|
+
const data = _interopRequireDefault(require("p-limit"));
|
|
29
|
+
_pLimit = function () {
|
|
30
30
|
return data;
|
|
31
31
|
};
|
|
32
32
|
return data;
|
|
@@ -316,7 +316,9 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
|
|
|
316
316
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
317
317
|
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
318
318
|
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
319
|
-
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
319
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* eslint-disable max-lines */
|
|
320
|
+
/** where the base was resolved from: `workspace` (already local) or `scope` (fetched from remote). */
|
|
321
|
+
|
|
320
322
|
class LanesMain {
|
|
321
323
|
constructor(workspace, scope, merging, componentAspect, logger, importer, exporter, componentCompare, componentWriter, remove, checkout, install) {
|
|
322
324
|
this.workspace = workspace;
|
|
@@ -331,8 +333,97 @@ class LanesMain {
|
|
|
331
333
|
this.remove = remove;
|
|
332
334
|
this.checkout = checkout;
|
|
333
335
|
this.install = install;
|
|
336
|
+
/**
|
|
337
|
+
* disk-persisted memo layer for the lane-diff computation (snaps-distance, change-types and the
|
|
338
|
+
* top-level result). extracted to its own class — see `LaneDiffCache` in lanes/diff.
|
|
339
|
+
*/
|
|
340
|
+
_defineProperty(this, "_laneDiffCache", void 0);
|
|
341
|
+
/**
|
|
342
|
+
* Bound the parallelism of `deriveChangeTypes` across all in-flight requests. graphql-js resolves
|
|
343
|
+
* list fields in parallel via `Promise.all`, so an uncapped derivation fires one `compare()` per
|
|
344
|
+
* component at once. The old cap of 4 existed because derivation also ran `getAPIDiff` (serial
|
|
345
|
+
* tsserver schema extraction) — that's now deferred to the API view, so derivation only does the
|
|
346
|
+
* I/O+CPU-bound `compare()`, which parallelizes well. Cap to the component concurrency limit.
|
|
347
|
+
*/
|
|
348
|
+
_defineProperty(this, "deriveChangesLimit", (0, _pLimit().default)((0, _harmonyModules().concurrentComponentsLimit)()));
|
|
349
|
+
/** single-flight for concurrent `deriveComponentChanges` calls on the same memo key. */
|
|
350
|
+
_defineProperty(this, "changeTypesInflight", new Map());
|
|
351
|
+
}
|
|
352
|
+
get laneDiffCache() {
|
|
353
|
+
if (!this._laneDiffCache) this._laneDiffCache = new (_lanesModules().LaneDiffCache)(this.logger);
|
|
354
|
+
return this._laneDiffCache;
|
|
355
|
+
}
|
|
356
|
+
tryDiffStatusResultMemo(resultMemoKey, sourceLaneId, targetLaneId) {
|
|
357
|
+
const cached = this.laneDiffCache.getDiffStatus(resultMemoKey);
|
|
358
|
+
if (!cached) return undefined;
|
|
359
|
+
return {
|
|
360
|
+
source: sourceLaneId,
|
|
361
|
+
target: targetLaneId || this.getDefaultLaneId(),
|
|
362
|
+
componentsStatus: cached
|
|
363
|
+
};
|
|
334
364
|
}
|
|
335
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Pre-warm the downstream caches the lane-compare UI hits right after `LaneDiffStatus`:
|
|
368
|
+
* - the scope's `ScopeComponentLoader.componentsCache` via `host.getMany([…])`
|
|
369
|
+
* - `componentCompare`'s disk-persisted result memo via `compareComponents(pairs)`
|
|
370
|
+
*
|
|
371
|
+
* Fire-and-forget. The UI's follow-up `Component` and `CompareComponents` queries arrive ~50–200 ms
|
|
372
|
+
* after we return; this kickoff happens *before* we return, so the work overlaps the UI's render
|
|
373
|
+
* tick and the response serialization. By the time the UI's queries land on the server, the caches
|
|
374
|
+
* are populated (or, worst case, the pre-warm work is in-flight and the resolver-level single-flight
|
|
375
|
+
* in `componentCompare` dedupes the load).
|
|
376
|
+
*/
|
|
377
|
+
prewarmCompareCaches(host, visibleDiffProps) {
|
|
378
|
+
const allVersionedIds = [];
|
|
379
|
+
const comparePairs = [];
|
|
380
|
+
for (const {
|
|
381
|
+
componentId,
|
|
382
|
+
sourceHead,
|
|
383
|
+
targetHead
|
|
384
|
+
} of visibleDiffProps) {
|
|
385
|
+
const compareId = componentId.changeVersion(sourceHead);
|
|
386
|
+
allVersionedIds.push(compareId);
|
|
387
|
+
if (targetHead) {
|
|
388
|
+
const baseId = componentId.changeVersion(targetHead);
|
|
389
|
+
allVersionedIds.push(baseId);
|
|
390
|
+
comparePairs.push({
|
|
391
|
+
baseId: baseId.toString(),
|
|
392
|
+
compareId: compareId.toString()
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (allVersionedIds.length === 0) return;
|
|
397
|
+
// NOTE: `host.getMany` uses mapSeries internally (sequential). For cold pre-warm to actually
|
|
398
|
+
// beat the UI's concurrent N-op `Component` batch we have to drive parallelism ourselves. pMap
|
|
399
|
+
// here uses the same `concurrentComponentsLimit()` cap as the lane diff body, matching what
|
|
400
|
+
// downstream loaders are tuned for.
|
|
401
|
+
Promise.all([
|
|
402
|
+
// wrap in try/catch since `host.get` could be missing or throw synchronously; the previous
|
|
403
|
+
// `host.get?.(id).catch(...)` chained .catch on `undefined` when get was absent.
|
|
404
|
+
(0, _pMap().default)(allVersionedIds, async id => {
|
|
405
|
+
try {
|
|
406
|
+
return await host.get?.(id);
|
|
407
|
+
} catch {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
}, {
|
|
411
|
+
concurrency: (0, _harmonyModules().concurrentComponentsLimit)()
|
|
412
|
+
}).catch(() => undefined), comparePairs.length > 0 ? this.componentCompare.compareComponents(comparePairs).catch(() => undefined) : Promise.resolve()]).catch(() => {});
|
|
413
|
+
}
|
|
414
|
+
populateDiffStatusMemoAsync(resultMemoKey, results) {
|
|
415
|
+
Promise.all(results.map(async status => {
|
|
416
|
+
if (status.changes) return;
|
|
417
|
+
status.changes = await this.deriveComponentChanges(status);
|
|
418
|
+
})).then(() => {
|
|
419
|
+
// don't persist a result set where a component's change types couldn't be derived: `changes`
|
|
420
|
+
// came back undefined for a reason OTHER than being explicitly skipped (i.e. a transient
|
|
421
|
+
// Version-load failure). persisting it under the immutable lane-pair key would serve that gap
|
|
422
|
+
// forever; it recomputes and persists on a later request once the objects are available.
|
|
423
|
+
const anyFailed = results.some(s => s.changes === undefined && !s.changesContext?.skipped);
|
|
424
|
+
if (!anyFailed) this.laneDiffCache.storeDiffStatus(resultMemoKey, results);
|
|
425
|
+
}).catch(() => {});
|
|
426
|
+
}
|
|
336
427
|
/**
|
|
337
428
|
* return the lane data without the deleted components.
|
|
338
429
|
* the deleted components are filtered out in legacyScope.lanes.getLanesData()
|
|
@@ -698,12 +789,9 @@ please create a new lane instead, which will include all components of this lane
|
|
|
698
789
|
return deletedComps.map(c => c.id);
|
|
699
790
|
}
|
|
700
791
|
|
|
701
|
-
/**
|
|
702
|
-
* get the head hash (snap) of main. return undefined if the component exists only on a lane and was never merged to main
|
|
703
|
-
*/
|
|
792
|
+
/** head hash of main, with remote-scope fallback. undefined only for a genuinely-new component. */
|
|
704
793
|
async getHeadOnMain(componentId) {
|
|
705
|
-
|
|
706
|
-
return modelComponent.head?.toString();
|
|
794
|
+
return (0, _lanesModules().getHeadOnMain)(this.scope, componentId);
|
|
707
795
|
}
|
|
708
796
|
|
|
709
797
|
/**
|
|
@@ -861,8 +949,13 @@ please create a new lane instead, which will include all components of this lane
|
|
|
861
949
|
if (!lane) return [];
|
|
862
950
|
const laneComponents = lane.components;
|
|
863
951
|
const workspace = this.workspace;
|
|
864
|
-
|
|
865
|
-
|
|
952
|
+
// the bitmap filter only makes sense for the lane currently checked out — it keeps that view
|
|
953
|
+
// aligned with what's actually in the workspace. any OTHER lane lives only in the local scope:
|
|
954
|
+
// its components are never in the bitmap, so filtering by it empties the lane (a fully-fetched
|
|
955
|
+
// lane renders as "no components" in the overview) even though every object is available.
|
|
956
|
+
const isCurrentLane = Boolean(workspace && this.getCurrentLaneId()?.isEqual(lane.id));
|
|
957
|
+
const bitIdsFromBitmap = workspace && isCurrentLane ? workspace.consumer.bitMap.getAllBitIdsFromAllLanes() : [];
|
|
958
|
+
const filteredComponentIds = isCurrentLane ? laneComponents.filter(laneComponent => bitIdsFromBitmap.some(bitmapComponentId => bitmapComponentId.isEqualWithoutVersion(laneComponent.id))) : laneComponents;
|
|
866
959
|
return filteredComponentIds.map(laneComponent => laneComponent.id.changeVersion(laneComponent.head));
|
|
867
960
|
}
|
|
868
961
|
|
|
@@ -927,6 +1020,7 @@ please create a new lane instead, which will include all components of this lane
|
|
|
927
1020
|
};
|
|
928
1021
|
}
|
|
929
1022
|
async diffStatus(sourceLaneId, targetLaneId, options) {
|
|
1023
|
+
await this.laneDiffCache.ensureLoaded();
|
|
930
1024
|
this.logger.profile(`diff status for source lane: ${sourceLaneId.name} and target lane: ${targetLaneId?.name}`);
|
|
931
1025
|
const sourceLane = sourceLaneId.isDefault() ? await this.getLaneDataOfDefaultLane() : await this.loadLane(sourceLaneId);
|
|
932
1026
|
const sourceLaneComponents = sourceLaneId.isDefault() ? sourceLane?.components.map(main => ({
|
|
@@ -936,13 +1030,39 @@ please create a new lane instead, which will include all components of this lane
|
|
|
936
1030
|
const targetLane = targetLaneId ? await this.loadLane(targetLaneId) : undefined;
|
|
937
1031
|
const targetLaneIds = targetLane?.toBitIds();
|
|
938
1032
|
const host = this.componentAspect.getHost();
|
|
939
|
-
|
|
1033
|
+
|
|
1034
|
+
// Resolve the main-side heads up-front: they feed BOTH the memo key (so the cache invalidates
|
|
1035
|
+
// when main advances) and the diff computation below. The target=main case has no single lane
|
|
1036
|
+
// hash, so without this the key was constant ('default') and stale results were served when main
|
|
1037
|
+
// moved ahead (lanes.spec "not up to date when main is ahead").
|
|
1038
|
+
const targetIsMain = !targetLaneId || Boolean(targetLaneId?.isDefault());
|
|
1039
|
+
// whether each base on main was already local (snapshot *before* the remote fetch) - drives the
|
|
1040
|
+
// UI "workspace vs remote scope" source indicator.
|
|
1041
|
+
const baseWasLocalByComp = new Map();
|
|
1042
|
+
if (targetIsMain) {
|
|
1043
|
+
const sourceComponentIds = await Promise.all((sourceLaneComponents || []).map(({
|
|
1044
|
+
id
|
|
1045
|
+
}) => host.resolveComponentId(id)));
|
|
1046
|
+
await Promise.all(sourceComponentIds.map(async cid => {
|
|
1047
|
+
const modelComp = await this.scope.legacyScope.getModelComponentIfExist(cid);
|
|
1048
|
+
baseWasLocalByComp.set(cid.toString(), Boolean(modelComp?.head));
|
|
1049
|
+
}));
|
|
1050
|
+
// the base on main may live only on the remote scope; pull it so the diff is real, not a spurious NEW.
|
|
1051
|
+
await (0, _lanesModules().importMainHeads)(this.scope, sourceComponentIds);
|
|
1052
|
+
}
|
|
1053
|
+
const targetMainHeads = targetIsMain ? (0, _lodash().compact)(await Promise.all((sourceLaneComponents || []).map(async ({
|
|
940
1054
|
id
|
|
941
1055
|
}) => {
|
|
942
1056
|
const componentId = await host.resolveComponentId(id);
|
|
943
1057
|
const headOnMain = await this.getHeadOnMain(componentId);
|
|
944
1058
|
return headOnMain ? id.changeVersion(headOnMain) : undefined;
|
|
945
1059
|
}))) : [];
|
|
1060
|
+
const resultMemoKey = this.laneDiffCache.diffStatusKey(sourceLaneId, sourceLane ?? undefined, targetLaneId, targetLane ?? undefined, options, targetMainHeads);
|
|
1061
|
+
const earlyReturn = this.tryDiffStatusResultMemo(resultMemoKey, sourceLaneId, targetLaneId);
|
|
1062
|
+
if (earlyReturn) {
|
|
1063
|
+
this.logger.profile(`diff status for source lane: ${sourceLaneId.name} and target lane: ${targetLaneId?.name}`);
|
|
1064
|
+
return earlyReturn;
|
|
1065
|
+
}
|
|
946
1066
|
await this.importer.importObjectsFromMainIfExist(targetMainHeads, {
|
|
947
1067
|
cache: true
|
|
948
1068
|
});
|
|
@@ -972,12 +1092,28 @@ please create a new lane instead, which will include all components of this lane
|
|
|
972
1092
|
})));
|
|
973
1093
|
const snapDistancesByComponentId = new Map();
|
|
974
1094
|
this.logger.profile(`get snaps distance for source lane: ${sourceLane?.id.name} and target lane: ${targetLane?.id.name} with ${diffProps.length} components`);
|
|
1095
|
+
let memoHits = 0;
|
|
975
1096
|
await (0, _pMap().default)(diffProps, async ({
|
|
976
1097
|
componentId,
|
|
977
1098
|
sourceHead,
|
|
978
1099
|
targetHead
|
|
979
1100
|
}) => {
|
|
980
|
-
const
|
|
1101
|
+
const memoKey = this.laneDiffCache.snapsDistanceKey(componentId, sourceHead, targetHead);
|
|
1102
|
+
const cached = this.laneDiffCache.getSnapsDistance(memoKey, componentId.toString());
|
|
1103
|
+
let snapsDistance;
|
|
1104
|
+
if (cached) {
|
|
1105
|
+
memoHits += 1;
|
|
1106
|
+
snapsDistance = cached;
|
|
1107
|
+
} else {
|
|
1108
|
+
const computed = await this.scope.getSnapsDistanceBetweenTwoSnaps(componentId, sourceHead, targetHead, false);
|
|
1109
|
+
if (computed) {
|
|
1110
|
+
// cache success + the deterministic "unrelated" outcome. transient errors stay uncached.
|
|
1111
|
+
if (!computed.err || computed.err instanceof _legacy().NoCommonSnap) {
|
|
1112
|
+
this.laneDiffCache.storeSnapsDistance(memoKey, computed);
|
|
1113
|
+
}
|
|
1114
|
+
snapsDistance = computed;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
981
1117
|
if (snapsDistance) {
|
|
982
1118
|
snapDistancesByComponentId.set(componentId.toString(), {
|
|
983
1119
|
snapsDistance,
|
|
@@ -989,21 +1125,64 @@ please create a new lane instead, which will include all components of this lane
|
|
|
989
1125
|
}, {
|
|
990
1126
|
concurrency: (0, _harmonyModules().concurrentComponentsLimit)()
|
|
991
1127
|
});
|
|
992
|
-
this.logger.profile(`get snaps distance for source lane: ${sourceLane?.id.name} and target lane: ${targetLane?.id.name} with ${diffProps.length} components`);
|
|
993
|
-
|
|
1128
|
+
this.logger.profile(`get snaps distance for source lane: ${sourceLane?.id.name} and target lane: ${targetLane?.id.name} with ${diffProps.length} components (memo hits: ${memoHits}/${diffProps.length})`);
|
|
1129
|
+
|
|
1130
|
+
// when `skipUpToDate` is set, drop only components with NO difference vs the target. keep any
|
|
1131
|
+
// divergence — crucially `isSourceAhead()` (the lane's own changes): a lane forked from main has no
|
|
1132
|
+
// target-only snaps, so `isUpToDate()` (=!isTargetAhead) alone would wrongly hide the whole diff.
|
|
1133
|
+
const visibleDiffProps = options?.skipUpToDate ? diffProps.filter(({
|
|
1134
|
+
componentId
|
|
1135
|
+
}) => {
|
|
1136
|
+
// `d` may be a memo reconstruction exposing only data + `isUpToDate()`; test source-ahead via
|
|
1137
|
+
// `snapsOnSourceOnly.length`, not the `isSourceAhead()` method (absent on the reconstruction).
|
|
1138
|
+
const d = snapDistancesByComponentId.get(componentId.toString())?.snapsDistance;
|
|
1139
|
+
return !d || (d.snapsOnSourceOnly?.length ?? 0) > 0 || !d.isUpToDate();
|
|
1140
|
+
}) : diffProps;
|
|
1141
|
+
const commonSnapsToImport = (0, _lodash().compact)(visibleDiffProps.map(({
|
|
1142
|
+
componentId
|
|
1143
|
+
}) => {
|
|
1144
|
+
const s = snapDistancesByComponentId.get(componentId.toString());
|
|
1145
|
+
return s?.snapsDistance.commonSnapBeforeDiverge ? s.componentId.changeVersion(s.snapsDistance.commonSnapBeforeDiverge.hash) : null;
|
|
1146
|
+
}));
|
|
994
1147
|
const sourceOrTargetLane = ((sourceLaneId.isDefault() ? null : sourceLane) || (targetLaneId?.isDefault() ? null : targetLane)) ?? undefined;
|
|
995
|
-
if (commonSnapsToImport.length > 0 && !options?.skipChanges) {
|
|
1148
|
+
if (commonSnapsToImport.length > 0 && !options?.skipChanges && !options?.deferChanges) {
|
|
1149
|
+
this.logger.profile(`import common snaps for lane diff (${commonSnapsToImport.length} snaps)`);
|
|
996
1150
|
await this.scope.legacyScope.scopeImporter.importWithoutDeps(_componentId().ComponentIdList.fromArray(commonSnapsToImport), {
|
|
997
1151
|
cache: true,
|
|
998
1152
|
reason: `get the common snap for lane diff`,
|
|
999
1153
|
lane: sourceOrTargetLane
|
|
1000
1154
|
});
|
|
1155
|
+
this.logger.profile(`import common snaps for lane diff (${commonSnapsToImport.length} snaps)`);
|
|
1001
1156
|
}
|
|
1002
|
-
|
|
1157
|
+
this.logger.profile(`componentDiffStatus pMap (${visibleDiffProps.length} components)`);
|
|
1158
|
+
// run in parallel — bounded by `concurrentComponentsLimit()`. previously this was pMapSeries which
|
|
1159
|
+
// serialized 30 schema extractions and dominated the cold call (minutes).
|
|
1160
|
+
const results = await (0, _pMap().default)(visibleDiffProps, async ({
|
|
1003
1161
|
componentId,
|
|
1004
1162
|
sourceHead,
|
|
1005
1163
|
targetHead
|
|
1006
|
-
}) =>
|
|
1164
|
+
}) => {
|
|
1165
|
+
// report whether an existing main base was already local (`workspace`) or pulled from remote (`scope`).
|
|
1166
|
+
const baseSource = targetIsMain && targetHead ? baseWasLocalByComp.get(componentId.toString()) ? 'workspace' : 'scope' : undefined;
|
|
1167
|
+
return this.componentDiffStatus(componentId, sourceHead, targetHead, snapDistancesByComponentId.get(componentId.toString())?.snapsDistance, options, baseSource);
|
|
1168
|
+
}, {
|
|
1169
|
+
concurrency: (0, _harmonyModules().concurrentComponentsLimit)()
|
|
1170
|
+
});
|
|
1171
|
+
this.logger.profile(`componentDiffStatus pMap (${visibleDiffProps.length} components)`);
|
|
1172
|
+
|
|
1173
|
+
// best-effort populate the top-level result memo. eagerly resolve `changes` for any deferred
|
|
1174
|
+
// components so the next cold call can return immediately — does not block this response.
|
|
1175
|
+
if (resultMemoKey) this.populateDiffStatusMemoAsync(resultMemoKey, results);
|
|
1176
|
+
|
|
1177
|
+
// Pre-warm caches the lane-compare UI will hit right after this query returns:
|
|
1178
|
+
// - `host.getMany([base+compare versioned ids])` populates `ScopeComponentLoader.componentsCache`
|
|
1179
|
+
// so the UI's 20 per-component `getHost.get` queries (componentFields + componentFieldWithLogs
|
|
1180
|
+
// for each side of each visible pair) all hit cache instead of racing through cold loads.
|
|
1181
|
+
// - `componentCompare.compareComponents(pairs)` populates the new compare-result memo so the UI's
|
|
1182
|
+
// `CompareComponents` query is answered from cache.
|
|
1183
|
+
// Fired *after* we have the answer ready — never blocks the response, catch() hides any failure.
|
|
1184
|
+
// Gated to `prewarmCaches` (the UI caller) so CLI/programmatic consumers don't pay & multiply it.
|
|
1185
|
+
if (options?.prewarmCaches && visibleDiffProps.length > 0) this.prewarmCompareCaches(host, visibleDiffProps);
|
|
1007
1186
|
this.logger.profile(`diff status for source lane: ${sourceLaneId.name} and target lane: ${targetLaneId?.name}`);
|
|
1008
1187
|
return {
|
|
1009
1188
|
source: sourceLaneId,
|
|
@@ -1011,20 +1190,26 @@ please create a new lane instead, which will include all components of this lane
|
|
|
1011
1190
|
componentsStatus: results
|
|
1012
1191
|
};
|
|
1013
1192
|
}
|
|
1014
|
-
async componentDiffStatus(componentId, sourceHead, targetHead, snapsDistance, options) {
|
|
1193
|
+
async componentDiffStatus(componentId, sourceHead, targetHead, snapsDistance, options, baseSource) {
|
|
1015
1194
|
if (snapsDistance?.err) {
|
|
1016
1195
|
const noCommonSnap = snapsDistance.err instanceof _legacy().NoCommonSnap;
|
|
1017
1196
|
return {
|
|
1018
1197
|
componentId,
|
|
1019
1198
|
sourceHead,
|
|
1020
1199
|
targetHead,
|
|
1200
|
+
baseSource,
|
|
1021
1201
|
upToDate: snapsDistance?.isUpToDate(),
|
|
1022
1202
|
unrelated: noCommonSnap || undefined,
|
|
1023
1203
|
changes: []
|
|
1024
1204
|
};
|
|
1025
1205
|
}
|
|
1026
1206
|
const commonSnap = snapsDistance?.commonSnapBeforeDiverge;
|
|
1027
|
-
|
|
1207
|
+
|
|
1208
|
+
// when changes are skipped or deferred, return the snap-distance metadata immediately and leave
|
|
1209
|
+
// `changes` unset. callers derive them lazily through `deriveComponentChanges` (used by the GraphQL
|
|
1210
|
+
// `changes` field resolver, which runs the work per-component in parallel only when the client
|
|
1211
|
+
// selects the field — keeps cold p50 under 200 ms even for many components).
|
|
1212
|
+
const changes = options?.skipChanges || options?.deferChanges ? undefined : await this.deriveChangeTypes(commonSnap, componentId, sourceHead);
|
|
1028
1213
|
const changeType = changes ? changes[0] : undefined;
|
|
1029
1214
|
return {
|
|
1030
1215
|
componentId,
|
|
@@ -1032,70 +1217,103 @@ please create a new lane instead, which will include all components of this lane
|
|
|
1032
1217
|
changes,
|
|
1033
1218
|
sourceHead,
|
|
1034
1219
|
targetHead: commonSnap?.hash,
|
|
1220
|
+
baseSource,
|
|
1035
1221
|
upToDate: snapsDistance?.isUpToDate(),
|
|
1036
1222
|
snapsDistance: {
|
|
1037
1223
|
onSource: snapsDistance?.snapsOnSourceOnly.map(s => s.hash) ?? [],
|
|
1038
1224
|
onTarget: snapsDistance?.snapsOnTargetOnly.map(s => s.hash) ?? [],
|
|
1039
1225
|
common: snapsDistance?.commonSnapBeforeDiverge?.hash
|
|
1226
|
+
},
|
|
1227
|
+
changesContext: {
|
|
1228
|
+
commonSnap,
|
|
1229
|
+
skipped: options?.skipChanges
|
|
1040
1230
|
}
|
|
1041
1231
|
};
|
|
1042
1232
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Lazily derive the change types for a single component diff status. Used by the GraphQL `changes`
|
|
1236
|
+
* field resolver so the (expensive) derivation only runs when the field is selected. Memoized per
|
|
1237
|
+
* status object via `changesContext.pending`, so selecting both `changes` and `changeType` computes
|
|
1238
|
+
* the value once.
|
|
1239
|
+
*/
|
|
1240
|
+
async deriveComponentChanges(status) {
|
|
1241
|
+
if (status.changes) return status.changes;
|
|
1242
|
+
const context = status.changesContext;
|
|
1243
|
+
if (!context || context.skipped) return undefined;
|
|
1244
|
+
await this.laneDiffCache.ensureLoaded();
|
|
1245
|
+
|
|
1246
|
+
// top-level memo on the final ChangeType[] — short-circuits BEFORE compare()/getAPIDiff() run.
|
|
1247
|
+
// Persisted to disk, keyed on immutable hashes, so a cold server start with a populated cache
|
|
1248
|
+
// answers the entire derivation from a Map.get().
|
|
1249
|
+
const memoKey = this.laneDiffCache.changeTypesKey(status.componentId, status.sourceHead, context.commonSnap?.hash ?? null);
|
|
1250
|
+
const cached = this.laneDiffCache.getChangeTypes(memoKey);
|
|
1251
|
+
if (cached) return cached;
|
|
1252
|
+
|
|
1253
|
+
// single-flight concurrent requests for the same (componentId, sourceHead, commonSnap) tuple.
|
|
1254
|
+
let pending = this.changeTypesInflight.get(memoKey);
|
|
1255
|
+
if (!pending) {
|
|
1256
|
+
pending = this.deriveChangesLimit(() => this.deriveChangeTypes(context.commonSnap, status.componentId, status.sourceHead)).then(result => {
|
|
1257
|
+
// `undefined` means the derivation couldn't load a Version object (transient) — don't persist
|
|
1258
|
+
// it under the immutable-hash key; let it recompute on a later request once the object exists.
|
|
1259
|
+
if (result) this.laneDiffCache.storeChangeTypes(memoKey, result);
|
|
1260
|
+
return result;
|
|
1261
|
+
}).finally(() => {
|
|
1262
|
+
this.changeTypesInflight.delete(memoKey);
|
|
1263
|
+
});
|
|
1264
|
+
this.changeTypesInflight.set(memoKey, pending);
|
|
1055
1265
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const changeType = changes ? changes[0] : undefined;
|
|
1059
|
-
return {
|
|
1060
|
-
componentId,
|
|
1061
|
-
changeType,
|
|
1062
|
-
changes,
|
|
1063
|
-
sourceHead,
|
|
1064
|
-
targetHead: commonSnap?.hash,
|
|
1065
|
-
upToDate: snapsDistance?.isUpToDate(),
|
|
1066
|
-
snapsDistance: {
|
|
1067
|
-
onSource: snapsDistance?.snapsOnSourceOnly.map(s => s.hash) ?? [],
|
|
1068
|
-
onTarget: snapsDistance?.snapsOnTargetOnly.map(s => s.hash) ?? [],
|
|
1069
|
-
common: snapsDistance?.commonSnapBeforeDiverge?.hash
|
|
1070
|
-
}
|
|
1071
|
-
};
|
|
1266
|
+
context.pending = pending;
|
|
1267
|
+
return pending;
|
|
1072
1268
|
}
|
|
1073
1269
|
async deriveChangeTypes(commonSnap, componentId, sourceHead) {
|
|
1074
1270
|
if (!commonSnap) return [_lanesEntities().ChangeType.NEW];
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1271
|
+
|
|
1272
|
+
// Classify changes by comparing the two raw `Version` objects directly (files-by-hash, deps,
|
|
1273
|
+
// extensions) instead of `componentCompare.compare()`. compare() loads full consumer components
|
|
1274
|
+
// (aspect calculation, capsules) for both sides just to extract two booleans — dominating the cold
|
|
1275
|
+
// derivation for a large lane. Loading the raw Version objects is a cheap object read.
|
|
1276
|
+
// The API change type is intentionally not computed here: it needs serial tsserver schema
|
|
1277
|
+
// extraction and is resolved lazily by the API view, gated on the SOURCE_CODE/DEPENDENCY evidence.
|
|
1278
|
+
const repo = this.scope.legacyScope.objects;
|
|
1279
|
+
const loadPair = () => Promise.all([repo.load(_objects().Ref.from(commonSnap.hash), false), repo.load(_objects().Ref.from(sourceHead), false)]);
|
|
1280
|
+
let [baseVersion, compareVersion] = await loadPair();
|
|
1281
|
+
|
|
1282
|
+
// the deferChanges path (the default for UI lane compares) skips the eager common-snap import, so
|
|
1283
|
+
// the merge-base Version can be absent locally here. rather than give up — which classifies a
|
|
1284
|
+
// genuinely-changed component as "couldn't classify" and drops it from the diff views — import the
|
|
1285
|
+
// missing snap(s) on demand and retry once. this fires only on a miss and is per-component, so the
|
|
1286
|
+
// fast deferred path stays fast while staying correct; the derived result is memoized afterwards.
|
|
1287
|
+
if (!baseVersion || !compareVersion) {
|
|
1288
|
+
const missing = [];
|
|
1289
|
+
if (!baseVersion) missing.push(componentId.changeVersion(commonSnap.hash));
|
|
1290
|
+
if (!compareVersion) missing.push(componentId.changeVersion(sourceHead));
|
|
1291
|
+
try {
|
|
1292
|
+
await this.scope.legacyScope.scopeImporter.importWithoutDeps(_componentId().ComponentIdList.fromArray(missing), {
|
|
1293
|
+
cache: true,
|
|
1294
|
+
ignoreMissingHead: true,
|
|
1295
|
+
reason: `derive lane diff change types (snap missing locally)`
|
|
1296
|
+
});
|
|
1297
|
+
[baseVersion, compareVersion] = await loadPair();
|
|
1298
|
+
} catch {
|
|
1299
|
+
// best-effort: if the import fails (e.g. the snap only exists on a lane we lack context for here),
|
|
1300
|
+
// fall through to the undefined return below rather than throwing out of the field resolver.
|
|
1301
|
+
}
|
|
1097
1302
|
}
|
|
1098
|
-
|
|
1303
|
+
|
|
1304
|
+
// still couldn't load a Version. this is a transient "couldn't classify", NOT "no changes"
|
|
1305
|
+
// ([ChangeType.NONE]) — return undefined so it's distinguishable and never persisted to the
|
|
1306
|
+
// immutable-hash-keyed memo, or the component would be reported with no change types forever.
|
|
1307
|
+
if (!baseVersion || !compareVersion) return undefined;
|
|
1308
|
+
|
|
1309
|
+
// pure classification of the two raw Version shapes — extracted & unit-tested in lanes/diff.
|
|
1310
|
+
return (0, _lanesModules().classifyVersionChanges)({
|
|
1311
|
+
obj: baseVersion.toObject(),
|
|
1312
|
+
extensionDependencies: baseVersion.extensionDependencies.cloneAsString()
|
|
1313
|
+
}, {
|
|
1314
|
+
obj: compareVersion.toObject(),
|
|
1315
|
+
extensionDependencies: compareVersion.extensionDependencies.cloneAsString()
|
|
1316
|
+
});
|
|
1099
1317
|
}
|
|
1100
1318
|
async recreateNewLaneIfDeleted() {
|
|
1101
1319
|
if (!this.workspace) return;
|