@libp2p/multistream-select 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +290 -0
- package/dist/src/constants.d.ts +2 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +2 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/handle.d.ts +7 -0
- package/dist/src/handle.d.ts.map +1 -0
- package/dist/src/handle.js +35 -0
- package/dist/src/handle.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +42 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/ls.d.ts +7 -0
- package/dist/src/ls.d.ts.map +1 -0
- package/dist/src/ls.js +32 -0
- package/dist/src/ls.js.map +1 -0
- package/dist/src/multistream.d.ts +16 -0
- package/dist/src/multistream.d.ts.map +1 -0
- package/dist/src/multistream.js +57 -0
- package/dist/src/multistream.js.map +1 -0
- package/dist/src/select.d.ts +7 -0
- package/dist/src/select.d.ts.map +1 -0
- package/dist/src/select.js +48 -0
- package/dist/src/select.js.map +1 -0
- package/dist/test/dialer.spec.d.ts +2 -0
- package/dist/test/dialer.spec.d.ts.map +1 -0
- package/dist/test/dialer.spec.js +144 -0
- package/dist/test/dialer.spec.js.map +1 -0
- package/dist/test/integration.spec.d.ts +2 -0
- package/dist/test/integration.spec.d.ts.map +1 -0
- package/dist/test/integration.spec.js +55 -0
- package/dist/test/integration.spec.js.map +1 -0
- package/dist/test/listener.spec.d.ts +2 -0
- package/dist/test/listener.spec.d.ts.map +1 -0
- package/dist/test/listener.spec.js +123 -0
- package/dist/test/listener.spec.js.map +1 -0
- package/dist/test/multistream.spec.d.ts +2 -0
- package/dist/test/multistream.spec.d.ts.map +1 -0
- package/dist/test/multistream.spec.js +86 -0
- package/dist/test/multistream.spec.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/constants.ts +2 -0
- package/src/handle.ts +43 -0
- package/src/index.ts +58 -0
- package/src/ls.ts +45 -0
- package/src/multistream.ts +82 -0
- package/src/select.ts +59 -0
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@libp2p/multistream-select",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
|
|
5
|
+
"description": "JavaScript implementation of multistream-select",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "aegir lint",
|
|
14
|
+
"dep-check": "aegir dep-check dist/src/**/*.js dist/test/**/*.js",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"pretest": "npm run build",
|
|
17
|
+
"test": "aegir test -f ./dist/test/*.js -f ./dist/test/**/*.js",
|
|
18
|
+
"test:chrome": "npm run test -- -t browser",
|
|
19
|
+
"test:chrome-webworker": "npm run test -- -t webworker",
|
|
20
|
+
"test:firefox": "npm run test -- -t browser -- --browser firefox",
|
|
21
|
+
"test:firefox-webworker": "npm run test -- -t webworker -- --browser firefox",
|
|
22
|
+
"test:node": "npm run test -- -t node --cov",
|
|
23
|
+
"test:electron-main": "npm run test -- -t electron-main"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/multiformats/js-multistream-select.git"
|
|
28
|
+
},
|
|
29
|
+
"pre-push": [
|
|
30
|
+
"lint"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"stream",
|
|
34
|
+
"multistream",
|
|
35
|
+
"protocol",
|
|
36
|
+
"ipfs",
|
|
37
|
+
"libp2p"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/multiformats/js-multistream-select/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/multiformats/js-multistream-select#readme",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@libp2p/interfaces": "^1.3.6",
|
|
46
|
+
"it-stream-types": "^1.0.4",
|
|
47
|
+
"abortable-iterator": "^4.0.2",
|
|
48
|
+
"@libp2p/logger": "^1.0.3",
|
|
49
|
+
"err-code": "^3.0.1",
|
|
50
|
+
"it-first": "^1.0.6",
|
|
51
|
+
"it-handshake": "^3.0.0",
|
|
52
|
+
"it-length-prefixed": "^7.0.0",
|
|
53
|
+
"it-pipe": "^2.0.3",
|
|
54
|
+
"it-reader": "^5.0.0",
|
|
55
|
+
"p-defer": "^4.0.0",
|
|
56
|
+
"uint8arrays": "^3.0.0",
|
|
57
|
+
"uint8arraylist": "^1.2.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"aegir": "^36.1.1",
|
|
61
|
+
"it-pair": "^2.0.2",
|
|
62
|
+
"p-timeout": "^5.0.2",
|
|
63
|
+
"timeout-abort-controller": "^3.0.0",
|
|
64
|
+
"util": "^0.12.4",
|
|
65
|
+
"varint": "^6.0.0"
|
|
66
|
+
},
|
|
67
|
+
"contributors": [
|
|
68
|
+
"David Dias <daviddias.p@gmail.com>",
|
|
69
|
+
"Jacob Heun <jacobheun@gmail.com>",
|
|
70
|
+
"Alex Potsides <alex@achingbrain.net>",
|
|
71
|
+
"dignifiedquire <dignifiedquire@gmail.com>",
|
|
72
|
+
"Victor Bjelkholm <victorbjelkholm@gmail.com>",
|
|
73
|
+
"Richard Littauer <richard.littauer@gmail.com>",
|
|
74
|
+
"Vasco Santos <vasco.santos@moxy.studio>",
|
|
75
|
+
"Alan Shaw <alan.shaw@protocol.ai>",
|
|
76
|
+
"Hugo Dias <mail@hugodias.me>",
|
|
77
|
+
"Didrik Nordström <didrik.nordstrom@gmail.com>",
|
|
78
|
+
"Projjol Banerji <probaner23@gmail.com>"
|
|
79
|
+
]
|
|
80
|
+
}
|
package/src/constants.ts
ADDED
package/src/handle.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { logger } from '@libp2p/logger'
|
|
2
|
+
import * as multistream from './multistream.js'
|
|
3
|
+
import { handshake } from 'it-handshake'
|
|
4
|
+
import { PROTOCOL_ID } from './constants.js'
|
|
5
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
6
|
+
import { Uint8ArrayList } from 'uint8arraylist'
|
|
7
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
8
|
+
import type { Duplex } from 'it-stream-types'
|
|
9
|
+
|
|
10
|
+
const log = logger('libp2p:mss:handle')
|
|
11
|
+
|
|
12
|
+
export async function handle (stream: Duplex<Uint8Array>, protocols: string | string[], options?: AbortOptions) {
|
|
13
|
+
protocols = Array.isArray(protocols) ? protocols : [protocols]
|
|
14
|
+
const { writer, reader, rest, stream: shakeStream } = handshake(stream)
|
|
15
|
+
|
|
16
|
+
while (true) {
|
|
17
|
+
const protocol = await multistream.readString(reader, options)
|
|
18
|
+
log('read "%s"', protocol)
|
|
19
|
+
|
|
20
|
+
if (protocol === PROTOCOL_ID) {
|
|
21
|
+
log('respond with "%s" for "%s"', PROTOCOL_ID, protocol)
|
|
22
|
+
multistream.write(writer, uint8ArrayFromString(PROTOCOL_ID))
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (protocols.includes(protocol)) {
|
|
27
|
+
multistream.write(writer, uint8ArrayFromString(protocol))
|
|
28
|
+
log('respond with "%s" for "%s"', protocol, protocol)
|
|
29
|
+
rest()
|
|
30
|
+
return { stream: shakeStream, protocol }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (protocol === 'ls') {
|
|
34
|
+
// <varint-msg-len><varint-proto-name-len><proto-name>\n<varint-proto-name-len><proto-name>\n\n
|
|
35
|
+
multistream.write(writer, new Uint8ArrayList(...protocols.map(p => multistream.encode(uint8ArrayFromString(p)))))
|
|
36
|
+
log('respond with "%s" for %s', protocols, protocol)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
multistream.write(writer, uint8ArrayFromString('na'))
|
|
41
|
+
log('respond with "na" for "%s"', protocol)
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { select } from './select.js'
|
|
2
|
+
import { handle } from './handle.js'
|
|
3
|
+
import { ls } from './ls.js'
|
|
4
|
+
import { PROTOCOL_ID } from './constants.js'
|
|
5
|
+
import type { Duplex } from 'it-stream-types'
|
|
6
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
7
|
+
|
|
8
|
+
export { PROTOCOL_ID }
|
|
9
|
+
|
|
10
|
+
export interface ProtocolStream {
|
|
11
|
+
stream: Duplex<Uint8Array>
|
|
12
|
+
protocol: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class MultistreamSelect {
|
|
16
|
+
protected stream: Duplex<Uint8Array>
|
|
17
|
+
protected shaken: boolean
|
|
18
|
+
|
|
19
|
+
constructor (stream: Duplex<Uint8Array>) {
|
|
20
|
+
this.stream = stream
|
|
21
|
+
this.shaken = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Perform the multistream-select handshake
|
|
26
|
+
*
|
|
27
|
+
* @param {AbortOptions} [options]
|
|
28
|
+
*/
|
|
29
|
+
async _handshake (options?: AbortOptions): Promise<void> {
|
|
30
|
+
if (this.shaken) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { stream } = await select(this.stream, PROTOCOL_ID, undefined, options)
|
|
35
|
+
this.stream = stream
|
|
36
|
+
this.shaken = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Dialer extends MultistreamSelect {
|
|
41
|
+
async select (protocols: string | string[], options?: AbortOptions): Promise<ProtocolStream> {
|
|
42
|
+
return await select(this.stream, protocols, this.shaken ? undefined : PROTOCOL_ID, options)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async ls (options?: AbortOptions): Promise<string[]> {
|
|
46
|
+
await this._handshake(options)
|
|
47
|
+
const res = await ls(this.stream, options)
|
|
48
|
+
const { stream, protocols } = res
|
|
49
|
+
this.stream = stream
|
|
50
|
+
return protocols
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Listener extends MultistreamSelect {
|
|
55
|
+
async handle (protocols: string | string[], options?: AbortOptions): Promise<ProtocolStream> {
|
|
56
|
+
return await handle(this.stream, protocols, options)
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/ls.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { reader as createReader } from 'it-reader'
|
|
2
|
+
import { logger } from '@libp2p/logger'
|
|
3
|
+
import * as multistream from './multistream.js'
|
|
4
|
+
import { handshake } from 'it-handshake'
|
|
5
|
+
import * as lp from 'it-length-prefixed'
|
|
6
|
+
import { pipe } from 'it-pipe'
|
|
7
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
8
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
9
|
+
import type { Duplex } from 'it-stream-types'
|
|
10
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
11
|
+
|
|
12
|
+
const log = logger('libp2p:mss:ls')
|
|
13
|
+
|
|
14
|
+
export async function ls (stream: Duplex<Uint8Array>, options?: AbortOptions): Promise<{ stream: Duplex<Uint8Array>, protocols: string[] }> {
|
|
15
|
+
const { reader, writer, rest, stream: shakeStream } = handshake(stream)
|
|
16
|
+
|
|
17
|
+
log('write "ls"')
|
|
18
|
+
multistream.write(writer, uint8ArrayFromString('ls'))
|
|
19
|
+
rest()
|
|
20
|
+
|
|
21
|
+
// Next message from remote will be (e.g. for 2 protocols):
|
|
22
|
+
// <varint-msg-len><varint-proto-name-len><proto-name>\n<varint-proto-name-len><proto-name>\n
|
|
23
|
+
const res = await multistream.read(reader, options)
|
|
24
|
+
|
|
25
|
+
// After reading response we have:
|
|
26
|
+
// <varint-proto-name-len><proto-name>\n<varint-proto-name-len><proto-name>\n
|
|
27
|
+
const protocolsReader = createReader([res])
|
|
28
|
+
const protocols: string[] = []
|
|
29
|
+
|
|
30
|
+
// Decode each of the protocols from the reader
|
|
31
|
+
await pipe(
|
|
32
|
+
protocolsReader,
|
|
33
|
+
lp.decode(),
|
|
34
|
+
async (source) => {
|
|
35
|
+
for await (const protocol of source) {
|
|
36
|
+
// Remove the newline
|
|
37
|
+
protocols.push(uint8ArrayToString(protocol.slice(0, -1)))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const output = { stream: shakeStream, protocols }
|
|
43
|
+
|
|
44
|
+
return output
|
|
45
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
import { Uint8ArrayList } from 'uint8arraylist'
|
|
3
|
+
import * as lp from 'it-length-prefixed'
|
|
4
|
+
import { pipe } from 'it-pipe'
|
|
5
|
+
import errCode from 'err-code'
|
|
6
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
7
|
+
import first from 'it-first'
|
|
8
|
+
import { abortableSource } from 'abortable-iterator'
|
|
9
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
10
|
+
import type { Pushable } from 'it-pushable'
|
|
11
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
12
|
+
import type { Source } from 'it-stream-types'
|
|
13
|
+
import type { Reader } from 'it-reader'
|
|
14
|
+
|
|
15
|
+
const NewLine = uint8ArrayFromString('\n')
|
|
16
|
+
|
|
17
|
+
export function encode (buffer: Uint8Array | Uint8ArrayList): Uint8Array {
|
|
18
|
+
const list = new Uint8ArrayList(buffer, NewLine)
|
|
19
|
+
|
|
20
|
+
return lp.encode.single(list).slice()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `write` encodes and writes a single buffer
|
|
25
|
+
*/
|
|
26
|
+
export function write (writer: Pushable<Uint8Array>, buffer: Uint8Array | Uint8ArrayList) {
|
|
27
|
+
writer.push(encode(buffer).slice())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `writeAll` behaves like `write`, except it encodes an array of items as a single write
|
|
32
|
+
*/
|
|
33
|
+
export function writeAll (writer: Pushable<Uint8Array>, buffers: Uint8Array[]) {
|
|
34
|
+
const list = new Uint8ArrayList()
|
|
35
|
+
|
|
36
|
+
for (const buf of buffers) {
|
|
37
|
+
list.append(encode(buf))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
writer.push(list.slice())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function read (reader: Reader, options?: AbortOptions) {
|
|
44
|
+
let byteLength = 1 // Read single byte chunks until the length is known
|
|
45
|
+
const varByteSource = { // No return impl - we want the reader to remain readable
|
|
46
|
+
[Symbol.asyncIterator]: () => varByteSource,
|
|
47
|
+
next: async () => await reader.next(byteLength)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let input: Source<Uint8ArrayList> = varByteSource
|
|
51
|
+
|
|
52
|
+
// If we have been passed an abort signal, wrap the input source in an abortable
|
|
53
|
+
// iterator that will throw if the operation is aborted
|
|
54
|
+
if (options?.signal != null) {
|
|
55
|
+
input = abortableSource(varByteSource, options.signal)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Once the length has been parsed, read chunk for that length
|
|
59
|
+
const onLength = (l: number) => { byteLength = l }
|
|
60
|
+
|
|
61
|
+
const buf = await pipe(
|
|
62
|
+
input,
|
|
63
|
+
lp.decode({ onLength }),
|
|
64
|
+
async (source) => await first(source)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (buf == null) {
|
|
68
|
+
throw errCode(new Error('no buffer returned'), 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (buf[buf.length - 1] !== NewLine[0]) {
|
|
72
|
+
throw errCode(new Error('missing newline'), 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return buf.slice(0, -1) // Remove newline
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function readString (reader: Reader, options?: AbortOptions) {
|
|
79
|
+
const buf = await read(reader, options)
|
|
80
|
+
|
|
81
|
+
return uint8ArrayToString(buf)
|
|
82
|
+
}
|
package/src/select.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { logger } from '@libp2p/logger'
|
|
2
|
+
import errCode from 'err-code'
|
|
3
|
+
import * as multistream from './multistream.js'
|
|
4
|
+
import { handshake } from 'it-handshake'
|
|
5
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
6
|
+
import type { AbortOptions } from '@libp2p/interfaces'
|
|
7
|
+
import type { Duplex } from 'it-stream-types'
|
|
8
|
+
|
|
9
|
+
const log = logger('libp2p:mss:select')
|
|
10
|
+
|
|
11
|
+
export async function select (stream: Duplex<Uint8Array>, protocols: string | string[], protocolId?: string, options?: AbortOptions) {
|
|
12
|
+
protocols = Array.isArray(protocols) ? [...protocols] : [protocols]
|
|
13
|
+
const { reader, writer, rest, stream: shakeStream } = handshake(stream)
|
|
14
|
+
|
|
15
|
+
const protocol = protocols.shift()
|
|
16
|
+
|
|
17
|
+
if (protocol == null) {
|
|
18
|
+
throw new Error('At least one protocol must be specified')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (protocolId != null) {
|
|
22
|
+
log('select: write ["%s", "%s"]', protocolId, protocol)
|
|
23
|
+
multistream.writeAll(writer, [uint8ArrayFromString(protocolId), uint8ArrayFromString(protocol)])
|
|
24
|
+
} else {
|
|
25
|
+
log('select: write "%s"', protocol)
|
|
26
|
+
multistream.write(writer, uint8ArrayFromString(protocol))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let response = await multistream.readString(reader, options)
|
|
30
|
+
log('select: read "%s"', response)
|
|
31
|
+
|
|
32
|
+
// Read the protocol response if we got the protocolId in return
|
|
33
|
+
if (response === protocolId) {
|
|
34
|
+
response = await multistream.readString(reader, options)
|
|
35
|
+
log('select: read "%s"', response)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// We're done
|
|
39
|
+
if (response === protocol) {
|
|
40
|
+
rest()
|
|
41
|
+
return { stream: shakeStream, protocol }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// We haven't gotten a valid ack, try the other protocols
|
|
45
|
+
for (const protocol of protocols) {
|
|
46
|
+
log('select: write "%s"', protocol)
|
|
47
|
+
multistream.write(writer, uint8ArrayFromString(protocol))
|
|
48
|
+
const response = await multistream.readString(reader, options)
|
|
49
|
+
log('select: read "%s" for "%s"', response, protocol)
|
|
50
|
+
|
|
51
|
+
if (response === protocol) {
|
|
52
|
+
rest() // End our writer so others can start writing to stream
|
|
53
|
+
return { stream: shakeStream, protocol }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
rest()
|
|
58
|
+
throw errCode(new Error('protocol selection failed'), 'ERR_UNSUPPORTED_PROTOCOL')
|
|
59
|
+
}
|