@gitkraken/core-gitlens 0.10.1 → 0.10.2

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.
@@ -165,12 +165,20 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
165
165
  get connectionExpired(): boolean | undefined;
166
166
  protected _session: ProviderAuthenticationSession | null | undefined;
167
167
  /**
168
- * Bumped whenever a forced re-sync's refetch settles `_session`. A forced re-sync keeps the stored token while it
169
- * fetches the replacement, so a read that resolved that token concurrently must not publish it over whatever the
170
- * refetch settled on: a fresh token, or nothing after the cloud said the connection is gone.
168
+ * Every change to the session (a resolution, a forced re-sync, a disconnect, a reauthentication, a connection
169
+ * switch, a refused token) runs here, one at a time. Each one reads storage and the cloud and then settles
170
+ * `_session`, the stored tokens and the connected flag; run concurrently, one would publish what it read over
171
+ * what another settled meanwhile (gitkraken/kepler#3546). Reads don't queue: they read `_session` as it is.
171
172
  */
172
- private _sessionGeneration;
173
+ private _sessionTransitions;
174
+ /**
175
+ * Runs `fn` after every transition queued before it. `fn` must not queue another transition and wait for it,
176
+ * which would wait for itself: inside a transition, call the `*Locked` methods instead.
177
+ */
178
+ protected runSessionTransition<T>(fn: () => Promise<T>): Promise<T>;
179
+ /** An expired session is refreshed first; when that fails, nothing is returned rather than a token the provider refuses. */
173
180
  getSession(source: Sources): ProviderAuthenticationSession | Promise<ProviderAuthenticationSession | undefined> | undefined;
181
+ private getRefreshedSession;
174
182
  private isSessionForIntegrationHost;
175
183
  /**
176
184
  * Resolves the session to read as, for a per-connection (multi-account) read. When `connectionId` is
@@ -188,6 +196,11 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
188
196
  * limit as `rate-limit`, anything else as `other`.
189
197
  */
190
198
  protected resolveReadSession(connectionId: string | undefined, scope: ScopedLogger | undefined, source?: Sources): Promise<ProviderAuthenticationSession | ReadSessionFailure | undefined>;
199
+ /**
200
+ * The session lookups' prelude for a caller that can surface a failure (`throwOnError`): the refresh's failure to
201
+ * report, or `'unconnected'`, so a throttled refresh is not mistaken for "no pull request" and kept.
202
+ */
203
+ protected prepareSessionLookup(): Promise<Error | 'unconnected' | undefined>;
191
204
  /**
192
205
  * {@link resolveReadSession} for a read that already rethrows its provider's failures (a repository or account
193
206
  * lookup), or whose callers treat any failure as "nothing to show": a token that could not be fetched is
@@ -201,9 +214,15 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
201
214
  silent?: boolean;
202
215
  currentSessionOnly?: boolean;
203
216
  }): Promise<void>;
217
+ private disconnectLocked;
204
218
  protected providerOnDisconnect?(): void | Promise<void>;
205
219
  reauthenticate(): Promise<void>;
206
220
  refresh(): void;
221
+ /**
222
+ * Drops the cached session when its token is no longer `accessToken` (a session replaced outside the
223
+ * integration) and resolves it again, in one transition, so nothing in flight publishes the replaced one.
224
+ */
225
+ protected resyncSessionIfTokenChanged(accessToken: string | undefined): Promise<void>;
207
226
  private _syncRequestsPerFailedUsecase;
208
227
  hasSessionSyncRequests(): boolean;
209
228
  requestSessionSyncForUsecase(syncReqUsecase: SyncReqUsecase): void;
@@ -247,10 +266,14 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
247
266
  * connection changed underneath a warm integration (e.g. after `setPrimaryConnection`/
248
267
  * `deleteConnection`). Unlike {@link reset}/{@link disconnect}, it deletes nothing from storage.
249
268
  */
250
- switchConnection(): void;
251
- private refreshAfterSwitch;
269
+ switchConnection(): Promise<void>;
252
270
  private skippedNonCloudReported;
253
- syncCloudConnection(state: 'connected' | 'disconnected', forceSync: boolean): Promise<void>;
271
+ /**
272
+ * Resolves the re-sync's failure, when its token fetch threw (cancellations included), so a caller can report it.
273
+ * Never rejects: the cloud sync fans this out over every integration and must not fail on one.
274
+ */
275
+ syncCloudConnection(state: 'connected' | 'disconnected', forceSync: boolean): Promise<Error | undefined>;
276
+ private syncCloudConnectionLocked;
254
277
  /**
255
278
  * Proves the credential with an uncached check, rejecting with the provider's `AuthenticationError` when it is
256
279
  * refused, and with anything else when the check proves nothing. A `403` proves nothing either: the credential
@@ -360,13 +383,28 @@ export declare abstract class IntegrationBase<ID extends IntegrationIds = Integr
360
383
  silent?: boolean;
361
384
  connectionId?: string;
362
385
  }): boolean;
386
+ private expireSessionIfToken;
387
+ /** Counts completed cloud re-syncs, so a refresh queued behind one reuses its outcome rather than running another. */
388
+ private _resyncs;
389
+ private _lastResyncFailure;
363
390
  private missingExpirityReported;
364
- protected refreshSessionIfExpired(scope?: ScopedLogger): Promise<void>;
391
+ /**
392
+ * Resolves the refresh's failure while the session it could not replace is still unusable, so a read can report
393
+ * that instead of reading with an expired token or as a missing connection.
394
+ */
395
+ protected refreshSessionIfExpired(): Promise<Error | undefined>;
365
396
  trackRequestException(options?: {
366
397
  silent?: boolean;
367
398
  }): void;
368
399
  isConnected(): Promise<boolean>;
369
400
  private ensureSession;
401
+ /**
402
+ * Asks the auth provider for a session interactively, storing what it gets; whether one came back. A declined
403
+ * consent or any other failure is not one, and never throws: it is resolved, like any read, from storage.
404
+ */
405
+ private signIn;
406
+ /** Resolves and publishes the session. Only ever runs inside a transition (see {@link runSessionTransition}). */
407
+ private resolveSessionLocked;
370
408
  getIgnoreSSLErrors(): boolean | 'force';
371
409
  searchMyIssues(resource?: ResourceDescriptor, cancellation?: AbortSignal, connectionId?: string): Promise<IssueShape[] | undefined>;
372
410
  searchMyIssues(resources?: ResourceDescriptor[], cancellation?: AbortSignal, connectionId?: string): Promise<IssueShape[] | undefined>;
@@ -144,17 +144,34 @@ var IntegrationBase = class IntegrationBase {
144
144
  }
145
145
  _session;
146
146
  /**
147
- * Bumped whenever a forced re-sync's refetch settles `_session`. A forced re-sync keeps the stored token while it
148
- * fetches the replacement, so a read that resolved that token concurrently must not publish it over whatever the
149
- * refetch settled on: a fresh token, or nothing after the cloud said the connection is gone.
147
+ * Every change to the session (a resolution, a forced re-sync, a disconnect, a reauthentication, a connection
148
+ * switch, a refused token) runs here, one at a time. Each one reads storage and the cloud and then settles
149
+ * `_session`, the stored tokens and the connected flag; run concurrently, one would publish what it read over
150
+ * what another settled meanwhile (gitkraken/kepler#3546). Reads don't queue: they read `_session` as it is.
150
151
  */
151
- _sessionGeneration = 0;
152
+ _sessionTransitions = Promise.resolve();
153
+ /**
154
+ * Runs `fn` after every transition queued before it. `fn` must not queue another transition and wait for it,
155
+ * which would wait for itself: inside a transition, call the `*Locked` methods instead.
156
+ */
157
+ runSessionTransition(fn) {
158
+ const run = this._sessionTransitions.then(fn);
159
+ this._sessionTransitions = run.then(() => void 0, () => void 0);
160
+ return run;
161
+ }
162
+ /** An expired session is refreshed first; when that fails, nothing is returned rather than a token the provider refuses. */
152
163
  getSession(source) {
153
164
  if (this._session === void 0) return this.ensureSession({
154
165
  createIfNeeded: false,
155
166
  source
156
167
  });
157
- return this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : void 0;
168
+ if (this._session == null || !this.isSessionForIntegrationHost(this._session)) return void 0;
169
+ if (this.connectionExpired) return this.getRefreshedSession();
170
+ return this._session;
171
+ }
172
+ async getRefreshedSession() {
173
+ if (await this.refreshSessionIfExpired() != null) return void 0;
174
+ return this._session != null && this.connectionExpired !== true && this.isSessionForIntegrationHost(this._session) ? this._session : void 0;
158
175
  }
159
176
  isSessionForIntegrationHost(session) {
160
177
  if (!isSelfManagedHostIntegrationId(this.id)) return true;
@@ -196,10 +213,21 @@ var IntegrationBase = class IntegrationBase {
196
213
  }
197
214
  }
198
215
  if (!(this.maybeConnected ?? await this.isConnected())) return void 0;
199
- await this.refreshSessionIfExpired(scope);
216
+ const refreshFailure = await this.refreshSessionIfExpired();
217
+ if (refreshFailure != null) return new ReadSessionFailure(refreshFailure);
200
218
  return this._session != null && this.isSessionForIntegrationHost(this._session) ? this._session : void 0;
201
219
  }
202
220
  /**
221
+ * The session lookups' prelude for a caller that can surface a failure (`throwOnError`): the refresh's failure to
222
+ * report, or `'unconnected'`, so a throttled refresh is not mistaken for "no pull request" and kept.
223
+ */
224
+ async prepareSessionLookup() {
225
+ if (!(this.maybeConnected ?? await this.isConnected())) return "unconnected";
226
+ const failure = await this.refreshSessionIfExpired();
227
+ if (failure != null) return failure;
228
+ return this._session != null ? void 0 : "unconnected";
229
+ }
230
+ /**
203
231
  * {@link resolveReadSession} for a read that already rethrows its provider's failures (a repository or account
204
232
  * lookup), or whose callers treat any failure as "nothing to show": a token that could not be fetched is
205
233
  * thrown like those failures, so it is classified as the failed request it is rather than as a missing
@@ -222,9 +250,8 @@ var IntegrationBase = class IntegrationBase {
222
250
  }
223
251
  async disconnect(options) {
224
252
  if (options?.currentSessionOnly && this._session === null) return;
225
- const connected = this._session != null;
226
253
  let signOut = !options?.currentSessionOnly;
227
- if (connected && !options?.currentSessionOnly && !options?.silent) {
254
+ if (this._session != null && !options?.currentSessionOnly && !options?.silent) {
228
255
  const decision = await this.ctx.hooks?.onConfirmDisconnect?.({
229
256
  integrationName: this.name,
230
257
  offerSignOut: this.authenticationService.supports(this.authProvider.id)
@@ -232,6 +259,11 @@ var IntegrationBase = class IntegrationBase {
232
259
  if (decision == null) return;
233
260
  signOut = decision.signOut;
234
261
  }
262
+ await this.runSessionTransition(() => this.disconnectLocked(options, signOut));
263
+ }
264
+ async disconnectLocked(options, signOut) {
265
+ if (options?.currentSessionOnly && this._session === null) return;
266
+ const connected = this._session != null;
235
267
  if (signOut) await (await this.authenticationService.get(this.authProvider.id)).deleteAllSessions(this.authProviderDescriptor);
236
268
  this.resetRequestExceptionCount("all");
237
269
  this._session = null;
@@ -248,8 +280,11 @@ var IntegrationBase = class IntegrationBase {
248
280
  }
249
281
  async reauthenticate() {
250
282
  this.onStoredTokensReplaced();
251
- if (this._session === void 0) return;
252
- this._session = void 0;
283
+ if (!await this.runSessionTransition(() => {
284
+ if (this._session === void 0) return Promise.resolve(false);
285
+ this._session = void 0;
286
+ return Promise.resolve(true);
287
+ })) return;
253
288
  await this.ensureSession({
254
289
  createIfNeeded: true,
255
290
  forceNewSession: true
@@ -258,6 +293,16 @@ var IntegrationBase = class IntegrationBase {
258
293
  refresh() {
259
294
  this.ensureSession({ createIfNeeded: false });
260
295
  }
296
+ /**
297
+ * Drops the cached session when its token is no longer `accessToken` (a session replaced outside the
298
+ * integration) and resolves it again, in one transition, so nothing in flight publishes the replaced one.
299
+ */
300
+ resyncSessionIfTokenChanged(accessToken) {
301
+ return this.runSessionTransition(async () => {
302
+ if (this._session != null && this._session.accessToken !== accessToken) this._session = void 0;
303
+ await this.resolveSessionLocked({ createIfNeeded: false });
304
+ });
305
+ }
261
306
  _syncRequestsPerFailedUsecase = /* @__PURE__ */ new Set();
262
307
  hasSessionSyncRequests() {
263
308
  return this._syncRequestsPerFailedUsecase.size > 0;
@@ -311,8 +356,10 @@ var IntegrationBase = class IntegrationBase {
311
356
  this.requestExceptionCount = 0;
312
357
  }
313
358
  async reset() {
314
- await this.disconnect({ silent: true });
315
- await this.ctx.storage.deleteWorkspace(this.connectedKey);
359
+ await this.runSessionTransition(async () => {
360
+ await this.disconnectLocked({ silent: true }, true);
361
+ await this.ctx.storage.deleteWorkspace(this.connectedKey);
362
+ });
316
363
  }
317
364
  /**
318
365
  * Drops the in-memory session so the next access re-resolves it from storage. Used when the primary
@@ -321,24 +368,30 @@ var IntegrationBase = class IntegrationBase {
321
368
  */
322
369
  switchConnection() {
323
370
  this.onStoredTokensReplaced();
324
- if (this._session === void 0) return;
325
- const wasConnected = this._session != null;
326
- this._session = void 0;
327
- this._onDidChange.fire();
328
- this.refreshAfterSwitch(wasConnected);
329
- }
330
- async refreshAfterSwitch(wasConnected) {
331
- if (await this.ensureSession({ createIfNeeded: false }) != null || !wasConnected) return;
332
- this._onDidChange.fire();
333
- this.didChangeConnection?.fire({
334
- integration: this,
335
- key: this.key,
336
- reason: "disconnected"
371
+ return this.runSessionTransition(async () => {
372
+ if (this._session === void 0) return;
373
+ const wasConnected = this._session != null;
374
+ this._session = void 0;
375
+ this._onDidChange.fire();
376
+ if (await this.resolveSessionLocked({ createIfNeeded: false }) != null || !wasConnected) return;
377
+ this._onDidChange.fire();
378
+ this.didChangeConnection?.fire({
379
+ integration: this,
380
+ key: this.key,
381
+ reason: "disconnected"
382
+ });
383
+ await this.providerOnDisconnect?.();
337
384
  });
338
- await this.providerOnDisconnect?.();
339
385
  }
340
386
  skippedNonCloudReported = false;
341
- async syncCloudConnection(state, forceSync) {
387
+ /**
388
+ * Resolves the re-sync's failure, when its token fetch threw (cancellations included), so a caller can report it.
389
+ * Never rejects: the cloud sync fans this out over every integration and must not fail on one.
390
+ */
391
+ syncCloudConnection(state, forceSync) {
392
+ return this.runSessionTransition(() => this.syncCloudConnectionLocked(state, forceSync));
393
+ }
394
+ async syncCloudConnectionLocked(state, forceSync) {
342
395
  const scope = getScopedLogger();
343
396
  if (this._session?.cloud === false && state !== "connected") {
344
397
  if (this.id !== "github" && !this.skippedNonCloudReported) {
@@ -351,13 +404,13 @@ var IntegrationBase = class IntegrationBase {
351
404
  }
352
405
  return;
353
406
  }
407
+ let failure;
354
408
  switch (state) {
355
409
  case "connected": {
356
410
  const oldSession = this._session;
357
411
  let resyncing = false;
358
412
  if (forceSync) {
359
413
  this.onStoredTokensReplaced();
360
- this._session = void 0;
361
414
  await this.ctx.storage.deleteWorkspace(this.connectedKey);
362
415
  resyncing = true;
363
416
  } else {
@@ -367,20 +420,24 @@ var IntegrationBase = class IntegrationBase {
367
420
  let newSession;
368
421
  let refetchFailed = false;
369
422
  try {
370
- newSession = await this.ensureSession({
423
+ newSession = await this.resolveSessionLocked({
371
424
  sync: forceSync,
372
425
  refetch: resyncing
373
426
  });
374
427
  } catch (ex) {
375
- if (!isCancellationError(ex)) scope?.error(ex);
376
428
  refetchFailed = true;
429
+ if (!isCancellationError(ex)) scope?.error(ex);
430
+ failure = toError(ex);
377
431
  }
378
432
  if (oldSession && newSession && newSession.accessToken !== oldSession.accessToken) this.resetRequestExceptionCount("all");
379
433
  if (resyncing && newSession == null && !refetchFailed) await (await this.authenticationService.get(this.authProvider.id)).deleteSession(this.authProviderDescriptor, { preserveConfigured: false });
380
434
  break;
381
435
  }
382
- case "disconnected": await this.disconnect({ silent: true });
436
+ case "disconnected": await this.disconnectLocked({ silent: true }, true);
383
437
  }
438
+ this._resyncs++;
439
+ this._lastResyncFailure = failure;
440
+ return failure;
384
441
  }
385
442
  /**
386
443
  * Credentials that passed {@link validateCredential} within the last minute. A probe already in flight is
@@ -544,24 +601,48 @@ var IntegrationBase = class IntegrationBase {
544
601
  }
545
602
  if (ex instanceof AuthenticationError && this._session?.cloud && !this.hasSessionSyncRequests()) {
546
603
  this.requestSessionSyncForUsecase(syncReqUsecase);
547
- this._session = {
548
- ...this._session,
549
- expiresAt: /* @__PURE__ */ new Date(Date.now() - 1)
550
- };
604
+ const refused = this._session.accessToken;
605
+ this.expireSessionIfToken(refused);
606
+ this.runSessionTransition(() => {
607
+ this.expireSessionIfToken(refused);
608
+ return Promise.resolve();
609
+ });
551
610
  return false;
552
611
  }
553
612
  if (!(ex instanceof AuthenticationError) && !(ex instanceof RequestClientError)) return false;
554
613
  this.trackRequestException(options);
555
614
  return true;
556
615
  }
616
+ expireSessionIfToken(accessToken) {
617
+ if (this._session?.accessToken !== accessToken || this.connectionExpired === true) return;
618
+ this._session = {
619
+ ...this._session,
620
+ expiresAt: /* @__PURE__ */ new Date(Date.now() - 1)
621
+ };
622
+ }
623
+ /** Counts completed cloud re-syncs, so a refresh queued behind one reuses its outcome rather than running another. */
624
+ _resyncs = 0;
625
+ _lastResyncFailure;
557
626
  missingExpirityReported = false;
558
- async refreshSessionIfExpired(scope) {
559
- if (this._session?.expiresAt != null && this._session.expiresAt < /* @__PURE__ */ new Date()) try {
560
- await this.syncCloudConnection("connected", true);
561
- } catch (ex) {
562
- scope?.error(ex);
563
- }
564
- else if (this._session?.expiresAt == null && this.id !== "github" && !this.missingExpirityReported) {
627
+ /**
628
+ * Resolves the refresh's failure while the session it could not replace is still unusable, so a read can report
629
+ * that instead of reading with an expired token or as a missing connection.
630
+ */
631
+ async refreshSessionIfExpired() {
632
+ if (this._session?.expiresAt != null && this._session.expiresAt < /* @__PURE__ */ new Date()) {
633
+ const resyncs = this._resyncs;
634
+ return this.runSessionTransition(async () => {
635
+ let failure;
636
+ if (this._resyncs !== resyncs) failure = this._lastResyncFailure;
637
+ else if (this._session != null && this.connectionExpired === true) try {
638
+ failure = await this.syncCloudConnectionLocked("connected", true);
639
+ } catch (ex) {
640
+ failure = toError(ex);
641
+ }
642
+ if (this._session == null || this.connectionExpired !== true) return void 0;
643
+ return failure ?? /* @__PURE__ */ new Error(`The ${this.name} session could not be refreshed`);
644
+ });
645
+ } else if (this._session?.expiresAt == null && this.id !== "github" && !this.missingExpirityReported) {
565
646
  this.ctx.hooks?.session?.onRefreshSkipped?.({
566
647
  id: this.id,
567
648
  reason: "missing-expiry",
@@ -578,11 +659,36 @@ var IntegrationBase = class IntegrationBase {
578
659
  }
579
660
  }
580
661
  async isConnected() {
581
- return await this.getSession("integrations") != null;
662
+ const session = this._session === void 0 ? await this.ensureSession({
663
+ createIfNeeded: false,
664
+ source: "integrations"
665
+ }) : this._session;
666
+ return session != null && this.isSessionForIntegrationHost(session);
582
667
  }
583
668
  async ensureSession(options) {
669
+ if (options.createIfNeeded || options.forceNewSession) {
670
+ if (!await this.signIn(options)) return this._session ?? void 0;
671
+ return this.runSessionTransition(() => this.resolveSessionLocked({ createIfNeeded: false }));
672
+ }
673
+ return this.runSessionTransition(() => this.resolveSessionLocked(options));
674
+ }
675
+ /**
676
+ * Asks the auth provider for a session interactively, storing what it gets; whether one came back. A declined
677
+ * consent or any other failure is not one, and never throws: it is resolved, like any read, from storage.
678
+ */
679
+ async signIn(options) {
680
+ if (this.ctx.config.isIntegrationsEnabled?.() === false) return false;
681
+ await this.ctx.storage.deleteWorkspace(this.connectedKey);
682
+ try {
683
+ return await (await this.authenticationService.get(this.authProvider.id)).getSession(this.authProviderDescriptor, options) != null;
684
+ } catch {
685
+ return false;
686
+ }
687
+ }
688
+ /** Resolves and publishes the session. Only ever runs inside a transition (see {@link runSessionTransition}). */
689
+ async resolveSessionLocked(options) {
584
690
  const scope = getScopedLogger();
585
- const { createIfNeeded, forceNewSession, source, sync, refetch } = options;
691
+ const { createIfNeeded, source, sync, refetch } = options;
586
692
  if (this._session != null && !refetch) {
587
693
  if (this.isSessionForIntegrationHost(this._session)) return this._session;
588
694
  this._session = null;
@@ -590,7 +696,6 @@ var IntegrationBase = class IntegrationBase {
590
696
  if (this.ctx.config.isIntegrationsEnabled?.() === false) return void 0;
591
697
  if (createIfNeeded || sync) await this.ctx.storage.deleteWorkspace(this.connectedKey);
592
698
  else if (this.ctx.storage.getWorkspace(this.connectedKey) === false) return;
593
- const generation = this._sessionGeneration;
594
699
  let session;
595
700
  try {
596
701
  session = await (await this.authenticationService.get(this.authProvider.id)).getSession(this.authProviderDescriptor, sync ? {
@@ -599,7 +704,6 @@ var IntegrationBase = class IntegrationBase {
599
704
  source
600
705
  } : {
601
706
  createIfNeeded,
602
- forceNewSession,
603
707
  source
604
708
  });
605
709
  if (session?.expiresAt != null && session.expiresAt < /* @__PURE__ */ new Date()) session = null;
@@ -613,8 +717,6 @@ var IntegrationBase = class IntegrationBase {
613
717
  }
614
718
  session = null;
615
719
  }
616
- if (refetch) this._sessionGeneration++;
617
- else if (generation !== this._sessionGeneration) return this._session ?? void 0;
618
720
  if (session === void 0 && !createIfNeeded && !sync) await this.ctx.storage.deleteWorkspace(this.connectedKey);
619
721
  this._session = session ?? null;
620
722
  this.smoothifyRequestExceptionCount();
@@ -707,8 +809,7 @@ var IntegrationBase = class IntegrationBase {
707
809
  }
708
810
  async getLinkedIssueOrPullRequest(resource, link, options) {
709
811
  const scope = getScopedLogger();
710
- if (!(this.maybeConnected ?? await this.isConnected())) return void 0;
711
- await this.refreshSessionIfExpired(scope);
812
+ if (await this.prepareSessionLookup() != null) return void 0;
712
813
  return this.ctx.cache.getIssueOrPullRequest(link.key, options?.type, resource, this, () => ({ value: (async () => {
713
814
  try {
714
815
  const result = await this.getProviderLinkedIssueOrPullRequest(this._session, resource, link, options?.type);
@@ -806,8 +907,12 @@ var IntegrationBase = class IntegrationBase {
806
907
  }
807
908
  async getPullRequest(resource, id, options) {
808
909
  const scope = getScopedLogger();
809
- if (!(this.maybeConnected ?? await this.isConnected())) return void 0;
810
- await this.refreshSessionIfExpired(scope);
910
+ const refreshFailure = await this.prepareSessionLookup();
911
+ if (refreshFailure === "unconnected") return void 0;
912
+ if (refreshFailure != null) {
913
+ if (options?.throwOnError) throw refreshFailure;
914
+ return;
915
+ }
811
916
  return await this.ctx.cache.getPullRequest(id, resource, this, (cacheable) => ({ value: (async () => {
812
917
  try {
813
918
  const result = await this.getProviderPullRequest?.(this._session, resource, id);