UNPKG

@textea/y-socket.io

Version:
208 lines (199 loc) 8.07 kB
/// <reference types="./server.d.ts" /> (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('http'), require('socket.io'), require('y-protocols/awareness'), require('yjs')) : typeof define === 'function' && define.amd ? define(['exports', 'http', 'socket.io', 'y-protocols/awareness', 'yjs'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.YSocketIO = {}, global.http, global.socket.io, global["y-protocols/awareness"], global.yjs)); })(this, (function (exports, http, socket_io, awareness, Y) { 'use strict'; function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var Y__namespace = /*#__PURE__*/_interopNamespace(Y); 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 socket_io.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__namespace.Doc(); const awareness$1 = new awareness.Awareness(doc); // delete local `clientId` from `awareness.getStates()` Map awareness$1.setLocalState(null); awareness$1.on('update', (changes, origin)=>{ const changedClients = Object.values(changes).reduce((res, cur)=>[ ...res, ...cur ]); const update = awareness.encodeAwarenessUpdate(awareness$1, 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__namespace.convertUpdateFormatV1ToV2(updateV1); io.to(roomName).except(origin).emit('doc:update', updateV2); }); return doc; }; const preparingDoc = prepareDoc(); const room = { owner: null, awareness: awareness$1, getDoc: ()=>preparingDoc, destroy: async ()=>{ await persistence?.writeState(roomName, doc); doc.destroy(); awareness$1.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 = awareness.encodeAwarenessUpdate(room.awareness, clients); socket.emit('awareness:update', awarenessUpdate); } room.getDoc().then((doc)=>{ const docDiff = Y__namespace.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__namespace.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__namespace.applyUpdateV2(doc, updateV2, socket.id); callback?.(); }); }); socket.on('awareness:update', (update)=>{ const room = roomMap.get(roomName); if (!room) { return; } awareness.applyAwarenessUpdate(room.awareness, update, socket.id); }); socket.on('disconnect', ()=>{ const room = roomMap.get(roomName); if (!room) { return; } const { clientId } = socket.yjs; awareness.removeAwarenessStates(room.awareness, [ clientId ], socket.id); }); }); return io; }; const createSimpleServer = (options = {})=>{ const httpServer = http.createServer((request, response)=>{ response.writeHead(200, { 'Content-Type': 'application/json' }); response.end('"okay"'); }); createSocketIOServer(httpServer, options); return httpServer; }; exports.createSimpleServer = createSimpleServer; exports.createSocketIOServer = createSocketIOServer; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=server.js.map