mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat: improve electron sandbox (#14156)
This commit is contained in:
@@ -2214,7 +2214,7 @@ describe('html to snapshot', () => {
|
|||||||
|
|
||||||
test('iframe', async () => {
|
test('iframe', async () => {
|
||||||
const html = template(
|
const html = template(
|
||||||
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&end=" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>`
|
`<iframe width="560" height="315" src="https://www.youtube.com/embed/QDsd0nyzwz0?start=&end=" title="YouTube video player" frameborder="0" allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin"></iframe>`
|
||||||
);
|
);
|
||||||
|
|
||||||
const blockSnapshot: BlockSnapshot = {
|
const blockSnapshot: BlockSnapshot = {
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent<EmbedFigmaMode
|
|||||||
<div class="affine-embed-figma-iframe-container">
|
<div class="affine-embed-figma-iframe-container">
|
||||||
<iframe
|
<iframe
|
||||||
src=${`https://www.figma.com/embed?embed_host=blocksuite&url=${url}`}
|
src=${`https://www.figma.com/embed?embed_host=blocksuite&url=${url}`}
|
||||||
allowfullscreen
|
sandbox="allow-same-origin allow-scripts allow-presentation"
|
||||||
|
allow="fullscreen"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
credentialless
|
credentialless
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { EmbedIframeConfigExtension } from '@blocksuite/affine-shared/services';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type EmbedIframeUrlValidationOptions,
|
||||||
|
validateEmbedIframeUrl,
|
||||||
|
} from '../../utils';
|
||||||
|
|
||||||
|
const BILIBILI_DEFAULT_WIDTH_IN_SURFACE = 800;
|
||||||
|
const BILIBILI_DEFAULT_HEIGHT_IN_SURFACE = 450;
|
||||||
|
const BILIBILI_DEFAULT_HEIGHT_IN_NOTE = 450;
|
||||||
|
const BILIBILI_DEFAULT_WIDTH_PERCENT = 100;
|
||||||
|
|
||||||
|
const bilibiliValidationOptions: EmbedIframeUrlValidationOptions = {
|
||||||
|
protocols: ['https:'],
|
||||||
|
hostnames: ['player.bilibili.com', 'www.bilibili.com', 'bilibili.com'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const biliPlayerValidationOptions: EmbedIframeUrlValidationOptions = {
|
||||||
|
protocols: ['https:'],
|
||||||
|
hostnames: ['player.bilibili.com'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const AV_REGEX = /av([0-9]+)/i;
|
||||||
|
const BV_REGEX = /(BV[0-9A-Za-z]{10})/;
|
||||||
|
|
||||||
|
const extractAvid = (url: string) => {
|
||||||
|
const match = url.match(AV_REGEX);
|
||||||
|
return match ? match[1] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractBvid = (url: string) => {
|
||||||
|
const match = url.match(BV_REGEX);
|
||||||
|
return match ? match[1] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildBiliPlayerEmbedUrl = (url: string) => {
|
||||||
|
// If the user pasted the embed URL directly, keep it
|
||||||
|
if (validateEmbedIframeUrl(url, biliPlayerValidationOptions)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
const avid = extractAvid(url);
|
||||||
|
if (avid) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
aid: avid,
|
||||||
|
autoplay: '0',
|
||||||
|
});
|
||||||
|
return `https://player.bilibili.com/player.html?${params.toString()}`;
|
||||||
|
}
|
||||||
|
const bvid = extractBvid(url);
|
||||||
|
if (bvid) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
bvid,
|
||||||
|
autoplay: '0',
|
||||||
|
});
|
||||||
|
return `https://player.bilibili.com/player.html?${params.toString()}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bilibiliConfig = {
|
||||||
|
name: 'bilibili',
|
||||||
|
match: (url: string) =>
|
||||||
|
validateEmbedIframeUrl(url, bilibiliValidationOptions) &&
|
||||||
|
(!!extractAvid(url) || !!extractBvid(url)),
|
||||||
|
buildOEmbedUrl: buildBiliPlayerEmbedUrl,
|
||||||
|
useOEmbedUrlDirectly: true,
|
||||||
|
options: {
|
||||||
|
widthInSurface: BILIBILI_DEFAULT_WIDTH_IN_SURFACE,
|
||||||
|
heightInSurface: BILIBILI_DEFAULT_HEIGHT_IN_SURFACE,
|
||||||
|
heightInNote: BILIBILI_DEFAULT_HEIGHT_IN_NOTE,
|
||||||
|
widthPercent: BILIBILI_DEFAULT_WIDTH_PERCENT,
|
||||||
|
allow: 'clipboard-write; encrypted-media; picture-in-picture',
|
||||||
|
sandbox: 'allow-same-origin allow-scripts',
|
||||||
|
style: 'border: none; border-radius: 8px;',
|
||||||
|
allowFullscreen: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BilibiliEmbedConfig = EmbedIframeConfigExtension(bilibiliConfig);
|
||||||
@@ -67,8 +67,9 @@ const genericConfig = {
|
|||||||
heightInNote: GENERIC_DEFAULT_HEIGHT_IN_NOTE,
|
heightInNote: GENERIC_DEFAULT_HEIGHT_IN_NOTE,
|
||||||
allowFullscreen: true,
|
allowFullscreen: true,
|
||||||
style: 'border: none; border-radius: 8px;',
|
style: 'border: none; border-radius: 8px;',
|
||||||
allow: 'clipboard-read; clipboard-write; picture-in-picture;',
|
allow: '',
|
||||||
referrerpolicy: 'no-referrer-when-downgrade',
|
referrerpolicy: 'no-referrer-when-downgrade',
|
||||||
|
sandbox: 'allow-scripts',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BilibiliEmbedConfig } from './bilibili';
|
||||||
import { ExcalidrawEmbedConfig } from './excalidraw';
|
import { ExcalidrawEmbedConfig } from './excalidraw';
|
||||||
import { GenericEmbedConfig } from './generic';
|
import { GenericEmbedConfig } from './generic';
|
||||||
import { GoogleDocsEmbedConfig } from './google-docs';
|
import { GoogleDocsEmbedConfig } from './google-docs';
|
||||||
@@ -11,5 +12,6 @@ export const EmbedIframeConfigExtensions = [
|
|||||||
MiroEmbedConfig,
|
MiroEmbedConfig,
|
||||||
ExcalidrawEmbedConfig,
|
ExcalidrawEmbedConfig,
|
||||||
GoogleDocsEmbedConfig,
|
GoogleDocsEmbedConfig,
|
||||||
|
BilibiliEmbedConfig,
|
||||||
GenericEmbedConfig,
|
GenericEmbedConfig,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
type ReadonlySignal,
|
type ReadonlySignal,
|
||||||
signal,
|
signal,
|
||||||
} from '@preact/signals-core';
|
} from '@preact/signals-core';
|
||||||
import { html } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
import { query } from 'lit/decorators.js';
|
import { query } from 'lit/decorators.js';
|
||||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
@@ -45,6 +45,10 @@ import { safeGetIframeSrc } from './utils.js';
|
|||||||
|
|
||||||
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
export type EmbedIframeStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
|
const TRUSTED_SANDBOX =
|
||||||
|
'allow-same-origin allow-scripts allow-forms allow-presentation';
|
||||||
|
const UNTRUSTED_SANDBOX = 'allow-scripts';
|
||||||
|
|
||||||
export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> {
|
export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIframeBlockModel> {
|
||||||
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
|
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -89,6 +93,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
|||||||
});
|
});
|
||||||
|
|
||||||
protected iframeOptions: IframeOptions | undefined = undefined;
|
protected iframeOptions: IframeOptions | undefined = undefined;
|
||||||
|
private currentConfigName: string | undefined;
|
||||||
|
|
||||||
get embedIframeService() {
|
get embedIframeService() {
|
||||||
return this.std.get(EmbedIframeService);
|
return this.std.get(EmbedIframeService);
|
||||||
@@ -279,6 +284,10 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
|||||||
const config = this.embedIframeService?.getConfig(url);
|
const config = this.embedIframeService?.getConfig(url);
|
||||||
if (config) {
|
if (config) {
|
||||||
this.iframeOptions = config.options;
|
this.iframeOptions = config.options;
|
||||||
|
this.currentConfigName = config.name;
|
||||||
|
} else {
|
||||||
|
this.iframeOptions = undefined;
|
||||||
|
this.currentConfigName = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,26 +337,46 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
|
|||||||
referrerpolicy,
|
referrerpolicy,
|
||||||
scrolling,
|
scrolling,
|
||||||
allowFullscreen,
|
allowFullscreen,
|
||||||
|
sandbox,
|
||||||
} = this.iframeOptions ?? {};
|
} = this.iframeOptions ?? {};
|
||||||
const width = `${widthPercent}%`;
|
const width = `${widthPercent}%`;
|
||||||
// if the block is in the surface, use 100% as the height
|
// if the block is in the surface, use 100% as the height
|
||||||
// otherwise, use the heightInNote
|
// otherwise, use the heightInNote
|
||||||
const height = this.inSurface ? '100%' : heightInNote;
|
const height = this.inSurface ? '100%' : heightInNote;
|
||||||
return html`
|
const sandboxValue =
|
||||||
<iframe
|
sandbox ??
|
||||||
|
(this.currentConfigName === 'generic'
|
||||||
|
? UNTRUSTED_SANDBOX
|
||||||
|
: TRUSTED_SANDBOX);
|
||||||
|
const sourceHost = this._getSourceHost();
|
||||||
|
return html`<iframe
|
||||||
width=${width ?? DEFAULT_IFRAME_WIDTH}
|
width=${width ?? DEFAULT_IFRAME_WIDTH}
|
||||||
height=${height ?? DEFAULT_IFRAME_HEIGHT}
|
height=${height ?? DEFAULT_IFRAME_HEIGHT}
|
||||||
?allowfullscreen=${allowFullscreen}
|
?allowfullscreen=${allowFullscreen}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
credentialless
|
credentialless
|
||||||
|
sandbox=${sandboxValue}
|
||||||
src=${ifDefined(iframeUrl)}
|
src=${ifDefined(iframeUrl)}
|
||||||
allow=${ifDefined(allow)}
|
allow=${ifDefined(allow)}
|
||||||
referrerpolicy=${ifDefined(referrerpolicy)}
|
referrerpolicy=${ifDefined(referrerpolicy)}
|
||||||
scrolling=${ifDefined(scrolling)}
|
scrolling=${ifDefined(scrolling)}
|
||||||
style=${ifDefined(style)}
|
style=${ifDefined(style)}
|
||||||
></iframe>
|
></iframe>
|
||||||
`;
|
${sourceHost
|
||||||
|
? html`<div class="affine-embed-iframe-source">${sourceHost}</div>`
|
||||||
|
: nothing}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _getSourceHost = () => {
|
||||||
|
const url = this.model.props.url ?? this.model.props.iframeUrl;
|
||||||
|
if (!url) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.hostname;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _renderContent = () => {
|
private readonly _renderContent = () => {
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ export const embedIframeBlockStyles = css`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.affine-embed-iframe-source {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.affine-embed-iframe-block-overlay.show {
|
.affine-embed-iframe-block-overlay.show {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ export class EmbedLoomBlockComponent extends EmbedBlockComponent<
|
|||||||
<iframe
|
<iframe
|
||||||
src=${`https://www.loom.com/embed/${videoId}?hide_title=true`}
|
src=${`https://www.loom.com/embed/${videoId}?hide_title=true`}
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-presentation"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
credentialless
|
credentialless
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export class EmbedYoutubeBlockComponent extends EmbedBlockComponent<
|
|||||||
type="text/html"
|
type="text/html"
|
||||||
src=${`https://www.youtube.com/embed/${videoId}`}
|
src=${`https://www.youtube.com/embed/${videoId}`}
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="fullscreen; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
|
||||||
allowfullscreen
|
sandbox="allow-scripts allow-same-origin allow-presentation"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
credentialless
|
credentialless
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type IframeOptions = {
|
|||||||
allow?: string;
|
allow?: string;
|
||||||
allowFullscreen?: boolean;
|
allowFullscreen?: boolean;
|
||||||
containerBorderRadius?: number;
|
containerBorderRadius?: number;
|
||||||
|
sandbox?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export class EmbedIframeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const oEmbedUrl = config.buildOEmbedUrl(url);
|
const oEmbedUrl = config.buildOEmbedUrl(url);
|
||||||
|
|
||||||
if (!oEmbedUrl) {
|
if (!oEmbedUrl) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"@opentelemetry/core": "^2.2.0",
|
"@opentelemetry/core": "^2.2.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.208.0",
|
"@opentelemetry/exporter-prometheus": "^0.208.0",
|
||||||
"@opentelemetry/exporter-zipkin": "^2.2.0",
|
"@opentelemetry/exporter-zipkin": "^2.2.0",
|
||||||
"@opentelemetry/host-metrics": "^0.37.0",
|
"@opentelemetry/host-metrics": "^0.38.0",
|
||||||
"@opentelemetry/instrumentation": "^0.208.0",
|
"@opentelemetry/instrumentation": "^0.208.0",
|
||||||
"@opentelemetry/instrumentation-graphql": "^0.56.0",
|
"@opentelemetry/instrumentation-graphql": "^0.56.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.208.0",
|
"@opentelemetry/instrumentation-http": "^0.208.0",
|
||||||
|
|||||||
@@ -4,15 +4,31 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|||||||
const SESSION_KEY = 'affine:debug';
|
const SESSION_KEY = 'affine:debug';
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
const getSessionValue = (key: string) => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(key);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSessionValue = (key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(key, value);
|
||||||
|
} catch {
|
||||||
|
// ignore if storage is not accessible (e.g., sandboxed renderer)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// enable debug logs if the URL search string contains `debug`
|
// enable debug logs if the URL search string contains `debug`
|
||||||
// e.g. http://localhost:3000/?debug
|
// e.g. http://localhost:3000/?debug
|
||||||
if (window.location.search.includes('debug')) {
|
if (window.location.search.includes('debug')) {
|
||||||
// enable debug logs for the current session
|
// enable debug logs for the current session
|
||||||
// since the query string may be removed by the browser after navigations,
|
// since the query string may be removed by the browser after navigations,
|
||||||
// we need to store the debug flag in sessionStorage
|
// we need to store the debug flag in sessionStorage
|
||||||
sessionStorage.setItem(SESSION_KEY, 'true');
|
setSessionValue(SESSION_KEY, 'true');
|
||||||
}
|
}
|
||||||
if (sessionStorage.getItem(SESSION_KEY) === 'true') {
|
if (getSessionValue(SESSION_KEY) === 'true') {
|
||||||
// enable all debug logs by default
|
// enable all debug logs by default
|
||||||
debug.enable('*');
|
debug.enable('*');
|
||||||
console.warn('Debug logs enabled');
|
console.warn('Debug logs enabled');
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { GraphQLError as BaseGraphQLError } from 'graphql';
|
|||||||
export type ErrorName =
|
export type ErrorName =
|
||||||
| keyof typeof ErrorNames
|
| keyof typeof ErrorNames
|
||||||
| 'NETWORK_ERROR'
|
| 'NETWORK_ERROR'
|
||||||
| 'CONTENT_TOO_LARGE';
|
| 'CONTENT_TOO_LARGE'
|
||||||
|
| 'REQUEST_ABORTED';
|
||||||
|
|
||||||
export interface UserFriendlyErrorResponse {
|
export interface UserFriendlyErrorResponse {
|
||||||
status: number;
|
status: number;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const anotherOrigin = `assets://${anotherHost}`;
|
|||||||
export const onboardingViewUrl = `${mainWindowOrigin}/onboarding`;
|
export const onboardingViewUrl = `${mainWindowOrigin}/onboarding`;
|
||||||
export const shellViewUrl = `${mainWindowOrigin}/shell.html`;
|
export const shellViewUrl = `${mainWindowOrigin}/shell.html`;
|
||||||
export const backgroundWorkerViewUrl = `${mainWindowOrigin}/background-worker.html`;
|
export const backgroundWorkerViewUrl = `${mainWindowOrigin}/background-worker.html`;
|
||||||
export const customThemeViewUrl = `${mainWindowOrigin}/theme-editor.html`;
|
export const customThemeViewUrl = `${mainWindowOrigin}/theme-editor`;
|
||||||
|
|
||||||
// mitigate the issue that popup window share the same zoom level of the main window
|
// mitigate the issue that popup window share the same zoom level of the main window
|
||||||
// Notes from electron official docs:
|
// Notes from electron official docs:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { BrowserWindow, WebContentsView } from 'electron';
|
import { ipcMain, webContents } from 'electron';
|
||||||
|
|
||||||
import { AFFINE_EVENT_CHANNEL_NAME } from '../shared/type';
|
import {
|
||||||
|
AFFINE_EVENT_CHANNEL_NAME,
|
||||||
|
AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME,
|
||||||
|
} from '../shared/type';
|
||||||
import { applicationMenuEvents } from './application-menu';
|
import { applicationMenuEvents } from './application-menu';
|
||||||
import { beforeAppQuit } from './cleanup';
|
import { beforeAppQuit } from './cleanup';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
@@ -19,12 +22,64 @@ export const allEvents = {
|
|||||||
popup: popupEvents,
|
popup: popupEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getActiveWindows() {
|
const subscriptions = new Map<number, Set<string>>();
|
||||||
return BrowserWindow.getAllWindows().filter(win => !win.isDestroyed());
|
|
||||||
|
function getTargetContents(channel: string) {
|
||||||
|
const targets: Electron.WebContents[] = [];
|
||||||
|
subscriptions.forEach((channels, id) => {
|
||||||
|
if (!channels.has(channel)) return;
|
||||||
|
const wc = webContents.fromId(id);
|
||||||
|
if (wc && !wc.isDestroyed()) {
|
||||||
|
targets.push(wc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSubscription(sender: Electron.WebContents, channel: string) {
|
||||||
|
const id = sender.id;
|
||||||
|
const set = subscriptions.get(id) ?? new Set<string>();
|
||||||
|
set.add(channel);
|
||||||
|
if (!subscriptions.has(id)) {
|
||||||
|
sender.once('destroyed', () => {
|
||||||
|
subscriptions.delete(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
subscriptions.set(id, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSubscription(sender: Electron.WebContents, channel: string) {
|
||||||
|
const id = sender.id;
|
||||||
|
const set = subscriptions.get(id);
|
||||||
|
if (!set) return;
|
||||||
|
set.delete(channel);
|
||||||
|
if (set.size === 0) {
|
||||||
|
subscriptions.delete(id);
|
||||||
|
} else {
|
||||||
|
subscriptions.set(id, set);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerEvents() {
|
export function registerEvents() {
|
||||||
const unsubs: (() => void)[] = [];
|
const unsubs: (() => void)[] = [];
|
||||||
|
|
||||||
|
const onSubscribe = (
|
||||||
|
event: Electron.IpcMainEvent,
|
||||||
|
action: 'subscribe' | 'unsubscribe',
|
||||||
|
channel: string
|
||||||
|
) => {
|
||||||
|
if (typeof channel !== 'string') return;
|
||||||
|
if (action === 'subscribe') {
|
||||||
|
addSubscription(event.sender, channel);
|
||||||
|
} else {
|
||||||
|
removeSubscription(event.sender, channel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.on(AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, onSubscribe);
|
||||||
|
unsubs.push(() =>
|
||||||
|
ipcMain.removeListener(AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME, onSubscribe)
|
||||||
|
);
|
||||||
// register events
|
// register events
|
||||||
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
|
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
|
||||||
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
|
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
|
||||||
@@ -40,22 +95,10 @@ export function registerEvents() {
|
|||||||
typeof a !== 'object'
|
typeof a !== 'object'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// is this efficient?
|
getTargetContents(chan).forEach(wc => {
|
||||||
getActiveWindows().forEach(win => {
|
if (!wc.isDestroyed()) {
|
||||||
if (win.isDestroyed()) {
|
wc.send(AFFINE_EVENT_CHANNEL_NAME, chan, ...args);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// .webContents could be undefined if the window is destroyed
|
|
||||||
win.webContents?.send(AFFINE_EVENT_CHANNEL_NAME, chan, ...args);
|
|
||||||
win.contentView.children.forEach(child => {
|
|
||||||
if (
|
|
||||||
child instanceof WebContentsView &&
|
|
||||||
child.webContents &&
|
|
||||||
!child.webContents.isDestroyed()
|
|
||||||
) {
|
|
||||||
child.webContents?.send(AFFINE_EVENT_CHANNEL_NAME, chan, ...args);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
unsubs.push(unsubscribe);
|
unsubs.push(unsubscribe);
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ class HelperProcessManager {
|
|||||||
beforeAppQuit(() => {
|
beforeAppQuit(() => {
|
||||||
this.#process.kill();
|
this.#process.kill();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.#process.on('exit', code => {
|
||||||
|
logger.error('[helper] process exited', { code });
|
||||||
|
HelperProcessManager._instance = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#process.on('error', err => {
|
||||||
|
logger.error('[helper] process error', err);
|
||||||
|
HelperProcessManager._instance = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// bridge renderer <-> helper process
|
// bridge renderer <-> helper process
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { app, net, protocol, session } from 'electron';
|
|||||||
import cookieParser from 'set-cookie-parser';
|
import cookieParser from 'set-cookie-parser';
|
||||||
|
|
||||||
import { isWindows, resourcesPath } from '../shared/utils';
|
import { isWindows, resourcesPath } from '../shared/utils';
|
||||||
|
import { isDev } from './config';
|
||||||
import { anotherHost, mainHost } from './constants';
|
import { anotherHost, mainHost } from './constants';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
@@ -13,11 +14,9 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
scheme: 'assets',
|
scheme: 'assets',
|
||||||
privileges: {
|
privileges: {
|
||||||
secure: true,
|
secure: true,
|
||||||
allowServiceWorkers: true,
|
|
||||||
corsEnabled: true,
|
corsEnabled: true,
|
||||||
supportFetchAPI: true,
|
supportFetchAPI: true,
|
||||||
standard: true,
|
standard: true,
|
||||||
bypassCSP: true,
|
|
||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -28,7 +27,6 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
corsEnabled: true,
|
corsEnabled: true,
|
||||||
supportFetchAPI: true,
|
supportFetchAPI: true,
|
||||||
standard: true,
|
standard: true,
|
||||||
bypassCSP: true,
|
|
||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -59,8 +57,13 @@ async function handleFileRequest(request: Request) {
|
|||||||
urlObject.pathname &&
|
urlObject.pathname &&
|
||||||
/\.(woff2?|ttf|otf)$/i.test(urlObject.pathname.split('?')[0] ?? '');
|
/\.(woff2?|ttf|otf)$/i.test(urlObject.pathname.split('?')[0] ?? '');
|
||||||
|
|
||||||
// Redirect to webpack dev server if defined
|
// Redirect to webpack dev server if available
|
||||||
if (process.env.DEV_SERVER_URL && !isAbsolutePath && !isFontRequest) {
|
if (
|
||||||
|
isDev &&
|
||||||
|
process.env.DEV_SERVER_URL &&
|
||||||
|
!isAbsolutePath &&
|
||||||
|
!isFontRequest
|
||||||
|
) {
|
||||||
const devServerUrl = new URL(
|
const devServerUrl = new URL(
|
||||||
`${urlObject.pathname}${urlObject.search}`,
|
`${urlObject.pathname}${urlObject.search}`,
|
||||||
process.env.DEV_SERVER_URL
|
process.env.DEV_SERVER_URL
|
||||||
@@ -103,19 +106,6 @@ async function handleFileRequest(request: Request) {
|
|||||||
return net.fetch(pathToFileURL(filepath).toString(), clonedRequest);
|
return net.fetch(pathToFileURL(filepath).toString(), clonedRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// whitelist for cors
|
|
||||||
// url patterns that are allowed to have cors headers
|
|
||||||
const corsWhitelist = [
|
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*googlevideo\.com$/,
|
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/,
|
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/,
|
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*gstatic\.com$/,
|
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*googleapis\.com$/,
|
|
||||||
/^localhost(?::\d+)?$/,
|
|
||||||
/^127\.0\.0\.1(?::\d+)?$/,
|
|
||||||
/^insider\.affine\.pro$/,
|
|
||||||
/^app\.affine\.pro$/,
|
|
||||||
];
|
|
||||||
const needRefererDomains = [
|
const needRefererDomains = [
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/,
|
/^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/,
|
||||||
/^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/,
|
/^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/,
|
||||||
@@ -123,6 +113,44 @@ const needRefererDomains = [
|
|||||||
];
|
];
|
||||||
const defaultReferer = 'https://client.affine.local/';
|
const defaultReferer = 'https://client.affine.local/';
|
||||||
|
|
||||||
|
function setHeader(
|
||||||
|
headers: Record<string, string[]>,
|
||||||
|
name: string,
|
||||||
|
value: string
|
||||||
|
) {
|
||||||
|
Object.keys(headers).forEach(key => {
|
||||||
|
if (key.toLowerCase() === name.toLowerCase()) {
|
||||||
|
delete headers[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
headers[name] = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureFrameAncestors(
|
||||||
|
headers: Record<string, string[]>,
|
||||||
|
directive: string
|
||||||
|
) {
|
||||||
|
const cspHeaderKey = Object.keys(headers).find(
|
||||||
|
key => key.toLowerCase() === 'content-security-policy'
|
||||||
|
);
|
||||||
|
if (!cspHeaderKey) {
|
||||||
|
headers['Content-Security-Policy'] = [`frame-ancestors ${directive}`];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = headers[cspHeaderKey];
|
||||||
|
headers[cspHeaderKey] = values.map(val => {
|
||||||
|
if (typeof val !== 'string') return val as any;
|
||||||
|
const directives = val
|
||||||
|
.split(';')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(d => !d.toLowerCase().startsWith('frame-ancestors'));
|
||||||
|
directives.push(`frame-ancestors ${directive}`);
|
||||||
|
return directives.join('; ');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function registerProtocol() {
|
export function registerProtocol() {
|
||||||
protocol.handle('file', request => {
|
protocol.handle('file', request => {
|
||||||
return handleFileRequest(request);
|
return handleFileRequest(request);
|
||||||
@@ -172,79 +200,19 @@ export function registerProtocol() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostname = new URL(url).hostname;
|
const { protocol } = new URL(url);
|
||||||
if (!corsWhitelist.some(domainRegex => domainRegex.test(hostname))) {
|
|
||||||
|
// Only adjust CORS for assets/file responses; leave remote http(s) headers intact
|
||||||
|
if (protocol === 'assets:' || protocol === 'file:') {
|
||||||
delete responseHeaders['access-control-allow-origin'];
|
delete responseHeaders['access-control-allow-origin'];
|
||||||
delete responseHeaders['access-control-allow-headers'];
|
delete responseHeaders['access-control-allow-headers'];
|
||||||
delete responseHeaders['Access-Control-Allow-Origin'];
|
delete responseHeaders['Access-Control-Allow-Origin'];
|
||||||
delete responseHeaders['Access-Control-Allow-Headers'];
|
delete responseHeaders['Access-Control-Allow-Headers'];
|
||||||
} else if (
|
|
||||||
!needRefererDomains.some(domainRegex => domainRegex.test(hostname))
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!responseHeaders['access-control-allow-origin'] &&
|
|
||||||
!responseHeaders['Access-Control-Allow-Origin']
|
|
||||||
) {
|
|
||||||
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!responseHeaders['access-control-allow-headers'] &&
|
|
||||||
!responseHeaders['Access-Control-Allow-Headers']
|
|
||||||
) {
|
|
||||||
responseHeaders['Access-Control-Allow-Headers'] = [
|
|
||||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!responseHeaders['access-control-allow-methods'] &&
|
|
||||||
!responseHeaders['Access-Control-Allow-Methods']
|
|
||||||
) {
|
|
||||||
responseHeaders['Access-Control-Allow-Methods'] = [
|
|
||||||
'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// to allow url embedding, remove "x-frame-options",
|
if (protocol === 'assets:' || protocol === 'file:') {
|
||||||
// if response header contains "content-security-policy", remove "frame-ancestors/frame-src"
|
setHeader(responseHeaders, 'X-Frame-Options', 'SAMEORIGIN');
|
||||||
delete responseHeaders['x-frame-options'];
|
ensureFrameAncestors(responseHeaders, "'self'");
|
||||||
delete responseHeaders['X-Frame-Options'];
|
|
||||||
|
|
||||||
// Handle Content Security Policy headers
|
|
||||||
const cspHeaders = [
|
|
||||||
'content-security-policy',
|
|
||||||
'Content-Security-Policy',
|
|
||||||
];
|
|
||||||
for (const cspHeader of cspHeaders) {
|
|
||||||
const cspValues = responseHeaders[cspHeader];
|
|
||||||
if (cspValues) {
|
|
||||||
// Remove frame-ancestors and frame-src directives from CSP
|
|
||||||
const modifiedCspValues = cspValues
|
|
||||||
.map(cspValue => {
|
|
||||||
if (typeof cspValue === 'string') {
|
|
||||||
return cspValue
|
|
||||||
.split(';')
|
|
||||||
.filter(directive => {
|
|
||||||
const trimmed = directive.trim().toLowerCase();
|
|
||||||
return (
|
|
||||||
!trimmed.startsWith('frame-ancestors') &&
|
|
||||||
!trimmed.startsWith('frame-src')
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.join(';');
|
|
||||||
}
|
|
||||||
return cspValue;
|
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
value => value && typeof value === 'string' && value.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (modifiedCspValues.length > 0) {
|
|
||||||
responseHeaders[cspHeader] = modifiedCspValues;
|
|
||||||
} else {
|
|
||||||
delete responseHeaders[cspHeader];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { WebPreferences } from 'electron';
|
||||||
|
|
||||||
|
const DEFAULT_WEB_PREFERENCES: Pick<
|
||||||
|
WebPreferences,
|
||||||
|
'contextIsolation' | 'nodeIntegration' | 'sandbox'
|
||||||
|
> = {
|
||||||
|
sandbox: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildWebPreferences(
|
||||||
|
overrides: Partial<WebPreferences> = {}
|
||||||
|
): WebPreferences {
|
||||||
|
return {
|
||||||
|
...DEFAULT_WEB_PREFERENCES,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { BrowserWindow, type Display, screen } from 'electron';
|
|||||||
import { isMacOS } from '../../shared/utils';
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { customThemeViewUrl } from '../constants';
|
import { customThemeViewUrl } from '../constants';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|
||||||
let customThemeWindow: Promise<BrowserWindow> | undefined;
|
let customThemeWindow: Promise<BrowserWindow> | undefined;
|
||||||
|
|
||||||
@@ -26,11 +27,11 @@ async function createCustomThemeWindow(additionalArguments: string[]) {
|
|||||||
resizable: true,
|
resizable: true,
|
||||||
maximizable: false,
|
maximizable: false,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
webgl: true,
|
webgl: true,
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
additionalArguments: additionalArguments,
|
additionalArguments: additionalArguments,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
await browserWindow.loadURL(customThemeViewUrl);
|
await browserWindow.loadURL(customThemeViewUrl);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { logger } from '../logger';
|
|||||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||||
import { globalStateStorage } from '../shared-storage/storage';
|
import { globalStateStorage } from '../shared-storage/storage';
|
||||||
import { uiSubjects } from '../ui/subject';
|
import { uiSubjects } from '../ui/subject';
|
||||||
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|
||||||
const IS_DEV: boolean =
|
const IS_DEV: boolean =
|
||||||
process.env.NODE_ENV === 'development' && !process.env.CI;
|
process.env.NODE_ENV === 'development' && !process.env.CI;
|
||||||
@@ -56,6 +57,7 @@ export class MainWindowManager {
|
|||||||
show: false,
|
show: false,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
webPreferences: buildWebPreferences(),
|
||||||
});
|
});
|
||||||
this.hiddenMacWindow.on('close', () => {
|
this.hiddenMacWindow.on('close', () => {
|
||||||
this.cleanupWindows();
|
this.cleanupWindows();
|
||||||
@@ -95,11 +97,9 @@ export class MainWindowManager {
|
|||||||
// backgroundMaterial: 'mica',
|
// backgroundMaterial: 'mica',
|
||||||
height: mainWindowState.height,
|
height: mainWindowState.height,
|
||||||
show: false, // Use 'ready-to-show' event to show window
|
show: false, // Use 'ready-to-show' event to show window
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
webgl: true,
|
webgl: true,
|
||||||
contextIsolation: true,
|
}),
|
||||||
sandbox: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const helper = await ensureHelperProcess();
|
const helper = await ensureHelperProcess();
|
||||||
helper.connectMain(browserWindow);
|
helper.connectMain(browserWindow);
|
||||||
@@ -283,10 +283,10 @@ export async function openUrlInHiddenWindow(urlObj: URL) {
|
|||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 600,
|
height: 600,
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
additionalArguments: await getWindowAdditionalArguments(),
|
additionalArguments: await getWindowAdditionalArguments(),
|
||||||
},
|
}),
|
||||||
show: BUILD_CONFIG.debug,
|
show: BUILD_CONFIG.debug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { isDev } from '../config';
|
|||||||
import { onboardingViewUrl } from '../constants';
|
import { onboardingViewUrl } from '../constants';
|
||||||
// import { getExposedMeta } from './exposed';
|
// import { getExposedMeta } from './exposed';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
import { fullscreenAndCenter, getScreenSize } from './utils';
|
import { fullscreenAndCenter, getScreenSize } from './utils';
|
||||||
|
|
||||||
// todo: not all window need all of the exposed meta
|
// todo: not all window need all of the exposed meta
|
||||||
@@ -40,11 +41,11 @@ async function createOnboardingWindow(additionalArguments: string[]) {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
hasShadow: false,
|
hasShadow: false,
|
||||||
roundedCorners: false,
|
roundedCorners: false,
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
webgl: true,
|
webgl: true,
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
additionalArguments: additionalArguments,
|
additionalArguments: additionalArguments,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// workaround for the phantom title bar on windows when losing focus
|
// workaround for the phantom title bar on windows when losing focus
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
import { popupViewUrl } from '../constants';
|
import { popupViewUrl } from '../constants';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import type { MainEventRegister, NamespaceHandlers } from '../type';
|
import type { MainEventRegister, NamespaceHandlers } from '../type';
|
||||||
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
import { getCurrentDisplay } from './utils';
|
import { getCurrentDisplay } from './utils';
|
||||||
|
|
||||||
type PopupWindowType = 'notification' | 'recording';
|
type PopupWindowType = 'notification' | 'recording';
|
||||||
@@ -85,17 +86,15 @@ abstract class PopupWindow {
|
|||||||
visualEffectState: 'active',
|
visualEffectState: 'active',
|
||||||
vibrancy: 'under-window',
|
vibrancy: 'under-window',
|
||||||
...this.windowOptions,
|
...this.windowOptions,
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
...this.windowOptions.webPreferences,
|
|
||||||
webgl: true,
|
webgl: true,
|
||||||
contextIsolation: true,
|
|
||||||
sandbox: false,
|
|
||||||
transparent: true,
|
transparent: true,
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
preload: join(__dirname, './preload.js'), // this points to the bundled preload module
|
preload: join(__dirname, './preload.js'), // this points to the bundled preload module
|
||||||
|
...this.windowOptions.webPreferences,
|
||||||
// serialize exposed meta that to be used in preload
|
// serialize exposed meta that to be used in preload
|
||||||
additionalArguments: await getAdditionalArguments(this.name),
|
additionalArguments: await getAdditionalArguments(this.name),
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// it seems that the dock will disappear when popup windows are shown
|
// it seems that the dock will disappear when popup windows are shown
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
type WorkbenchViewMeta,
|
type WorkbenchViewMeta,
|
||||||
} from '../shared-state-schema';
|
} from '../shared-state-schema';
|
||||||
import { globalStateStorage } from '../shared-storage/storage';
|
import { globalStateStorage } from '../shared-storage/storage';
|
||||||
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
import { getMainWindow, MainWindowManager } from './main-window';
|
import { getMainWindow, MainWindowManager } from './main-window';
|
||||||
|
|
||||||
async function getAdditionalArguments() {
|
async function getAdditionalArguments() {
|
||||||
@@ -511,6 +512,7 @@ export class WebContentViewsManager {
|
|||||||
if (view) {
|
if (view) {
|
||||||
this.resizeView(view);
|
this.resizeView(view);
|
||||||
}
|
}
|
||||||
|
this.updateBackgroundThrottling();
|
||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -759,6 +761,10 @@ export class WebContentViewsManager {
|
|||||||
|
|
||||||
this.mainWindow?.on('focus', () => {
|
this.mainWindow?.on('focus', () => {
|
||||||
focusActiveView();
|
focusActiveView();
|
||||||
|
this.updateBackgroundThrottling();
|
||||||
|
});
|
||||||
|
this.mainWindow?.on('blur', () => {
|
||||||
|
this.updateBackgroundThrottling();
|
||||||
});
|
});
|
||||||
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
@@ -768,6 +774,7 @@ export class WebContentViewsManager {
|
|||||||
// makes sure the active view is always focused
|
// makes sure the active view is always focused
|
||||||
if (window?.isFocused()) {
|
if (window?.isFocused()) {
|
||||||
focusActiveView();
|
focusActiveView();
|
||||||
|
this.updateBackgroundThrottling();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -810,17 +817,15 @@ export class WebContentViewsManager {
|
|||||||
additionalArguments.push(`--view-id=${viewId}`);
|
additionalArguments.push(`--view-id=${viewId}`);
|
||||||
|
|
||||||
const view = new WebContentsView({
|
const view = new WebContentsView({
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
webgl: true,
|
webgl: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
contextIsolation: true,
|
|
||||||
sandbox: false,
|
|
||||||
spellcheck: spellCheckSettings.enabled,
|
spellcheck: spellCheckSettings.enabled,
|
||||||
preload: join(__dirname, './preload.js'), // this points to the bundled preload module
|
preload: join(__dirname, './preload.js'), // this points to the bundled preload module
|
||||||
// serialize exposed meta that to be used in preload
|
// serialize exposed meta that to be used in preload
|
||||||
additionalArguments: additionalArguments,
|
additionalArguments: additionalArguments,
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: true,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
view.webContents.on('context-menu', (_event, params) => {
|
view.webContents.on('context-menu', (_event, params) => {
|
||||||
@@ -872,13 +877,16 @@ export class WebContentViewsManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.webViewsMap$.next(this.tabViewsMap.set(viewId, view));
|
this.webViewsMap$.next(this.tabViewsMap.set(viewId, view));
|
||||||
let unsub = () => {};
|
let disconnectHelperProcess: (() => void) | null = null;
|
||||||
|
|
||||||
// shell process do not need to connect to helper process
|
// shell process do not need to connect to helper process
|
||||||
if (type !== 'shell') {
|
if (type !== 'shell') {
|
||||||
view.webContents.on('did-finish-load', () => {
|
view.webContents.on('did-finish-load', () => {
|
||||||
unsub();
|
disconnectHelperProcess?.();
|
||||||
unsub = helperProcessManager.connectRenderer(view.webContents);
|
disconnectHelperProcess = helperProcessManager.connectRenderer(
|
||||||
|
view.webContents
|
||||||
|
);
|
||||||
|
this.updateBackgroundThrottling();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
view.webContents.on('focus', () => {
|
view.webContents.on('focus', () => {
|
||||||
@@ -892,6 +900,8 @@ export class WebContentViewsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.webContents.on('destroyed', () => {
|
view.webContents.on('destroyed', () => {
|
||||||
|
disconnectHelperProcess?.();
|
||||||
|
disconnectHelperProcess = null;
|
||||||
this.webViewsMap$.next(
|
this.webViewsMap$.next(
|
||||||
new Map(
|
new Map(
|
||||||
[...this.tabViewsMap.entries()].filter(([key]) => key !== viewId)
|
[...this.tabViewsMap.entries()].filter(([key]) => key !== viewId)
|
||||||
@@ -902,6 +912,7 @@ export class WebContentViewsManager {
|
|||||||
if (this.tabViewsMap.size === 0) {
|
if (this.tabViewsMap.size === 0) {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
this.updateBackgroundThrottling();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resizeView(view);
|
this.resizeView(view);
|
||||||
@@ -920,6 +931,30 @@ export class WebContentViewsManager {
|
|||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly updateBackgroundThrottling = () => {
|
||||||
|
const mainFocused = this.mainWindow?.isFocused() ?? false;
|
||||||
|
const activeId = this.activeWorkbenchId;
|
||||||
|
this.webViewsMap$.value.forEach((view, id) => {
|
||||||
|
if (id === 'shell') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shouldThrottle = !mainFocused || id !== activeId;
|
||||||
|
try {
|
||||||
|
view.webContents.setBackgroundThrottling(shouldThrottle);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('failed to set backgroundThrottling', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (this.shellView) {
|
||||||
|
const shellThrottle = !mainFocused;
|
||||||
|
try {
|
||||||
|
this.shellView.webContents.setBackgroundThrottling(shellThrottle);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('failed to set shell backgroundThrottling', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private async skipOnboarding(view: WebContentsView) {
|
private async skipOnboarding(view: WebContentsView) {
|
||||||
await view.webContents.executeJavaScript(`
|
await view.webContents.executeJavaScript(`
|
||||||
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { BrowserWindow, MessageChannelMain, type WebContents } from 'electron';
|
|||||||
import { backgroundWorkerViewUrl } from '../constants';
|
import { backgroundWorkerViewUrl } from '../constants';
|
||||||
import { ensureHelperProcess } from '../helper-process';
|
import { ensureHelperProcess } from '../helper-process';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|
||||||
async function getAdditionalArguments() {
|
async function getAdditionalArguments() {
|
||||||
const { getExposedMeta } = await import('../exposed');
|
const { getExposedMeta } = await import('../exposed');
|
||||||
@@ -41,10 +42,10 @@ export class WorkerManager {
|
|||||||
const worker = new BrowserWindow({
|
const worker = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 600,
|
height: 600,
|
||||||
webPreferences: {
|
webPreferences: buildWebPreferences({
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
additionalArguments: additionalArguments,
|
additionalArguments: additionalArguments,
|
||||||
},
|
}),
|
||||||
show: false,
|
show: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,9 +57,32 @@ export class WorkerManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let disconnectHelperProcess: (() => void) | null = null;
|
let disconnectHelperProcess: (() => void) | null = null;
|
||||||
worker.on('closed', () => {
|
let cleanedUp = false;
|
||||||
|
const cleanup = () => {
|
||||||
|
if (cleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanedUp = true;
|
||||||
this.workers.delete(key);
|
this.workers.delete(key);
|
||||||
disconnectHelperProcess?.();
|
disconnectHelperProcess?.();
|
||||||
|
disconnectHelperProcess = null;
|
||||||
|
};
|
||||||
|
const handleWorkerFailure = (reason: string) => {
|
||||||
|
logger.error('[worker] renderer process gone', { key, reason });
|
||||||
|
record.loaded.reject(new Error(`worker ${key} failed: ${reason}`));
|
||||||
|
cleanup();
|
||||||
|
try {
|
||||||
|
worker.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('failed to destroy worker window', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
worker.on('closed', cleanup);
|
||||||
|
worker.webContents.on('render-process-gone', (_event, details) => {
|
||||||
|
handleWorkerFailure(details.reason ?? 'unknown');
|
||||||
|
});
|
||||||
|
worker.webContents.on('unresponsive', () => {
|
||||||
|
handleWorkerFailure('unresponsive');
|
||||||
});
|
});
|
||||||
worker.loadURL(backgroundWorkerViewUrl).catch(e => {
|
worker.loadURL(backgroundWorkerViewUrl).catch(e => {
|
||||||
logger.error('failed to load url', e);
|
logger.error('failed to load url', e);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { z } from 'zod';
|
|||||||
import {
|
import {
|
||||||
AFFINE_API_CHANNEL_NAME,
|
AFFINE_API_CHANNEL_NAME,
|
||||||
AFFINE_EVENT_CHANNEL_NAME,
|
AFFINE_EVENT_CHANNEL_NAME,
|
||||||
|
AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME,
|
||||||
type ExposedMeta,
|
type ExposedMeta,
|
||||||
type HelperToRenderer,
|
type HelperToRenderer,
|
||||||
type RendererToHelper,
|
type RendererToHelper,
|
||||||
@@ -83,6 +84,33 @@ function getMainAPIs() {
|
|||||||
|
|
||||||
// channel -> callback[]
|
// channel -> callback[]
|
||||||
const listenersMap = new Map<string, ((...args: any[]) => void)[]>();
|
const listenersMap = new Map<string, ((...args: any[]) => void)[]>();
|
||||||
|
const subscribeCounts = new Map<string, number>();
|
||||||
|
|
||||||
|
const subscribe = (channel: string) => {
|
||||||
|
const count = (subscribeCounts.get(channel) ?? 0) + 1;
|
||||||
|
subscribeCounts.set(channel, count);
|
||||||
|
if (count === 1) {
|
||||||
|
ipcRenderer.send(
|
||||||
|
AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME,
|
||||||
|
'subscribe',
|
||||||
|
channel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = (channel: string) => {
|
||||||
|
const count = (subscribeCounts.get(channel) ?? 0) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
subscribeCounts.delete(channel);
|
||||||
|
ipcRenderer.send(
|
||||||
|
AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME,
|
||||||
|
'unsubscribe',
|
||||||
|
channel
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
subscribeCounts.set(channel, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, ...args) => {
|
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, ...args) => {
|
||||||
if (typeof channel !== 'string') {
|
if (typeof channel !== 'string') {
|
||||||
@@ -108,12 +136,20 @@ function getMainAPIs() {
|
|||||||
...(listenersMap.get(channel) ?? []),
|
...(listenersMap.get(channel) ?? []),
|
||||||
callback,
|
callback,
|
||||||
]);
|
]);
|
||||||
|
subscribe(channel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const listeners = listenersMap.get(channel) ?? [];
|
const listeners = listenersMap.get(channel) ?? [];
|
||||||
const index = listeners.indexOf(callback);
|
const index = listeners.indexOf(callback);
|
||||||
if (index !== -1) {
|
if (index === -1) {
|
||||||
listeners.splice(index, 1);
|
return;
|
||||||
|
}
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
unsubscribe(channel);
|
||||||
|
if (listeners.length === 0) {
|
||||||
|
listenersMap.delete(channel);
|
||||||
|
} else {
|
||||||
|
listenersMap.set(channel, listeners);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,16 +6,6 @@ import {
|
|||||||
AFFINE_EVENT_CHANNEL_NAME,
|
AFFINE_EVENT_CHANNEL_NAME,
|
||||||
} from '../shared/type';
|
} from '../shared/type';
|
||||||
|
|
||||||
// Load persisted data from main process synchronously at preload time
|
|
||||||
const initialGlobalState = ipcRenderer.sendSync(
|
|
||||||
AFFINE_API_CHANNEL_NAME,
|
|
||||||
'sharedStorage:getAllGlobalState'
|
|
||||||
);
|
|
||||||
const initialGlobalCache = ipcRenderer.sendSync(
|
|
||||||
AFFINE_API_CHANNEL_NAME,
|
|
||||||
'sharedStorage:getAllGlobalCache'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Unique id for this renderer instance, used to ignore self-originated broadcasts
|
// Unique id for this renderer instance, used to ignore self-originated broadcasts
|
||||||
const CLIENT_ID: string = Math.random().toString(36).slice(2);
|
const CLIENT_ID: string = Math.random().toString(36).slice(2);
|
||||||
|
|
||||||
@@ -35,42 +25,97 @@ function createSharedStorageApi(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const memory = new MemoryMemento();
|
const memory = new MemoryMemento();
|
||||||
memory.setAll(init);
|
const revisions = new Map<string, number>();
|
||||||
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => {
|
const updateQueue: Record<string, any>[] = [];
|
||||||
if (channel === `sharedStorage:${event}`) {
|
let loaded = false;
|
||||||
for (const [key, raw] of Object.entries(updates)) {
|
|
||||||
// support both legacy plain value and new { v, r, s } structure
|
|
||||||
let value: any;
|
|
||||||
let source: string | undefined;
|
|
||||||
|
|
||||||
if (raw && typeof raw === 'object' && 'v' in raw) {
|
const applyUpdates = (updates: Record<string, any>) => {
|
||||||
value = (raw as any).v;
|
for (const [key, raw] of Object.entries(updates)) {
|
||||||
source = (raw as any).s;
|
// '*' means "reset everything" coming from a clear operation
|
||||||
} else {
|
if (key === '*') {
|
||||||
value = raw;
|
memory.clear();
|
||||||
}
|
revisions.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore our own broadcasts
|
// support both legacy plain value and new { v, r, s } structure
|
||||||
if (source && source === CLIENT_ID) {
|
let value: any;
|
||||||
|
let source: string | undefined;
|
||||||
|
let rev: number | undefined;
|
||||||
|
|
||||||
|
if (raw && typeof raw === 'object' && 'v' in raw) {
|
||||||
|
value = raw.v;
|
||||||
|
source = raw.s;
|
||||||
|
rev = typeof raw.r === 'number' ? raw.r : undefined;
|
||||||
|
} else {
|
||||||
|
value = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore our own broadcasts
|
||||||
|
if (source && source === CLIENT_ID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rev !== undefined) {
|
||||||
|
const current = revisions.get(key) ?? -1;
|
||||||
|
if (rev <= current) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
revisions.set(key, rev);
|
||||||
|
}
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
memory.del(key);
|
memory.del(key);
|
||||||
} else {
|
} else {
|
||||||
memory.set(key, value);
|
memory.set(key, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => {
|
||||||
|
if (channel === `sharedStorage:${event}`) {
|
||||||
|
if (loaded) {
|
||||||
|
applyUpdates(updates);
|
||||||
|
} else {
|
||||||
|
updateQueue.push(updates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initPromise = (async () => {
|
||||||
|
try {
|
||||||
|
memory.setAll(init);
|
||||||
|
const latest = await ipcRenderer.invoke(
|
||||||
|
AFFINE_API_CHANNEL_NAME,
|
||||||
|
event === 'onGlobalStateChanged'
|
||||||
|
? 'sharedStorage:getAllGlobalState'
|
||||||
|
: 'sharedStorage:getAllGlobalCache'
|
||||||
|
);
|
||||||
|
if (latest && typeof latest === 'object') {
|
||||||
|
memory.setAll(latest);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load initial shared storage', err);
|
||||||
|
} finally {
|
||||||
|
loaded = true;
|
||||||
|
while (updateQueue.length) {
|
||||||
|
const updates = updateQueue.shift();
|
||||||
|
if (updates) {
|
||||||
|
applyUpdates(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
ready: initPromise,
|
||||||
del(key: string) {
|
del(key: string) {
|
||||||
memory.del(key);
|
memory.del(key);
|
||||||
invokeWithCatch(`sharedStorage:${api.del}`, key, CLIENT_ID);
|
invokeWithCatch(`sharedStorage:${api.del}`, key, CLIENT_ID);
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
memory.clear();
|
memory.clear();
|
||||||
|
revisions.clear();
|
||||||
invokeWithCatch(`sharedStorage:${api.clear}`, CLIENT_ID);
|
invokeWithCatch(`sharedStorage:${api.clear}`, CLIENT_ID);
|
||||||
},
|
},
|
||||||
get<T>(key: string): T | undefined {
|
get<T>(key: string): T | undefined {
|
||||||
@@ -90,25 +135,17 @@ function createSharedStorageApi(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const globalState = createSharedStorageApi(
|
export const globalState = createSharedStorageApi({}, 'onGlobalStateChanged', {
|
||||||
initialGlobalState,
|
clear: 'clearGlobalState',
|
||||||
'onGlobalStateChanged',
|
del: 'delGlobalState',
|
||||||
{
|
set: 'setGlobalState',
|
||||||
clear: 'clearGlobalState',
|
});
|
||||||
del: 'delGlobalState',
|
|
||||||
set: 'setGlobalState',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const globalCache = createSharedStorageApi(
|
export const globalCache = createSharedStorageApi({}, 'onGlobalCacheChanged', {
|
||||||
initialGlobalCache,
|
clear: 'clearGlobalCache',
|
||||||
'onGlobalCacheChanged',
|
del: 'delGlobalCache',
|
||||||
{
|
set: 'setGlobalCache',
|
||||||
clear: 'clearGlobalCache',
|
});
|
||||||
del: 'delGlobalCache',
|
|
||||||
set: 'setGlobalCache',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sharedStorage = {
|
export const sharedStorage = {
|
||||||
globalState,
|
globalState,
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ export type MainToHelper = Pick<
|
|||||||
|
|
||||||
export const AFFINE_API_CHANNEL_NAME = 'affine-ipc-api';
|
export const AFFINE_API_CHANNEL_NAME = 'affine-ipc-api';
|
||||||
export const AFFINE_EVENT_CHANNEL_NAME = 'affine-ipc-event';
|
export const AFFINE_EVENT_CHANNEL_NAME = 'affine-ipc-event';
|
||||||
|
export const AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME = 'affine-ipc-event-subscribe';
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export const createIframeRenderer: (
|
|||||||
class="ai-answer-iframe"
|
class="ai-answer-iframe"
|
||||||
sandbox="allow-scripts"
|
sandbox="allow-scripts"
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
allowfullscreen
|
|
||||||
.srcdoc=${preprocessHtml(answer)}
|
.srcdoc=${preprocessHtml(answer)}
|
||||||
>
|
>
|
||||||
</iframe>`;
|
</iframe>`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||||
import type { UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import {
|
import {
|
||||||
addContextBlobMutation,
|
addContextBlobMutation,
|
||||||
addContextCategoryMutation,
|
addContextCategoryMutation,
|
||||||
@@ -50,6 +50,20 @@ export enum Endpoint {
|
|||||||
type OptionsField<T extends GraphQLQuery> =
|
type OptionsField<T extends GraphQLQuery> =
|
||||||
RequestOptions<T>['variables'] extends { options: infer U } ? U : never;
|
RequestOptions<T>['variables'] extends { options: infer U } ? U : never;
|
||||||
|
|
||||||
|
function toUserFriendlyError(err: any): UserFriendlyError {
|
||||||
|
return err instanceof UserFriendlyError
|
||||||
|
? err
|
||||||
|
: UserFriendlyError.fromAny(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbortError(error: UserFriendlyError) {
|
||||||
|
return (
|
||||||
|
error.name === 'REQUEST_ABORTED' ||
|
||||||
|
error.code === 'REQUEST_ABORTED' ||
|
||||||
|
error.message?.toLowerCase().includes('aborted') === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function codeToError(error: UserFriendlyError) {
|
function codeToError(error: UserFriendlyError) {
|
||||||
switch (error.status) {
|
switch (error.status) {
|
||||||
case 401:
|
case 401:
|
||||||
@@ -66,7 +80,7 @@ function codeToError(error: UserFriendlyError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveError(err: any) {
|
export function resolveError(err: any) {
|
||||||
return codeToError(err);
|
return codeToError(toUserFriendlyError(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleError(src: any) {
|
export function handleError(src: any) {
|
||||||
@@ -185,7 +199,11 @@ export class CopilotClient {
|
|||||||
});
|
});
|
||||||
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw resolveError(err);
|
const parsed = toUserFriendlyError(err);
|
||||||
|
if (isAbortError(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw resolveError(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +223,11 @@ export class CopilotClient {
|
|||||||
});
|
});
|
||||||
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw resolveError(err);
|
const parsed = toUserFriendlyError(err);
|
||||||
|
if (isAbortError(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw resolveError(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +252,11 @@ export class CopilotClient {
|
|||||||
|
|
||||||
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw resolveError(err);
|
const parsed = toUserFriendlyError(err);
|
||||||
|
if (isAbortError(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw resolveError(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +281,11 @@ export class CopilotClient {
|
|||||||
|
|
||||||
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
return res.currentUser?.copilot?.chats.edges.map(e => e.node);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw resolveError(err);
|
const parsed = toUserFriendlyError(err);
|
||||||
|
if (isAbortError(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw resolveError(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,13 +63,22 @@ export class FetchService extends Service {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
const isAbort =
|
||||||
|
err?.name === 'AbortError' ||
|
||||||
|
err?.code === 'ABORT_ERR' ||
|
||||||
|
err?.type === 'aborted' ||
|
||||||
|
abortController.signal.aborted;
|
||||||
|
|
||||||
|
const message =
|
||||||
|
err?.message || (isAbort ? 'Request aborted' : 'Unknown network error');
|
||||||
|
|
||||||
throw new UserFriendlyError({
|
throw new UserFriendlyError({
|
||||||
status: 504,
|
status: isAbort ? 499 : 504,
|
||||||
code: 'NETWORK_ERROR',
|
code: isAbort ? 'REQUEST_ABORTED' : 'NETWORK_ERROR',
|
||||||
type: 'NETWORK_ERROR',
|
type: isAbort ? 'REQUEST_ABORTED' : 'NETWORK_ERROR',
|
||||||
name: 'NETWORK_ERROR',
|
name: isAbort ? 'REQUEST_ABORTED' : 'NETWORK_ERROR',
|
||||||
message: `Network error: ${err.message}`,
|
message: `Network error: ${message}`,
|
||||||
stacktrace: err.stack,
|
stacktrace: err?.stack,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -948,7 +948,7 @@ __metadata:
|
|||||||
"@opentelemetry/core": "npm:^2.2.0"
|
"@opentelemetry/core": "npm:^2.2.0"
|
||||||
"@opentelemetry/exporter-prometheus": "npm:^0.208.0"
|
"@opentelemetry/exporter-prometheus": "npm:^0.208.0"
|
||||||
"@opentelemetry/exporter-zipkin": "npm:^2.2.0"
|
"@opentelemetry/exporter-zipkin": "npm:^2.2.0"
|
||||||
"@opentelemetry/host-metrics": "npm:^0.37.0"
|
"@opentelemetry/host-metrics": "npm:^0.38.0"
|
||||||
"@opentelemetry/instrumentation": "npm:^0.208.0"
|
"@opentelemetry/instrumentation": "npm:^0.208.0"
|
||||||
"@opentelemetry/instrumentation-graphql": "npm:^0.56.0"
|
"@opentelemetry/instrumentation-graphql": "npm:^0.56.0"
|
||||||
"@opentelemetry/instrumentation-http": "npm:^0.208.0"
|
"@opentelemetry/instrumentation-http": "npm:^0.208.0"
|
||||||
@@ -11440,14 +11440,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/host-metrics@npm:^0.37.0":
|
"@opentelemetry/host-metrics@npm:^0.38.0":
|
||||||
version: 0.37.0
|
version: 0.38.0
|
||||||
resolution: "@opentelemetry/host-metrics@npm:0.37.0"
|
resolution: "@opentelemetry/host-metrics@npm:0.38.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
systeminformation: "npm:5.23.8"
|
systeminformation: "npm:5.23.8"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@opentelemetry/api": ^1.3.0
|
"@opentelemetry/api": ^1.3.0
|
||||||
checksum: 10/c2929a28f046ff99633ae4dafb758967be5623fa9c456f01a82d7f58823bf328a388f928e36a0591d963adff532fe9461abbebc532a7604ec48f3a194cb7f129
|
checksum: 10/22f088b4c1541e86b4447d6386b771bed9c332bf0449b5c6fdc1ebbc1ccd5dc5f37223ad10e4591040cd99123596a92f05aa77587cdd2702c53c6a1b77169b79
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user