diff --git a/web/src/core-bridge/worker/account.ts b/web/src/core-bridge/worker/account.ts index a8a97bb..7f53435 100644 --- a/web/src/core-bridge/worker/account.ts +++ b/web/src/core-bridge/worker/account.ts @@ -106,14 +106,17 @@ async function syncVault(): Promise { async function activate(identity: { username: string; token: string }, key: CryptoKey, remote: VaultBody, remember = true, cachedPersonal?: PersonalProfile): Promise { const profile = validateVault(await openJson(remote.vault, key)); - const deviceSigning = await ensureDeviceSigning(identity.username, identity.token, profile.keys.signing); + // 设备签名密钥与个人资料互不依赖,同时请求,冷启动少等一个往返 + const [deviceSigning, personal] = await Promise.all([ + ensureDeviceSigning(identity.username, identity.token, profile.keys.signing), + loadPersonal(identity.token).catch((error: unknown) => { + if (!(error instanceof Error && error.message === '网络连接失败')) throw error; + return cachedPersonal; + }), + ]); clearResourceSessions(); setResourceScope(identity.username); const session: AccountSession = { ...identity, key, profile, revision: remote.revision, remember, controller: new AbortController(), dirty: false, deviceSigning }; - try { session.personal = await loadPersonal(identity.token); } - catch (error) { - if (!(error instanceof Error && error.message === '网络连接失败')) throw error; - session.personal = cachedPersonal; - } + session.personal = personal; account = session; setAccountIdentity(identity, profile.keys.publicKey, session.personal); await applyProfile(mergeVault(profile, await listAllResources(), await listResourceDeletes())); setResourceChangeListener(scheduleSync); @@ -135,7 +138,8 @@ export function restoreAccount(): Promise { try { const session = await activate(saved, saved.key, remote, true, saved.personal); setAccountIdentity(session, session.profile.keys.publicKey, session.personal); - await syncVault().catch(() => {}); + // 保险库已在 activate 里并入本机资源,完整同步(约 4 次往返)放到后台,不挡住打开页面 + void syncVault().catch(() => {}); } catch (error) { await logoutAccount(); throw error; } })().catch((error: unknown) => { restoration = undefined; throw error; }); return restoration; diff --git a/web/src/core-bridge/worker/channelTransport.ts b/web/src/core-bridge/worker/channelTransport.ts index 04416f5..85e2a4d 100644 --- a/web/src/core-bridge/worker/channelTransport.ts +++ b/web/src/core-bridge/worker/channelTransport.ts @@ -21,8 +21,10 @@ export async function connectChannel(id: string): Promise { if (resource.kind !== 'channel') throw new Error('资源类型不匹配'); // Device sessions can be revoked independently of account membership; rejoin idempotently. const joinedAt = freshlyJoined.get(id); freshlyJoined.delete(id); - if (joinedAt === undefined || Date.now() - joinedAt > 60_000) - await apiFetch(`/channels/${id}/join`, { method: 'POST', headers: session.authHeaders(session.can('owner') ? 'owner' : 'read'), signal }); + const join = () => apiFetch(`/channels/${id}/join`, { method: 'POST', headers: session.authHeaders(session.can('owner') ? 'owner' : 'read'), signal }); + // 已保存的成员重连只是确认身份:与首屏数据同时发出,失败就由下面的重连循环再来一次。首次加入(needsJoin)仍然先 join + const joining = joinedAt !== undefined && Date.now() - joinedAt <= 60_000 ? undefined + : resource.needsJoin ? await join().then(() => undefined) : join().then(() => undefined); if (resource.needsJoin) { if (session.can('owner')) await channelMetadata(resource, session); resource.needsJoin = false; @@ -49,7 +51,7 @@ export async function connectChannel(id: string): Promise { while (!signal.aborted) { try { if (!initialized) { - [stream.cursor] = await Promise.all([syncChannel(resource, session, signal), channelMetadata(resource, session)]); + [stream.cursor] = await Promise.all([syncChannel(resource, session, signal), channelMetadata(resource, session), joining]); if (signal.aborted) break; await flushOutbox(resource, session); initialized = true; diff --git a/web/src/store/channels.ts b/web/src/store/channels.ts index 71218cb..50d0951 100644 --- a/web/src/store/channels.ts +++ b/web/src/store/channels.ts @@ -24,7 +24,7 @@ interface ChannelsState { const upsert = (list: ChannelInfo[], c: ChannelInfo) => [c, ...list.filter((x) => x.id !== c.id)]; let hydration: Promise | undefined; -export const useChannels = create()((set) => ({ +export const useChannels = create()((set, get) => ({ list: [], hydrated: false, pendingLink: null, @@ -41,6 +41,8 @@ export const useChannels = create()((set) => ({ return c; }, hydrate: () => { + // 账号刷新已经列过一次,别再请求一遍 + if (get().hydrated) return Promise.resolve(); hydration ??= api.listSaved().then((list) => set({ list, hydrated: true }), () => set({ hydrated: true })); return hydration; },