chore: drop old client support (#14369)

This commit is contained in:
DarkSky
2026-02-05 02:49:33 +08:00
committed by GitHub
parent de29e8300a
commit 403f16b404
103 changed files with 3293 additions and 997 deletions
@@ -1,5 +1,339 @@
import test from 'ava';
import test, { type ExecutionContext } from 'ava';
import { io, type Socket as SocketIOClient } from 'socket.io-client';
import { Doc, encodeStateAsUpdate } from 'yjs';
test('should test through sync gateway', t => {
t.pass();
import { createTestingApp, TestingApp } from '../utils';
type WebsocketResponse<T> =
| { error: { name: string; message: string } }
| { data: T };
const WS_TIMEOUT_MS = 5_000;
function unwrapResponse<T>(t: ExecutionContext, res: WebsocketResponse<T>): T {
if ('data' in res) {
return res.data;
}
t.log(res);
throw new Error(`Websocket error: ${res.error.name}: ${res.error.message}`);
}
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);
}
}
function createClient(url: string, cookie: string): SocketIOClient {
return io(url, {
transports: ['websocket'],
reconnection: false,
forceNew: true,
extraHeaders: {
cookie,
},
});
}
function waitForConnect(socket: SocketIOClient) {
if (socket.connected) {
return Promise.resolve();
}
return withTimeout(
new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('connect_error', reject);
}),
WS_TIMEOUT_MS,
'socket connect'
);
}
function waitForDisconnect(socket: SocketIOClient) {
if (socket.disconnected) {
return Promise.resolve();
}
return withTimeout(
new Promise<void>(resolve => {
socket.once('disconnect', () => resolve());
}),
WS_TIMEOUT_MS,
'socket disconnect'
);
}
function emitWithAck<T>(socket: SocketIOClient, event: string, data: unknown) {
return withTimeout(
new Promise<WebsocketResponse<T>>(resolve => {
socket.emit(event, data, (res: WebsocketResponse<T>) => resolve(res));
}),
WS_TIMEOUT_MS,
`ack ${event}`
);
}
function waitForEvent<T>(socket: SocketIOClient, event: string) {
return withTimeout(
new Promise<T>(resolve => {
socket.once(event, (payload: T) => resolve(payload));
}),
WS_TIMEOUT_MS,
`event ${event}`
);
}
function expectNoEvent(
socket: SocketIOClient,
event: string,
durationMs = 200
) {
return withTimeout(
new Promise<void>((resolve, reject) => {
let timer: NodeJS.Timeout;
const onEvent = () => {
clearTimeout(timer);
socket.off(event, onEvent);
reject(new Error(`Unexpected event received: ${event}`));
};
timer = setTimeout(() => {
socket.off(event, onEvent);
resolve();
}, durationMs);
socket.on(event, onEvent);
}),
WS_TIMEOUT_MS,
`expect no event ${event}`
);
}
async function login(app: TestingApp) {
const user = await app.createUser('u1@affine.pro');
const res = await app
.POST('/api/auth/sign-in')
.send({ email: user.email, password: user.password })
.expect(200);
const cookies = res.get('Set-Cookie') ?? [];
const cookieHeader = cookies.map(c => c.split(';')[0]).join('; ');
return { user, cookieHeader };
}
function createYjsUpdateBase64() {
const doc = new Doc();
doc.getMap('m').set('k', 'v');
const update = encodeStateAsUpdate(doc);
return Buffer.from(update).toString('base64');
}
let app: TestingApp;
let url: string;
test.before(async () => {
app = await createTestingApp();
url = app.url();
});
test.beforeEach(async () => {
await app.initTestingDB();
});
test.after.always(async () => {
await app.close();
});
test('clientVersion=0.25.0 should only receive space:broadcast-doc-update', async t => {
const { user, cookieHeader } = await login(app);
const spaceId = user.id;
const update = createYjsUpdateBase64();
const sender = createClient(url, cookieHeader);
const receiver = createClient(url, cookieHeader);
try {
await Promise.all([waitForConnect(sender), waitForConnect(receiver)]);
const receiverJoin = unwrapResponse(
t,
await emitWithAck<{ clientId: string; success: boolean }>(
receiver,
'space:join',
{ spaceType: 'userspace', spaceId, clientVersion: '0.25.0' }
)
);
t.true(receiverJoin.success);
const senderJoin = unwrapResponse(
t,
await emitWithAck<{ clientId: string; success: boolean }>(
sender,
'space:join',
{ spaceType: 'userspace', spaceId, clientVersion: '0.26.0' }
)
);
t.true(senderJoin.success);
const onUpdate = waitForEvent<{
spaceType: string;
spaceId: string;
docId: string;
update: string;
}>(receiver, 'space:broadcast-doc-update');
const noUpdates = expectNoEvent(receiver, 'space:broadcast-doc-updates');
const pushRes = await emitWithAck<{ accepted: true; timestamp?: number }>(
sender,
'space:push-doc-update',
{
spaceType: 'userspace',
spaceId,
docId: 'doc-1',
update,
}
);
unwrapResponse(t, pushRes);
const message = await onUpdate;
t.is(message.spaceType, 'userspace');
t.is(message.spaceId, spaceId);
t.is(message.docId, 'doc-1');
t.is(message.update, update);
await noUpdates;
} finally {
sender.disconnect();
receiver.disconnect();
}
});
test('clientVersion>=0.26.0 should only receive space:broadcast-doc-updates', async t => {
const { user, cookieHeader } = await login(app);
const spaceId = user.id;
const update = createYjsUpdateBase64();
const sender = createClient(url, cookieHeader);
const receiver = createClient(url, cookieHeader);
try {
await Promise.all([waitForConnect(sender), waitForConnect(receiver)]);
const receiverJoin = unwrapResponse(
t,
await emitWithAck<{ clientId: string; success: boolean }>(
receiver,
'space:join',
{ spaceType: 'userspace', spaceId, clientVersion: '0.26.0' }
)
);
t.true(receiverJoin.success);
const senderJoin = unwrapResponse(
t,
await emitWithAck<{ clientId: string; success: boolean }>(
sender,
'space:join',
{ spaceType: 'userspace', spaceId, clientVersion: '0.25.0' }
)
);
t.true(senderJoin.success);
const onUpdates = waitForEvent<{
spaceType: string;
spaceId: string;
docId: string;
updates: string[];
}>(receiver, 'space:broadcast-doc-updates');
const noUpdate = expectNoEvent(receiver, 'space:broadcast-doc-update');
const pushRes = await emitWithAck<{ accepted: true; timestamp?: number }>(
sender,
'space:push-doc-update',
{
spaceType: 'userspace',
spaceId,
docId: 'doc-2',
update,
}
);
unwrapResponse(t, pushRes);
const message = await onUpdates;
t.is(message.spaceType, 'userspace');
t.is(message.spaceId, spaceId);
t.is(message.docId, 'doc-2');
t.deepEqual(message.updates, [update]);
await noUpdate;
} finally {
sender.disconnect();
receiver.disconnect();
}
});
test('clientVersion<0.25.0 should be rejected and disconnected', async t => {
const { user, cookieHeader } = await login(app);
const spaceId = user.id;
const socket = createClient(url, cookieHeader);
try {
await waitForConnect(socket);
const res = unwrapResponse(
t,
await emitWithAck<{ clientId: string; success: boolean }>(
socket,
'space:join',
{ spaceType: 'userspace', spaceId, clientVersion: '0.24.4' }
)
);
t.false(res.success);
await waitForDisconnect(socket);
} finally {
socket.disconnect();
}
});
test('space:join-awareness should reject clientVersion<0.25.0', async t => {
const { user, cookieHeader } = await login(app);
const spaceId = user.id;
const socket = createClient(url, cookieHeader);
try {
await waitForConnect(socket);
const res = unwrapResponse(
t,
await emitWithAck<{ clientId: string; success: boolean }>(
socket,
'space:join-awareness',
{
spaceType: 'userspace',
spaceId,
docId: 'doc-awareness',
clientVersion: '0.24.4',
}
)
);
t.false(res.success);
await waitForDisconnect(socket);
} finally {
socket.disconnect();
}
});