@gitkraken/core-gitlens 0.4.101 → 0.5.101
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/CHANGELOG.md +29 -6
- package/dist/git/models/graph.d.ts +63 -33
- package/dist/git/models/graph.d.ts.map +1 -1
- package/dist/git/models/graph.js.map +1 -1
- package/dist/git/models/graphSession.d.ts +3 -84
- package/dist/git/models/graphSession.d.ts.map +1 -1
- package/dist/git/models/graphSession.js +1 -6
- package/dist/git/models/graphSession.js.map +1 -1
- package/dist/git/models/issue.d.ts +4 -3
- package/dist/git/models/issue.d.ts.map +1 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/providers/graph.d.ts +1 -15
- package/dist/git/providers/graph.d.ts.map +1 -1
- package/dist/git/providers/pausedOperations.d.ts +2 -0
- package/dist/git/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git/utils/issue.utils.d.ts.map +1 -1
- package/dist/git/utils/issue.utils.js +8 -6
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/reachability.utils.d.ts +1 -1
- package/dist/git/utils/reachability.utils.js +1 -1
- package/dist/git/utils/remote.utils.d.ts +3 -0
- package/dist/git/utils/remote.utils.d.ts.map +1 -1
- package/dist/git/utils/remote.utils.js +5 -0
- package/dist/git/utils/remote.utils.js.map +1 -1
- package/dist/git-cli/providers/graph.d.ts +1 -3
- package/dist/git-cli/providers/graph.d.ts.map +1 -1
- package/dist/git-cli/providers/graph.js +37 -214
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
- package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git-cli/providers/pausedOperations.js +21 -1
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/staging.d.ts.map +1 -1
- package/dist/git-cli/providers/staging.js +10 -1
- package/dist/git-cli/providers/staging.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +18 -5
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +6 -4
- package/dist/plus/git-github/models.d.ts.map +1 -1
- package/dist/plus/git-github/models.js +19 -26
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/git-github/providers/github/graph.d.ts +1 -3
- package/dist/plus/git-github/providers/github/graph.d.ts.map +1 -1
- package/dist/plus/git-github/providers/github/graph.js +8 -10
- package/dist/plus/git-github/providers/github/graph.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts +38 -48
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.d.ts.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +22 -5
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
- package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +39 -0
- package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts.map +1 -0
- package/dist/plus/integrations/authentication/rejectedTokenTracker.js +53 -0
- package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -0
- package/dist/plus/integrations/collectionMetadata.d.ts +21 -1
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +82 -6
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +3 -3
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +19 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +19 -6
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +20 -0
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js +64 -6
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js +5 -5
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +1 -0
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
- package/dist/plus/integrations/providers/providersApi.js +4 -2
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.d.ts +2 -0
- package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -1
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +4 -2
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +34 -22
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +4 -2
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +4 -2
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/utils/date.d.ts +1 -1
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +5 -3
- package/dist/utils/date.js.map +1 -1
- package/package.json +2 -2
- package/src/git/models/graph.ts +59 -41
- package/src/git/models/graphSession.ts +3 -106
- package/src/git/models/issue.ts +3 -2
- package/src/git/providers/graph.ts +1 -15
- package/src/git/providers/pausedOperations.ts +8 -1
- package/src/git/utils/issue.utils.ts +9 -6
- package/src/git/utils/reachability.utils.ts +1 -1
- package/src/git/utils/remote.utils.ts +6 -0
- package/src/git-cli/providers/graph.ts +37 -250
- package/src/git-cli/providers/pausedOperations.ts +29 -2
- package/src/git-cli/providers/staging.ts +8 -1
- package/src/plus/git-github/api/github.ts +19 -9
- package/src/plus/git-github/models.ts +27 -31
- package/src/plus/git-github/providers/github/graph.ts +8 -15
- package/src/plus/integrations/authentication/integrationAuthenticationProvider.ts +62 -40
- package/src/plus/integrations/authentication/rejectedTokenTracker.ts +56 -0
- package/src/plus/integrations/collectionMetadata.ts +97 -7
- package/src/plus/integrations/integrationService.ts +37 -9
- package/src/plus/integrations/models/gitHostIntegration.ts +19 -6
- package/src/plus/integrations/models/integration.ts +71 -7
- package/src/plus/integrations/models/issuesIntegration.ts +5 -5
- package/src/plus/integrations/providers/models.ts +4 -1
- package/src/plus/integrations/providers/providersApi.ts +6 -10
- package/src/plus/integrations/providers/trello.ts +3 -2
- package/src/plus/integrations/providers/utils/providerPaging.ts +46 -24
- package/src/plus/integrations/reads/warnings.ts +4 -2
- package/src/utils/date.ts +6 -3
|
@@ -555,7 +555,7 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
555
555
|
async getMyIssues(
|
|
556
556
|
integrationIds?: (GitCloudHostIntegrationId | IssuesCloudHostIntegrationId | GitSelfManagedHostIntegrationId)[],
|
|
557
557
|
options?: { openRepositoriesOnly?: boolean; cancellation?: AbortSignal },
|
|
558
|
-
): Promise<IssueShape[] | undefined
|
|
558
|
+
): Promise<IntegrationResult<IssueShape[] | undefined>> {
|
|
559
559
|
const integrations: Map<Integration, ResourceDescriptor[] | undefined> = new Map();
|
|
560
560
|
const hostingIntegrationIds = integrationIds?.filter(
|
|
561
561
|
id => id in GitCloudHostIntegrationId || id in GitSelfManagedHostIntegrationId,
|
|
@@ -623,26 +623,54 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
623
623
|
private async getMyIssuesCore(
|
|
624
624
|
integrations: Map<Integration, ResourceDescriptor[] | undefined>,
|
|
625
625
|
cancellation?: AbortSignal,
|
|
626
|
-
): Promise<IssueShape[] | undefined
|
|
627
|
-
const
|
|
626
|
+
): Promise<IntegrationResult<IssueShape[] | undefined>> {
|
|
627
|
+
const start = performance.now();
|
|
628
|
+
|
|
629
|
+
const promises: Promise<IntegrationResult<IssueShape[] | undefined>>[] = [];
|
|
628
630
|
for (const [integration, repos] of integrations) {
|
|
629
631
|
if (integration == null) continue;
|
|
630
632
|
|
|
631
|
-
promises.push(integration.
|
|
633
|
+
promises.push(integration.searchMyIssuesResult(repos, cancellation));
|
|
632
634
|
}
|
|
633
635
|
|
|
634
636
|
const results = await Promise.allSettled(promises);
|
|
635
|
-
|
|
637
|
+
const successfulResults = [
|
|
638
|
+
...flatten(
|
|
639
|
+
filterMap(results, r =>
|
|
640
|
+
r.status === 'fulfilled' && r.value?.value != null ? r.value.value : undefined,
|
|
641
|
+
),
|
|
642
|
+
),
|
|
643
|
+
];
|
|
644
|
+
const errors = [
|
|
645
|
+
...filterMap(results, r =>
|
|
646
|
+
r.status === 'fulfilled' && r.value?.error != null ? r.value.error : undefined,
|
|
647
|
+
),
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
const error =
|
|
651
|
+
errors.length === 0
|
|
652
|
+
? undefined
|
|
653
|
+
: errors.length === 1
|
|
654
|
+
? errors[0]
|
|
655
|
+
: new AggregateError(errors, 'Failed to get some issues');
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
value: successfulResults,
|
|
659
|
+
error: error,
|
|
660
|
+
duration: performance.now() - start,
|
|
661
|
+
};
|
|
636
662
|
}
|
|
637
663
|
|
|
638
|
-
async getMyIssuesForRemotes(remote: GitRemote): Promise<IssueShape[] | undefined
|
|
639
|
-
async getMyIssuesForRemotes(remotes: GitRemote[]): Promise<IssueShape[] | undefined
|
|
664
|
+
async getMyIssuesForRemotes(remote: GitRemote): Promise<IntegrationResult<IssueShape[] | undefined>>;
|
|
665
|
+
async getMyIssuesForRemotes(remotes: GitRemote[]): Promise<IntegrationResult<IssueShape[] | undefined>>;
|
|
640
666
|
@trace({
|
|
641
667
|
args: (remoteOrRemotes: GitRemote | GitRemote[]) => ({
|
|
642
668
|
remoteOrRemotes: Array.isArray(remoteOrRemotes) ? remoteOrRemotes.map(rp => rp.name) : remoteOrRemotes.name,
|
|
643
669
|
}),
|
|
644
670
|
})
|
|
645
|
-
async getMyIssuesForRemotes(
|
|
671
|
+
async getMyIssuesForRemotes(
|
|
672
|
+
remoteOrRemotes: GitRemote | GitRemote[],
|
|
673
|
+
): Promise<IntegrationResult<IssueShape[] | undefined>> {
|
|
646
674
|
if (!Array.isArray(remoteOrRemotes)) {
|
|
647
675
|
remoteOrRemotes = [remoteOrRemotes];
|
|
648
676
|
}
|
|
@@ -653,7 +681,7 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
|
|
|
653
681
|
if (remote?.provider == null) return undefined;
|
|
654
682
|
|
|
655
683
|
const integration = await this.getByRemote(remote);
|
|
656
|
-
return integration?.
|
|
684
|
+
return integration?.searchMyIssuesResult(remote.provider.repoDesc);
|
|
657
685
|
}
|
|
658
686
|
|
|
659
687
|
const integrations = new Map<GitHostIntegration, ResourceDescriptor[]>();
|
|
@@ -373,7 +373,10 @@ export abstract class GitHostIntegration<
|
|
|
373
373
|
this.resetRequestExceptionCount('getOrganizationsForUser');
|
|
374
374
|
return { value: result, duration: performance.now() - start };
|
|
375
375
|
} catch (ex) {
|
|
376
|
-
this.handleProviderException('getOrganizationsForUser', ex, {
|
|
376
|
+
this.handleProviderException('getOrganizationsForUser', ex, {
|
|
377
|
+
scope: scope,
|
|
378
|
+
connectionId: connectionId,
|
|
379
|
+
});
|
|
377
380
|
return { error: toError(ex), duration: performance.now() - start };
|
|
378
381
|
}
|
|
379
382
|
}
|
|
@@ -409,7 +412,7 @@ export abstract class GitHostIntegration<
|
|
|
409
412
|
this.resetRequestExceptionCount('getProjectsForOrg');
|
|
410
413
|
return { value: result, duration: performance.now() - start };
|
|
411
414
|
} catch (ex) {
|
|
412
|
-
this.handleProviderException('getProjectsForOrg', ex, { scope: scope });
|
|
415
|
+
this.handleProviderException('getProjectsForOrg', ex, { scope: scope, connectionId: connectionId });
|
|
413
416
|
return { error: toError(ex), duration: performance.now() - start };
|
|
414
417
|
}
|
|
415
418
|
}
|
|
@@ -446,7 +449,10 @@ export abstract class GitHostIntegration<
|
|
|
446
449
|
this.resetRequestExceptionCount('getRepositoriesForOrg');
|
|
447
450
|
return { value: result, duration: performance.now() - start };
|
|
448
451
|
} catch (ex) {
|
|
449
|
-
this.handleProviderException('getRepositoriesForOrg', ex, {
|
|
452
|
+
this.handleProviderException('getRepositoriesForOrg', ex, {
|
|
453
|
+
scope: scope,
|
|
454
|
+
connectionId: options?.connectionId,
|
|
455
|
+
});
|
|
450
456
|
return { error: toError(ex), duration: performance.now() - start };
|
|
451
457
|
}
|
|
452
458
|
}
|
|
@@ -484,7 +490,10 @@ export abstract class GitHostIntegration<
|
|
|
484
490
|
this.resetRequestExceptionCount('getRepositoriesForUser');
|
|
485
491
|
return { value: result, duration: performance.now() - start };
|
|
486
492
|
} catch (ex) {
|
|
487
|
-
this.handleProviderException('getRepositoriesForUser', ex, {
|
|
493
|
+
this.handleProviderException('getRepositoriesForUser', ex, {
|
|
494
|
+
scope: scope,
|
|
495
|
+
connectionId: options?.connectionId,
|
|
496
|
+
});
|
|
488
497
|
return { error: toError(ex), duration: performance.now() - start };
|
|
489
498
|
}
|
|
490
499
|
}
|
|
@@ -1414,6 +1423,7 @@ export abstract class GitHostIntegration<
|
|
|
1414
1423
|
this.handleProviderException('searchMyPullRequests', ex, {
|
|
1415
1424
|
scope: scope,
|
|
1416
1425
|
silent: true,
|
|
1426
|
+
connectionId: connectionId,
|
|
1417
1427
|
});
|
|
1418
1428
|
return {
|
|
1419
1429
|
error: toError(ex),
|
|
@@ -1456,7 +1466,10 @@ export abstract class GitHostIntegration<
|
|
|
1456
1466
|
this.resetRequestExceptionCount('getMyPullRequestsForUser');
|
|
1457
1467
|
return { value: result, duration: performance.now() - start };
|
|
1458
1468
|
} catch (ex) {
|
|
1459
|
-
this.handleProviderException('getMyPullRequestsForUser', ex, {
|
|
1469
|
+
this.handleProviderException('getMyPullRequestsForUser', ex, {
|
|
1470
|
+
scope: scope,
|
|
1471
|
+
connectionId: connectionId,
|
|
1472
|
+
});
|
|
1460
1473
|
return { error: toError(ex), duration: performance.now() - start };
|
|
1461
1474
|
}
|
|
1462
1475
|
}
|
|
@@ -1588,7 +1601,7 @@ export abstract class GitHostIntegration<
|
|
|
1588
1601
|
this.resetRequestExceptionCount('searchPullRequests');
|
|
1589
1602
|
return prs;
|
|
1590
1603
|
} catch (ex) {
|
|
1591
|
-
this.handleProviderException('searchPullRequests', ex, { scope: scope });
|
|
1604
|
+
this.handleProviderException('searchPullRequests', ex, { scope: scope, connectionId: connectionId });
|
|
1592
1605
|
return undefined;
|
|
1593
1606
|
}
|
|
1594
1607
|
}
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from '../authentication/integrationAuthenticationProvider.js';
|
|
21
21
|
import type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';
|
|
22
22
|
import type { ProviderAuthenticationSession } from '../authentication/models.js';
|
|
23
|
+
import { RejectedTokenTracker } from '../authentication/rejectedTokenTracker.js';
|
|
23
24
|
import type { IntegrationIds, IssuesCloudHostIntegrationId } from '../constants.js';
|
|
24
25
|
import { GitCloudHostIntegrationId } from '../constants.js';
|
|
25
26
|
import type { IntegrationServiceContext } from '../context.js';
|
|
@@ -240,13 +241,18 @@ export abstract class IntegrationBase<
|
|
|
240
241
|
// A truthy connectionId targets a specific account; an empty string is not a real target, so it falls
|
|
241
242
|
// through to the primary path below.
|
|
242
243
|
if (connectionId) {
|
|
244
|
+
// A read of this connection previously failed with an AuthenticationError, so the token in
|
|
245
|
+
// storage is known-refused. Ask for a refresh through the GK cloud before reading again, which
|
|
246
|
+
// is this branch's equivalent of the primary path's `refreshSessionIfExpired`. Claimed here so
|
|
247
|
+
// the forced refresh runs at most once per rejection.
|
|
248
|
+
const refreshRejectedToken = this._rejectedTokens.claimRefresh(connectionId);
|
|
243
249
|
// Degrade to "no results" on failure, matching the primary path (whose ensureSession/
|
|
244
250
|
// refreshSessionIfExpired swallow errors) so read methods keep their never-throws contract.
|
|
245
251
|
try {
|
|
246
252
|
const authProvider = await this.authenticationService.get(this.authProvider.id);
|
|
247
253
|
const session = await authProvider.getSession(
|
|
248
254
|
{ ...this.authProviderDescriptor, connectionId: connectionId, cloud: true },
|
|
249
|
-
{ source: source },
|
|
255
|
+
{ source: source, refreshRejectedToken: refreshRejectedToken },
|
|
250
256
|
);
|
|
251
257
|
return session != null && this.isSessionForIntegrationHost(session) ? session : undefined;
|
|
252
258
|
} catch (ex) {
|
|
@@ -326,6 +332,10 @@ export abstract class IntegrationBase<
|
|
|
326
332
|
|
|
327
333
|
@debug()
|
|
328
334
|
async reauthenticate(): Promise<void> {
|
|
335
|
+
// `forceNewSession` below deletes the stored secrets to reconnect. Ahead of the guard — see
|
|
336
|
+
// `onStoredTokensReplaced`.
|
|
337
|
+
this.onStoredTokensReplaced();
|
|
338
|
+
|
|
329
339
|
if (this._session === undefined) return;
|
|
330
340
|
|
|
331
341
|
this._session = undefined;
|
|
@@ -343,6 +353,30 @@ export abstract class IntegrationBase<
|
|
|
343
353
|
requestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void {
|
|
344
354
|
this._syncRequestsPerFailedUsecase.add(syncReqUsecase);
|
|
345
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The per-connection counterpart of the primary session's expire-and-resync recovery: a token the
|
|
359
|
+
* provider refused is refreshed once through the GK cloud, which exchanges the refresh token server-side
|
|
360
|
+
* (the client never holds one). Armed by {@link handleProviderException} and consumed by
|
|
361
|
+
* {@link resolveReadSession}; a rejection it declines belongs to {@link trackRequestException} instead.
|
|
362
|
+
*/
|
|
363
|
+
private readonly _rejectedTokens = new RejectedTokenTracker();
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Called by every path that replaces or removes the stored tokens — a disconnect, a forced re-sync, a
|
|
367
|
+
* reauthentication, or the connection set changing. A rejection names a specific credential, so once that
|
|
368
|
+
* credential is gone the rejection describes nothing and must not force a refresh (or, for a deleted
|
|
369
|
+
* connection, outlive it).
|
|
370
|
+
*
|
|
371
|
+
* Deliberately NOT conditioned on `_session`. A per-connection read resolves through the auth provider and
|
|
372
|
+
* never populates the cached primary session, so the connections this recovery exists for are exactly the
|
|
373
|
+
* ones with no `_session` to inspect — the same blind spot the recovery itself was added to fix. Callers
|
|
374
|
+
* that guard on `_session` therefore invoke this ahead of that guard.
|
|
375
|
+
*/
|
|
376
|
+
protected onStoredTokensReplaced(): void {
|
|
377
|
+
this._rejectedTokens.clear();
|
|
378
|
+
}
|
|
379
|
+
|
|
346
380
|
private static readonly requestExceptionLimit = 5;
|
|
347
381
|
private requestExceptionCount = 0;
|
|
348
382
|
|
|
@@ -350,6 +384,8 @@ export abstract class IntegrationBase<
|
|
|
350
384
|
this.requestExceptionCount = 0;
|
|
351
385
|
if (syncReqUsecase === 'all') {
|
|
352
386
|
this._syncRequestsPerFailedUsecase.clear();
|
|
387
|
+
// 'all' is the whole-integration reset: a disconnect, or a re-sync that produced a new access token.
|
|
388
|
+
this.onStoredTokensReplaced();
|
|
353
389
|
} else {
|
|
354
390
|
this._syncRequestsPerFailedUsecase.delete(syncReqUsecase);
|
|
355
391
|
}
|
|
@@ -374,6 +410,10 @@ export abstract class IntegrationBase<
|
|
|
374
410
|
* `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.
|
|
375
411
|
*/
|
|
376
412
|
switchConnection(): void {
|
|
413
|
+
// A connection was deleted, or a different one became primary. Ahead of the guard — see
|
|
414
|
+
// `onStoredTokensReplaced`.
|
|
415
|
+
this.onStoredTokensReplaced();
|
|
416
|
+
|
|
377
417
|
if (this._session === undefined) return;
|
|
378
418
|
|
|
379
419
|
const wasConnected = this._session != null;
|
|
@@ -419,6 +459,9 @@ export abstract class IntegrationBase<
|
|
|
419
459
|
// Reset our stored session so that we get a new one from the cloud
|
|
420
460
|
const authProvider = await this.authenticationService.get(this.authProvider.id);
|
|
421
461
|
await authProvider.deleteSession(this.authProviderDescriptor);
|
|
462
|
+
// The stored token was just deleted. Not left to the token-changed check below, which needs
|
|
463
|
+
// an `oldSession` — see `onStoredTokensReplaced`.
|
|
464
|
+
this.onStoredTokensReplaced();
|
|
422
465
|
// Reset the session and clear our "stay disconnected" flag
|
|
423
466
|
this._session = undefined;
|
|
424
467
|
await this.ctx.storage.deleteWorkspace(this.connectedKey);
|
|
@@ -474,12 +517,29 @@ export abstract class IntegrationBase<
|
|
|
474
517
|
protected handleProviderException(
|
|
475
518
|
syncReqUsecase: SyncReqUsecase,
|
|
476
519
|
ex: Error,
|
|
477
|
-
options?: { scope?: ScopedLogger | undefined; silent?: boolean },
|
|
520
|
+
options?: { scope?: ScopedLogger | undefined; silent?: boolean; connectionId?: string },
|
|
478
521
|
): void {
|
|
479
522
|
if (isCancellationError(ex)) return;
|
|
480
523
|
|
|
481
524
|
options?.scope?.error(ex);
|
|
482
525
|
|
|
526
|
+
// A per-connection (multi-account) read resolved its session through `resolveReadSession`'s
|
|
527
|
+
// `connectionId` branch, which deliberately never touches the cached primary `_session`. So the
|
|
528
|
+
// primary-session recovery below cannot apply to it: expiring `_session` would mark a session this
|
|
529
|
+
// read never used, while the rejected connection kept its stored token and re-sent it on every
|
|
530
|
+
// later read — a token the provider has already refused (expired scopes, a revoked grant, an
|
|
531
|
+
// uninstalled app) is not self-healing, so the read failed identically until the user reconnected
|
|
532
|
+
// by hand. Record the rejection against the connection instead; `resolveReadSession` consumes it
|
|
533
|
+
// and forces the cloud `/refresh` on the next read of that connection. When there is nothing to
|
|
534
|
+
// recover, fall through to the shared failure budget, which disconnects after
|
|
535
|
+
// `requestExceptionLimit` and surfaces the reconnect prompt.
|
|
536
|
+
if (ex instanceof AuthenticationError && options?.connectionId) {
|
|
537
|
+
if (!this._rejectedTokens.recordRejection(options.connectionId)) {
|
|
538
|
+
this.trackRequestException(options);
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
483
543
|
if (ex instanceof AuthenticationError && this._session?.cloud) {
|
|
484
544
|
if (!this.hasSessionSyncRequests()) {
|
|
485
545
|
this.requestSessionSyncForUsecase(syncReqUsecase);
|
|
@@ -677,6 +737,7 @@ export abstract class IntegrationBase<
|
|
|
677
737
|
const session = await this.resolveReadSession(connectionId, scope);
|
|
678
738
|
if (session == null) return undefined;
|
|
679
739
|
|
|
740
|
+
const start = performance.now();
|
|
680
741
|
try {
|
|
681
742
|
const issues = await this.searchProviderMyIssues(
|
|
682
743
|
session,
|
|
@@ -684,10 +745,10 @@ export abstract class IntegrationBase<
|
|
|
684
745
|
cancellation,
|
|
685
746
|
);
|
|
686
747
|
this.resetRequestExceptionCount('searchMyIssues');
|
|
687
|
-
return { value: issues };
|
|
748
|
+
return { value: issues, duration: performance.now() - start };
|
|
688
749
|
} catch (ex) {
|
|
689
|
-
this.handleProviderException('searchMyIssues', ex, { scope: scope });
|
|
690
|
-
return { error: toError(ex) };
|
|
750
|
+
this.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });
|
|
751
|
+
return { error: toError(ex), duration: performance.now() - start };
|
|
691
752
|
}
|
|
692
753
|
}
|
|
693
754
|
|
|
@@ -738,7 +799,7 @@ export abstract class IntegrationBase<
|
|
|
738
799
|
this.resetRequestExceptionCount('searchMyIssues');
|
|
739
800
|
return { value: result };
|
|
740
801
|
} catch (ex) {
|
|
741
|
-
this.handleProviderException('searchMyIssues', ex, { scope: scope });
|
|
802
|
+
this.handleProviderException('searchMyIssues', ex, { scope: scope, connectionId: connectionId });
|
|
742
803
|
return { error: toError(ex) };
|
|
743
804
|
}
|
|
744
805
|
}
|
|
@@ -858,7 +919,10 @@ export abstract class IntegrationBase<
|
|
|
858
919
|
return undefined;
|
|
859
920
|
}
|
|
860
921
|
|
|
861
|
-
this.handleProviderException('getCurrentAccount', ex, {
|
|
922
|
+
this.handleProviderException('getCurrentAccount', ex, {
|
|
923
|
+
scope: scope,
|
|
924
|
+
connectionId: connectionId,
|
|
925
|
+
});
|
|
862
926
|
|
|
863
927
|
// Invalidate the cache on error, except for auth errors
|
|
864
928
|
if (!(ex instanceof AuthenticationError)) {
|
|
@@ -48,7 +48,7 @@ export abstract class IssuesIntegration<
|
|
|
48
48
|
this.resetRequestExceptionCount('getAccountForResource');
|
|
49
49
|
return { value: account };
|
|
50
50
|
} catch (ex) {
|
|
51
|
-
this.handleProviderException('getAccountForResource', ex);
|
|
51
|
+
this.handleProviderException('getAccountForResource', ex, { connectionId: connectionId });
|
|
52
52
|
return { error: toError(ex) };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -80,7 +80,7 @@ export abstract class IssuesIntegration<
|
|
|
80
80
|
this.resetRequestExceptionCount('getResourcesForUser');
|
|
81
81
|
return { value: resources };
|
|
82
82
|
} catch (ex) {
|
|
83
|
-
this.handleProviderException('getResourcesForUser', ex);
|
|
83
|
+
this.handleProviderException('getResourcesForUser', ex, { connectionId: connectionId });
|
|
84
84
|
return { error: toError(ex) };
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -108,7 +108,7 @@ export abstract class IssuesIntegration<
|
|
|
108
108
|
this.resetRequestExceptionCount('getProjectsForResources');
|
|
109
109
|
return { value: projects };
|
|
110
110
|
} catch (ex) {
|
|
111
|
-
this.handleProviderException('getProjectsForResources', ex);
|
|
111
|
+
this.handleProviderException('getProjectsForResources', ex, { connectionId: connectionId });
|
|
112
112
|
return { error: toError(ex) };
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -161,7 +161,7 @@ export abstract class IssuesIntegration<
|
|
|
161
161
|
this.resetRequestExceptionCount('getIssuesForProject');
|
|
162
162
|
return { value: issues };
|
|
163
163
|
} catch (ex) {
|
|
164
|
-
this.handleProviderException('getIssuesForProject', ex);
|
|
164
|
+
this.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });
|
|
165
165
|
return { error: toError(ex) };
|
|
166
166
|
}
|
|
167
167
|
}
|
|
@@ -188,7 +188,7 @@ export abstract class IssuesIntegration<
|
|
|
188
188
|
this.resetRequestExceptionCount('getIssuesForProject');
|
|
189
189
|
return { value: result };
|
|
190
190
|
} catch (ex) {
|
|
191
|
-
this.handleProviderException('getIssuesForProject', ex);
|
|
191
|
+
this.handleProviderException('getIssuesForProject', ex, { connectionId: connectionId });
|
|
192
192
|
return { error: toError(ex) };
|
|
193
193
|
}
|
|
194
194
|
}
|
|
@@ -470,7 +470,10 @@ export type GetIssueFn = (
|
|
|
470
470
|
export type GetIssuesForReposFn = (
|
|
471
471
|
input: (GetIssuesForReposInput | GetIssuesForRepoIdsInput) & PagingInput,
|
|
472
472
|
options?: EnterpriseOptions,
|
|
473
|
-
|
|
473
|
+
// GitHub's multi-repo issue search reports its 1,000-result cap — and, when it recovers past it by
|
|
474
|
+
// partitioning, what that recovery could not reach — through `metadata`. Documented on the contract for
|
|
475
|
+
// the reader; `getPagedResult` accepts and forwards `metadata` from any provider fn regardless.
|
|
476
|
+
) => Promise<{ data: ProviderIssue[]; pageInfo?: PageInfo; metadata?: CollectionMetadata }>;
|
|
474
477
|
|
|
475
478
|
export type GetIssuesForCurrentUserInput = PagingInput & {
|
|
476
479
|
// GitLab's account-wide REST read (`GET /issues`): `scope` controls breadth ('assigned_to_me' vs 'all') and
|
|
@@ -13,6 +13,7 @@ import type { PagedResult } from '../../../utils/paging.js';
|
|
|
13
13
|
import type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';
|
|
14
14
|
import type { TokenOptInfo, TokenWithInfo } from '../authentication/models.js';
|
|
15
15
|
import { toTokenWithInfo } from '../authentication/models.js';
|
|
16
|
+
import { isIncompleteCollection } from '../collectionMetadata.js';
|
|
16
17
|
import type { IntegrationIds } from '../constants.js';
|
|
17
18
|
import {
|
|
18
19
|
GitCloudHostIntegrationId,
|
|
@@ -32,7 +33,6 @@ import type {
|
|
|
32
33
|
GetIssueFn,
|
|
33
34
|
GetIssuesForReposFn,
|
|
34
35
|
GetIssuesOptions,
|
|
35
|
-
GetPullRequestsForRepoFn,
|
|
36
36
|
GetPullRequestsForReposFn,
|
|
37
37
|
GetPullRequestsForUserFn,
|
|
38
38
|
GetPullRequestsForUserOptions,
|
|
@@ -275,9 +275,7 @@ export class ProvidersApi {
|
|
|
275
275
|
getPullRequestsForReposFn: providerApis.gitlab.getPullRequestsForRepos.bind(
|
|
276
276
|
providerApis.gitlab,
|
|
277
277
|
) as GetPullRequestsForReposFn,
|
|
278
|
-
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(
|
|
279
|
-
providerApis.gitlab,
|
|
280
|
-
) as GetPullRequestsForRepoFn,
|
|
278
|
+
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(providerApis.gitlab),
|
|
281
279
|
getPullRequestsForUserFn: providerApis.gitlab.getPullRequestsAssociatedWithUser.bind(
|
|
282
280
|
providerApis.gitlab,
|
|
283
281
|
) as GetPullRequestsForUserFn,
|
|
@@ -302,9 +300,7 @@ export class ProvidersApi {
|
|
|
302
300
|
getPullRequestsForReposFn: providerApis.gitlab.getPullRequestsForRepos.bind(
|
|
303
301
|
providerApis.gitlab,
|
|
304
302
|
) as GetPullRequestsForReposFn,
|
|
305
|
-
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(
|
|
306
|
-
providerApis.gitlab,
|
|
307
|
-
) as GetPullRequestsForRepoFn,
|
|
303
|
+
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(providerApis.gitlab),
|
|
308
304
|
getPullRequestsForUserFn: providerApis.gitlab.getPullRequestsAssociatedWithUser.bind(
|
|
309
305
|
providerApis.gitlab,
|
|
310
306
|
) as GetPullRequestsForUserFn,
|
|
@@ -681,9 +677,9 @@ export class ProvidersApi {
|
|
|
681
677
|
// SDK collection completeness is independent from provider-native pagination: a result can expose a
|
|
682
678
|
// real next page (`more`) and still have a failed sibling scope (`partial`/`unknown`). Surface the
|
|
683
679
|
// latter as `truncated` so consumers treat the page as incomplete. Absent metadata (old providers,
|
|
684
|
-
// test doubles) leaves `truncated` unset for backward compatibility.
|
|
685
|
-
|
|
686
|
-
|
|
680
|
+
// test doubles) leaves `truncated` unset for backward compatibility. Shares the incompleteness
|
|
681
|
+
// predicate with the facade assessment so one metadata object can't be truncated there and whole here.
|
|
682
|
+
const truncated = isIncompleteCollection(normalizedMetadata) ? true : undefined;
|
|
687
683
|
|
|
688
684
|
return {
|
|
689
685
|
values: result.data,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CollectionMetadata } from '@gitkraken/provider-apis';
|
|
1
2
|
import type { Account } from '../../../git/models/author.js';
|
|
2
3
|
import type { Issue, IssueShape } from '../../../git/models/issue.js';
|
|
3
4
|
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest.js';
|
|
@@ -103,7 +104,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
103
104
|
session: ProviderAuthenticationSession,
|
|
104
105
|
project: ResourceDescriptor,
|
|
105
106
|
options?: { user?: string; filters?: IssueFilter[] },
|
|
106
|
-
): Promise<{ values: IssueShape[]; truncated: boolean } | undefined> {
|
|
107
|
+
): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
|
|
107
108
|
// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.
|
|
108
109
|
if (!isIssueResourceDescriptor(project)) return undefined;
|
|
109
110
|
|
|
@@ -139,7 +140,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
139
140
|
// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the
|
|
140
141
|
// same read cannot recover the omitted cards (D11).
|
|
141
142
|
const truncated = result.metadata != null && result.metadata.completeness !== 'complete';
|
|
142
|
-
return { values: values, truncated: truncated };
|
|
143
|
+
return { values: values, truncated: truncated, metadata: result.metadata };
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
protected override searchProviderMyIssues(
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CollectionCompleteness,
|
|
3
|
+
CollectionMetadata,
|
|
4
|
+
CollectionOmission,
|
|
5
|
+
CollectionScopeFailure,
|
|
6
|
+
} from '@gitkraken/provider-apis';
|
|
2
7
|
import { isCancellationError } from '../../../../utils/cancellation.js';
|
|
3
|
-
import {
|
|
8
|
+
import { uniqueBy } from '../../../../utils/iterable.js';
|
|
9
|
+
import { collectionScopeKey, toCollectionScopeFailure } from '../../collectionMetadata.js';
|
|
4
10
|
import type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';
|
|
5
11
|
|
|
6
12
|
/**
|
|
@@ -42,21 +48,29 @@ const completenessRank: Record<CollectionCompleteness, number> = { partial: 2, u
|
|
|
42
48
|
|
|
43
49
|
/** A stable key for deduplicating structurally-identical scope failures accumulated across drained pages. */
|
|
44
50
|
function collectionFailureKey(failure: CollectionScopeFailure): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
return [failure.kind, collectionScopeKey(failure.scope), failure.message ?? ''].join(' ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A stable key for collapsing omissions accumulated across drained pages, matching the SDK's own
|
|
56
|
+
* `dedupeOmissions`: kind plus scope IDs, deliberately WITHOUT `limit`/`totalCount`.
|
|
57
|
+
*
|
|
58
|
+
* Those counts are a re-measurement, not an identity. GitHub recomputes the match total on every request, so
|
|
59
|
+
* one repository drained over several pages reports the same cap with a drifting total; keying on the total
|
|
60
|
+
* would emit a near-identical warning per page ("matched 1393…", "matched 1402…") that
|
|
61
|
+
* `appendDedupedWarning` cannot collapse, since the messages genuinely differ. Independently-scoped omissions
|
|
62
|
+
* still stay distinct, which is the case that carries information.
|
|
63
|
+
*/
|
|
64
|
+
function collectionOmissionKey(omission: CollectionOmission): string {
|
|
65
|
+
return [omission.kind, collectionScopeKey(omission.scope)].join(' ');
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
69
|
* Merges SDK collection metadata across drained pages. Completeness follows {@link completenessRank};
|
|
58
|
-
* failures are
|
|
59
|
-
*
|
|
70
|
+
* failures are deduplicated by kind, scope IDs, and message, and omissions are collapsed per kind and scope
|
|
71
|
+
* keeping the highest reported total ({@link collectionOmissionKey}). Both preserve first-reported order.
|
|
72
|
+
* Returns `undefined` when no page supplied metadata, so metadata-free providers and test doubles keep
|
|
73
|
+
* behaving as before.
|
|
60
74
|
*/
|
|
61
75
|
export function mergeCollectionMetadata(
|
|
62
76
|
base: CollectionMetadata | undefined,
|
|
@@ -70,17 +84,25 @@ export function mergeCollectionMetadata(
|
|
|
70
84
|
? next.completeness
|
|
71
85
|
: base.completeness;
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// First occurrence wins: a repeated failure carries no new information, since the message is part of its key.
|
|
88
|
+
const failures = [
|
|
89
|
+
...uniqueBy([...(base.failures ?? []), ...(next.failures ?? [])], collectionFailureKey, original => original),
|
|
90
|
+
];
|
|
91
|
+
// Highest total wins, so re-measuring one cap across pages collapses to a single omission holding the
|
|
92
|
+
// largest figure reported for it — the same rule the SDK applies in `createCollectionMetadata`.
|
|
93
|
+
const omissions = [
|
|
94
|
+
...uniqueBy(
|
|
95
|
+
[...(base.omissions ?? []), ...(next.omissions ?? [])],
|
|
96
|
+
collectionOmissionKey,
|
|
97
|
+
(original, current) => ((current.totalCount ?? -1) > (original.totalCount ?? -1) ? current : undefined),
|
|
98
|
+
),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
completeness: completeness,
|
|
103
|
+
...(failures.length ? { failures: failures } : {}),
|
|
104
|
+
...(omissions.length ? { omissions: omissions } : {}),
|
|
105
|
+
};
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
@@ -15,8 +15,10 @@ import type { ProviderWarning } from '../results.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* The single builder for a provider-neutral, non-auth warning: an unsupported capability, a
|
|
17
17
|
* contradictory/inexpressible request, or a read that couldn't confirm completeness. Every `kind: 'other'`
|
|
18
|
-
* warning the facade
|
|
19
|
-
* recurring message), so the discriminant is assigned in
|
|
18
|
+
* warning the facade raises on its own terms goes through here (directly, or via one of the named builders
|
|
19
|
+
* below that pin a recurring message), so the discriminant is assigned in one place. The warnings mapped from
|
|
20
|
+
* SDK collection metadata are the documented exception — `collectionMetadata.ts` builds those from the
|
|
21
|
+
* structured failure/omission it is given.
|
|
20
22
|
*
|
|
21
23
|
* The kinds that carry a programmatic remedy — `auth`, `rate-limit`, `not-found`, `no-connection` — are derived
|
|
22
24
|
* from the caught error's type instead and never come from here; see `ProviderWarningKind` and
|
package/src/utils/date.ts
CHANGED
|
@@ -149,9 +149,12 @@ export function unitThresholdMs(unit: Intl.RelativeTimeFormatUnit): number {
|
|
|
149
149
|
|
|
150
150
|
// fromNow keeps its OWN single-pass loop (not routed through fromNowUnit) — one iteration, no
|
|
151
151
|
// intermediate {unit,value} object allocation on this hot formatting path; fromNowUnit below iterates
|
|
152
|
-
// the SAME `relativeUnitThresholds` array, so the two classifications can't drift apart.
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
// the SAME `relativeUnitThresholds` array, so the two classifications can't drift apart. `now` defaults
|
|
153
|
+
// to `Date.now()`, like fromNowUnit's — pass it to pin the instant (a caller comparing against its own
|
|
154
|
+
// clock read otherwise sits one unpredictable tick away from the bucket it expects).
|
|
155
|
+
export function fromNow(date: Date | number, short?: boolean, now: Date | number = Date.now()): string {
|
|
156
|
+
const elapsed =
|
|
157
|
+
(typeof date === 'number' ? date : date.getTime()) - (typeof now === 'number' ? now : now.getTime());
|
|
155
158
|
// Guard against invalid dates (e.g. `new Date(<unparseable>)`), since a non-finite value would throw in `Intl.RelativeTimeFormat.format`
|
|
156
159
|
if (!Number.isFinite(elapsed)) return '';
|
|
157
160
|
|