@toa.io/core 1.0.0-alpha.284 → 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} +87 -53
  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 +49 -15
  73. package/test/query.test.js +69 -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 -192
  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
@@ -0,0 +1,33 @@
1
+ import { Connector } from './connector.js'
2
+ import type { Locator } from './locator.js'
3
+
4
+ interface Manifest {
5
+ namespace: string
6
+ name: string
7
+ entity?: object
8
+ operations?: object
9
+ events?: object
10
+ }
11
+
12
+ export class Exposition extends Connector {
13
+ public readonly locator: Locator
14
+
15
+ readonly #exposition: Partial<Manifest>
16
+
17
+ public constructor(locator: Locator, manifest: Manifest) {
18
+ super()
19
+
20
+ this.locator = locator
21
+ this.#exposition = expose(manifest)
22
+ }
23
+
24
+ public async invoke(): Promise<{ output: Partial<Manifest> }> {
25
+ return { output: this.#exposition }
26
+ }
27
+ }
28
+
29
+ function expose(manifest: Manifest): Partial<Manifest> {
30
+ const { namespace, name, entity, operations, events } = manifest
31
+
32
+ return { namespace, name, entity, operations, events }
33
+ }
@@ -0,0 +1,16 @@
1
+ import type { Guard as Bridge } from './types/bridges.js'
2
+
3
+ export class Guard {
4
+ public readonly name: string
5
+
6
+ readonly #bridge: Bridge
7
+
8
+ public constructor(name: string, bridge: Bridge) {
9
+ this.name = name
10
+ this.#bridge = bridge
11
+ }
12
+
13
+ public fit(state: object, origin: object | null): boolean {
14
+ return this.#bridge.fit(state, origin)
15
+ }
16
+ }
@@ -26,3 +26,11 @@ export { Guard } from './guard.js'
26
26
  export * as entities from './entities/index.js'
27
27
  export * as exceptions from './exceptions.js'
28
28
  export * as contract from './contract/index.js'
29
+
30
+ export type { Exception } from './exceptions.js'
31
+ export type { Invocable } from './component.js'
32
+ export type { Explicable } from './remote.js'
33
+ export type { Scoped, Store } from './operation.js'
34
+ export type { Source as ReflectionSource } from './reflection.js'
35
+ export type { Explanation } from './contract/request.js'
36
+ export type { JSONSchema } from './contract/schemas.js'
@@ -1,22 +1,16 @@
1
1
  import { concat } from '@toa.io/generic'
2
2
 
3
- /**
4
- * @implements {toa.core.Locator}
5
- */
3
+ /** Where a component is, as every name derived from it. */
6
4
  export class Locator {
7
- name
8
- namespace
9
-
10
- id
11
- label
12
- uppercase
13
- lowercase
14
-
15
- /**
16
- * @param {string} name
17
- * @param {string} [namespace]
18
- */
19
- constructor (name, namespace) {
5
+ public readonly name: string
6
+ public readonly namespace: string | undefined
7
+
8
+ public readonly id: string
9
+ public readonly label: string
10
+ public readonly uppercase: string
11
+ public readonly lowercase: string
12
+
13
+ public constructor(name: string, namespace?: string) {
20
14
  if (name === undefined) throw new TypeError('Locator name must be defined')
21
15
 
22
16
  this.name = name
@@ -28,22 +22,15 @@ export class Locator {
28
22
  this.lowercase = (concat(namespace, '_') + name).toLowerCase()
29
23
  }
30
24
 
31
- hostname (prefix) {
32
- return concat(prefix?.toLowerCase(), '-') + this.label
33
- }
34
-
35
- /**
36
- * @param {string} string
37
- * @returns {Locator}
38
- */
39
- static parse (string) {
25
+ public static parse(string: string): Locator {
40
26
  const [namespace, name] = string.split(DOT)
41
27
 
42
- if (name === undefined) {
43
- return new Locator(namespace)
44
- } else {
45
- return new Locator(name, namespace)
46
- }
28
+ if (name === undefined) return new Locator(namespace)
29
+ else return new Locator(name, namespace)
30
+ }
31
+
32
+ public hostname(prefix?: string): string {
33
+ return concat(prefix?.toLowerCase(), '-') + this.label
47
34
  }
48
35
  }
49
36
 
@@ -0,0 +1,16 @@
1
+ import { Operation } from './operation.js'
2
+ import type { Store } from './operation.js'
3
+ import type { Entity } from './entities/entity.js'
4
+
5
+ export class Observation extends Operation {
6
+ protected override async run(store: Store): Promise<void> {
7
+ const scope = store.scope as Entity | null
8
+
9
+ if (
10
+ scope === null ||
11
+ (scope?.deleted === true && (store.request.query as any)?.options?.deleted !== true)
12
+ )
13
+ store.reply = null
14
+ else await super.run(store)
15
+ }
16
+ }
@@ -0,0 +1,138 @@
1
+ import { Readable } from 'node:stream'
2
+ import { Connector } from './connector.js'
3
+ import { SystemException, RequestContractException } from './exceptions.js'
4
+ import type { Cascade } from './cascade.js'
5
+ import type { State } from './state.js'
6
+ import type { Query as Translator } from './query.js'
7
+ import type { Contract } from './contract/contract.js'
8
+ import type { Entity } from './entities/entity.js'
9
+ import type { EntitySet } from './entities/set.js'
10
+ import type { Changeset } from './entities/changeset.js'
11
+ import type { scope as Scope } from './types/operations.js'
12
+ import type { Query, Request } from './types/request.js'
13
+
14
+ /** What an operation acquires for the algorithm to run against. */
15
+ export type Scoped = Entity | EntitySet | Changeset | Readable | null
16
+
17
+ /** What one invocation carries from step to step. */
18
+ export interface Store {
19
+ request: Request
20
+ scope?: Scoped
21
+ state?: any
22
+ reply?: any
23
+ /** set by a transition, which is the only operation that runs its steps again */
24
+ retry?: () => Promise<any>
25
+ }
26
+
27
+ export interface Contracts {
28
+ request: Contract
29
+ reply: Contract
30
+ }
31
+
32
+ export interface Definition {
33
+ scope: Scope
34
+ concurrency?: string
35
+ }
36
+
37
+ export class Operation extends Connector {
38
+ public scope: State
39
+
40
+ /**
41
+ * Whether what this operation acquires may be modified and committed. Only a
42
+ * transition commits, and only a commit needs the pre-image an entity keeps
43
+ * to diff the new state against.
44
+ *
45
+ * @protected
46
+ */
47
+ protected mutable: boolean = false
48
+
49
+ readonly #cascade: Cascade
50
+ readonly #contracts: Contracts
51
+ readonly #query: Translator
52
+ readonly #scope: Scope
53
+
54
+ // eslint-disable-next-line max-params
55
+ public constructor(
56
+ cascade: Cascade,
57
+ scope: State,
58
+ contracts: Contracts,
59
+ query: Translator,
60
+ definition: Definition
61
+ ) {
62
+ super()
63
+
64
+ this.scope = scope
65
+
66
+ this.#cascade = cascade
67
+ this.#contracts = contracts
68
+ this.#query = query
69
+ this.#scope = definition.scope
70
+
71
+ this.depends(cascade)
72
+ }
73
+
74
+ public async invoke(request: Request): Promise<any> {
75
+ try {
76
+ if (request.authentic !== true) this.#contracts.request.fit(request)
77
+
78
+ // the request carries the query onward in its parsed form: what a storage is given,
79
+ // not what the caller sent
80
+ if ('query' in request)
81
+ request.query = this.#query.parse(request.query as Query) as any
82
+
83
+ // validate entity
84
+ if ('entity' in request) this.scope.fit(request.entity)
85
+
86
+ const store = { request }
87
+
88
+ return await this.process(store)
89
+ } catch (e) {
90
+ const exception = e instanceof Error ? new SystemException(e) : e
91
+
92
+ return { exception }
93
+ }
94
+ }
95
+
96
+ protected async process(store: Store): Promise<any> {
97
+ await this.acquire(store)
98
+ await this.run(store)
99
+ await this.commit(store)
100
+
101
+ return store.reply
102
+ }
103
+
104
+ protected async acquire(store: Store): Promise<void> {
105
+ if (this.#scope === 'none') return
106
+
107
+ const scope = await this.query(store.request.query)
108
+ const raw = scope === null || scope instanceof Readable
109
+
110
+ store.scope = scope
111
+ store.state = raw ? scope : (scope as Entity).get()
112
+ }
113
+
114
+ protected async run(store: Store): Promise<void> {
115
+ const { request, state } = store
116
+ const reply = await this.#cascade.run(request.input, state)
117
+
118
+ // validate reply only on local environments
119
+ if (process.env.TOA_ENV === 'local' && !(reply instanceof Readable))
120
+ this.#contracts.reply.fit(reply)
121
+
122
+ store.reply = reply
123
+ }
124
+
125
+ protected async commit(_store: Store): Promise<void> {}
126
+
127
+ protected async query(query?: Query): Promise<Scoped> {
128
+ if (query === undefined)
129
+ throw new RequestContractException('Request query is required')
130
+
131
+ const acquire = this.scope[this.#scope] as (
132
+ query: Query,
133
+ mutable?: boolean
134
+ ) => Promise<Scoped>
135
+
136
+ return acquire.call(this.scope, query, this.mutable)
137
+ }
138
+ }
@@ -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
+ }