From eb6d12f349248d7a9af5f05e6b3a1efd79bb9a61 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 7 Feb 2025 15:04:01 +0200 Subject: [PATCH 01/21] chore(clerk-js): Pause/resume session touch while offline --- .changeset/breezy-dogs-greet.md | 5 +++ packages/clerk-js/src/core/clerk.ts | 35 ++++++++++++++++---- packages/clerk-js/src/utils/pageLifecycle.ts | 10 +++--- packages/clerk-js/src/utils/runtime.ts | 4 +-- 4 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 .changeset/breezy-dogs-greet.md diff --git a/.changeset/breezy-dogs-greet.md b/.changeset/breezy-dogs-greet.md new file mode 100644 index 00000000000..ad152f6ead8 --- /dev/null +++ b/.changeset/breezy-dogs-greet.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Pause session touch when browser is offline, and resume it when the device comes back online. diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2bd3ccd9b63..d8cbac497b5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,4 +1,4 @@ -import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; +import { inBrowser, isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -7,7 +7,7 @@ import { logger } from '@clerk/shared/logger'; import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; -import { handleValueOrFn, noop } from '@clerk/shared/utils'; +import { createDeferredPromise, handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, ActiveSessionResource, @@ -84,7 +84,6 @@ import { hasExternalAccountSignUpError, ignoreEventValue, inActiveBrowserTab, - inBrowser, isDevAccountPortalOrigin, isError, isOrganizationId, @@ -190,6 +189,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; + #touchedWhileOffline = false; public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) @@ -1859,7 +1859,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; @@ -1994,13 +1994,34 @@ export class Clerk implements ClerkInterface { return session || null; }; - #setupListeners = (): void => { - if (!inClientSide()) { + #setupBrowserListeners = (): void => { + if (!inBrowser()) { return; } - this.#pageLifecycle?.onPageFocus(() => { + this.#pageLifecycle?.onPageFocus(async () => { if (this.session) { + if (!isBrowserOnline()) { + if (this.#touchedWhileOffline) { + return; + } + this.#touchedWhileOffline = true; + const promiseWithResolvers = createDeferredPromise(); + const controller = new AbortController(); + window.addEventListener( + 'online', + e => { + promiseWithResolvers.resolve(e); + }, + { + signal: controller.signal, + }, + ); + await promiseWithResolvers.promise; + controller.abort(); + this.#touchedWhileOffline = false; + } + if (this.#touchThrottledUntil > Date.now()) { return; } diff --git a/packages/clerk-js/src/utils/pageLifecycle.ts b/packages/clerk-js/src/utils/pageLifecycle.ts index 52c51396b6e..d85a4bb921a 100644 --- a/packages/clerk-js/src/utils/pageLifecycle.ts +++ b/packages/clerk-js/src/utils/pageLifecycle.ts @@ -1,8 +1,5 @@ import { inBrowser } from '@clerk/shared/browser'; - -const noop = () => { - // -}; +import { noop } from '@clerk/shared/utils'; /** * Abstracts native browser event listener registration. @@ -19,17 +16,18 @@ export const createPageLifecycle = () => { return { onPageFocus: noop }; } - const callbackQueue: Record void>> = { + const callbackQueue: Record void | Promise>> = { focus: [], }; window.addEventListener('focus', () => { if (document.visibilityState === 'visible') { + // eslint-disable-next-line @typescript-eslint/no-misused-promises callbackQueue['focus'].forEach(cb => cb()); } }); - const onPageFocus = (cb: () => void) => { + const onPageFocus = (cb: () => void | Promise) => { callbackQueue['focus'].push(cb); }; diff --git a/packages/clerk-js/src/utils/runtime.ts b/packages/clerk-js/src/utils/runtime.ts index 10b190d4344..df1e4694089 100644 --- a/packages/clerk-js/src/utils/runtime.ts +++ b/packages/clerk-js/src/utils/runtime.ts @@ -1,6 +1,4 @@ -export function inBrowser() { - return typeof globalThis.document !== 'undefined'; -} +import { inBrowser } from '@clerk/shared/browser'; export function inActiveBrowserTab() { return inBrowser() && globalThis.document.hasFocus(); From 9184b06815987a96e6d55e38bf4c79fe4d849f48 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 7 Feb 2025 15:31:39 +0200 Subject: [PATCH 02/21] revert --- packages/clerk-js/src/core/clerk.ts | 5 +++-- packages/clerk-js/src/utils/runtime.ts | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d8cbac497b5..43d296b71be 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,4 +1,4 @@ -import { inBrowser, isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; +import { inBrowser as inClientSide, isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -84,6 +84,7 @@ import { hasExternalAccountSignUpError, ignoreEventValue, inActiveBrowserTab, + inBrowser, isDevAccountPortalOrigin, isError, isOrganizationId, @@ -1995,7 +1996,7 @@ export class Clerk implements ClerkInterface { }; #setupBrowserListeners = (): void => { - if (!inBrowser()) { + if (!inClientSide()) { return; } diff --git a/packages/clerk-js/src/utils/runtime.ts b/packages/clerk-js/src/utils/runtime.ts index df1e4694089..10b190d4344 100644 --- a/packages/clerk-js/src/utils/runtime.ts +++ b/packages/clerk-js/src/utils/runtime.ts @@ -1,4 +1,6 @@ -import { inBrowser } from '@clerk/shared/browser'; +export function inBrowser() { + return typeof globalThis.document !== 'undefined'; +} export function inActiveBrowserTab() { return inBrowser() && globalThis.document.hasFocus(); From 40d0c80a1410c5788182b75b8bda0500cfa72cdf Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 17:03:19 +0200 Subject: [PATCH 03/21] handler `/tokens` --- .../src/core/auth/AuthCookieService.ts | 33 ++++++++++++- .../src/core/auth/SessionCookiePoller.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 47 ++++++++++--------- .../clerk-js/src/core/resources/Session.ts | 4 +- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 530d0cc4c4b..7e30d6cdf1a 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -1,3 +1,4 @@ +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'; @@ -39,6 +40,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; + private isRefreshTokenOnFocusPending = false; public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -118,13 +120,36 @@ export class AuthCookieService { private refreshTokenOnFocus() { window.addEventListener('focus', () => { - if (document.visibilityState === 'visible') { + const refreshImmediately = () => { // Certain data-fetching libraries that refetch on focus (such as swr) use setTimeout(cb, 0) to schedule a task on the event loop. // This gives us an opportunity to ensure the session cookie is updated with a fresh token before the fetch occurs, but it needs to // 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 }); + return this.refreshSessionToken({ updateCookieImmediately: true }); + }; + if (document.visibilityState === 'visible') { + if (this.isRefreshTokenOnFocusPending) { + return; + } + + if (isBrowserOnline()) { + return void refreshImmediately(); + } + + this.isRefreshTokenOnFocusPending = true; + const controller = new AbortController(); + window.addEventListener( + 'online', + () => { + void refreshImmediately(); + this.isRefreshTokenOnFocusPending = false; + controller.abort(); + }, + { + signal: controller.signal, + }, + ); } }); } @@ -138,6 +163,10 @@ export class AuthCookieService { return; } + if (!isBrowserOnline()) { + return; + } + try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { diff --git a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts index dd4ff1f2a05..4497b87c28d 100644 --- a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts @@ -3,7 +3,7 @@ import { createWorkerTimers } from '@clerk/shared/workerTimers'; import { SafeLock } from './safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; -const INTERVAL_IN_MS = 5 * 1000; +const INTERVAL_IN_MS = 5 * 1_000; export class SessionCookiePoller { private lock = SafeLock(REFRESH_SESSION_TOKEN_LOCK_KEY); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 43d296b71be..f0657833aeb 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -7,7 +7,7 @@ import { logger } from '@clerk/shared/logger'; import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; -import { createDeferredPromise, handleValueOrFn, noop } from '@clerk/shared/utils'; +import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, ActiveSessionResource, @@ -2001,35 +2001,36 @@ export class Clerk implements ClerkInterface { } this.#pageLifecycle?.onPageFocus(async () => { - if (this.session) { - if (!isBrowserOnline()) { - if (this.#touchedWhileOffline) { - return; - } - this.#touchedWhileOffline = true; - const promiseWithResolvers = createDeferredPromise(); - const controller = new AbortController(); - window.addEventListener( - 'online', - e => { - promiseWithResolvers.resolve(e); - }, - { - signal: controller.signal, - }, - ); - await promiseWithResolvers.promise; - controller.abort(); - this.#touchedWhileOffline = false; - } + if (!this.session || this.#touchedWhileOffline) { + return; + } + const performTouch = async () => { if (this.#touchThrottledUntil > Date.now()) { return; } this.#touchThrottledUntil = Date.now() + 5_000; - void this.#touchLastActiveSession(this.session); + return this.#touchLastActiveSession(this.session); + }; + + if (isBrowserOnline()) { + return performTouch(); } + + this.#touchedWhileOffline = true; + const controller = new AbortController(); + window.addEventListener( + 'online', + () => { + void performTouch(); + this.#touchedWhileOffline = false; + controller.abort(); + }, + { + signal: controller.signal, + }, + ); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 2a6c1b6e3aa..67684eff70d 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,4 +1,5 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; +import { isBrowserOnline } from '@clerk/shared/browser'; import { is4xxError } from '@clerk/shared/error'; import { runWithExponentialBackOff } from '@clerk/shared/utils'; import type { @@ -85,7 +86,8 @@ export class Session extends BaseResource implements SessionResource { getToken: GetToken = async (options?: GetTokenOptions): Promise => { return runWithExponentialBackOff(() => this._getToken(options), { - shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4, + shouldRetry: (error: unknown, currentIteration: number) => + !is4xxError(error) && currentIteration < 4 && isBrowserOnline(), }); }; From f4bd4039ead970590cc483b1f8232de1408a6e35 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 17:16:44 +0200 Subject: [PATCH 04/21] abstraction --- .../src/core/auth/AuthCookieService.ts | 25 ++------------- packages/clerk-js/src/core/clerk.ts | 25 +++------------ .../clerk-js/src/utils/offlineScheduler.ts | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 42 deletions(-) create mode 100644 packages/clerk-js/src/utils/offlineScheduler.ts diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 7e30d6cdf1a..3b05761c5d5 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -4,6 +4,7 @@ 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'; @@ -40,7 +41,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; - private isRefreshTokenOnFocusPending = false; + private offlineScheduler = createOfflineScheduler(); public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -129,27 +130,7 @@ export class AuthCookieService { return this.refreshSessionToken({ updateCookieImmediately: true }); }; if (document.visibilityState === 'visible') { - if (this.isRefreshTokenOnFocusPending) { - return; - } - - if (isBrowserOnline()) { - return void refreshImmediately(); - } - - this.isRefreshTokenOnFocusPending = true; - const controller = new AbortController(); - window.addEventListener( - 'online', - () => { - void refreshImmediately(); - this.isRefreshTokenOnFocusPending = false; - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + this.offlineScheduler.schedule(refreshImmediately); } }); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f0657833aeb..d1634d92599 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,4 +1,4 @@ -import { inBrowser as inClientSide, isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; +import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -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'; @@ -190,7 +191,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; - #touchedWhileOffline = false; + #offlineScheduler = createOfflineScheduler(); public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) @@ -2001,7 +2002,7 @@ export class Clerk implements ClerkInterface { } this.#pageLifecycle?.onPageFocus(async () => { - if (!this.session || this.#touchedWhileOffline) { + if (!this.session) { return; } @@ -2014,23 +2015,7 @@ export class Clerk implements ClerkInterface { return this.#touchLastActiveSession(this.session); }; - if (isBrowserOnline()) { - return performTouch(); - } - - this.#touchedWhileOffline = true; - const controller = new AbortController(); - window.addEventListener( - 'online', - () => { - void performTouch(); - this.#touchedWhileOffline = false; - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + this.#offlineScheduler.schedule(performTouch); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { diff --git a/packages/clerk-js/src/utils/offlineScheduler.ts b/packages/clerk-js/src/utils/offlineScheduler.ts new file mode 100644 index 00000000000..54ffd73ad81 --- /dev/null +++ b/packages/clerk-js/src/utils/offlineScheduler.ts @@ -0,0 +1,32 @@ +import { isBrowserOnline } from '@clerk/shared/browser'; + +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, + }; +}; From 0e8d7b9c57dfa4b0cc0cd76ea522fad97b98db39 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 19:31:06 +0200 Subject: [PATCH 05/21] Revert "abstraction" This reverts commit f4bd4039ead970590cc483b1f8232de1408a6e35. --- .../src/core/auth/AuthCookieService.ts | 25 +++++++++++++-- packages/clerk-js/src/core/clerk.ts | 25 ++++++++++++--- .../clerk-js/src/utils/offlineScheduler.ts | 32 ------------------- 3 files changed, 42 insertions(+), 40 deletions(-) delete mode 100644 packages/clerk-js/src/utils/offlineScheduler.ts diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 3b05761c5d5..7e30d6cdf1a 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -4,7 +4,6 @@ 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'; @@ -41,7 +40,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; - private offlineScheduler = createOfflineScheduler(); + private isRefreshTokenOnFocusPending = false; public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -130,7 +129,27 @@ export class AuthCookieService { return this.refreshSessionToken({ updateCookieImmediately: true }); }; if (document.visibilityState === 'visible') { - this.offlineScheduler.schedule(refreshImmediately); + if (this.isRefreshTokenOnFocusPending) { + return; + } + + if (isBrowserOnline()) { + return void refreshImmediately(); + } + + this.isRefreshTokenOnFocusPending = true; + const controller = new AbortController(); + window.addEventListener( + 'online', + () => { + void refreshImmediately(); + this.isRefreshTokenOnFocusPending = false; + controller.abort(); + }, + { + signal: controller.signal, + }, + ); } }); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d1634d92599..f0657833aeb 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,4 +1,4 @@ -import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; +import { inBrowser as inClientSide, isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -99,7 +99,6 @@ 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'; @@ -191,7 +190,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; - #offlineScheduler = createOfflineScheduler(); + #touchedWhileOffline = false; public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) @@ -2002,7 +2001,7 @@ export class Clerk implements ClerkInterface { } this.#pageLifecycle?.onPageFocus(async () => { - if (!this.session) { + if (!this.session || this.#touchedWhileOffline) { return; } @@ -2015,7 +2014,23 @@ export class Clerk implements ClerkInterface { return this.#touchLastActiveSession(this.session); }; - this.#offlineScheduler.schedule(performTouch); + if (isBrowserOnline()) { + return performTouch(); + } + + this.#touchedWhileOffline = true; + const controller = new AbortController(); + window.addEventListener( + 'online', + () => { + void performTouch(); + this.#touchedWhileOffline = false; + controller.abort(); + }, + { + signal: controller.signal, + }, + ); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { diff --git a/packages/clerk-js/src/utils/offlineScheduler.ts b/packages/clerk-js/src/utils/offlineScheduler.ts deleted file mode 100644 index 54ffd73ad81..00000000000 --- a/packages/clerk-js/src/utils/offlineScheduler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { isBrowserOnline } from '@clerk/shared/browser'; - -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, - }; -}; From 64cf3d49262e7b153fda6afbba8c15069fe02f88 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 19:41:07 +0200 Subject: [PATCH 06/21] Revert "handler `/tokens`" This reverts commit 40d0c80a1410c5788182b75b8bda0500cfa72cdf. --- .../src/core/auth/AuthCookieService.ts | 33 +------------ .../src/core/auth/SessionCookiePoller.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 47 +++++++++---------- .../clerk-js/src/core/resources/Session.ts | 4 +- 4 files changed, 27 insertions(+), 59 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 7e30d6cdf1a..530d0cc4c4b 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -1,4 +1,3 @@ -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'; @@ -40,7 +39,6 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; - private isRefreshTokenOnFocusPending = false; public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -120,36 +118,13 @@ export class AuthCookieService { private refreshTokenOnFocus() { window.addEventListener('focus', () => { - const refreshImmediately = () => { + if (document.visibilityState === 'visible') { // Certain data-fetching libraries that refetch on focus (such as swr) use setTimeout(cb, 0) to schedule a task on the event loop. // This gives us an opportunity to ensure the session cookie is updated with a fresh token before the fetch occurs, but it needs to // 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. - return this.refreshSessionToken({ updateCookieImmediately: true }); - }; - if (document.visibilityState === 'visible') { - if (this.isRefreshTokenOnFocusPending) { - return; - } - - if (isBrowserOnline()) { - return void refreshImmediately(); - } - - this.isRefreshTokenOnFocusPending = true; - const controller = new AbortController(); - window.addEventListener( - 'online', - () => { - void refreshImmediately(); - this.isRefreshTokenOnFocusPending = false; - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + void this.refreshSessionToken({ updateCookieImmediately: true }); } }); } @@ -163,10 +138,6 @@ export class AuthCookieService { return; } - if (!isBrowserOnline()) { - return; - } - try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { diff --git a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts index 4497b87c28d..dd4ff1f2a05 100644 --- a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts @@ -3,7 +3,7 @@ import { createWorkerTimers } from '@clerk/shared/workerTimers'; import { SafeLock } from './safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; -const INTERVAL_IN_MS = 5 * 1_000; +const INTERVAL_IN_MS = 5 * 1000; export class SessionCookiePoller { private lock = SafeLock(REFRESH_SESSION_TOKEN_LOCK_KEY); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f0657833aeb..43d296b71be 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -7,7 +7,7 @@ import { logger } from '@clerk/shared/logger'; import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; -import { handleValueOrFn, noop } from '@clerk/shared/utils'; +import { createDeferredPromise, handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, ActiveSessionResource, @@ -2001,36 +2001,35 @@ export class Clerk implements ClerkInterface { } this.#pageLifecycle?.onPageFocus(async () => { - if (!this.session || this.#touchedWhileOffline) { - return; - } + if (this.session) { + if (!isBrowserOnline()) { + if (this.#touchedWhileOffline) { + return; + } + this.#touchedWhileOffline = true; + const promiseWithResolvers = createDeferredPromise(); + const controller = new AbortController(); + window.addEventListener( + 'online', + e => { + promiseWithResolvers.resolve(e); + }, + { + signal: controller.signal, + }, + ); + await promiseWithResolvers.promise; + controller.abort(); + this.#touchedWhileOffline = false; + } - const performTouch = async () => { if (this.#touchThrottledUntil > Date.now()) { return; } this.#touchThrottledUntil = Date.now() + 5_000; - return this.#touchLastActiveSession(this.session); - }; - - if (isBrowserOnline()) { - return performTouch(); + void this.#touchLastActiveSession(this.session); } - - this.#touchedWhileOffline = true; - const controller = new AbortController(); - window.addEventListener( - 'online', - () => { - void performTouch(); - this.#touchedWhileOffline = false; - controller.abort(); - }, - { - signal: controller.signal, - }, - ); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 67684eff70d..2a6c1b6e3aa 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,5 +1,4 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; -import { isBrowserOnline } from '@clerk/shared/browser'; import { is4xxError } from '@clerk/shared/error'; import { runWithExponentialBackOff } from '@clerk/shared/utils'; import type { @@ -86,8 +85,7 @@ export class Session extends BaseResource implements SessionResource { getToken: GetToken = async (options?: GetTokenOptions): Promise => { return runWithExponentialBackOff(() => this._getToken(options), { - shouldRetry: (error: unknown, currentIteration: number) => - !is4xxError(error) && currentIteration < 4 && isBrowserOnline(), + shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4, }); }; From 486fe226ae678c36a013166953ed6d7fa66e3f9b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 19:49:18 +0200 Subject: [PATCH 07/21] handle `/tokens` --- .../src/core/auth/AuthCookieService.ts | 33 +++++++++++++++++-- .../src/core/auth/SessionCookiePoller.ts | 2 +- .../clerk-js/src/core/resources/Session.ts | 4 ++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 530d0cc4c4b..7e30d6cdf1a 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -1,3 +1,4 @@ +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'; @@ -39,6 +40,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; + private isRefreshTokenOnFocusPending = false; public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -118,13 +120,36 @@ export class AuthCookieService { private refreshTokenOnFocus() { window.addEventListener('focus', () => { - if (document.visibilityState === 'visible') { + const refreshImmediately = () => { // Certain data-fetching libraries that refetch on focus (such as swr) use setTimeout(cb, 0) to schedule a task on the event loop. // This gives us an opportunity to ensure the session cookie is updated with a fresh token before the fetch occurs, but it needs to // 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 }); + return this.refreshSessionToken({ updateCookieImmediately: true }); + }; + if (document.visibilityState === 'visible') { + if (this.isRefreshTokenOnFocusPending) { + return; + } + + if (isBrowserOnline()) { + return void refreshImmediately(); + } + + this.isRefreshTokenOnFocusPending = true; + const controller = new AbortController(); + window.addEventListener( + 'online', + () => { + void refreshImmediately(); + this.isRefreshTokenOnFocusPending = false; + controller.abort(); + }, + { + signal: controller.signal, + }, + ); } }); } @@ -138,6 +163,10 @@ export class AuthCookieService { return; } + if (!isBrowserOnline()) { + return; + } + try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { diff --git a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts index dd4ff1f2a05..4497b87c28d 100644 --- a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts @@ -3,7 +3,7 @@ import { createWorkerTimers } from '@clerk/shared/workerTimers'; import { SafeLock } from './safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; -const INTERVAL_IN_MS = 5 * 1000; +const INTERVAL_IN_MS = 5 * 1_000; export class SessionCookiePoller { private lock = SafeLock(REFRESH_SESSION_TOKEN_LOCK_KEY); diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 2a6c1b6e3aa..67684eff70d 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,4 +1,5 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; +import { isBrowserOnline } from '@clerk/shared/browser'; import { is4xxError } from '@clerk/shared/error'; import { runWithExponentialBackOff } from '@clerk/shared/utils'; import type { @@ -85,7 +86,8 @@ export class Session extends BaseResource implements SessionResource { getToken: GetToken = async (options?: GetTokenOptions): Promise => { return runWithExponentialBackOff(() => this._getToken(options), { - shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4, + shouldRetry: (error: unknown, currentIteration: number) => + !is4xxError(error) && currentIteration < 4 && isBrowserOnline(), }); }; From cc56db2242e68ca53af780384d0d7aab1ee5c0aa Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 19:57:04 +0200 Subject: [PATCH 08/21] Revert "handle `/tokens`" This reverts commit 486fe226ae678c36a013166953ed6d7fa66e3f9b. --- .../src/core/auth/AuthCookieService.ts | 33 ++----------------- .../src/core/auth/SessionCookiePoller.ts | 2 +- .../clerk-js/src/core/resources/Session.ts | 4 +-- 3 files changed, 4 insertions(+), 35 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 7e30d6cdf1a..530d0cc4c4b 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -1,4 +1,3 @@ -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'; @@ -40,7 +39,6 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; - private isRefreshTokenOnFocusPending = false; public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -120,36 +118,13 @@ export class AuthCookieService { private refreshTokenOnFocus() { window.addEventListener('focus', () => { - const refreshImmediately = () => { + if (document.visibilityState === 'visible') { // Certain data-fetching libraries that refetch on focus (such as swr) use setTimeout(cb, 0) to schedule a task on the event loop. // This gives us an opportunity to ensure the session cookie is updated with a fresh token before the fetch occurs, but it needs to // 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. - return this.refreshSessionToken({ updateCookieImmediately: true }); - }; - if (document.visibilityState === 'visible') { - if (this.isRefreshTokenOnFocusPending) { - return; - } - - if (isBrowserOnline()) { - return void refreshImmediately(); - } - - this.isRefreshTokenOnFocusPending = true; - const controller = new AbortController(); - window.addEventListener( - 'online', - () => { - void refreshImmediately(); - this.isRefreshTokenOnFocusPending = false; - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + void this.refreshSessionToken({ updateCookieImmediately: true }); } }); } @@ -163,10 +138,6 @@ export class AuthCookieService { return; } - if (!isBrowserOnline()) { - return; - } - try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { diff --git a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts index 4497b87c28d..dd4ff1f2a05 100644 --- a/packages/clerk-js/src/core/auth/SessionCookiePoller.ts +++ b/packages/clerk-js/src/core/auth/SessionCookiePoller.ts @@ -3,7 +3,7 @@ import { createWorkerTimers } from '@clerk/shared/workerTimers'; import { SafeLock } from './safeLock'; const REFRESH_SESSION_TOKEN_LOCK_KEY = 'clerk.lock.refreshSessionToken'; -const INTERVAL_IN_MS = 5 * 1_000; +const INTERVAL_IN_MS = 5 * 1000; export class SessionCookiePoller { private lock = SafeLock(REFRESH_SESSION_TOKEN_LOCK_KEY); diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 67684eff70d..2a6c1b6e3aa 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -1,5 +1,4 @@ import { createCheckAuthorization } from '@clerk/shared/authorization'; -import { isBrowserOnline } from '@clerk/shared/browser'; import { is4xxError } from '@clerk/shared/error'; import { runWithExponentialBackOff } from '@clerk/shared/utils'; import type { @@ -86,8 +85,7 @@ export class Session extends BaseResource implements SessionResource { getToken: GetToken = async (options?: GetTokenOptions): Promise => { return runWithExponentialBackOff(() => this._getToken(options), { - shouldRetry: (error: unknown, currentIteration: number) => - !is4xxError(error) && currentIteration < 4 && isBrowserOnline(), + shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4, }); }; From 4994b6df07245da2f2a15d1962acbc294de07fac Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 19:58:50 +0200 Subject: [PATCH 09/21] add cleanup --- packages/clerk-js/src/core/clerk.ts | 47 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 43d296b71be..f0657833aeb 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -7,7 +7,7 @@ import { logger } from '@clerk/shared/logger'; import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; -import { createDeferredPromise, handleValueOrFn, noop } from '@clerk/shared/utils'; +import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, ActiveSessionResource, @@ -2001,35 +2001,36 @@ export class Clerk implements ClerkInterface { } this.#pageLifecycle?.onPageFocus(async () => { - if (this.session) { - if (!isBrowserOnline()) { - if (this.#touchedWhileOffline) { - return; - } - this.#touchedWhileOffline = true; - const promiseWithResolvers = createDeferredPromise(); - const controller = new AbortController(); - window.addEventListener( - 'online', - e => { - promiseWithResolvers.resolve(e); - }, - { - signal: controller.signal, - }, - ); - await promiseWithResolvers.promise; - controller.abort(); - this.#touchedWhileOffline = false; - } + if (!this.session || this.#touchedWhileOffline) { + return; + } + const performTouch = async () => { if (this.#touchThrottledUntil > Date.now()) { return; } this.#touchThrottledUntil = Date.now() + 5_000; - void this.#touchLastActiveSession(this.session); + return this.#touchLastActiveSession(this.session); + }; + + if (isBrowserOnline()) { + return performTouch(); } + + this.#touchedWhileOffline = true; + const controller = new AbortController(); + window.addEventListener( + 'online', + () => { + void performTouch(); + this.#touchedWhileOffline = false; + controller.abort(); + }, + { + signal: controller.signal, + }, + ); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { From f0ffdac707e17b57e9687f590ed37515307641ac Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 20:19:45 +0200 Subject: [PATCH 10/21] explicit offline scheduler --- packages/clerk-js/src/core/clerk.ts | 25 +++------------ .../clerk-js/src/utils/offlineScheduler.ts | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 packages/clerk-js/src/utils/offlineScheduler.ts diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f0657833aeb..d1634d92599 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1,4 +1,4 @@ -import { inBrowser as inClientSide, isBrowserOnline, isValidBrowserOnline } from '@clerk/shared/browser'; +import { inBrowser as inClientSide, isValidBrowserOnline } from '@clerk/shared/browser'; import { deprecated } from '@clerk/shared/deprecated'; import { ClerkRuntimeError, is4xxError, isClerkAPIResponseError } from '@clerk/shared/error'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -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'; @@ -190,7 +191,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; - #touchedWhileOffline = false; + #offlineScheduler = createOfflineScheduler(); public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) @@ -2001,7 +2002,7 @@ export class Clerk implements ClerkInterface { } this.#pageLifecycle?.onPageFocus(async () => { - if (!this.session || this.#touchedWhileOffline) { + if (!this.session) { return; } @@ -2014,23 +2015,7 @@ export class Clerk implements ClerkInterface { return this.#touchLastActiveSession(this.session); }; - if (isBrowserOnline()) { - return performTouch(); - } - - this.#touchedWhileOffline = true; - const controller = new AbortController(); - window.addEventListener( - 'online', - () => { - void performTouch(); - this.#touchedWhileOffline = false; - controller.abort(); - }, - { - signal: controller.signal, - }, - ); + this.#offlineScheduler.schedule(performTouch); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { diff --git a/packages/clerk-js/src/utils/offlineScheduler.ts b/packages/clerk-js/src/utils/offlineScheduler.ts new file mode 100644 index 00000000000..54ffd73ad81 --- /dev/null +++ b/packages/clerk-js/src/utils/offlineScheduler.ts @@ -0,0 +1,32 @@ +import { isBrowserOnline } from '@clerk/shared/browser'; + +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, + }; +}; From ed34e41cac28b5efb9b64acd28be2baecef0e60d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 10 Feb 2025 20:35:43 +0200 Subject: [PATCH 11/21] update auth cookie service --- packages/clerk-js/src/core/auth/AuthCookieService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 530d0cc4c4b..69f18782717 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -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'; @@ -39,6 +41,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; + private offlineScheduler = createOfflineScheduler(); public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -124,7 +127,7 @@ 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 }); + this.offlineScheduler.schedule(() => this.refreshSessionToken({ updateCookieImmediately: true })); } }); } @@ -138,6 +141,10 @@ export class AuthCookieService { return; } + if (!isBrowserOnline()) { + return; + } + try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { From 649981c34eef5163d18cbd55d90097eebcfe1a21 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 12:04:45 +0200 Subject: [PATCH 12/21] remove offline scheduler from auth cookie service --- packages/clerk-js/src/core/auth/AuthCookieService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 69f18782717..7ed1cb21330 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -4,7 +4,6 @@ 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'; @@ -41,7 +40,6 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; - private offlineScheduler = createOfflineScheduler(); public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -127,7 +125,7 @@ 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. - this.offlineScheduler.schedule(() => this.refreshSessionToken({ updateCookieImmediately: true })); + void this.refreshSessionToken({ updateCookieImmediately: true }); } }); } From e2a8f3a6297176888b87d1b76f6b49c1bb8950ac Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 12:20:56 +0200 Subject: [PATCH 13/21] remove offline check from AuthCookieService.ts --- packages/clerk-js/src/core/auth/AuthCookieService.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 7ed1cb21330..530d0cc4c4b 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -1,4 +1,3 @@ -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'; @@ -139,10 +138,6 @@ export class AuthCookieService { return; } - if (!isBrowserOnline()) { - return; - } - try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { From 6e1f1a56a9b9c533868cb80ed6ee295729f128da Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 12:30:44 +0200 Subject: [PATCH 14/21] Revert "remove offline scheduler from auth cookie service" This reverts commit 649981c34eef5163d18cbd55d90097eebcfe1a21. --- packages/clerk-js/src/core/auth/AuthCookieService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 530d0cc4c4b..1819a9b39f0 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -3,6 +3,7 @@ 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'; @@ -39,6 +40,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; + private offlineScheduler = createOfflineScheduler(); public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -124,7 +126,7 @@ 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 }); + this.offlineScheduler.schedule(() => this.refreshSessionToken({ updateCookieImmediately: true })); } }); } From 43074fef28eec35ce39cb94183fcd85d71025047 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 12:47:33 +0200 Subject: [PATCH 15/21] force online --- integration/tests/sign-out-smoke.test.ts | 7 +++++++ packages/clerk-js/src/core/auth/AuthCookieService.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/integration/tests/sign-out-smoke.test.ts b/integration/tests/sign-out-smoke.test.ts index 129947774c7..647e352757c 100644 --- a/integration/tests/sign-out-smoke.test.ts +++ b/integration/tests/sign-out-smoke.test.ts @@ -22,6 +22,9 @@ 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(() => { + Object.defineProperty(Object.getPrototypeOf(navigator), 'onLine', { value: true }); + }); await mainTab.po.signIn.goTo(); await mainTab.po.signIn.setIdentifier(fakeUser.email); await mainTab.po.signIn.continue(); @@ -30,6 +33,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out await mainTab.po.expect.toBeSignedIn(); await mainTab.tabs.runInNewTab(async m => { + await m.page.addInitScript(() => { + Object.defineProperty(Object.getPrototypeOf(navigator), 'onLine', { value: true }); + }); + await m.page.goToAppHome(); await m.page.waitForClerkJsLoaded(); diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 1819a9b39f0..69f18782717 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -1,3 +1,4 @@ +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'; @@ -140,6 +141,10 @@ export class AuthCookieService { return; } + if (!isBrowserOnline()) { + return; + } + try { const token = await this.clerk.session.getToken(); if (updateCookieImmediately) { From 5b122a64c9d99ab8021e588703e7b71e1f1d05a8 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 13:46:16 +0200 Subject: [PATCH 16/21] relax restrictions in `isBrowserOnline` --- packages/shared/src/browser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/browser.ts b/packages/shared/src/browser.ts index 399ff23c7d2..6cbae78e8f0 100644 --- a/packages/shared/src/browser.ts +++ b/packages/shared/src/browser.ts @@ -75,7 +75,7 @@ export function isBrowserOnline(): boolean { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection#browser_compatibility // @ts-ignore const isExperimentalConnectionOnline = navigator?.connection?.rtt !== 0 && navigator?.connection?.downlink !== 0; - return isExperimentalConnectionOnline && isNavigatorOnline; + return isExperimentalConnectionOnline || isNavigatorOnline; } /** From a50e808d0c2cd306b65b0eb5b658982c59b00912 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 14:31:39 +0200 Subject: [PATCH 17/21] mock `navigator?.connection` --- integration/tests/sign-out-smoke.test.ts | 5 ++++- packages/shared/src/browser.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/integration/tests/sign-out-smoke.test.ts b/integration/tests/sign-out-smoke.test.ts index 647e352757c..3567741b719 100644 --- a/integration/tests/sign-out-smoke.test.ts +++ b/integration/tests/sign-out-smoke.test.ts @@ -23,7 +23,10 @@ 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(() => { - Object.defineProperty(Object.getPrototypeOf(navigator), 'onLine', { value: true }); + // @ts-expect-error + if (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); diff --git a/packages/shared/src/browser.ts b/packages/shared/src/browser.ts index 6cbae78e8f0..399ff23c7d2 100644 --- a/packages/shared/src/browser.ts +++ b/packages/shared/src/browser.ts @@ -75,7 +75,7 @@ export function isBrowserOnline(): boolean { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection#browser_compatibility // @ts-ignore const isExperimentalConnectionOnline = navigator?.connection?.rtt !== 0 && navigator?.connection?.downlink !== 0; - return isExperimentalConnectionOnline || isNavigatorOnline; + return isExperimentalConnectionOnline && isNavigatorOnline; } /** From cea243ebd9caa8146b9c0d05deb2a0dc4e8d1a31 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 14:52:31 +0200 Subject: [PATCH 18/21] cleanup --- integration/tests/sign-out-smoke.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/integration/tests/sign-out-smoke.test.ts b/integration/tests/sign-out-smoke.test.ts index 3567741b719..d2c57c819a5 100644 --- a/integration/tests/sign-out-smoke.test.ts +++ b/integration/tests/sign-out-smoke.test.ts @@ -23,8 +23,14 @@ 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(() => { - // @ts-expect-error - if (navigator?.connection?.rtt === 0 || navigator?.downlink?.rtt === 0) { + /** + * 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 }); } }); @@ -36,10 +42,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out await mainTab.po.expect.toBeSignedIn(); await mainTab.tabs.runInNewTab(async m => { - await m.page.addInitScript(() => { - Object.defineProperty(Object.getPrototypeOf(navigator), 'onLine', { value: true }); - }); - await m.page.goToAppHome(); await m.page.waitForClerkJsLoaded(); From e25b21790b5028838cde332d37c10ea2ee8c7c92 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Feb 2025 15:14:41 +0200 Subject: [PATCH 19/21] Update .changeset/breezy-dogs-greet.md --- .changeset/breezy-dogs-greet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/breezy-dogs-greet.md b/.changeset/breezy-dogs-greet.md index ad152f6ead8..2f6cb584661 100644 --- a/.changeset/breezy-dogs-greet.md +++ b/.changeset/breezy-dogs-greet.md @@ -2,4 +2,4 @@ '@clerk/clerk-js': patch --- -Pause session touch when browser is offline, and resume it when the device comes back online. +Pause session touch and token refresh while browser is offline, and resume it when the device comes back online. From 39c9745b968d9311af00b63595bd77f915e999a1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 17 Feb 2025 19:47:05 +0200 Subject: [PATCH 20/21] add comments and rename --- packages/clerk-js/src/core/auth/AuthCookieService.ts | 5 +++-- packages/clerk-js/src/core/clerk.ts | 4 ++-- packages/clerk-js/src/utils/offlineScheduler.ts | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 69f18782717..4ed81e7fc75 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -41,7 +41,7 @@ export class AuthCookieService { private sessionCookie: SessionCookieHandler; private activeOrgCookie: ReturnType; private devBrowser: DevBrowser; - private offlineScheduler = createOfflineScheduler(); + private sessionRefreshOfflineScheduler = createOfflineScheduler(); public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) { const cookieSuffix = await getCookieSuffix(clerk.publishableKey); @@ -127,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. - this.offlineScheduler.schedule(() => 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 })); } }); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d1634d92599..6bf5328c44d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -191,7 +191,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; - #offlineScheduler = createOfflineScheduler(); + #sessionTouchOfflineScheduler = createOfflineScheduler(); public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) @@ -2015,7 +2015,7 @@ export class Clerk implements ClerkInterface { return this.#touchLastActiveSession(this.session); }; - this.#offlineScheduler.schedule(performTouch); + this.#sessionTouchOfflineScheduler.schedule(performTouch); }); this.#broadcastChannel?.addEventListener('message', ({ data }) => { diff --git a/packages/clerk-js/src/utils/offlineScheduler.ts b/packages/clerk-js/src/utils/offlineScheduler.ts index 54ffd73ad81..27bf6885ac7 100644 --- a/packages/clerk-js/src/utils/offlineScheduler.ts +++ b/packages/clerk-js/src/utils/offlineScheduler.ts @@ -1,5 +1,9 @@ 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; From 1366045db168afd899654f6c0e3b3679e9694fd1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 17 Feb 2025 19:49:55 +0200 Subject: [PATCH 21/21] remove necessary changes --- packages/clerk-js/src/core/clerk.ts | 4 ++-- packages/clerk-js/src/utils/pageLifecycle.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 6bf5328c44d..6129c8b47d7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2001,12 +2001,12 @@ export class Clerk implements ClerkInterface { return; } - this.#pageLifecycle?.onPageFocus(async () => { + this.#pageLifecycle?.onPageFocus(() => { if (!this.session) { return; } - const performTouch = async () => { + const performTouch = () => { if (this.#touchThrottledUntil > Date.now()) { return; } diff --git a/packages/clerk-js/src/utils/pageLifecycle.ts b/packages/clerk-js/src/utils/pageLifecycle.ts index d85a4bb921a..663c586e474 100644 --- a/packages/clerk-js/src/utils/pageLifecycle.ts +++ b/packages/clerk-js/src/utils/pageLifecycle.ts @@ -16,18 +16,17 @@ export const createPageLifecycle = () => { return { onPageFocus: noop }; } - const callbackQueue: Record void | Promise>> = { + const callbackQueue: Record void>> = { focus: [], }; window.addEventListener('focus', () => { if (document.visibilityState === 'visible') { - // eslint-disable-next-line @typescript-eslint/no-misused-promises callbackQueue['focus'].forEach(cb => cb()); } }); - const onPageFocus = (cb: () => void | Promise) => { + const onPageFocus = (cb: () => void) => { callbackQueue['focus'].push(cb); };