UNPKG

@textea/y-socket.io

Version:
182 lines (177 loc) 6.55 kB
/// <reference types="./server.d.ts" /> import { createServer } from 'http'; import { Server } from 'socket.io'; import { Awareness, encodeAwarenessUpdate, applyAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'; import * as Y from 'yjs'; const getClients = (awareness)=>[ ...awareness.getStates().keys() ]; /** * There are four scenarios: * 1. Signed User share sheet to specified person with write permission (both side need authorization) * 2. Signed User share sheet to specified person with only view permission (both side need authorization) * 3. Signed User share sheet to everyone with write permission (only one side need authorization) * 4. Signed User share sheet to everyone with only view permission (only one side need authorization) * * If sheet owner close sharing (or disable sharing), others won't see the sheet anymore, * which means we will delete the sheet in their browser. * * We only consider scenario 3 for now, because It's easy to implement */ const createSocketIOServer = (httpServer, { getUserId , persistence , autoDeleteRoom =false , cors , allowRequest } = {})=>{ const roomMap = new Map(); const io = new Server(httpServer, { cors, allowRequest }); io.use((socket, next)=>{ const { roomName , clientId } = socket.handshake.query; if (typeof roomName !== 'string') { return next(new Error("wrong type of query parameter 'roomName'")); } if (typeof clientId !== 'string' || Number.isNaN(+clientId)) { return next(new Error("wrong type of query parameter 'clientId'")); } socket.yjs = { roomMap, roomName, clientId: Number(clientId) }; return next(); }); io.use((socket, next)=>{ const result = getUserId?.(socket) ?? socket.yjs.clientId; if (result instanceof Error) { return next(result); } socket.userId = result; return next(); }); const { adapter } = io.of('/'); const isDefaultRoom = (roomName)=>{ return adapter.sids.has(roomName); // ^^^^ Map<SocketId, Set<RoomName>> }; adapter.on('create-room', (roomName)=>{ if (isDefaultRoom(roomName) || roomMap.has(roomName)) { return; } const doc = new Y.Doc(); const awareness = new Awareness(doc); // delete local `clientId` from `awareness.getStates()` Map awareness.setLocalState(null); awareness.on('update', (changes, origin)=>{ const changedClients = Object.values(changes).reduce((res, cur)=>[ ...res, ...cur ]); const update = encodeAwarenessUpdate(awareness, changedClients); io.to(roomName).except(origin).emit('awareness:update', update); }); const prepareDoc = async ()=>{ await persistence?.bindState(roomName, doc); doc.on('update', (updateV1, origin)=>{ const updateV2 = Y.convertUpdateFormatV1ToV2(updateV1); io.to(roomName).except(origin).emit('doc:update', updateV2); }); return doc; }; const preparingDoc = prepareDoc(); const room = { owner: null, awareness, getDoc: ()=>preparingDoc, destroy: async ()=>{ await persistence?.writeState(roomName, doc); doc.destroy(); awareness.destroy(); } }; roomMap.set(roomName, room); }); if (autoDeleteRoom) { adapter.on('delete-room', (roomName)=>{ if (isDefaultRoom(roomName) || !roomMap.has(roomName)) { return; } const room = roomMap.get(roomName); roomMap.delete(roomName); return room.destroy(); }); } io.on('connection', (socket)=>{ const { roomName } = socket.yjs; socket.join(roomName); const room = roomMap.get(roomName); if (room.owner === null) { room.owner = socket.userId; } const clients = getClients(room.awareness); if (clients.length) { const awarenessUpdate = encodeAwarenessUpdate(room.awareness, clients); socket.emit('awareness:update', awarenessUpdate); } room.getDoc().then((doc)=>{ const docDiff = Y.encodeStateVector(doc); socket.emit('doc:diff', docDiff); }); socket.on('room:close', ()=>{ const room = roomMap.get(roomName); if (!room || socket.userId !== room.owner) { return; } io.to(roomName).disconnectSockets(); roomMap.delete(roomName); return room.destroy(); }); socket.on('doc:diff', (diff)=>{ const room = roomMap.get(roomName); if (!room) { return; } room.getDoc().then((doc)=>{ const updateV2 = Y.encodeStateAsUpdateV2(doc, diff); socket.emit('doc:update', updateV2); }); }); socket.on('doc:update', (updateV2, callback)=>{ const room = roomMap.get(roomName); if (!room) { return; } room.getDoc().then((doc)=>{ Y.applyUpdateV2(doc, updateV2, socket.id); callback?.(); }); }); socket.on('awareness:update', (update)=>{ const room = roomMap.get(roomName); if (!room) { return; } applyAwarenessUpdate(room.awareness, update, socket.id); }); socket.on('disconnect', ()=>{ const room = roomMap.get(roomName); if (!room) { return; } const { clientId } = socket.yjs; removeAwarenessStates(room.awareness, [ clientId ], socket.id); }); }); return io; }; const createSimpleServer = (options = {})=>{ const httpServer = createServer((request, response)=>{ response.writeHead(200, { 'Content-Type': 'application/json' }); response.end('"okay"'); }); createSocketIOServer(httpServer, options); return httpServer; }; export { createSimpleServer, createSocketIOServer }; //# sourceMappingURL=server.mjs.map