Skip to content

Commit

Permalink
chore(clerk-js): Pause/resume session touch while offline (#5098)
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef authored Feb 17, 2025
1 parent a4bf420 commit cab9408
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-dogs-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Pause session touch and token refresh while browser is offline, and resume it when the device comes back online.
12 changes: 12 additions & 0 deletions integration/tests/sign-out-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out

test('sign out through all open tabs at once', async ({ page, context }) => {
const mainTab = createTestUtils({ app, page, context });
await mainTab.page.addInitScript(() => {
/**
* Playwright may define connection incorrectly, we are overriding to null
*/
if (
navigator.onLine &&
// @ts-expect-error Cannot find `connection`
(navigator?.connection?.rtt === 0 || navigator?.downlink?.rtt === 0)
) {
Object.defineProperty(Object.getPrototypeOf(navigator), 'connection', { value: null });
}
});
await mainTab.po.signIn.goTo();
await mainTab.po.signIn.setIdentifier(fakeUser.email);
await mainTab.po.signIn.continue();
Expand Down
10 changes: 9 additions & 1 deletion packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isBrowserOnline } from '@clerk/shared/browser';
import { createCookieHandler } from '@clerk/shared/cookie';
import { setDevBrowserJWTInURL } from '@clerk/shared/devBrowser';
import { is4xxError, isClerkAPIResponseError, isNetworkError } from '@clerk/shared/error';
import type { Clerk, InstanceType } from '@clerk/types';

import { createOfflineScheduler } from '../../utils/offlineScheduler';
import { clerkCoreErrorTokenRefreshFailed, clerkMissingDevBrowserJwt } from '../errors';
import { eventBus, events } from '../events';
import type { FapiClient } from '../fapiClient';
Expand Down Expand Up @@ -39,6 +41,7 @@ export class AuthCookieService {
private sessionCookie: SessionCookieHandler;
private activeOrgCookie: ReturnType<typeof createCookieHandler>;
private devBrowser: DevBrowser;
private sessionRefreshOfflineScheduler = createOfflineScheduler();

public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) {
const cookieSuffix = await getCookieSuffix(clerk.publishableKey);
Expand Down Expand Up @@ -124,7 +127,8 @@ export class AuthCookieService {
// be done with a microtask. Promises schedule microtasks, and so by using `updateCookieImmediately: true`, we ensure that the cookie
// is updated as part of the scheduled microtask. Our existing event-based mechanism to update the cookie schedules a task, and so the cookie
// is updated too late and not guaranteed to be fresh before the refetch occurs.
void this.refreshSessionToken({ updateCookieImmediately: true });
// While online `.schedule()` executes synchronously and immediately, ensuring the above mechanism will not break.
this.sessionRefreshOfflineScheduler.schedule(() => this.refreshSessionToken({ updateCookieImmediately: true }));
}
});
}
Expand All @@ -138,6 +142,10 @@ export class AuthCookieService {
return;
}

if (!isBrowserOnline()) {
return;
}

try {
const token = await this.clerk.session.getToken();
if (updateCookieImmediately) {
Expand Down
18 changes: 13 additions & 5 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
} from '../utils';
import { assertNoLegacyProp } from '../utils/assertNoLegacyProp';
import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback';
import { createOfflineScheduler } from '../utils/offlineScheduler';
import { RedirectUrls } from '../utils/redirectUrls';
import { AuthCookieService } from './auth/AuthCookieService';
import { CaptchaHeartbeat } from './auth/CaptchaHeartbeat';
Expand Down Expand Up @@ -190,6 +191,7 @@ export class Clerk implements ClerkInterface {
#options: ClerkOptions = {};
#pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
#touchThrottledUntil = 0;
#sessionTouchOfflineScheduler = createOfflineScheduler();

public __internal_getCachedResources:
| (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>)
Expand Down Expand Up @@ -1902,7 +1904,7 @@ export class Clerk implements ClerkInterface {
this.#pageLifecycle = createPageLifecycle();

this.#broadcastChannel = new LocalStorageBroadcastChannel('clerk');
this.#setupListeners();
this.#setupBrowserListeners();

const isInAccountsHostedPages = isDevAccountPortalOrigin(window?.location.hostname);
const shouldTouchEnv = this.#instanceType === 'development' && !isInAccountsHostedPages;
Expand Down Expand Up @@ -2037,20 +2039,26 @@ export class Clerk implements ClerkInterface {
return session || null;
};

#setupListeners = (): void => {
#setupBrowserListeners = (): void => {
if (!inClientSide()) {
return;
}

this.#pageLifecycle?.onPageFocus(() => {
if (this.session) {
if (!this.session) {
return;
}

const performTouch = () => {
if (this.#touchThrottledUntil > Date.now()) {
return;
}
this.#touchThrottledUntil = Date.now() + 5_000;

void this.#touchLastActiveSession(this.session);
}
return this.#touchLastActiveSession(this.session);
};

this.#sessionTouchOfflineScheduler.schedule(performTouch);
});

this.#broadcastChannel?.addEventListener('message', ({ data }) => {
Expand Down
36 changes: 36 additions & 0 deletions packages/clerk-js/src/utils/offlineScheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isBrowserOnline } from '@clerk/shared/browser';

/**
* While online callbacks passed to `.schedule` will execute immediately.
* While offline callbacks passed to `.schedule` are de-duped and only the first one will be scheduled for execution when online.
*/
export const createOfflineScheduler = () => {
let scheduled = false;

const schedule = (cb: () => void) => {
if (scheduled) {
return;
}
if (isBrowserOnline()) {
cb();
return;
}
scheduled = true;
const controller = new AbortController();
window.addEventListener(
'online',
() => {
void cb();
scheduled = false;
controller.abort();
},
{
signal: controller.signal,
},
);
};

return {
schedule,
};
};
5 changes: 1 addition & 4 deletions packages/clerk-js/src/utils/pageLifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { inBrowser } from '@clerk/shared/browser';

const noop = () => {
//
};
import { noop } from '@clerk/shared/utils';

/**
* Abstracts native browser event listener registration.
Expand Down

0 comments on commit cab9408

Please sign in to comment.