@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.
Files changed (49) hide show
  1. package/README.md +290 -0
  2. package/dist/src/constants.d.ts +2 -0
  3. package/dist/src/constants.d.ts.map +1 -0
  4. package/dist/src/constants.js +2 -0
  5. package/dist/src/constants.js.map +1 -0
  6. package/dist/src/handle.d.ts +7 -0
  7. package/dist/src/handle.d.ts.map +1 -0
  8. package/dist/src/handle.js +35 -0
  9. package/dist/src/handle.js.map +1 -0
  10. package/dist/src/index.d.ts +27 -0
  11. package/dist/src/index.d.ts.map +1 -0
  12. package/dist/src/index.js +42 -0
  13. package/dist/src/index.js.map +1 -0
  14. package/dist/src/ls.d.ts +7 -0
  15. package/dist/src/ls.d.ts.map +1 -0
  16. package/dist/src/ls.js +32 -0
  17. package/dist/src/ls.js.map +1 -0
  18. package/dist/src/multistream.d.ts +16 -0
  19. package/dist/src/multistream.d.ts.map +1 -0
  20. package/dist/src/multistream.js +57 -0
  21. package/dist/src/multistream.js.map +1 -0
  22. package/dist/src/select.d.ts +7 -0
  23. package/dist/src/select.d.ts.map +1 -0
  24. package/dist/src/select.js +48 -0
  25. package/dist/src/select.js.map +1 -0
  26. package/dist/test/dialer.spec.d.ts +2 -0
  27. package/dist/test/dialer.spec.d.ts.map +1 -0
  28. package/dist/test/dialer.spec.js +144 -0
  29. package/dist/test/dialer.spec.js.map +1 -0
  30. package/dist/test/integration.spec.d.ts +2 -0
  31. package/dist/test/integration.spec.d.ts.map +1 -0
  32. package/dist/test/integration.spec.js +55 -0
  33. package/dist/test/integration.spec.js.map +1 -0
  34. package/dist/test/listener.spec.d.ts +2 -0
  35. package/dist/test/listener.spec.d.ts.map +1 -0
  36. package/dist/test/listener.spec.js +123 -0
  37. package/dist/test/listener.spec.js.map +1 -0
  38. package/dist/test/multistream.spec.d.ts +2 -0
  39. package/dist/test/multistream.spec.d.ts.map +1 -0
  40. package/dist/test/multistream.spec.js +86 -0
  41. package/dist/test/multistream.spec.js.map +1 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -0
  43. package/package.json +80 -0
  44. package/src/constants.ts +2 -0
  45. package/src/handle.ts +43 -0
  46. package/src/index.ts +58 -0
  47. package/src/ls.ts +45 -0
  48. package/src/multistream.ts +82 -0
  49. 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
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export const PROTOCOL_ID = '/multistream/1.0.0'
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
+ }