@toa.io/bindings.amqp 0.2.0-dev.1 → 0.2.1-dev.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/bindings.amqp",
3
- "version": "0.2.0-dev.1",
3
+ "version": "0.2.1-dev.2",
4
4
  "description": "Toa AMQP Binding",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -18,9 +18,16 @@
18
18
  "scripts": {
19
19
  "test": "echo \"Error: run tests from root\" && exit 1"
20
20
  },
21
+ "devDependencies": {
22
+ "@toa.io/mock": "*",
23
+ "@types/amqplib": "0.8.2"
24
+ },
21
25
  "dependencies": {
22
- "amqplib": "0.8.0",
23
- "execa": "5.1.1"
26
+ "@toa.io/console": "*",
27
+ "@toa.io/core": "*",
28
+ "@toa.io/generic": "*",
29
+ "@toa.io/pointer": "*",
30
+ "amqplib": "0.10.0"
24
31
  },
25
- "gitHead": "a0df6f203bca3e26e3a4a791a213e418d2b65764"
32
+ "gitHead": "2be07592325b2e4dc823e81d882a4e50bf50de24"
26
33
  }
package/readme.md ADDED
@@ -0,0 +1,45 @@
1
+ # AMQP Binding
2
+
3
+ AMQP binding is asynchronous, broadcast and *systemic*, that is being used by the runtime.
4
+ See [Bindings](#).
5
+
6
+ ## Deployment
7
+
8
+ AMQP binding requires RabbitMQ broker(s) available from the cluster. As AMQP is a systemic binding,
9
+ so at least one `system` or `default` broker must be provisioned.
10
+
11
+ ### Declaration
12
+
13
+ AMQP deployment must be declared by [URI Set annotation](#) with a `system` extension, which value
14
+ must be the host of the broker to be used by the runtime. Either `system` or `default` hosts must be
15
+ defined.
16
+
17
+ ```yaml
18
+ # context.toa.yaml
19
+ annotations:
20
+ @toa.io/bindings.amqp:
21
+ system: host0 # the runtime
22
+ default: host1 # all undeclared
23
+ dummies: host2 # namespace-wide
24
+ dummies.dummy1: host3 # component exclusive
25
+ ```
26
+
27
+ ### Concise Declaration
28
+
29
+ > Well-known shortcut `amqp` is available.
30
+
31
+ String annotation value is considered as `default`.
32
+
33
+ The next two declarations are equivalent.
34
+
35
+ ```yaml
36
+ # context.toa.yaml
37
+ annotations:
38
+ @toa.io/bindings.amqp:
39
+ default: host1
40
+ ```
41
+
42
+ ```yaml
43
+ # context.toa.yaml
44
+ amqp: host1
45
+ ```
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const { uris } = require('@toa.io/pointer')
4
+
5
+ exports.annotation = uris.construct
@@ -1,9 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const { Connector } = require('@toa.io/core')
4
- const { newid } = require('@toa.io/gears')
4
+ const { newid } = require('@toa.io/generic')
5
5
 
6
- class Broadcast extends Connector {
6
+ /**
7
+ * @implements {toa.core.bindings.Broadcaster}
8
+ */
9
+ class Broadcaster extends Connector {
7
10
  #group
8
11
  #channel
9
12
  #prefix
@@ -20,7 +23,10 @@ class Broadcast extends Connector {
20
23
  }
21
24
 
22
25
  async send (label, payload) {
23
- await this.#channel.publish(this.#prefix + label, payload, { expiration: 10, persistent: false })
26
+ await this.#channel.publish(this.#prefix + label, payload, {
27
+ expiration: 10,
28
+ persistent: false
29
+ })
24
30
  }
25
31
 
26
32
  async receive (label, callback) {
@@ -28,4 +34,4 @@ class Broadcast extends Connector {
28
34
  }
29
35
  }
30
36
 
31
- exports.Broadcast = Broadcast
37
+ exports.Broadcaster = Broadcaster
package/src/channel.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { Connector } = require('@toa.io/core')
4
- const { console, newid } = require('@toa.io/gears')
4
+ const { newid, timeout } = require('@toa.io/generic')
5
+ const { console } = require('@toa.io/console')
5
6
 
6
7
  const { pack, unpack } = require('./message')
7
8
 
@@ -24,6 +25,12 @@ class Channel extends Connector {
24
25
 
25
26
  async connection () {
26
27
  this.#channel = await this.#connection.channel()
28
+ this.#channel.prefetch(100)
29
+ }
30
+
31
+ async disconnection () {
32
+ // solves 'Channel ended, no reply will be forthcoming'
33
+ await timeout(50)
27
34
  }
28
35
 
29
36
  async request (label, request) {
@@ -54,37 +61,43 @@ class Channel extends Connector {
54
61
  const content = unpack(received.content)
55
62
  const reply = await invocation(content)
56
63
 
57
- const message = pack(reply)
64
+ const buffer = pack(reply)
58
65
  const properties = { correlationId: received.properties.correlationId }
59
66
 
60
- await this.#channel.publish(exchange, received.properties.replyTo, message, properties)
67
+ await this.#channel.publish(exchange, received.properties.replyTo, buffer, properties)
61
68
  await this.#channel.ack(received)
62
69
  })
63
70
  }
64
71
 
65
- async publish (label, payload, options) {
72
+ async publish (label, content, options) {
66
73
  const exchange = 'event.' + label
67
74
 
75
+ // TODO: assert once per exchange
68
76
  await this.#channel.assertExchange(exchange, 'fanout', EXCHANGE)
69
77
 
70
- const message = pack(payload)
78
+ const buffer = pack(content)
71
79
 
72
- await this.#channel.publish(exchange, '', message, options)
80
+ await this.#channel.publish(exchange, '', buffer, options)
73
81
  }
74
82
 
75
83
  async subscribe (label, id, callback) {
76
84
  const exchange = 'event.' + label
77
85
  const queue = exchange + '..' + id
86
+ const options = { consumerTag: id }
78
87
 
79
88
  await this.#channel.assertExchange(exchange, 'fanout', EXCHANGE)
80
89
  await this.#channel.assertQueue(queue, QUEUE)
81
90
  await this.#channel.bindQueue(queue, exchange, '')
82
91
 
83
- await this.#channel.consume(queue, async (received) => {
84
- const content = unpack(received.content)
92
+ await this.#channel.consume(queue, async (message) => {
93
+ const content = unpack(message.content)
85
94
  await callback(content)
86
- await this.#channel.ack(received)
87
- })
95
+ await this.#channel.ack(message)
96
+ }, options)
97
+ }
98
+
99
+ async unsubscribe (id) {
100
+ await this.#channel.cancel(id)
88
101
  }
89
102
 
90
103
  async #bind (label) {
package/src/connection.js CHANGED
@@ -3,40 +3,46 @@
3
3
  const amqp = require('amqplib')
4
4
 
5
5
  const { Connector } = require('@toa.io/core')
6
- const { console } = require('@toa.io/gears')
6
+ const { console } = require('@toa.io/console')
7
7
 
8
8
  class Connection extends Connector {
9
- #url
9
+ /** @type {toa.amqp.Pointer} */
10
+ #pointer
11
+
12
+ /** @type {import('amqplib').Connection} */
10
13
  #connection
11
14
 
12
- constructor (host) {
15
+ /**
16
+ * @param {toa.amqp.Pointer} pointer
17
+ */
18
+ constructor (pointer) {
13
19
  super()
14
20
 
15
- this.#url = Connection.#locate(host)
21
+ this.#pointer = pointer
16
22
  }
17
23
 
18
24
  async connection () {
19
- this.#connection = await amqp.connect(this.#url)
20
- console.info(`AMQP Binding connected to ${this.#url}`)
25
+ try {
26
+ this.#connection = await amqp.connect(this.#pointer.reference)
27
+ } catch (e) {
28
+ console.error(`Connection to ${this.#pointer.label} has failed`)
29
+
30
+ throw e
31
+ }
32
+
33
+ console.info(`AMQP Binding connected to ${this.#pointer.label}`)
21
34
  }
22
35
 
23
36
  async disconnection () {
24
37
  // TODO: handle current operations
25
38
  // http://www.squaremobius.net/amqp.node/channel_api.html#model_close
26
39
  await this.#connection.close()
27
- console.info(`AMQP Binding disconnected from ${this.#url}`)
28
- }
29
40
 
30
- async channel () {
31
- return this.#connection.createChannel()
41
+ console.info(`AMQP Binding disconnected from ${this.#pointer.label}`)
32
42
  }
33
43
 
34
- static #locate (host) {
35
- // TODO: read ./deployments.js
36
- const user = 'user'
37
- const password = 'password'
38
-
39
- return `amqp://${user}:${password}@${host}`
44
+ async channel () {
45
+ return await this.#connection.createChannel()
40
46
  }
41
47
  }
42
48
 
@@ -0,0 +1,4 @@
1
+ 'use strict'
2
+
3
+ exports.PREFIX = 'bindings-amqp'
4
+ exports.SYSTEM = 'system'
package/src/consumer.js CHANGED
@@ -4,6 +4,9 @@ const { Connector } = require('@toa.io/core')
4
4
 
5
5
  const { name } = require('./queue')
6
6
 
7
+ /**
8
+ * @implements {toa.core.bindings.Consumer}
9
+ */
7
10
  class Consumer extends Connector {
8
11
  #channel
9
12
  #queue
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const connectors = require('@toa.io/pointer')
4
+
5
+ const { PREFIX } = require('./constants')
6
+
7
+ /**
8
+ * @type {toa.deployment.dependency.Constructor}
9
+ */
10
+ const deployment = (instances, annotation) => {
11
+ validate(annotation)
12
+
13
+ /** @type {toa.pointer.deployment.Options} */
14
+ const options = { prefix: PREFIX }
15
+
16
+ return connectors.deployment(instances, annotation, options)
17
+ }
18
+
19
+ /**
20
+ * @param {toa.pointer.URIs} annotation
21
+ */
22
+ const validate = (annotation) => {
23
+ const defined = annotation !== undefined
24
+ const defaults = defined && (typeof annotation === 'string' || annotation.default !== undefined)
25
+ const correct = defined && (defaults || annotation.system !== undefined)
26
+
27
+ if (!correct) {
28
+ throw new Error('AMQP deployment requires either \'system\' or \'default\' pointer annotation')
29
+ }
30
+ }
31
+
32
+ exports.deployment = deployment
package/src/emitter.js CHANGED
@@ -3,6 +3,9 @@
3
3
  const { Connector } = require('@toa.io/core')
4
4
  const { name } = require('./queue')
5
5
 
6
+ /**
7
+ * @implements {toa.core.bindings.Emitter}
8
+ */
6
9
  class Emitter extends Connector {
7
10
  #channel
8
11
  #locator
package/src/factory.js CHANGED
@@ -7,9 +7,14 @@ const { Consumer } = require('./consumer')
7
7
  const { Producer } = require('./producer')
8
8
  const { Emitter } = require('./emitter')
9
9
  const { Receiver } = require('./receiver')
10
- const { Broadcast } = require('./broadcast')
10
+ const { Broadcaster } = require('./broadcaster')
11
11
  const { Connection } = require('./connection')
12
+ const { Pointer } = require('./pointer')
13
+ const { SYSTEM } = require('./constants')
12
14
 
15
+ /**
16
+ * @implements {toa.core.bindings.Factory}
17
+ */
13
18
  class Factory {
14
19
  #connections = {}
15
20
 
@@ -37,21 +42,27 @@ class Factory {
37
42
  return new Receiver(channel, locator, label, id, receiver)
38
43
  }
39
44
 
40
- broadcast (name, group) {
41
- const locator = new Locator()
42
- const channel = this.#channel(locator)
45
+ broadcaster (name, group) {
46
+ const channel = this.#channel()
43
47
 
44
- return new Broadcast(channel, name, group)
48
+ return new Broadcaster(channel, name, group)
45
49
  }
46
50
 
47
- #channel () {
48
- const host = 'rabbitmq' // locator.host('rabbitmq')
51
+ /**
52
+ * @param {toa.core.Locator} [locator]
53
+ * @returns {Channel}
54
+ */
55
+ #channel (locator) {
56
+ if (locator === undefined) locator = new Locator(SYSTEM)
57
+
58
+ const pointer = new Pointer(locator)
59
+ const key = pointer.reference
49
60
 
50
- if (this.#connections[host] === undefined) {
51
- this.#connections[host] = new Connection(host)
61
+ if (this.#connections[key] === undefined) {
62
+ this.#connections[key] = new Connection(pointer)
52
63
  }
53
64
 
54
- return new Channel(this.#connections[host])
65
+ return new Channel(this.#connections[key])
55
66
  }
56
67
  }
57
68
 
package/src/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const { Factory } = require('./factory')
4
- const { deployments } = require('./deployments')
4
+ const { deployment } = require('./deployment')
5
+ const { annotation } = require('./annotation')
5
6
 
6
- exports.Factory = Factory
7
7
  exports.properties = { async: true }
8
- exports.deployments = deployments
8
+ exports.annotation = annotation
9
+ exports.deployment = deployment
10
+ exports.Factory = Factory
package/src/pointer.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ const { Pointer: Base } = require('@toa.io/pointer')
4
+
5
+ const { PREFIX } = require('./constants')
6
+
7
+ // noinspection JSClosureCompilerSyntax
8
+ /**
9
+ * @implements {toa.amqp.Pointer}
10
+ */
11
+ class Pointer extends Base {
12
+ /**
13
+ * @param {toa.core.Locator} locator
14
+ */
15
+ constructor (locator) {
16
+ super(PREFIX, locator, OPTIONS)
17
+ }
18
+ }
19
+
20
+ /** @type {toa.pointer.Options} */
21
+ const OPTIONS = { protocol: 'amqp:' }
22
+
23
+ exports.Pointer = Pointer
package/src/producer.js CHANGED
@@ -6,8 +6,11 @@ const { name } = require('./queue')
6
6
 
7
7
  class Producer extends Connector {
8
8
  #channel
9
+ /** @type {toa.core.Locator} */
9
10
  #locator
11
+ /** @type {toa.core.Component} */
10
12
  #producer
13
+ /** @type {Array<string>} */
11
14
  #endpoints
12
15
 
13
16
  constructor (channel, locator, endpoints, producer) {
@@ -23,12 +26,11 @@ class Producer extends Connector {
23
26
  }
24
27
 
25
28
  async connection () {
26
- return Promise.all(this.#endpoints.map((endpoint) => this.#endpoint(endpoint)))
29
+ await Promise.all(this.#endpoints.map((endpoint) => this.#endpoint(endpoint)))
27
30
  }
28
31
 
29
32
  async #endpoint (endpoint) {
30
33
  const queue = name(this.#locator, endpoint)
31
-
32
34
  await this.#channel.reply(queue, async (request) => this.#producer.invoke(endpoint, request))
33
35
  }
34
36
  }
package/src/queue.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const { concat } = require('@toa.io/gears')
3
+ const { concat } = require('@toa.io/generic')
4
4
 
5
- const name = (locator, endpoint) => locator.domain + '.' + concat(locator.name, '.') + endpoint
5
+ const name = (locator, endpoint) => locator.namespace + '.' + concat(locator.name, '.') + endpoint
6
6
 
7
7
  exports.name = name
package/src/receiver.js CHANGED
@@ -6,8 +6,13 @@ const { name } = require('./queue')
6
6
 
7
7
  class Receiver extends Connector {
8
8
  #channel
9
+ /** @type {toa.core.Receiver} */
9
10
  #receiver
11
+
12
+ /** @type {string} */
10
13
  #label
14
+
15
+ /** @type {string} */
11
16
  #id
12
17
 
13
18
  constructor (channel, locator, label, id, receiver) {
@@ -19,10 +24,15 @@ class Receiver extends Connector {
19
24
  this.#id = id
20
25
 
21
26
  this.depends(channel)
27
+ this.depends(receiver)
22
28
  }
23
29
 
24
30
  async connection () {
25
- await this.#channel.subscribe(this.#label, this.#id, (payload) => this.#receiver.receive(payload))
31
+ await this.#channel.subscribe(this.#label, this.#id, (message) => this.#receiver.receive(message))
32
+ }
33
+
34
+ async disconnection () {
35
+ await this.#channel.unsubscribe(this.#id)
26
36
  }
27
37
  }
28
38
 
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const mock = { uris: { construct: () => generate() }, Pointer: class {} }
5
+
6
+ jest.mock('@toa.io/pointer', () => mock)
7
+ const { annotation } = require('../')
8
+
9
+ it('should export annotations', () => {
10
+ expect(annotation).toBeDefined()
11
+ })
12
+
13
+ it('should export connectors.uris construct', () => {
14
+ expect(annotation).toStrictEqual(mock.uris.construct)
15
+ })
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { random } = require('@toa.io/generic')
5
+ const mock = require('@toa.io/mock')
6
+
7
+ const { deployment } = require('../')
8
+
9
+ /** @type {toa.norm.context.dependencies.Instance[]} */
10
+ let instances
11
+
12
+ /** @returns {URL} */
13
+ const gen = () => new URL('amqp://host-' + generate() + ':' + (random(1000) + 1000))
14
+
15
+ beforeEach(() => {
16
+ instances = mock.dependencies.instances()
17
+ })
18
+
19
+ it('should exist', () => {
20
+ expect(deployment).toBeDefined()
21
+ })
22
+
23
+ it('should throw if annotation is not defined', () => {
24
+ expect(() => deployment(instances, undefined))
25
+ .toThrow('AMQP deployment requires either \'system\' or \'default\' pointer annotation')
26
+ })
27
+
28
+ it('should throw if \'system\' is not defined', () => {
29
+ const url = gen()
30
+ const annotation = {}
31
+
32
+ for (const instance of instances) annotation[instance.locator.id] = url.href
33
+
34
+ expect(() => deployment(instances, annotation))
35
+ .toThrow('AMQP deployment requires either \'system\' or \'default\' pointer annotation')
36
+ })
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { Locator } = require('@toa.io/core')
5
+ const { encode, letters: { up } } = require('@toa.io/generic')
6
+
7
+ const { Pointer } = require('../src/pointer')
8
+ const { PREFIX } = require('../src/constants')
9
+
10
+ /** @type {toa.core.Locator} */
11
+ let locator
12
+
13
+ /** @type {toa.amqp.Pointer} */
14
+ let pointer
15
+
16
+ const protocol = 'amqp:'
17
+
18
+ let url
19
+
20
+ beforeAll(() => {
21
+ const username = generate()
22
+ const password = generate()
23
+
24
+ url = new URL('amqps://whatever:5672')
25
+
26
+ url.username = username
27
+ url.password = password
28
+
29
+ process.env.TOA_BINDINGS_AMQP_DEFAULT_USERNAME = username
30
+ process.env.TOA_BINDINGS_AMQP_DEFAULT_PASSWORD = password
31
+ })
32
+
33
+ beforeEach(() => {
34
+ const name = generate()
35
+ const namespace = generate()
36
+ const uris = { default: url.href }
37
+ const value = encode(uris)
38
+ const key = `TOA_${up(PREFIX)}_POINTER`
39
+
40
+ process.env[key] = value
41
+
42
+ locator = new Locator(name, namespace)
43
+ pointer = new Pointer(locator)
44
+ })
45
+
46
+ it('should be', () => undefined)
47
+
48
+ it('should expose reference', () => {
49
+ expect(pointer.reference).toStrictEqual(url.href)
50
+ })
51
+
52
+ it('should set amqp: protocol on localhost', () => {
53
+ process.env.TOA_ENV = 'local'
54
+
55
+ pointer = new Pointer(locator)
56
+
57
+ expect(pointer.protocol).toStrictEqual(protocol)
58
+
59
+ delete process.env.TOA_ENV
60
+ })
@@ -0,0 +1,8 @@
1
+ import type { Pointer as Base } from '@toa.io/pointer/types'
2
+
3
+ declare namespace toa.amqp {
4
+
5
+ interface Pointer extends Base {
6
+ }
7
+
8
+ }
package/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- Copyright (c) 2020-present Artem Gurtovoi
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,28 +0,0 @@
1
- 'use strict'
2
-
3
- const deployments = () => {
4
- const fullname = 'rabbitmq'
5
-
6
- // TODO: provide passwords as secrets for component containers
7
- const user = 'user'
8
- const password = 'password'
9
- const erlangCookie = 'cookie'
10
-
11
- return [{
12
- chart: {
13
- name: 'rabbitmq',
14
- version: '8.24.3',
15
- repository: 'https://charts.bitnami.com/bitnami'
16
- },
17
- values: {
18
- fullnameOverride: fullname,
19
- auth: {
20
- user,
21
- password,
22
- erlangCookie
23
- }
24
- }
25
- }]
26
- }
27
-
28
- exports.deployments = deployments