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
|
node_modules
|
||||||
bower_components
|
|
||||||
build
|
|
||||||
build_test
|
|
||||||
.directory
|
|
||||||
.codio
|
|
||||||
.settings
|
|
||||||
.jshintignore
|
|
||||||
.jshintrc
|
|
||||||
.validate.json
|
|
||||||
/y.js
|
|
||||||
/y.js.map
|
|
||||||
/y-*
|
|
||||||
.vscode
|
.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",
|
"name": "y-webrtc",
|
||||||
"version": "8.0.7",
|
"version": "8.0.7",
|
||||||
"description": "WebRTC Connector for Yjs",
|
"description": "WebRTC provider for Yjs",
|
||||||
"main": "./src/WebRTC.js",
|
"main": "./dist/y-webrtc.js",
|
||||||
|
"module": "./src/y-webrtc.js",
|
||||||
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --harmony ./node_modules/.bin/gulp test",
|
"now-start": "node ./bin/server.js",
|
||||||
"lint": "./node_modules/.bin/standard",
|
"dist": "rm -rf dist && rollup -c",
|
||||||
"build": "./node_modules/.bin/gulp build"
|
"lint": "standard",
|
||||||
|
"preversion": "npm run lint && npm run dist"
|
||||||
},
|
},
|
||||||
"pre-commit": [
|
"bin": {
|
||||||
"lint",
|
"y-webrtc-server": "./bin/server.js"
|
||||||
"test"
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"bin/*",
|
||||||
|
"src/*"
|
||||||
],
|
],
|
||||||
"standard": {
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"ignore": [
|
|
||||||
"build/**",
|
|
||||||
"dist/**"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/y-js/y-webrtc.git"
|
"url": "git+https://github.com/yjs/y-webrtc.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Yjs",
|
"Yjs"
|
||||||
"OT",
|
|
||||||
"Collaboration",
|
|
||||||
"Synchronization",
|
|
||||||
"ShareJS",
|
|
||||||
"Coweb",
|
|
||||||
"Concurrency"
|
|
||||||
],
|
],
|
||||||
"author": "Kevin Jahns <kevin.jahns@rwth-aachen.de>",
|
"author": "Kevin Jahns <kevin.jahns@protonmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/y-js/y-webrtc/issues"
|
"url": "https://github.com/yjs/y-webrtc/issues"
|
||||||
},
|
},
|
||||||
"homepage": "http://y-js.org",
|
"homepage": "https://github.com/yjs/y-webrtc#readme",
|
||||||
"dependencies": {
|
"standard": {
|
||||||
"simplewebrtc": "^1.18.0"
|
"ignore": [
|
||||||
|
"/dist",
|
||||||
|
"/node_modules"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gulp-uglify": "^2.0.0",
|
"simple-peer": "^9.6.2",
|
||||||
"babel-eslint": "^4.1.2",
|
"lib0": "^0.1.4",
|
||||||
"babel-plugin-transform-runtime": "^6.1.18",
|
"rollup": "^1.27.8",
|
||||||
"babel-preset-es2015": "^6.1.18",
|
"rollup-cli": "^1.0.9",
|
||||||
"babel-runtime": "^6.1.18",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
"babelify": "^7.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"browserify": "^12.0.1",
|
"rollup-plugin-terser": "^5.1.2",
|
||||||
"gulp": "^3.9.0",
|
"standard": "^12.0.1",
|
||||||
"gulp-babel": "^5.2.1",
|
"y-protocols": "^0.1.0",
|
||||||
"gulp-bump": "^1.0.0",
|
"yjs": "13.0.0-102"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependenies": {
|
||||||
"yjs": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0"
|
"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