@toa.io/bindings.amqp 0.2.1-dev.3 → 0.3.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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@toa.io/bindings.amqp",
3
- "version": "0.2.1-dev.3",
3
+ "version": "0.3.0",
4
4
  "description": "Toa AMQP Binding",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
7
- "main": "src/index.js",
7
+ "main": "source/index.js",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/toa-io/toa.git"
@@ -27,7 +27,7 @@
27
27
  "@toa.io/core": "*",
28
28
  "@toa.io/generic": "*",
29
29
  "@toa.io/pointer": "*",
30
- "amqplib": "0.10.0"
30
+ "comq": "*"
31
31
  },
32
32
  "gitHead": "2be07592325b2e4dc823e81d882a4e50bf50de24"
33
33
  }
package/readme.md CHANGED
@@ -1,45 +1,54 @@
1
- # AMQP Binding
1
+ # Toa AMQP Binding
2
2
 
3
- AMQP binding is asynchronous, broadcast and *systemic*, that is being used by the runtime.
4
- See [Bindings](#).
3
+ AMQP asynchronous binding on top of [ComQ](/libraries/comq).
5
4
 
6
5
  ## Deployment
7
6
 
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.
7
+ AMQP deployment must be declared with
8
+ the [Pointer annotation](/libraries/pointer/readme.md#annotation). Either `system` or `default`
9
+ pointers must be defined.
10
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.
11
+ Well-known annotation shortcut `amqp` is available.
16
12
 
17
13
  ```yaml
18
14
  # context.toa.yaml
19
15
  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
16
+ "@toa.io/bindings.amqp":
17
+ system: url0 # the runtime
18
+ default: url1 # all undeclared
19
+ dummies: url2 # namespace-wide
20
+ dummies.dummy1: url # component exclusive
25
21
  ```
26
22
 
27
23
  ### Concise Declaration
28
24
 
29
- > Well-known shortcut `amqp` is available.
25
+ Well-known shortcut `amqp` is available. The next two declarations are equivalent:
30
26
 
31
- String annotation value is considered as `default`.
27
+ ```yaml
28
+ # context.toa.yaml
29
+ annotations:
30
+ "@toa.io/bindings.amqp":
31
+ system: url0
32
+ dummies: url1
33
+ ```
34
+
35
+ ```yaml
36
+ # context.toa.yaml
37
+ amqp:
38
+ system: url0
39
+ dummies: url1
40
+ ```
32
41
 
33
- The next two declarations are equivalent.
42
+ `string` annotation value is considered as `default`. The next two declarations are equivalent:
34
43
 
35
44
  ```yaml
36
45
  # context.toa.yaml
37
46
  annotations:
38
- @toa.io/bindings.amqp:
39
- default: host1
47
+ "@toa.io/bindings.amqp":
48
+ default: url1
40
49
  ```
41
50
 
42
51
  ```yaml
43
52
  # context.toa.yaml
44
- amqp: host1
53
+ amqp: url1
45
54
  ```
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('@toa.io/core')
4
+ const { newid } = require('@toa.io/generic')
5
+
6
+ const { name } = require('./queues')
7
+
8
+ /**
9
+ * @implements {toa.core.bindings.Broadcast}
10
+ */
11
+ class Broadcast extends Connector {
12
+ /** @type {toa.amqp.Communication} */
13
+ #comm
14
+
15
+ /** @type {toa.core.Locator} */
16
+ #locator
17
+
18
+ /** @type {string} */
19
+ #group
20
+
21
+ /**
22
+ * @param {toa.amqp.Communication} comm
23
+ * @param {toa.core.Locator} locator
24
+ * @param {string} [group]
25
+ */
26
+ constructor (comm, locator, group) {
27
+ super()
28
+
29
+ this.#comm = comm
30
+ this.#locator = locator
31
+ this.#group = group ?? newid()
32
+
33
+ this.depends(comm)
34
+ }
35
+
36
+ async transmit (label, payload) {
37
+ const exchange = name(this.#locator, label)
38
+
39
+ await this.#comm.emit(exchange, payload)
40
+ }
41
+
42
+ async receive (label, callback) {
43
+ const exchange = name(this.#locator, label)
44
+
45
+ await this.#comm.consume(exchange, this.#group, callback)
46
+ }
47
+ }
48
+
49
+ exports.Broadcast = Broadcast
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ const { connect } = require('comq')
4
+ const { Connector } = require('@toa.io/core')
5
+
6
+ /**
7
+ * @implements {toa.amqp.Communication}
8
+ */
9
+ class Communication extends Connector {
10
+ /** @type {toa.pointer.Pointer} */
11
+ #pointer
12
+
13
+ /** @type {comq.IO} */
14
+ #io
15
+
16
+ /**
17
+ * @param {toa.pointer.Pointer} pointer
18
+ */
19
+ constructor (pointer) {
20
+ super()
21
+
22
+ this.#pointer = pointer
23
+ }
24
+
25
+ async open () {
26
+ this.#io = await connect(this.#pointer.reference)
27
+ }
28
+
29
+ async close () {
30
+ await this.#io.seal()
31
+ }
32
+
33
+ async dispose () {
34
+ await this.#io.close()
35
+ }
36
+
37
+ async reply (queue, process) {
38
+ await this.#io.reply(queue, process)
39
+ }
40
+
41
+ async request (queue, request) {
42
+ return this.#io.request(queue, request)
43
+ }
44
+
45
+ async emit (exchange, message) {
46
+ await this.#io.emit(exchange, message)
47
+ }
48
+
49
+ async consume (exchange, group, consumer) {
50
+ await this.#io.consume(exchange, group, consumer)
51
+ }
52
+ }
53
+
54
+ exports.Communication = Communication
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('@toa.io/core')
4
+ const { name } = require('./queues')
5
+
6
+ /**
7
+ * @implements {toa.core.bindings.Consumer}
8
+ */
9
+ class Consumer extends Connector {
10
+ /** @type {string} */
11
+ #queue
12
+
13
+ /** @type {toa.amqp.Communication} */
14
+ #comm
15
+
16
+ /**
17
+ * @param {toa.amqp.Communication} comm
18
+ * @param {toa.core.Locator} locator
19
+ * @param {string} endpoint
20
+ */
21
+ constructor (comm, locator, endpoint) {
22
+ super()
23
+
24
+ this.#queue = name(locator, endpoint)
25
+ this.#comm = comm
26
+
27
+ this.depends(comm)
28
+ }
29
+
30
+ async request (request) {
31
+ return this.#comm.request(this.#queue, request)
32
+ }
33
+ }
34
+
35
+ exports.Consumer = Consumer
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('@toa.io/core')
4
+
5
+ const { name } = require('./queues')
6
+
7
+ /**
8
+ * @implements {toa.core.bindings.Emitter}
9
+ */
10
+ class Emitter extends Connector {
11
+ /** @type {string} */
12
+ #exchange
13
+
14
+ /** @type {toa.amqp.Communication} */
15
+ #comm
16
+
17
+ /**
18
+ * @param {toa.amqp.Communication} comm
19
+ * @param {toa.core.Locator} locator
20
+ * @param {string} label
21
+ */
22
+ constructor (comm, locator, label) {
23
+ super()
24
+
25
+ this.#exchange = name(locator, label)
26
+ this.#comm = comm
27
+
28
+ this.depends(comm)
29
+ }
30
+
31
+ async emit (message) {
32
+ await this.#comm.emit(this.#exchange, message)
33
+ }
34
+ }
35
+
36
+ exports.Emitter = Emitter
@@ -0,0 +1,62 @@
1
+ 'use strict'
2
+
3
+ const { Locator } = require('@toa.io/core')
4
+
5
+ const { Pointer } = require('./pointer')
6
+ const { Communication } = require('./communication')
7
+ const { Producer } = require('./producer')
8
+ const { Consumer } = require('./consumer')
9
+ const { Emitter } = require('./emitter')
10
+ const { Receiver } = require('./receiver')
11
+ const { Broadcast } = require('./broadcast')
12
+
13
+ const { SYSTEM } = require('./constants')
14
+
15
+ /**
16
+ * @implements {toa.core.bindings.Factory}
17
+ */
18
+ class Factory {
19
+ producer (locator, endpoints, component) {
20
+ const comm = this.#getCommunication(locator)
21
+
22
+ return new Producer(comm, locator, endpoints, component)
23
+ }
24
+
25
+ consumer (locator, endpoint) {
26
+ const comm = this.#getCommunication(locator)
27
+
28
+ return new Consumer(comm, locator, endpoint)
29
+ }
30
+
31
+ emitter (locator, label) {
32
+ const comm = this.#getCommunication(locator)
33
+
34
+ return new Emitter(comm, locator, label)
35
+ }
36
+
37
+ receiver (locator, label, group, receiver) {
38
+ const comm = this.#getCommunication(locator)
39
+
40
+ return new Receiver(comm, locator, label, group, receiver)
41
+ }
42
+
43
+ broadcast (name, group) {
44
+ const locator = new Locator(name, SYSTEM)
45
+ const comm = this.#getCommunication(locator)
46
+
47
+ return new Broadcast(comm, locator, group)
48
+ }
49
+
50
+ /**
51
+ *
52
+ * @param {toa.core.Locator} locator
53
+ * @return {toa.amqp.Communication}
54
+ */
55
+ #getCommunication (locator) {
56
+ const pointer = /** @type {toa.pointer.Pointer} */ new Pointer(locator)
57
+
58
+ return new Communication(pointer)
59
+ }
60
+ }
61
+
62
+ exports.Factory = Factory
@@ -1,10 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const { Factory } = require('./factory')
4
3
  const { deployment } = require('./deployment')
5
4
  const { annotation } = require('./annotation')
5
+ const { Factory } = require('./factory')
6
+
7
+ /** @type {toa.core.bindings.Properties} */
8
+ const properties = { async: true }
6
9
 
7
- exports.properties = { async: true }
10
+ exports.properties = properties
8
11
  exports.annotation = annotation
9
12
  exports.deployment = deployment
10
13
  exports.Factory = Factory
@@ -1,13 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { Pointer: Base } = require('@toa.io/pointer')
4
-
5
4
  const { PREFIX } = require('./constants')
6
5
 
7
- // noinspection JSClosureCompilerSyntax
8
- /**
9
- * @implements {toa.amqp.Pointer}
10
- */
11
6
  class Pointer extends Base {
12
7
  /**
13
8
  * @param {toa.core.Locator} locator
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('@toa.io/core')
4
+
5
+ const { name } = require('./queues')
6
+
7
+ class Producer extends Connector {
8
+ /** @type {toa.amqp.Communication} */
9
+ #comm
10
+
11
+ /** @type {toa.core.Locator} */
12
+ #locator
13
+
14
+ /** @type {string[]} */
15
+ #endpoints
16
+
17
+ /** @type {toa.core.Component} */
18
+ #component
19
+
20
+ /**
21
+ * @param {toa.amqp.Communication} comm
22
+ * @param {toa.core.Locator} locator
23
+ * @param {string[]} endpoints
24
+ * @param {toa.core.Component} component
25
+ */
26
+ constructor (comm, locator, endpoints, component) {
27
+ super()
28
+
29
+ this.#comm = comm
30
+ this.#locator = locator
31
+ this.#endpoints = endpoints
32
+ this.#component = component
33
+
34
+ this.depends(comm)
35
+ this.depends(component)
36
+ }
37
+
38
+ async open () {
39
+ await Promise.all(this.#endpoints.map((endpoint) => this.#endpoint(endpoint)))
40
+ }
41
+
42
+ async #endpoint (endpoint) {
43
+ const queue = name(this.#locator, endpoint)
44
+
45
+ await this.#comm.reply(queue, (request) => this.#component.invoke(endpoint, request))
46
+ }
47
+ }
48
+
49
+ exports.Producer = Producer
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ const { concat } = require('@toa.io/generic')
4
+
5
+ /**
6
+ * @param {toa.core.Locator} locator
7
+ * @param {string} endpoint
8
+ * @returns {string}
9
+ */
10
+ const name = (locator, endpoint) =>
11
+ locator.namespace + '.' + concat(locator.name, '.') + endpoint
12
+
13
+ exports.name = name
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('@toa.io/core')
4
+ const { name } = require('./queues')
5
+
6
+ class Receiver extends Connector {
7
+ /** @type {string} */
8
+ #exchange
9
+
10
+ /** @type {string} */
11
+ #group
12
+
13
+ /** @type {toa.amqp.Communication} */
14
+ #comm
15
+
16
+ /** @type {toa.core.Receiver} */
17
+ #receiver
18
+
19
+ /**
20
+ * @param {toa.amqp.Communication} comm
21
+ * @param {toa.core.Locator} locator
22
+ * @param {string} label
23
+ * @param {string} group
24
+ * @param {toa.core.Receiver} receiver
25
+ */
26
+ constructor (comm, locator, label, group, receiver) {
27
+ super()
28
+
29
+ this.#exchange = name(locator, label)
30
+ this.#group = group
31
+ this.#comm = comm
32
+ this.#receiver = receiver
33
+
34
+ this.depends(comm)
35
+ }
36
+
37
+ async open () {
38
+ await this.#comm.consume(this.#exchange, this.#group, (message) => this.#receiver.receive(message))
39
+ }
40
+ }
41
+
42
+ exports.Receiver = Receiver
@@ -4,6 +4,7 @@ const { generate } = require('randomstring')
4
4
  const mock = { uris: { construct: () => generate() }, Pointer: class {} }
5
5
 
6
6
  jest.mock('@toa.io/pointer', () => mock)
7
+
7
8
  const { annotation } = require('../')
8
9
 
9
10
  it('should export annotations', () => {
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ // region setup
4
+
5
+ const { generate } = require('randomstring')
6
+ const { Connector } = require('@toa.io/core')
7
+
8
+ const mock = {
9
+ communication: require('./communication.mock').communication,
10
+ queues: require('./queues.mock')
11
+ }
12
+
13
+ jest.mock('../source/queues', () => mock.queues)
14
+
15
+ const { Broadcast } = require('../source/broadcast')
16
+
17
+ it('should be', async () => {
18
+ expect(Broadcast).toBeDefined()
19
+ })
20
+
21
+ const comm = mock.communication()
22
+ const locator = /** @type {toa.core.Locator} */ { namespace: generate(), name: generate() }
23
+ const group = generate()
24
+
25
+ /** @type {toa.core.bindings.Broadcast} */
26
+ let broadcast
27
+
28
+ beforeEach(() => {
29
+ jest.clearAllMocks()
30
+
31
+ broadcast = new Broadcast(comm, locator, group)
32
+ })
33
+
34
+ // endregion
35
+
36
+ it('should be instance of Connector', async () => {
37
+ expect(broadcast).toBeInstanceOf(Connector)
38
+ })
39
+
40
+ it('should depend on communication', async () => {
41
+ expect(comm.link).toHaveBeenCalledWith(broadcast)
42
+ })
43
+
44
+ it('should transmit', async () => {
45
+ const label = generate()
46
+ const message = generate()
47
+
48
+ await broadcast.transmit(label, message)
49
+
50
+ expect(mock.queues.name).toHaveBeenCalledWith(locator, label)
51
+
52
+ const exchange = mock.queues.name.mock.results[0].value
53
+
54
+ expect(comm.emit).toHaveBeenCalledWith(exchange, message)
55
+ })
56
+
57
+ it('should receive', async () => {
58
+ const label = generate()
59
+ const process = jest.fn(async () => undefined)
60
+
61
+ await broadcast.receive(label, process)
62
+
63
+ expect(mock.queues.name).toHaveBeenCalledWith(locator, label)
64
+
65
+ const exchange = mock.queues.name.mock.results[0].value
66
+
67
+ expect(comm.consume).toHaveBeenCalledWith(exchange, group, expect.any(Function))
68
+ })
69
+
70
+ it('should create unique group if not provided', async () => {
71
+ jest.clearAllMocks()
72
+
73
+ broadcast = new Broadcast(comm, locator)
74
+
75
+ const label = generate()
76
+ const process = jest.fn(async () => undefined)
77
+
78
+ await broadcast.receive(label, process)
79
+
80
+ const group = comm.consume.mock.calls[0][1]
81
+
82
+ expect(group).toBeDefined()
83
+ })
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+
5
+ /**
6
+ * @return {jest.MockedObject<toa.amqp.Communication>}
7
+ */
8
+ const communication = () => (
9
+ /** @type {jest.MockedObject<toa.amqp.Communication>} */ {
10
+ connect: jest.fn(async () => undefined),
11
+ request: jest.fn(async () => generate()),
12
+ reply: jest.fn(async () => undefined),
13
+ emit: jest.fn(async () => undefined),
14
+ consume: jest.fn(async () => undefined),
15
+
16
+ link: jest.fn()
17
+ }
18
+ )
19
+
20
+ exports.communication = communication
@@ -0,0 +1,97 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { Connector } = require('@toa.io/core')
5
+ const mock = require('./comq.mock')
6
+
7
+ jest.mock('comq', () => mock.comq)
8
+
9
+ const { Communication } = require('../source/communication')
10
+
11
+ it('should be', async () => {
12
+ expect(Communication).toBeDefined()
13
+ })
14
+
15
+ /** @type {toa.pointer.Pointer} */
16
+ let pointer
17
+
18
+ /** @type {toa.amqp.Communication} */
19
+ let comm
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks()
23
+
24
+ pointer = /** @type {toa.pointer.Pointer} */ { reference: generate() }
25
+ comm = new Communication(pointer)
26
+ })
27
+
28
+ it('should be instance of Connector', async () => {
29
+ expect(comm).toBeInstanceOf(Connector)
30
+ })
31
+
32
+ it('should connect to a given pointer reference', async () => {
33
+ await comm.open()
34
+
35
+ expect(mock.comq.connect).toHaveBeenCalledWith(pointer.reference)
36
+ })
37
+
38
+ describe('connected', () => {
39
+ /** @type {jest.MockedObject<comq.IO>} */
40
+ let io
41
+
42
+ beforeEach(async () => {
43
+ await comm.open()
44
+
45
+ io = await mock.comq.connect.mock.results[0].value
46
+ })
47
+
48
+ it('should close', async () => {
49
+ await comm.close()
50
+
51
+ expect(io.seal).toHaveBeenCalled()
52
+ })
53
+
54
+ it('should dispose', async () => {
55
+ await comm.dispose()
56
+
57
+ expect(io.close).toHaveBeenCalled()
58
+ })
59
+
60
+ it('should bind reply', async () => {
61
+ const queue = generate()
62
+ const producer = jest.fn(async () => undefined)
63
+
64
+ await comm.reply(queue, producer)
65
+
66
+ expect(io.reply).toHaveBeenCalledWith(queue, producer)
67
+ })
68
+
69
+ it('should send request', async () => {
70
+ const queue = generate()
71
+ const request = generate()
72
+
73
+ const reply = await comm.request(queue, request)
74
+
75
+ expect(io.request).toHaveBeenCalledWith(queue, request)
76
+ expect(reply).toStrictEqual(await io.request.mock.results[0].value)
77
+ })
78
+
79
+ it('should emit', async () => {
80
+ const exchange = generate()
81
+ const message = generate()
82
+
83
+ await comm.emit(exchange, message)
84
+
85
+ expect(io.emit).toHaveBeenCalledWith(exchange, message)
86
+ })
87
+
88
+ it('should consume', async () => {
89
+ const exchange = generate()
90
+ const group = generate()
91
+ const consumer = jest.fn(async () => undefined)
92
+
93
+ await comm.consume(exchange, group, consumer)
94
+
95
+ expect(io.consume).toHaveBeenCalledWith(exchange, group, consumer)
96
+ })
97
+ })