@textea/y-socket.io
Version:
Socket.io Connector for Yjs
208 lines (199 loc) • 8.07 kB
JavaScript
/// <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