@toa.io/core 1.0.0-alpha.284 → 1.0.0-alpha.287

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/CHANGELOG.md +1 -185
  2. package/package.json +19 -7
  3. package/source/assignment.ts +27 -0
  4. package/{src/call.js → source/call.ts} +16 -21
  5. package/{src/cascade.js → source/cascade.ts} +6 -8
  6. package/{src/component.js → source/component.ts} +27 -25
  7. package/{src/composition.js → source/composition.ts} +9 -3
  8. package/{src/connector.js → source/connector.ts} +36 -55
  9. package/source/context.ts +67 -0
  10. package/source/contract/contract.ts +23 -0
  11. package/{src/contract/reply.js → source/contract/reply.ts} +26 -19
  12. package/source/contract/request.ts +85 -0
  13. package/source/contract/schemas.ts +39 -0
  14. package/{src/discovery.js → source/discovery.ts} +15 -8
  15. package/source/effect.ts +14 -0
  16. package/{src/emission.js → source/emission.ts} +5 -3
  17. package/source/entities/changeset.ts +37 -0
  18. package/source/entities/entity.ts +137 -0
  19. package/source/entities/factory.ts +52 -0
  20. package/source/entities/index.ts +4 -0
  21. package/{src/entities/newid.js → source/entities/newid.ts} +1 -1
  22. package/{src/entities/set.js → source/entities/set.ts} +8 -5
  23. package/source/event.ts +57 -0
  24. package/{src/exceptions.js → source/exceptions.ts} +52 -33
  25. package/source/exposition.ts +33 -0
  26. package/source/guard.ts +16 -0
  27. package/{src/index.js → source/index.ts} +8 -0
  28. package/{src/locator.js → source/locator.ts} +17 -30
  29. package/source/observation.ts +16 -0
  30. package/source/operation.ts +138 -0
  31. package/{src/outbox/outbox.js → source/outbox/outbox.ts} +87 -53
  32. package/source/query/criteria.ts +90 -0
  33. package/source/query/options.ts +35 -0
  34. package/{src/query.js → source/query.ts} +14 -17
  35. package/source/receiver.ts +120 -0
  36. package/source/reflection.ts +20 -0
  37. package/source/remote.ts +20 -0
  38. package/source/state.ts +179 -0
  39. package/source/transition.ts +79 -0
  40. package/{src/transmission.js → source/transmission.ts} +8 -8
  41. package/source/types/atomicity.ts +63 -0
  42. package/source/types/bindings.ts +59 -0
  43. package/source/types/bridges.ts +54 -0
  44. package/source/types/extensions.ts +89 -0
  45. package/{types → source/types}/index.ts +9 -16
  46. package/{types/message.d.ts → source/types/message.ts} +2 -1
  47. package/source/types/operations.ts +31 -0
  48. package/source/types/outbox.ts +35 -0
  49. package/source/types/receiver.ts +10 -0
  50. package/{types/request.d.ts → source/types/request.ts} +8 -9
  51. package/source/types/state.ts +9 -0
  52. package/source/types/storages.ts +129 -0
  53. package/source/unmanaged.ts +8 -0
  54. package/test/call.fixtures.js +3 -1
  55. package/test/call.test.js +28 -7
  56. package/test/component.test.js +10 -5
  57. package/test/connector.fixtures.js +7 -7
  58. package/test/connector.test.js +15 -2
  59. package/test/context.fixtures.js +1 -1
  60. package/test/context.test.js +4 -4
  61. package/test/contract/conditions.test.js +6 -2
  62. package/test/contract/contract.fixtures.js +3 -4
  63. package/test/contract/request.test.js +68 -34
  64. package/test/discovery.test.js +10 -3
  65. package/test/emission.test.js +16 -8
  66. package/test/entities/entity.fixtures.js +5 -6
  67. package/test/entities/entity.test.js +54 -20
  68. package/test/entities/factory.fixtures.js +11 -10
  69. package/test/entities/factory.test.js +47 -10
  70. package/test/entities/set.test.js +1 -1
  71. package/test/event.test.js +29 -11
  72. package/test/locator.test.js +10 -4
  73. package/test/outbox.test.js +49 -15
  74. package/test/query.test.js +69 -7
  75. package/test/receiver.fixtures.js +2 -2
  76. package/test/receiver.test.js +53 -27
  77. package/test/reflection.test.js +3 -3
  78. package/test/state.fixtures.js +10 -3
  79. package/test/state.test.js +77 -14
  80. package/test/transmission.test.js +25 -12
  81. package/transpiled/assignment.d.ts +6 -0
  82. package/transpiled/assignment.js +20 -0
  83. package/transpiled/assignment.js.map +1 -0
  84. package/transpiled/call.d.ts +10 -0
  85. package/transpiled/call.js +52 -0
  86. package/transpiled/call.js.map +1 -0
  87. package/transpiled/cascade.d.ts +7 -0
  88. package/transpiled/cascade.js +28 -0
  89. package/transpiled/cascade.js.map +1 -0
  90. package/transpiled/component.d.ts +15 -0
  91. package/transpiled/component.js +61 -0
  92. package/transpiled/component.js.map +1 -0
  93. package/transpiled/composition.d.ts +6 -0
  94. package/transpiled/composition.js +23 -0
  95. package/transpiled/composition.js.map +1 -0
  96. package/transpiled/connector.d.ts +47 -0
  97. package/transpiled/connector.js +143 -0
  98. package/transpiled/connector.js.map +1 -0
  99. package/transpiled/context.d.ts +18 -0
  100. package/transpiled/context.js +41 -0
  101. package/transpiled/context.js.map +1 -0
  102. package/transpiled/contract/contract.d.ts +10 -0
  103. package/transpiled/contract/contract.js +14 -0
  104. package/transpiled/contract/contract.js.map +1 -0
  105. package/transpiled/contract/index.d.ts +2 -0
  106. package/transpiled/contract/index.js +3 -0
  107. package/transpiled/contract/index.js.map +1 -0
  108. package/transpiled/contract/reply.d.ts +7 -0
  109. package/transpiled/contract/reply.js +48 -0
  110. package/transpiled/contract/reply.js.map +1 -0
  111. package/transpiled/contract/request.d.ts +24 -0
  112. package/transpiled/contract/request.js +59 -0
  113. package/transpiled/contract/request.js.map +1 -0
  114. package/transpiled/contract/schemas.d.ts +4 -0
  115. package/transpiled/contract/schemas.js +36 -0
  116. package/transpiled/contract/schemas.js.map +1 -0
  117. package/transpiled/discovery.d.ts +12 -0
  118. package/transpiled/discovery.js +34 -0
  119. package/transpiled/discovery.js.map +1 -0
  120. package/transpiled/effect.d.ts +5 -0
  121. package/transpiled/effect.js +11 -0
  122. package/transpiled/effect.js.map +1 -0
  123. package/transpiled/emission.d.ts +8 -0
  124. package/transpiled/emission.js +14 -0
  125. package/transpiled/emission.js.map +1 -0
  126. package/transpiled/entities/changeset.d.ts +10 -0
  127. package/transpiled/entities/changeset.js +27 -0
  128. package/transpiled/entities/changeset.js.map +1 -0
  129. package/transpiled/entities/entity.d.ts +19 -0
  130. package/transpiled/entities/entity.js +109 -0
  131. package/transpiled/entities/entity.js.map +1 -0
  132. package/transpiled/entities/factory.d.ts +22 -0
  133. package/transpiled/entities/factory.js +34 -0
  134. package/transpiled/entities/factory.js.map +1 -0
  135. package/transpiled/entities/index.d.ts +4 -0
  136. package/transpiled/entities/index.js +5 -0
  137. package/transpiled/entities/index.js.map +1 -0
  138. package/transpiled/entities/newid.d.ts +1 -0
  139. package/transpiled/entities/newid.js +7 -0
  140. package/transpiled/entities/newid.js.map +1 -0
  141. package/transpiled/entities/set.d.ts +10 -0
  142. package/transpiled/entities/set.js +19 -0
  143. package/transpiled/entities/set.js.map +1 -0
  144. package/transpiled/event.d.ts +14 -0
  145. package/transpiled/event.js +38 -0
  146. package/transpiled/event.js.map +1 -0
  147. package/transpiled/exceptions.d.ts +61 -0
  148. package/transpiled/exceptions.js +93 -0
  149. package/transpiled/exceptions.js.map +1 -0
  150. package/transpiled/exposition.d.ts +18 -0
  151. package/transpiled/exposition.js +18 -0
  152. package/transpiled/exposition.js.map +1 -0
  153. package/transpiled/guard.d.ts +7 -0
  154. package/transpiled/guard.js +12 -0
  155. package/transpiled/guard.js.map +1 -0
  156. package/transpiled/index.d.ts +35 -0
  157. package/transpiled/index.js +29 -0
  158. package/transpiled/index.js.map +1 -0
  159. package/transpiled/locator.d.ts +12 -0
  160. package/transpiled/locator.js +32 -0
  161. package/transpiled/locator.js.map +1 -0
  162. package/transpiled/observation.d.ts +5 -0
  163. package/transpiled/observation.js +12 -0
  164. package/transpiled/observation.js.map +1 -0
  165. package/transpiled/operation.d.ts +49 -0
  166. package/transpiled/operation.js +77 -0
  167. package/transpiled/operation.js.map +1 -0
  168. package/transpiled/outbox/index.d.ts +1 -0
  169. package/transpiled/outbox/index.js +2 -0
  170. package/transpiled/outbox/index.js.map +1 -0
  171. package/transpiled/outbox/outbox.d.ts +46 -0
  172. package/transpiled/outbox/outbox.js +267 -0
  173. package/transpiled/outbox/outbox.js.map +1 -0
  174. package/transpiled/query/criteria.d.ts +6 -0
  175. package/transpiled/query/criteria.js +74 -0
  176. package/transpiled/query/criteria.js.map +1 -0
  177. package/transpiled/query/options.d.ts +3 -0
  178. package/transpiled/query/options.js +27 -0
  179. package/transpiled/query/options.js.map +1 -0
  180. package/transpiled/query.d.ts +9 -0
  181. package/transpiled/query.js +57 -0
  182. package/transpiled/query.js.map +1 -0
  183. package/transpiled/receiver.d.ts +20 -0
  184. package/transpiled/receiver.js +92 -0
  185. package/transpiled/receiver.js.map +1 -0
  186. package/transpiled/reflection.d.ts +9 -0
  187. package/transpiled/reflection.js +13 -0
  188. package/transpiled/reflection.js.map +1 -0
  189. package/transpiled/remote.d.ts +10 -0
  190. package/transpiled/remote.js +12 -0
  191. package/transpiled/remote.js.map +1 -0
  192. package/transpiled/state.d.ts +33 -0
  193. package/transpiled/state.js +126 -0
  194. package/transpiled/state.js.map +1 -0
  195. package/transpiled/transition.d.ts +14 -0
  196. package/transpiled/transition.js +51 -0
  197. package/transpiled/transition.js.map +1 -0
  198. package/transpiled/transmission.d.ts +8 -0
  199. package/transpiled/transmission.js +30 -0
  200. package/transpiled/transmission.js.map +1 -0
  201. package/transpiled/types/atomicity.d.ts +56 -0
  202. package/transpiled/types/atomicity.js +2 -0
  203. package/transpiled/types/atomicity.js.map +1 -0
  204. package/transpiled/types/bindings.d.ts +42 -0
  205. package/transpiled/types/bindings.js +2 -0
  206. package/transpiled/types/bindings.js.map +1 -0
  207. package/transpiled/types/bridges.d.ts +38 -0
  208. package/transpiled/types/bridges.js +2 -0
  209. package/transpiled/types/bridges.js.map +1 -0
  210. package/transpiled/types/extensions.d.ts +62 -0
  211. package/transpiled/types/extensions.js +2 -0
  212. package/transpiled/types/extensions.js.map +1 -0
  213. package/transpiled/types/index.d.ts +13 -0
  214. package/transpiled/types/index.js +8 -0
  215. package/transpiled/types/index.js.map +1 -0
  216. package/transpiled/types/message.d.ts +5 -0
  217. package/transpiled/types/message.js +2 -0
  218. package/transpiled/types/message.js.map +1 -0
  219. package/transpiled/types/operations.d.ts +15 -0
  220. package/transpiled/types/operations.js +2 -0
  221. package/transpiled/types/operations.js.map +1 -0
  222. package/transpiled/types/outbox.d.ts +28 -0
  223. package/transpiled/types/outbox.js +2 -0
  224. package/transpiled/types/outbox.js.map +1 -0
  225. package/transpiled/types/receiver.d.ts +9 -0
  226. package/transpiled/types/receiver.js +2 -0
  227. package/transpiled/types/receiver.js.map +1 -0
  228. package/transpiled/types/request.d.ts +55 -0
  229. package/transpiled/types/request.js +2 -0
  230. package/transpiled/types/request.js.map +1 -0
  231. package/transpiled/types/state.d.ts +9 -0
  232. package/transpiled/types/state.js +2 -0
  233. package/transpiled/types/state.js.map +1 -0
  234. package/transpiled/types/storages.d.ts +106 -0
  235. package/transpiled/types/storages.js +2 -0
  236. package/transpiled/types/storages.js.map +1 -0
  237. package/transpiled/unmanaged.d.ts +5 -0
  238. package/transpiled/unmanaged.js +7 -0
  239. package/transpiled/unmanaged.js.map +1 -0
  240. package/tsconfig.json +10 -0
  241. package/tsconfig.tsbuildinfo +1 -0
  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 -44
  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 -79
  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 -19
  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
@@ -1,6 +1,17 @@
1
1
  import { console } from 'openspan'
2
2
  import { Connector } from '../connector.js'
3
3
  import { newid } from '../entities/newid.js'
4
+ import type { Emission } from '../emission.js'
5
+ import type { Atom } from '../types/atomicity.js'
6
+ import type { Storage } from '../types/storages.js'
7
+ import type { Row } from '../types/outbox.js'
8
+ import type { Event } from '../types/state.js'
9
+
10
+ export interface Options {
11
+ interval?: number
12
+ batch?: number
13
+ gap?: number
14
+ }
4
15
 
5
16
  /**
6
17
  * Owns the intent to publish. A row is built before the write so that the storage can commit
@@ -14,30 +25,36 @@ import { newid } from '../entities/newid.js'
14
25
  * inline emission it replaces.
15
26
  */
16
27
  export class Outbox extends Connector {
17
- #emission
18
- #storage
19
- #atom
28
+ readonly #emission: Emission
29
+ readonly #storage: Storage | undefined
30
+ readonly #atom: Atom
20
31
 
21
- #gap
22
- #interval
23
- #batch
24
- #defer
32
+ readonly #gap: number
33
+ readonly #interval: number
34
+ readonly #batch: number
35
+ readonly #defer: boolean
25
36
 
26
37
  /** ids this process has published, held until a cycle marks them */
27
- #published = new Set()
38
+ readonly #published = new Set<string>()
28
39
 
29
40
  /** in-flight publications, awaited (with a bound) on close */
30
- #inflight = new Set()
41
+ readonly #inflight = new Set<Promise<void>>()
31
42
 
32
43
  /** rows this replica is publishing right now, so a cycle does not pick them up again */
33
- #publishing = new Set()
44
+ readonly #publishing = new Set<string>()
34
45
 
35
- #timer
36
- #off
46
+ #timer: NodeJS.Timeout | undefined
47
+ #off: (() => void) | undefined
37
48
  #pumping = false
38
49
  #closing = false
39
50
 
40
- constructor (emission, storage, atom, options = {}) {
51
+ // eslint-disable-next-line max-params
52
+ public constructor(
53
+ emission: Emission,
54
+ storage: Storage | undefined,
55
+ atom: Atom,
56
+ options: Options = {}
57
+ ) {
41
58
  super()
42
59
 
43
60
  this.#emission = emission
@@ -56,21 +73,21 @@ export class Outbox extends Connector {
56
73
  }
57
74
 
58
75
  /** whether the storage can commit a row atomically with the entity */
59
- get durable () {
76
+ public get durable(): boolean {
60
77
  return this.#storage?.outbox !== undefined
61
78
  }
62
79
 
63
80
  /**
64
- * @param event {toa.core.transition.Event}
65
- * @returns {object}
81
+ * An assignment's images are the write's own, so it hands over an event with neither, and
82
+ * the storage fills them in.
66
83
  */
67
- row (event) {
84
+ public row(event: Partial<Event>): Row {
68
85
  return {
69
86
  id: newid(),
70
87
  lane: this.#lane(),
71
88
  published: false,
72
89
  pending: Date.now() + this.#gap,
73
- event
90
+ event: event as Event
74
91
  }
75
92
  }
76
93
 
@@ -78,29 +95,37 @@ export class Outbox extends Connector {
78
95
  * Hands a committed row over. Awaited by the caller only on the legacy path — with an
79
96
  * outbox this returns at once and the broker leaves the operation's path.
80
97
  */
81
- publish (row) {
82
- if (!this.durable)
83
- return this.#emission.emit(row.event)
98
+ public publish(row: Row): Promise<void> | void {
99
+ // without a durable outbox this is the inline path, and the caller awaits the emission
100
+ if (!this.durable) return this.#emission.emit(row.event)
84
101
 
85
102
  /*
86
103
  * A publication started while the pump is closing would outlive the emitters it needs,
87
104
  * and `comq` waits on a connection that is going rather than failing. The row is already
88
105
  * durable, so leaving it is exactly what it is for.
89
106
  */
90
- if (this.#closing || this.#defer ||
91
- this.#inflight.size >= INFLIGHT || this.#published.size >= PUBLISHED)
107
+ if (
108
+ this.#closing ||
109
+ this.#defer ||
110
+ this.#inflight.size >= INFLIGHT ||
111
+ this.#published.size >= PUBLISHED
112
+ )
92
113
  return
93
114
 
94
115
  void this.#publish(row)
95
116
  }
96
117
 
97
- async open () {
118
+ protected override async open(): Promise<void> {
98
119
  if (!this.durable) return
99
120
 
100
121
  if (this.#defer)
101
- console.warn('Outbox immediate publication is deferred; events are published by the pump only')
122
+ console.warn(
123
+ 'Outbox immediate publication is deferred; events are published by the pump only'
124
+ )
102
125
 
103
- this.#timer = setInterval(() => this.#tick(), this.#interval)
126
+ this.#timer = setInterval(() => {
127
+ this.#tick()
128
+ }, this.#interval)
104
129
  this.#timer.unref()
105
130
 
106
131
  /*
@@ -108,10 +133,12 @@ export class Outbox extends Connector {
108
133
  * publish, and the cycle would not notice for up to an interval. Being told costs a cycle
109
134
  * that finds nothing in the usual case, where the claim arrives once and never changes.
110
135
  */
111
- this.#off = this.#atom.onassigned(() => this.#tick())
136
+ this.#off = this.#atom.onassigned(() => {
137
+ this.#tick()
138
+ })
112
139
  }
113
140
 
114
- async close () {
141
+ protected override async close(): Promise<void> {
115
142
  this.#closing = true
116
143
 
117
144
  this.#off?.()
@@ -131,9 +158,8 @@ export class Outbox extends Connector {
131
158
  * it would not stop it, it would only mean the row is published twice once it lands. What
132
159
  * bounds this instead is the in-flight cap and the drain on close.
133
160
  *
134
- * @private
135
161
  */
136
- async #publish (row) {
162
+ async #publish(row: Row): Promise<void> {
137
163
  this.#publishing.add(row.id)
138
164
 
139
165
  const publishing = this.#emission.emit(row.event)
@@ -156,9 +182,8 @@ export class Outbox extends Connector {
156
182
  * `comq` retries a publish for as long as the broker is down rather than rejecting, so an
157
183
  * unbounded drain outlives any grace period.
158
184
  *
159
- * @private
160
185
  */
161
- async #drain () {
186
+ async #drain(): Promise<void> {
162
187
  if (this.#inflight.size === 0) return
163
188
 
164
189
  await Promise.race([Promise.allSettled([...this.#inflight]), delay(DRAIN)])
@@ -168,9 +193,8 @@ export class Outbox extends Connector {
168
193
  * Reads what is due, publishes it, and marks everything this process has sent — what it just
169
194
  * published and what the immediate path published since the last cycle. One cycle at a time.
170
195
  *
171
- * @private
172
196
  */
173
- #tick () {
197
+ #tick(): void {
174
198
  if (this.#pumping) return
175
199
 
176
200
  this.#pumping = true
@@ -178,29 +202,33 @@ export class Outbox extends Connector {
178
202
  void this.#pump().finally(() => (this.#pumping = false))
179
203
  }
180
204
 
181
- /** @private */
182
- async #pump () {
183
- let page
205
+ async #pump(): Promise<void> {
206
+ let page: Row[]
207
+ let after: string | undefined
184
208
 
185
209
  do {
186
- page = await this.#read(page?.[page.length - 1]?.id)
210
+ page = await this.#read(after)
187
211
 
188
212
  if (page.length === 0) break
189
213
 
214
+ // so a page is never read twice
215
+ after = page[page.length - 1]?.id
216
+
190
217
  /*
191
218
  * A row is unpublished in the database until a cycle marks it, so a page includes what
192
219
  * this replica is sending right now and what a failed marking left behind. Only this
193
220
  * process knows either.
194
221
  */
195
- const rows = page.filter((row) =>
196
- !this.#published.has(row.id) && !this.#publishing.has(row.id))
222
+ const rows = page.filter(
223
+ (row) => !this.#published.has(row.id) && !this.#publishing.has(row.id)
224
+ )
197
225
 
198
226
  if (rows.length > 0) {
199
227
  console.info('Outbox recovering unpublished events', { count: rows.length })
200
228
 
201
229
  // every row is given its chance; what the broker refused stays unpublished and comes
202
230
  // back on a later cycle
203
- await Promise.allSettled(rows.map((row) => this.#publish(row)))
231
+ await Promise.allSettled(rows.map(async (row) => this.#publish(row)))
204
232
  }
205
233
 
206
234
  // a full page is a page that may have been cut short
@@ -218,15 +246,15 @@ export class Outbox extends Connector {
218
246
  * assignment arrives. Reading without an assignment would be a different guarantee, where
219
247
  * every replica publishes every stranded row.
220
248
  *
221
- * @private
222
- * @param {string} [after] the last id of the page before, so a page is never read twice
249
+ * @param after the last id of the page before, so a page is never read twice
223
250
  */
224
- async #read (after) {
251
+ async #read(after?: string): Promise<Row[]> {
225
252
  const lanes = this.#atom.slots(LANES)
226
253
 
227
254
  if (lanes === null || lanes.length === 0) return []
228
255
 
229
- return this.#storage.outbox.pending(lanes, Date.now(), this.#batch, after)
256
+ return this.#storage!
257
+ .outbox!.pending(lanes, Date.now(), this.#batch, after)
230
258
  .catch((error) => {
231
259
  console.warn('Outbox read failed', { error })
232
260
 
@@ -239,15 +267,14 @@ export class Outbox extends Connector {
239
267
  * marked one by one. Ids that fail to be marked are kept and retried; a row that is never
240
268
  * marked is simply published again, which is within the contract.
241
269
  *
242
- * @private
243
270
  */
244
- async #mark () {
271
+ async #mark(): Promise<void> {
245
272
  if (this.#published.size === 0) return
246
273
 
247
274
  const ids = [...this.#published]
248
275
 
249
276
  try {
250
- await this.#storage.outbox.settle(ids)
277
+ await this.#storage!.outbox!.settle(ids)
251
278
 
252
279
  for (const id of ids) this.#published.delete(id)
253
280
  } catch (error) {
@@ -255,14 +282,13 @@ export class Outbox extends Connector {
255
282
  }
256
283
  }
257
284
 
258
- /**
285
+ /**
259
286
  * A lane this replica currently owns, so that in steady state it settles its own rows
260
287
  * before it ever reads them. Any lane at all when it owns none: the row still has to be
261
288
  * written, and whoever ends up owning that lane will pump it.
262
289
  *
263
- * @private
264
290
  */
265
- #lane () {
291
+ #lane(): number {
266
292
  const owned = this.#atom.slots(LANES)
267
293
 
268
294
  return owned === null || owned.length === 0
@@ -271,7 +297,11 @@ export class Outbox extends Connector {
271
297
  }
272
298
  }
273
299
 
274
- function number (variable, declared, fallback) {
300
+ function number(
301
+ variable: string,
302
+ declared: number | undefined,
303
+ fallback: number
304
+ ): number {
275
305
  if (declared !== undefined) return declared
276
306
 
277
307
  const value = Number(process.env[variable])
@@ -279,7 +309,11 @@ function number (variable, declared, fallback) {
279
309
  return Number.isNaN(value) || value <= 0 ? fallback : value
280
310
  }
281
311
 
282
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms).unref())
312
+ async function delay(ms: number): Promise<void> {
313
+ return new Promise((resolve) => {
314
+ setTimeout(resolve, ms).unref()
315
+ })
316
+ }
283
317
 
284
318
  /**
285
319
  * Constant, never configuration: rows carry their lane, so lowering this would leave rows in
@@ -0,0 +1,90 @@
1
+ import { parse } from '@rsql/parser'
2
+ import { QuerySyntaxException } from '../exceptions.js'
3
+ import type { Node } from '../types/storages.js'
4
+
5
+ /** What a component declares about the properties a criteria may select on. */
6
+ export type Properties = Record<string, { type: string }>
7
+
8
+ export function criteria(expression: string, properties?: Properties): Node {
9
+ let ast: Node
10
+
11
+ try {
12
+ ast = parse(expression) as unknown as Node
13
+ } catch (e) {
14
+ throw new QuerySyntaxException((e as Error).message)
15
+ }
16
+
17
+ if (properties !== undefined) read(ast, properties)
18
+
19
+ return ast
20
+ }
21
+
22
+ function read(node: Node, properties: Properties): void {
23
+ if (
24
+ node.type === 'COMPARISON' &&
25
+ node.left?.type === 'SELECTOR' &&
26
+ node.right?.type === 'VALUE'
27
+ ) {
28
+ const selector = node.left.selector as string
29
+ const property = properties[selector]
30
+
31
+ if (property === undefined) {
32
+ throw new QuerySyntaxException(`Criteria selector '${selector}' is not defined`)
33
+ }
34
+
35
+ const cast = CAST[property.type]
36
+
37
+ // `=in=` and `=out=` carry a list, and casting that as one value gives whatever
38
+ // a comma-separated string reads as
39
+ if (cast !== undefined)
40
+ node.right.value = Array.isArray(node.right.value)
41
+ ? node.right.value.map((value: string) => cast(value, selector))
42
+ : cast(node.right.value as string, selector)
43
+ } else {
44
+ if (node.left !== undefined) read(node.left, properties)
45
+ if (node.right !== undefined) read(node.right, properties)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * A criteria arrives as text, so a value is read as what the property it selects on holds. What
51
+ * cannot be read as that is refused rather than passed on: `parseInt` answers `NaN` for a word
52
+ * and `12` for `12kg`, and a storage asked to match either finds nothing and says nothing —
53
+ * a mistyped filter reads as an empty result.
54
+ *
55
+ * A string is left as it came, because every text is one: what a criteria may compare a string
56
+ * to is not the type's to say.
57
+ */
58
+ const CAST: Record<string, (value: string, selector: string) => unknown> = {
59
+ number: (value, selector) => finite(value, selector, 'a number'),
60
+
61
+ integer: (value, selector) => {
62
+ const number = finite(value, selector, 'an integer')
63
+
64
+ if (!Number.isInteger(number)) refuse(selector, 'an integer', value)
65
+
66
+ return number
67
+ },
68
+
69
+ boolean: (value, selector) => {
70
+ if (value === 'true') return true
71
+ if (value === 'false') return false
72
+
73
+ return refuse(selector, 'a boolean', value)
74
+ }
75
+ }
76
+
77
+ function finite(value: string, selector: string, expected: string): number {
78
+ // `Number` reads a blank string as zero, and a criteria that says nothing says nothing
79
+ const number = value.trim() === '' ? Number.NaN : Number(value)
80
+
81
+ if (!Number.isFinite(number)) refuse(selector, expected, value)
82
+
83
+ return number
84
+ }
85
+
86
+ function refuse(selector: string, expected: string, value: string): never {
87
+ throw new QuerySyntaxException(
88
+ `Criteria selector '${selector}' takes ${expected}, and '${value}' is not one`
89
+ )
90
+ }
@@ -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
+ }