@toa.io/core 1.0.0-alpha.28 → 1.0.0-alpha.282
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/CHANGELOG.md +168 -0
- package/package.json +8 -10
- package/src/assignment.js +5 -8
- package/src/call.js +35 -9
- package/src/cascade.js +9 -11
- package/src/component.js +61 -21
- package/src/composition.js +3 -7
- package/src/connector.js +29 -22
- package/src/context.js +8 -6
- package/src/contract/contract.js +18 -0
- package/src/contract/index.js +2 -7
- package/src/contract/reply.js +36 -15
- package/src/contract/request.js +20 -10
- package/src/contract/schemas/index.js +9 -5
- package/src/contract/schemas/query.yaml +14 -1
- package/src/contract/schemas/source.yaml +23 -0
- package/src/discovery.js +15 -13
- package/src/effect.js +15 -0
- package/src/emission.js +2 -6
- package/src/entities/changeset.js +7 -14
- package/src/entities/entity.js +75 -29
- package/src/entities/factory.js +21 -14
- package/src/entities/index.js +1 -5
- package/src/entities/newid.js +9 -0
- package/src/entities/set.js +13 -4
- package/src/event.js +21 -7
- package/src/exceptions.js +53 -39
- package/src/exposition.js +5 -8
- package/src/guard.js +13 -0
- package/src/index.js +28 -47
- package/src/locator.js +2 -6
- package/src/observation.js +3 -15
- package/src/operation.js +40 -14
- package/src/outbox/index.js +1 -0
- package/src/outbox/outbox.js +296 -0
- package/src/query/criteria.js +3 -7
- package/src/query/options.js +5 -8
- package/src/query.js +36 -7
- package/src/receiver.js +76 -18
- package/src/reflection.js +2 -6
- package/src/remote.js +9 -12
- package/src/state.js +89 -50
- package/src/transition.js +13 -25
- package/src/transmission.js +15 -10
- package/src/unmanaged.js +7 -0
- package/test/call.fixtures.js +8 -12
- package/test/call.test.js +21 -9
- package/test/component.fixtures.js +5 -8
- package/test/component.test.js +22 -10
- package/test/connector.fixtures.js +10 -8
- package/test/connector.test.js +59 -38
- package/test/context.fixtures.js +8 -12
- package/test/context.test.js +18 -7
- package/test/contract/conditions.test.js +11 -9
- package/test/contract/contract.fixtures.js +12 -16
- package/test/contract/request.test.js +84 -34
- package/test/discovery.test.js +64 -0
- package/test/emission.fixtures.js +6 -10
- package/test/emission.test.js +29 -12
- package/test/entities/entity.fixtures.js +7 -11
- package/test/entities/entity.test.js +70 -49
- package/test/entities/factory.fixtures.js +22 -10
- package/test/entities/factory.test.js +29 -15
- package/test/entities/set.fixtures.js +6 -8
- package/test/entities/set.test.js +6 -5
- package/test/event.fixtures.js +9 -14
- package/test/event.test.js +57 -31
- package/test/locator.test.js +25 -24
- package/test/outbox.test.js +121 -0
- package/test/query.fixtures.js +1 -5
- package/test/query.test.js +42 -14
- package/test/receiver.fixtures.js +9 -12
- package/test/receiver.test.js +45 -22
- package/test/reflection.test.js +7 -6
- package/test/state.fixtures.js +25 -30
- package/test/state.test.js +72 -19
- package/test/transmission.fixtures.js +4 -6
- package/test/transmission.test.js +33 -15
- package/types/atomicity.d.ts +58 -0
- package/types/bindings.d.ts +8 -6
- package/types/bridges.ts +5 -2
- package/types/component.d.ts +7 -4
- package/types/context.d.ts +6 -3
- package/types/entity.d.ts +4 -4
- package/types/event.d.ts +1 -1
- package/types/extensions.d.ts +35 -10
- package/types/index.ts +16 -13
- package/types/message.d.ts +1 -0
- package/types/operations.d.ts +6 -0
- package/types/outbox.d.ts +53 -0
- package/types/query.d.ts +2 -0
- package/types/receiver.d.ts +2 -2
- package/types/reflection.d.ts +1 -1
- package/types/remote.d.ts +18 -0
- package/types/request.d.ts +16 -1
- package/types/state.d.ts +11 -8
- package/types/storages.d.ts +36 -11
- package/src/contract/conditions.js +0 -21
package/src/query/criteria.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { parse } from '@rsql/parser'
|
|
2
|
+
import { QuerySyntaxException } from '../exceptions.js'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
const { QuerySyntaxException } = require('../exceptions')
|
|
5
|
-
|
|
6
|
-
const criteria = (criteria, properties) => {
|
|
4
|
+
export const criteria = (criteria, properties) => {
|
|
7
5
|
let ast
|
|
8
6
|
|
|
9
7
|
try {
|
|
@@ -37,5 +35,3 @@ const COERCE = {
|
|
|
37
35
|
integer: Number.parseInt,
|
|
38
36
|
boolean: (value) => value === 'true'
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
exports.criteria = criteria
|
package/src/query/options.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { QuerySyntaxException } from '../exceptions.js'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const options = (options, properties, system) => {
|
|
3
|
+
export const options = (options, properties, system) => {
|
|
6
4
|
if (options.sort !== undefined) options.sort = sort(options.sort, properties)
|
|
7
5
|
|
|
8
6
|
if (options.projection !== undefined) projection(options.projection, properties)
|
|
@@ -33,8 +31,7 @@ const projection = (projection, properties) => {
|
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
projection.
|
|
34
|
+
for (const property of ['_version', '_created', '_updated', '_deleted'])
|
|
35
|
+
if (!projection.includes(property))
|
|
36
|
+
projection.push(property)
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
exports.options = options
|
package/src/query.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { empty } from '@toa.io/generic'
|
|
2
|
+
import * as criteria from './query/criteria.js'
|
|
3
|
+
import * as options from './query/options.js'
|
|
2
4
|
|
|
3
|
-
const {
|
|
4
|
-
const parse = { ...require('./query/criteria'), ...require('./query/options') }
|
|
5
|
+
const parse = { ...criteria, ...options }
|
|
5
6
|
|
|
6
|
-
class Query {
|
|
7
|
+
export class Query {
|
|
7
8
|
#properties
|
|
8
9
|
#system
|
|
9
10
|
|
|
11
|
+
/** @type {Map<string, object>} parsed criteria by their expression */
|
|
12
|
+
#asts = new Map()
|
|
13
|
+
|
|
10
14
|
constructor (properties) {
|
|
11
15
|
this.#properties = properties
|
|
12
16
|
this.#system = Object.keys(properties).filter((key) => properties[key].system === true)
|
|
@@ -19,13 +23,15 @@ class Query {
|
|
|
19
23
|
parse (query) {
|
|
20
24
|
/** @type {toa.core.storages.Query} */
|
|
21
25
|
const result = {}
|
|
22
|
-
const { id, version, criteria, ...rest } = query
|
|
26
|
+
const { id, ids, version, criteria, search, ...rest } = query
|
|
23
27
|
|
|
24
28
|
const options = this.#options(rest)
|
|
25
29
|
|
|
26
30
|
if (id !== undefined) result.id = id
|
|
31
|
+
if (ids !== undefined) result.ids = ids
|
|
27
32
|
if (version !== undefined) result.version = version
|
|
28
|
-
if (criteria !== undefined) result.criteria =
|
|
33
|
+
if (criteria !== undefined) result.criteria = this.#criteria(criteria)
|
|
34
|
+
if (search !== undefined) result.search = search
|
|
29
35
|
if (options !== undefined) result.options = options
|
|
30
36
|
|
|
31
37
|
return result
|
|
@@ -36,6 +42,29 @@ class Query {
|
|
|
36
42
|
|
|
37
43
|
return parse.options(options, this.#properties, this.#system)
|
|
38
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Lexing an RSQL expression is the most expensive thing this class does, and the same
|
|
48
|
+
* expression arrives over and over: a route builds it from its own declaration and the
|
|
49
|
+
* request parameters. The tree is only read downstream — a storage builds a fresh query
|
|
50
|
+
* from it — so it is kept rather than parsed again.
|
|
51
|
+
*
|
|
52
|
+
* Expressions come from the client, hence the bound. An invalid one throws before it
|
|
53
|
+
* reaches the cache.
|
|
54
|
+
*/
|
|
55
|
+
#criteria (criteria) {
|
|
56
|
+
const known = this.#asts.get(criteria)
|
|
57
|
+
|
|
58
|
+
if (known !== undefined) return known
|
|
59
|
+
|
|
60
|
+
const ast = parse.criteria(criteria, this.#properties)
|
|
61
|
+
|
|
62
|
+
if (this.#asts.size >= LIMIT) this.#asts.clear()
|
|
63
|
+
|
|
64
|
+
this.#asts.set(criteria, ast)
|
|
65
|
+
|
|
66
|
+
return ast
|
|
67
|
+
}
|
|
39
68
|
}
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
const LIMIT = 1024
|
package/src/receiver.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const { Connector } = require('./connector')
|
|
1
|
+
import { console, decode, run } from 'openspan'
|
|
2
|
+
import { add } from '@toa.io/generic'
|
|
3
|
+
import { Connector } from './connector.js'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* @implements {toa.core.Receiver}
|
|
8
7
|
*/
|
|
9
|
-
class Receiver extends Connector {
|
|
8
|
+
export class Receiver extends Connector {
|
|
10
9
|
/** @type {boolean} */
|
|
11
10
|
#conditioned
|
|
12
11
|
|
|
@@ -16,18 +15,28 @@ class Receiver extends Connector {
|
|
|
16
15
|
/** @type {string} */
|
|
17
16
|
#endpoint
|
|
18
17
|
|
|
19
|
-
/** @type {
|
|
18
|
+
/** @type {string} */
|
|
19
|
+
#label
|
|
20
|
+
|
|
21
|
+
/** @type {string} */
|
|
22
|
+
#destination
|
|
23
|
+
|
|
24
|
+
/** @type {unknown[]} */
|
|
25
|
+
#arguments
|
|
26
|
+
|
|
27
|
+
/** @type {toa.core.Source} */
|
|
28
|
+
#origin
|
|
29
|
+
|
|
20
30
|
#local
|
|
21
31
|
|
|
22
|
-
/** @type {toa.core.bridges.Receiver} */
|
|
23
32
|
#bridge
|
|
24
33
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
/** @type {object} */
|
|
35
|
+
#delivery
|
|
36
|
+
|
|
37
|
+
/** @type {object} */
|
|
38
|
+
#processing
|
|
39
|
+
|
|
31
40
|
constructor (definition, local, bridge) {
|
|
32
41
|
super()
|
|
33
42
|
|
|
@@ -36,17 +45,42 @@ class Receiver extends Connector {
|
|
|
36
45
|
this.#conditioned = conditioned
|
|
37
46
|
this.#adaptive = adaptive
|
|
38
47
|
this.#endpoint = operation
|
|
48
|
+
this.#label = definition.label ?? operation
|
|
49
|
+
this.#destination = definition.destination ?? this.#label
|
|
50
|
+
this.#arguments = definition.arguments
|
|
51
|
+
this.#origin = definition.origin
|
|
39
52
|
|
|
40
53
|
this.#local = local
|
|
41
54
|
this.#bridge = bridge
|
|
42
55
|
|
|
43
56
|
this.depends(local)
|
|
44
57
|
if (bridge !== undefined) this.depends(bridge)
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* The delivery span is created on behalf of the messaging destination, so that
|
|
61
|
+
* each consumer forms its own complete producer/consumer pair, and service graphs
|
|
62
|
+
* display fan-out correctly: producer -> destination -> each consumer
|
|
63
|
+
* (Tempo pairs spans one-to-one, thus multiple consumers can not pair
|
|
64
|
+
* with a single producer span, see grafana/tempo#5408)
|
|
65
|
+
*/
|
|
66
|
+
this.#delivery = {
|
|
67
|
+
name: `${this.#label} deliver`,
|
|
68
|
+
kind: 'producer',
|
|
69
|
+
service: this.#destination,
|
|
70
|
+
attributes: { 'messaging.destination.name': this.#destination }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.#processing = {
|
|
74
|
+
name: `${this.#label} process`,
|
|
75
|
+
kind: 'consumer',
|
|
76
|
+
service: local.locator.id,
|
|
77
|
+
attributes: { 'messaging.destination.name': this.#destination }
|
|
78
|
+
}
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
/** @hot */
|
|
48
82
|
async receive (message) {
|
|
49
|
-
const { payload, ...extensions } = message
|
|
83
|
+
const { payload, telemetry, ...extensions } = message
|
|
50
84
|
|
|
51
85
|
if (this.#conditioned && await this.#bridge.condition(payload) === false) return
|
|
52
86
|
|
|
@@ -54,12 +88,36 @@ class Receiver extends Connector {
|
|
|
54
88
|
|
|
55
89
|
add(request, extensions)
|
|
56
90
|
|
|
57
|
-
|
|
91
|
+
// set after `add`, so that a message field can not spoof the origin
|
|
92
|
+
if (this.#origin !== undefined) request.source = this.#origin
|
|
93
|
+
|
|
94
|
+
// continue the trace from the producer span
|
|
95
|
+
const remote = telemetry === undefined ? null : decode(telemetry)
|
|
96
|
+
const task = () => this.#process(request)
|
|
97
|
+
|
|
98
|
+
if (remote === null)
|
|
99
|
+
await task()
|
|
100
|
+
else
|
|
101
|
+
await run(remote, task)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async #process (request) {
|
|
105
|
+
return console.span(this.#delivery, async () => console.span(this.#processing, async () => {
|
|
106
|
+
try {
|
|
107
|
+
await this.#local.invoke(this.#endpoint, request)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Receiver error', {
|
|
110
|
+
component: this.#local.locator.id,
|
|
111
|
+
endpoint: this.#endpoint,
|
|
112
|
+
error
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
}))
|
|
58
118
|
}
|
|
59
119
|
|
|
60
120
|
async #request (payload) {
|
|
61
|
-
return this.#adaptive ? await this.#bridge.request(payload) : { input: payload }
|
|
121
|
+
return this.#adaptive ? await this.#bridge.request(payload, ...(this.#arguments ?? [])) : { input: payload }
|
|
62
122
|
}
|
|
63
123
|
}
|
|
64
|
-
|
|
65
|
-
exports.Receiver = Receiver
|
package/src/reflection.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { Connector } = require('./connector')
|
|
1
|
+
import { Connector } from './connector.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* @implements {toa.core.Reflection}
|
|
7
5
|
*/
|
|
8
|
-
class Reflection extends Connector {
|
|
6
|
+
export class Reflection extends Connector {
|
|
9
7
|
/** @type {toa.core.reflection.Source} */
|
|
10
8
|
#source
|
|
11
9
|
|
|
@@ -24,5 +22,3 @@ class Reflection extends Connector {
|
|
|
24
22
|
this.value = await this.#source()
|
|
25
23
|
}
|
|
26
24
|
}
|
|
27
|
-
|
|
28
|
-
exports.Reflection = Reflection
|
package/src/remote.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { Component } from './component.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
export class Remote extends Component {
|
|
5
|
+
kind = 'client'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
explain (endpoint) {
|
|
8
|
+
if (!(endpoint in this.operations))
|
|
9
|
+
// `assert.fail`, not `assert.ok`: the message is built only when it is needed
|
|
10
|
+
assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
async open () {
|
|
9
|
-
console.info(`Remote '${this.locator.id}' connected`)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async dispose () {
|
|
13
|
-
console.info(`Remote '${this.locator.id}' disconnected`)
|
|
12
|
+
return this.operations[endpoint].explain()
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
exports.Remote = Remote
|
package/src/state.js
CHANGED
|
@@ -1,82 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
import { StatePreconditionException, StateNotFoundException } from './exceptions.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export class State {
|
|
4
|
+
storage
|
|
4
5
|
|
|
5
|
-
const {
|
|
6
|
-
StatePreconditionException,
|
|
7
|
-
StateNotFoundException
|
|
8
|
-
} = require('./exceptions')
|
|
9
|
-
|
|
10
|
-
class State {
|
|
11
6
|
#associated
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
this.#
|
|
18
|
-
this.#
|
|
19
|
-
this.#emission = emission
|
|
7
|
+
#entities
|
|
8
|
+
#outbox
|
|
9
|
+
|
|
10
|
+
constructor (storage, entity, outbox, associated) {
|
|
11
|
+
this.storage = storage
|
|
12
|
+
this.#entities = entity
|
|
13
|
+
this.#outbox = outbox
|
|
20
14
|
this.#associated = associated === true
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
init (id) {
|
|
24
|
-
return this.#
|
|
18
|
+
return this.#entities.init(id)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fit (values) {
|
|
22
|
+
return this.#entities.fit(values)
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
async object (query) {
|
|
28
|
-
const record = await this
|
|
25
|
+
async object (query, mutable = true) {
|
|
26
|
+
const record = await this.storage.get(query)
|
|
29
27
|
|
|
30
28
|
if (record === null) {
|
|
31
|
-
if (this.#associated && query.id !== undefined && query.version === undefined)
|
|
29
|
+
if (this.#associated && query.id !== undefined && query.criteria === undefined && query.version === undefined)
|
|
32
30
|
return this.init(query.id)
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
else if (query.version !== undefined)
|
|
32
|
+
throw new StatePreconditionException()
|
|
35
33
|
|
|
36
|
-
if (record === null) {
|
|
37
34
|
return null
|
|
38
|
-
} else
|
|
39
|
-
return this.#
|
|
40
|
-
}
|
|
35
|
+
} else
|
|
36
|
+
return this.#entities.object(record, mutable)
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
async objects (query) {
|
|
44
|
-
const recordset = await this
|
|
39
|
+
async objects (query, mutable = true) {
|
|
40
|
+
const recordset = await this.storage.find(query)
|
|
41
|
+
const missing = this.#associated && query.ids !== undefined && recordset.length < query.ids.length
|
|
42
|
+
const init = missing ? query.ids.filter((id) => !recordset.some((record) => record.id === id)) : undefined
|
|
43
|
+
|
|
44
|
+
return this.#entities.objects(recordset, init, mutable)
|
|
45
|
+
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
async stream (query) {
|
|
48
|
+
return this.storage.stream(query)
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
changeset (query) {
|
|
50
|
-
return this.#
|
|
52
|
+
return this.#entities.changeset(query)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
none () {
|
|
54
56
|
return null
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
async
|
|
58
|
-
const
|
|
59
|
+
async ensure (query, properties, input) {
|
|
60
|
+
const object = this.#entities.init()
|
|
61
|
+
const blank = object.get()
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
Object.assign(blank, properties)
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
const object = state.get()
|
|
65
|
+
object.set(blank)
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const row = this.#outbox?.row(object.event(input))
|
|
68
|
+
const record = await this.storage.ensure(query, properties, object.get(), row)
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
if (record.id !== blank.id) // exists
|
|
71
|
+
return this.#entities.object(record, NOT_MUTABLE)
|
|
72
|
+
|
|
73
|
+
if (row !== undefined)
|
|
74
|
+
await this.#outbox.publish(row)
|
|
75
|
+
|
|
76
|
+
return object
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async commit (state, input) {
|
|
80
|
+
if (state.constructor.name === 'EntitySet')
|
|
81
|
+
return this.massCommit(state, input)
|
|
82
|
+
|
|
83
|
+
const data = state.get()
|
|
84
|
+
|
|
85
|
+
// the row is built before the write so that the storage can commit it in the same
|
|
86
|
+
// transaction, closing the window this used to have
|
|
87
|
+
const row = this.#outbox?.row(state.event(input))
|
|
88
|
+
const ok = await this.storage.store(data, row)
|
|
89
|
+
|
|
90
|
+
if (ok === true && row !== undefined)
|
|
91
|
+
await this.#outbox.publish(row)
|
|
92
|
+
|
|
93
|
+
return ok
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async massCommit (state, input) {
|
|
97
|
+
const data = state.get()
|
|
98
|
+
const rows = this.#outbox === undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: state.events(input).map((event) => this.#outbox.row(event))
|
|
101
|
+
|
|
102
|
+
const ok = await this.storage.massStore(data, rows)
|
|
103
|
+
|
|
104
|
+
if (ok === true && rows !== undefined)
|
|
105
|
+
await Promise.all(rows.map((row) => this.#outbox.publish(row)))
|
|
72
106
|
|
|
73
107
|
return ok
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
async apply (state) {
|
|
110
|
+
async apply (state, input) {
|
|
77
111
|
const changeset = state.export()
|
|
78
112
|
|
|
79
|
-
|
|
113
|
+
// an assignment's event is the write's own images, so the storage fills them in;
|
|
114
|
+
// see `apply` in the outbox design
|
|
115
|
+
const row = this.#outbox?.row({ input })
|
|
116
|
+
const result = await this.storage.upsert(state.query, changeset, row)
|
|
80
117
|
|
|
81
118
|
if (result === null) {
|
|
82
119
|
if (state.query.version !== undefined) {
|
|
@@ -84,16 +121,18 @@ class State {
|
|
|
84
121
|
} else {
|
|
85
122
|
throw new StateNotFoundException()
|
|
86
123
|
}
|
|
87
|
-
} else {
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
} else if (row !== undefined) {
|
|
125
|
+
// the storage fills `origin` and `state` from its own write; a storage that does not
|
|
126
|
+
// know how leaves the event with what it was given
|
|
127
|
+
row.event.state ??= result
|
|
128
|
+
row.event.origin ??= null
|
|
129
|
+
|
|
130
|
+
await this.#outbox.publish(row)
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
return result
|
|
96
134
|
}
|
|
97
135
|
}
|
|
98
136
|
|
|
99
|
-
|
|
137
|
+
/** an effect never commits what `ensure` hands it, see `Effect` */
|
|
138
|
+
const NOT_MUTABLE = false
|
package/src/transition.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { retry } from '@toa.io/generic'
|
|
2
|
+
import { Operation } from './operation.js'
|
|
3
|
+
import { StateConcurrencyException, StateNotFoundException } from './exceptions.js'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
export class Transition extends Operation {
|
|
6
|
+
/** a transition is the only operation that commits */
|
|
7
|
+
mutable = true
|
|
4
8
|
|
|
5
|
-
const { Operation } = require('./operation')
|
|
6
|
-
const {
|
|
7
|
-
StateConcurrencyException,
|
|
8
|
-
StateNotFoundException
|
|
9
|
-
} = require('./exceptions')
|
|
10
|
-
|
|
11
|
-
class Transition extends Operation {
|
|
12
9
|
#concurrency
|
|
13
10
|
|
|
14
11
|
constructor (cascade, scope, contract, query, definition) {
|
|
@@ -26,33 +23,26 @@ class Transition extends Operation {
|
|
|
26
23
|
|
|
27
24
|
store.scope = request.query ? await this.query(request.query) : this.scope.init()
|
|
28
25
|
|
|
29
|
-
if (store.scope === null)
|
|
26
|
+
if (store.scope === null || (store.scope.deleted === true && request.query?.options?.deleted !== true))
|
|
30
27
|
throw new StateNotFoundException()
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
|
|
33
29
|
store.state = store.scope.get()
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
async commit (store) {
|
|
37
|
-
const {
|
|
38
|
-
scope,
|
|
39
|
-
state,
|
|
40
|
-
reply,
|
|
41
|
-
retry
|
|
42
|
-
} = store
|
|
33
|
+
const { scope, state, reply, retry } = store
|
|
43
34
|
|
|
44
35
|
if (reply.error !== undefined) return
|
|
45
36
|
|
|
46
37
|
scope.set(state)
|
|
47
38
|
|
|
48
|
-
const result = await this.scope.commit(scope)
|
|
39
|
+
const result = await this.scope.commit(scope, store.request.input)
|
|
49
40
|
|
|
50
41
|
if (result === false) {
|
|
51
|
-
if (this.#concurrency === 'retry')
|
|
42
|
+
if (this.#concurrency === 'retry')
|
|
52
43
|
return retry()
|
|
53
|
-
|
|
44
|
+
else
|
|
54
45
|
throw new StateConcurrencyException()
|
|
55
|
-
}
|
|
56
46
|
}
|
|
57
47
|
}
|
|
58
48
|
|
|
@@ -67,7 +57,5 @@ const RETRY = {
|
|
|
67
57
|
base: 10,
|
|
68
58
|
max: 5000,
|
|
69
59
|
dispersion: 1,
|
|
70
|
-
retries:
|
|
60
|
+
retries: 32
|
|
71
61
|
}
|
|
72
|
-
|
|
73
|
-
exports.Transition = Transition
|
package/src/transmission.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { Connector } from './connector.js'
|
|
2
|
+
import { TransmissionException } from './exceptions.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const { TransmissionException } = require('./exceptions')
|
|
5
|
-
|
|
6
|
-
class Transmission extends Connector {
|
|
4
|
+
export class Transmission extends Connector {
|
|
7
5
|
#bindings
|
|
8
6
|
|
|
9
7
|
constructor (bindings) {
|
|
@@ -18,16 +16,23 @@ class Transmission extends Connector {
|
|
|
18
16
|
let i = 0
|
|
19
17
|
|
|
20
18
|
while (reply === false && i < this.#bindings.length) {
|
|
21
|
-
|
|
19
|
+
const binding = this.#bindings[i]
|
|
20
|
+
|
|
22
21
|
i++
|
|
22
|
+
|
|
23
|
+
if (request?.task === true) {
|
|
24
|
+
if (binding.task === undefined)
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
await binding.task(request)
|
|
28
|
+
reply = null
|
|
29
|
+
} else
|
|
30
|
+
reply = await binding.request(request)
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
if (reply === false)
|
|
33
|
+
if (reply === false)
|
|
26
34
|
throw new TransmissionException(`All (${this.#bindings.length}) bindings rejected.`)
|
|
27
|
-
}
|
|
28
35
|
|
|
29
36
|
return reply
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
|
-
|
|
33
|
-
exports.Transmission = Transmission
|
package/src/unmanaged.js
ADDED
package/test/call.fixtures.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { mock } from 'node:test'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { generate } from 'randomstring'
|
|
4
4
|
|
|
5
5
|
// noinspection JSCheckFunctionSignatures
|
|
6
|
-
const transmission = {
|
|
7
|
-
request:
|
|
8
|
-
link:
|
|
6
|
+
export const transmission = {
|
|
7
|
+
request: mock.fn((request) => ({ [request.invalid ? 'exception' : 'output']: generate() })),
|
|
8
|
+
link: mock.fn()
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const contract = {
|
|
12
|
-
fit:
|
|
11
|
+
export const contract = {
|
|
12
|
+
fit: mock.fn(() => null)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const request = () => ({
|
|
15
|
+
export const request = () => ({
|
|
16
16
|
ok: {
|
|
17
17
|
input: { [generate()]: generate() },
|
|
18
18
|
query: { [generate()]: generate() }
|
|
@@ -21,7 +21,3 @@ const request = () => ({
|
|
|
21
21
|
invalid: true
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
|
-
|
|
25
|
-
exports.transmission = transmission
|
|
26
|
-
exports.contract = contract
|
|
27
|
-
exports.request = request
|