UNPKG

delta-component

Version:

embeddable react component

356 lines (321 loc) 14.2 kB
'use strict'; const controller = require('./controllers/incomingMessages'); const document = require('./document'); const log = require('./../../common/logger').log; const confTransport = require('../../common/logger').confTransport; const deserialize = require('../../common/crdt/serialize').deserialize; const io = require('socket.io-client'); const cursors = require('./logic/cursors'); const documentId = require('../../common/logic/documentId'); const UserParams = require('./../../common/messages/user/params').UserParams; const uniqid = require('uniqid'); const generateSiteId = require('./logic/siteId').generateSiteId; const opLogic = require('../../common/logic/operation'); // WebsocketTransport is singleton let instance = null; class WebsocketTransport { /** * Точка входа для UI в приложение: * let transport = new Transport('i_am_epad'); // началась установка соединения * .... где-то в UI: * transport.getDocument('epad/page/10').then( * document => { подписаться на события, отрендерить }, * error => { нарисовать ошибку } * ) * .... где-то в UI: * transport.createDocument('epad/page/10', 'bazinga!').then( * document => { подписаться на события, отрендерить }, * error => { нарисовать ошибку } * ) */ constructor(serviceSlug, host, docId) { /** * @param serviceSlug просто чтобы в логах различать кто сейчас подключился. На логику не влияет * @param host: "https://delta.yandex-team.ru" - некорректно указание аргумент приведет к странным ошибкам * `${window.location.protocol}//${global.location.host}` - можно указывать так */ this.serviceSlug = serviceSlug; this.log = log; this.websocket = null; this.handledDocuments = {}; this.incomingMessages = null; // обработчик входящих сообщений this._socketHost = host; this._siteId = null; this.currentPerson = null; // TODO: Поддержать работу нескольких документов через одно соединение // // 1) Сейчас в нужный инстанс с документом мы проксируем через nginx, // поэтому на каждый документ придется держать отдельное соединение. // // 2) Кроме того, временно documentString явно передается в transport, // хотя transport поддерживает работу с несколькими документами. this._docId = docId; this.standalone = this._standalone(); this.establishConnection(); confTransport(this); } static getInstance(serviceSlug, host, docId) { if(instance === null) { instance = new WebsocketTransport(serviceSlug, host, docId); } return instance; } establishConnection() { let that = this; const wsParams = { //reconnectionAttempts: 10, query: { 'iam': this.serviceSlug }, // transports: ['websocket'] }; if (!this.standalone && this._docId != null) { wsParams.path = `/get/${this._docId}`; } let websocket = io.connect(this._socketHost, wsParams); websocket.on('connect', () => { // TODO: разрешить изменения в документах, перезапросить существующие документы, сообщить об этом в UI const params = new UserParams(); if (this.currentPerson !== null) { params.name = this.currentPerson.name; } websocket.emit('whoami', this.mySiteId(), params.toClientFormat(), (me) => { // TODO: siteId выдавать на инстанс ноды, к которой подключен, а не на вкладку браузера that._authenticated(new cursors.Participant(me.siteId, me.uid, me.name)) }); }); websocket.on('disconnect', () => { // TODO: блокировать контроллер и транспорт до успешного ответа на whoami const docsTotal = Object.keys(this.handledDocuments).length; this.log.warn(`I lost connection, locking ${docsTotal} documents`); for(let documentString in this.handledDocuments) { let document = this.handledDocuments[documentString]; if(!document.isLocked()) document.lock(); } }); this.websocket = websocket; } getHandledDocument(documentString) { return this.handledDocuments[documentString]; } mySiteId() { if (this._siteId === null) { this._siteId = generateSiteId(uniqid()); } return this._siteId; } _authenticated(me) { this.currentPerson = me; this.log.info(`connected, i am ${me}`); if(this.incomingMessages === null) { this.incomingMessages = controller.controllerProducer( this.websocket, me, this ); } // Восстановить все заблокированные документы for(let documentString in this.handledDocuments) { this._refreshDocument(this.handledDocuments[documentString], me); } } _refreshDocument(document) { const log = this.log; const currentPerson = this.currentPerson; log.debug(`Restoring ${document}`); this._getTreedocFromNetwork(document.documentString).then( treedoc => { // TODO: поменять siteId document.generateTreedocClient(treedoc, currentPerson); document.somewhenLoadCurrentParticipants(currentPerson); document.textChanged(document.getText()); document.unlock(); log.info(`${document} is restored`); }, error => { log.error(`could not restore document ${document.toString()} after network failure`, error) } ); } getParticipants(documentString) { /** * Список участников документа */ // бросить исключение, если documentString невалиден this._validateDocumentString(documentString); let that = this; return new Promise(function (resolve, reject) { that.websocket.emit('GetParticipants', documentString, (participantsOrError) => { if(participantsOrError.error) { reject(participantsOrError); } else { resolve(participantsOrError.people.map((person) => { return new cursors.Participant(person.siteId, person.uid, person.name); })); } }) }) } _validateDocumentString(documentString) { return documentId.validateDocumentString(documentString); } getDocument(documentString) { /** * Подключиться к существующему документу. * * @type {WebsocketTransport} */ // бросить исключение, если documentString невалиден this._validateDocumentString(documentString); // убедиться что вебсокет подключился let that = this; return new Promise(function (resolve, reject) { that.log.debug(`getting ${documentString}`); if(documentString in that.handledDocuments) { resolve(that.handledDocuments[documentString]); return; } that._getTreedocFromNetwork(documentString).then( treedoc => { let doc = new document.Document( documentString, treedoc, that, that.currentPerson ); that.handledDocuments[documentString] = doc; resolve(doc); }, error => reject(error) ) }) } _getTreedocFromNetwork(documentString) { let that = this; return new Promise(function (resolve, reject) { that.websocket.emit('JoinDocument', documentString, (documentOrError) => { if(documentOrError.error) { reject(documentOrError); } else { resolve(deserialize(documentOrError.treedoc)); } }); }); } _standalone() { return typeof window !== 'undefined' && window.document.cookie.indexOf('delta-standalone=1') > -1; } createDocument(documentString, initialText) { /** * Создать документ. * Если документ уже создан - будет ошибка. * Вы сами должны отслеживать что документа не существует. * * @type {WebsocketTransport} */ // бросить исключение, если documentString невалиден this._validateDocumentString(documentString); // убедиться что вебсокет подключился let that = this; return new Promise(function (resolve, reject) { that.log.debug(`creating document "${documentString}"`); // if(documentString in that.handledDocuments) { // reject( сериализовать ExistsError ); // return; // } // TODO: вернуть ошибку that.websocket.emit('CreateDocument', documentString, initialText, (treedocOrError) => { if(treedocOrError.error) { reject(treedocOrError); } else { let doc = new document.Document( documentString, deserialize(treedocOrError.treedoc), that, that.currentPerson ); that.handledDocuments[documentString] = doc; that.log.info(`created document "${documentString}"`); resolve(doc); } }); }) } leaveDocument(documentString){ // убедиться что вебсокет подключился let that = this; return new Promise(function (resolve) { that.websocket.emit('LeaveDocument', documentString); delete that.handledDocuments[documentString]; log.info(`Left document ${documentString}`); resolve(true); }) } viewerJoined(documentString, siteId, uid, displayName) { siteId = parseInt(siteId); if (documentString in this.handledDocuments) { const person = new cursors.Participant(siteId, uid, displayName); log.info(`${person} joined document ${documentString}`); this.handledDocuments[documentString].joined(person); } else { this.log.error(`Unhandled document "${documentString}"`) } } viewerLeft(documentString, siteId, uid, displayName) { siteId = parseInt(siteId); if (documentString in this.handledDocuments) { this.handledDocuments[documentString].left(siteId); } else { this.log.error(`Unhandled document "${documentString}"`) } } changeUserParams({displayName=null}) { const params = new UserParams(); params.name = displayName; params.validate(); this.currentPerson.name = displayName; this.websocket.emit('ChangeParams', params.toClientFormat()); } sendOperations(documentString, ops) { /** * Отправить локальные операции. */ this.websocket.emit('ApplyOps', documentString, ops); } getTimelineOfChanges(documentString) { let that = this; return new Promise(function (resolve, reject) { that.websocket.emit('TimelineOfChanges', documentString, (timelineOrError) => { if (timelineOrError.error) { reject(timelineOrError); } else { resolve(timelineOrError.epochs.map( ([treedocJson, ops]) => [ treedocJson, ops.map(({op, meta}) => { meta.created_at = new Date(meta.created_at); return {op: opLogic.deserialize(op), meta: meta} }) ] )); } }); }); } userChangedParams(siteId, newParams) { for (const documentString of Object.keys(this.handledDocuments)) { const document = this.handledDocuments[documentString]; const participant = document.viewersMap.getBySiteId(siteId); if (participant !== null) { participant.displayName = newParams.name; document.participantsChanged(); } } } clientLog(documentString, message, context) { this.websocket.emit('ClientLog', documentString, message, context); } } const transportFabric = WebsocketTransport.getInstance; module.exports = { transportFabric, WebsocketTransport, };