feat(server): make server storage adapters (#7902)

This commit is contained in:
forehalo
2024-08-21 05:30:26 +00:00
parent 6f9f579e5d
commit e20bdbf925
29 changed files with 1987 additions and 2111 deletions

View File

@@ -1,327 +0,0 @@
import { applyDecorators, Logger } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage as RawSubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
import {
CallTimer,
Config,
DocNotFound,
GatewayErrorWrapper,
metrics,
NotInWorkspace,
VersionRejected,
WorkspaceAccessDenied,
} from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocManager } from '../../doc';
import { Permission, PermissionService } from '../../permission';
import { DocID } from '../../utils/doc';
const SubscribeMessage = (event: string) =>
applyDecorators(
GatewayErrorWrapper(event),
CallTimer('socketio', 'event_duration', { event }),
RawSubscribeMessage(event)
);
type EventResponse<Data = any> = Data extends never
? {
data?: never;
}
: {
data: Data;
};
function Sync(workspaceId: string): `${string}:sync` {
return `${workspaceId}:sync`;
}
function Awareness(workspaceId: string): `${string}:awareness` {
return `${workspaceId}:awareness`;
}
@WebSocketGateway()
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
protected logger = new Logger(EventsGateway.name);
private connectionCount = 0;
constructor(
private readonly config: Config,
private readonly docManager: DocManager,
private readonly permissions: PermissionService
) {}
@WebSocketServer()
server!: Server;
handleConnection() {
this.connectionCount++;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
handleDisconnect() {
this.connectionCount--;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
async assertVersion(client: Socket, version?: string) {
const shouldCheckClientVersion = await this.config.runtime.fetch(
'flags/syncClientVersionCheck'
);
if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable
shouldCheckClientVersion &&
version !== AFFiNE.version
) {
client.emit('server-version-rejected', {
currentVersion: version,
requiredVersion: AFFiNE.version,
reason: `Client version${
version ? ` ${version}` : ''
} is outdated, please update to ${AFFiNE.version}`,
});
throw new VersionRejected({
version: version || 'unknown',
serverVersion: AFFiNE.version,
});
}
}
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 NotInWorkspace({ workspaceId: room.split(':')[0] });
}
}
async assertWorkspaceAccessible(
workspaceId: string,
userId: string,
permission: Permission = Permission.Read
) {
if (
!(await this.permissions.isWorkspaceMember(
workspaceId,
userId,
permission
))
) {
throw new WorkspaceAccessDenied({ workspaceId });
}
}
@Auth()
@SubscribeMessage('client-handshake-sync')
async handleClientHandshakeSync(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
await this.assertWorkspaceAccessible(
workspaceId,
user.id,
Permission.Write
);
await this.joinWorkspace(client, Sync(workspaceId));
return {
data: {
clientId: client.id,
},
};
}
@Auth()
@SubscribeMessage('client-handshake-awareness')
async handleClientHandshakeAwareness(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
await this.assertWorkspaceAccessible(
workspaceId,
user.id,
Permission.Write
);
await this.joinWorkspace(client, Awareness(workspaceId));
return {
data: {
clientId: client.id,
},
};
}
@SubscribeMessage('client-leave-sync')
async handleLeaveSync(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
this.assertInWorkspace(client, Sync(workspaceId));
await this.leaveWorkspace(client, Sync(workspaceId));
return {};
}
@SubscribeMessage('client-leave-awareness')
async handleLeaveAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
this.assertInWorkspace(client, Awareness(workspaceId));
await this.leaveWorkspace(client, Awareness(workspaceId));
return {};
}
@SubscribeMessage('client-pre-sync')
async loadDocStats(
@ConnectedSocket() client: Socket,
@MessageBody()
{ workspaceId, timestamp }: { workspaceId: string; timestamp?: number }
): Promise<EventResponse<Record<string, number>>> {
this.assertInWorkspace(client, Sync(workspaceId));
const stats = await this.docManager.getDocTimestamps(
workspaceId,
timestamp
);
return {
data: stats,
};
}
@SubscribeMessage('client-update-v2')
async handleClientUpdateV2(
@MessageBody()
{
workspaceId,
guid,
updates,
}: {
workspaceId: string;
guid: string;
updates: string[];
},
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
this.assertInWorkspace(client, Sync(workspaceId));
const docId = new DocID(guid, workspaceId);
const buffers = updates.map(update => Buffer.from(update, 'base64'));
const timestamp = await this.docManager.batchPush(
docId.workspace,
docId.guid,
buffers
);
client
.to(Sync(workspaceId))
.emit('server-updates', { workspaceId, guid, updates, timestamp });
return {
data: {
accepted: true,
timestamp,
},
};
}
@SubscribeMessage('doc-load-v2')
async loadDocV2(
@ConnectedSocket() client: Socket,
@MessageBody()
{
workspaceId,
guid,
stateVector,
}: {
workspaceId: string;
guid: string;
stateVector?: string;
}
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
this.assertInWorkspace(client, Sync(workspaceId));
const docId = new DocID(guid, workspaceId);
const res = await this.docManager.get(docId.workspace, docId.guid);
if (!res) {
throw new DocNotFound({ workspaceId, docId: docId.guid });
}
const missing = Buffer.from(
encodeStateAsUpdate(
res.doc,
stateVector ? Buffer.from(stateVector, 'base64') : undefined
)
).toString('base64');
const state = Buffer.from(encodeStateVector(res.doc)).toString('base64');
return {
data: {
missing,
state,
timestamp: res.timestamp,
},
};
}
@SubscribeMessage('awareness-init')
async handleInitAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
this.assertInWorkspace(client, Awareness(workspaceId));
client.to(Awareness(workspaceId)).emit('new-client-awareness-init');
return {
data: {
clientId: client.id,
},
};
}
@SubscribeMessage('awareness-update')
async handleHelpGatheringAwareness(
@MessageBody()
{
workspaceId,
awarenessUpdate,
}: { workspaceId: string; awarenessUpdate: string },
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
this.assertInWorkspace(client, Awareness(workspaceId));
client
.to(Awareness(workspaceId))
.emit('server-awareness-broadcast', { workspaceId, awarenessUpdate });
return {};
}
}

View File

@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { DocModule } from '../../doc';
import { PermissionModule } from '../../permission';
import { EventsGateway } from './events.gateway';
@Module({
imports: [DocModule, PermissionModule],
providers: [EventsGateway],
})
export class EventsModule {}

View File

@@ -0,0 +1,666 @@
import { applyDecorators, Logger } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage as RawSubscribeMessage,
WebSocketGateway,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs';
import {
CallTimer,
Config,
DocNotFound,
GatewayErrorWrapper,
metrics,
NotInSpace,
SpaceAccessDenied,
VersionRejected,
} from '../../fundamentals';
import { Auth, CurrentUser } from '../auth';
import {
DocStorageAdapter,
PgUserspaceDocStorageAdapter,
PgWorkspaceDocStorageAdapter,
} from '../doc';
import { Permission, PermissionService } from '../permission';
import { DocID } from '../utils/doc';
const SubscribeMessage = (event: string) =>
applyDecorators(
GatewayErrorWrapper(event),
CallTimer('socketio', 'event_duration', { event }),
RawSubscribeMessage(event)
);
type EventResponse<Data = any> = Data extends never
? {
data?: never;
}
: {
data: Data;
};
type RoomType = 'sync' | `${string}:awareness`;
function Room(
spaceId: string,
type: RoomType = 'sync'
): `${string}:${RoomType}` {
return `${spaceId}:${type}`;
}
enum SpaceType {
Workspace = 'workspace',
Userspace = 'userspace',
}
interface JoinSpaceMessage {
spaceType: SpaceType;
spaceId: string;
clientVersion: string;
}
interface JoinSpaceAwarenessMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
clientVersion: string;
}
interface LeaveSpaceMessage {
spaceType: SpaceType;
spaceId: string;
}
interface LeaveSpaceAwarenessMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
}
interface PushDocUpdatesMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
updates: string[];
}
interface LoadDocMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
stateVector?: string;
}
interface LoadDocTimestampsMessage {
spaceType: SpaceType;
spaceId: string;
timestamp?: number;
}
interface LoadSpaceAwarenessesMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
}
interface UpdateAwarenessMessage {
spaceType: SpaceType;
spaceId: string;
docId: string;
awarenessUpdate: string;
}
@WebSocketGateway()
export class SpaceSyncGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
protected logger = new Logger(SpaceSyncGateway.name);
private connectionCount = 0;
constructor(
private readonly config: Config,
private readonly permissions: PermissionService,
private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly userspace: PgUserspaceDocStorageAdapter
) {}
handleConnection() {
this.connectionCount++;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
handleDisconnect() {
this.connectionCount--;
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
selectAdapter(client: Socket, spaceType: SpaceType): SyncSocketAdapter {
let adapters: Record<SpaceType, SyncSocketAdapter> = (client as any)
.affineSyncAdapters;
if (!adapters) {
const workspace = new WorkspaceSyncAdapter(
client,
this.workspace,
this.permissions
);
const userspace = new UserspaceSyncAdapter(client, this.userspace);
adapters = { workspace, userspace };
(client as any).affineSyncAdapters = adapters;
}
return adapters[spaceType];
}
async assertVersion(client: Socket, version?: string) {
const shouldCheckClientVersion = await this.config.runtime.fetch(
'flags/syncClientVersionCheck'
);
if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable
shouldCheckClientVersion &&
version !== AFFiNE.version
) {
client.emit('server-version-rejected', {
currentVersion: version,
requiredVersion: AFFiNE.version,
reason: `Client version${
version ? ` ${version}` : ''
} is outdated, please update to ${AFFiNE.version}`,
});
throw new VersionRejected({
version: version || 'unknown',
serverVersion: AFFiNE.version,
});
}
}
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
@Auth()
@SubscribeMessage('space:join')
async onJoinSpace(
@CurrentUser() user: CurrentUser,
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, clientVersion }: JoinSpaceMessage
): Promise<EventResponse<{ clientId: string; success: true }>> {
await this.assertVersion(client, clientVersion);
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:leave')
async onLeaveSpace(
@ConnectedSocket() client: Socket,
@MessageBody() { spaceType, spaceId }: LeaveSpaceMessage
): Promise<EventResponse<{ clientId: string; success: true }>> {
await this.selectAdapter(client, spaceType).leave(spaceId);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:load-doc')
async onLoadSpaceDoc(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, docId, stateVector }: LoadDocMessage
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
const adapter = this.selectAdapter(client, spaceType);
adapter.assertIn(spaceId);
const doc = await adapter.get(spaceId, docId);
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,
timestamp: doc.timestamp,
},
};
}
@SubscribeMessage('space:push-doc-updates')
async onReceiveDocUpdates(
@ConnectedSocket() client: Socket,
@MessageBody()
message: PushDocUpdatesMessage
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
const { spaceType, spaceId, docId, updates } = 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,
updates.map(update => Buffer.from(update, 'base64'))
);
// could be put in [adapter.push]
// but the event should be kept away from adapter
// so
client
.to(adapter.room(spaceId))
.emit('space:broadcast-doc-updates', { ...message, timestamp });
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
const id = new DocID(docId, spaceId);
client.to(adapter.room(spaceId)).emit('server-updates', {
workspaceId: spaceId,
guid: id.guid,
updates,
timestamp,
});
}
return {
data: {
accepted: true,
timestamp,
},
};
}
@SubscribeMessage('space:load-doc-timestamps')
async onLoadDocTimestamps(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, timestamp }: LoadDocTimestampsMessage
): Promise<EventResponse<Record<string, number>>> {
const adapter = this.selectAdapter(client, spaceType);
const stats = await adapter.getTimestamps(spaceId, timestamp);
return {
data: stats ?? {},
};
}
@Auth()
@SubscribeMessage('space:join-awareness')
async onJoinAwareness(
@ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody()
{ spaceType, spaceId, docId, clientVersion }: JoinSpaceAwarenessMessage
) {
await this.assertVersion(client, clientVersion);
await this.selectAdapter(client, spaceType).join(
user.id,
spaceId,
`${docId}:awareness`
);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:leave-awareness')
async onLeaveAwareness(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, docId }: LeaveSpaceAwarenessMessage
) {
await this.selectAdapter(client, spaceType).leave(
spaceId,
`${docId}:awareness`
);
return { data: { clientId: client.id, success: true } };
}
@SubscribeMessage('space:load-awarenesses')
async onLoadAwareness(
@ConnectedSocket() client: Socket,
@MessageBody()
{ spaceType, spaceId, docId }: LoadSpaceAwarenessesMessage
) {
const adapter = this.selectAdapter(client, spaceType);
const roomType = `${docId}:awareness` as const;
adapter.assertIn(spaceId, roomType);
client
.to(adapter.room(spaceId, roomType))
.emit('space:collect-awareness', { spaceType, spaceId, docId });
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
client
.to(adapter.room(spaceId, roomType))
.emit('new-client-awareness-init');
}
return { data: { clientId: client.id } };
}
@SubscribeMessage('space:update-awareness')
async onUpdateAwareness(
@ConnectedSocket() client: Socket,
@MessageBody() message: UpdateAwarenessMessage
) {
const { spaceType, spaceId, docId } = message;
const adapter = this.selectAdapter(client, spaceType);
const roomType = `${docId}:awareness` as const;
adapter.assertIn(spaceId, roomType);
client
.to(adapter.room(spaceId, roomType))
.emit('space:broadcast-awareness-update', message);
// TODO(@forehalo): remove backward compatibility
if (spaceType === SpaceType.Workspace) {
client
.to(adapter.room(spaceId, roomType))
.emit('server-awareness-broadcast', {
workspaceId: spaceId,
awarenessUpdate: message.awarenessUpdate,
});
}
return {};
}
// TODO(@forehalo): remove
// deprecated section
@Auth()
@SubscribeMessage('client-handshake-sync')
async handleClientHandshakeSync(
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
await this.assertVersion(client, version);
return this.onJoinSpace(user, client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
clientVersion: version,
});
}
@SubscribeMessage('client-leave-sync')
async handleLeaveSync(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onLeaveSpace(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
});
}
@SubscribeMessage('client-pre-sync')
async loadDocStats(
@ConnectedSocket() client: Socket,
@MessageBody()
{ workspaceId, timestamp }: { workspaceId: string; timestamp?: number }
): Promise<EventResponse<Record<string, number>>> {
return this.onLoadDocTimestamps(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
timestamp,
});
}
@SubscribeMessage('client-update-v2')
async handleClientUpdateV2(
@MessageBody()
{
workspaceId,
guid,
updates,
}: {
workspaceId: string;
guid: string;
updates: string[];
},
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
return this.onReceiveDocUpdates(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: guid,
updates,
});
}
@SubscribeMessage('doc-load-v2')
async loadDocV2(
@ConnectedSocket() client: Socket,
@MessageBody()
{
workspaceId,
guid,
stateVector,
}: {
workspaceId: string;
guid: string;
stateVector?: string;
}
): Promise<
EventResponse<{ missing: string; state?: string; timestamp: number }>
> {
return this.onLoadSpaceDoc(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: guid,
stateVector,
});
}
@Auth()
@SubscribeMessage('client-handshake-awareness')
async handleClientHandshakeAwareness(
@ConnectedSocket() client: Socket,
@CurrentUser() user: CurrentUser,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string
): Promise<EventResponse<{ clientId: string }>> {
return this.onJoinAwareness(client, user, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
clientVersion: version,
});
}
@SubscribeMessage('client-leave-awareness')
async handleLeaveAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onLeaveAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
});
}
@SubscribeMessage('awareness-init')
async handleInitAwareness(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
return this.onLoadAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
});
}
@SubscribeMessage('awareness-update')
async handleHelpGatheringAwareness(
@MessageBody()
{
workspaceId,
awarenessUpdate,
}: { workspaceId: string; awarenessUpdate: string },
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
return this.onUpdateAwareness(client, {
spaceType: SpaceType.Workspace,
spaceId: workspaceId,
docId: workspaceId,
awarenessUpdate,
});
}
}
abstract class SyncSocketAdapter {
constructor(
private readonly spaceType: SpaceType,
public readonly client: Socket,
public readonly storage: DocStorageAdapter
) {}
room(spaceId: string, roomType: RoomType = 'sync') {
return `${this.spaceType}:${Room(spaceId, roomType)}`;
}
async join(userId: string, spaceId: string, roomType: RoomType = 'sync') {
this.assertNotIn(spaceId, roomType);
await this.assertAccessible(spaceId, userId, Permission.Read);
return this.client.join(this.room(spaceId, roomType));
}
async leave(spaceId: string, roomType: RoomType = 'sync') {
this.assertIn(spaceId, roomType);
return this.client.leave(this.room(spaceId, roomType));
}
in(spaceId: string, roomType: RoomType = 'sync') {
return this.client.rooms.has(this.room(spaceId, roomType));
}
assertNotIn(spaceId: string, roomType: RoomType = 'sync') {
if (this.client.rooms.has(this.room(spaceId, roomType))) {
// TODO(@forehalo): use new AlreadyInSpace({ spaceId }) instead
throw new NotInSpace({ spaceId });
}
}
assertIn(spaceId: string, roomType: RoomType = 'sync') {
if (!this.client.rooms.has(this.room(spaceId, roomType))) {
throw new NotInSpace({ spaceId });
}
}
abstract assertAccessible(
spaceId: string,
userId: string,
permission?: Permission
): Promise<void>;
push(spaceId: string, docId: string, updates: Buffer[]) {
this.assertIn(spaceId);
return this.storage.pushDocUpdates(spaceId, docId, updates);
}
get(spaceId: string, docId: string) {
this.assertIn(spaceId);
return this.storage.getDoc(spaceId, docId);
}
getTimestamps(spaceId: string, timestamp?: number) {
this.assertIn(spaceId);
return this.storage.getSpaceDocTimestamps(spaceId, timestamp);
}
}
class WorkspaceSyncAdapter extends SyncSocketAdapter {
constructor(
client: Socket,
storage: DocStorageAdapter,
private readonly permission: PermissionService
) {
super(SpaceType.Workspace, client, storage);
}
// backward compatibility
override room(spaceId: string, roomType: RoomType = 'sync') {
return Room(spaceId, roomType);
}
override push(spaceId: string, docId: string, updates: Buffer[]) {
const id = new DocID(docId, spaceId);
return super.push(spaceId, id.guid, updates);
}
override get(spaceId: string, docId: string) {
const id = new DocID(docId, spaceId);
return this.storage.getDoc(spaceId, id.guid);
}
async assertAccessible(
spaceId: string,
userId: string,
permission: Permission = Permission.Read
) {
if (
!(await this.permission.isWorkspaceMember(spaceId, userId, permission))
) {
throw new SpaceAccessDenied({ spaceId });
}
}
}
class UserspaceSyncAdapter extends SyncSocketAdapter {
constructor(client: Socket, storage: DocStorageAdapter) {
super(SpaceType.Userspace, client, storage);
}
async assertAccessible(
spaceId: string,
userId: string,
_permission: Permission = Permission.Read
) {
if (spaceId !== userId) {
throw new SpaceAccessDenied({ spaceId });
}
}
}

View File

@@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { EventsModule } from './events/events.module';
import { DocStorageModule } from '../doc';
import { PermissionModule } from '../permission';
import { SpaceSyncGateway } from './gateway';
@Module({
imports: [EventsModule],
imports: [DocStorageModule, PermissionModule],
providers: [SpaceSyncGateway],
})
export class SyncModule {}