mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
feat(core): integrate realtime features (#15003)
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
listNotificationsQuery,
|
||||
MentionNotificationBodyType,
|
||||
mentionUserMutation,
|
||||
notificationCountQuery,
|
||||
NotificationObjectType,
|
||||
NotificationType,
|
||||
readAllNotificationsMutation,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
} from '@affine/graphql';
|
||||
|
||||
import { Mockers } from '../../mocks';
|
||||
import { createRealtimeClient, realtimeRequest } from '../realtime';
|
||||
import { app, e2e } from '../test';
|
||||
|
||||
async function init() {
|
||||
@@ -270,10 +270,10 @@ e2e('should mark notification as read', async t => {
|
||||
},
|
||||
});
|
||||
}
|
||||
const count = await app.gql({
|
||||
query: notificationCountQuery,
|
||||
});
|
||||
t.is(count.currentUser!.notificationCount, 0);
|
||||
const socket = await createRealtimeClient(app, member);
|
||||
t.teardown(() => socket.disconnect());
|
||||
const count = await realtimeRequest(socket, 'notification.count.get', {});
|
||||
t.is(count.count, 0);
|
||||
|
||||
// read again should work
|
||||
for (const notification of notifications) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
RealtimeAck,
|
||||
RealtimeRequestInputOf,
|
||||
RealtimeRequestName,
|
||||
RealtimeRequestOutputOf,
|
||||
} from '@affine/realtime';
|
||||
import { io, type Socket as SocketIOClient } from 'socket.io-client';
|
||||
import type { Response } from 'supertest';
|
||||
|
||||
import type { MockedUser } from '../mocks';
|
||||
import type { TestingApp } from './create-app';
|
||||
|
||||
const REALTIME_CLIENT_VERSION = '0.26.0';
|
||||
const WS_TIMEOUT_MS = 5_000;
|
||||
|
||||
function cookieHeader(res: Response) {
|
||||
return (res.get('Set-Cookie') ?? [])
|
||||
.map(cookie => cookie.split(';')[0])
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
label: string
|
||||
) {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(`Timeout (${timeoutMs}ms): ${label}`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([promise, timeout]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForConnect(socket: SocketIOClient) {
|
||||
if (socket.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
await withTimeout(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
socket.once('connect', resolve);
|
||||
socket.once('connect_error', reject);
|
||||
}),
|
||||
WS_TIMEOUT_MS,
|
||||
'realtime socket connect'
|
||||
);
|
||||
}
|
||||
|
||||
export async function createRealtimeClient(app: TestingApp, user: MockedUser) {
|
||||
const login = await app.login(user);
|
||||
const socket = io(app.url, {
|
||||
transports: ['websocket'],
|
||||
reconnection: false,
|
||||
forceNew: true,
|
||||
extraHeaders: {
|
||||
cookie: cookieHeader(login),
|
||||
},
|
||||
});
|
||||
await waitForConnect(socket);
|
||||
return socket;
|
||||
}
|
||||
|
||||
export async function realtimeRequest<Op extends RealtimeRequestName>(
|
||||
socket: SocketIOClient,
|
||||
op: Op,
|
||||
input: RealtimeRequestInputOf<Op>
|
||||
): Promise<RealtimeRequestOutputOf<Op>> {
|
||||
const ack = await withTimeout(
|
||||
new Promise<RealtimeAck<RealtimeRequestOutputOf<Op>>>(resolve => {
|
||||
socket.emit(
|
||||
'realtime:request',
|
||||
{ op, input, clientVersion: REALTIME_CLIENT_VERSION },
|
||||
(res: RealtimeAck<RealtimeRequestOutputOf<Op>>) => resolve(res)
|
||||
);
|
||||
}),
|
||||
WS_TIMEOUT_MS,
|
||||
`realtime request ${op}`
|
||||
);
|
||||
|
||||
if ('error' in ack) {
|
||||
throw new Error(`${ack.error.name}: ${ack.error.message}`);
|
||||
}
|
||||
|
||||
return ack.data;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
approveWorkspaceTeamMemberMutation,
|
||||
createInviteLinkMutation,
|
||||
getInviteInfoQuery,
|
||||
getMembersByWorkspaceIdQuery,
|
||||
inviteByEmailsMutation,
|
||||
leaveWorkspaceMutation,
|
||||
revokeMemberPermissionMutation,
|
||||
@@ -13,16 +12,21 @@ import {
|
||||
WorkspaceMemberStatus,
|
||||
} from '@affine/graphql';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
WorkspaceMemberSource,
|
||||
WorkspaceMemberStatus as PrismaWorkspaceMemberStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { EntitlementService } from '../../../core/entitlement';
|
||||
import { WorkspacePolicyService } from '../../../core/permission';
|
||||
import { Models } from '../../../models';
|
||||
import { Models, WorkspaceRole as ModelWorkspaceRole } from '../../../models';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
} from '../../../plugins/payment/types';
|
||||
import { Mockers } from '../../mocks';
|
||||
import { createRealtimeClient, realtimeRequest } from '../realtime';
|
||||
import { app, e2e } from '../test';
|
||||
|
||||
const TWO_BILLION_BYTES = 2_000_000_000;
|
||||
@@ -380,39 +384,31 @@ e2e('should support pagination for member', async t => {
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
let result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 2,
|
||||
},
|
||||
const socket = await createRealtimeClient(app, owner);
|
||||
t.teardown(() => socket.disconnect());
|
||||
let result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 2,
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 2);
|
||||
t.is(result.memberCount, 3);
|
||||
t.is(result.members.length, 2);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 2,
|
||||
take: 2,
|
||||
},
|
||||
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
skip: 2,
|
||||
take: 2,
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 1);
|
||||
t.is(result.memberCount, 3);
|
||||
t.is(result.members.length, 1);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 3,
|
||||
take: 2,
|
||||
},
|
||||
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
skip: 3,
|
||||
take: 2,
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 0);
|
||||
t.is(result.memberCount, 3);
|
||||
t.is(result.members.length, 0);
|
||||
});
|
||||
|
||||
e2e('should limit member count correctly', async t => {
|
||||
@@ -428,17 +424,15 @@ e2e('should limit member count correctly', async t => {
|
||||
})
|
||||
);
|
||||
|
||||
await app.login(owner);
|
||||
const result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
},
|
||||
const socket = await createRealtimeClient(app, owner);
|
||||
t.teardown(() => socket.disconnect());
|
||||
const result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
});
|
||||
t.is(result.workspace.memberCount, 11);
|
||||
t.is(result.workspace.members.length, 10);
|
||||
t.is(result.memberCount, 11);
|
||||
t.is(result.members.length, 10);
|
||||
});
|
||||
|
||||
e2e('should get invite link info with status', async t => {
|
||||
@@ -634,38 +628,54 @@ e2e(
|
||||
userId: user2.id,
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
let result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
query: 'lucy',
|
||||
},
|
||||
const socket = await createRealtimeClient(app, owner);
|
||||
t.teardown(() => socket.disconnect());
|
||||
let result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
query: 'lucy',
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 1);
|
||||
t.is(result.workspace.members[0].name, user1.name);
|
||||
t.is(result.memberCount, 3);
|
||||
t.is(result.members.length, 1);
|
||||
t.is(result.members[0].name, user1.name);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
query: 'LUCY',
|
||||
},
|
||||
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
query: 'LUCY',
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 1);
|
||||
t.is(result.workspace.members[0].name, user1.name);
|
||||
t.is(result.memberCount, 3);
|
||||
t.is(result.members.length, 1);
|
||||
t.is(result.members[0].name, user1.name);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
query: 'jeanne_doe',
|
||||
},
|
||||
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
query: 'jeanne_doe',
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 1);
|
||||
t.is(result.workspace.members[0].email, user2.email);
|
||||
t.is(result.memberCount, 3);
|
||||
t.is(result.members.length, 1);
|
||||
t.is(result.members[0].email, user2.email);
|
||||
|
||||
const pendingEmail = `pending_search.${randomUUID()}@affine.pro`;
|
||||
const pendingUser = await app.create(Mockers.User, {
|
||||
email: pendingEmail,
|
||||
});
|
||||
await app
|
||||
.get(Models)
|
||||
.workspaceUser.set(
|
||||
workspace.id,
|
||||
pendingUser.id,
|
||||
ModelWorkspaceRole.Collaborator,
|
||||
{
|
||||
status: PrismaWorkspaceMemberStatus.Pending,
|
||||
source: WorkspaceMemberSource.Email,
|
||||
}
|
||||
);
|
||||
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||
workspaceId: workspace.id,
|
||||
query: 'pending_search',
|
||||
});
|
||||
t.is(result.memberCount, 4);
|
||||
t.is(result.members.length, 1);
|
||||
t.is(result.members[0].email, pendingEmail);
|
||||
t.is(result.members[0].status, WorkspaceMemberStatus.Pending);
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user