@toa.io/core 1.0.0-alpha.24 → 1.0.0-alpha.246

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 (56) hide show
  1. package/package.json +7 -9
  2. package/src/call.js +12 -0
  3. package/src/cascade.js +7 -5
  4. package/src/component.js +41 -17
  5. package/src/composition.js +1 -1
  6. package/src/connector.js +21 -14
  7. package/src/context.js +4 -0
  8. package/src/contract/contract.js +22 -0
  9. package/src/contract/reply.js +26 -9
  10. package/src/contract/request.js +15 -5
  11. package/src/contract/schemas/query.yaml +14 -1
  12. package/src/discovery.js +13 -7
  13. package/src/effect.js +19 -0
  14. package/src/entities/changeset.js +5 -8
  15. package/src/entities/entity.js +48 -23
  16. package/src/entities/factory.js +15 -4
  17. package/src/entities/newid.js +11 -0
  18. package/src/entities/set.js +13 -0
  19. package/src/event.js +19 -1
  20. package/src/exceptions.js +26 -19
  21. package/src/exposition.js +3 -2
  22. package/src/guard.js +17 -0
  23. package/src/index.js +6 -0
  24. package/src/observation.js +1 -9
  25. package/src/operation.js +28 -7
  26. package/src/query/options.js +3 -2
  27. package/src/query.js +3 -1
  28. package/src/receiver.js +60 -11
  29. package/src/remote.js +7 -7
  30. package/src/state.js +69 -33
  31. package/src/transition.js +8 -19
  32. package/src/transmission.js +12 -3
  33. package/src/unmanaged.js +11 -0
  34. package/test/component.test.js +4 -3
  35. package/test/connector.fixtures.js +8 -0
  36. package/test/connector.test.js +20 -0
  37. package/test/contract/conditions.test.js +5 -5
  38. package/test/contract/request.test.js +7 -7
  39. package/test/discovery.test.js +56 -0
  40. package/test/entities/entity.test.js +0 -45
  41. package/test/entities/factory.test.js +3 -3
  42. package/test/event.test.js +4 -4
  43. package/test/receiver.fixtures.js +1 -0
  44. package/test/state.test.js +0 -14
  45. package/types/bindings.d.ts +7 -5
  46. package/types/bridges.ts +3 -0
  47. package/types/component.d.ts +4 -1
  48. package/types/extensions.d.ts +6 -3
  49. package/types/index.ts +1 -0
  50. package/types/message.d.ts +1 -0
  51. package/types/operations.d.ts +6 -0
  52. package/types/query.d.ts +2 -0
  53. package/types/remote.d.ts +18 -0
  54. package/types/request.d.ts +5 -0
  55. package/types/storages.d.ts +11 -9
  56. package/src/contract/conditions.js +0 -21
@@ -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,19 @@ 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
+
20
29
  #local
21
30
 
22
- /** @type {toa.core.bridges.Receiver} */
23
31
  #bridge
24
32
 
25
- /**
26
- *
27
- * @param {toa.norm.component.Receiver} definition
28
- * @param {toa.core.Component} local
29
- * @param {toa.core.bridges.Receiver} bridge
30
- */
31
33
  constructor (definition, local, bridge) {
32
34
  super()
33
35
 
@@ -36,6 +38,9 @@ class Receiver extends Connector {
36
38
  this.#conditioned = conditioned
37
39
  this.#adaptive = adaptive
38
40
  this.#endpoint = operation
41
+ this.#label = definition.label ?? operation
42
+ this.#destination = definition.destination ?? this.#label
43
+ this.#arguments = definition.arguments
39
44
 
40
45
  this.#local = local
41
46
  this.#bridge = bridge
@@ -46,7 +51,7 @@ class Receiver extends Connector {
46
51
 
47
52
  /** @hot */
48
53
  async receive (message) {
49
- const { payload, ...extensions } = message
54
+ const { payload, telemetry, ...extensions } = message
50
55
 
51
56
  if (this.#conditioned && await this.#bridge.condition(payload) === false) return
52
57
 
@@ -54,11 +59,55 @@ class Receiver extends Connector {
54
59
 
55
60
  add(request, extensions)
56
61
 
57
- await this.#local.invoke(this.#endpoint, request)
62
+ // continue the trace from the producer span
63
+ const remote = telemetry === undefined ? null : decode(telemetry)
64
+ const task = () => this.#process(request)
65
+
66
+ if (remote === null)
67
+ await task()
68
+ else
69
+ await run(remote, task)
70
+ }
71
+
72
+ async #process (request) {
73
+ /*
74
+ * The delivery span is created on behalf of the messaging destination, so that
75
+ * each consumer forms its own complete producer/consumer pair, and service graphs
76
+ * display fan-out correctly: producer -> destination -> each consumer
77
+ * (Tempo pairs spans one-to-one, thus multiple consumers can not pair
78
+ * with a single producer span, see grafana/tempo#5408)
79
+ */
80
+ const delivery = {
81
+ name: `${this.#label} deliver`,
82
+ kind: 'producer',
83
+ service: this.#destination,
84
+ attributes: { 'messaging.destination.name': this.#destination }
85
+ }
86
+
87
+ const options = {
88
+ name: `${this.#label} process`,
89
+ kind: 'consumer',
90
+ service: this.#local.locator.id,
91
+ attributes: { 'messaging.destination.name': this.#destination }
92
+ }
93
+
94
+ return console.span(delivery, async () => console.span(options, async () => {
95
+ try {
96
+ await this.#local.invoke(this.#endpoint, request)
97
+ } catch (error) {
98
+ console.error('Receiver error', {
99
+ component: this.#local.locator.id,
100
+ endpoint: this.#endpoint,
101
+ error
102
+ })
103
+
104
+ throw error
105
+ }
106
+ }))
58
107
  }
59
108
 
60
109
  async #request (payload) {
61
- return this.#adaptive ? await this.#bridge.request(payload) : { input: payload }
110
+ return this.#adaptive ? await this.#bridge.request(payload, ...(this.#arguments ?? [])) : { input: payload }
62
111
  }
63
112
  }
64
113
 
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
 
package/src/state.js CHANGED
@@ -1,73 +1,109 @@
1
1
  'use strict'
2
2
 
3
- const { empty } = require('@toa.io/generic')
4
-
5
- const {
6
- StatePreconditionException,
7
- StateNotFoundException
8
- } = require('./exceptions')
3
+ const { StatePreconditionException, StateNotFoundException } = require('./exceptions')
9
4
 
10
5
  class State {
6
+ storage
7
+
11
8
  #associated
12
- #storage
13
- #entity
9
+ #entities
14
10
  #emission
15
11
 
16
12
  constructor (storage, entity, emission, associated) {
17
- this.#storage = storage
18
- this.#entity = entity
13
+ this.storage = storage
14
+ this.#entities = entity
19
15
  this.#emission = emission
20
16
  this.#associated = associated === true
21
17
  }
22
18
 
23
19
  init (id) {
24
- return this.#entity.init(id)
20
+ return this.#entities.init(id)
21
+ }
22
+
23
+ fit (values) {
24
+ return this.#entities.fit(values)
25
25
  }
26
26
 
27
27
  async object (query) {
28
- const record = await this.#storage.get(query)
28
+ const record = await this.storage.get(query)
29
29
 
30
30
  if (record === null) {
31
- if (this.#associated && query.id !== undefined && query.version === undefined) {
31
+ if (this.#associated && query.id !== undefined && query.criteria === undefined && query.version === undefined)
32
32
  return this.init(query.id)
33
- } else if (query.version !== undefined) throw new StatePreconditionException()
34
- }
33
+ else if (query.version !== undefined)
34
+ throw new StatePreconditionException()
35
35
 
36
- if (record === null) {
37
36
  return null
38
- } else {
39
- return this.#entity.object(record)
40
- }
37
+ } else
38
+ return this.#entities.object(record)
41
39
  }
42
40
 
43
41
  async objects (query) {
44
- const recordset = await this.#storage.find(query)
42
+ const recordset = await this.storage.find(query)
43
+ const missing = this.#associated && query.ids !== undefined && recordset.length < query.ids.length
44
+ const init = missing ? query.ids.filter((id) => !recordset.some((record) => record.id === id)) : undefined
45
+
46
+ return this.#entities.objects(recordset, init)
47
+ }
45
48
 
46
- return this.#entity.objects(recordset)
49
+ async stream (query) {
50
+ return this.storage.stream(query)
47
51
  }
48
52
 
49
53
  changeset (query) {
50
- return this.#entity.changeset(query)
54
+ return this.#entities.changeset(query)
51
55
  }
52
56
 
53
57
  none () {
54
58
  return null
55
59
  }
56
60
 
57
- async commit (state) {
58
- const event = state.event()
61
+ async ensure (query, properties, input) {
62
+ const object = this.#entities.init()
63
+ const blank = object.get()
59
64
 
60
- let ok = true
65
+ Object.assign(blank, properties)
61
66
 
62
- if (!empty(event.changeset)) {
63
- const object = state.get()
67
+ object.set(blank)
64
68
 
65
- ok = await this.#storage.store(object)
69
+ const record = await this.storage.ensure(query, properties, object.get())
66
70
 
67
- // #20
68
- if (ok === true) {
69
- await this.#emission.emit(event)
70
- }
71
+ if (record.id !== blank.id) // exists
72
+ return this.#entities.object(record)
73
+
74
+ const event = object.event(input)
75
+
76
+ await this.#emission.emit(event)
77
+
78
+ return object
79
+ }
80
+
81
+ async commit (state, input) {
82
+ if (state.constructor.name === 'EntitySet')
83
+ return this.massCommit(state, input)
84
+
85
+ const data = state.get()
86
+ const ok = await this.storage.store(data)
87
+
88
+ // #20
89
+ if (ok === true) {
90
+ const event = state.event(input)
91
+
92
+ await this.#emission.emit(event)
93
+ }
94
+
95
+ return ok
96
+ }
97
+
98
+ async massCommit (state, input) {
99
+ const data = state.get()
100
+ const ok = await this.storage.massStore(data)
101
+
102
+ // #20
103
+ if (ok === true) {
104
+ const events = state.events(input)
105
+
106
+ await Promise.all(events.map((event) => this.#emission.emit(event)))
71
107
  }
72
108
 
73
109
  return ok
@@ -76,7 +112,7 @@ class State {
76
112
  async apply (state) {
77
113
  const changeset = state.export()
78
114
 
79
- const result = await this.#storage.upsert(state.query, changeset)
115
+ const result = await this.storage.upsert(state.query, changeset)
80
116
 
81
117
  if (result === null) {
82
118
  if (state.query.version !== undefined) {