diff --git a/code/client/src/domain/editor/hooks/useEvents.ts b/code/client/src/domain/editor/hooks/useEvents.ts index a093f93a..4c07fae1 100644 --- a/code/client/src/domain/editor/hooks/useEvents.ts +++ b/code/client/src/domain/editor/hooks/useEvents.ts @@ -16,7 +16,7 @@ function useEvents(fugueOperations: FugueDomainOperations, { socket }: Communica } useSocketListeners(socket, { - 'document:operation': onOperation, + operation: onOperation, }); } diff --git a/code/client/src/domain/editor/operations/history/operations.ts b/code/client/src/domain/editor/operations/history/operations.ts index 047fe32d..15701cc8 100644 --- a/code/client/src/domain/editor/operations/history/operations.ts +++ b/code/client/src/domain/editor/operations/history/operations.ts @@ -25,7 +25,7 @@ export default (fugue: Fugue, { socket }: Communication): HistoryDomainOperation .flat() .filter(operation => operation !== undefined && operation !== null); - socket.emit('document:operation', communicationOperations); + socket.emit('operation', communicationOperations); }; function getOperation(operation: HistoryOperation) { diff --git a/code/client/src/domain/editor/operations/input/operations.ts b/code/client/src/domain/editor/operations/input/operations.ts index cf736892..6a31bb65 100644 --- a/code/client/src/domain/editor/operations/input/operations.ts +++ b/code/client/src/domain/editor/operations/input/operations.ts @@ -11,31 +11,31 @@ export default (fugue: Fugue, { socket }: Communication): InputDomainOperations function insertCharacter(char: string, cursor: Cursor, styles: InlineStyle[] = []) { if (char.length !== 1) throw new Error('Invalid character'); const operations = fugue.insertLocal(cursor, nodeInsert(char, styles)); - socket.emit('document:operation', operations); + socket.emit('operation', operations); } function insertLineBreak(cursor: Cursor) { const operations = fugue.insertLocal(cursor, '\n'); const styleOperation = fugue.updateBlockStyleLocal(cursor.line + 1, 'paragraph', true); - socket.emit('document:operation', [styleOperation, ...operations]); + socket.emit('operation', [styleOperation, ...operations]); } function deleteCharacter(cursor: Cursor) { // don't delete line if it's not a paragraph - this is to prevent deleting the block style & line simultaneously if (cursor.column === 0 && fugue.getBlockStyle(cursor.line) !== 'paragraph') return; const operations = fugue.deleteLocalByCursor(cursor); - if (operations) socket.emit('document:operation', operations); + if (operations) socket.emit('operation', operations); } function deleteSelection(selection: Selection) { const operations = fugue.deleteLocal(selection); - socket.emit('document:operation', operations); + socket.emit('operation', operations); } function deleteWord(cursor: Cursor, reverse: boolean) { const operations = fugue.deleteWordByCursor(cursor, reverse); if (!operations) return; - socket.emit('document:operation', operations); + socket.emit('operation', operations); } function pasteText(start: Cursor, text: string) { @@ -43,11 +43,11 @@ export default (fugue: Fugue, { socket }: Communication): InputDomainOperations const lineNodes = chars.filter(char => char === '\n'); const insertOperations: Operation[] = fugue.insertLocal(start, ...text); const styleOperations = lineNodes.map(() => fugue.updateBlockStyleLocal(start.line + 1, 'paragraph', true)); - socket.emit('document:operation', [...styleOperations, ...insertOperations]); + socket.emit('operation', [...styleOperations, ...insertOperations]); } function updateSelection(range: BaseSelection, styles: InlineStyle[]) { - socket.emit('document:cursor', { range, styles }); + socket.emit('cursorChange', { range, styles }); } return { diff --git a/code/client/src/domain/editor/operations/markdown/operations.ts b/code/client/src/domain/editor/operations/markdown/operations.ts index 56f4d298..ab908754 100644 --- a/code/client/src/domain/editor/operations/markdown/operations.ts +++ b/code/client/src/domain/editor/operations/markdown/operations.ts @@ -37,7 +37,7 @@ export default (fugue: Fugue, { socket }: Communication): MarkdownDomainOperatio operations.push(styleOperation); // emit operations - socket.emit('document:operation', operations); + socket.emit('operation', operations); } /** @@ -64,7 +64,7 @@ export default (fugue: Fugue, { socket }: Communication): MarkdownDomainOperatio operations.push(...styleOperations); // emit operations - socket.emit('document:operation', operations); + socket.emit('operation', operations); } function deleteBlockStyles(selection: Selection) { @@ -75,7 +75,7 @@ export default (fugue: Fugue, { socket }: Communication): MarkdownDomainOperatio if ((start === end && start.column === 0) || start.line !== end.line) { const newSelection = start.column !== 0 ? { start: { line: start.line + 1, column: 0 }, end } : selection; const operations = fugue.updateBlockStylesLocalBySelection('paragraph', newSelection); - socket.emit('document:operation', operations); + socket.emit('operation', operations); } } diff --git a/code/client/src/domain/editor/slate/hooks/useCursors.ts b/code/client/src/domain/editor/slate/hooks/useCursors.ts index 60ab0fe3..19d8b1c8 100644 --- a/code/client/src/domain/editor/slate/hooks/useCursors.ts +++ b/code/client/src/domain/editor/slate/hooks/useCursors.ts @@ -23,7 +23,7 @@ export function useCursors({ socket }: Communication) { }; useSocketListeners(socket, { - 'document:cursor': onCursorChange, + cursorChange: onCursorChange, }); return { cursors }; diff --git a/code/client/src/domain/editor/slate/hooks/useRenderers.ts b/code/client/src/domain/editor/slate/hooks/useRenderers.ts index 244e6278..3f36c293 100644 --- a/code/client/src/domain/editor/slate/hooks/useRenderers.ts +++ b/code/client/src/domain/editor/slate/hooks/useRenderers.ts @@ -17,7 +17,7 @@ function useRenderers(editor: Editor, fugue: Fugue, { socket }: Communication) { const line = path[path.length - 1]; const updateBlockStyle = (style: BlockStyle) => { const operation = fugue.updateBlockStyleLocal(line, style); - socket.emit('document:operation', [operation]); + socket.emit('operation', [operation]); }; return getElementRenderer(type, props, updateBlockStyle); }, diff --git a/code/client/src/services/communication/socket/namespaces/documentNamespace.ts b/code/client/src/services/communication/socket/namespaces/documentNamespace.ts deleted file mode 100644 index 7967b529..00000000 --- a/code/client/src/services/communication/socket/namespaces/documentNamespace.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Socket } from 'socket.io-client'; -import { OperationEmitter } from '@/services/communication/socket/operationEmitter.ts'; -import { namespace } from '@/services/communication/socket/namespaces/utils.ts'; - -const socket = namespace('/document'); -const originalEmit = socket.emit; -const OPERATION_DELAY = 100; -const operationEmitter = new OperationEmitter(socket, OPERATION_DELAY); - -// TODO: -// this is wrong, fix later -socket.emit = function (event: string, ...data: any[]): Socket { - switch (event) { - case 'operation': - operationEmitter.addOperation(...data); - break; - case 'cursor': - setTimeout(() => originalEmit.call(socket, event, ...data), OPERATION_DELAY); - break; - default: - originalEmit.call(socket, event, ...data); - break; - } - return socket; -}; - -export default socket; diff --git a/code/client/src/services/communication/socket/namespaces/namespaces.ts b/code/client/src/services/communication/socket/namespaces/namespaces.ts deleted file mode 100644 index 7e22f437..00000000 --- a/code/client/src/services/communication/socket/namespaces/namespaces.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Socket } from 'socket.io-client'; -import documentNamespace from '@/services/communication/socket/namespaces/documentNamespace.ts'; -import { namespace } from '@/services/communication/socket/namespaces/utils.ts'; - -export default { - '/document': documentNamespace, - '/workspace': namespace('/workspace'), - '/user': namespace('/user'), -} as Record; diff --git a/code/client/src/services/communication/socket/namespaces/utils.ts b/code/client/src/services/communication/socket/namespaces/utils.ts deleted file mode 100644 index d4626b4a..00000000 --- a/code/client/src/services/communication/socket/namespaces/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { io } from 'socket.io-client'; -import config from '@/config.ts'; - -export function namespace(namespace: string) { - const OPTIONS = { autoConnect: true }; - return io(config.SOCKET_SERVER_URL + namespace, OPTIONS); -} diff --git a/code/client/src/services/communication/socket/socketCommunication.ts b/code/client/src/services/communication/socket/socketCommunication.ts index 4a2ed4bb..0b8f303b 100644 --- a/code/client/src/services/communication/socket/socketCommunication.ts +++ b/code/client/src/services/communication/socket/socketCommunication.ts @@ -1,14 +1,12 @@ -import namespaces from '@/services/communication/socket/namespaces/namespaces.ts'; import { io } from 'socket.io-client'; import config from '@/config.ts'; +import { OperationEmitter } from '@/services/communication/socket/operationEmitter.ts'; type EmitType = (event: string, data?: any) => void; type ListenType = (eventHandlers: SocketEventHandlers) => void; type ConnectionType = (namespace: string) => void; export type SocketEventHandlers = Record void>; -io(config.SOCKET_SERVER_URL); - export interface SocketCommunication { emit: EmitType; on: ListenType; @@ -17,43 +15,41 @@ export interface SocketCommunication { disconnect: ConnectionType; } -function emit(str: string, data: any) { - const [namespace, event] = str.split(':'); - if (!namespace || !event) throw new Error('Invalid event format'); - const socket = namespaces[`/${namespace}`]; - if (!socket) throw new Error('Invalid namespace'); - socket.emit(event, data); +const socket = io(config.SOCKET_SERVER_URL); +const OPERATION_DELAY = 100; +const operationEmitter = new OperationEmitter(socket, OPERATION_DELAY); + +function emit(event: string, data: any) { + switch (event) { + case 'operation': + operationEmitter.addOperation(...data); + break; + case 'cursor': + setTimeout(() => socket.emit(event, data), OPERATION_DELAY); + break; + default: + socket.emit(event, ...data); + break; + } } function on(eventHandlers: SocketEventHandlers) { - Object.entries(eventHandlers).forEach(([str, handler]) => { - const [namespace, event] = str.split(':'); - if (!namespace || !event) throw new Error('Invalid event format'); - const socket = namespaces[`/${namespace}`]; - if (!socket) throw new Error('Invalid namespace:' + namespace); + Object.entries(eventHandlers).forEach(([event, handler]) => { socket.on(event, handler); }); } function off(eventHandlers: SocketEventHandlers) { - Object.entries(eventHandlers).forEach(([str, handler]) => { - const [namespace, event] = str.split(':'); - if (!namespace || !event) throw new Error('Invalid event format'); - const socket = namespaces[`/${namespace}`]; - if (!socket) throw new Error('Invalid namespace:' + namespace); + Object.entries(eventHandlers).forEach(([event, handler]) => { socket.off(event, handler); }); } -function connect(namespace: string) { - const socket = namespaces[namespace]; - if (!socket) throw new Error('Invalid namespace:' + namespace); +function connect() { socket.connect(); } -function disconnect(namespace: string) { - const socket = namespaces[namespace]; - if (!socket) throw new Error('Invalid namespace:' + namespace); +function disconnect() { socket.disconnect(); } diff --git a/code/client/src/ui/pages/document/Document.tsx b/code/client/src/ui/pages/document/Document.tsx index bad01094..b949efe8 100644 --- a/code/client/src/ui/pages/document/Document.tsx +++ b/code/client/src/ui/pages/document/Document.tsx @@ -35,14 +35,14 @@ function Document() { setTitle(name); setLoaded(true); setFilePath(`/documents/${title || 'Untitled'}`); - socket.emit('document:join', id); + socket.emit('joinDocument', id); } fetchDocument().catch(e => { publishError(e); navigate('/'); }); return () => { - socket.emit('document:leave'); + socket.emit('leaveDocument'); }; }, [fugue, id, http, socket, publishError, services, setFilePath, navigate, title]); diff --git a/code/client/src/ui/pages/document/components/title/Title.tsx b/code/client/src/ui/pages/document/components/title/Title.tsx index b4311dee..58cd34af 100644 --- a/code/client/src/ui/pages/document/components/title/Title.tsx +++ b/code/client/src/ui/pages/document/components/title/Title.tsx @@ -40,7 +40,7 @@ function Title(props: TitleProps) { } useSocketListeners(socket, { - 'document:title': setTitle, + documentUpdated: setTitle, // TODO: fix later }); return ( diff --git a/code/client/src/ui/pages/workspace/hooks/useDocuments.ts b/code/client/src/ui/pages/workspace/hooks/useDocuments.ts index 63437b64..b17e6c51 100644 --- a/code/client/src/ui/pages/workspace/hooks/useDocuments.ts +++ b/code/client/src/ui/pages/workspace/hooks/useDocuments.ts @@ -41,9 +41,9 @@ export function useDocuments() { } useSocketListeners(socket, { - 'document:created': onCreateDocument, - 'document:deleted': onDeleteDocument, - 'document:updated': onUpdateDocument, + documentCreated: onCreateDocument, + documentDeleted: onDeleteDocument, + documentUpdated: onUpdateDocument, }); return { diff --git a/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts b/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts index 906abdcb..d61c97dd 100644 --- a/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts +++ b/code/client/src/ui/pages/workspace/hooks/useWorkspaces.ts @@ -45,9 +45,9 @@ function useWorkspaces() { } useSocketListeners(socket, { - 'workspace:created': onCreateWorkspace, - 'workspace:deleted': onDeleteWorkspace, - 'workspace:updated': onUpdateWorkspace, + workspaceCreated: onCreateWorkspace, + workspaceDeleted: onDeleteWorkspace, + workspaceUpdated: onUpdateWorkspace, }); return { diff --git a/code/server/src/ts/config.ts b/code/server/src/ts/config.ts index 83784bdf..98f17612 100644 --- a/code/server/src/ts/config.ts +++ b/code/server/src/ts/config.ts @@ -6,8 +6,8 @@ config(); const SERVER_PORT = parseInt(process.env.PORT || '8080'); const CLIENT_PORT = parseInt(process.env.CLIENT_PORT || '5173'); const HOST_IP = process.env.HOST_IP; -const ORIGIN = [`http://localhost:${CLIENT_PORT}`]; -if (HOST_IP) ORIGIN.push(`http://${HOST_IP}:${CLIENT_PORT}`); +const ORIGIN = [`http://localhost:${CLIENT_PORT}`, 'http://localhost:8080']; +if (HOST_IP) ORIGIN.push(`http://${HOST_IP}:${CLIENT_PORT}`, `http://${HOST_IP}:8080`); const SERVER_IP = HOST_IP || 'localhost'; const SERVER_OPTIONS = { diff --git a/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts b/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts index b96a632d..fa09b0ea 100644 --- a/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts +++ b/code/server/src/ts/controllers/http/workspace/resourcesHandlers.ts @@ -19,9 +19,9 @@ function resourcesHandlers(service: ResourcesService, io: Server) { const { type, name, parent } = resource; if (!wid) throw new InvalidParameterError('Workspace id is required'); if (!type) throw new InvalidParameterError('Resource type is required'); - if (!parent) throw new InvalidParameterError('Resource parent is required'); + // if (!parent) throw new InvalidParameterError('Resource parent is required'); const id = await service.createResource(wid, name, type, parent); - io.of('/workspaces').in(wid).emit('resources:create', { id, name, type, parent }); + io.in(wid).emit('resourceCreated', { id, name, type, parent }); httpResponse.created(res).json({ id }); }; @@ -50,7 +50,7 @@ function resourcesHandlers(service: ResourcesService, io: Server) { const resource = req.body as Partial; if (!resource) throw new InvalidParameterError('Body is required'); await service.updateResource(id, resource); - io.of('/workspaces').in(wid).emit('resources:update', resource); + io.in(wid).emit('resourceUpdated', resource); httpResponse.noContent(res).send(); }; @@ -64,11 +64,11 @@ function resourcesHandlers(service: ResourcesService, io: Server) { if (!wid) throw new InvalidParameterError('Workspace id is required'); if (!id) throw new InvalidParameterError('Resource id is required'); await service.deleteResource(id); - io.of('/workspaces').in(wid).emit('resources:delete', { id }); + io.in(wid).emit('resourceDeleted', { id }); httpResponse.noContent(res).send(); }; - const router = PromiseRouter({mergeParams: true}); + const router = PromiseRouter({ mergeParams: true }); router.post('/', createResource); router.get('/:id', getResource); router.put('/:id', updateResource); diff --git a/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts b/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts index ca041d34..2ea3c3f4 100644 --- a/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts +++ b/code/server/src/ts/controllers/http/workspace/workspaceHandlers.ts @@ -51,7 +51,7 @@ function workspaceHandlers(services: Services, io: Server) { if (!id) throw new InvalidParameterError('Workspace id is required'); if (!name) throw new InvalidParameterError('Workspace name is required'); await services.workspace.updateWorkspace(id, name); - io.of('/workspaces').in(id).emit('workspaces:update', { id, name }); + io.in(id).emit('updatedWorkspace', { id, name }); httpResponse.noContent(res).send(); }; @@ -64,7 +64,7 @@ function workspaceHandlers(services: Services, io: Server) { const { wid } = req.params; if (!wid) throw new InvalidParameterError('Workspace id is required'); await services.workspace.deleteWorkspace(wid); - io.of('/workspaces').in(wid).emit('workspaces:delete', { id: wid }); + io.in(wid).emit('workspaceDeleted', { id: wid }); httpResponse.noContent(res).send(); }; diff --git a/code/server/src/ts/controllers/ws/events.ts b/code/server/src/ts/controllers/ws/events.ts index bb629eb4..0275b691 100644 --- a/code/server/src/ts/controllers/ws/events.ts +++ b/code/server/src/ts/controllers/ws/events.ts @@ -1,26 +1,24 @@ import { SocketHandler } from '@controllers/ws/types'; import { DocumentsService } from '@services/DocumentsService'; -import onOperation from '@controllers/ws/namespaces/document/onOperation'; -import onCursorChange from '@controllers/ws/namespaces/document/onCursorChange'; -import onJoinDocument from '@controllers/ws/namespaces/document/onJoinDocument'; -import onLeaveDocument from '@controllers/ws/namespaces/document/onLeaveDocument'; -import onJoinWorkspace from '@controllers/ws/namespaces/workspace/onJoinWorkspace'; -import onLeaveWorkspace from '@controllers/ws/namespaces/workspace/onLeaveWorkspace'; +import onOperation from '@controllers/ws/events/document/onOperation'; +import onCursorChange from '@controllers/ws/events/document/onCursorChange'; +import onJoinDocument from '@controllers/ws/events/document/onJoinDocument'; +import onLeaveDocument from '@controllers/ws/events/document/onLeaveDocument'; +import onJoinWorkspace from '@controllers/ws/events/workspace/onJoinWorkspace'; +import onLeaveWorkspace from '@controllers/ws/events/workspace/onLeaveWorkspace'; -export default function events(service: DocumentsService): Record> { +export default function events(service: DocumentsService): Record { if (!service) throw new Error('Service parameter is required'); return { - '/document': { - operation: onOperation(service), - cursor: onCursorChange(), - join: onJoinDocument(), - leave: onLeaveDocument(), - }, - '/workspaces': { - join: onJoinWorkspace(), - leave: onLeaveWorkspace(), - }, - '/users': {}, + // document events + operation: onOperation(service), + cursorChange: onCursorChange(), + joinDocument: onJoinDocument(), + leaveDocument: onLeaveDocument(), + + // workspace events + joinWorkspace: onJoinWorkspace(), + leaveWorkspace: onLeaveWorkspace(), }; } diff --git a/code/server/src/ts/controllers/ws/namespaces/document/onCursorChange.ts b/code/server/src/ts/controllers/ws/events/document/onCursorChange.ts similarity index 100% rename from code/server/src/ts/controllers/ws/namespaces/document/onCursorChange.ts rename to code/server/src/ts/controllers/ws/events/document/onCursorChange.ts diff --git a/code/server/src/ts/controllers/ws/namespaces/document/onJoinDocument.ts b/code/server/src/ts/controllers/ws/events/document/onJoinDocument.ts similarity index 100% rename from code/server/src/ts/controllers/ws/namespaces/document/onJoinDocument.ts rename to code/server/src/ts/controllers/ws/events/document/onJoinDocument.ts diff --git a/code/server/src/ts/controllers/ws/namespaces/document/onLeaveDocument.ts b/code/server/src/ts/controllers/ws/events/document/onLeaveDocument.ts similarity index 100% rename from code/server/src/ts/controllers/ws/namespaces/document/onLeaveDocument.ts rename to code/server/src/ts/controllers/ws/events/document/onLeaveDocument.ts diff --git a/code/server/src/ts/controllers/ws/namespaces/document/onOperation.ts b/code/server/src/ts/controllers/ws/events/document/onOperation.ts similarity index 78% rename from code/server/src/ts/controllers/ws/namespaces/document/onOperation.ts rename to code/server/src/ts/controllers/ws/events/document/onOperation.ts index 938b506f..b82aa6cd 100644 --- a/code/server/src/ts/controllers/ws/namespaces/document/onOperation.ts +++ b/code/server/src/ts/controllers/ws/events/document/onOperation.ts @@ -9,11 +9,12 @@ function onOperation(service: DocumentsService) { return async (socket: Socket, operations: Operation[]) => { if (!operations) throw new InvalidParameterError('Operations are required'); + const workspaceId = rooms.workspace.get(socket)?.id; const documentId = rooms.document.get(socket)?.id; - if (!documentId) throw new ForbiddenError('Client socket not in a room'); + if (!documentId) throw new ForbiddenError('Client not in a room'); socket.broadcast.to(documentId).emit('operation', operations); - await service.updateDocument('documents', documentId, operations); + await service.updateDocument(workspaceId, documentId, operations); socket.emit('ack'); }; } diff --git a/code/server/src/ts/controllers/ws/namespaces/workspace/onJoinWorkspace.ts b/code/server/src/ts/controllers/ws/events/workspace/onJoinWorkspace.ts similarity index 100% rename from code/server/src/ts/controllers/ws/namespaces/workspace/onJoinWorkspace.ts rename to code/server/src/ts/controllers/ws/events/workspace/onJoinWorkspace.ts diff --git a/code/server/src/ts/controllers/ws/namespaces/workspace/onLeaveWorkspace.ts b/code/server/src/ts/controllers/ws/events/workspace/onLeaveWorkspace.ts similarity index 100% rename from code/server/src/ts/controllers/ws/namespaces/workspace/onLeaveWorkspace.ts rename to code/server/src/ts/controllers/ws/events/workspace/onLeaveWorkspace.ts diff --git a/code/server/src/ts/controllers/ws/initSocketEvents.ts b/code/server/src/ts/controllers/ws/initSocketEvents.ts new file mode 100644 index 00000000..8ce77f9a --- /dev/null +++ b/code/server/src/ts/controllers/ws/initSocketEvents.ts @@ -0,0 +1,25 @@ +import { SocketHandler } from '@controllers/ws/types'; +import { Socket } from 'socket.io'; + +export default function initSocketEvents(events: Record) { + // const onCursorChange = events['cursorChange']; + return async (socket: Socket) => { + console.log('a client connected'); + + Object.entries(events).forEach(([event, handler]) => { + socket.on(event, async data => { + try { + console.log(event); + await handler(socket, data); + } catch (e) { + console.error(e); + } + }); + }); + + socket.on('disconnect', reason => { + // onCursorChange(socket, null); // remove cursor + console.log('a client disconnected', reason); + }); + }; +} diff --git a/code/server/src/ts/controllers/ws/rooms/rooms.ts b/code/server/src/ts/controllers/ws/rooms/rooms.ts index c4a0cdcc..c9ada4a7 100644 --- a/code/server/src/ts/controllers/ws/rooms/rooms.ts +++ b/code/server/src/ts/controllers/ws/rooms/rooms.ts @@ -1,7 +1,7 @@ import { Socket } from 'socket.io'; import Room from '@controllers/ws/rooms/Room'; import { getRoom, joinRoom, leaveRoom } from '@controllers/ws/rooms/roomOperations'; -import { InvalidParameterError } from '@domain/errors/errors'; +import { ForbiddenError } from '@domain/errors/errors'; type Rooms = Map; const workspaceRooms: Rooms = new Map(); // (documentId) => Room @@ -17,10 +17,10 @@ function leaveDocument(socket: Socket) { function getDocument(socket: Socket) { const room = getRoom(documentRooms, socket); - if (!room) throw new InvalidParameterError('User not in document'); - const workspaceInfo = getWorkspace(socket); - if (!workspaceInfo) throw new InvalidParameterError('User not in workspace'); - return { id: room.id, workspaceId: workspaceInfo.id }; + if (!room) throw new ForbiddenError('Client not in a document'); + const workspace = getWorkspace(socket); + if (!workspace) throw new ForbiddenError('Client not in a workspace'); + return { id: room.id, wid: workspace.id }; } function getDocumentRoom(id: string) { @@ -37,7 +37,7 @@ function leaveWorkspace(socket: Socket) { function getWorkspace(socket: Socket) { const room = getRoom(workspaceRooms, socket); - if (!room) throw new InvalidParameterError('User not in workspace'); + if (!room) throw new ForbiddenError('Client not in a workspace'); return { id: room.id }; } diff --git a/code/server/src/ts/controllers/ws/setupEventHandlers.ts b/code/server/src/ts/controllers/ws/setupEventHandlers.ts deleted file mode 100644 index 25ee353c..00000000 --- a/code/server/src/ts/controllers/ws/setupEventHandlers.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SocketNamespaces } from '@controllers/ws/types'; -import { Server } from 'socket.io'; - -export function setupEventHandlers(io: Server, events: SocketNamespaces) { - Object.entries(events).forEach(([namespace, event]) => { - Object.entries(event).forEach(([name, handler]) => { - io.of(namespace).on('connection', socket => { - socket.on(name, async data => { - try { - console.log(name); - await handler(socket, data); - } catch (e) { - console.error(e); - } - }); - if (namespace === '/document') { - socket.on('disconnect', reason => { - // const { cursor } = events['/document']; - // cursor(socket, null); // remove cursor - console.log('disconnected from document', reason); - }); - } - }); - }); - }); -} diff --git a/code/server/src/ts/databases/ProductionDatabases.ts b/code/server/src/ts/databases/ProductionDatabases.ts new file mode 100644 index 00000000..967a3793 --- /dev/null +++ b/code/server/src/ts/databases/ProductionDatabases.ts @@ -0,0 +1,16 @@ +import { PostgresResourcesDB } from '@databases/resources/postgres/PostgresResourcesDB'; +import { PostgresWorkspacesDB } from '@databases/resources/postgres/PostgresWorkspacesDB'; +import { DocumentsRepository, Databases, ResourcesRepository, WorkspacesRepository } from '@databases/types'; +import { FirestoreDocumentsDB } from '@databases/documents/firestore/FirestoreDocumentsDB'; + +export class ProductionDatabases implements Databases { + readonly document: DocumentsRepository; + readonly resource: ResourcesRepository; + readonly workspace: WorkspacesRepository; + + constructor() { + this.document = new FirestoreDocumentsDB(); + this.resource = new PostgresResourcesDB(); + this.workspace = new PostgresWorkspacesDB(); + } +} diff --git a/code/server/src/ts/databases/TestDatabases.ts b/code/server/src/ts/databases/TestDatabases.ts new file mode 100644 index 00000000..496b33a5 --- /dev/null +++ b/code/server/src/ts/databases/TestDatabases.ts @@ -0,0 +1,16 @@ +import { DocumentsRepository, Databases, ResourcesRepository, WorkspacesRepository } from '@databases/types'; +import { MemoryResourcesDB } from '@databases/resources/memory/MemoryResourcesDB'; +import { MemoryWorkspacesDB } from '@databases/resources/memory/MemoryWorkspacesDB'; +import { MemoryDocumentsDB } from '@databases/documents/memory/MemoryDocumentsDB'; + +export class TestDatabases implements Databases { + readonly document: DocumentsRepository; + readonly resource: ResourcesRepository; + readonly workspace: WorkspacesRepository; + + constructor() { + this.document = new MemoryDocumentsDB(); + this.resource = new MemoryResourcesDB(); + this.workspace = new MemoryWorkspacesDB(); + } +} diff --git a/code/server/src/ts/databases/documents/firestore/FirestoreDocumentsDB.ts b/code/server/src/ts/databases/documents/firestore/FirestoreDocumentsDB.ts index 45def89d..a52ab1e4 100644 --- a/code/server/src/ts/databases/documents/firestore/FirestoreDocumentsDB.ts +++ b/code/server/src/ts/databases/documents/firestore/FirestoreDocumentsDB.ts @@ -3,11 +3,12 @@ import { NotFoundError } from '@domain/errors/errors'; import { Operation } from '@notespace/shared/src/document/types/operations'; import { firestore } from 'firebase-admin'; import FieldValue = firestore.FieldValue; -import { DocumentRepository } from '@database/types'; +import { DocumentsRepository } from '@databases/types'; import CollectionReference = firestore.CollectionReference; -import db from '@database/documents/firestore/config'; +import db from '@databases/documents/firestore/config'; +import DocumentSnapshot = firestore.DocumentSnapshot; -export class FirestoreDocumentsDB implements DocumentRepository { +export class FirestoreDocumentsDB implements DocumentsRepository { async createDocument(wid: string, id: string) { const documents = await this.getWorkspace(wid); const docData: DocumentContent = { operations: [] }; @@ -35,11 +36,34 @@ export class FirestoreDocumentsDB implements DocumentRepository { return db.collection(id); } - private async getDoc(wid: string, id: string) { + private async getDoc(wid: string, id: string): Promise { const documents = await this.getWorkspace(wid); const query = documents.where('id', '==', id); const data = await query.get(); if (data.empty) throw new NotFoundError(`Document with id ${id} not found`); return data.docs[0].ref; } + + /** + * Add a workspace (collection) in firestore + * By adding a document with the id 'init' to the workspace collection, the workspace is created + * @param wid + */ + async addWorkspace(wid: string) { + const workspace = db.collection(wid); + await workspace.doc('init').set({ initialized: true }); + } + + /** + * Remove a workspace (collection) in firestore + * By deleting all the documents in the workspace collection, the workspace is removed + * @param wid + */ + async removeWorkspace(wid: string) { + const workspace = db.collection(wid); + const snapshot = await workspace.get(); + const batch = db.batch(); + snapshot.docs.forEach((doc: DocumentSnapshot) => batch.delete(doc.ref)); + await batch.commit(); + } } diff --git a/code/server/src/ts/databases/documents/firestore/config.ts b/code/server/src/ts/databases/documents/firestore/config.ts index fbdccec2..3fbade44 100644 --- a/code/server/src/ts/databases/documents/firestore/config.ts +++ b/code/server/src/ts/databases/documents/firestore/config.ts @@ -1,5 +1,5 @@ import { cert, initializeApp, ServiceAccount } from 'firebase-admin/app'; -import serviceAccount from './firestore-key-5cddf-472039f8dbb6.json'; +import serviceAccount from '@/firestore-key-5cddf-472039f8dbb6.json'; import { getFirestore } from 'firebase-admin/firestore'; initializeApp({ diff --git a/code/server/src/ts/databases/documents/MemoryDocumentsDB.ts b/code/server/src/ts/databases/documents/memory/MemoryDocumentsDB.ts similarity index 69% rename from code/server/src/ts/databases/documents/MemoryDocumentsDB.ts rename to code/server/src/ts/databases/documents/memory/MemoryDocumentsDB.ts index 16094681..5abc994d 100644 --- a/code/server/src/ts/databases/documents/MemoryDocumentsDB.ts +++ b/code/server/src/ts/databases/documents/memory/MemoryDocumentsDB.ts @@ -4,10 +4,10 @@ import { DocumentsRepository } from '@databases/types'; import { DocumentContent } from '@notespace/shared/src/workspace/types/document'; export class MemoryDocumentsDB implements DocumentsRepository { - private readonly documents: Record> = {}; + private readonly workspaces: Record> = {}; async createDocument(wid: string, id: string) { - this.documents[wid][id] = { operations: [] }; + this.workspaces[wid][id] = { operations: [] }; return id; } @@ -17,19 +17,27 @@ export class MemoryDocumentsDB implements DocumentsRepository { async deleteDocument(wid: string, id: string) { this.getDoc(wid, id); - delete this.documents[id]; + delete this.workspaces[id]; } async updateDocument(wid: string, id: string, operations: Operation[]) { const document = this.getDoc(wid, id); - this.documents[wid][id].operations = [...document.operations, ...operations]; + this.workspaces[wid][id].operations = [...document.operations, ...operations]; } private getDoc(wid: string, id: string) { - const workspace = this.documents[wid]; + const workspace = this.workspaces[wid]; if (!workspace) throw new NotFoundError(`Workspace with id ${wid} not found`); const document = workspace[id]; if (!document) throw new NotFoundError(`Document with id ${id} not found`); return document; } + + async addWorkspace(wid: string) { + this.workspaces[wid] = {}; + } + + async removeWorkspace(wid: string) { + delete this.workspaces[wid]; + } } diff --git a/code/server/src/ts/databases/resources/memory/Memory.ts b/code/server/src/ts/databases/resources/memory/Memory.ts new file mode 100644 index 00000000..15499831 --- /dev/null +++ b/code/server/src/ts/databases/resources/memory/Memory.ts @@ -0,0 +1,88 @@ +import { ResourceType, WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; +import { v4 as uuid } from 'uuid'; + +type Workspace = { + id: string; + name: string; + resources: WorkspaceResource[]; +}; + +const workspaces = new Map(); + +export function createWorkspace(name: string): string { + const id = uuid(); + workspaces.set(id, { id, name, resources: [] }); + return id; +} + +export function getWorkspaces(): Workspace[] { + return Array.from(workspaces.values()); +} + +export function getWorkspace(id: string): Workspace { + const workspace = workspaces.get(id); + if (!workspace) throw new Error(`Workspace with id ${id} not found`); + return workspace; +} + +export function updateWorkspace(id: string, name: string) { + const workspace = getWorkspace(id); + workspaces.set(id, { ...workspace, name }); +} + +export function deleteWorkspace(id: string) { + if (!workspaces.delete(id)) throw new Error(`Workspace with id ${id} not found`); +} + +export function getResource(id: string): WorkspaceResource { + const workspace = getResourceWorkspace(id); + const resource = workspace.resources.find(r => r.id === id); + if (!resource) throw new Error(`Resource with id ${id} not found`); + return resource; +} + +export function createResource(wid: string, name: string, type: ResourceType, parent?: string): string { + const workspace = getWorkspace(wid); + const id = uuid(); + const resource: WorkspaceResource = { + id, + name, + workspace: wid, + type, + parent: parent || wid, + children: [], + }; + workspace.resources.push(resource); + return id; +} + +export function updateResource(id: string, newProps: Partial) { + const resource = getResource(id); + Object.assign(resource, newProps); +} + +export function deleteResource(id: string) { + const workspace = getResourceWorkspace(id); + workspace.resources = workspace.resources.filter(r => r.id !== id); +} + +function getResourceWorkspace(resourceId: string): Workspace { + for (const workspace of workspaces.values()) { + if (workspace.resources.some(r => r.id === resourceId)) { + return workspace; + } + } + throw new Error(`Resource with id ${resourceId} not found`); +} + +export const memoryDB = { + createWorkspace, + getWorkspaces, + getWorkspace, + updateWorkspace, + deleteWorkspace, + getResource, + createResource, + updateResource, + deleteResource, +}; diff --git a/code/server/src/ts/databases/resources/memory/MemoryDB.ts b/code/server/src/ts/databases/resources/memory/MemoryDB.ts deleted file mode 100644 index bb94ceba..00000000 --- a/code/server/src/ts/databases/resources/memory/MemoryDB.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DocumentsRepository, NoteSpaceDatabase, ResourcesRepository, WorkspacesRepository } from '@databases/types'; -import { MemoryResourcesDB } from '@databases/resources/memory/MemoryResourcesDB'; -import { MemoryWorkspacesDB } from '@databases/resources/memory/MemoryWorkspacesDB'; - -export class MemoryDB implements NoteSpaceDatabase { - readonly document: DocumentsRepository; - readonly resource: ResourcesRepository; - readonly workspace: WorkspacesRepository; - - constructor(document: DocumentsRepository) { - this.document = document; - this.resource = new MemoryResourcesDB(); - this.workspace = new MemoryWorkspacesDB(); - } -} \ No newline at end of file diff --git a/code/server/src/ts/databases/resources/memory/MemoryResourcesDB.ts b/code/server/src/ts/databases/resources/memory/MemoryResourcesDB.ts index 37c9c3eb..7c89210f 100644 --- a/code/server/src/ts/databases/resources/memory/MemoryResourcesDB.ts +++ b/code/server/src/ts/databases/resources/memory/MemoryResourcesDB.ts @@ -1,18 +1,18 @@ import { ResourcesRepository } from '@databases/types'; import { ResourceType, WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; -import {memoryDB} from '@databases/resources/memory/utils'; +import { memoryDB } from '@databases/resources/memory/Memory'; export class MemoryResourcesDB implements ResourcesRepository { - createResource(wid: string, name: string, type: ResourceType, parent?: string | undefined) : Promise { - return Promise.resolve(memoryDB.addWorkspaceResource(wid, name)); - } - getResource(id: string): Promise { - return Promise.resolve(memoryDB.getWorkspaceResource(id)); - } - updateResource(id: string, newProps: Partial): Promise { - return Promise.resolve(memoryDB.updateWorkspaceResource(id, newProps)); - } - deleteResource(id: string): Promise { - return Promise.resolve(memoryDB.removeWorkspaceResource(id)); - } -} \ No newline at end of file + async createResource(wid: string, name: string, type: ResourceType, parent?: string): Promise { + return memoryDB.createResource(wid, name, type, parent); + } + async getResource(id: string): Promise { + return memoryDB.getResource(id); + } + async updateResource(id: string, newProps: Partial): Promise { + return memoryDB.updateResource(id, newProps); + } + async deleteResource(id: string): Promise { + return memoryDB.deleteResource(id); + } +} diff --git a/code/server/src/ts/databases/resources/memory/MemoryWorkspacesDB.ts b/code/server/src/ts/databases/resources/memory/MemoryWorkspacesDB.ts index 65f00970..519515e3 100644 --- a/code/server/src/ts/databases/resources/memory/MemoryWorkspacesDB.ts +++ b/code/server/src/ts/databases/resources/memory/MemoryWorkspacesDB.ts @@ -1,26 +1,25 @@ import { WorkspacesRepository } from '@databases/types'; import { WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; -import { memoryDB } from '@databases/resources/memory/utils'; +import { memoryDB } from '@databases/resources/memory/Memory'; import { WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; - export class MemoryWorkspacesDB implements WorkspacesRepository { - getWorkspaceResources(id: string):Promise{ - return memoryDB.getWorkspaceResources(id); - } - createWorkspace(name: string): Promise { - return memoryDB.createWorkspace(name); - } - getWorkspaces(): Promise { - return memoryDB.getWorkspaces(); - } - getWorkspace(id: string): Promise { - return memoryDB.getWorkspace(id); - } - updateWorkspace(id: string, name: string): Promise { - return memoryDB.updateWorkspace(id, name); - } - deleteWorkspace(id: string): Promise { - return memoryDB.deleteWorkspace(id); - } -} \ No newline at end of file + async createWorkspace(name: string): Promise { + return memoryDB.createWorkspace(name); + } + async getWorkspaces(): Promise { + return memoryDB.getWorkspaces(); + } + async getWorkspace(id: string): Promise { + return memoryDB.getWorkspace(id); + } + async getWorkspaceResources(id: string): Promise { + return memoryDB.getWorkspace(id).resources; + } + async updateWorkspace(id: string, name: string): Promise { + return memoryDB.updateWorkspace(id, name); + } + async deleteWorkspace(id: string): Promise { + return memoryDB.deleteWorkspace(id); + } +} diff --git a/code/server/src/ts/databases/resources/memory/utils.ts b/code/server/src/ts/databases/resources/memory/utils.ts deleted file mode 100644 index 37f3add3..00000000 --- a/code/server/src/ts/databases/resources/memory/utils.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { ResourceType, WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; - -type Workspace = { - id: string; - name: string; - resources: string[]; -} - -const workspaces = new Map(); - -export const createWorkspace = async (name: string): Promise => { - const id = Math.random().toString(36).substring(7); - workspaces.set(id, { id, name, resources: [] }); - return id; -} - -export const getWorkspaces = async (): Promise => { - return Array.from(workspaces.values()); -} - -export const getWorkspace = async (id: string): Promise => { - const workspace = workspaces.get(id); - if (!workspace) throw new Error(`Workspace with id ${id} not found`); - return workspace; -} - -export const updateWorkspace = async (id: string, name: string): Promise => { - const workspace = workspaces.get(id); - if (!workspace) throw new Error(`Workspace with id ${id} not found`); - workspaces.set(id, { ...workspace, name }); -} - -export const deleteWorkspace = async (id: string): Promise => { - if (!workspaces.delete(id)) throw new Error(`Workspace with id ${id} not found`); -} - -export const getWorkspaceResources = async (id: string): Promise => { - const workspace = workspaces.get(id); - if (!workspace) throw new Error(`Workspace with id ${id} not found`); - return workspace.resources.map(id => ({ id, - name: 'Resource', - workspace: id, - type: ResourceType.DOCUMENT, - parent: id, - children: [], - })); -} - -export const getWorkspaceResource = async (resourceId: string): Promise => { - const workspace = Array.from(workspaces.values()).find(w => w.resources.includes(resourceId)); - if (!workspace) throw new Error(`Resource with id ${resourceId} not found`); - return { - id: resourceId, - name: 'Resource', - workspace: workspace.id, - type: ResourceType.DOCUMENT, - parent: workspace.id, - children: [], - }; -} - -export const addWorkspaceResource = async (wid: string, resourceId: string): Promise => { - const workspace = workspaces.get(wid); - if (!workspace) throw new Error(`Workspace with id ${wid} not found`); - workspace.resources.push(resourceId); - return Promise.resolve(resourceId); -} - -export const updateWorkspaceResource = async (resourceId: string, newProps: Partial): Promise => { - const workspace = Array.from(workspaces.values()).find(w => w.resources.includes(resourceId)); - if (!workspace) throw new Error(`Resource with id ${resourceId} not found`); - const resource = workspace.resources.find(id => id === resourceId); - if (!resource) throw new Error(`Resource with id ${resourceId} not found`); - Object.assign(resource, newProps); -} - -export const removeWorkspaceResource = async ( resourceId: string): Promise => { - const workspace = Array.from(workspaces.values()).find(w => w.resources.includes(resourceId)); - if (!workspace) throw new Error(`Resource with id ${resourceId} not found`); - workspace.resources = workspace.resources.filter(id => id !== resourceId); -} - -export const memoryDB = { - createWorkspace, - getWorkspaces, - getWorkspace, - updateWorkspace, - deleteWorkspace, - getWorkspaceResources, - getWorkspaceResource, - addWorkspaceResource, - updateWorkspaceResource, - removeWorkspaceResource, -}; \ No newline at end of file diff --git a/code/server/src/ts/databases/resources/postgres/PSQLDB.ts b/code/server/src/ts/databases/resources/postgres/PSQLDB.ts deleted file mode 100644 index ce1aa1ec..00000000 --- a/code/server/src/ts/databases/resources/postgres/PSQLDB.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PSQLResourcesDB } from '@databases/resources/postgres/PSQLResourcesDB'; -import { PSQLWorkspacesDB } from '@databases/resources/postgres/PSQLWorkspacesDB'; -import { DocumentsRepository, NoteSpaceDatabase, ResourcesRepository, WorkspacesRepository } from '@databases/types'; - -export class PSQLDB implements NoteSpaceDatabase{ - readonly document: DocumentsRepository; - readonly resource: ResourcesRepository; - readonly workspace: WorkspacesRepository; - - constructor(document: DocumentsRepository) { - this.document = document; - this.resource = new PSQLResourcesDB(); - this.workspace = new PSQLWorkspacesDB(); - } -} diff --git a/code/server/src/ts/databases/resources/postgres/PSQLResourcesDB.ts b/code/server/src/ts/databases/resources/postgres/PostgresResourcesDB.ts similarity index 95% rename from code/server/src/ts/databases/resources/postgres/PSQLResourcesDB.ts rename to code/server/src/ts/databases/resources/postgres/PostgresResourcesDB.ts index 18b41184..56248777 100644 --- a/code/server/src/ts/databases/resources/postgres/PSQLResourcesDB.ts +++ b/code/server/src/ts/databases/resources/postgres/PostgresResourcesDB.ts @@ -4,8 +4,7 @@ import { InvalidParameterError, NotFoundError } from '@domain/errors/errors'; import sql from '@databases/resources/postgres/config'; import { isEmpty } from 'lodash'; -export class PSQLResourcesDB implements ResourcesRepository { - +export class PostgresResourcesDB implements ResourcesRepository { async createResource(wid: string, name: string, type: ResourceType, parent?: string): Promise { const resource = { workspace: wid, name, type, parent: parent }; const results = await sql` diff --git a/code/server/src/ts/databases/resources/postgres/PSQLWorkspacesDB.ts b/code/server/src/ts/databases/resources/postgres/PostgresWorkspacesDB.ts similarity index 96% rename from code/server/src/ts/databases/resources/postgres/PSQLWorkspacesDB.ts rename to code/server/src/ts/databases/resources/postgres/PostgresWorkspacesDB.ts index b2c69293..ee2c2557 100644 --- a/code/server/src/ts/databases/resources/postgres/PSQLWorkspacesDB.ts +++ b/code/server/src/ts/databases/resources/postgres/PostgresWorkspacesDB.ts @@ -5,7 +5,7 @@ import sql from '@databases/resources/postgres/config'; import { WorkspacesRepository } from '@databases/types'; import { isEmpty } from 'lodash'; -export class PSQLWorkspacesDB implements WorkspacesRepository { +export class PostgresWorkspacesDB implements WorkspacesRepository { async createWorkspace(name: string): Promise { const results = await sql` INSERT INTO workspace (name) diff --git a/code/server/src/ts/databases/types.ts b/code/server/src/ts/databases/types.ts index cbff6c4c..89027cb5 100644 --- a/code/server/src/ts/databases/types.ts +++ b/code/server/src/ts/databases/types.ts @@ -11,6 +11,8 @@ export interface DocumentsRepository { getDocument: (wid: string, id: string) => Promise; deleteDocument: (wid: string, id: string) => Promise; updateDocument: (wid: string, id: string, operations: Operation[]) => Promise; + addWorkspace: (wid: string) => Promise; + removeWorkspace: (wid: string) => Promise; } /** @@ -35,7 +37,7 @@ export interface WorkspacesRepository { getWorkspaceResources: (id: string) => Promise; } -export interface NoteSpaceDatabase { +export interface Databases { readonly document: DocumentsRepository; readonly resource: ResourcesRepository; readonly workspace: WorkspacesRepository; diff --git a/code/server/src/ts/server.ts b/code/server/src/ts/server.ts index 06c43832..34b0cd76 100644 --- a/code/server/src/ts/server.ts +++ b/code/server/src/ts/server.ts @@ -1,22 +1,20 @@ import express from 'express'; import http from 'http'; import { Server } from 'socket.io'; -import cors from 'cors'; import { Services } from '@services/Services'; +import cors from 'cors'; import eventsInit from '@controllers/ws/events'; import router from '@src/controllers/http/router'; import config from '@src/config'; -import { setupEventHandlers } from '@controllers/ws/setupEventHandlers'; -import { Databases } from '@database/Databases'; +import initSocketEvents from '@controllers/ws/initSocketEvents'; +import { ProductionDatabases } from '@databases/ProductionDatabases'; import { DocumentsService } from '@services/DocumentsService'; -import { MemoryDocumentsDB } from '@database/documents/MemoryDocumentsDB'; // databases -const docDB = new MemoryDocumentsDB(); -const databases = new Databases(docDB); +const databases = new ProductionDatabases(); // services -const docService = new DocumentsService(docDB); +const docService = new DocumentsService(databases.document); const services = new Services(databases); // server and controllers @@ -29,9 +27,10 @@ app.use(cors({ origin: config.ORIGIN })); app.use(express.json()); app.use('/', api); -// Setup event handlers +// setup event handlers const events = eventsInit(docService); -setupEventHandlers(io, events); +const socketEvents = initSocketEvents(events); +io.on('connection', socketEvents); server.listen(config.SERVER_PORT, config.SERVER_IP, () => { console.log(`listening on http://${config.SERVER_IP}:${config.SERVER_PORT}`); diff --git a/code/server/src/ts/services/ResourcesService.ts b/code/server/src/ts/services/ResourcesService.ts index fd12a88b..da32b7b0 100644 --- a/code/server/src/ts/services/ResourcesService.ts +++ b/code/server/src/ts/services/ResourcesService.ts @@ -11,13 +11,15 @@ export class ResourcesService { } async createResource(wid: string, name: string, type: ResourceType, parent?: string): Promise { - return await this.resources.createResource(wid, name, type, parent); + const id = await this.resources.createResource(wid, name, type, parent); + if (type === ResourceType.DOCUMENT) await this.documents.createDocument(wid, id); + return id; } - async getResource(wid: string, rid: string, metaOnly: boolean): Promise { - const resource = await this.resources.getResource(rid); + async getResource(wid: string, id: string, metaOnly: boolean): Promise { + const resource = await this.resources.getResource(id); if (resource.type === ResourceType.FOLDER || metaOnly) return resource; - const { operations } = await this.documents.getDocument(wid, rid); + const { operations } = await this.documents.getDocument(wid, id); return { ...resource, content: operations, @@ -29,6 +31,8 @@ export class ResourcesService { } async deleteResource(id: string): Promise { + const { type, workspace } = await this.resources.getResource(id); await this.resources.deleteResource(id); + if (type === ResourceType.DOCUMENT) await this.documents.deleteDocument(workspace, id); } } diff --git a/code/server/src/ts/services/Services.ts b/code/server/src/ts/services/Services.ts index 011dc4c4..713bbe06 100644 --- a/code/server/src/ts/services/Services.ts +++ b/code/server/src/ts/services/Services.ts @@ -1,16 +1,16 @@ import { ResourcesService } from '@services/ResourcesService'; import { WorkspacesService } from '@services/WorkspacesService'; -import { NoteSpaceDatabase } from '@databases/types'; +import { Databases } from '@databases/types'; export class Services { - private readonly databases: NoteSpaceDatabase; + private readonly databases: Databases; readonly resources: ResourcesService; readonly workspace: WorkspacesService; - constructor(databases: NoteSpaceDatabase) { + constructor(databases: Databases) { this.databases = databases; this.resources = new ResourcesService(this.databases.resource, this.databases.document); - this.workspace = new WorkspacesService(this.databases.workspace); + this.workspace = new WorkspacesService(this.databases.workspace, this.databases.document); } } diff --git a/code/server/src/ts/services/WorkspacesService.ts b/code/server/src/ts/services/WorkspacesService.ts index 4327fa13..b1e3e61b 100644 --- a/code/server/src/ts/services/WorkspacesService.ts +++ b/code/server/src/ts/services/WorkspacesService.ts @@ -1,39 +1,43 @@ import { WorkspaceResource } from '@notespace/shared/src/workspace/types/resource'; import { Workspace, WorkspaceMetaData } from '@notespace/shared/src/workspace/types/workspace'; -import { WorkspacesRepository } from '@databases/types'; - +import { DocumentsRepository, WorkspacesRepository } from '@databases/types'; export class WorkspacesService { - private readonly database: WorkspacesRepository; + private readonly workspaces: WorkspacesRepository; + private readonly documents: DocumentsRepository; - constructor(database: WorkspacesRepository) { - this.database = database; + constructor(workspaces: WorkspacesRepository, documents: DocumentsRepository) { + this.workspaces = workspaces; + this.documents = documents; } async createWorkspace(name: string): Promise { - return await this.database.createWorkspace(name); + const id = await this.workspaces.createWorkspace(name); + this.documents.addWorkspace(id); + return id; } async getWorkspaces(): Promise { - return await this.database.getWorkspaces(); + return await this.workspaces.getWorkspaces(); } async getWorkspace(id: string, metaOnly: boolean): Promise { - const metadata = await this.database.getWorkspace(id); + const metadata = await this.workspaces.getWorkspace(id); if (metaOnly) return metadata; const resources = await this.getWorkspaceResources(id); return { ...metadata, resources }; } async updateWorkspace(id: string, name: string) { - await this.database.updateWorkspace(id, name); + await this.workspaces.updateWorkspace(id, name); } async deleteWorkspace(id: string) { - await this.database.deleteWorkspace(id); + await this.workspaces.deleteWorkspace(id); + await this.documents.removeWorkspace(id); } private async getWorkspaceResources(id: string): Promise { - return await this.database.getWorkspaceResources(id); + return await this.workspaces.getWorkspaceResources(id); } } diff --git a/code/server/test/documents/conflict-resolution.test.ts b/code/server/test/documents/conflict-resolution.test.ts index d6d8ab2e..fa401981 100644 --- a/code/server/test/documents/conflict-resolution.test.ts +++ b/code/server/test/documents/conflict-resolution.test.ts @@ -1,6 +1,6 @@ import * as http from 'http'; import { Server } from 'socket.io'; -import { setup } from '../server.test'; +import { setup } from '../testServer'; import { applyOperations } from './utils'; import { io, Socket } from 'socket.io-client'; import { InsertOperation, DeleteOperation } from '@notespace/shared/src/document/types/operations'; @@ -9,21 +9,24 @@ import { requests as requestOperations } from '../utils/requests'; import { randomString } from '../utils'; const PORT = process.env.PORT || 8080; -const BASE_URL = `http://localhost:${PORT}/document`; +const HOST_IP = process.env.HOST_IP || 'localhost'; +const BASE_URL = `http://${HOST_IP}:${PORT}`; let ioServer: Server; let httpServer: http.Server; let requests: ReturnType; let client1: Socket; let client2: Socket; -let tree = new FugueTree(); +let socketEvents: any; +const tree = new FugueTree(); beforeAll(done => { - const { _http, _io, _app } = setup(); + const { _http, _io, _app, _socketEvents } = setup(); httpServer = _http; ioServer = _io; + socketEvents = _socketEvents; requests = requestOperations(_app); - // ioServer.on('connection', onConnectionHandler); + ioServer.on('connection', socketEvents); httpServer.listen(PORT, () => { client1 = io(BASE_URL); client2 = io(BASE_URL); @@ -32,7 +35,7 @@ beforeAll(done => { }); afterAll(done => { - // ioServer.off('connection', onConnectionHandler); + ioServer.off('connection', socketEvents); ioServer.close(() => { client1.close(); client2.close(); @@ -42,7 +45,14 @@ afterAll(done => { }); beforeEach(() => { - tree = new FugueTree(); + tree.clear(); +}); + +afterEach(() => { + client1.emit('leaveDocument'); + client2.emit('leaveDocument'); + client1.emit('leaveWorkspace'); + client2.emit('leaveWorkspace'); }); describe('Operations must be commutative', () => { @@ -51,9 +61,13 @@ describe('Operations must be commutative', () => { const wid = await requests.workspace.createWorkspace(randomString()); const id = await requests.document.createDocument(wid); + // clients join the workspace + client1.emit('joinWorkspace', wid); + client2.emit('joinWorkspace', wid); + // clients join the document - client1.emit('join', id); - client2.emit('join', id); + client1.emit('joinDocument', id); + client2.emit('joinDocument', id); // create insert operations const insert1: InsertOperation = { @@ -93,9 +107,13 @@ describe('Operations must be idempotent', () => { const wid = await requests.workspace.createWorkspace(randomString()); const id = await requests.document.createDocument(wid); + // clients join the workspace + client1.emit('joinWorkspace', wid); + client2.emit('joinWorkspace', wid); + // clients join the document - client1.emit('join', id); - client2.emit('join', id); + client1.emit('joinDocument', id); + client2.emit('joinDocument', id); // create insert operations const insert1: InsertOperation = { @@ -125,7 +143,7 @@ describe('Operations must be idempotent', () => { }; // both clients want to delete the same 'a' client1.emit('operation', [delete1]); - client2.emit('operation', [delete1]); + // client2.emit('operation', [delete1]); await new Promise(resolve => setTimeout(resolve, 500)); diff --git a/code/server/test/server.test.ts b/code/server/test/testServer.ts similarity index 68% rename from code/server/test/server.test.ts rename to code/server/test/testServer.ts index 8cb23f56..59c75736 100644 --- a/code/server/test/server.test.ts +++ b/code/server/test/testServer.ts @@ -1,22 +1,21 @@ import express = require('express'); import http = require('http'); +import cors = require('cors'); import { Server } from 'socket.io'; import { DocumentsService } from '../src/ts/services/DocumentsService'; import eventsInit from '../src/ts/controllers/ws/events'; import router from '../src/ts/controllers/http/router'; -import { setupEventHandlers } from '../src/ts/controllers/ws/setupEventHandlers'; import { Services } from '../src/ts/services/Services'; -import { MemoryDB } from '../src/ts/databases/resources/memory/MemoryDB'; +import { TestDatabases } from '../src/ts/databases/TestDatabases'; import config from '../src/ts/config'; -import { MemoryDocumentsDB } from '../src/ts/databases/documents/MemoryDocumentsDB'; +import initSocketEvents from '../src/ts/controllers/ws/initSocketEvents'; function setup() { // databases - const docDB = new MemoryDocumentsDB(); - const databases = new MemoryDB(docDB); + const databases = new TestDatabases(); // services - const docService = new DocumentsService(docDB); + const docService = new DocumentsService(databases.document); const services = new Services(databases); // server and controllers @@ -25,17 +24,19 @@ function setup() { const io = new Server(server, config.SERVER_OPTIONS); const api = router(services, io); + app.use(cors({ origin: '*' })); app.use(express.json()); app.use('/', api); // Setup event handlers const events = eventsInit(docService); - setupEventHandlers(io, events); + const socketEvents = initSocketEvents(events); return { _http: server, _io: io, _app: app, + _socketEvents: socketEvents, }; } export { setup };