-
-
Notifications
You must be signed in to change notification settings - Fork 119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
extend simple peer (not peerjs, oops) to handle buffered/packet transmission; add raw dependency w/MIT license #25
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import * as Y from 'yjs' // eslint-disable-line | ||
import Peer from 'simple-peer/simplepeer.min.js' | ||
const { Int64BE } = require('./int64-buffer.min.js') | ||
|
||
export const CHUNK_SIZE = (1024 * 16) - 512 // 16KB - data header | ||
export const TX_SEND_TTL = 1000 * 30 // 30 seconds | ||
export const MAX_BUFFERED_AMOUNT = 64 * 1024 // simple peer value | ||
|
||
function concatenate (Constructor, arrays) { | ||
let totalLength = 0 | ||
for (const arr of arrays) totalLength += arr.length | ||
const result = new Constructor(totalLength) | ||
let offset = 0 | ||
for (const arr of arrays) { | ||
result.set(arr, offset) | ||
offset += arr.length | ||
} | ||
return result | ||
} | ||
|
||
class SimplePeerExtended extends Peer { | ||
constructor (opts) { | ||
super(opts) | ||
this._opts = opts | ||
this._txOrdinal = 0 | ||
this._rxPackets = [] | ||
this._txPause = false | ||
this.webRTCMessageQueue = [] | ||
this.webRTCPaused = false | ||
} | ||
|
||
encodePacket ({ chunk, txOrd, index, length, totalSize, chunkSize }) { | ||
const encoded = concatenate(Uint8Array, [ | ||
new Uint8Array(new Int64BE(txOrd).toArrayBuffer()), // 8 bytes | ||
new Uint8Array(new Int64BE(index).toArrayBuffer()), // 8 bytes | ||
new Uint8Array(new Int64BE(length).toArrayBuffer()), // 8 bytes | ||
new Uint8Array(new Int64BE(totalSize).toArrayBuffer()), // 8 bytes | ||
new Uint8Array(new Int64BE(chunkSize).toArrayBuffer()), // 8 bytes | ||
chunk // CHUNK_SIZE | ||
]) | ||
return encoded | ||
} | ||
|
||
decodePacket (array) { | ||
return { | ||
txOrd: new Int64BE(array.slice(0, 8)).toNumber(), | ||
index: new Int64BE(array.slice(8, 16)).toNumber(), | ||
length: new Int64BE(array.slice(16, 24)).toNumber(), | ||
totalSize: new Int64BE(array.slice(24, 32)).toNumber(), | ||
chunkSize: new Int64BE(array.slice(32, 40)).toNumber(), | ||
chunk: array.slice(40) | ||
} | ||
} | ||
|
||
packetArray (array, size) { | ||
const txOrd = this._txOrdinal | ||
this._txOrdinal++ | ||
const chunkedArr = [] | ||
const totalSize = array.length || array.byteLength | ||
let index = 0 | ||
while (index < totalSize) { | ||
chunkedArr.push(array.slice(index, size + index)) | ||
index += size | ||
} | ||
return chunkedArr.map((chunk, index) => { | ||
return this.encodePacket({ | ||
chunk, | ||
txOrd, | ||
index, | ||
totalSize, | ||
length: chunkedArr.length, | ||
chunkSize: chunk.byteLength | ||
}) | ||
}) | ||
} | ||
|
||
_onChannelMessage (event) { | ||
const { data } = event | ||
const packet = this.decodePacket(data) | ||
if (packet.chunk instanceof ArrayBuffer) packet.chunk = new Uint8Array(packet.chunk) | ||
if (packet.chunkSize === packet.totalSize) { | ||
this.push(packet.chunk) | ||
} else { | ||
const data = this._rxPackets.filter((p) => p.txOrd === packet.txOrd) | ||
data.push(packet) | ||
const indices = data.map(p => p.index) | ||
if (new Set(indices).size === packet.length) { | ||
data.sort(this.sortPacketArray) | ||
const chunks = concatenate(Uint8Array, data.map(p => p.chunk)) | ||
this.push(chunks) | ||
setTimeout(() => { this._rxPackets = this._rxPackets.filter((p) => p.txOrd !== packet.txOrd) }, TX_SEND_TTL) | ||
} else { | ||
this._rxPackets.push(packet) | ||
} | ||
} | ||
} | ||
|
||
sortPacketArray (a, b) { return a.index > b.index ? 1 : -1 } | ||
send (chunk) { | ||
if (chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk) | ||
const chunks = this.packetArray(chunk, CHUNK_SIZE) | ||
this.webRTCMessageQueue = this.webRTCMessageQueue.concat(chunks) | ||
if (this.webRTCPaused) return | ||
this.sendMessageQueued() | ||
} | ||
|
||
sendMessageQueued () { | ||
this.webRTCPaused = false | ||
let message = this.webRTCMessageQueue.shift() | ||
while (message) { | ||
if (this._channel.bufferedAmount && this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { | ||
this.webRTCPaused = true | ||
this.webRTCMessageQueue.unshift(message) | ||
const listener = () => { | ||
this._channel.removeEventListener('bufferedamountlow', listener) | ||
this.sendMessageQueued() | ||
} | ||
this._channel.addEventListener('bufferedamountlow', listener) | ||
return | ||
} | ||
try { | ||
super.send(message) | ||
message = this.webRTCMessageQueue.shift() | ||
} catch (error) { | ||
console.warn(error) | ||
} | ||
} | ||
} | ||
} | ||
|
||
export default SimplePeerExtended |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,9 @@ import * as math from 'lib0/math.js' | |
import { createMutex } from 'lib0/mutex.js' | ||
|
||
import * as Y from 'yjs' // eslint-disable-line | ||
import Peer from 'simple-peer/simplepeer.min.js' | ||
|
||
// import Peer from 'simple-peer/simplepeer.min.js' | ||
import Peer from './SimplePeerExtended' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting. So you implemented a layer around simple-peer that handles this issue. Would it be possible that you publish a separate package that we can include as a polyfill for the default implementation? This is already done in y-websocket where we define the new WebsocketProvider(URL, room, { WebSocket: MyCustomWebsocketPolyfill }) We could do something similar to y-webrtc without breaking the existing API. new WebrtcProvider(room, yjs, { SimplePeer: SimplePeerExtended }) I'd prefer that approach because it allows us to test out your implementation before breaking everyone else's existing deployments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i can publish a module and put something up on github, but it seems like you'd need move SimplePeer to a peerDependency to avoid duplicating code, otherwise your library is pulling in Peer while your user is also pulling in a modified peer. Also, it looks like I can't figure out how you'd thread that option into the proper slot because it call comes from a global |
||
|
||
import * as syncProtocol from 'y-protocols/sync.js' | ||
import * as awarenessProtocol from 'y-protocols/awareness.js' | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the reason why you don't use BigUint64Array is that it is not yet supported in Safari.
I developed an efficient encoder exactly for this problem: https://github.com/dmonad/lib0/blob/main/encoding.js
The documentation for other encoding techniques is here: https://github.com/dmonad/lib0
But I realize that this is already working in the current state. But maybe we can avoid pulling in more dependencies that are superflous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a header, we need to pad the values. Note the
// 8 bytes comments
I've mocked up a replacement class like:However, when I test the recommended dependency, we get:
its not obvious to me how to do pad/unpad safely, so the whole decode/encode would need a rewrite