diff --git a/web/src/core-bridge/worker/channelDataKeys.ts b/web/src/core-bridge/worker/channelDataKeys.ts index 9221ebc..d6ff609 100644 --- a/web/src/core-bridge/worker/channelDataKeys.ts +++ b/web/src/core-bridge/worker/channelDataKeys.ts @@ -5,7 +5,7 @@ import { apiFetch } from './http'; import { accountPublicKey, accountToken, requireAccount } from './accountSession'; import { decryptInvitation, encryptInvitation } from './accountCrypto'; import { randomDomainKey, newObjectKey, unwrapObjectKey } from './domainCrypto'; -import { loadWorkspace } from './workspaceStructure'; +import { cachedWorkspace, loadWorkspace } from './workspaceStructure'; import { pinSigningRoot } from './accountPersistence'; import { signExistingContentEvent, verifyDeviceCertificate } from './accountSigning'; @@ -13,12 +13,17 @@ const objectKeys = new Map(); const states = new Map(); const domainId = (channelId: string, domain: string, epoch: number) => `${channelId}:${domain}:${epoch}`; const contextId = (spaceId: string, channelId: string, domain: string, epoch: number) => `${spaceId}:${channelId}:${domain}:${epoch}`; -export function clearDataKeys(): void { objectKeys.clear(); states.clear(); } +const loaded = new Map(); +export function clearDataKeys(): void { objectKeys.clear(); states.clear(); loaded.clear(); } export function attachDataKeys(session: ChannelSession, resource: SavedResource): void { if (resource.kind !== 'channel') return; session.objectKeyResolver = async itemId => { const cached = objectKeys.get(`${resource.id}:${itemId}`); if (cached) return cached; - const { keys } = await readDataKeys(resource, session); + // 内存里已有这个对象和它的数据域密钥就直接用;否则才重新拉(每次都拉整份 crypto 状态,历史越长越慢) + const { keys } = await cachedKeys(resource, session, data => { + const known = data.state.objects.find(o => o.itemId === itemId); + return Boolean(known && data.keys[domainId(known.channelId, known.domain, known.epoch)]); + }); const object = states.get(resource.id)?.objects.find(o => o.itemId === itemId); if (!object || object.domain !== 'chat' && object.domain !== 'files') throw new Error('该对象没有已授权的解密数据域'); const key = keys[domainId(object.channelId, object.domain, object.epoch)]; @@ -35,6 +40,11 @@ export function readDataKeys(resource: SavedResource, session: ChannelSession): if (!task) { task = loadDataKeys(resource, session).finally(() => pendingKeys.delete(resource.id)); pendingKeys.set(resource.id, task); } return task; } +/** 内存里的结果满足 enough 就复用,不满足再请求 */ +async function cachedKeys(resource: SavedResource, session: ChannelSession, enough: (data: DataKeys) => boolean): Promise { + const have = loaded.get(resource.id); + return have && enough(have) ? have : readDataKeys(resource, session); +} async function loadDataKeys(resource: SavedResource, session: ChannelSession): Promise { const state = await apiFetch(`/spaces/${resource.id}/crypto`, { headers: session.authHeaders(session.can('owner') ? 'owner' : 'read') }).then(r => r.json() as Promise); states.set(resource.id, state); @@ -49,7 +59,9 @@ async function loadDataKeys(resource: SavedResource, session: ChannelSession): P keys[id] = value.key; } if (latest && JSON.stringify(keys) !== JSON.stringify(latest.domainKeys ?? {})) await saveResource({ ...latest, domainKeys: keys }); - return { state, keys }; + const result = { state, keys }; + loaded.set(resource.id, result); + return result; } async function envelope(spaceId: string, channelId: string, domain: 'chat' | 'files', epoch: number, key: string, username: string): Promise { const publicKey = username === requireAccount() ? accountPublicKey() : JSON.parse((await apiFetch(`/accounts/users/${username}/key`).then(r => r.json() as Promise)).public_key) as JsonWebKey; @@ -102,7 +114,8 @@ export async function initializeChannelKeys(resource: SavedResource, session: Ch export interface PreparedObject { itemIdBytes: Uint8Array; itemId: string; channelId: string; domain: 'chat' | 'files'; epoch: number } export async function prepareObject(resource: SavedResource, session: ChannelSession, channelId: string, domain: 'chat' | 'files'): Promise { const idBytes = crypto.getRandomValues(new Uint8Array(16)); const itemId = toBase64Url(idBytes); - const { structure } = await loadWorkspace(resource.id); let channel = structure.channels.find(c => c.id === channelId); + let channel = (await cachedWorkspace(resource.id)).structure.channels.find(c => c.id === channelId); + if (!channel) channel = (await loadWorkspace(resource.id)).structure.channels.find(c => c.id === channelId); if (!channel) throw new Error('请先创建频道'); if (resource.mode === 'enc' && !(domain === 'chat' ? channel.chatEpoch : channel.filesEpoch)) { await initializeChannelKeys(resource, session, channelId); @@ -111,7 +124,7 @@ export async function prepareObject(resource: SavedResource, session: ChannelSes const epoch = domain === 'chat' ? channel.chatEpoch : channel.filesEpoch; let keyEnvelope = ''; if (resource.mode === 'enc') { - const { keys } = await readDataKeys(resource, session); const key = keys[domainId(channelId, domain, epoch)]; + const { keys } = await cachedKeys(resource, session, data => Boolean(data.keys[domainId(channelId, domain, epoch)])); const key = keys[domainId(channelId, domain, epoch)]; if (!key) throw new Error('未获得该频道数据域的访问授权'); const created = await newObjectKey(key, { spaceId: resource.id, channelId, domain, epoch, itemId }); keyEnvelope = created.envelope; objectKeys.set(`${resource.id}:${itemId}`, created.key); diff --git a/web/src/core-bridge/worker/workspaceStructure.ts b/web/src/core-bridge/worker/workspaceStructure.ts index f5a1902..6d95836 100644 --- a/web/src/core-bridge/worker/workspaceStructure.ts +++ b/web/src/core-bridge/worker/workspaceStructure.ts @@ -24,6 +24,10 @@ async function publish(id: string, body: WorkspaceBody): Promise publishWorkspace(id, { t: 'structure', state: body.structure, people }); return body; } +/** 结构由流里的 settings 通知保持最新;发消息等热路径直接用缓存,不再每次多等一个往返 */ +export async function cachedWorkspace(id: string): Promise { + return current.get(id) ?? loadWorkspace(id); +} export async function loadWorkspace(id: string): Promise { const { session } = await getSession(id); const body = await apiFetch(`/spaces/${id}/structure`, { headers: session.authHeaders('read') }).then(r => r.json() as Promise);