@textea/y-socket.io
Version:
Socket.io Connector for Yjs
279 lines (271 loc) • 10.2 kB
JavaScript
/// <reference types="./provider.d.ts" />
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('socket.io-client'), require('uuid'), require('y-protocols/awareness'), require('yjs'), require('zustand'), require('zustand/middleware')) :
typeof define === 'function' && define.amd ? define(['exports', 'socket.io-client', 'uuid', 'y-protocols/awareness', 'yjs', 'zustand', 'zustand/middleware'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.YSocketIO = {}, global.socket["io-client"], global.uuid, global["y-protocols/awareness"], global.yjs, global.zustand, global["zustand/middleware"]));
})(this, (function (exports, socket_ioClient, uuid, awareness, Y, zustand, middleware) { '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()
];
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: awareness$1 =new awareness.Awareness(doc) , autoConnect =true , autoConnectBroadcastChannel =true } = {})=>{
const syncingDocUpdates = new Set();
const store = zustand.createStore()(middleware.subscribeWithSelector(()=>({
...INITIAL_STATE,
connecting: autoConnect
})));
const queryParameters = {
roomName,
clientId: String(awareness$1.clientID)
};
const socket = socket_ioClient.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__namespace.encodeStateVector(doc);
socket.emit('doc:diff', docDiff);
socket.once('doc:update', ()=>{
if (!syncingDocUpdates.size) {
store.setState({
synced: true
});
}
});
const awarenessUpdate = awareness.encodeAwarenessUpdate(awareness$1, [
awareness$1.clientID
]);
socket.emit('awareness:update', awarenessUpdate);
});
socket.on('data:update', (data)=>{
store.setState({
data
});
});
socket.on('doc:diff', (diff)=>{
const updateV2 = Y__namespace.encodeStateAsUpdateV2(doc, new Uint8Array(diff));
socket.emit('doc:update', updateV2);
});
socket.on('doc:update', (updateV2)=>{
Y__namespace.applyUpdateV2(doc, new Uint8Array(updateV2), socket);
});
socket.on('awareness:update', (update)=>{
awareness.applyAwarenessUpdate(awareness$1, 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$1);
awareness.removeAwarenessStates(awareness$1, 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__namespace.encodeStateAsUpdateV2(doc, diff);
broadcastChannel.postMessage([
'doc:update',
updateV2,
clientId
]);
break;
}
case 'doc:update':
{
const [, updateV21, clientId1] = event.data;
if (!clientId1 || clientId1 === awareness$1.clientID) {
Y__namespace.applyUpdateV2(doc, updateV21, socket);
}
break;
}
case 'awareness:query':
{
const [, clientId2] = event.data;
const clients = getClients(awareness$1);
const update = awareness.encodeAwarenessUpdate(awareness$1, clients);
broadcastChannel.postMessage([
'awareness:update',
update,
clientId2
]);
break;
}
case 'awareness:update':
{
const [, update1, clientId3] = event.data;
if (!clientId3 || clientId3 === awareness$1.clientID) {
awareness.applyAwarenessUpdate(awareness$1, update1, socket);
}
break;
}
}
};
const connectBroadcastChannel = ()=>{
if (broadcastChannel) {
return;
}
broadcastChannel = Object.assign(new BroadcastChannel(broadcastChannelName), {
onmessage: handleBroadcastChannelMessage
});
const docDiff = Y__namespace.encodeStateVector(doc);
broadcastChannel.postMessage([
'doc:diff',
docDiff,
awareness$1.clientID
]);
const docUpdateV2 = Y__namespace.encodeStateAsUpdateV2(doc);
broadcastChannel.postMessage([
'doc:update',
docUpdateV2
]);
broadcastChannel.postMessage([
'awareness:query',
awareness$1.clientID
]);
const awarenessUpdate = awareness.encodeAwarenessUpdate(awareness$1, [
awareness$1.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__namespace.convertUpdateFormatV1ToV2(updateV1);
if (socket.connected) {
const updateId = uuid.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 = awareness.encodeAwarenessUpdate(awareness$1, changedClients);
socket.volatile.emit('awareness:update', update);
broadcastChannel?.postMessage([
'awareness:update',
update
]);
};
awareness$1.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$1.off('update', handleAwarenessUpdate);
}
};
};
exports.INITIAL_STATE = INITIAL_STATE;
exports.createSocketIOProvider = createSocketIOProvider;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=provider.js.map