@toa.io/core 1.0.0-alpha.283 → 1.0.0-alpha.286

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 (290) hide show
  1. package/package.json +19 -7
  2. package/source/assignment.ts +27 -0
  3. package/{src/call.js → source/call.ts} +16 -21
  4. package/{src/cascade.js → source/cascade.ts} +6 -8
  5. package/{src/component.js → source/component.ts} +27 -25
  6. package/{src/composition.js → source/composition.ts} +9 -3
  7. package/{src/connector.js → source/connector.ts} +36 -55
  8. package/source/context.ts +67 -0
  9. package/source/contract/contract.ts +23 -0
  10. package/{src/contract/reply.js → source/contract/reply.ts} +26 -19
  11. package/source/contract/request.ts +85 -0
  12. package/source/contract/schemas.ts +39 -0
  13. package/{src/discovery.js → source/discovery.ts} +15 -8
  14. package/source/effect.ts +14 -0
  15. package/{src/emission.js → source/emission.ts} +5 -3
  16. package/source/entities/changeset.ts +37 -0
  17. package/source/entities/entity.ts +137 -0
  18. package/source/entities/factory.ts +52 -0
  19. package/source/entities/index.ts +4 -0
  20. package/{src/entities/newid.js → source/entities/newid.ts} +1 -1
  21. package/{src/entities/set.js → source/entities/set.ts} +8 -5
  22. package/source/event.ts +57 -0
  23. package/{src/exceptions.js → source/exceptions.ts} +52 -33
  24. package/source/exposition.ts +33 -0
  25. package/source/guard.ts +16 -0
  26. package/{src/index.js → source/index.ts} +8 -0
  27. package/{src/locator.js → source/locator.ts} +17 -30
  28. package/source/observation.ts +16 -0
  29. package/source/operation.ts +138 -0
  30. package/{src/outbox/outbox.js → source/outbox/outbox.ts} +95 -51
  31. package/source/query/criteria.ts +90 -0
  32. package/source/query/options.ts +35 -0
  33. package/{src/query.js → source/query.ts} +14 -17
  34. package/source/receiver.ts +120 -0
  35. package/source/reflection.ts +20 -0
  36. package/source/remote.ts +20 -0
  37. package/source/state.ts +179 -0
  38. package/source/transition.ts +79 -0
  39. package/{src/transmission.js → source/transmission.ts} +8 -8
  40. package/source/types/atomicity.ts +63 -0
  41. package/source/types/bindings.ts +59 -0
  42. package/source/types/bridges.ts +54 -0
  43. package/source/types/extensions.ts +89 -0
  44. package/{types → source/types}/index.ts +9 -16
  45. package/{types/message.d.ts → source/types/message.ts} +2 -1
  46. package/source/types/operations.ts +31 -0
  47. package/source/types/outbox.ts +35 -0
  48. package/source/types/receiver.ts +10 -0
  49. package/{types/request.d.ts → source/types/request.ts} +8 -9
  50. package/source/types/state.ts +9 -0
  51. package/source/types/storages.ts +129 -0
  52. package/source/unmanaged.ts +8 -0
  53. package/test/call.fixtures.js +3 -1
  54. package/test/call.test.js +28 -7
  55. package/test/component.test.js +10 -5
  56. package/test/connector.fixtures.js +7 -7
  57. package/test/connector.test.js +15 -2
  58. package/test/context.fixtures.js +1 -1
  59. package/test/context.test.js +4 -4
  60. package/test/contract/conditions.test.js +6 -2
  61. package/test/contract/contract.fixtures.js +3 -4
  62. package/test/contract/request.test.js +68 -34
  63. package/test/discovery.test.js +10 -3
  64. package/test/emission.test.js +16 -8
  65. package/test/entities/entity.fixtures.js +5 -6
  66. package/test/entities/entity.test.js +54 -20
  67. package/test/entities/factory.fixtures.js +11 -10
  68. package/test/entities/factory.test.js +47 -10
  69. package/test/entities/set.test.js +1 -1
  70. package/test/event.test.js +29 -11
  71. package/test/locator.test.js +10 -4
  72. package/test/outbox.test.js +95 -14
  73. package/test/query.test.js +76 -7
  74. package/test/receiver.fixtures.js +2 -2
  75. package/test/receiver.test.js +53 -27
  76. package/test/reflection.test.js +3 -3
  77. package/test/state.fixtures.js +10 -3
  78. package/test/state.test.js +77 -14
  79. package/test/transmission.test.js +25 -12
  80. package/transpiled/assignment.d.ts +6 -0
  81. package/transpiled/assignment.js +20 -0
  82. package/transpiled/assignment.js.map +1 -0
  83. package/transpiled/call.d.ts +10 -0
  84. package/transpiled/call.js +52 -0
  85. package/transpiled/call.js.map +1 -0
  86. package/transpiled/cascade.d.ts +7 -0
  87. package/transpiled/cascade.js +28 -0
  88. package/transpiled/cascade.js.map +1 -0
  89. package/transpiled/component.d.ts +15 -0
  90. package/transpiled/component.js +61 -0
  91. package/transpiled/component.js.map +1 -0
  92. package/transpiled/composition.d.ts +6 -0
  93. package/transpiled/composition.js +23 -0
  94. package/transpiled/composition.js.map +1 -0
  95. package/transpiled/connector.d.ts +47 -0
  96. package/transpiled/connector.js +143 -0
  97. package/transpiled/connector.js.map +1 -0
  98. package/transpiled/context.d.ts +18 -0
  99. package/transpiled/context.js +41 -0
  100. package/transpiled/context.js.map +1 -0
  101. package/transpiled/contract/contract.d.ts +10 -0
  102. package/transpiled/contract/contract.js +14 -0
  103. package/transpiled/contract/contract.js.map +1 -0
  104. package/transpiled/contract/index.d.ts +2 -0
  105. package/transpiled/contract/index.js +3 -0
  106. package/transpiled/contract/index.js.map +1 -0
  107. package/transpiled/contract/reply.d.ts +7 -0
  108. package/transpiled/contract/reply.js +48 -0
  109. package/transpiled/contract/reply.js.map +1 -0
  110. package/transpiled/contract/request.d.ts +24 -0
  111. package/transpiled/contract/request.js +59 -0
  112. package/transpiled/contract/request.js.map +1 -0
  113. package/transpiled/contract/schemas.d.ts +4 -0
  114. package/transpiled/contract/schemas.js +36 -0
  115. package/transpiled/contract/schemas.js.map +1 -0
  116. package/transpiled/discovery.d.ts +12 -0
  117. package/transpiled/discovery.js +34 -0
  118. package/transpiled/discovery.js.map +1 -0
  119. package/transpiled/effect.d.ts +5 -0
  120. package/transpiled/effect.js +11 -0
  121. package/transpiled/effect.js.map +1 -0
  122. package/transpiled/emission.d.ts +8 -0
  123. package/transpiled/emission.js +14 -0
  124. package/transpiled/emission.js.map +1 -0
  125. package/transpiled/entities/changeset.d.ts +10 -0
  126. package/transpiled/entities/changeset.js +27 -0
  127. package/transpiled/entities/changeset.js.map +1 -0
  128. package/transpiled/entities/entity.d.ts +19 -0
  129. package/transpiled/entities/entity.js +109 -0
  130. package/transpiled/entities/entity.js.map +1 -0
  131. package/transpiled/entities/factory.d.ts +22 -0
  132. package/transpiled/entities/factory.js +34 -0
  133. package/transpiled/entities/factory.js.map +1 -0
  134. package/transpiled/entities/index.d.ts +4 -0
  135. package/transpiled/entities/index.js +5 -0
  136. package/transpiled/entities/index.js.map +1 -0
  137. package/transpiled/entities/newid.d.ts +1 -0
  138. package/transpiled/entities/newid.js +7 -0
  139. package/transpiled/entities/newid.js.map +1 -0
  140. package/transpiled/entities/set.d.ts +10 -0
  141. package/transpiled/entities/set.js +19 -0
  142. package/transpiled/entities/set.js.map +1 -0
  143. package/transpiled/event.d.ts +14 -0
  144. package/transpiled/event.js +38 -0
  145. package/transpiled/event.js.map +1 -0
  146. package/transpiled/exceptions.d.ts +61 -0
  147. package/transpiled/exceptions.js +93 -0
  148. package/transpiled/exceptions.js.map +1 -0
  149. package/transpiled/exposition.d.ts +18 -0
  150. package/transpiled/exposition.js +18 -0
  151. package/transpiled/exposition.js.map +1 -0
  152. package/transpiled/guard.d.ts +7 -0
  153. package/transpiled/guard.js +12 -0
  154. package/transpiled/guard.js.map +1 -0
  155. package/transpiled/index.d.ts +35 -0
  156. package/transpiled/index.js +29 -0
  157. package/transpiled/index.js.map +1 -0
  158. package/transpiled/locator.d.ts +12 -0
  159. package/transpiled/locator.js +32 -0
  160. package/transpiled/locator.js.map +1 -0
  161. package/transpiled/observation.d.ts +5 -0
  162. package/transpiled/observation.js +12 -0
  163. package/transpiled/observation.js.map +1 -0
  164. package/transpiled/operation.d.ts +49 -0
  165. package/transpiled/operation.js +77 -0
  166. package/transpiled/operation.js.map +1 -0
  167. package/transpiled/outbox/index.d.ts +1 -0
  168. package/transpiled/outbox/index.js +2 -0
  169. package/transpiled/outbox/index.js.map +1 -0
  170. package/transpiled/outbox/outbox.d.ts +46 -0
  171. package/transpiled/outbox/outbox.js +267 -0
  172. package/transpiled/outbox/outbox.js.map +1 -0
  173. package/transpiled/query/criteria.d.ts +6 -0
  174. package/transpiled/query/criteria.js +74 -0
  175. package/transpiled/query/criteria.js.map +1 -0
  176. package/transpiled/query/options.d.ts +3 -0
  177. package/transpiled/query/options.js +27 -0
  178. package/transpiled/query/options.js.map +1 -0
  179. package/transpiled/query.d.ts +9 -0
  180. package/transpiled/query.js +57 -0
  181. package/transpiled/query.js.map +1 -0
  182. package/transpiled/receiver.d.ts +20 -0
  183. package/transpiled/receiver.js +92 -0
  184. package/transpiled/receiver.js.map +1 -0
  185. package/transpiled/reflection.d.ts +9 -0
  186. package/transpiled/reflection.js +13 -0
  187. package/transpiled/reflection.js.map +1 -0
  188. package/transpiled/remote.d.ts +10 -0
  189. package/transpiled/remote.js +12 -0
  190. package/transpiled/remote.js.map +1 -0
  191. package/transpiled/state.d.ts +33 -0
  192. package/transpiled/state.js +126 -0
  193. package/transpiled/state.js.map +1 -0
  194. package/transpiled/transition.d.ts +14 -0
  195. package/transpiled/transition.js +51 -0
  196. package/transpiled/transition.js.map +1 -0
  197. package/transpiled/transmission.d.ts +8 -0
  198. package/transpiled/transmission.js +30 -0
  199. package/transpiled/transmission.js.map +1 -0
  200. package/transpiled/types/atomicity.d.ts +56 -0
  201. package/transpiled/types/atomicity.js +2 -0
  202. package/transpiled/types/atomicity.js.map +1 -0
  203. package/transpiled/types/bindings.d.ts +42 -0
  204. package/transpiled/types/bindings.js +2 -0
  205. package/transpiled/types/bindings.js.map +1 -0
  206. package/transpiled/types/bridges.d.ts +38 -0
  207. package/transpiled/types/bridges.js +2 -0
  208. package/transpiled/types/bridges.js.map +1 -0
  209. package/transpiled/types/extensions.d.ts +62 -0
  210. package/transpiled/types/extensions.js +2 -0
  211. package/transpiled/types/extensions.js.map +1 -0
  212. package/transpiled/types/index.d.ts +13 -0
  213. package/transpiled/types/index.js +8 -0
  214. package/transpiled/types/index.js.map +1 -0
  215. package/transpiled/types/message.d.ts +5 -0
  216. package/transpiled/types/message.js +2 -0
  217. package/transpiled/types/message.js.map +1 -0
  218. package/transpiled/types/operations.d.ts +15 -0
  219. package/transpiled/types/operations.js +2 -0
  220. package/transpiled/types/operations.js.map +1 -0
  221. package/transpiled/types/outbox.d.ts +28 -0
  222. package/transpiled/types/outbox.js +2 -0
  223. package/transpiled/types/outbox.js.map +1 -0
  224. package/transpiled/types/receiver.d.ts +9 -0
  225. package/transpiled/types/receiver.js +2 -0
  226. package/transpiled/types/receiver.js.map +1 -0
  227. package/transpiled/types/request.d.ts +55 -0
  228. package/transpiled/types/request.js +2 -0
  229. package/transpiled/types/request.js.map +1 -0
  230. package/transpiled/types/state.d.ts +9 -0
  231. package/transpiled/types/state.js +2 -0
  232. package/transpiled/types/state.js.map +1 -0
  233. package/transpiled/types/storages.d.ts +106 -0
  234. package/transpiled/types/storages.js +2 -0
  235. package/transpiled/types/storages.js.map +1 -0
  236. package/transpiled/unmanaged.d.ts +5 -0
  237. package/transpiled/unmanaged.js +7 -0
  238. package/transpiled/unmanaged.js.map +1 -0
  239. package/tsconfig.json +10 -0
  240. package/tsconfig.tsbuildinfo +1 -0
  241. package/CHANGELOG.md +0 -180
  242. package/src/assignment.js +0 -28
  243. package/src/context.js +0 -57
  244. package/src/contract/contract.js +0 -18
  245. package/src/contract/request.js +0 -77
  246. package/src/contract/schemas/error.yaml +0 -9
  247. package/src/contract/schemas/index.js +0 -11
  248. package/src/contract/schemas/query.yaml +0 -43
  249. package/src/contract/schemas/source.yaml +0 -23
  250. package/src/effect.js +0 -15
  251. package/src/entities/changeset.js +0 -36
  252. package/src/entities/entity.js +0 -119
  253. package/src/entities/factory.js +0 -40
  254. package/src/entities/index.js +0 -1
  255. package/src/event.js +0 -54
  256. package/src/exposition.js +0 -24
  257. package/src/guard.js +0 -13
  258. package/src/observation.js +0 -8
  259. package/src/operation.js +0 -95
  260. package/src/query/criteria.js +0 -37
  261. package/src/query/options.js +0 -37
  262. package/src/receiver.js +0 -123
  263. package/src/reflection.js +0 -24
  264. package/src/remote.js +0 -14
  265. package/src/state.js +0 -138
  266. package/src/transition.js +0 -61
  267. package/src/unmanaged.js +0 -7
  268. package/types/atomicity.d.ts +0 -58
  269. package/types/bindings.d.ts +0 -42
  270. package/types/bridges.ts +0 -33
  271. package/types/call.d.ts +0 -13
  272. package/types/component.d.ts +0 -12
  273. package/types/connector.d.ts +0 -18
  274. package/types/context.d.ts +0 -24
  275. package/types/entity.d.ts +0 -46
  276. package/types/event.d.ts +0 -11
  277. package/types/exception.d.ts +0 -4
  278. package/types/extensions.d.ts +0 -73
  279. package/types/guard.d.ts +0 -5
  280. package/types/locator.d.ts +0 -13
  281. package/types/operations.d.ts +0 -8
  282. package/types/outbox.d.ts +0 -53
  283. package/types/query.d.ts +0 -17
  284. package/types/receiver.d.ts +0 -12
  285. package/types/reflection.d.ts +0 -14
  286. package/types/remote.d.ts +0 -18
  287. package/types/state.d.ts +0 -38
  288. package/types/storages.d.ts +0 -96
  289. /package/{src/contract/index.js → source/contract/index.ts} +0 -0
  290. /package/{src/outbox/index.js → source/outbox/index.ts} +0 -0
@@ -0,0 +1,35 @@
1
+ import { QuerySyntaxException } from '../exceptions.js'
2
+ import type { Options } from '../types/storages.js'
3
+ import type { Properties } from './criteria.js'
4
+
5
+ export function options(given: Record<string, any>, properties: Properties): Options {
6
+ if (given.sort !== undefined) given.sort = sort(given.sort, properties)
7
+
8
+ if (given.projection !== undefined) projection(given.projection, properties)
9
+
10
+ return given as Options
11
+ }
12
+
13
+ function sort(sort: string[], properties: Properties): Array<[string, string]> {
14
+ const result: Array<[string, string]> = []
15
+
16
+ for (const sorting of sort) {
17
+ const [property, direction] = sorting.split(':')
18
+
19
+ if (properties[property] === undefined)
20
+ throw new QuerySyntaxException(`Sort property '${property}' is not defined`)
21
+
22
+ result.push([property, direction ?? 'asc'])
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ function projection(projection: string[], properties: Properties): void {
29
+ for (const property of projection)
30
+ if (properties[property] === undefined)
31
+ throw new QuerySyntaxException(`Projection property '${property}' is not defined`)
32
+
33
+ for (const property of ['VERSION', 'CREATED', 'UPDATED', 'DELETED'])
34
+ if (!projection.includes(property)) projection.push(property)
35
+ }
@@ -1,28 +1,25 @@
1
1
  import { empty } from '@toa.io/generic'
2
2
  import * as criteria from './query/criteria.js'
3
3
  import * as options from './query/options.js'
4
+ import type { Properties } from './query/criteria.js'
5
+ import type { Node, Options, Query as Parsed } from './types/storages.js'
6
+ import type { Query as Requested } from './types/request.js'
4
7
 
5
8
  const parse = { ...criteria, ...options }
6
9
 
10
+ /** Translates the query a request carries into the one a storage is given. */
7
11
  export class Query {
8
- #properties
9
- #system
12
+ readonly #properties: Properties
10
13
 
11
- /** @type {Map<string, object>} parsed criteria by their expression */
12
- #asts = new Map()
14
+ /** parsed criteria by their expression */
15
+ readonly #asts = new Map<string, Node>()
13
16
 
14
- constructor (properties) {
17
+ public constructor(properties: Properties) {
15
18
  this.#properties = properties
16
- this.#system = Object.keys(properties).filter((key) => properties[key].system === true)
17
19
  }
18
20
 
19
- /**
20
- * @param {toa.core.request.Query} query
21
- * @returns {toa.core.storages.Query}
22
- */
23
- parse (query) {
24
- /** @type {toa.core.storages.Query} */
25
- const result = {}
21
+ public parse(query: Requested): Parsed {
22
+ const result: Parsed = {}
26
23
  const { id, ids, version, criteria, search, ...rest } = query
27
24
 
28
25
  const options = this.#options(rest)
@@ -37,10 +34,10 @@ export class Query {
37
34
  return result
38
35
  }
39
36
 
40
- #options (options) {
41
- if (empty(options)) return
37
+ #options(given: Record<string, any>): Options | undefined {
38
+ if (empty(given)) return undefined
42
39
 
43
- return parse.options(options, this.#properties, this.#system)
40
+ return parse.options(given, this.#properties)
44
41
  }
45
42
 
46
43
  /**
@@ -52,7 +49,7 @@ export class Query {
52
49
  * Expressions come from the client, hence the bound. An invalid one throws before it
53
50
  * reaches the cache.
54
51
  */
55
- #criteria (criteria) {
52
+ #criteria(criteria: string): Node {
56
53
  const known = this.#asts.get(criteria)
57
54
 
58
55
  if (known !== undefined) return known
@@ -0,0 +1,120 @@
1
+ import { console, decode, run, type SpanOptions } from 'openspan'
2
+ import { add } from '@toa.io/generic'
3
+ import { Connector } from './connector.js'
4
+ import type { Component } from './component.js'
5
+ import type { Receiver as Bridge } from './types/bridges.js'
6
+ import type { Message } from './types/message.js'
7
+ import type { Request, Source } from './types/request.js'
8
+
9
+ export interface Definition {
10
+ conditioned?: boolean
11
+ adaptive?: boolean
12
+ operation: string
13
+ label?: string
14
+ destination?: string
15
+ arguments?: unknown[]
16
+ origin?: Source
17
+ }
18
+
19
+ export class Receiver extends Connector {
20
+ readonly #conditioned: boolean | undefined
21
+ readonly #adaptive: boolean | undefined
22
+ readonly #endpoint: string
23
+ readonly #label: string
24
+ readonly #destination: string
25
+ readonly #arguments: unknown[] | undefined
26
+ readonly #origin: Source | undefined
27
+ readonly #local: Component
28
+ readonly #bridge: Bridge | undefined
29
+ readonly #delivery: SpanOptions
30
+ readonly #processing: SpanOptions
31
+
32
+ public constructor(definition: Definition, local: Component, bridge?: Bridge) {
33
+ super()
34
+
35
+ const { conditioned, adaptive, operation } = definition
36
+
37
+ this.#conditioned = conditioned
38
+ this.#adaptive = adaptive
39
+ this.#endpoint = operation
40
+ this.#label = definition.label ?? operation
41
+ this.#destination = definition.destination ?? this.#label
42
+ this.#arguments = definition.arguments
43
+ this.#origin = definition.origin
44
+
45
+ this.#local = local
46
+ this.#bridge = bridge
47
+
48
+ this.depends(local)
49
+ if (bridge !== undefined) this.depends(bridge)
50
+
51
+ /*
52
+ * The delivery span is created on behalf of the messaging destination, so that
53
+ * each consumer forms its own complete producer/consumer pair, and service graphs
54
+ * display fan-out correctly: producer -> destination -> each consumer
55
+ * (Tempo pairs spans one-to-one, thus multiple consumers can not pair
56
+ * with a single producer span, see grafana/tempo#5408)
57
+ */
58
+ this.#delivery = {
59
+ name: `${this.#label} deliver`,
60
+ kind: 'producer',
61
+ service: this.#destination,
62
+ attributes: { 'messaging.destination.name': this.#destination }
63
+ }
64
+
65
+ this.#processing = {
66
+ name: `${this.#label} process`,
67
+ kind: 'consumer',
68
+ service: local.locator.id,
69
+ attributes: { 'messaging.destination.name': this.#destination }
70
+ }
71
+ }
72
+
73
+ /** @hot */
74
+ public async receive(message: Message): Promise<void> {
75
+ const { payload, telemetry, ...extensions } = message
76
+
77
+ if (this.#conditioned === true && (await this.#bridge?.condition(payload)) === false)
78
+ return
79
+
80
+ const request = await this.#request(payload)
81
+
82
+ add(request, extensions)
83
+
84
+ // set after `add`, so that a message field can not spoof the origin
85
+ if (this.#origin !== undefined) request.source = this.#origin
86
+
87
+ // continue the trace from the producer span
88
+ const remote = telemetry === undefined ? null : decode(telemetry)
89
+ const task = async (): Promise<void> => {
90
+ await this.#process(request)
91
+ }
92
+
93
+ if (remote === null) await task()
94
+ else await run(remote, task)
95
+ }
96
+
97
+ async #process(request: Request): Promise<void> {
98
+ return console.span(this.#delivery, async () =>
99
+ console.span(this.#processing, async () => {
100
+ try {
101
+ await this.#local.invoke(this.#endpoint, request)
102
+ } catch (error) {
103
+ console.error('Receiver error', {
104
+ component: this.#local.locator.id,
105
+ endpoint: this.#endpoint,
106
+ error
107
+ })
108
+
109
+ throw error
110
+ }
111
+ })
112
+ )
113
+ }
114
+
115
+ async #request(payload: object): Promise<Request> {
116
+ return this.#adaptive === true
117
+ ? await this.#bridge!.request(payload, ...(this.#arguments ?? []))
118
+ : { input: payload }
119
+ }
120
+ }
@@ -0,0 +1,20 @@
1
+ import { Connector } from './connector.js'
2
+
3
+ /** What a reflection reads, once, as it connects. */
4
+ export type Source<T = any> = () => Promise<T>
5
+
6
+ export class Reflection<T = any> extends Connector {
7
+ public value: T | undefined
8
+
9
+ readonly #source: Source<T>
10
+
11
+ public constructor(source: Source<T>) {
12
+ super()
13
+
14
+ this.#source = source
15
+ }
16
+
17
+ protected override async open(): Promise<void> {
18
+ this.value = await this.#source()
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert'
2
+ import { Component } from './component.js'
3
+ import type { Invocable } from './component.js'
4
+
5
+ /** A call knows what its endpoint declares, which is what an explanation is read from. */
6
+ export interface Explicable extends Invocable {
7
+ explain(): any
8
+ }
9
+
10
+ export class Remote extends Component<Explicable> {
11
+ protected override kind = 'client' as const
12
+
13
+ public explain(endpoint: string): any {
14
+ if (!(endpoint in this.operations))
15
+ // `assert.fail`, not `assert.ok`: the message is built only when it is needed
16
+ assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
17
+
18
+ return this.operations[endpoint].explain()
19
+ }
20
+ }
@@ -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
+ }
@@ -1,18 +1,20 @@
1
1
  import { Connector } from './connector.js'
2
2
  import { TransmissionException } from './exceptions.js'
3
+ import type { Consumer } from './types/bindings.js'
4
+ import type { Request } from './types/request.js'
3
5
 
4
6
  export class Transmission extends Connector {
5
- #bindings
7
+ readonly #bindings: Consumer[]
6
8
 
7
- constructor (bindings) {
9
+ public constructor(bindings: Consumer[]) {
8
10
  super()
9
11
 
10
12
  this.#bindings = bindings
11
13
  this.depends(bindings)
12
14
  }
13
15
 
14
- async request (request) {
15
- let reply = false
16
+ public async request(request: Request): Promise<any> {
17
+ let reply: any = false
16
18
  let i = 0
17
19
 
18
20
  while (reply === false && i < this.#bindings.length) {
@@ -21,13 +23,11 @@ export class Transmission extends Connector {
21
23
  i++
22
24
 
23
25
  if (request?.task === true) {
24
- if (binding.task === undefined)
25
- continue
26
+ if (binding.task === undefined) continue
26
27
 
27
28
  await binding.task(request)
28
29
  reply = null
29
- } else
30
- reply = await binding.request(request)
30
+ } else reply = await binding.request(request)
31
31
  }
32
32
 
33
33
  if (reply === false)
@@ -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
+ }