Skip to content

Commit 0ae5673

Browse files
committed
feat(electron): add offline mode (#8086)
fix AF-1334 It seems `session.enableNetworkEmulation({ offline: true });` does not work - electron/electron#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)
1 parent 51bc40d commit 0ae5673

File tree

4 files changed

+37
-56
lines changed

4 files changed

+37
-56
lines changed

packages/common/infra/src/modules/feature-flag/constant.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ export const AFFINE_FLAGS = {
9696
configurable: isCanaryBuild,
9797
defaultState: isCanaryBuild,
9898
},
99+
enable_offline_mode: {
100+
category: 'affine',
101+
displayName: 'Offline Mode',
102+
description: 'Enables offline mode.',
103+
configurable: isDesktopEnvironment,
104+
defaultState: false,
105+
},
99106
} satisfies { [key in string]: FlagInfo };
100107

101108
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;

packages/frontend/electron/src/main/protocol.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join } from 'node:path';
33
import { net, protocol, session } from 'electron';
44

55
import { CLOUD_BASE_URL } from './config';
6+
import { isOfflineModeEnabled } from './utils';
67
import { getCookies } from './windows-manager';
78

89
protocol.registerSchemesAsPrivileged([
@@ -105,9 +106,21 @@ export function registerProtocol() {
105106
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
106107
const url = new URL(details.url);
107108
const pathname = url.pathname;
109+
const protocol = url.protocol;
110+
const origin = url.origin;
111+
112+
const sameOrigin = origin === CLOUD_BASE_URL || protocol === 'file:';
113+
114+
if (isOfflineModeEnabled() && (sameOrigin || 'devtools:' !== protocol)) {
115+
callback({
116+
cancel: true,
117+
});
118+
return;
119+
}
120+
108121
// session cookies are set to file:// on production
109122
// if sending request to the cloud, attach the session cookie (to affine cloud server)
110-
if (isNetworkResource(pathname)) {
123+
if (isNetworkResource(pathname) && sameOrigin) {
111124
const cookie = getCookies();
112125
if (cookie) {
113126
const cookieString = cookie.map(c => `${c.name}=${c.value}`).join('; ');

packages/frontend/electron/src/main/updater/electron-updater.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { autoUpdater as defaultAutoUpdater } from 'electron-updater';
33

44
import { buildType } from '../config';
55
import { logger } from '../logger';
6+
import { isOfflineModeEnabled } from '../utils';
67
import { AFFiNEUpdateProvider } from './affine-update-provider';
78
import { updaterSubjects } from './event';
89
import { WindowsUpdater } from './windows-updater';
@@ -54,7 +55,7 @@ export const setConfig = (newConfig: Partial<UpdaterConfig> = {}): void => {
5455
};
5556

5657
export const checkForUpdates = async () => {
57-
if (disabled || checkingUpdate) {
58+
if (disabled || checkingUpdate || isOfflineModeEnabled()) {
5859
return;
5960
}
6061
checkingUpdate = true;
Lines changed: 14 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import http from 'node:http';
2-
import https from 'node:https';
3-
41
import type { CookiesSetDetails } from 'electron';
52

3+
import { logger } from './logger';
4+
import { globalStateStorage } from './shared-storage/storage';
5+
66
export function parseCookie(
77
cookieString: string,
88
url: string
@@ -56,55 +56,15 @@ export function parseCookie(
5656
return details;
5757
}
5858

59-
/**
60-
* Send a GET request to a specified URL.
61-
* This function uses native http/https modules instead of fetch to
62-
* bypassing set-cookies headers
63-
*/
64-
export async function simpleGet(requestUrl: string): Promise<{
65-
body: string;
66-
headers: [string, string][];
67-
statusCode: number;
68-
}> {
69-
return new Promise((resolve, reject) => {
70-
const parsedUrl = new URL(requestUrl);
71-
const protocol = parsedUrl.protocol === 'https:' ? https : http;
72-
const options = {
73-
hostname: parsedUrl.hostname,
74-
port: parsedUrl.port,
75-
path: parsedUrl.pathname + parsedUrl.search,
76-
method: 'GET',
77-
};
78-
const req = protocol.request(options, res => {
79-
let data = '';
80-
res.on('data', chunk => {
81-
data += chunk;
82-
});
83-
res.on('end', () => {
84-
resolve({
85-
body: data,
86-
headers: toStandardHeaders(res.headers),
87-
statusCode: res.statusCode || 200,
88-
});
89-
});
90-
});
91-
req.on('error', error => {
92-
reject(error);
93-
});
94-
req.end();
95-
});
96-
97-
function toStandardHeaders(headers: http.IncomingHttpHeaders) {
98-
const result: [string, string][] = [];
99-
for (const [key, value] of Object.entries(headers)) {
100-
if (Array.isArray(value)) {
101-
value.forEach(v => {
102-
result.push([key, v]);
103-
});
104-
} else {
105-
result.push([key, value || '']);
106-
}
107-
}
108-
return result;
59+
export const isOfflineModeEnabled = () => {
60+
try {
61+
return (
62+
// todo(pengx17): better abstraction for syncing flags with electron
63+
// packages/common/infra/src/modules/feature-flag/entities/flags.ts
64+
globalStateStorage.get('affine-flag:enable_offline_mode') ?? false
65+
);
66+
} catch (error) {
67+
logger.error('Failed to get offline mode flag', error);
68+
return false;
10969
}
110-
}
70+
};

0 commit comments

Comments
 (0)