@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,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,29 +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
46
+ #timer: NodeJS.Timeout | undefined
47
+ #off: (() => void) | undefined
36
48
  #pumping = false
37
49
  #closing = false
38
50
 
39
- 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
+ ) {
40
58
  super()
41
59
 
42
60
  this.#emission = emission
@@ -55,21 +73,21 @@ export class Outbox extends Connector {
55
73
  }
56
74
 
57
75
  /** whether the storage can commit a row atomically with the entity */
58
- get durable () {
76
+ public get durable(): boolean {
59
77
  return this.#storage?.outbox !== undefined
60
78
  }
61
79
 
62
80
  /**
63
- * @param event {toa.core.transition.Event}
64
- * @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.
65
83
  */
66
- row (event) {
84
+ public row(event: Partial<Event>): Row {
67
85
  return {
68
86
  id: newid(),
69
87
  lane: this.#lane(),
70
88
  published: false,
71
89
  pending: Date.now() + this.#gap,
72
- event
90
+ event: event as Event
73
91
  }
74
92
  }
75
93
 
@@ -77,35 +95,54 @@ export class Outbox extends Connector {
77
95
  * Hands a committed row over. Awaited by the caller only on the legacy path — with an
78
96
  * outbox this returns at once and the broker leaves the operation's path.
79
97
  */
80
- publish (row) {
81
- if (!this.durable)
82
- 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)
83
101
 
84
102
  /*
85
103
  * A publication started while the pump is closing would outlive the emitters it needs,
86
104
  * and `comq` waits on a connection that is going rather than failing. The row is already
87
105
  * durable, so leaving it is exactly what it is for.
88
106
  */
89
- if (this.#closing || this.#defer ||
90
- 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
+ )
91
113
  return
92
114
 
93
115
  void this.#publish(row)
94
116
  }
95
117
 
96
- async open () {
118
+ protected override async open(): Promise<void> {
97
119
  if (!this.durable) return
98
120
 
99
121
  if (this.#defer)
100
- 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
+ )
101
125
 
102
- this.#timer = setInterval(() => this.#tick(), this.#interval)
126
+ this.#timer = setInterval(() => {
127
+ this.#tick()
128
+ }, this.#interval)
103
129
  this.#timer.unref()
130
+
131
+ /*
132
+ * A lane changing hands is exactly when rows stranded in it become this replica's to
133
+ * publish, and the cycle would not notice for up to an interval. Being told costs a cycle
134
+ * that finds nothing in the usual case, where the claim arrives once and never changes.
135
+ */
136
+ this.#off = this.#atom.onassigned(() => {
137
+ this.#tick()
138
+ })
104
139
  }
105
140
 
106
- async close () {
141
+ protected override async close(): Promise<void> {
107
142
  this.#closing = true
108
143
 
144
+ this.#off?.()
145
+
109
146
  if (this.#timer !== undefined) clearInterval(this.#timer)
110
147
 
111
148
  await this.#drain()
@@ -121,9 +158,8 @@ export class Outbox extends Connector {
121
158
  * it would not stop it, it would only mean the row is published twice once it lands. What
122
159
  * bounds this instead is the in-flight cap and the drain on close.
123
160
  *
124
- * @private
125
161
  */
126
- async #publish (row) {
162
+ async #publish(row: Row): Promise<void> {
127
163
  this.#publishing.add(row.id)
128
164
 
129
165
  const publishing = this.#emission.emit(row.event)
@@ -146,9 +182,8 @@ export class Outbox extends Connector {
146
182
  * `comq` retries a publish for as long as the broker is down rather than rejecting, so an
147
183
  * unbounded drain outlives any grace period.
148
184
  *
149
- * @private
150
185
  */
151
- async #drain () {
186
+ async #drain(): Promise<void> {
152
187
  if (this.#inflight.size === 0) return
153
188
 
154
189
  await Promise.race([Promise.allSettled([...this.#inflight]), delay(DRAIN)])
@@ -158,9 +193,8 @@ export class Outbox extends Connector {
158
193
  * Reads what is due, publishes it, and marks everything this process has sent — what it just
159
194
  * published and what the immediate path published since the last cycle. One cycle at a time.
160
195
  *
161
- * @private
162
196
  */
163
- #tick () {
197
+ #tick(): void {
164
198
  if (this.#pumping) return
165
199
 
166
200
  this.#pumping = true
@@ -168,29 +202,33 @@ export class Outbox extends Connector {
168
202
  void this.#pump().finally(() => (this.#pumping = false))
169
203
  }
170
204
 
171
- /** @private */
172
- async #pump () {
173
- let page
205
+ async #pump(): Promise<void> {
206
+ let page: Row[]
207
+ let after: string | undefined
174
208
 
175
209
  do {
176
- page = await this.#read(page?.[page.length - 1]?.id)
210
+ page = await this.#read(after)
177
211
 
178
212
  if (page.length === 0) break
179
213
 
214
+ // so a page is never read twice
215
+ after = page[page.length - 1]?.id
216
+
180
217
  /*
181
218
  * A row is unpublished in the database until a cycle marks it, so a page includes what
182
219
  * this replica is sending right now and what a failed marking left behind. Only this
183
220
  * process knows either.
184
221
  */
185
- const rows = page.filter((row) =>
186
- !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
+ )
187
225
 
188
226
  if (rows.length > 0) {
189
227
  console.info('Outbox recovering unpublished events', { count: rows.length })
190
228
 
191
229
  // every row is given its chance; what the broker refused stays unpublished and comes
192
230
  // back on a later cycle
193
- await Promise.allSettled(rows.map((row) => this.#publish(row)))
231
+ await Promise.allSettled(rows.map(async (row) => this.#publish(row)))
194
232
  }
195
233
 
196
234
  // a full page is a page that may have been cut short
@@ -208,15 +246,15 @@ export class Outbox extends Connector {
208
246
  * assignment arrives. Reading without an assignment would be a different guarantee, where
209
247
  * every replica publishes every stranded row.
210
248
  *
211
- * @private
212
- * @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
213
250
  */
214
- async #read (after) {
251
+ async #read(after?: string): Promise<Row[]> {
215
252
  const lanes = this.#atom.slots(LANES)
216
253
 
217
254
  if (lanes === null || lanes.length === 0) return []
218
255
 
219
- return this.#storage.outbox.pending(lanes, Date.now(), this.#batch, after)
256
+ return this.#storage!
257
+ .outbox!.pending(lanes, Date.now(), this.#batch, after)
220
258
  .catch((error) => {
221
259
  console.warn('Outbox read failed', { error })
222
260
 
@@ -229,15 +267,14 @@ export class Outbox extends Connector {
229
267
  * marked one by one. Ids that fail to be marked are kept and retried; a row that is never
230
268
  * marked is simply published again, which is within the contract.
231
269
  *
232
- * @private
233
270
  */
234
- async #mark () {
271
+ async #mark(): Promise<void> {
235
272
  if (this.#published.size === 0) return
236
273
 
237
274
  const ids = [...this.#published]
238
275
 
239
276
  try {
240
- await this.#storage.outbox.settle(ids)
277
+ await this.#storage!.outbox!.settle(ids)
241
278
 
242
279
  for (const id of ids) this.#published.delete(id)
243
280
  } catch (error) {
@@ -245,14 +282,13 @@ export class Outbox extends Connector {
245
282
  }
246
283
  }
247
284
 
248
- /**
285
+ /**
249
286
  * A lane this replica currently owns, so that in steady state it settles its own rows
250
287
  * before it ever reads them. Any lane at all when it owns none: the row still has to be
251
288
  * written, and whoever ends up owning that lane will pump it.
252
289
  *
253
- * @private
254
290
  */
255
- #lane () {
291
+ #lane(): number {
256
292
  const owned = this.#atom.slots(LANES)
257
293
 
258
294
  return owned === null || owned.length === 0
@@ -261,7 +297,11 @@ export class Outbox extends Connector {
261
297
  }
262
298
  }
263
299
 
264
- function number (variable, declared, fallback) {
300
+ function number(
301
+ variable: string,
302
+ declared: number | undefined,
303
+ fallback: number
304
+ ): number {
265
305
  if (declared !== undefined) return declared
266
306
 
267
307
  const value = Number(process.env[variable])
@@ -269,7 +309,11 @@ function number (variable, declared, fallback) {
269
309
  return Number.isNaN(value) || value <= 0 ? fallback : value
270
310
  }
271
311
 
272
- 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
+ }
273
317
 
274
318
  /**
275
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
+ }