@toa.io/core 1.0.0-alpha.29 → 1.0.0-alpha.292
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 +43 -0
- package/package.json +22 -12
- package/source/assignment.ts +27 -0
- package/source/call.ts +63 -0
- package/source/cascade.ts +32 -0
- package/source/component.ts +83 -0
- package/source/composition.ts +27 -0
- package/source/connector.ts +217 -0
- package/source/context.ts +68 -0
- package/source/contract/contract.ts +23 -0
- package/source/contract/index.ts +2 -0
- package/source/contract/reply.ts +56 -0
- package/source/contract/request.ts +140 -0
- package/source/contract/schemas.ts +39 -0
- package/source/discovery.ts +53 -0
- package/source/effect.ts +14 -0
- package/source/emission.ts +21 -0
- package/source/entities/changeset.ts +37 -0
- package/source/entities/entity.ts +137 -0
- package/source/entities/factory.ts +52 -0
- package/source/entities/index.ts +4 -0
- package/source/entities/newid.ts +9 -0
- package/source/entities/set.ts +27 -0
- package/source/event.ts +57 -0
- package/source/exceptions.ts +133 -0
- package/source/exposition.ts +33 -0
- package/source/guard.ts +16 -0
- package/source/index.ts +36 -0
- package/source/locator.ts +37 -0
- package/source/observation.ts +16 -0
- package/source/operation.ts +139 -0
- package/source/outbox/index.ts +1 -0
- package/source/outbox/outbox.ts +341 -0
- package/source/query/criteria.ts +90 -0
- package/source/query/options.ts +35 -0
- package/source/query.ts +67 -0
- package/source/receiver.ts +120 -0
- package/source/reflection.ts +20 -0
- package/source/remote.ts +20 -0
- package/source/state.ts +179 -0
- package/source/transition.ts +79 -0
- package/source/transmission.ts +38 -0
- package/source/types/atomicity.ts +63 -0
- package/source/types/bindings.ts +59 -0
- package/source/types/bridges.ts +54 -0
- package/source/types/extensions.ts +89 -0
- package/source/types/index.ts +14 -0
- package/{types/message.d.ts → source/types/message.ts} +2 -0
- package/source/types/operations.ts +31 -0
- package/source/types/outbox.ts +35 -0
- package/source/types/receiver.ts +10 -0
- package/source/types/request.ts +54 -0
- package/source/types/state.ts +9 -0
- package/source/types/storages.ts +134 -0
- package/source/unmanaged.ts +8 -0
- package/test/call.fixtures.js +10 -12
- package/test/call.test.js +42 -9
- package/test/component.fixtures.js +5 -8
- package/test/component.test.js +27 -10
- package/test/connector.fixtures.js +29 -13
- package/test/connector.test.js +128 -38
- package/test/context.fixtures.js +8 -12
- package/test/context.test.js +19 -8
- package/test/contract/conditions.test.js +15 -9
- package/test/contract/contract.fixtures.js +11 -16
- package/test/contract/request.test.js +130 -46
- package/test/discovery.test.js +71 -0
- package/test/emission.fixtures.js +6 -10
- package/test/emission.test.js +37 -12
- package/test/entities/entity.fixtures.js +11 -16
- package/test/entities/entity.test.js +105 -50
- package/test/entities/factory.fixtures.js +26 -13
- package/test/entities/factory.test.js +68 -17
- 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 +78 -34
- package/test/locator.test.js +32 -25
- package/test/outbox.test.js +202 -0
- package/test/query.fixtures.js +1 -5
- package/test/query.test.js +111 -14
- package/test/receiver.fixtures.js +9 -12
- package/test/receiver.test.js +80 -31
- package/test/reflection.test.js +9 -8
- package/test/state.fixtures.js +33 -31
- package/test/state.test.js +135 -19
- package/test/transmission.fixtures.js +4 -6
- package/test/transmission.test.js +47 -16
- package/transpiled/assignment.d.ts +6 -0
- package/transpiled/assignment.js +20 -0
- package/transpiled/assignment.js.map +1 -0
- package/transpiled/call.d.ts +10 -0
- package/transpiled/call.js +52 -0
- package/transpiled/call.js.map +1 -0
- package/transpiled/cascade.d.ts +7 -0
- package/transpiled/cascade.js +28 -0
- package/transpiled/cascade.js.map +1 -0
- package/transpiled/component.d.ts +15 -0
- package/transpiled/component.js +61 -0
- package/transpiled/component.js.map +1 -0
- package/transpiled/composition.d.ts +6 -0
- package/transpiled/composition.js +23 -0
- package/transpiled/composition.js.map +1 -0
- package/transpiled/connector.d.ts +47 -0
- package/transpiled/connector.js +185 -0
- package/transpiled/connector.js.map +1 -0
- package/transpiled/context.d.ts +18 -0
- package/transpiled/context.js +42 -0
- package/transpiled/context.js.map +1 -0
- package/transpiled/contract/contract.d.ts +10 -0
- package/transpiled/contract/contract.js +14 -0
- package/transpiled/contract/contract.js.map +1 -0
- package/transpiled/contract/index.d.ts +2 -0
- package/transpiled/contract/index.js +3 -0
- package/transpiled/contract/index.js.map +1 -0
- package/transpiled/contract/reply.d.ts +7 -0
- package/transpiled/contract/reply.js +48 -0
- package/transpiled/contract/reply.js.map +1 -0
- package/transpiled/contract/request.d.ts +29 -0
- package/transpiled/contract/request.js +103 -0
- package/transpiled/contract/request.js.map +1 -0
- package/transpiled/contract/schemas.d.ts +4 -0
- package/transpiled/contract/schemas.js +36 -0
- package/transpiled/contract/schemas.js.map +1 -0
- package/transpiled/discovery.d.ts +12 -0
- package/transpiled/discovery.js +34 -0
- package/transpiled/discovery.js.map +1 -0
- package/transpiled/effect.d.ts +5 -0
- package/transpiled/effect.js +11 -0
- package/transpiled/effect.js.map +1 -0
- package/transpiled/emission.d.ts +8 -0
- package/transpiled/emission.js +14 -0
- package/transpiled/emission.js.map +1 -0
- package/transpiled/entities/changeset.d.ts +10 -0
- package/transpiled/entities/changeset.js +27 -0
- package/transpiled/entities/changeset.js.map +1 -0
- package/transpiled/entities/entity.d.ts +19 -0
- package/transpiled/entities/entity.js +109 -0
- package/transpiled/entities/entity.js.map +1 -0
- package/transpiled/entities/factory.d.ts +22 -0
- package/transpiled/entities/factory.js +34 -0
- package/transpiled/entities/factory.js.map +1 -0
- package/transpiled/entities/index.d.ts +4 -0
- package/transpiled/entities/index.js +5 -0
- package/transpiled/entities/index.js.map +1 -0
- package/transpiled/entities/newid.d.ts +1 -0
- package/transpiled/entities/newid.js +7 -0
- package/transpiled/entities/newid.js.map +1 -0
- package/transpiled/entities/set.d.ts +10 -0
- package/transpiled/entities/set.js +19 -0
- package/transpiled/entities/set.js.map +1 -0
- package/transpiled/event.d.ts +14 -0
- package/transpiled/event.js +38 -0
- package/transpiled/event.js.map +1 -0
- package/transpiled/exceptions.d.ts +61 -0
- package/transpiled/exceptions.js +93 -0
- package/transpiled/exceptions.js.map +1 -0
- package/transpiled/exposition.d.ts +18 -0
- package/transpiled/exposition.js +18 -0
- package/transpiled/exposition.js.map +1 -0
- package/transpiled/guard.d.ts +7 -0
- package/transpiled/guard.js +12 -0
- package/transpiled/guard.js.map +1 -0
- package/transpiled/index.d.ts +35 -0
- package/transpiled/index.js +29 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/locator.d.ts +12 -0
- package/transpiled/locator.js +32 -0
- package/transpiled/locator.js.map +1 -0
- package/transpiled/observation.d.ts +5 -0
- package/transpiled/observation.js +12 -0
- package/transpiled/observation.js.map +1 -0
- package/transpiled/operation.d.ts +49 -0
- package/transpiled/operation.js +78 -0
- package/transpiled/operation.js.map +1 -0
- package/transpiled/outbox/index.d.ts +1 -0
- package/transpiled/outbox/index.js +2 -0
- package/transpiled/outbox/index.js.map +1 -0
- package/transpiled/outbox/outbox.d.ts +46 -0
- package/transpiled/outbox/outbox.js +268 -0
- package/transpiled/outbox/outbox.js.map +1 -0
- package/transpiled/query/criteria.d.ts +6 -0
- package/transpiled/query/criteria.js +74 -0
- package/transpiled/query/criteria.js.map +1 -0
- package/transpiled/query/options.d.ts +3 -0
- package/transpiled/query/options.js +27 -0
- package/transpiled/query/options.js.map +1 -0
- package/transpiled/query.d.ts +9 -0
- package/transpiled/query.js +57 -0
- package/transpiled/query.js.map +1 -0
- package/transpiled/receiver.d.ts +20 -0
- package/transpiled/receiver.js +92 -0
- package/transpiled/receiver.js.map +1 -0
- package/transpiled/reflection.d.ts +9 -0
- package/transpiled/reflection.js +13 -0
- package/transpiled/reflection.js.map +1 -0
- package/transpiled/remote.d.ts +10 -0
- package/transpiled/remote.js +12 -0
- package/transpiled/remote.js.map +1 -0
- package/transpiled/state.d.ts +33 -0
- package/transpiled/state.js +126 -0
- package/transpiled/state.js.map +1 -0
- package/transpiled/transition.d.ts +14 -0
- package/transpiled/transition.js +51 -0
- package/transpiled/transition.js.map +1 -0
- package/transpiled/transmission.d.ts +8 -0
- package/transpiled/transmission.js +30 -0
- package/transpiled/transmission.js.map +1 -0
- package/transpiled/types/atomicity.d.ts +56 -0
- package/transpiled/types/atomicity.js +2 -0
- package/transpiled/types/atomicity.js.map +1 -0
- package/transpiled/types/bindings.d.ts +42 -0
- package/transpiled/types/bindings.js +2 -0
- package/transpiled/types/bindings.js.map +1 -0
- package/transpiled/types/bridges.d.ts +38 -0
- package/transpiled/types/bridges.js +2 -0
- package/transpiled/types/bridges.js.map +1 -0
- package/transpiled/types/extensions.d.ts +62 -0
- package/transpiled/types/extensions.js +2 -0
- package/transpiled/types/extensions.js.map +1 -0
- package/transpiled/types/index.d.ts +13 -0
- package/transpiled/types/index.js +8 -0
- package/transpiled/types/index.js.map +1 -0
- package/transpiled/types/message.d.ts +5 -0
- package/transpiled/types/message.js +2 -0
- package/transpiled/types/message.js.map +1 -0
- package/transpiled/types/operations.d.ts +15 -0
- package/transpiled/types/operations.js +2 -0
- package/transpiled/types/operations.js.map +1 -0
- package/transpiled/types/outbox.d.ts +28 -0
- package/transpiled/types/outbox.js +2 -0
- package/transpiled/types/outbox.js.map +1 -0
- package/transpiled/types/receiver.d.ts +9 -0
- package/transpiled/types/receiver.js +2 -0
- package/transpiled/types/receiver.js.map +1 -0
- package/transpiled/types/request.d.ts +55 -0
- package/transpiled/types/request.js +2 -0
- package/transpiled/types/request.js.map +1 -0
- package/transpiled/types/state.d.ts +9 -0
- package/transpiled/types/state.js +2 -0
- package/transpiled/types/state.js.map +1 -0
- package/transpiled/types/storages.d.ts +111 -0
- package/transpiled/types/storages.js +2 -0
- package/transpiled/types/storages.js.map +1 -0
- package/transpiled/unmanaged.d.ts +5 -0
- package/transpiled/unmanaged.js +7 -0
- package/transpiled/unmanaged.js.map +1 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/src/assignment.js +0 -31
- package/src/call.js +0 -42
- package/src/cascade.js +0 -36
- package/src/component.js +0 -41
- package/src/composition.js +0 -25
- package/src/connector.js +0 -183
- package/src/context.js +0 -55
- package/src/contract/conditions.js +0 -21
- package/src/contract/index.js +0 -7
- package/src/contract/reply.js +0 -25
- package/src/contract/request.js +0 -67
- package/src/contract/schemas/error.yaml +0 -9
- package/src/contract/schemas/index.js +0 -7
- package/src/contract/schemas/query.yaml +0 -30
- package/src/discovery.js +0 -44
- package/src/emission.js +0 -23
- package/src/entities/changeset.js +0 -43
- package/src/entities/entity.js +0 -73
- package/src/entities/factory.js +0 -33
- package/src/entities/index.js +0 -5
- package/src/entities/set.js +0 -15
- package/src/event.js +0 -40
- package/src/exceptions.js +0 -100
- package/src/exposition.js +0 -27
- package/src/index.js +0 -47
- package/src/locator.js +0 -54
- package/src/observation.js +0 -20
- package/src/operation.js +0 -69
- package/src/query/criteria.js +0 -41
- package/src/query/options.js +0 -40
- package/src/query.js +0 -41
- package/src/receiver.js +0 -65
- package/src/reflection.js +0 -28
- package/src/remote.js +0 -17
- package/src/state.js +0 -99
- package/src/transition.js +0 -73
- package/src/transmission.js +0 -33
- package/types/bindings.d.ts +0 -40
- package/types/bridges.ts +0 -30
- package/types/component.d.ts +0 -9
- package/types/connector.d.ts +0 -18
- package/types/context.d.ts +0 -21
- package/types/entity.d.ts +0 -46
- package/types/event.d.ts +0 -11
- package/types/exception.d.ts +0 -4
- package/types/extensions.d.ts +0 -29
- package/types/index.ts +0 -14
- package/types/locator.d.ts +0 -13
- package/types/operations.d.ts +0 -2
- package/types/query.d.ts +0 -15
- package/types/receiver.d.ts +0 -12
- package/types/reflection.d.ts +0 -14
- package/types/request.d.ts +0 -23
- package/types/state.d.ts +0 -35
- package/types/storages.d.ts +0 -71
package/source/state.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { EntitySet } from './entities/set.js'
|
|
2
|
+
import { StatePreconditionException, StateNotFoundException } from './exceptions.js'
|
|
3
|
+
import type { Readable } from 'node:stream'
|
|
4
|
+
import type { Factory } from './entities/factory.js'
|
|
5
|
+
import type { Entity } from './entities/entity.js'
|
|
6
|
+
import type { Changeset } from './entities/changeset.js'
|
|
7
|
+
import type { Outbox } from './outbox/outbox.js'
|
|
8
|
+
import type { Row } from './types/outbox.js'
|
|
9
|
+
import type { Query, Record, Storage } from './types/storages.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What an operation acquires and commits. One method per scope a manifest may declare, which
|
|
13
|
+
* is how an operation reaches the one it was declared with.
|
|
14
|
+
*/
|
|
15
|
+
export class State {
|
|
16
|
+
public readonly storage: Storage
|
|
17
|
+
|
|
18
|
+
readonly #entities: Factory
|
|
19
|
+
readonly #outbox: Outbox | undefined
|
|
20
|
+
|
|
21
|
+
/** whether an entity exists by virtue of the identity it is looked up by */
|
|
22
|
+
readonly #associated: boolean
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line max-params
|
|
25
|
+
public constructor(
|
|
26
|
+
storage: Storage,
|
|
27
|
+
entities: Factory,
|
|
28
|
+
outbox: Outbox | undefined,
|
|
29
|
+
associated?: boolean
|
|
30
|
+
) {
|
|
31
|
+
this.storage = storage
|
|
32
|
+
|
|
33
|
+
this.#entities = entities
|
|
34
|
+
this.#outbox = outbox
|
|
35
|
+
this.#associated = associated === true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public init(id?: string): Entity {
|
|
39
|
+
return this.#entities.init(id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public fit(values: object): void {
|
|
43
|
+
this.#entities.fit(values)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** scope `object` */
|
|
47
|
+
public async object(query: Query, mutable = true): Promise<Entity | null> {
|
|
48
|
+
const record = await this.storage.get(query)
|
|
49
|
+
|
|
50
|
+
if (record !== null) return this.#entities.object(record, mutable)
|
|
51
|
+
|
|
52
|
+
// an associated entity is whatever the identity it is looked up by names, so a miss on
|
|
53
|
+
// that identity alone is a blank rather than an absence
|
|
54
|
+
if (
|
|
55
|
+
this.#associated &&
|
|
56
|
+
query.id !== undefined &&
|
|
57
|
+
query.criteria === undefined &&
|
|
58
|
+
query.version === undefined
|
|
59
|
+
)
|
|
60
|
+
return this.init(query.id)
|
|
61
|
+
|
|
62
|
+
if (query.version !== undefined) throw new StatePreconditionException()
|
|
63
|
+
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** scope `objects` */
|
|
68
|
+
public async objects(query: Query, mutable = true): Promise<EntitySet> {
|
|
69
|
+
const recordset = await this.storage.find(query)
|
|
70
|
+
const ids = query.ids
|
|
71
|
+
|
|
72
|
+
const missing = this.#associated && ids !== undefined && recordset.length < ids.length
|
|
73
|
+
|
|
74
|
+
const init = missing
|
|
75
|
+
? ids.filter((id) => !recordset.some((record) => record.id === id))
|
|
76
|
+
: undefined
|
|
77
|
+
|
|
78
|
+
return this.#entities.objects(recordset, init, mutable)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** scope `stream` */
|
|
82
|
+
public async stream(query: Query): Promise<Readable> {
|
|
83
|
+
return this.storage.stream(query)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** scope `changeset` */
|
|
87
|
+
public changeset(query: Query): Changeset {
|
|
88
|
+
return this.#entities.changeset(query)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** scope `none`: an operation that acquires nothing still asks for its scope */
|
|
92
|
+
public none(): null {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** get-or-create, in one indivisible step */
|
|
97
|
+
public async ensure(
|
|
98
|
+
query: Query | undefined,
|
|
99
|
+
properties: object,
|
|
100
|
+
input?: object
|
|
101
|
+
): Promise<Entity> {
|
|
102
|
+
const object = this.#entities.init()
|
|
103
|
+
const blank = object.get()
|
|
104
|
+
|
|
105
|
+
Object.assign(blank, properties)
|
|
106
|
+
object.set(blank)
|
|
107
|
+
|
|
108
|
+
const row = this.#outbox?.row(object.event(input))
|
|
109
|
+
const record = await this.storage.ensure(query, properties, object.get(), row)
|
|
110
|
+
|
|
111
|
+
// whatever came back under another id was already there, and an effect never commits it
|
|
112
|
+
if (record.id !== blank.id) return this.#entities.object(record, NOT_MUTABLE)
|
|
113
|
+
|
|
114
|
+
await this.#publish(row)
|
|
115
|
+
|
|
116
|
+
return object
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public async commit(state: Entity | EntitySet, input?: object): Promise<boolean> {
|
|
120
|
+
if (state instanceof EntitySet) return this.massCommit(state, input)
|
|
121
|
+
|
|
122
|
+
// the row is built before the write so that the storage can commit it in the same
|
|
123
|
+
// transaction, closing the window this used to have
|
|
124
|
+
const row = this.#outbox?.row(state.event(input))
|
|
125
|
+
const ok = await this.storage.store(state.get(), row)
|
|
126
|
+
|
|
127
|
+
if (ok) await this.#publish(row)
|
|
128
|
+
|
|
129
|
+
return ok
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async massCommit(state: EntitySet, input?: object): Promise<boolean> {
|
|
133
|
+
const outbox = this.#outbox
|
|
134
|
+
|
|
135
|
+
const rows =
|
|
136
|
+
outbox === undefined
|
|
137
|
+
? undefined
|
|
138
|
+
: state.events(input).map((event) => outbox.row(event))
|
|
139
|
+
|
|
140
|
+
const ok = await this.storage.massStore(state.get(), rows)
|
|
141
|
+
|
|
142
|
+
if (ok && rows !== undefined)
|
|
143
|
+
await Promise.all(rows.map(async (row) => this.#publish(row)))
|
|
144
|
+
|
|
145
|
+
return ok
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public async apply(state: Changeset, input?: object): Promise<Record> {
|
|
149
|
+
// an assignment's images are the write's own, so the storage fills them in
|
|
150
|
+
const row = this.#outbox?.row({ input })
|
|
151
|
+
const result = await this.storage.upsert(state.query, state.export(), row)
|
|
152
|
+
|
|
153
|
+
if (result === null)
|
|
154
|
+
throw state.query.version === undefined
|
|
155
|
+
? new StateNotFoundException()
|
|
156
|
+
: new StatePreconditionException()
|
|
157
|
+
|
|
158
|
+
if (row !== undefined) {
|
|
159
|
+
// a storage that does not know how leaves the event with what it was given
|
|
160
|
+
row.event.state ??= result
|
|
161
|
+
row.event.origin ??= null
|
|
162
|
+
|
|
163
|
+
await this.#publish(row)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Without a durable outbox this is the emission itself, and the operation waits for it;
|
|
171
|
+
* with one the row is already committed and this returns at once.
|
|
172
|
+
*/
|
|
173
|
+
async #publish(row: Row | undefined): Promise<void> {
|
|
174
|
+
if (row !== undefined) await this.#outbox?.publish(row)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** an effect never commits what `ensure` hands it, see `Effect` */
|
|
179
|
+
const NOT_MUTABLE = false
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { retry } from '@toa.io/generic'
|
|
2
|
+
import { Operation } from './operation.js'
|
|
3
|
+
import { StateConcurrencyException, StateNotFoundException } from './exceptions.js'
|
|
4
|
+
import type { Contracts, Definition, Store } from './operation.js'
|
|
5
|
+
import type { Cascade } from './cascade.js'
|
|
6
|
+
import type { State } from './state.js'
|
|
7
|
+
import type { Query as Translator } from './query.js'
|
|
8
|
+
import type { Entity } from './entities/entity.js'
|
|
9
|
+
|
|
10
|
+
export class Transition extends Operation {
|
|
11
|
+
/** a transition is the only operation that commits */
|
|
12
|
+
protected override mutable = true
|
|
13
|
+
|
|
14
|
+
readonly #concurrency: string | undefined
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line max-params
|
|
17
|
+
public constructor(
|
|
18
|
+
cascade: Cascade,
|
|
19
|
+
scope: State,
|
|
20
|
+
contract: Contracts,
|
|
21
|
+
query: Translator,
|
|
22
|
+
definition: Definition
|
|
23
|
+
) {
|
|
24
|
+
super(cascade, scope, contract, query, definition)
|
|
25
|
+
|
|
26
|
+
this.#concurrency = definition.concurrency
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected override async process(store: Store): Promise<any> {
|
|
30
|
+
return retry(async (retry) => this.#retry(store, retry), RETRY)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected override async acquire(store: Store): Promise<void> {
|
|
34
|
+
const { request } = store
|
|
35
|
+
|
|
36
|
+
store.scope =
|
|
37
|
+
request.query === undefined ? this.scope.init() : await this.query(request.query)
|
|
38
|
+
|
|
39
|
+
const entity = store.scope as Entity | null
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
entity === null ||
|
|
43
|
+
(entity.deleted && (request.query as any)?.options?.deleted !== true)
|
|
44
|
+
)
|
|
45
|
+
throw new StateNotFoundException()
|
|
46
|
+
|
|
47
|
+
store.state = entity.get()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected override async commit(store: Store): Promise<void> {
|
|
51
|
+
const { scope, state, reply, retry } = store
|
|
52
|
+
|
|
53
|
+
if (reply.error !== undefined) return
|
|
54
|
+
|
|
55
|
+
const entity = scope as Entity
|
|
56
|
+
|
|
57
|
+
entity.set(state)
|
|
58
|
+
|
|
59
|
+
const result = await this.scope.commit(entity, store.request.input)
|
|
60
|
+
|
|
61
|
+
if (result !== false) return
|
|
62
|
+
|
|
63
|
+
if (this.#concurrency === 'retry') await retry?.()
|
|
64
|
+
else throw new StateConcurrencyException()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async #retry(store: Store, retry: () => Promise<any>): Promise<any> {
|
|
68
|
+
store.retry = retry
|
|
69
|
+
|
|
70
|
+
return super.process(store)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const RETRY = {
|
|
75
|
+
base: 10,
|
|
76
|
+
max: 5000,
|
|
77
|
+
dispersion: 1,
|
|
78
|
+
retries: 32
|
|
79
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Connector } from './connector.js'
|
|
2
|
+
import { TransmissionException } from './exceptions.js'
|
|
3
|
+
import type { Consumer } from './types/bindings.js'
|
|
4
|
+
import type { Request } from './types/request.js'
|
|
5
|
+
|
|
6
|
+
export class Transmission extends Connector {
|
|
7
|
+
readonly #bindings: Consumer[]
|
|
8
|
+
|
|
9
|
+
public constructor(bindings: Consumer[]) {
|
|
10
|
+
super()
|
|
11
|
+
|
|
12
|
+
this.#bindings = bindings
|
|
13
|
+
this.depends(bindings)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async request(request: Request): Promise<any> {
|
|
17
|
+
let reply: any = false
|
|
18
|
+
let i = 0
|
|
19
|
+
|
|
20
|
+
while (reply === false && i < this.#bindings.length) {
|
|
21
|
+
const binding = this.#bindings[i]
|
|
22
|
+
|
|
23
|
+
i++
|
|
24
|
+
|
|
25
|
+
if (request?.task === true) {
|
|
26
|
+
if (binding.task === undefined) continue
|
|
27
|
+
|
|
28
|
+
await binding.task(request)
|
|
29
|
+
reply = null
|
|
30
|
+
} else reply = await binding.request(request)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (reply === false)
|
|
34
|
+
throw new TransmissionException(`All (${this.#bindings.length}) bindings rejected.`)
|
|
35
|
+
|
|
36
|
+
return reply
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Connector } from '../connector.js'
|
|
2
|
+
|
|
3
|
+
/** Which of the group this replica is, and how many of them there are. */
|
|
4
|
+
export interface Assignment {
|
|
5
|
+
i: number
|
|
6
|
+
n: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What one group of replicas decides together, in one place. The decisions here are the ones
|
|
11
|
+
* processes cannot arrange by talking to each other: they need a single arbiter and a step
|
|
12
|
+
* indivisible from its point of view.
|
|
13
|
+
*/
|
|
14
|
+
export interface Atom extends Connector {
|
|
15
|
+
/**
|
|
16
|
+
* An exclusive claim on slots of `0..total`: while this replica holds one, no other
|
|
17
|
+
* replica of the group does. Answered from memory, so it costs nothing to ask.
|
|
18
|
+
*
|
|
19
|
+
* `null` while this replica owns nothing — after a restart, during a rollout, or while
|
|
20
|
+
* coordination is unreachable. Whoever asks must be able to stand down: acting on a claim
|
|
21
|
+
* that cannot be supported is a different guarantee, not a degraded one.
|
|
22
|
+
*/
|
|
23
|
+
slots(total: number): number[] | null
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calls `listener` with the assignment this replica holds, and again whenever it changes —
|
|
27
|
+
* one arrived, was lost, or the group resized. Answers with what removes the listener
|
|
28
|
+
* again.
|
|
29
|
+
*
|
|
30
|
+
* A change and not a heartbeat: a group that stays as it is never calls back. It is called
|
|
31
|
+
* once as it is added, with the claim as it stands.
|
|
32
|
+
*/
|
|
33
|
+
onassigned(listener: (assignment: Assignment | null) => void): () => void
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Debt the group has run up under each key, in milliseconds. Every call adds its own deltas
|
|
37
|
+
* and reads back where the group stands, so a replica reports what it alone has spent and
|
|
38
|
+
* still decides on what all of them have.
|
|
39
|
+
*
|
|
40
|
+
* Rejects where there is nothing to arbitrate through.
|
|
41
|
+
*/
|
|
42
|
+
meter(keys: string[], deltas: number[]): Promise<number[]>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Runs `routine` holding `keys`, and while it holds them no other replica of the group
|
|
46
|
+
* does. Waits for as long as it takes to acquire them.
|
|
47
|
+
*
|
|
48
|
+
* The lease is extended for as long as the routine runs. An extension that fails aborts the
|
|
49
|
+
* signal the routine is given, which is the only way it learns it no longer holds what it
|
|
50
|
+
* is working under.
|
|
51
|
+
*
|
|
52
|
+
* Rejects where there is nothing to arbitrate through.
|
|
53
|
+
*/
|
|
54
|
+
lock<T>(
|
|
55
|
+
keys: string | string[],
|
|
56
|
+
routine: (signal: AbortSignal, context: unknown) => Promise<T>
|
|
57
|
+
): Promise<T>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Factory {
|
|
61
|
+
/** @param group what the replicas deciding together have in common */
|
|
62
|
+
atom(group: string, options?: { interval?: number }): Atom
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream'
|
|
2
|
+
import type { Connector } from '../connector.js'
|
|
3
|
+
import type { Locator } from '../locator.js'
|
|
4
|
+
import type { Component } from '../component.js'
|
|
5
|
+
import type { Receiver } from './receiver.js'
|
|
6
|
+
import type { Message } from './message.js'
|
|
7
|
+
import type { Reply, Request } from './request.js'
|
|
8
|
+
|
|
9
|
+
/** What a binding module exports beside its factory. */
|
|
10
|
+
export interface Properties {
|
|
11
|
+
/** delivers asynchronously; an event's binding must */
|
|
12
|
+
async?: boolean
|
|
13
|
+
/** in-process: its producers are connected first and torn down last */
|
|
14
|
+
local?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Consumer extends Connector {
|
|
18
|
+
/**
|
|
19
|
+
* `false` says this binding does not carry the endpoint, and the transmission tries the
|
|
20
|
+
* next one. A `Readable` is a streamed reply.
|
|
21
|
+
*/
|
|
22
|
+
request(request: Request): Promise<Reply | Readable | false>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Absent where the binding cannot enqueue — a transmission skips a binding that offers
|
|
26
|
+
* no `task` rather than failing over it.
|
|
27
|
+
*/
|
|
28
|
+
task?(request: Request): Promise<void>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Emitter extends Connector {
|
|
32
|
+
emit(message: Message): Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Broadcast<L extends string = string> extends Connector {
|
|
36
|
+
transmit<T>(label: L, payload: T): Promise<void>
|
|
37
|
+
|
|
38
|
+
receive<T>(label: L, callback: (payload: T) => void | Promise<void>): Promise<void>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Factory {
|
|
42
|
+
producer(locator: Locator, endpoints: string[], component: Component): Connector
|
|
43
|
+
|
|
44
|
+
consumer(locator: Locator, endpoint: string): Consumer
|
|
45
|
+
|
|
46
|
+
/** only the binding an event declares is asked for one */
|
|
47
|
+
emitter?(locator: Locator, label: string): Emitter
|
|
48
|
+
|
|
49
|
+
/** `group` is absent for an exclusive subscription */
|
|
50
|
+
// eslint-disable-next-line max-params
|
|
51
|
+
receiver?(
|
|
52
|
+
locator: Locator,
|
|
53
|
+
label: string,
|
|
54
|
+
group: string | undefined,
|
|
55
|
+
receiver: Receiver
|
|
56
|
+
): Connector
|
|
57
|
+
|
|
58
|
+
broadcast?(name: string, group?: string): Broadcast
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream'
|
|
2
|
+
import type { Connector } from '../connector.js'
|
|
3
|
+
import type { Context } from '../context.js'
|
|
4
|
+
import type { Event as StateEvent } from './state.js'
|
|
5
|
+
import type { Reply, Request } from './request.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What runs one endpoint. `mount` and `unmount` are the component author's, called by the
|
|
9
|
+
* bridge as it opens and closes; core only ever asks for `execute`.
|
|
10
|
+
*/
|
|
11
|
+
export interface Algorithm extends Connector {
|
|
12
|
+
execute(input: any, scope?: object | object[]): Promise<Reply | Readable>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Event extends Connector {
|
|
16
|
+
condition(event: StateEvent): Promise<boolean>
|
|
17
|
+
|
|
18
|
+
payload(event: StateEvent): Promise<object>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Receiver extends Connector {
|
|
22
|
+
condition(payload: object): Promise<boolean>
|
|
23
|
+
|
|
24
|
+
/** the trailing arguments are what the receiver declaration named */
|
|
25
|
+
request(payload: object, ...args: unknown[]): Promise<Request>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runs on every change to an entity's state, before the contract is applied. */
|
|
29
|
+
export interface Guard {
|
|
30
|
+
fit(state: object, origin: object | null): boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Connectors whose lifecycle moments are the component's. */
|
|
34
|
+
export interface RunCommands {
|
|
35
|
+
preflight?: Connector
|
|
36
|
+
settle?: Connector
|
|
37
|
+
dispose?: Connector
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Factory {
|
|
41
|
+
algorithm(
|
|
42
|
+
path: string,
|
|
43
|
+
endpoint: string,
|
|
44
|
+
context: Context
|
|
45
|
+
): Algorithm | Promise<Algorithm>
|
|
46
|
+
|
|
47
|
+
event?(path: string, label: string, context: Context): Event | Promise<Event>
|
|
48
|
+
|
|
49
|
+
receiver?(path: string, label: string): Receiver | Promise<Receiver>
|
|
50
|
+
|
|
51
|
+
guard?(path: string, name: string, context: Context): Guard | Promise<Guard>
|
|
52
|
+
|
|
53
|
+
rc?(path: string, context: Context): Promise<RunCommands | undefined>
|
|
54
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Connector } from '../connector.js'
|
|
2
|
+
import type { Locator } from '../locator.js'
|
|
3
|
+
import type { Component } from '../component.js'
|
|
4
|
+
import type { Remote } from '../remote.js'
|
|
5
|
+
import type { Receiver } from './receiver.js'
|
|
6
|
+
import type { Context } from '../context.js'
|
|
7
|
+
import type { Storage } from './storages.js'
|
|
8
|
+
import type { Broadcast, Emitter } from './bindings.js'
|
|
9
|
+
import type { Atom } from './atomicity.js'
|
|
10
|
+
import type { Source } from './request.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the process hosting an extension provides to it: the counterpart of a component's
|
|
14
|
+
* context. What is returned is a connector the extension depends on.
|
|
15
|
+
*/
|
|
16
|
+
export interface Host {
|
|
17
|
+
/** a component of the context, by locator */
|
|
18
|
+
remote(locator: Locator, source?: Source): Promise<Remote>
|
|
19
|
+
|
|
20
|
+
/** a channel of the messaging binding */
|
|
21
|
+
broadcast<L extends string = string>(
|
|
22
|
+
channel: string,
|
|
23
|
+
group?: string
|
|
24
|
+
): Promise<Broadcast<L>>
|
|
25
|
+
|
|
26
|
+
/** components to run inside the extension's own process */
|
|
27
|
+
composition(paths: string[]): Promise<Connector>
|
|
28
|
+
|
|
29
|
+
/** a consumer of an event of the context, `namespace.component.event` */
|
|
30
|
+
receive(label: string, receiver: Receiver): Promise<Connector>
|
|
31
|
+
|
|
32
|
+
/** what the replicas of one group decide together */
|
|
33
|
+
atom(group: string): Atom
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `Manifest` is a type parameter rather than an import: `@toa.io/norm` depends on core, so
|
|
38
|
+
* core cannot name its types.
|
|
39
|
+
*/
|
|
40
|
+
export interface Factory<Manifest = unknown> {
|
|
41
|
+
tenant?(
|
|
42
|
+
locator: Locator,
|
|
43
|
+
declaration: any,
|
|
44
|
+
manifest: Manifest
|
|
45
|
+
): Connector | Promise<Connector>
|
|
46
|
+
|
|
47
|
+
aspect?(locator: Locator, declaration: any): Aspect | Aspect[]
|
|
48
|
+
|
|
49
|
+
/** what the extension runs as a process of its own; `null` where it is off here */
|
|
50
|
+
service?(): Connector | null | Promise<Connector | null>
|
|
51
|
+
|
|
52
|
+
component?(component: Component): Component
|
|
53
|
+
|
|
54
|
+
context?(context: Context): Context
|
|
55
|
+
|
|
56
|
+
manage?(composition: Connector): Connector
|
|
57
|
+
|
|
58
|
+
storage?(storage: Storage): Storage
|
|
59
|
+
|
|
60
|
+
emitter?(emitter: Emitter, label: string, locator: Locator): Emitter
|
|
61
|
+
|
|
62
|
+
receiver?(receiver: Receiver, locator: Locator): Receiver
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Aspect extends Connector {
|
|
66
|
+
/** the key it takes on the context; a duplicate is a boot error */
|
|
67
|
+
readonly name: string
|
|
68
|
+
|
|
69
|
+
invoke(...args: any[]): any
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What an extension puts on a component's context, as the extension states it. How it is
|
|
74
|
+
* presented there is the bridge's — a bash bridge has no context to put anything on — and
|
|
75
|
+
* what is declared here is the key and what it holds.
|
|
76
|
+
*/
|
|
77
|
+
export interface Contribution {
|
|
78
|
+
/** the key on the context */
|
|
79
|
+
name: string
|
|
80
|
+
|
|
81
|
+
/** what the key holds, as TypeScript */
|
|
82
|
+
type?: string
|
|
83
|
+
|
|
84
|
+
/** what `type` names, by the module it comes from */
|
|
85
|
+
imports?: Record<string, string[]>
|
|
86
|
+
|
|
87
|
+
/** a JSON Schema to read the type from instead, where a component states one */
|
|
88
|
+
schema?: object
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * as atomicity from './atomicity.js'
|
|
2
|
+
export * as bindings from './bindings.js'
|
|
3
|
+
export * as bridges from './bridges.js'
|
|
4
|
+
export * as extensions from './extensions.js'
|
|
5
|
+
export * as operations from './operations.js'
|
|
6
|
+
export * as outbox from './outbox.js'
|
|
7
|
+
export * as storages from './storages.js'
|
|
8
|
+
|
|
9
|
+
export type { Contribution } from './extensions.js'
|
|
10
|
+
export type { Event } from './state.js'
|
|
11
|
+
export type { Message } from './message.js'
|
|
12
|
+
export type { Receiver } from './receiver.js'
|
|
13
|
+
export type { Call, Guard, Observation, Transition } from './operations.js'
|
|
14
|
+
export type { Maybe, Query, Reply, RemoteError, Request, Source } from './request.js'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Request } from './request.js'
|
|
2
|
+
|
|
3
|
+
export type type =
|
|
4
|
+
| 'transition'
|
|
5
|
+
| 'observation'
|
|
6
|
+
| 'assignment'
|
|
7
|
+
| 'computation'
|
|
8
|
+
| 'effect'
|
|
9
|
+
| 'unmanaged'
|
|
10
|
+
|
|
11
|
+
export type scope = 'object' | 'objects' | 'changeset' | 'stream' | 'none'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How an endpoint is called. What it resolves to is what the operation declares — `toa types`
|
|
15
|
+
* writes that per operation, and these are what a hand-written context says instead.
|
|
16
|
+
*/
|
|
17
|
+
export type Call<Output = any, Input = any> = (request: Request<Input>) => Promise<Output>
|
|
18
|
+
|
|
19
|
+
export type Observation<Output = any, Input = never, Entity = unknown> = (
|
|
20
|
+
request: Request<Input, Entity>
|
|
21
|
+
) => Promise<Output extends unknown[] ? Output : Output | null>
|
|
22
|
+
|
|
23
|
+
export type Transition<Output = any, Input = never, Entity = unknown> = (
|
|
24
|
+
request: Request<Input, Entity>
|
|
25
|
+
) => Promise<Output | null>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runs on every change to an entity's state, before the contract is applied. `false` refuses
|
|
29
|
+
* the change as `EntityGuard`.
|
|
30
|
+
*/
|
|
31
|
+
export type Guard<T, C = unknown> = (state: T, origin: T | null, context: C) => boolean
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Event } from './state.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The intent to publish, committed with the state change it belongs to. Everything about it
|
|
5
|
+
* is core's: the storage writes it and, where the images are the write's own, fills
|
|
6
|
+
* `event.origin` and `event.state` in.
|
|
7
|
+
*/
|
|
8
|
+
export interface Row {
|
|
9
|
+
id: string
|
|
10
|
+
|
|
11
|
+
/** which replica pumps this row; carries no other meaning, and no ordering */
|
|
12
|
+
lane: number
|
|
13
|
+
|
|
14
|
+
published: boolean
|
|
15
|
+
|
|
16
|
+
/** not before this */
|
|
17
|
+
pending: number
|
|
18
|
+
|
|
19
|
+
/** an assignment's images are absent until the storage fills them in */
|
|
20
|
+
event: Event
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The read side of an outbox. What writes a row is the storage's own: it happens inside the
|
|
25
|
+
* transaction the storage opened, which core never reaches into.
|
|
26
|
+
*/
|
|
27
|
+
export interface Storage {
|
|
28
|
+
/**
|
|
29
|
+
* One page of what is due, still unpublished, and in one of the given lanes, in the order
|
|
30
|
+
* the rows were written. `after` continues from the last id of the page before.
|
|
31
|
+
*/
|
|
32
|
+
pending(lanes: number[], now: number, limit: number, after?: string): Promise<Row[]>
|
|
33
|
+
|
|
34
|
+
settle(ids: string[]): Promise<void>
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Connector } from '../connector.js'
|
|
2
|
+
import type { Message } from './message.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What consumes an event of the context. Core ships one, and an extension may put its own
|
|
6
|
+
* in its place, so what a binding is handed is this rather than core's class.
|
|
7
|
+
*/
|
|
8
|
+
export interface Receiver extends Connector {
|
|
9
|
+
receive(message: Message): Promise<void>
|
|
10
|
+
}
|