@toa.io/core 0.1.0-alpha.12

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 (68) hide show
  1. package/LICENSE +22 -0
  2. package/package.json +28 -0
  3. package/src/assignment.js +22 -0
  4. package/src/call.js +29 -0
  5. package/src/cascade.js +32 -0
  6. package/src/composition.js +26 -0
  7. package/src/connector.js +122 -0
  8. package/src/context.js +43 -0
  9. package/src/contract/conditions.js +21 -0
  10. package/src/contract/index.js +7 -0
  11. package/src/contract/reply.js +22 -0
  12. package/src/contract/request.js +49 -0
  13. package/src/contract/schemas/error.yaml +7 -0
  14. package/src/contract/schemas/index.js +7 -0
  15. package/src/contract/schemas/query.yaml +31 -0
  16. package/src/discovery.js +33 -0
  17. package/src/emission.js +23 -0
  18. package/src/entities/changeset.js +46 -0
  19. package/src/entities/entity.js +48 -0
  20. package/src/entities/factory.js +33 -0
  21. package/src/entities/index.js +5 -0
  22. package/src/entities/set.js +15 -0
  23. package/src/event.js +33 -0
  24. package/src/exceptions.js +88 -0
  25. package/src/exposition.js +24 -0
  26. package/src/index.js +43 -0
  27. package/src/locator.js +49 -0
  28. package/src/observation.js +19 -0
  29. package/src/operation.js +61 -0
  30. package/src/query/criteria.js +41 -0
  31. package/src/query/options.js +40 -0
  32. package/src/query.js +36 -0
  33. package/src/receiver.js +36 -0
  34. package/src/remote.js +17 -0
  35. package/src/runtime.js +38 -0
  36. package/src/state.js +95 -0
  37. package/src/transition.js +50 -0
  38. package/src/transmission.js +33 -0
  39. package/test/call.fixtures.js +25 -0
  40. package/test/call.test.js +52 -0
  41. package/test/cascade.fixtures.js +11 -0
  42. package/test/cascade.test.js +42 -0
  43. package/test/connector.fixtures.js +40 -0
  44. package/test/connector.test.js +199 -0
  45. package/test/contract/conditions.test.js +26 -0
  46. package/test/contract/contract.fixtures.js +27 -0
  47. package/test/contract/request.test.js +99 -0
  48. package/test/emission.fixtures.js +16 -0
  49. package/test/emission.test.js +35 -0
  50. package/test/entities/entity.fixtures.js +26 -0
  51. package/test/entities/entity.test.js +64 -0
  52. package/test/entities/factory.fixtures.js +18 -0
  53. package/test/entities/factory.test.js +48 -0
  54. package/test/entities/set.fixtures.js +11 -0
  55. package/test/entities/set.test.js +12 -0
  56. package/test/event.fixtures.js +28 -0
  57. package/test/event.test.js +106 -0
  58. package/test/locator.test.js +34 -0
  59. package/test/query.fixtures.js +100 -0
  60. package/test/query.test.js +86 -0
  61. package/test/receiver.fixtures.js +22 -0
  62. package/test/receiver.test.js +66 -0
  63. package/test/runtime.fixtures.js +19 -0
  64. package/test/runtime.test.js +40 -0
  65. package/test/state.fixtures.js +46 -0
  66. package/test/state.test.js +54 -0
  67. package/test/transmission.fixtures.js +15 -0
  68. package/test/transmission.test.js +46 -0
package/src/event.js ADDED
@@ -0,0 +1,33 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('./connector')
4
+
5
+ class Event extends Connector {
6
+ #binding
7
+ #bridge
8
+ #conditioned
9
+ #subjective
10
+
11
+ constructor (definition, binding, bridge = undefined) {
12
+ super()
13
+
14
+ this.#conditioned = definition.conditioned
15
+ this.#subjective = definition.subjective
16
+ this.#binding = binding
17
+ this.#bridge = bridge
18
+
19
+ this.depends(binding)
20
+
21
+ if (bridge !== undefined) this.depends(bridge)
22
+ }
23
+
24
+ async emit (event) {
25
+ if (this.#conditioned === false || await this.#bridge.condition(event) === true) {
26
+ const payload = this.#subjective ? await this.#bridge.payload(event) : event.state
27
+
28
+ await this.#binding.emit(payload)
29
+ }
30
+ }
31
+ }
32
+
33
+ exports.Event = Event
@@ -0,0 +1,88 @@
1
+ 'use strict'
2
+
3
+ const codes = {
4
+ System: 0,
5
+ NotImplemented: 10,
6
+
7
+ Contract: 200,
8
+ RequestSyntax: 201,
9
+ RequestContract: 202,
10
+ RequestConflict: 203,
11
+ ResponseContract: 211,
12
+ EntityContract: 212,
13
+ QuerySyntax: 221,
14
+
15
+ State: 300,
16
+ StateNotFound: 302,
17
+ StatePrecondition: 303,
18
+ StateConcurrency: 304,
19
+ StateInitialization: 305,
20
+
21
+ Communication: 400,
22
+ Transmission: 401
23
+ }
24
+
25
+ class Exception {
26
+ code
27
+ message
28
+
29
+ constructor (code, message) {
30
+ this.code = code
31
+ this.message = message
32
+ }
33
+ }
34
+
35
+ class SystemException extends Exception {
36
+ stack
37
+
38
+ constructor (error) {
39
+ super(codes.System, error.message)
40
+
41
+ if (error.stack !== undefined && process.env.TOA_ENV === 'dev') this.stack = error.stack
42
+ }
43
+ }
44
+
45
+ class ContractException extends Exception {
46
+ keyword
47
+ property
48
+ schema
49
+ path
50
+
51
+ constructor (code, error) {
52
+ super(code || codes.Contract, error.message)
53
+
54
+ this.keyword = error.keyword
55
+ this.property = error.property
56
+ this.schema = error.schema
57
+ this.path = error.path
58
+ }
59
+ }
60
+
61
+ class RequestContractException extends ContractException {
62
+ constructor (error) { super(codes.RequestContract, error) }
63
+ }
64
+
65
+ class ResponseContractException extends ContractException {
66
+ constructor (error) { super(codes.ResponseContract, error) }
67
+ }
68
+
69
+ class EntityContractException extends ContractException {
70
+ constructor (error) { super(codes.EntityContract, error) }
71
+ }
72
+
73
+ // #region exports
74
+ exports.SystemException = SystemException
75
+ exports.RequestContractException = RequestContractException
76
+ exports.ResponseContractException = ResponseContractException
77
+ exports.EntityContractException = EntityContractException
78
+
79
+ for (const [name, code] of Object.entries(codes)) {
80
+ const classname = name + 'Exception'
81
+
82
+ if (exports[classname] === undefined) {
83
+ exports[classname] = class extends Exception {constructor (message) { super(code, message) }}
84
+ }
85
+ }
86
+
87
+ exports.codes = codes
88
+ // #endregion
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ class Exposition {
4
+ locator
5
+
6
+ #manifest
7
+
8
+ constructor (locator, manifest) {
9
+ this.locator = locator
10
+
11
+ this.#manifest = Exposition.#expose(manifest)
12
+ }
13
+
14
+ async invoke () {
15
+ return { output: this.#manifest }
16
+ }
17
+
18
+ static #expose (manifest) {
19
+ const { domain, name, operations, events } = manifest
20
+ return { domain, name, operations, events }
21
+ }
22
+ }
23
+
24
+ exports.Exposition = Exposition
package/src/index.js ADDED
@@ -0,0 +1,43 @@
1
+ const { Assignment } = require('./assignment')
2
+ const { Call } = require('./call')
3
+ const { Cascade } = require('./cascade')
4
+ const { Composition } = require('./composition')
5
+ const { Connector } = require('./connector')
6
+ const { Context } = require('./context')
7
+ const { Discovery } = require('./discovery')
8
+ const { Emission } = require('./emission')
9
+ const { Event } = require('./event')
10
+ const { Exposition } = require('./exposition')
11
+ const { Locator } = require('./locator')
12
+ const { Observation } = require('./observation')
13
+ const { Query } = require('./query')
14
+ const { Receiver } = require('./receiver')
15
+ const { Remote } = require('./remote')
16
+ const { Runtime } = require('./runtime')
17
+ const { State } = require('./state')
18
+ const { Transition } = require('./transition')
19
+ const { Transmission } = require('./transmission')
20
+
21
+ exports.entities = require('./entities')
22
+ exports.exceptions = require('./exceptions')
23
+ exports.contract = require('./contract')
24
+
25
+ exports.Assignment = Assignment
26
+ exports.Call = Call
27
+ exports.Cascade = Cascade
28
+ exports.Composition = Composition
29
+ exports.Connector = Connector
30
+ exports.Context = Context
31
+ exports.Discovery = Discovery
32
+ exports.Emission = Emission
33
+ exports.Event = Event
34
+ exports.Exposition = Exposition
35
+ exports.Locator = Locator
36
+ exports.Observation = Observation
37
+ exports.Query = Query
38
+ exports.Receiver = Receiver
39
+ exports.Remote = Remote
40
+ exports.Runtime = Runtime
41
+ exports.State = State
42
+ exports.Transition = Transition
43
+ exports.Transmission = Transmission
package/src/locator.js ADDED
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+
3
+ const { concat } = require('@toa.io/gears')
4
+
5
+ class Locator {
6
+ domain = 'system'
7
+ name
8
+ id
9
+
10
+ constructor (manifest) {
11
+ if (manifest !== undefined) {
12
+ if (typeof manifest === 'string') {
13
+ manifest = Locator.parse(manifest)
14
+ }
15
+
16
+ this.domain = manifest.domain
17
+ this.name = manifest.name
18
+ }
19
+
20
+ this.id = `${this.domain}${concat('.', this.name)}`
21
+ }
22
+
23
+ host (type, level = 0) {
24
+ let host = ''
25
+
26
+ const segments = LEVELS.slice(0, level + 1)
27
+
28
+ for (const segment of segments) {
29
+ host += concat(segment(this), SEPARATOR)
30
+ }
31
+
32
+ return host + type.toLowerCase()
33
+ }
34
+
35
+ static parse (label) {
36
+ const [domain, name, ...rest] = label.split('.')
37
+
38
+ return { domain, name, endpoint: rest.join('.') }
39
+ }
40
+ }
41
+
42
+ const SEPARATOR = '-'
43
+
44
+ const LEVELS = [
45
+ (locator) => locator.domain,
46
+ (locator) => locator.name
47
+ ]
48
+
49
+ exports.Locator = Locator
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const { freeze } = require('@toa.io/gears')
4
+
5
+ const { Operation } = require('./operation')
6
+
7
+ class Observation extends Operation {
8
+ async acquire (scope) {
9
+ const subject = await this.subject.query(scope.request.query)
10
+ const state = subject.get()
11
+
12
+ freeze(state)
13
+
14
+ scope.subject = subject
15
+ scope.state = state
16
+ }
17
+ }
18
+
19
+ exports.Observation = Observation
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('./connector')
4
+ const { SystemException } = require('./exceptions')
5
+
6
+ class Operation extends Connector {
7
+ subject
8
+
9
+ #cascade
10
+ #contract
11
+ #query
12
+
13
+ constructor (cascade, subject, contract, query) {
14
+ super()
15
+
16
+ this.subject = subject
17
+
18
+ this.#cascade = cascade
19
+ this.#contract = contract
20
+ this.#query = query
21
+
22
+ this.depends(cascade)
23
+ }
24
+
25
+ async invoke (request) {
26
+ try {
27
+ if (request.query) request.query = this.#query.parse(request.query)
28
+
29
+ const scope = { request }
30
+
31
+ return await this.process(scope)
32
+ } catch (e) {
33
+ const exception = e instanceof Error ? new SystemException(e) : e
34
+
35
+ return { exception }
36
+ }
37
+ }
38
+
39
+ async process (scope) {
40
+ await this.acquire(scope)
41
+ await this.run(scope)
42
+ await this.commit(scope)
43
+
44
+ return scope.reply
45
+ }
46
+
47
+ async acquire () {}
48
+
49
+ async run (scope) {
50
+ const { request, state } = scope
51
+ const reply = await this.#cascade.run(request.input, state) || {}
52
+
53
+ this.#contract.fit(reply)
54
+
55
+ scope.reply = reply
56
+ }
57
+
58
+ async commit () {}
59
+ }
60
+
61
+ exports.Operation = Operation
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ const { parse } = require('@rsql/parser')
4
+ const { QuerySyntaxException } = require('../exceptions')
5
+
6
+ const criteria = (criteria, properties) => {
7
+ let ast
8
+
9
+ try {
10
+ ast = parse(criteria)
11
+ } catch (e) {
12
+ throw new QuerySyntaxException(e.message)
13
+ }
14
+
15
+ if (properties !== undefined) coerce(ast, properties)
16
+
17
+ return ast
18
+ }
19
+
20
+ const coerce = (node, properties) => {
21
+ if (node.type === 'COMPARISON' && node.left?.type === 'SELECTOR' && node.right?.type === 'VALUE') {
22
+ const property = properties[node.left.selector]
23
+
24
+ if (property === undefined) {
25
+ throw new QuerySyntaxException(`Criteria selector '${node.left.selector}' is not defined`)
26
+ }
27
+
28
+ if (COERCE[property.type] !== undefined) { node.right.value = COERCE[property.type](node.right.value) }
29
+ } else {
30
+ if (node.left !== undefined) coerce(node.left, properties)
31
+ if (node.right !== undefined) coerce(node.right, properties)
32
+ }
33
+ }
34
+
35
+ const COERCE = {
36
+ number: Number,
37
+ integer: parseInt,
38
+ boolean: Boolean
39
+ }
40
+
41
+ exports.criteria = criteria
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const { QuerySyntaxException } = require('../exceptions')
4
+
5
+ const options = (options, properties, system) => {
6
+ if (options.sort !== undefined) options.sort = sort(options.sort, properties)
7
+ if (options.projection !== undefined) options.projection = projection(options.projection, properties, system)
8
+
9
+ return options
10
+ }
11
+
12
+ const sort = (sort, properties) => {
13
+ const result = []
14
+
15
+ for (const sorting of sort) {
16
+ const [property, direction] = sorting.split(':')
17
+
18
+ if (properties[property] === undefined) {
19
+ throw new QuerySyntaxException(`Sort property '${property}' is not defined`)
20
+ }
21
+
22
+ result.push([property, direction || 'asc'])
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ const projection = (projection, properties, system) => {
29
+ const set = [...new Set(system.concat(projection))]
30
+
31
+ for (const property of set) {
32
+ if (properties[property] === undefined) {
33
+ throw new QuerySyntaxException(`Projection property '${property}' is not defined`)
34
+ }
35
+ }
36
+
37
+ return set
38
+ }
39
+
40
+ exports.options = options
package/src/query.js ADDED
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { empty } = require('@toa.io/gears')
4
+ const parse = { ...require('./query/criteria'), ...require('./query/options') }
5
+
6
+ class Query {
7
+ #properties
8
+ #system
9
+
10
+ constructor (properties) {
11
+ this.#properties = properties
12
+ this.#system = Object.keys(properties).filter((key) => properties[key].system === true)
13
+ }
14
+
15
+ parse (query) {
16
+ const result = {}
17
+ const { id, version, criteria, ...rest } = query
18
+
19
+ const options = this.#options(rest)
20
+
21
+ if (id !== undefined) result.id = id
22
+ if (version !== undefined) result.version = version
23
+ if (criteria !== undefined) result.criteria = parse.criteria(criteria, this.#properties)
24
+ if (options !== undefined) result.options = options
25
+
26
+ return result
27
+ }
28
+
29
+ #options (options) {
30
+ if (empty(options)) return
31
+
32
+ return parse.options(options, this.#properties, this.#system)
33
+ }
34
+ }
35
+
36
+ exports.Query = Query
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('./connector')
4
+
5
+ class Receiver extends Connector {
6
+ #conditioned
7
+ #adaptive
8
+ #transition
9
+
10
+ #local
11
+ #bridge
12
+
13
+ constructor (definition, local, bridge) {
14
+ super()
15
+
16
+ this.#conditioned = definition.conditioned
17
+ this.#adaptive = definition.adaptive
18
+ this.#transition = definition.transition
19
+
20
+ this.#local = local
21
+ this.#bridge = bridge
22
+
23
+ this.depends(local)
24
+ this.depends(bridge)
25
+ }
26
+
27
+ async receive (payload) {
28
+ if (this.#conditioned && await this.#bridge.condition(payload) === false) return
29
+
30
+ const request = this.#adaptive ? await this.#bridge.request(payload) : payload
31
+
32
+ await this.#local.invoke(this.#transition, request)
33
+ }
34
+ }
35
+
36
+ exports.Receiver = Receiver
package/src/remote.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const { console } = require('@toa.io/gears')
4
+
5
+ const { Runtime } = require('./runtime')
6
+
7
+ class Remote extends Runtime {
8
+ async connection () {
9
+ console.info(`Remote '${this.locator.id}' connected`)
10
+ }
11
+
12
+ async disconnected () {
13
+ console.info(`Remote '${this.locator.id}' disconnected`)
14
+ }
15
+ }
16
+
17
+ exports.Remote = Remote
package/src/runtime.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+
3
+ const { console } = require('@toa.io/gears')
4
+ const { Connector } = require('./connector')
5
+ const { NotImplementedException } = require('./exceptions')
6
+
7
+ class Runtime extends Connector {
8
+ locator
9
+
10
+ #operations
11
+
12
+ constructor (locator, operations) {
13
+ super()
14
+
15
+ this.locator = locator
16
+ this.#operations = operations
17
+
18
+ Object.values(operations).forEach((operation) => this.depends(operation))
19
+ }
20
+
21
+ connection () {
22
+ console.info(`Runtime '${this.locator.id}' connected`)
23
+ }
24
+
25
+ disconnected () {
26
+ console.info(`Runtime '${this.locator.id}' disconnected`)
27
+ }
28
+
29
+ async invoke (endpoint, request) {
30
+ if (!(endpoint in this.#operations)) {
31
+ throw new NotImplementedException(`Endpoint '${endpoint}' not found in '${this.locator.id}'`)
32
+ }
33
+
34
+ return this.#operations[endpoint].invoke(request)
35
+ }
36
+ }
37
+
38
+ exports.Runtime = Runtime
package/src/state.js ADDED
@@ -0,0 +1,95 @@
1
+ 'use strict'
2
+
3
+ const { empty } = require('@toa.io/gears')
4
+
5
+ const {
6
+ StatePreconditionException,
7
+ StateNotFoundException,
8
+ StateInitializationException
9
+ } = require('./exceptions')
10
+
11
+ class State {
12
+ #storage
13
+ #entity
14
+ #emitter
15
+ #initialized
16
+
17
+ constructor (storage, entity, emitter, initialized) {
18
+ this.#storage = storage
19
+ this.#entity = entity
20
+ this.#emitter = emitter
21
+ this.#initialized = initialized
22
+ }
23
+
24
+ init (id) {
25
+ if (this.#initialized === true && id === undefined) {
26
+ throw new StateInitializationException('Entity is initialized')
27
+ }
28
+
29
+ return this.#entity.init(id)
30
+ }
31
+
32
+ async entity (query) {
33
+ const record = await this.#storage.get(query)
34
+
35
+ if (record === null) {
36
+ if (this.#initialized && query.id !== undefined && query.version === undefined) return this.init(query.id)
37
+ else if (query.version !== undefined) throw new StatePreconditionException()
38
+ else throw new StateNotFoundException()
39
+ }
40
+
41
+ return this.#entity.entity(record)
42
+ }
43
+
44
+ async set (query) {
45
+ const recordset = await this.#storage.find(query)
46
+
47
+ return this.#entity.set(recordset)
48
+ }
49
+
50
+ changeset (query) {
51
+ return this.#entity.changeset(query)
52
+ }
53
+
54
+ async commit (subject) {
55
+ const event = subject.event()
56
+
57
+ let ok = true
58
+
59
+ if (!empty(event.changeset)) {
60
+ ok = await this.#storage.store(subject.get())
61
+
62
+ // TODO: do not wait because outbox will handle failures
63
+ // TODO: handle slow emissions (too many concurrent emissions)
64
+ if (global.TOA_INTEGRATION_OMIT_EMISSION !== true) {
65
+ await this.#emitter.emit(event)
66
+ }
67
+ }
68
+
69
+ return ok
70
+ }
71
+
72
+ async apply (subject) {
73
+ const { changeset, insert } = subject.export()
74
+
75
+ let upsert
76
+
77
+ if (this.#initialized && subject.query.id !== undefined && subject.query.version === undefined) {
78
+ upsert = insert
79
+ }
80
+
81
+ const state = await this.#storage.upsert(subject.query, changeset, upsert)
82
+
83
+ if (state === null) {
84
+ if (subject.query.version !== undefined) throw new StatePreconditionException()
85
+ else throw new StateNotFoundException()
86
+ }
87
+
88
+ // TODO: same as above
89
+ if (global.TOA_INTEGRATION_OMIT_EMISSION !== true) {
90
+ await this.#emitter.emit({ changeset, state })
91
+ }
92
+ }
93
+ }
94
+
95
+ exports.State = State
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const { retry } = require('@toa.io/gears')
4
+
5
+ const { Operation } = require('./operation')
6
+ const { StateConcurrencyException } = require('./exceptions')
7
+
8
+ class Transition extends Operation {
9
+ #concurrency
10
+
11
+ constructor (cascade, subject, contract, query, definition) {
12
+ super(cascade, subject, contract, query)
13
+
14
+ this.#concurrency = definition.concurrency
15
+ }
16
+
17
+ async process (scope) {
18
+ return retry((retry) => this.#retry(scope, retry), { base: 0 })
19
+ }
20
+
21
+ async acquire (scope) {
22
+ const { request } = scope
23
+
24
+ scope.subject = request.query ? await this.subject.query(request.query) : this.subject.init()
25
+ scope.state = scope.subject.get()
26
+ }
27
+
28
+ async commit (scope) {
29
+ const { subject, state, reply, retry } = scope
30
+
31
+ if (reply.error !== undefined) return
32
+
33
+ subject.set(state)
34
+
35
+ const ok = await this.subject.commit(subject)
36
+
37
+ if (ok !== true) {
38
+ if (this.#concurrency === 'retry') retry()
39
+ else throw new StateConcurrencyException()
40
+ }
41
+ }
42
+
43
+ async #retry (scope, retry) {
44
+ scope.retry = retry
45
+
46
+ return super.process(scope)
47
+ }
48
+ }
49
+
50
+ exports.Transition = Transition