rewrite for v13
This commit is contained in:
parent
1c6559b57d
commit
1fac4380fe
14 changed files with 2717 additions and 220 deletions
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -1,14 +1,3 @@
|
|||
dist
|
||||
node_modules
|
||||
bower_components
|
||||
build
|
||||
build_test
|
||||
.directory
|
||||
.codio
|
||||
.settings
|
||||
.jshintignore
|
||||
.jshintrc
|
||||
.validate.json
|
||||
/y.js
|
||||
/y.js.map
|
||||
/y-*
|
||||
.vscode
|
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -1,4 +0,0 @@
|
|||
[submodule "dist"]
|
||||
path = dist
|
||||
url = https://github.com/y-js/y-webrtc.git
|
||||
branch = dist
|
141
bin/server.js
Executable file
141
bin/server.js
Executable file
|
@ -0,0 +1,141 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const ws = require('ws')
|
||||
const http = require('http')
|
||||
const map = require('lib0/dist/map.js')
|
||||
|
||||
const wsReadyStateConnecting = 0
|
||||
const wsReadyStateOpen = 1
|
||||
const wsReadyStateClosing = 2 // eslint-disable-line
|
||||
const wsReadyStateClosed = 3 // eslint-disable-line
|
||||
|
||||
const pingTimeout = 30000
|
||||
|
||||
const port = process.env.PORT || 4444
|
||||
// @ts-ignore
|
||||
const wss = new ws.Server({ noServer: true })
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
response.writeHead(200, { 'Content-Type': 'text/plain' })
|
||||
response.end('okay')
|
||||
})
|
||||
|
||||
/**
|
||||
* Map froms topic-name to set of subscribed clients.
|
||||
* @type {Map<string, Set<any>>}
|
||||
*/
|
||||
const topics = new Map()
|
||||
|
||||
/**
|
||||
* @param {any} conn
|
||||
* @param {object} message
|
||||
*/
|
||||
const send = (conn, message) => {
|
||||
if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
|
||||
conn.close()
|
||||
}
|
||||
try {
|
||||
conn.send(JSON.stringify(message))
|
||||
} catch (e) {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a new client
|
||||
* @param {any} conn
|
||||
*/
|
||||
const onconnection = conn => {
|
||||
/**
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const subscribedTopics = new Set()
|
||||
let closed = false
|
||||
// Check if connection is still alive
|
||||
let pongReceived = true
|
||||
const pingInterval = setInterval(() => {
|
||||
if (!pongReceived) {
|
||||
conn.close()
|
||||
clearInterval(pingInterval)
|
||||
} else {
|
||||
pongReceived = false
|
||||
try {
|
||||
conn.ping()
|
||||
} catch (e) {
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
}, pingTimeout)
|
||||
conn.on('pong', () => {
|
||||
pongReceived = true
|
||||
})
|
||||
conn.on('close', () => {
|
||||
subscribedTopics.forEach(topicName => {
|
||||
const subs = topics.get(topicName) || new Set()
|
||||
subs.delete(conn)
|
||||
if (subs.size === 0) {
|
||||
topics.delete(topicName)
|
||||
}
|
||||
})
|
||||
subscribedTopics.clear()
|
||||
closed = true
|
||||
})
|
||||
conn.on('message', /** @param {object} message */ message => {
|
||||
if (typeof message === 'string') {
|
||||
message = JSON.parse(message)
|
||||
}
|
||||
if (message && message.type && !closed) {
|
||||
switch (message.type) {
|
||||
case 'subscribe':
|
||||
/** @type {Array<string>} */ (message.topics || []).forEach(topicName => {
|
||||
if (typeof topicName === 'string') {
|
||||
// add conn to topic
|
||||
const topic = map.setIfUndefined(topics, topicName, () => new Set())
|
||||
topic.add(conn)
|
||||
// add topic to conn
|
||||
subscribedTopics.add(topicName)
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'unsubscribe':
|
||||
/** @type {Array<string>} */ (message.topics || []).forEach(topicName => {
|
||||
const subs = topics.get(topicName)
|
||||
if (subs) {
|
||||
subs.delete(conn)
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'publish':
|
||||
if (message.topics) {
|
||||
/**
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const receivers = new Set()
|
||||
message.topics.forEach(/** @param {string} topicName */ topicName => {
|
||||
(topics.get(topicName) || []).forEach(sub => receivers.add(sub))
|
||||
})
|
||||
receivers.forEach(receiver => send(receiver, message))
|
||||
}
|
||||
break
|
||||
case 'ping':
|
||||
send(conn, { type: 'pong' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
wss.on('connection', onconnection)
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
// You may check auth of request here..
|
||||
/**
|
||||
* @param {any} ws
|
||||
*/
|
||||
const handleAuth = ws => {
|
||||
wss.emit('connection', ws, request)
|
||||
}
|
||||
wss.handleUpgrade(request, socket, head, handleAuth)
|
||||
})
|
||||
|
||||
server.listen(port)
|
||||
|
||||
console.log('Signalling server running on localhost:', port)
|
11
demo/index.html
Normal file
11
demo/index.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>y-webrtc demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<button type="button" id="y-connect-btn">Disconnect</button>
|
||||
<script type="text/javascript" src="../dist/demo.js"></script>
|
||||
</body>
|
||||
</html>
|
19
demo/index.js
Normal file
19
demo/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* eslint-env browser */
|
||||
|
||||
import * as Y from 'yjs'
|
||||
import { WebrtcProvider } from '../src/y-webrtc.js'
|
||||
|
||||
const ydoc = new Y.Doc()
|
||||
const provider = new WebrtcProvider('prosemirror', ydoc)
|
||||
const yarray = ydoc.get('prosemirror', Y.XmlFragment)
|
||||
|
||||
provider.on('synced', synced => {
|
||||
console.log('synced!', synced)
|
||||
})
|
||||
|
||||
yarray.observeDeep(() => {
|
||||
console.log('yarray updated: ', yarray.toJSON())
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
window.example = { provider, ydoc, yarray }
|
1
dist
1
dist
|
@ -1 +0,0 @@
|
|||
Subproject commit 0f203f1768cb5a206ee14fb0946f5ac218caefe9
|
40
gulpfile.js
40
gulpfile.js
|
@ -1,40 +0,0 @@
|
|||
/* eslint-env node */
|
||||
|
||||
var gulp = require('gulp')
|
||||
var $ = require('gulp-load-plugins')()
|
||||
var runSequence = require('run-sequence').use(gulp)
|
||||
|
||||
require('../yjs/gulpfile.helper.js')(gulp, {
|
||||
polyfills: [],
|
||||
entry: './src/WebRTC.js',
|
||||
targetName: 'y-webrtc.js',
|
||||
moduleName: 'yWebrtc',
|
||||
specs: []
|
||||
})
|
||||
|
||||
gulp.task('default', ['updateSubmodule'], function (cb) {
|
||||
gulp.src('package.json')
|
||||
.pipe($.prompt.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'tasks',
|
||||
message: 'Which tasks would you like to run?',
|
||||
choices: [
|
||||
'test Test this project',
|
||||
'dev:browser Watch files & serve the testsuite for the browser',
|
||||
'dev:nodejs Watch filse & test this project with nodejs',
|
||||
'bump Bump the current state of the project',
|
||||
'publish Publish this project. Creates a github tag',
|
||||
'dist Build the distribution files'
|
||||
]
|
||||
}, function (res) {
|
||||
var tasks = res.tasks.map(function (task) {
|
||||
return task.split(' ')[0]
|
||||
})
|
||||
if (tasks.length > 0) {
|
||||
console.info('gulp ' + tasks.join(' '))
|
||||
runSequence(tasks, cb)
|
||||
} else {
|
||||
console.info('Ok, .. goodbye')
|
||||
}
|
||||
}))
|
||||
})
|
4
now.json
Normal file
4
now.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "y-webrtc",
|
||||
"version": 1
|
||||
}
|
2030
package-lock.json
generated
Normal file
2030
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
105
package.json
105
package.json
|
@ -1,80 +1,63 @@
|
|||
{
|
||||
"name": "y-webrtc",
|
||||
"version": "8.0.7",
|
||||
"description": "WebRTC Connector for Yjs",
|
||||
"main": "./src/WebRTC.js",
|
||||
"description": "WebRTC provider for Yjs",
|
||||
"main": "./dist/y-webrtc.js",
|
||||
"module": "./src/y-webrtc.js",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"test": "node --harmony ./node_modules/.bin/gulp test",
|
||||
"lint": "./node_modules/.bin/standard",
|
||||
"build": "./node_modules/.bin/gulp build"
|
||||
"now-start": "node ./bin/server.js",
|
||||
"dist": "rm -rf dist && rollup -c",
|
||||
"lint": "standard",
|
||||
"preversion": "npm run lint && npm run dist"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint",
|
||||
"test"
|
||||
"bin": {
|
||||
"y-webrtc-server": "./bin/server.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"bin/*",
|
||||
"src/*"
|
||||
],
|
||||
"standard": {
|
||||
"parser": "babel-eslint",
|
||||
"ignore": [
|
||||
"build/**",
|
||||
"dist/**"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/y-js/y-webrtc.git"
|
||||
"url": "git+https://github.com/yjs/y-webrtc.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Yjs",
|
||||
"OT",
|
||||
"Collaboration",
|
||||
"Synchronization",
|
||||
"ShareJS",
|
||||
"Coweb",
|
||||
"Concurrency"
|
||||
"Yjs"
|
||||
],
|
||||
"author": "Kevin Jahns <kevin.jahns@rwth-aachen.de>",
|
||||
"author": "Kevin Jahns <kevin.jahns@protonmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/y-js/y-webrtc/issues"
|
||||
"url": "https://github.com/yjs/y-webrtc/issues"
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"dependencies": {
|
||||
"simplewebrtc": "^1.18.0"
|
||||
"homepage": "https://github.com/yjs/y-webrtc#readme",
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/dist",
|
||||
"/node_modules"
|
||||
]
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"babel-eslint": "^4.1.2",
|
||||
"babel-plugin-transform-runtime": "^6.1.18",
|
||||
"babel-preset-es2015": "^6.1.18",
|
||||
"babel-runtime": "^6.1.18",
|
||||
"babelify": "^7.2.0",
|
||||
"browserify": "^12.0.1",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-babel": "^5.2.1",
|
||||
"gulp-bump": "^1.0.0",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-filter": "^3.0.1",
|
||||
"gulp-git": "^1.6.0",
|
||||
"gulp-jasmine": "^2.0.1",
|
||||
"gulp-jasmine-browser": "^0.2.3",
|
||||
"gulp-load-plugins": "^1.0.0",
|
||||
"gulp-prompt": "^0.1.2",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-serve": "^1.2.0",
|
||||
"gulp-shell": "^0.5.1",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-tag-version": "^1.3.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"minimist": "^1.2.0",
|
||||
"pre-commit": "^1.1.1",
|
||||
"promise-polyfill": "^2.1.0",
|
||||
"run-sequence": "^1.1.4",
|
||||
"standard": "^5.2.2",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
"simple-peer": "^9.6.2",
|
||||
"lib0": "^0.1.4",
|
||||
"rollup": "^1.27.8",
|
||||
"rollup-cli": "^1.0.9",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-terser": "^5.1.2",
|
||||
"standard": "^12.0.1",
|
||||
"y-protocols": "^0.1.0",
|
||||
"yjs": "13.0.0-102"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"yjs": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0"
|
||||
"peerDependenies": {
|
||||
"yjs": ">=13.0.0-102"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ws": "^7.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.x"
|
||||
}
|
||||
}
|
||||
|
|
100
rollup.config.js
Normal file
100
rollup.config.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
// If truthy, it expects all y-* dependencies in the upper directory.
|
||||
// This is only necessary if you want to test and make changes to several repositories.
|
||||
const localImports = process.env.LOCALIMPORTS
|
||||
|
||||
if (localImports) {
|
||||
console.info('Using local imports')
|
||||
}
|
||||
|
||||
const customModules = new Set([
|
||||
'y-websocket',
|
||||
'y-codemirror',
|
||||
'y-ace',
|
||||
'y-textarea',
|
||||
'y-quill',
|
||||
'y-dom',
|
||||
'y-prosemirror',
|
||||
'y-monaco'
|
||||
])
|
||||
/**
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const customLibModules = new Set([
|
||||
'lib0',
|
||||
'y-protocols'
|
||||
])
|
||||
|
||||
// @ts-ignore We use this for debugging
|
||||
const debugResolve = {
|
||||
resolveId (importee) {
|
||||
if (localImports) {
|
||||
if (importee === 'yjs') {
|
||||
return `${process.cwd()}/../yjs/src/index.js`
|
||||
}
|
||||
if (customModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}/src/${importee}.js`
|
||||
}
|
||||
if (customLibModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
const minificationPlugins = process.env.PRODUCTION ? [terser({
|
||||
module: true,
|
||||
compress: {
|
||||
hoist_vars: true,
|
||||
module: true,
|
||||
passes: 1,
|
||||
pure_getters: true,
|
||||
unsafe_comps: true,
|
||||
unsafe_undefined: true
|
||||
},
|
||||
mangle: {
|
||||
toplevel: true
|
||||
}
|
||||
})] : []
|
||||
|
||||
const plugins = [
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
mainFields: ['module', 'browser', 'main']
|
||||
}),
|
||||
commonjs(),
|
||||
...minificationPlugins
|
||||
]
|
||||
|
||||
export default [
|
||||
{
|
||||
input: './demo/index.js',
|
||||
output: [{
|
||||
name: 'demo',
|
||||
file: 'dist/demo.js',
|
||||
format: 'iife',
|
||||
sourcemap: true
|
||||
}],
|
||||
plugins
|
||||
}, {
|
||||
input: './src/y-webrtc.js',
|
||||
external: id => /^(lib0|yjs|y-protocols|simple-peer)/.test(id),
|
||||
output: [{
|
||||
name: 'y-webrtc',
|
||||
file: 'dist/y-webrtc.js',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
paths: path => {
|
||||
if (/^lib0\//.test(path)) {
|
||||
return `lib0/dist${path.slice(4)}`
|
||||
} else if (/^y-protocols\//.test(path)) {
|
||||
return `y-protocols/dist${path.slice(11)}`
|
||||
}
|
||||
return path
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
102
src/WebRTC.js
102
src/WebRTC.js
|
@ -1,102 +0,0 @@
|
|||
/* global Y */
|
||||
'use strict'
|
||||
|
||||
var SimpleWebRTC = require('simplewebrtc')
|
||||
|
||||
function extend (Y) {
|
||||
class WebRTC extends Y.AbstractConnector {
|
||||
constructor (y, options) {
|
||||
if (options === undefined) {
|
||||
throw new Error('Options must not be undefined!')
|
||||
}
|
||||
if (options.room == null) {
|
||||
throw new Error('You must define a room name!')
|
||||
}
|
||||
options.role = 'slave'
|
||||
super(y, options)
|
||||
this.webrtcOptions = {
|
||||
url: options.url || 'https://yjs.dbis.rwth-aachen.de:5078',
|
||||
room: options.room
|
||||
}
|
||||
var swr = new SimpleWebRTC(this.webrtcOptions)
|
||||
this.swr = swr
|
||||
var self = this
|
||||
swr.once('connectionReady', function (userId) {
|
||||
// SimpleWebRTC (swr) is initialized
|
||||
swr.joinRoom(self.webrtcOptions.room)
|
||||
|
||||
swr.once('joinedRoom', function () {
|
||||
self.setUserId(userId)
|
||||
/*
|
||||
var i
|
||||
// notify the connector class about all the users that already
|
||||
// joined the session
|
||||
for(i in self.swr.webrtc.peers){
|
||||
self.userJoined(self.swr.webrtc.peers[i].id, "master")
|
||||
}*/
|
||||
swr.on('channelMessage', function (peer, room_, message) {
|
||||
// The client received a message
|
||||
// Check if the connector is already initialized,
|
||||
// only then forward the message to the connector class
|
||||
if (message.type != null) {
|
||||
self.receiveMessage(peer.id, message.payload)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
swr.on('createdPeer', function (peer) {
|
||||
// a new peer/client joined the session.
|
||||
// Notify the connector class, if the connector
|
||||
// is already initialized
|
||||
self.userJoined(peer.id, 'master')
|
||||
})
|
||||
|
||||
swr.on('peerStreamRemoved', function (peer) {
|
||||
// a client left the session.
|
||||
// Notify the connector class, if the connector
|
||||
// is already initialized
|
||||
self.userLeft(peer.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnect () {
|
||||
this.swr.leaveRoom()
|
||||
super.disconnect()
|
||||
}
|
||||
reconnect () {
|
||||
this.swr.joinRoom(this.webrtcOptions.room)
|
||||
super.reconnect()
|
||||
}
|
||||
send (uid, message) {
|
||||
var self = this
|
||||
// we have to make sure that the message is sent under all circumstances
|
||||
var send = function () {
|
||||
// check if the clients still exists
|
||||
var peer = self.swr.webrtc.getPeers(uid)[0]
|
||||
var success
|
||||
if (peer) {
|
||||
// success is true, if the message is successfully sent
|
||||
success = peer.sendDirectly('simplewebrtc', 'yjs', message)
|
||||
}
|
||||
if (!success) {
|
||||
// resend the message if it didn't work
|
||||
setTimeout(send, 500)
|
||||
}
|
||||
}
|
||||
// try to send the message
|
||||
send()
|
||||
}
|
||||
broadcast (message) {
|
||||
this.swr.sendDirectlyToAll('simplewebrtc', 'yjs', message)
|
||||
}
|
||||
isDisconnected () {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Y.extend('webrtc', WebRTC)
|
||||
}
|
||||
|
||||
module.exports = extend
|
||||
if (typeof Y !== 'undefined') {
|
||||
extend(Y)
|
||||
}
|
305
src/y-webrtc.js
Normal file
305
src/y-webrtc.js
Normal file
|
@ -0,0 +1,305 @@
|
|||
import * as ws from 'lib0/websocket.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as decoding from 'lib0/decoding.js'
|
||||
import { Observable } from 'lib0/observable.js'
|
||||
import * as Y from 'yjs' // eslint-disable-line
|
||||
import Peer from 'simple-peer/simplepeer.min.js'
|
||||
|
||||
import * as syncProtocol from 'y-protocols/sync.js'
|
||||
import * as awarenessProtocol from 'y-protocols/awareness.js'
|
||||
|
||||
const messageSync = 0
|
||||
const messageQueryAwareness = 3
|
||||
const messageAwareness = 1
|
||||
|
||||
/**
|
||||
* @param {WebrtcRoom} webrtcRoom
|
||||
*/
|
||||
const checkIsSynced = webrtcRoom => {
|
||||
let synced = true
|
||||
webrtcRoom.peers.forEach(peer => {
|
||||
if (!peer.syncedRooms.has(webrtcRoom.name)) {
|
||||
synced = false
|
||||
}
|
||||
})
|
||||
if ((!synced && webrtcRoom.synced) || (synced && !webrtcRoom.synced)) {
|
||||
webrtcRoom.synced = synced
|
||||
webrtcRoom.provider.emit('synced', [{ synced }])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SignalingConn} signaling
|
||||
* @param {WebrtcConn} peerConn
|
||||
* @param {Uint8Array} buf
|
||||
* @return {encoding.Encoder?}
|
||||
*/
|
||||
const readPeerMessage = (signaling, peerConn, buf) => {
|
||||
const decoder = decoding.createDecoder(buf)
|
||||
const encoder = encoding.createEncoder()
|
||||
const messageType = decoding.readVarUint(decoder)
|
||||
const roomName = decoding.readVarString(decoder)
|
||||
const webrtcRoom = signaling.rooms.get(roomName)
|
||||
if (webrtcRoom === undefined) {
|
||||
return null
|
||||
}
|
||||
const provider = webrtcRoom.provider
|
||||
const doc = webrtcRoom.doc
|
||||
let sendReply = false
|
||||
switch (messageType) {
|
||||
case messageSync:
|
||||
encoding.writeVarUint(encoder, messageSync)
|
||||
encoding.writeVarString(encoder, roomName)
|
||||
const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, doc, webrtcRoom.provider)
|
||||
if (syncMessageType === syncProtocol.messageYjsSyncStep2 && !webrtcRoom.synced) {
|
||||
peerConn.syncedRooms.add(roomName)
|
||||
checkIsSynced(webrtcRoom)
|
||||
}
|
||||
if (syncMessageType === syncProtocol.messageYjsSyncStep1) {
|
||||
sendReply = true
|
||||
}
|
||||
break
|
||||
case messageQueryAwareness:
|
||||
encoding.writeVarUint(encoder, messageAwareness)
|
||||
encoding.writeVarString(encoder, roomName)
|
||||
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())))
|
||||
sendReply = true
|
||||
break
|
||||
case messageAwareness:
|
||||
awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider)
|
||||
break
|
||||
default:
|
||||
console.error('Unable to compute message')
|
||||
return encoder
|
||||
}
|
||||
if (!sendReply) {
|
||||
// nothing has been written, no answer created
|
||||
return null
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WebrtcConn} webrtcConn
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
const send = (webrtcConn, encoder) => {
|
||||
webrtcConn.peer.send(encoding.toUint8Array(encoder))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WebrtcRoom} webrtcRoom
|
||||
* @param {encoding.Encoder} encoder
|
||||
*/
|
||||
const broadcast = (webrtcRoom, encoder) => {
|
||||
const m = encoding.toUint8Array(encoder)
|
||||
webrtcRoom.peers.forEach(peer => peer.peer.send(m))
|
||||
}
|
||||
|
||||
export class WebrtcConn {
|
||||
/**
|
||||
* @param {SignalingConn} signalingConn
|
||||
* @param {boolean} initiator
|
||||
* @param {string} remotePeerId
|
||||
* @param {Array<string>} announcedTopics
|
||||
*/
|
||||
constructor (signalingConn, initiator, remotePeerId, announcedTopics) {
|
||||
this.remotePeerId = remotePeerId
|
||||
this.closed = false
|
||||
this.connected = false
|
||||
/**
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
this.syncedRooms = new Set()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
this.peer = new Peer({ initiator })
|
||||
this.peer.on('signal', data => {
|
||||
signalingConn.send({ type: 'publish', topics: announcedTopics, to: remotePeerId, from: signalingConn.peerId, messageType: 'signal', data })
|
||||
})
|
||||
this.peer.on('connect', () => {
|
||||
this.connected = true
|
||||
announcedTopics.forEach(roomName => {
|
||||
const room = signalingConn.rooms.get(roomName)
|
||||
if (room) {
|
||||
// add peer to room
|
||||
room.peers.add(this)
|
||||
// send sync step 1
|
||||
const provider = room.provider
|
||||
const doc = provider.doc
|
||||
const awareness = provider.awareness
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarUint(encoder, messageSync)
|
||||
encoding.writeVarString(encoder, room.name)
|
||||
syncProtocol.writeSyncStep1(encoder, doc)
|
||||
send(this, encoder)
|
||||
const awarenessStates = awareness.getStates()
|
||||
if (awarenessStates.size > 0) {
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarUint(encoder, messageAwareness)
|
||||
encoding.writeVarString(encoder, room.name)
|
||||
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awarenessStates.keys())))
|
||||
send(this, encoder)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
this.peer.on('close', () => {
|
||||
this.connected = false
|
||||
this.closed = true
|
||||
signalingConn.conns.delete(this.remotePeerId)
|
||||
signalingConn.rooms.forEach(room => {
|
||||
room.peers.delete(this)
|
||||
checkIsSynced(room)
|
||||
})
|
||||
this.peer.destroy()
|
||||
})
|
||||
this.peer.on('error', () => {
|
||||
this.connected = false
|
||||
this.closed = true
|
||||
})
|
||||
this.peer.on('data', data => {
|
||||
const answer = readPeerMessage(signalingConn, this, data)
|
||||
if (answer !== null) {
|
||||
send(this, answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class WebrtcRoom {
|
||||
/**
|
||||
* @param {Y.Doc} doc
|
||||
* @param {WebrtcProvider} provider
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor (doc, provider, name) {
|
||||
/**
|
||||
* @type {Set<WebrtcConn>}
|
||||
*/
|
||||
this.peers = new Set()
|
||||
this.doc = doc
|
||||
this.provider = provider
|
||||
this.synced = false
|
||||
this.name = name
|
||||
}
|
||||
}
|
||||
|
||||
export class SignalingConn extends ws.WebsocketClient {
|
||||
constructor (url) {
|
||||
super(url)
|
||||
this.peerId = random.uuidv4()
|
||||
/**
|
||||
* @type {Map<string,WebrtcRoom>}
|
||||
*/
|
||||
this.rooms = new Map()
|
||||
/**
|
||||
* @type {Map<string,WebrtcConn>}
|
||||
*/
|
||||
this.conns = new Map()
|
||||
this.afterOpen.push(() => ({ type: 'subscribe', topics: Array.from(this.rooms.keys()) }))
|
||||
this.afterOpen.push(() => ({ type: 'publish', messageType: 'announce', topics: Array.from(this.rooms.keys()), from: this.peerId }))
|
||||
this.on('message', m => {
|
||||
if (m.from === this.peerId || (m.to !== undefined && m.to !== this.peerId)) {
|
||||
return
|
||||
}
|
||||
switch (m.type) {
|
||||
case 'publish': {
|
||||
switch (m.messageType) {
|
||||
case 'announce':
|
||||
map.setIfUndefined(this.conns, m.from, () => new WebrtcConn(this, true, m.from, m.topics))
|
||||
break
|
||||
case 'signal':
|
||||
if (m.to === this.peerId) {
|
||||
map.setIfUndefined(this.conns, m.from, () => new WebrtcConn(this, false, m.from, m.topics)).peer.signal(m.data)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Map<string, SignalingConn>}
|
||||
*/
|
||||
const conns = new Map()
|
||||
|
||||
/**
|
||||
* @extends Observable<string>
|
||||
*/
|
||||
export class WebrtcProvider extends Observable {
|
||||
/**
|
||||
* @param {string} room
|
||||
* @param {Y.Doc} doc
|
||||
* @param {Object} [opts]
|
||||
* @param {string} [opts.url]
|
||||
*/
|
||||
constructor (room, doc, { url = 'wss://y-webrtc-rgksxuhaol.now.sh' } = {}) {
|
||||
super()
|
||||
this.url = url
|
||||
this.room = room
|
||||
this.doc = doc
|
||||
this.conn = map.setIfUndefined(conns, url, () => new SignalingConn(url))
|
||||
if (this.conn.rooms.has(room)) {
|
||||
throw error.create('A Yjs Doc connected to that room already exists!')
|
||||
}
|
||||
if (this.conn.connected) {
|
||||
this.conn.send({ type: 'subscribe', topics: [room] })
|
||||
}
|
||||
const webrtcRoom = new WebrtcRoom(doc, this, room)
|
||||
this.conn.rooms.set(room, webrtcRoom)
|
||||
/**
|
||||
* @type {awarenessProtocol.Awareness}
|
||||
*/
|
||||
this.awareness = new awarenessProtocol.Awareness(doc)
|
||||
/**
|
||||
* Listens to Yjs updates and sends them to remote peers
|
||||
*
|
||||
* @param {Uint8Array} update
|
||||
* @param {any} origin
|
||||
*/
|
||||
this._docUpdateHandler = (update, origin) => {
|
||||
if (origin !== this || origin === null) {
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarUint(encoder, messageSync)
|
||||
encoding.writeVarString(encoder, room)
|
||||
syncProtocol.writeUpdate(encoder, update)
|
||||
broadcast(webrtcRoom, encoder)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Listens to Awareness updates and sends them to remote peers
|
||||
*
|
||||
* @param {any} changed
|
||||
* @param {any} origin
|
||||
*/
|
||||
this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
|
||||
const changedClients = added.concat(updated).concat(removed)
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarUint(encoder, messageAwareness)
|
||||
encoding.writeVarString(encoder, room)
|
||||
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients))
|
||||
broadcast(webrtcRoom, encoder)
|
||||
}
|
||||
this.doc.on('update', this._docUpdateHandler)
|
||||
this.awareness.on('change', this._awarenessUpdateHandler)
|
||||
window.addEventListener('beforeunload', () => {
|
||||
awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], 'window unload')
|
||||
})
|
||||
}
|
||||
destroy () {
|
||||
if (this.conn.connected) {
|
||||
this.conn.send({ type: 'unsubscribe', topics: [this.room] })
|
||||
}
|
||||
this.conn.rooms.delete(this.room)
|
||||
this.doc.off('update', this._docUpdateHandler)
|
||||
this.awareness.off('change', this._awarenessUpdateHandler)
|
||||
super.destroy()
|
||||
}
|
||||
}
|
62
tsconfig.json
Normal file
62
tsconfig.json
Normal file
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2018",
|
||||
"lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./build", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
"noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
"paths": {
|
||||
"yjs": ["../yjs/src/index.js"],
|
||||
"lib0/*": ["../lib0/*"]
|
||||
},
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"maxNodeModuleJsDepth": 5
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue