feat(nbstore): add cloud implementation (#8810)

This commit is contained in:
forehalo
2024-12-10 10:48:27 +00:00
parent 1721875ab6
commit 2f80b4f822
32 changed files with 1030 additions and 315 deletions

View File

@@ -8,7 +8,6 @@ import {
WebSocketGateway,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs';
import {
AlreadyInSpace,
@@ -83,6 +82,9 @@ interface LeaveSpaceAwarenessMessage {
docId: string;
}
/**
* @deprecated
*/
interface PushDocUpdatesMessage {
spaceType: SpaceType;
spaceId: string;
@@ -90,6 +92,13 @@ interface PushDocUpdatesMessage {
updates: string[];
}
interface PushDocUpdateMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
update: string;
}
interface LoadDocMessage {
spaceType: SpaceType;
spaceId: string;
@@ -97,6 +106,12 @@ interface LoadDocMessage {
stateVector?: string;
}
interface DeleteDocMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
}
interface LoadDocTimestampsMessage {
spaceType: SpaceType;
spaceId: string;
@@ -114,6 +129,7 @@ interface UpdateAwarenessMessage {
docId: string;
awarenessUpdate: string;
}
@WebSocketGateway()
export class SpaceSyncGateway
implements OnGatewayConnection, OnGatewayDisconnect
@@ -182,26 +198,6 @@ export class SpaceSyncGateway
}
}
async joinWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.join(room);
}
async leaveWorkspace(
client: Socket,
room: `${string}:${'sync' | 'awareness'}`
) {
await client.leave(room);
}
assertInWorkspace(client: Socket, room: `${string}:${'sync' | 'awareness'}`) {
if (!client.rooms.has(room)) {
throw new NotInSpace({ spaceId: room.split(':')[0] });
}
}
// v3
@SubscribeMessage('space:join')
async onJoinSpace(
@@ -233,36 +229,42 @@ export class SpaceSyncGateway
@MessageBody()
{ spaceType, spaceId, docId, stateVector }: LoadDocMessage
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
EventResponse<{ missing: string; state: string; timestamp: number }>
> {
const adapter = this.selectAdapter(client, spaceType);
adapter.assertIn(spaceId);
const doc = await adapter.get(spaceId, docId);
const doc = await adapter.diff(
spaceId,
docId,
stateVector ? Buffer.from(stateVector, 'base64') : undefined
);
if (!doc) {
throw new DocNotFound({ spaceId, docId });
}
const missing = Buffer.from(
stateVector
? diffUpdate(doc.bin, Buffer.from(stateVector, 'base64'))
: doc.bin
).toString('base64');
const state = Buffer.from(encodeStateVectorFromUpdate(doc.bin)).toString(
'base64'
);
return {
data: {
missing,
state,
missing: Buffer.from(doc.missing).toString('base64'),
state: Buffer.from(doc.state).toString('base64'),
timestamp: doc.timestamp,
},
};
}
@SubscribeMessage('space:delete-doc')
async onDeleteSpaceDoc(
@ConnectedSocket() client: Socket,
@MessageBody() { spaceType, spaceId, docId }: DeleteDocMessage
) {
const adapter = this.selectAdapter(client, spaceType);
await adapter.delete(spaceId, docId);
}
/**
* @deprecated use [space:push-doc-update] instead, client should always merge updates on their own
*/
@SubscribeMessage('space:push-doc-updates')
async onReceiveDocUpdates(
@ConnectedSocket() client: Socket,
@@ -307,6 +309,51 @@ export class SpaceSyncGateway
};
}
@SubscribeMessage('space:push-doc-update')
async onReceiveDocUpdate(
@ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody()
message: PushDocUpdateMessage
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
const { spaceType, spaceId, docId, update } = message;
const adapter = this.selectAdapter(client, spaceType);
// TODO(@forehalo): we might need to check write permission before push updates
const timestamp = await adapter.push(
spaceId,
docId,
[Buffer.from(update, 'base64')],
user.id
);
// TODO(@forehalo): separate different version of clients into different rooms,
// so the clients won't receive useless updates events
client.to(adapter.room(spaceId)).emit('space:broadcast-doc-updates', {
spaceType,
spaceId,
docId,
updates: [update],
timestamp,
});
client.to(adapter.room(spaceId)).emit('space:broadcast-doc-update', {
spaceType,
spaceId,
docId,
update,
timestamp,
editor: user.id,
});
return {
data: {
accepted: true,
timestamp,
},
};
}
@SubscribeMessage('space:load-doc-timestamps')
async onLoadDocTimestamps(
@ConnectedSocket() client: Socket,
@@ -600,9 +647,14 @@ abstract class SyncSocketAdapter {
return this.storage.pushDocUpdates(spaceId, docId, updates, editorId);
}
get(spaceId: string, docId: string) {
diff(spaceId: string, docId: string, stateVector?: Uint8Array) {
this.assertIn(spaceId);
return this.storage.getDoc(spaceId, docId);
return this.storage.getDocDiff(spaceId, docId, stateVector);
}
delete(spaceId: string, docId: string) {
this.assertIn(spaceId);
return this.storage.deleteDoc(spaceId, docId);
}
getTimestamps(spaceId: string, timestamp?: number) {
@@ -630,9 +682,9 @@ class WorkspaceSyncAdapter extends SyncSocketAdapter {
return super.push(spaceId, id.guid, updates, editorId);
}
override get(spaceId: string, docId: string) {
override diff(spaceId: string, docId: string, stateVector?: Uint8Array) {
const id = new DocID(docId, spaceId);
return this.storage.getDoc(spaceId, id.guid);
return this.storage.getDocDiff(spaceId, id.guid, stateVector);
}
async assertAccessible(