@toa.io/core 1.0.0-alpha.26 → 1.0.0-alpha.264

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 (60) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +7 -9
  3. package/src/call.js +19 -1
  4. package/src/cascade.js +7 -5
  5. package/src/component.js +41 -17
  6. package/src/composition.js +1 -1
  7. package/src/connector.js +21 -14
  8. package/src/context.js +6 -0
  9. package/src/contract/contract.js +22 -0
  10. package/src/contract/reply.js +26 -9
  11. package/src/contract/request.js +19 -5
  12. package/src/contract/schemas/index.js +1 -0
  13. package/src/contract/schemas/query.yaml +14 -1
  14. package/src/contract/schemas/source.yaml +23 -0
  15. package/src/discovery.js +13 -7
  16. package/src/effect.js +19 -0
  17. package/src/entities/changeset.js +5 -8
  18. package/src/entities/entity.js +58 -22
  19. package/src/entities/factory.js +15 -4
  20. package/src/entities/newid.js +11 -0
  21. package/src/entities/set.js +13 -0
  22. package/src/event.js +19 -1
  23. package/src/exceptions.js +26 -19
  24. package/src/exposition.js +3 -2
  25. package/src/guard.js +17 -0
  26. package/src/index.js +6 -0
  27. package/src/observation.js +1 -9
  28. package/src/operation.js +28 -7
  29. package/src/query/options.js +3 -2
  30. package/src/query.js +3 -1
  31. package/src/receiver.js +67 -11
  32. package/src/remote.js +7 -7
  33. package/src/state.js +69 -33
  34. package/src/transition.js +8 -19
  35. package/src/transmission.js +12 -3
  36. package/src/unmanaged.js +11 -0
  37. package/test/component.test.js +4 -3
  38. package/test/connector.fixtures.js +8 -0
  39. package/test/connector.test.js +20 -0
  40. package/test/contract/conditions.test.js +5 -5
  41. package/test/contract/request.test.js +46 -7
  42. package/test/discovery.test.js +56 -0
  43. package/test/entities/entity.test.js +24 -41
  44. package/test/entities/factory.test.js +3 -3
  45. package/test/event.test.js +4 -4
  46. package/test/receiver.fixtures.js +1 -0
  47. package/test/state.test.js +0 -14
  48. package/types/bindings.d.ts +7 -5
  49. package/types/bridges.ts +3 -0
  50. package/types/component.d.ts +4 -1
  51. package/types/context.d.ts +3 -0
  52. package/types/extensions.d.ts +7 -4
  53. package/types/index.ts +2 -1
  54. package/types/message.d.ts +1 -0
  55. package/types/operations.d.ts +6 -0
  56. package/types/query.d.ts +2 -0
  57. package/types/remote.d.ts +18 -0
  58. package/types/request.d.ts +15 -0
  59. package/types/storages.d.ts +11 -9
  60. package/src/contract/conditions.js +0 -21
@@ -1,23 +1,23 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- difference,
5
- newid
6
- } = require('@toa.io/generic')
7
-
8
- const { EntityContractException } = require('../exceptions')
3
+ const { difference } = require('@toa.io/generic')
4
+ const { EntityContractException, EntityGuardException } = require('../exceptions')
5
+ const { newid } = require('./newid')
9
6
 
10
7
  class Entity {
8
+ deleted = false
11
9
  #schema
10
+ #guards
12
11
  #origin = null
13
12
  #state
14
13
 
15
- constructor (schema, argument) {
14
+ constructor (schema, argument, guards) {
16
15
  this.#schema = schema
16
+ this.#guards = guards
17
17
 
18
18
  if (typeof argument === 'object') {
19
19
  const object = structuredClone(argument)
20
- this.set(object)
20
+ this.#set(object)
21
21
  this.#origin = argument
22
22
  } else {
23
23
  const id = argument === undefined ? newid() : argument
@@ -29,37 +29,73 @@ class Entity {
29
29
  return this.#state
30
30
  }
31
31
 
32
- set (value) {
33
- const error = this.#schema.fit(value)
32
+ set (value, optional = false) {
33
+ if (!optional)
34
+ this.#guard(value)
35
+
36
+ const error = optional ? this.#schema.fitOptional(value) : this.#schema.fit(value)
34
37
 
35
- if (error !== null) throw new EntityContractException(error)
38
+ if (error !== null)
39
+ throw new EntityContractException(error, value)
36
40
 
41
+ this.#revive(value)
37
42
  this.#set(value)
38
43
  }
39
44
 
40
- event () {
45
+ event (input = undefined) {
41
46
  return {
42
47
  origin: this.#origin,
43
48
  state: this.#state,
44
- changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state)
49
+ changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state),
50
+ trailers: this.#state._trailers,
51
+ input
45
52
  }
46
53
  }
47
54
 
48
55
  #init (id) {
49
- const value = {
50
- ...this.#schema.defaults({ id }),
51
- _version: 0,
52
- _created: Date.now()
56
+ const value = { id, _version: 0 }
57
+
58
+ this.set(value, true)
59
+ }
60
+
61
+ #guard (value) {
62
+ if (this.#guards === undefined)
63
+ return
64
+
65
+ for (const guard of this.#guards) {
66
+ const ok = guard.fit(value, this.#origin)
67
+
68
+ if (ok === false)
69
+ throw new EntityGuardException(guard.name, value)
53
70
  }
71
+ }
54
72
 
55
- this.#set(value)
73
+ // deletion is only expressed as a new _deleted timestamp,
74
+ // so committing over a tombstone without touching it means revival
75
+ #revive (value) {
76
+ if (this.#origin?._deleted == null || value._deleted !== this.#origin._deleted)
77
+ return
78
+
79
+ value._deleted = null
80
+ this.deleted = false
56
81
  }
57
82
 
58
83
  #set (value) {
59
- Object.defineProperty(value, 'id', {
60
- writable: false,
61
- configurable: false
62
- })
84
+ if (!('_trailers' in value))
85
+ Object.defineProperty(value, '_trailers', {
86
+ writable: false,
87
+ configurable: false,
88
+ enumerable: false,
89
+ value: {}
90
+ })
91
+
92
+ if (!('_created' in value)) {
93
+ value._created = Date.now()
94
+ value._updated ??= value._created
95
+ }
96
+
97
+ if ('_deleted' in value && value._deleted !== null)
98
+ this.deleted = true
63
99
 
64
100
  if (this.#state !== undefined) {
65
101
  value._updated = Date.now()
@@ -1,27 +1,38 @@
1
1
  'use strict'
2
2
 
3
+ const { newid } = require('./newid')
3
4
  const { Entity } = require('./entity')
4
5
  const { EntitySet } = require('./set')
5
6
  const { Changeset } = require('./changeset')
6
7
 
7
8
  class Factory {
8
9
  #schema
10
+ #guards
9
11
 
10
- constructor (schema) {
12
+ constructor (schema, guards) {
11
13
  this.#schema = schema
14
+ this.#guards = guards
15
+ }
16
+
17
+ fit (values) {
18
+ this.#schema.validate({ id: newid(), ...values }, 'Entity')
12
19
  }
13
20
 
14
21
  init (id) {
15
- return new Entity(this.#schema, id)
22
+ return new Entity(this.#schema, id, this.#guards)
16
23
  }
17
24
 
18
25
  object (record) {
19
- return new Entity(this.#schema, record)
26
+ return new Entity(this.#schema, record, this.#guards)
20
27
  }
21
28
 
22
- objects (recordset) {
29
+ objects (recordset, init) {
23
30
  const set = recordset.map((record) => this.object(record))
24
31
 
32
+ if (init !== undefined)
33
+ for (const id of init)
34
+ set.unshift(this.init(id))
35
+
25
36
  return new EntitySet(set)
26
37
  }
27
38
 
@@ -0,0 +1,11 @@
1
+ const uuid = require('uuid')
2
+
3
+ function newid () {
4
+ const buf = Buffer.alloc(16)
5
+
6
+ uuid.v7(undefined, buf)
7
+
8
+ return buf.toString('hex')
9
+ }
10
+
11
+ module.exports = { newid }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { SystemException } = require("../exceptions")
4
+
3
5
  class EntitySet {
4
6
  #set
5
7
 
@@ -10,6 +12,17 @@ class EntitySet {
10
12
  get () {
11
13
  return this.#set.map((entity) => entity.get())
12
14
  }
15
+
16
+ set (values) {
17
+ if (values.length !== this.#set.length)
18
+ throw new SystemException('Objects array must not be modified')
19
+
20
+ values.forEach((value, index) => this.#set[index].set(value))
21
+ }
22
+
23
+ events (input = undefined) {
24
+ return this.#set.map((entity) => entity.event(input))
25
+ }
13
26
  }
14
27
 
15
28
  exports.EntitySet = EntitySet
package/src/event.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { console, current, encode } = require('openspan')
3
4
  const { Connector } = require('./connector')
4
5
 
5
6
  /**
@@ -12,11 +13,15 @@ class Event extends Connector {
12
13
  #conditioned
13
14
  #subjective
14
15
 
16
+ /** @type {string} */
17
+ #label
18
+
15
19
  constructor (definition, emitter, bridge = undefined) {
16
20
  super()
17
21
 
18
22
  this.#conditioned = definition.conditioned
19
23
  this.#subjective = definition.subjective
24
+ this.#label = definition.label ?? 'event'
20
25
  this.#emitter = emitter
21
26
  this.#bridge = bridge
22
27
 
@@ -32,7 +37,20 @@ class Event extends Connector {
32
37
  /** @type {toa.core.Message} */
33
38
  const message = { payload }
34
39
 
35
- await this.#emitter.emit(message)
40
+ const options = {
41
+ name: `${this.#label} publish`,
42
+ kind: 'producer',
43
+ attributes: { 'messaging.destination.name': this.#label }
44
+ }
45
+
46
+ await console.span(options, async () => {
47
+ const context = current()
48
+
49
+ if (context !== undefined)
50
+ message.telemetry = encode(context)
51
+
52
+ await this.#emitter.emit(message)
53
+ })
36
54
  }
37
55
  }
38
56
  }
package/src/exceptions.js CHANGED
@@ -4,7 +4,6 @@ const { swap } = require('@toa.io/generic')
4
4
 
5
5
  const codes = {
6
6
  System: 0,
7
- NotImplemented: 10,
8
7
 
9
8
  Contract: 200,
10
9
  RequestSyntax: 201,
@@ -12,6 +11,7 @@ const codes = {
12
11
  RequestConflict: 203,
13
12
  ResponseContract: 211,
14
13
  EntityContract: 212,
14
+ EntityGuard: 213,
15
15
  QuerySyntax: 221,
16
16
 
17
17
  State: 300,
@@ -32,9 +32,12 @@ class Exception {
32
32
  code
33
33
  message
34
34
 
35
- constructor (code, message) {
35
+ constructor (code, message, cause) {
36
36
  this.code = code
37
37
  this.message = message
38
+
39
+ if (cause !== undefined)
40
+ this.cause = cause
38
41
  }
39
42
  }
40
43
 
@@ -49,31 +52,30 @@ class SystemException extends Exception {
49
52
  }
50
53
 
51
54
  class ContractException extends Exception {
52
- keyword
53
- property
54
- schema
55
- path
56
-
57
- constructor (code, error) {
58
- super(code || codes.Contract, error.message)
59
-
60
- this.keyword = error.keyword
61
- this.property = error.property
62
- this.schema = error.schema
63
- this.path = error.path
55
+ constructor (code, error, cause) {
56
+ super(code || codes.Contract, typeof error === 'string' ? error : error?.message, cause)
57
+
58
+ if (typeof error === 'object' && error !== null)
59
+ for (const k of ['keyword', 'property', 'schema', 'path', 'params'])
60
+ if (k in error)
61
+ this[k] = error[k]
64
62
  }
65
63
  }
66
64
 
67
65
  class RequestContractException extends ContractException {
68
- constructor (error) { super(codes.RequestContract, error) }
66
+ constructor (error, cause) { super(codes.RequestContract, error, cause) }
69
67
  }
70
68
 
71
69
  class ResponseContractException extends ContractException {
72
- constructor (error) { super(codes.ResponseContract, error) }
70
+ constructor (error, cause) { super(codes.ResponseContract, error, cause) }
73
71
  }
74
72
 
75
73
  class EntityContractException extends ContractException {
76
- constructor (error) { super(codes.EntityContract, error) }
74
+ constructor (error, cause) { super(codes.EntityContract, error, cause) }
75
+ }
76
+
77
+ class EntityGuardException extends ContractException {
78
+ constructor (name, cause) { super(codes.EntityGuard, name, cause) }
77
79
  }
78
80
 
79
81
  // #region exports
@@ -82,14 +84,19 @@ exports.SystemException = SystemException
82
84
  exports.RequestContractException = RequestContractException
83
85
  exports.ResponseContractException = ResponseContractException
84
86
  exports.EntityContractException = EntityContractException
87
+ exports.EntityGuardException = EntityGuardException
85
88
 
86
89
  for (const [name, code] of Object.entries(codes)) {
87
90
  const classname = name + 'Exception'
88
91
 
89
92
  if (exports[classname] === undefined) {
90
93
  exports[classname] = class extends Exception {
91
- constructor (message) {
92
- super(code, message ?? classname)
94
+ constructor (message, cause) {
95
+ message = message
96
+ ? `${classname}: ${message}`
97
+ : classname
98
+
99
+ super(code, message ?? classname, cause)
93
100
  }
94
101
  }
95
102
  }
package/src/exposition.js CHANGED
@@ -20,8 +20,9 @@ class Exposition extends Connector {
20
20
  }
21
21
 
22
22
  const expose = (manifest) => {
23
- const { namespace, name, operations, events, entity } = manifest
24
- return { namespace, name, operations, events, entity }
23
+ const { namespace, name, entity, operations, events } = manifest
24
+
25
+ return { namespace, name, entity, operations, events }
25
26
  }
26
27
 
27
28
  exports.Exposition = Exposition
package/src/guard.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ class Guard {
4
+ name
5
+ #bridge
6
+
7
+ constructor (name, bridge) {
8
+ this.name = name
9
+ this.#bridge = bridge
10
+ }
11
+
12
+ fit (state, origin) {
13
+ return this.#bridge.fit(state, origin)
14
+ }
15
+ }
16
+
17
+ exports.Guard = Guard
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ const { Composition } = require('./composition')
5
5
  const { Connector } = require('./connector')
6
6
  const { Context } = require('./context')
7
7
  const { Discovery } = require('./discovery')
8
+ const { Effect } = require('./effect')
8
9
  const { Emission } = require('./emission')
9
10
  const { Event } = require('./event')
10
11
  const { Exposition } = require('./exposition')
@@ -19,6 +20,8 @@ const { Component } = require('./component')
19
20
  const { State } = require('./state')
20
21
  const { Transition } = require('./transition')
21
22
  const { Transmission } = require('./transmission')
23
+ const { Unmanaged } = require('./unmanaged')
24
+ const { Guard } = require('./guard')
22
25
 
23
26
  exports.entities = require('./entities')
24
27
  exports.exceptions = require('./exceptions')
@@ -32,6 +35,7 @@ exports.Composition = Composition
32
35
  exports.Connector = Connector
33
36
  exports.Context = Context
34
37
  exports.Discovery = Discovery
38
+ exports.Effect = Effect
35
39
  exports.Emission = Emission
36
40
  exports.Event = Event
37
41
  exports.Exposition = Exposition
@@ -45,3 +49,5 @@ exports.Remote = Remote
45
49
  exports.State = State
46
50
  exports.Transition = Transition
47
51
  exports.Transmission = Transmission
52
+ exports.Unmanaged = Unmanaged
53
+ exports.Guard = Guard
@@ -3,16 +3,8 @@
3
3
  const { Operation } = require('./operation')
4
4
 
5
5
  class Observation extends Operation {
6
- async acquire (store) {
7
- const scope = await this.query(store.request.query)
8
- const state = scope === null ? null : scope.get()
9
-
10
- store.scope = scope
11
- store.state = state
12
- }
13
-
14
6
  async run (store) {
15
- if (store.scope === null) store.reply = null
7
+ if (store.scope === null || (store.scope?.deleted === true && store.request.query?.options?.deleted !== true)) store.reply = null
16
8
  else await super.run(store)
17
9
  }
18
10
  }
package/src/operation.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { Connector } = require('./connector')
4
- const { SystemException } = require('./exceptions')
4
+ const { SystemException, RequestContractException } = require('./exceptions')
5
+ const { Readable } = require('node:stream')
5
6
 
6
7
  class Operation extends Connector {
7
8
  scope
@@ -26,8 +27,15 @@ class Operation extends Connector {
26
27
 
27
28
  async invoke (request) {
28
29
  try {
29
- if (request.authentic !== true) this.#contracts.request.fit(request)
30
- if ('query' in request) request.query = this.#query.parse(request.query)
30
+ if (request.authentic !== true)
31
+ this.#contracts.request.fit(request)
32
+
33
+ if ('query' in request)
34
+ request.query = this.#query.parse(request.query)
35
+
36
+ // validate entity
37
+ if ('entity' in request)
38
+ this.scope.fit(request.entity)
31
39
 
32
40
  const store = { request }
33
41
 
@@ -47,14 +55,24 @@ class Operation extends Connector {
47
55
  return store.reply
48
56
  }
49
57
 
50
- async acquire () {}
58
+ async acquire (store) {
59
+ if (this.#scope === 'none')
60
+ return
61
+
62
+ const scope = await this.query(store.request.query)
63
+ const raw = scope === null || scope instanceof Readable
64
+
65
+ store.scope = scope
66
+ store.state = raw ? scope : scope.get()
67
+ }
51
68
 
52
69
  async run (store) {
53
70
  const { request, state } = store
54
- // noinspection UnnecessaryLocalVariableJS
55
- const reply = await this.#cascade.run(request.input, state) || {}
71
+ const reply = await this.#cascade.run(request.input, state)
56
72
 
57
- // this.#contracts.reply.fit(reply)
73
+ // validate reply only on local environments
74
+ if (process.env.TOA_ENV === 'local' && !(reply instanceof Readable))
75
+ this.#contracts.reply.fit(reply)
58
76
 
59
77
  store.reply = reply
60
78
  }
@@ -62,6 +80,9 @@ class Operation extends Connector {
62
80
  async commit () {}
63
81
 
64
82
  async query (query) {
83
+ if (query === undefined)
84
+ throw new RequestContractException('Request query is required')
85
+
65
86
  return this.scope[this.#scope](query)
66
87
  }
67
88
  }
@@ -33,8 +33,9 @@ const projection = (projection, properties) => {
33
33
  }
34
34
  }
35
35
 
36
- if (projection.includes('_version') === false)
37
- projection.push('_version')
36
+ for (const property of ['_version', '_created', '_updated', '_deleted'])
37
+ if (!projection.includes(property))
38
+ projection.push(property)
38
39
  }
39
40
 
40
41
  exports.options = options
package/src/query.js CHANGED
@@ -19,13 +19,15 @@ class Query {
19
19
  parse (query) {
20
20
  /** @type {toa.core.storages.Query} */
21
21
  const result = {}
22
- const { id, version, criteria, ...rest } = query
22
+ const { id, ids, version, criteria, search, ...rest } = query
23
23
 
24
24
  const options = this.#options(rest)
25
25
 
26
26
  if (id !== undefined) result.id = id
27
+ if (ids !== undefined) result.ids = ids
27
28
  if (version !== undefined) result.version = version
28
29
  if (criteria !== undefined) result.criteria = parse.criteria(criteria, this.#properties)
30
+ if (search !== undefined) result.search = search
29
31
  if (options !== undefined) result.options = options
30
32
 
31
33
  return result
package/src/receiver.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { console, decode, run } = require('openspan')
3
4
  const { add } = require('@toa.io/generic')
4
5
  const { Connector } = require('./connector')
5
6
 
@@ -16,18 +17,22 @@ class Receiver extends Connector {
16
17
  /** @type {string} */
17
18
  #endpoint
18
19
 
19
- /** @type {toa.core.Component} */
20
+ /** @type {string} */
21
+ #label
22
+
23
+ /** @type {string} */
24
+ #destination
25
+
26
+ /** @type {unknown[]} */
27
+ #arguments
28
+
29
+ /** @type {toa.core.Source} */
30
+ #origin
31
+
20
32
  #local
21
33
 
22
- /** @type {toa.core.bridges.Receiver} */
23
34
  #bridge
24
35
 
25
- /**
26
- *
27
- * @param {toa.norm.component.Receiver} definition
28
- * @param {toa.core.Component} local
29
- * @param {toa.core.bridges.Receiver} bridge
30
- */
31
36
  constructor (definition, local, bridge) {
32
37
  super()
33
38
 
@@ -36,6 +41,10 @@ class Receiver extends Connector {
36
41
  this.#conditioned = conditioned
37
42
  this.#adaptive = adaptive
38
43
  this.#endpoint = operation
44
+ this.#label = definition.label ?? operation
45
+ this.#destination = definition.destination ?? this.#label
46
+ this.#arguments = definition.arguments
47
+ this.#origin = definition.origin
39
48
 
40
49
  this.#local = local
41
50
  this.#bridge = bridge
@@ -46,7 +55,7 @@ class Receiver extends Connector {
46
55
 
47
56
  /** @hot */
48
57
  async receive (message) {
49
- const { payload, ...extensions } = message
58
+ const { payload, telemetry, ...extensions } = message
50
59
 
51
60
  if (this.#conditioned && await this.#bridge.condition(payload) === false) return
52
61
 
@@ -54,11 +63,58 @@ class Receiver extends Connector {
54
63
 
55
64
  add(request, extensions)
56
65
 
57
- await this.#local.invoke(this.#endpoint, request)
66
+ // set after `add`, so that a message field can not spoof the origin
67
+ if (this.#origin !== undefined) request.source = this.#origin
68
+
69
+ // continue the trace from the producer span
70
+ const remote = telemetry === undefined ? null : decode(telemetry)
71
+ const task = () => this.#process(request)
72
+
73
+ if (remote === null)
74
+ await task()
75
+ else
76
+ await run(remote, task)
77
+ }
78
+
79
+ async #process (request) {
80
+ /*
81
+ * The delivery span is created on behalf of the messaging destination, so that
82
+ * each consumer forms its own complete producer/consumer pair, and service graphs
83
+ * display fan-out correctly: producer -> destination -> each consumer
84
+ * (Tempo pairs spans one-to-one, thus multiple consumers can not pair
85
+ * with a single producer span, see grafana/tempo#5408)
86
+ */
87
+ const delivery = {
88
+ name: `${this.#label} deliver`,
89
+ kind: 'producer',
90
+ service: this.#destination,
91
+ attributes: { 'messaging.destination.name': this.#destination }
92
+ }
93
+
94
+ const options = {
95
+ name: `${this.#label} process`,
96
+ kind: 'consumer',
97
+ service: this.#local.locator.id,
98
+ attributes: { 'messaging.destination.name': this.#destination }
99
+ }
100
+
101
+ return console.span(delivery, async () => console.span(options, async () => {
102
+ try {
103
+ await this.#local.invoke(this.#endpoint, request)
104
+ } catch (error) {
105
+ console.error('Receiver error', {
106
+ component: this.#local.locator.id,
107
+ endpoint: this.#endpoint,
108
+ error
109
+ })
110
+
111
+ throw error
112
+ }
113
+ }))
58
114
  }
59
115
 
60
116
  async #request (payload) {
61
- return this.#adaptive ? await this.#bridge.request(payload) : { input: payload }
117
+ return this.#adaptive ? await this.#bridge.request(payload, ...(this.#arguments ?? [])) : { input: payload }
62
118
  }
63
119
  }
64
120
 
package/src/remote.js CHANGED
@@ -1,16 +1,16 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
4
-
3
+ const assert = require('node:assert')
5
4
  const { Component } = require('./component')
6
5
 
7
6
  class Remote extends Component {
8
- async open () {
9
- console.info(`Remote '${this.locator.id}' connected`)
10
- }
7
+ kind = 'client'
8
+
9
+ explain (endpoint) {
10
+ assert.ok(endpoint in this.operations,
11
+ `Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
11
12
 
12
- async dispose () {
13
- console.info(`Remote '${this.locator.id}' disconnected`)
13
+ return this.operations[endpoint].explain()
14
14
  }
15
15
  }
16
16