first commit
This commit is contained in:
commit
ce309533f4
|
@ -0,0 +1,23 @@
|
|||
name: Build Status
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/*]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
sandbox.js
|
||||
sandbox
|
||||
coverage
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Mathias Buus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,17 @@
|
|||
# protomux
|
||||
|
||||
Multiplex multiple message oriented protocols over a stream
|
||||
|
||||
```
|
||||
npm install protomux
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
``` js
|
||||
const Protomux = require('protomux')
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,305 @@
|
|||
const c = require('compact-encoding')
|
||||
const m = require('./messages')
|
||||
|
||||
class Protocol {
|
||||
constructor (muxer, offset, protocol) {
|
||||
this.muxer = muxer
|
||||
this.stream = muxer.stream
|
||||
this.start = offset
|
||||
this.end = offset + protocol.messages.length
|
||||
this.name = protocol.name
|
||||
this.version = protocol.version || { major: 0, minor: 0 }
|
||||
this.messages = protocol.messages.length
|
||||
this.remoteOpened = false
|
||||
this.removed = false
|
||||
this.encodings = protocol.messages
|
||||
this.onmessage = noop
|
||||
this.onopen = noop
|
||||
this.onclose = noop
|
||||
}
|
||||
|
||||
get corked () {
|
||||
return this.muxer.corked
|
||||
}
|
||||
|
||||
cork () {
|
||||
this.muxer.cork()
|
||||
}
|
||||
|
||||
uncork () {
|
||||
this.muxer.uncork()
|
||||
}
|
||||
|
||||
send (type, message) {
|
||||
const t = this.start + type
|
||||
const enc = this.encodings[type]
|
||||
|
||||
if (this.muxer.corked > 0) {
|
||||
this.muxer._batch.push({ type: t, encoding: enc, message })
|
||||
return false
|
||||
}
|
||||
|
||||
const state = { start: 0, end: 0, buffer: null }
|
||||
|
||||
c.uint.preencode(state, t)
|
||||
enc.preencode(state, message)
|
||||
|
||||
state.buffer = this.stream.alloc(state.end)
|
||||
|
||||
c.uint.encode(state, t)
|
||||
enc.encode(state, message)
|
||||
|
||||
return this.stream.write(state.buffer)
|
||||
}
|
||||
|
||||
recv (type, state) {
|
||||
this.onmessage(type, this.encodings[type].decode(state))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class Protomux {
|
||||
constructor (stream, protocols, opts = {}) {
|
||||
this.stream = stream
|
||||
|
||||
this.protocols = []
|
||||
this.offset = 2
|
||||
|
||||
this.remoteProtocols = []
|
||||
this.remoteOffset = 2
|
||||
|
||||
this.remoteHandshake = null
|
||||
this.onhandshake = noop
|
||||
|
||||
this.corked = 0
|
||||
|
||||
this._batch = null
|
||||
this._unmatchedProtocols = []
|
||||
|
||||
for (const p of protocols) this.addProtocol(p)
|
||||
|
||||
this.stream.on('data', this._ondata.bind(this))
|
||||
|
||||
if (opts.cork) this.cork()
|
||||
this._sendHandshake()
|
||||
}
|
||||
|
||||
remoteOpened (name) {
|
||||
for (const p of this.remoteProtocols) {
|
||||
if (p.local.name === name) return true
|
||||
}
|
||||
for (const { remote } of this._unmatchedProtocols) {
|
||||
if (remote.name === name) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
addProtocol (p) {
|
||||
const local = new Protocol(this, this.offset, p)
|
||||
|
||||
this.protocols.push(local)
|
||||
this.offset += p.messages.length
|
||||
|
||||
for (let i = 0; i < this._unmatchedProtocols.length; i++) {
|
||||
const { start, remote } = this._unmatchedProtocols[i]
|
||||
if (remote.name !== p.name || remote.version.major !== local.version.major) continue
|
||||
local.remoteOpened = true
|
||||
this._unmatchedProtocols.splice(i, 1)
|
||||
const end = start + Math.min(p.messages, local.messages)
|
||||
this.remoteProtocols.push({ local, remote, start, end })
|
||||
break
|
||||
}
|
||||
|
||||
return local
|
||||
}
|
||||
|
||||
removeProtocol (p) {
|
||||
for (let i = 0; i < this.protocols.length; i++) {
|
||||
const local = this.protocols[i]
|
||||
if (local.name !== p.name || local.version.major !== p.version.major) continue
|
||||
p.removed = true
|
||||
this.protocols.splice(i, 1)
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.remoteProtocols.length; i++) {
|
||||
const { local, remote, start } = this.remoteProtocols[i]
|
||||
if (local.name !== p.name || local.version.major !== p.version.major) continue
|
||||
this.remoteProtocols.splice(i, 1)
|
||||
this._unmatchedProtocols.push({ start, remote })
|
||||
}
|
||||
}
|
||||
|
||||
addRemoteProtocol (p) {
|
||||
const local = this.get(p.name)
|
||||
const start = this.remoteOffset
|
||||
|
||||
this.remoteOffset += p.messages
|
||||
|
||||
if (!local || local.version.major !== p.version.major) {
|
||||
this._unmatchedProtocols.push({ start, remote: p })
|
||||
return
|
||||
}
|
||||
|
||||
if (local.remoteOpened) {
|
||||
this.destroy(new Error('Remote sent duplicate protocols'))
|
||||
return
|
||||
}
|
||||
|
||||
const end = start + Math.min(p.messages, local.messages)
|
||||
|
||||
this.remoteProtocols.push({ local, remote: p, start, end })
|
||||
|
||||
local.remoteOpened = true
|
||||
local.onopen()
|
||||
}
|
||||
|
||||
removeRemoteProtocol (p) {
|
||||
for (let i = 0; i < this.remoteProtocols.length; i++) {
|
||||
const { local } = this.remoteProtocols[i]
|
||||
if (local.name !== p.name || local.version.major !== p.version.major) continue
|
||||
this.remoteProtocols.splice(i, 1)
|
||||
local.remoteOpened = false
|
||||
local.onclose()
|
||||
break
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._unmatchedProtocols.length; i++) {
|
||||
const { remote } = this._unmatchedProtocols[i]
|
||||
if (remote.name !== p.name || remote.version.major !== p.version.major) continue
|
||||
this._unmatchedProtocols.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ondata (buffer) {
|
||||
const state = { start: 0, end: buffer.byteLength, buffer }
|
||||
|
||||
try {
|
||||
this._recv(state, false)
|
||||
} catch (err) {
|
||||
this.destroy(err)
|
||||
}
|
||||
}
|
||||
|
||||
_recv (state) {
|
||||
const t = c.uint.decode(state)
|
||||
|
||||
if (t < 2) {
|
||||
if (t === 0) {
|
||||
this._recvBatch(state)
|
||||
} else {
|
||||
this._recvHandshake(state)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.remoteProtocols.length; i++) {
|
||||
const p = this.remoteProtocols[i]
|
||||
|
||||
if (p.start <= t && t <= p.end) {
|
||||
p.local.recv(t - p.start, state)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
state.start = state.end
|
||||
}
|
||||
|
||||
_recvBatch (state) {
|
||||
const end = state.end
|
||||
|
||||
while (state.start < state.end) {
|
||||
const len = c.uint.decode(state)
|
||||
state.end = state.start + len
|
||||
this._recv(state, true)
|
||||
state.end = end
|
||||
}
|
||||
}
|
||||
|
||||
_recvHandshake (state) {
|
||||
if (this.remoteHandshake !== null) {
|
||||
this.destroy(new Error('Double handshake'))
|
||||
return
|
||||
}
|
||||
|
||||
this.remoteHandshake = m.handshake.decode(state)
|
||||
for (const p of this.remoteHandshake.protocols) this.addRemoteProtocol(p)
|
||||
|
||||
this.onhandshake(this.remoteHandshake)
|
||||
}
|
||||
|
||||
destroy (err) {
|
||||
this.stream.destroy(err)
|
||||
}
|
||||
|
||||
get (name) {
|
||||
for (const p of this.protocols) {
|
||||
if (p.name === name) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
cork () {
|
||||
if (++this.corked === 1) this._batch = []
|
||||
}
|
||||
|
||||
uncork () {
|
||||
if (--this.corked !== 0) return
|
||||
|
||||
const batch = this._batch
|
||||
this._batch = null
|
||||
|
||||
const state = { start: 0, end: 1, buffer: null }
|
||||
const lens = new Array(batch.length)
|
||||
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
const b = batch[i]
|
||||
const end = state.end
|
||||
|
||||
c.uint.preencode(state, b.type)
|
||||
b.encoding.preencode(state, b.message)
|
||||
c.uint.preencode(state, lens[i] = (state.end - end))
|
||||
}
|
||||
|
||||
state.buffer = this.stream.alloc(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
const b = batch[i]
|
||||
|
||||
c.uint.encode(state, lens[i])
|
||||
c.uint.encode(state, b.type)
|
||||
b.encoding.encode(state, b.message)
|
||||
}
|
||||
|
||||
this.stream.write(state.buffer)
|
||||
}
|
||||
|
||||
sendKeepAlive () {
|
||||
this.stream.write(this.stream.alloc(0))
|
||||
}
|
||||
|
||||
_sendHandshake () {
|
||||
const hs = {
|
||||
protocols: this.protocols
|
||||
}
|
||||
|
||||
if (this.corked > 0) {
|
||||
this._batch.push({ type: 0, encoding: m.handshake, message: hs })
|
||||
return
|
||||
}
|
||||
|
||||
const state = { start: 0, end: 0, buffer: null }
|
||||
|
||||
c.uint.preencode(state, 1)
|
||||
m.handshake.preencode(state, hs)
|
||||
|
||||
state.buffer = this.stream.alloc(state.end)
|
||||
|
||||
c.uint.encode(state, 1)
|
||||
m.handshake.encode(state, hs)
|
||||
|
||||
this.stream.write(state.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
function noop () {}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "protomux",
|
||||
"version": "0.0.0",
|
||||
"description": "Multiplex multiple message oriented protocols over a stream",
|
||||
"main": "index.js",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"brittle": "^1.6.0",
|
||||
"standard": "^16.0.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mafintosh/protomux.git"
|
||||
},
|
||||
"author": "Mathias Buus (@mafintosh)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mafintosh/protomux/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mafintosh/protomux"
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
const Protomux = require('./')
|
||||
const SecretStream = require('@hyperswarm/secret-stream')
|
||||
const test = require('brittle')
|
||||
const c = require('compact-encoding')
|
||||
|
||||
test('basic', function (t) {
|
||||
const a = new Protomux(new SecretStream(true), [{
|
||||
name: 'foo',
|
||||
messages: [c.string]
|
||||
}])
|
||||
|
||||
const b = new Protomux(new SecretStream(false), [{
|
||||
name: 'foo',
|
||||
messages: [c.string]
|
||||
}])
|
||||
|
||||
replicate(a, b)
|
||||
|
||||
const ap = a.get('foo')
|
||||
const bp = b.get('foo')
|
||||
|
||||
t.plan(3)
|
||||
|
||||
ap.onopen = function () {
|
||||
t.pass('a opened')
|
||||
}
|
||||
|
||||
ap.onmessage = function (type, message) {
|
||||
t.is(type, 0)
|
||||
t.is(message, 'hello world')
|
||||
}
|
||||
|
||||
bp.send(0, 'hello world')
|
||||
})
|
||||
|
||||
test('echo message', function (t) {
|
||||
const a = new Protomux(new SecretStream(true), [{
|
||||
name: 'foo',
|
||||
messages: [c.string]
|
||||
}])
|
||||
|
||||
const b = new Protomux(new SecretStream(false), [{
|
||||
name: 'other',
|
||||
messages: [c.bool, c.bool]
|
||||
}, {
|
||||
name: 'foo',
|
||||
messages: [c.string]
|
||||
}])
|
||||
|
||||
replicate(a, b)
|
||||
|
||||
const ap = a.get('foo')
|
||||
const bp = b.get('foo')
|
||||
|
||||
t.plan(3)
|
||||
|
||||
ap.onmessage = function (type, message) {
|
||||
ap.send(type, 'echo: ' + message)
|
||||
}
|
||||
|
||||
bp.send(0, 'hello world')
|
||||
|
||||
bp.onopen = function () {
|
||||
t.pass('b opened')
|
||||
}
|
||||
|
||||
bp.onmessage = function (type, message) {
|
||||
t.is(type, 0)
|
||||
t.is(message, 'echo: hello world')
|
||||
}
|
||||
})
|
||||
|
||||
test('multi message', function (t) {
|
||||
const a = new Protomux(new SecretStream(true), [{
|
||||
name: 'other',
|
||||
messages: [c.bool, c.bool]
|
||||
}, {
|
||||
name: 'multi',
|
||||
messages: [c.int, c.string, c.string]
|
||||
}])
|
||||
|
||||
const b = new Protomux(new SecretStream(false), [{
|
||||
name: 'multi',
|
||||
messages: [c.int, c.string]
|
||||
}])
|
||||
|
||||
replicate(a, b)
|
||||
|
||||
t.plan(4)
|
||||
|
||||
const ap = a.get('multi')
|
||||
const bp = b.get('multi')
|
||||
|
||||
ap.send(0, 42)
|
||||
ap.send(1, 'a string with 42')
|
||||
ap.send(2, 'should be ignored')
|
||||
|
||||
const expected = [
|
||||
[0, 42],
|
||||
[1, 'a string with 42']
|
||||
]
|
||||
|
||||
bp.onmessage = function (type, message) {
|
||||
const e = expected.shift()
|
||||
t.is(type, e[0])
|
||||
t.is(message, e[1])
|
||||
}
|
||||
})
|
||||
|
||||
// test('corks', function (t) {
|
||||
// const a = new Protomux(new SecretStream(true), [{
|
||||
// name: 'other',
|
||||
// messages: [c.bool, c.bool]
|
||||
// }, {
|
||||
// name: 'multi',
|
||||
// messages: [c.int, c.string]
|
||||
// }])
|
||||
|
||||
// const b = new Protomux(new SecretStream(false), [{
|
||||
// name: 'multi',
|
||||
// messages: [c.int, c.string]
|
||||
// }])
|
||||
|
||||
// replicate(a, b)
|
||||
|
||||
// t.plan(4)
|
||||
|
||||
// const ap = a.get('multi')
|
||||
// const bp = b.get('multi')
|
||||
|
||||
// // ap.cork()
|
||||
// // ap.send(0, 1)
|
||||
// // ap.send(0, 2)
|
||||
// // ap.send(0, 3)
|
||||
// // ap.uncork()
|
||||
|
||||
// bp.onmessage = function (type, message) {
|
||||
// console.log(type, message)
|
||||
// }
|
||||
// })
|
||||
|
||||
function replicate (a, b) {
|
||||
a.stream.rawStream.pipe(b.stream.rawStream).pipe(a.stream.rawStream)
|
||||
}
|
Reference in New Issue