diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index 1e5889aa5..719edc83d 100644 --- a/packages/common/infra/src/modules/feature-flag/constant.ts +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -96,6 +96,13 @@ export const AFFINE_FLAGS = { configurable: isCanaryBuild, defaultState: isCanaryBuild, }, + enable_offline_mode: { + category: 'affine', + displayName: 'Offline Mode', + description: 'Enables offline mode.', + configurable: isDesktopEnvironment, + defaultState: false, + }, } satisfies { [key in string]: FlagInfo }; export type AFFINE_FLAGS = typeof AFFINE_FLAGS; diff --git a/packages/frontend/electron/src/main/protocol.ts b/packages/frontend/electron/src/main/protocol.ts index b8bc02959..201566dd5 100644 --- a/packages/frontend/electron/src/main/protocol.ts +++ b/packages/frontend/electron/src/main/protocol.ts @@ -3,6 +3,7 @@ import { join } from 'node:path'; import { net, protocol, session } from 'electron'; import { CLOUD_BASE_URL } from './config'; +import { isOfflineModeEnabled } from './utils'; import { getCookies } from './windows-manager'; protocol.registerSchemesAsPrivileged([ @@ -105,9 +106,21 @@ export function registerProtocol() { session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { const url = new URL(details.url); const pathname = url.pathname; + const protocol = url.protocol; + const origin = url.origin; + + const sameOrigin = origin === CLOUD_BASE_URL || protocol === 'file:'; + + if (isOfflineModeEnabled() && (sameOrigin || 'devtools:' !== protocol)) { + callback({ + cancel: true, + }); + return; + } + // session cookies are set to file:// on production // if sending request to the cloud, attach the session cookie (to affine cloud server) - if (isNetworkResource(pathname)) { + if (isNetworkResource(pathname) && sameOrigin) { const cookie = getCookies(); if (cookie) { const cookieString = cookie.map(c => `${c.name}=${c.value}`).join('; '); diff --git a/packages/frontend/electron/src/main/updater/electron-updater.ts b/packages/frontend/electron/src/main/updater/electron-updater.ts index a32d75fbe..0609bd802 100644 --- a/packages/frontend/electron/src/main/updater/electron-updater.ts +++ b/packages/frontend/electron/src/main/updater/electron-updater.ts @@ -3,6 +3,7 @@ import { autoUpdater as defaultAutoUpdater } from 'electron-updater'; import { buildType } from '../config'; import { logger } from '../logger'; +import { isOfflineModeEnabled } from '../utils'; import { AFFiNEUpdateProvider } from './affine-update-provider'; import { updaterSubjects } from './event'; import { WindowsUpdater } from './windows-updater'; @@ -54,7 +55,7 @@ export const setConfig = (newConfig: Partial = {}): void => { }; export const checkForUpdates = async () => { - if (disabled || checkingUpdate) { + if (disabled || checkingUpdate || isOfflineModeEnabled()) { return; } checkingUpdate = true; diff --git a/packages/frontend/electron/src/main/utils.ts b/packages/frontend/electron/src/main/utils.ts index 42f0a6802..f95443b87 100644 --- a/packages/frontend/electron/src/main/utils.ts +++ b/packages/frontend/electron/src/main/utils.ts @@ -1,8 +1,8 @@ -import http from 'node:http'; -import https from 'node:https'; - import type { CookiesSetDetails } from 'electron'; +import { logger } from './logger'; +import { globalStateStorage } from './shared-storage/storage'; + export function parseCookie( cookieString: string, url: string @@ -56,55 +56,15 @@ export function parseCookie( return details; } -/** - * Send a GET request to a specified URL. - * This function uses native http/https modules instead of fetch to - * bypassing set-cookies headers - */ -export async function simpleGet(requestUrl: string): Promise<{ - body: string; - headers: [string, string][]; - statusCode: number; -}> { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(requestUrl); - const protocol = parsedUrl.protocol === 'https:' ? https : http; - const options = { - hostname: parsedUrl.hostname, - port: parsedUrl.port, - path: parsedUrl.pathname + parsedUrl.search, - method: 'GET', - }; - const req = protocol.request(options, res => { - let data = ''; - res.on('data', chunk => { - data += chunk; - }); - res.on('end', () => { - resolve({ - body: data, - headers: toStandardHeaders(res.headers), - statusCode: res.statusCode || 200, - }); - }); - }); - req.on('error', error => { - reject(error); - }); - req.end(); - }); - - function toStandardHeaders(headers: http.IncomingHttpHeaders) { - const result: [string, string][] = []; - for (const [key, value] of Object.entries(headers)) { - if (Array.isArray(value)) { - value.forEach(v => { - result.push([key, v]); - }); - } else { - result.push([key, value || '']); - } - } - return result; +export const isOfflineModeEnabled = () => { + try { + return ( + // todo(pengx17): better abstraction for syncing flags with electron + // packages/common/infra/src/modules/feature-flag/entities/flags.ts + globalStateStorage.get('affine-flag:enable_offline_mode') ?? false + ); + } catch (error) { + logger.error('Failed to get offline mode flag', error); + return false; } -} +};