@textea/y-socket.io
Version:
Socket.io Connector for Yjs
255 lines (251 loc) • 8.38 kB
JavaScript
/// <reference types="./provider.d.ts" />
import { io } from 'socket.io-client';
import { v4 } from 'uuid';
import { encodeAwarenessUpdate, applyAwarenessUpdate, removeAwarenessStates, Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import { createStore } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
const getClients = (awareness)=>[
...awareness.getStates().keys()
];
const getOtherClients = (awareness)=>{
const clients = getClients(awareness);
return clients.filter((clientId)=>clientId !== awareness.clientID);
};
const INITIAL_SOCKET_STATE = {
connecting: false,
connected: false,
synced: false,
error: null
};
/**
* @internal
*/ const INITIAL_STATE = {
...INITIAL_SOCKET_STATE,
data: null
};
const createSocketIOProvider = (serverUrl, roomName, doc, { awareness =new Awareness(doc) , autoConnect =true , autoConnectBroadcastChannel =true } = {})=>{
const syncingDocUpdates = new Set();
const store = createStore()(subscribeWithSelector(()=>({
...INITIAL_STATE,
connecting: autoConnect
})));
const queryParameters = {
roomName,
clientId: String(awareness.clientID)
};
const socket = io(serverUrl, {
query: queryParameters,
autoConnect
});
socket.on('connect_error', (err)=>{
store.setState({
connecting: false,
error: err.message
});
});
socket.on('connect', ()=>{
store.setState({
connecting: false,
connected: true,
error: null
});
const docDiff = Y.encodeStateVector(doc);
socket.emit('doc:diff', docDiff);
socket.once('doc:update', ()=>{
if (!syncingDocUpdates.size) {
store.setState({
synced: true
});
}
});
const awarenessUpdate = encodeAwarenessUpdate(awareness, [
awareness.clientID
]);
socket.emit('awareness:update', awarenessUpdate);
});
socket.on('data:update', (data)=>{
store.setState({
data
});
});
socket.on('doc:diff', (diff)=>{
const updateV2 = Y.encodeStateAsUpdateV2(doc, new Uint8Array(diff));
socket.emit('doc:update', updateV2);
});
socket.on('doc:update', (updateV2)=>{
Y.applyUpdateV2(doc, new Uint8Array(updateV2), socket);
});
socket.on('awareness:update', (update)=>{
applyAwarenessUpdate(awareness, new Uint8Array(update), socket);
});
socket.on('disconnect', (_reason, description)=>{
const err = description instanceof Error ? description : null;
syncingDocUpdates.clear();
store.setState({
...INITIAL_STATE,
error: err?.message
});
const otherClients = getOtherClients(awareness);
removeAwarenessStates(awareness, otherClients, socket);
});
let broadcastChannel;
const broadcastChannelName = new URL(roomName, serverUrl).toString();
const handleBroadcastChannelMessage = (event)=>{
const [eventName] = event.data;
switch(eventName){
case 'doc:diff':
{
const [, diff, clientId] = event.data;
const updateV2 = Y.encodeStateAsUpdateV2(doc, diff);
broadcastChannel.postMessage([
'doc:update',
updateV2,
clientId
]);
break;
}
case 'doc:update':
{
const [, updateV21, clientId1] = event.data;
if (!clientId1 || clientId1 === awareness.clientID) {
Y.applyUpdateV2(doc, updateV21, socket);
}
break;
}
case 'awareness:query':
{
const [, clientId2] = event.data;
const clients = getClients(awareness);
const update = encodeAwarenessUpdate(awareness, clients);
broadcastChannel.postMessage([
'awareness:update',
update,
clientId2
]);
break;
}
case 'awareness:update':
{
const [, update1, clientId3] = event.data;
if (!clientId3 || clientId3 === awareness.clientID) {
applyAwarenessUpdate(awareness, update1, socket);
}
break;
}
}
};
const connectBroadcastChannel = ()=>{
if (broadcastChannel) {
return;
}
broadcastChannel = Object.assign(new BroadcastChannel(broadcastChannelName), {
onmessage: handleBroadcastChannelMessage
});
const docDiff = Y.encodeStateVector(doc);
broadcastChannel.postMessage([
'doc:diff',
docDiff,
awareness.clientID
]);
const docUpdateV2 = Y.encodeStateAsUpdateV2(doc);
broadcastChannel.postMessage([
'doc:update',
docUpdateV2
]);
broadcastChannel.postMessage([
'awareness:query',
awareness.clientID
]);
const awarenessUpdate = encodeAwarenessUpdate(awareness, [
awareness.clientID
]);
broadcastChannel.postMessage([
'awareness:update',
awarenessUpdate
]);
};
const disconnectBroadcastChannel = ()=>{
if (broadcastChannel) {
broadcastChannel.close();
broadcastChannel = undefined;
}
};
if (autoConnectBroadcastChannel) {
connectBroadcastChannel();
}
const shouldSyncUpdate = ()=>socket.connected || broadcastChannel;
const handleDocUpdate = (updateV1, origin)=>{
if (origin === socket || !shouldSyncUpdate()) {
return;
}
const updateV2 = Y.convertUpdateFormatV1ToV2(updateV1);
if (socket.connected) {
const updateId = v4();
syncingDocUpdates.add(updateId);
store.setState({
synced: false
});
socket.emit('doc:update', updateV2, ()=>{
syncingDocUpdates.delete(updateId);
if (!syncingDocUpdates.size) {
store.setState({
synced: true
});
}
});
}
broadcastChannel?.postMessage([
'doc:update',
updateV2
]);
};
doc.on('update', handleDocUpdate);
const handleAwarenessUpdate = (changes, origin)=>{
if (origin === socket || !shouldSyncUpdate()) {
return;
}
const changedClients = Object.values(changes).reduce((res, cur)=>[
...res,
...cur
]);
const update = encodeAwarenessUpdate(awareness, changedClients);
socket.volatile.emit('awareness:update', update);
broadcastChannel?.postMessage([
'awareness:update',
update
]);
};
awareness.on('update', handleAwarenessUpdate);
return {
getState: store.getState,
connect: ()=>{
const { connecting , connected } = store.getState();
if (!connecting && !connected) {
store.setState({
connecting: true,
error: null
});
socket.connect();
}
},
closeRoom: ()=>{
socket.volatile.emit('room:close');
},
disconnect: ()=>{
socket.disconnect();
},
connectBroadcastChannel,
disconnectBroadcastChannel,
subscribe: store.subscribe,
destroy: ()=>{
store.destroy();
socket.disconnect();
broadcastChannel?.close();
doc.off('update', handleDocUpdate);
awareness.off('update', handleAwarenessUpdate);
}
};
};
export { INITIAL_STATE, createSocketIOProvider };
//# sourceMappingURL=provider.mjs.map