@toa.io/core 0.1.0-dev.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/LICENSE +22 -0
- package/package.json +28 -0
- package/src/assignment.js +22 -0
- package/src/call.js +29 -0
- package/src/cascade.js +32 -0
- package/src/composition.js +26 -0
- package/src/connector.js +122 -0
- package/src/context.js +43 -0
- package/src/contract/conditions.js +21 -0
- package/src/contract/index.js +7 -0
- package/src/contract/reply.js +22 -0
- package/src/contract/request.js +49 -0
- package/src/contract/schemas/error.yaml +7 -0
- package/src/contract/schemas/index.js +7 -0
- package/src/contract/schemas/query.yaml +31 -0
- package/src/discovery.js +33 -0
- package/src/emission.js +23 -0
- package/src/entities/changeset.js +46 -0
- package/src/entities/entity.js +48 -0
- package/src/entities/factory.js +33 -0
- package/src/entities/index.js +5 -0
- package/src/entities/set.js +15 -0
- package/src/event.js +33 -0
- package/src/exceptions.js +88 -0
- package/src/exposition.js +24 -0
- package/src/index.js +43 -0
- package/src/locator.js +49 -0
- package/src/observation.js +19 -0
- package/src/operation.js +61 -0
- package/src/query/criteria.js +41 -0
- package/src/query/options.js +40 -0
- package/src/query.js +36 -0
- package/src/receiver.js +36 -0
- package/src/remote.js +17 -0
- package/src/runtime.js +38 -0
- package/src/state.js +95 -0
- package/src/transition.js +50 -0
- package/src/transmission.js +33 -0
- package/test/call.fixtures.js +25 -0
- package/test/call.test.js +52 -0
- package/test/cascade.fixtures.js +11 -0
- package/test/cascade.test.js +42 -0
- package/test/connector.fixtures.js +40 -0
- package/test/connector.test.js +199 -0
- package/test/contract/conditions.test.js +26 -0
- package/test/contract/contract.fixtures.js +27 -0
- package/test/contract/request.test.js +99 -0
- package/test/emission.fixtures.js +16 -0
- package/test/emission.test.js +35 -0
- package/test/entities/entity.fixtures.js +26 -0
- package/test/entities/entity.test.js +64 -0
- package/test/entities/factory.fixtures.js +18 -0
- package/test/entities/factory.test.js +48 -0
- package/test/entities/set.fixtures.js +11 -0
- package/test/entities/set.test.js +12 -0
- package/test/event.fixtures.js +28 -0
- package/test/event.test.js +106 -0
- package/test/locator.test.js +34 -0
- package/test/query.fixtures.js +100 -0
- package/test/query.test.js +86 -0
- package/test/receiver.fixtures.js +22 -0
- package/test/receiver.test.js +66 -0
- package/test/runtime.fixtures.js +19 -0
- package/test/runtime.test.js +40 -0
- package/test/state.fixtures.js +46 -0
- package/test/state.test.js +54 -0
- package/test/transmission.fixtures.js +15 -0
- 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
|
package/src/operation.js
ADDED
|
@@ -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
|
package/src/receiver.js
ADDED
|
@@ -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
|