mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 12:06:35 +08:00
feat: improve electron sandbox (#14156)
This commit is contained in:
@@ -10,6 +10,7 @@ import { z } from 'zod';
|
||||
import {
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
AFFINE_EVENT_CHANNEL_NAME,
|
||||
AFFINE_EVENT_SUBSCRIBE_CHANNEL_NAME,
|
||||
type ExposedMeta,
|
||||
type HelperToRenderer,
|
||||
type RendererToHelper,
|
||||
@@ -83,6 +84,33 @@ function getMainAPIs() {
|
||||
|
||||
// channel -> callback[]
|
||||
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) => {
|
||||
if (typeof channel !== 'string') {
|
||||
@@ -108,12 +136,20 @@ function getMainAPIs() {
|
||||
...(listenersMap.get(channel) ?? []),
|
||||
callback,
|
||||
]);
|
||||
subscribe(channel);
|
||||
|
||||
return () => {
|
||||
const listeners = listenersMap.get(channel) ?? [];
|
||||
const index = listeners.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
if (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,
|
||||
} 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
|
||||
const CLIENT_ID: string = Math.random().toString(36).slice(2);
|
||||
|
||||
@@ -35,42 +25,97 @@ function createSharedStorageApi(
|
||||
}
|
||||
) {
|
||||
const memory = new MemoryMemento();
|
||||
memory.setAll(init);
|
||||
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => {
|
||||
if (channel === `sharedStorage:${event}`) {
|
||||
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;
|
||||
const revisions = new Map<string, number>();
|
||||
const updateQueue: Record<string, any>[] = [];
|
||||
let loaded = false;
|
||||
|
||||
if (raw && typeof raw === 'object' && 'v' in raw) {
|
||||
value = (raw as any).v;
|
||||
source = (raw as any).s;
|
||||
} else {
|
||||
value = raw;
|
||||
}
|
||||
const applyUpdates = (updates: Record<string, any>) => {
|
||||
for (const [key, raw] of Object.entries(updates)) {
|
||||
// '*' means "reset everything" coming from a clear operation
|
||||
if (key === '*') {
|
||||
memory.clear();
|
||||
revisions.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore our own broadcasts
|
||||
if (source && source === CLIENT_ID) {
|
||||
// support both legacy plain value and new { v, r, s } structure
|
||||
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;
|
||||
}
|
||||
revisions.set(key, rev);
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
memory.del(key);
|
||||
} else {
|
||||
memory.set(key, value);
|
||||
}
|
||||
if (value === undefined) {
|
||||
memory.del(key);
|
||||
} else {
|
||||
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 {
|
||||
ready: initPromise,
|
||||
del(key: string) {
|
||||
memory.del(key);
|
||||
invokeWithCatch(`sharedStorage:${api.del}`, key, CLIENT_ID);
|
||||
},
|
||||
clear() {
|
||||
memory.clear();
|
||||
revisions.clear();
|
||||
invokeWithCatch(`sharedStorage:${api.clear}`, CLIENT_ID);
|
||||
},
|
||||
get<T>(key: string): T | undefined {
|
||||
@@ -90,25 +135,17 @@ function createSharedStorageApi(
|
||||
};
|
||||
}
|
||||
|
||||
export const globalState = createSharedStorageApi(
|
||||
initialGlobalState,
|
||||
'onGlobalStateChanged',
|
||||
{
|
||||
clear: 'clearGlobalState',
|
||||
del: 'delGlobalState',
|
||||
set: 'setGlobalState',
|
||||
}
|
||||
);
|
||||
export const globalState = createSharedStorageApi({}, 'onGlobalStateChanged', {
|
||||
clear: 'clearGlobalState',
|
||||
del: 'delGlobalState',
|
||||
set: 'setGlobalState',
|
||||
});
|
||||
|
||||
export const globalCache = createSharedStorageApi(
|
||||
initialGlobalCache,
|
||||
'onGlobalCacheChanged',
|
||||
{
|
||||
clear: 'clearGlobalCache',
|
||||
del: 'delGlobalCache',
|
||||
set: 'setGlobalCache',
|
||||
}
|
||||
);
|
||||
export const globalCache = createSharedStorageApi({}, 'onGlobalCacheChanged', {
|
||||
clear: 'clearGlobalCache',
|
||||
del: 'delGlobalCache',
|
||||
set: 'setGlobalCache',
|
||||
});
|
||||
|
||||
export const sharedStorage = {
|
||||
globalState,
|
||||
|
||||
Reference in New Issue
Block a user