perf(web): shorten the first open of a space for new members
Accepting an invitation no longer waits for the vault sync, the redundant join right after joining is skipped, and the crypto state, first log page and channel metadata are requested in parallel.
This commit is contained in:
1 parent
d4a72bf177
commit
06fe077ef1
6 files changed
+26
-9
No files matched your search
@@ -260,7 +260,8 @@ export async function acceptInvitation(channelId: string): Promise<void> {
|
||||
const invitation = (await listAccountInvitations()).find((row) => row.channel_id === channelId);
|
||||
if (!invitation) throw new Error('邀请已失效');
|
||||
const link = await openChannelGrant(invitation.payload, session.username, channelId, session.profile.keys.privateKey, 'member', self.location.origin);
|
||||
stillActive(session); await joinChannel({ link }); await syncVault();
|
||||
// 保险库同步(约 5 次往返)放到后台,不让用户等着:频道资源已在本机保存,可以直接打开
|
||||
stillActive(session); await joinChannel({ link }); scheduleSync();
|
||||
}
|
||||
export async function inviteChannelUser(channelId: string, input: string): Promise<void> {
|
||||
requireAccount(); const username = usernameOf(input); const { session, resource } = await getSession(channelId);
|
||||
|
||||
@@ -27,7 +27,15 @@ export function attachDataKeys(session: ChannelSession, resource: SavedResource)
|
||||
objectKeys.set(`${resource.id}:${itemId}`, unwrapped); return unwrapped;
|
||||
};
|
||||
}
|
||||
export async function readDataKeys(resource: SavedResource, session: ChannelSession): Promise<{ state: CryptoState; keys: Record<string, string> }> {
|
||||
type DataKeys = { state: CryptoState; keys: Record<string, string> };
|
||||
const pendingKeys = new Map<string, Promise<DataKeys>>();
|
||||
/** 同一空间同时发起的读取合并成一次请求 */
|
||||
export function readDataKeys(resource: SavedResource, session: ChannelSession): Promise<DataKeys> {
|
||||
let task = pendingKeys.get(resource.id);
|
||||
if (!task) { task = loadDataKeys(resource, session).finally(() => pendingKeys.delete(resource.id)); pendingKeys.set(resource.id, task); }
|
||||
return task;
|
||||
}
|
||||
async function loadDataKeys(resource: SavedResource, session: ChannelSession): Promise<DataKeys> {
|
||||
const state = await apiFetch(`/spaces/${resource.id}/crypto`, { headers: session.authHeaders(session.can('owner') ? 'owner' : 'read') }).then(r => r.json() as Promise<CryptoState>);
|
||||
states.set(resource.id, state);
|
||||
const latest = await getResource(resource.id); const keys = { ...latest?.domainKeys };
|
||||
|
||||
@@ -129,15 +129,19 @@ export async function channelMetadata(resource: SavedResource, session: ChannelS
|
||||
}
|
||||
|
||||
export async function syncChannel(resource: SavedResource, session: ChannelSession, signal: AbortSignal): Promise<number> {
|
||||
if (resource.mode === 'enc') await readDataKeys(resource, session);
|
||||
// reset 必须同步发出:connectChannel 随后并行拉取频道信息,它们的结果要落在 reset 之后
|
||||
publishWorkspace(resource.id, { t: 'reset' });
|
||||
clearFileCatalog(resource.id);
|
||||
readers.set(resource.id, session.createItemReader());
|
||||
// 密钥与第一页日志互不依赖,同时发出,首次打开少等一个往返
|
||||
const keys = resource.mode === 'enc' ? readDataKeys(resource, session) : undefined;
|
||||
keys?.catch(() => undefined);
|
||||
let from = 0;
|
||||
for (;;) {
|
||||
for (let first = true; ; first = false) {
|
||||
const response = await apiFetch(`/channels/${resource.id}/log?from=${from}&limit=200`, {
|
||||
headers: session.authHeaders('read'), signal,
|
||||
});
|
||||
if (first) await keys;
|
||||
const records = parseLogRecords(new Uint8Array(await response.arrayBuffer()));
|
||||
for (const record of records) {
|
||||
await applyChannelRecord(resource, session, record);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { channelCache, isOfflineFailure } from './resourceMetadata';
|
||||
import { accountToken } from './accountSession';
|
||||
import { attachDataKeys, initializeChannelKeys } from './channelDataKeys';
|
||||
import type { ChannelSettingsPatch as SettingsRequest, JoinResult, MemberBody } from '../generated/api';
|
||||
import { getSession, sessions } from './sessions';
|
||||
import { freshlyJoined, getSession, sessions } from './sessions';
|
||||
import { makeLink } from './links';
|
||||
import { toChannelInfo, type ChannelWire } from './wire';
|
||||
import { stopChannel } from './channelTransport';
|
||||
@@ -90,6 +90,7 @@ export async function joinChannel(input: { link: ParsedChannelLink; password?: s
|
||||
};
|
||||
await saveResource(resource);
|
||||
sessions.set(id, session);
|
||||
freshlyJoined.set(id, Date.now());
|
||||
return toChannelInfo(info, resource);
|
||||
} catch (error) {
|
||||
session.dispose();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IntegrityError } from '@webdrop/core';
|
||||
import { active, getSession } from './sessions';
|
||||
import { active, freshlyJoined, getSession } from './sessions';
|
||||
import { apiFetch, type ApiError } from './http';
|
||||
import { publishWorkspace } from './presentation';
|
||||
import { channelMetadata, processChannelBytes, syncChannel } from './channelEvents';
|
||||
@@ -20,7 +20,9 @@ export async function connectChannel(id: string): Promise<void> {
|
||||
if (signal.aborted) return;
|
||||
if (resource.kind !== 'channel') throw new Error('资源类型不匹配');
|
||||
// Device sessions can be revoked independently of account membership; rejoin idempotently.
|
||||
await apiFetch(`/channels/${id}/join`, { method: 'POST', headers: session.authHeaders(session.can('owner') ? 'owner' : 'read'), signal });
|
||||
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 });
|
||||
if (resource.needsJoin) {
|
||||
if (session.can('owner')) await channelMetadata(resource, session);
|
||||
resource.needsJoin = false;
|
||||
@@ -47,9 +49,8 @@ export async function connectChannel(id: string): Promise<void> {
|
||||
while (!signal.aborted) {
|
||||
try {
|
||||
if (!initialized) {
|
||||
stream.cursor = await syncChannel(resource, session, signal);
|
||||
[stream.cursor] = await Promise.all([syncChannel(resource, session, signal), channelMetadata(resource, session)]);
|
||||
if (signal.aborted) break;
|
||||
await channelMetadata(resource, session);
|
||||
await flushOutbox(resource, session);
|
||||
initialized = true;
|
||||
publishWorkspace(id, { t: 'ready' });
|
||||
|
||||
@@ -8,6 +8,8 @@ import { clearWorkspaceStructures } from './workspaceStructure';
|
||||
import { attachDataKeys, clearDataKeys } from './channelDataKeys';
|
||||
export const sessions = new Map<string, ChannelSession>();
|
||||
export const readers = new Map<string, ReturnType<ChannelSession['createItemReader']>>();
|
||||
/** 刚加入的频道 → 加入时间;connectChannel 据此跳过紧接着的重复 join(省一次往返) */
|
||||
export const freshlyJoined = new Map<string, number>();
|
||||
export const active = new Map<string, AbortController>();
|
||||
export const blobs = new Map<string, Blob>();
|
||||
const opening = new Map<string, Promise<ChannelSession>>();
|
||||
|
||||
Reference in new issue
Block a user