From 0ae5673aaa52104f3b81010152634c329668bb4b Mon Sep 17 00:00:00 2001 From: pengx17 Date: Wed, 4 Sep 2024 10:46:43 +0000 Subject: [PATCH] feat(electron): add offline mode (#8086) fix AF-1334 It seems `session.enableNetworkEmulation({ offline: true });` does not work - https://github.com/electron/electron/issues/21250 implemented using an in-house solution. When turned on: ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/6805735b-1006-4e51-be46-c047b0f1a82c.png) --- .../src/modules/feature-flag/constant.ts | 7 ++ .../frontend/electron/src/main/protocol.ts | 15 +++- .../src/main/updater/electron-updater.ts | 3 +- packages/frontend/electron/src/main/utils.ts | 68 ++++--------------- 4 files changed, 37 insertions(+), 56 deletions(-) diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index 1e5889aa54..719edc83d6 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 b8bc02959a..201566dd58 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 a32d75fbed..0609bd8028 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 42f0a6802e..f95443b87b 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; } -} +};