mirror of
https://github.com/cudr/slate-collaborative.git
synced 2026-03-02 03:40:18 +00:00
initial commit
This commit is contained in:
147
packages/backend/src/Connection.ts
Normal file
147
packages/backend/src/Connection.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import io from 'socket.io'
|
||||
import { ValueJSON } from 'slate'
|
||||
import * as Automerge from 'automerge'
|
||||
import throttle from 'lodash/throttle'
|
||||
|
||||
import { toSync, toJS } from '@slate-collaborative/bridge'
|
||||
|
||||
import { getClients, defaultValue, defaultOptions } from './utils'
|
||||
import { ConnectionOptions } from './model'
|
||||
|
||||
class Connection {
|
||||
private io: any
|
||||
private docSet: any
|
||||
private connections: { [key: string]: Automerge.Connection<any> }
|
||||
private options: ConnectionOptions
|
||||
|
||||
constructor(options: ConnectionOptions = defaultOptions) {
|
||||
this.io = io(options.port, options.connectOpts)
|
||||
this.docSet = new Automerge.DocSet()
|
||||
this.connections = {}
|
||||
this.options = options
|
||||
|
||||
this.configure()
|
||||
}
|
||||
|
||||
private configure = () =>
|
||||
this.io
|
||||
.of(this.nspMiddleware)
|
||||
.use(this.authMiddleware)
|
||||
.on('connect', this.onConnect)
|
||||
|
||||
private appendDoc = (path: string, value: ValueJSON) => {
|
||||
const sync = toSync(value)
|
||||
|
||||
const doc = Automerge.from(sync)
|
||||
|
||||
this.docSet.setDoc(path, doc)
|
||||
}
|
||||
|
||||
private saveDoc = throttle(pathname => {
|
||||
if (this.options.onDocumentSave) {
|
||||
const doc = this.docSet.getDoc(pathname)
|
||||
|
||||
this.options.onDocumentSave(pathname, toJS(doc))
|
||||
}
|
||||
}, (this.options && this.options.saveTreshold) || 2000)
|
||||
|
||||
private nspMiddleware = async (path, query, next) => {
|
||||
const { onDocumentLoad } = this.options
|
||||
|
||||
if (!this.docSet.getDoc(path)) {
|
||||
const valueJson = onDocumentLoad
|
||||
? await onDocumentLoad(path)
|
||||
: this.options.defaultValue || defaultValue
|
||||
|
||||
if (!valueJson) return next(null, false)
|
||||
|
||||
this.appendDoc(path, valueJson)
|
||||
}
|
||||
|
||||
return next(null, true)
|
||||
}
|
||||
|
||||
private authMiddleware = async (socket, next) => {
|
||||
const { query } = socket.handshake
|
||||
const { onAuthRequest } = this.options
|
||||
|
||||
if (onAuthRequest) {
|
||||
const permit = await onAuthRequest(query, socket)
|
||||
|
||||
if (!permit)
|
||||
return next(new Error(`Authentification error: ${socket.id}`))
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
private onConnect = socket => {
|
||||
const { id, conn } = socket
|
||||
const { name } = socket.nsp
|
||||
|
||||
const doc = this.docSet.getDoc(name)
|
||||
|
||||
const data = Automerge.save(doc)
|
||||
|
||||
this.connections[id] = new Automerge.Connection(this.docSet, data => {
|
||||
socket.emit('operation', { id: conn.id, ...data })
|
||||
})
|
||||
|
||||
socket.join(id, () => {
|
||||
this.connections[id].open()
|
||||
|
||||
socket.emit('document', data)
|
||||
})
|
||||
|
||||
socket.on('operation', this.onOperation(id, name))
|
||||
|
||||
socket.on('disconnect', this.onDisconnect(id, socket))
|
||||
}
|
||||
|
||||
private onOperation = (id, name) => data => {
|
||||
try {
|
||||
this.connections[id].receiveMsg(data)
|
||||
|
||||
this.saveDoc(name)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
private onDisconnect = (id, socket) => () => {
|
||||
this.connections[id].close()
|
||||
delete this.connections[id]
|
||||
|
||||
socket.leave(id)
|
||||
|
||||
this.garbageNsp()
|
||||
}
|
||||
|
||||
garbageNsp = () => {
|
||||
Object.keys(this.io.nsps)
|
||||
.filter(n => n !== '/')
|
||||
.forEach(nsp => {
|
||||
getClients(this.io, nsp).then((clientsList: any[]) => {
|
||||
if (!clientsList.length) this.removeDoc(nsp)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
removeDoc = async nsp => {
|
||||
const doc = this.docSet.getDoc(nsp)
|
||||
|
||||
if (this.options.onDocumentSave) {
|
||||
await this.options.onDocumentSave(nsp, toJS(doc))
|
||||
}
|
||||
|
||||
this.docSet.removeDoc(nsp)
|
||||
|
||||
delete this.io.nsps[nsp]
|
||||
}
|
||||
|
||||
destroy = async () => {
|
||||
this.io.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default Connection
|
||||
3
packages/backend/src/index.ts
Normal file
3
packages/backend/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Connection from './Connection'
|
||||
|
||||
module.exports = Connection
|
||||
17
packages/backend/src/model.ts
Normal file
17
packages/backend/src/model.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ValueJSON } from 'slate'
|
||||
|
||||
export interface ConnectionOptions {
|
||||
port: number
|
||||
connectOpts?: SocketIO.ServerOptions
|
||||
defaultValue?: ValueJSON
|
||||
saveTreshold?: number
|
||||
onAuthRequest?: (
|
||||
query: Object,
|
||||
socket?: SocketIO.Socket
|
||||
) => Promise<boolean> | boolean
|
||||
onDocumentLoad?: (
|
||||
pathname: string,
|
||||
query?: Object
|
||||
) => ValueJSON | null | false | undefined
|
||||
onDocumentSave?: (pathname: string, json: ValueJSON) => Promise<void> | void
|
||||
}
|
||||
21
packages/backend/src/utils/defaultValue.ts
Normal file
21
packages/backend/src/utils/defaultValue.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ValueJSON } from 'slate'
|
||||
|
||||
const json: ValueJSON = {
|
||||
document: {
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
marks: [],
|
||||
text: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default json
|
||||
13
packages/backend/src/utils/index.ts
Normal file
13
packages/backend/src/utils/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import defaultValue from './defaultValue'
|
||||
|
||||
export const getClients = (io, nsp) =>
|
||||
new Promise((r, j) => {
|
||||
io.of(nsp).clients((e, c) => (e ? j(e) : r(c)))
|
||||
})
|
||||
|
||||
export const defaultOptions = {
|
||||
port: 9000,
|
||||
saveTreshold: 2000
|
||||
}
|
||||
|
||||
export { defaultValue }
|
||||
Reference in New Issue
Block a user