@toa.io/core 1.0.0-alpha.29 → 1.0.0-alpha.291

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 (304) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +22 -12
  3. package/source/assignment.ts +27 -0
  4. package/source/call.ts +63 -0
  5. package/source/cascade.ts +32 -0
  6. package/source/component.ts +83 -0
  7. package/source/composition.ts +27 -0
  8. package/source/connector.ts +217 -0
  9. package/source/context.ts +68 -0
  10. package/source/contract/contract.ts +23 -0
  11. package/source/contract/index.ts +2 -0
  12. package/source/contract/reply.ts +56 -0
  13. package/source/contract/request.ts +140 -0
  14. package/source/contract/schemas.ts +39 -0
  15. package/source/discovery.ts +53 -0
  16. package/source/effect.ts +14 -0
  17. package/source/emission.ts +21 -0
  18. package/source/entities/changeset.ts +37 -0
  19. package/source/entities/entity.ts +137 -0
  20. package/source/entities/factory.ts +52 -0
  21. package/source/entities/index.ts +4 -0
  22. package/source/entities/newid.ts +9 -0
  23. package/source/entities/set.ts +27 -0
  24. package/source/event.ts +57 -0
  25. package/source/exceptions.ts +133 -0
  26. package/source/exposition.ts +33 -0
  27. package/source/guard.ts +16 -0
  28. package/source/index.ts +36 -0
  29. package/source/locator.ts +37 -0
  30. package/source/observation.ts +16 -0
  31. package/source/operation.ts +139 -0
  32. package/source/outbox/index.ts +1 -0
  33. package/source/outbox/outbox.ts +341 -0
  34. package/source/query/criteria.ts +90 -0
  35. package/source/query/options.ts +35 -0
  36. package/source/query.ts +67 -0
  37. package/source/receiver.ts +120 -0
  38. package/source/reflection.ts +20 -0
  39. package/source/remote.ts +20 -0
  40. package/source/state.ts +179 -0
  41. package/source/transition.ts +79 -0
  42. package/source/transmission.ts +38 -0
  43. package/source/types/atomicity.ts +63 -0
  44. package/source/types/bindings.ts +59 -0
  45. package/source/types/bridges.ts +54 -0
  46. package/source/types/extensions.ts +89 -0
  47. package/source/types/index.ts +14 -0
  48. package/{types/message.d.ts → source/types/message.ts} +2 -0
  49. package/source/types/operations.ts +31 -0
  50. package/source/types/outbox.ts +35 -0
  51. package/source/types/receiver.ts +10 -0
  52. package/source/types/request.ts +54 -0
  53. package/source/types/state.ts +9 -0
  54. package/source/types/storages.ts +134 -0
  55. package/source/unmanaged.ts +8 -0
  56. package/test/call.fixtures.js +10 -12
  57. package/test/call.test.js +42 -9
  58. package/test/component.fixtures.js +5 -8
  59. package/test/component.test.js +27 -10
  60. package/test/connector.fixtures.js +29 -13
  61. package/test/connector.test.js +128 -38
  62. package/test/context.fixtures.js +8 -12
  63. package/test/context.test.js +19 -8
  64. package/test/contract/conditions.test.js +15 -9
  65. package/test/contract/contract.fixtures.js +11 -16
  66. package/test/contract/request.test.js +130 -46
  67. package/test/discovery.test.js +71 -0
  68. package/test/emission.fixtures.js +6 -10
  69. package/test/emission.test.js +37 -12
  70. package/test/entities/entity.fixtures.js +11 -16
  71. package/test/entities/entity.test.js +105 -50
  72. package/test/entities/factory.fixtures.js +26 -13
  73. package/test/entities/factory.test.js +68 -17
  74. package/test/entities/set.fixtures.js +6 -8
  75. package/test/entities/set.test.js +6 -5
  76. package/test/event.fixtures.js +9 -14
  77. package/test/event.test.js +78 -34
  78. package/test/locator.test.js +32 -25
  79. package/test/outbox.test.js +202 -0
  80. package/test/query.fixtures.js +1 -5
  81. package/test/query.test.js +111 -14
  82. package/test/receiver.fixtures.js +9 -12
  83. package/test/receiver.test.js +80 -31
  84. package/test/reflection.test.js +9 -8
  85. package/test/state.fixtures.js +33 -31
  86. package/test/state.test.js +135 -19
  87. package/test/transmission.fixtures.js +4 -6
  88. package/test/transmission.test.js +47 -16
  89. package/transpiled/assignment.d.ts +6 -0
  90. package/transpiled/assignment.js +20 -0
  91. package/transpiled/assignment.js.map +1 -0
  92. package/transpiled/call.d.ts +10 -0
  93. package/transpiled/call.js +52 -0
  94. package/transpiled/call.js.map +1 -0
  95. package/transpiled/cascade.d.ts +7 -0
  96. package/transpiled/cascade.js +28 -0
  97. package/transpiled/cascade.js.map +1 -0
  98. package/transpiled/component.d.ts +15 -0
  99. package/transpiled/component.js +61 -0
  100. package/transpiled/component.js.map +1 -0
  101. package/transpiled/composition.d.ts +6 -0
  102. package/transpiled/composition.js +23 -0
  103. package/transpiled/composition.js.map +1 -0
  104. package/transpiled/connector.d.ts +47 -0
  105. package/transpiled/connector.js +185 -0
  106. package/transpiled/connector.js.map +1 -0
  107. package/transpiled/context.d.ts +18 -0
  108. package/transpiled/context.js +42 -0
  109. package/transpiled/context.js.map +1 -0
  110. package/transpiled/contract/contract.d.ts +10 -0
  111. package/transpiled/contract/contract.js +14 -0
  112. package/transpiled/contract/contract.js.map +1 -0
  113. package/transpiled/contract/index.d.ts +2 -0
  114. package/transpiled/contract/index.js +3 -0
  115. package/transpiled/contract/index.js.map +1 -0
  116. package/transpiled/contract/reply.d.ts +7 -0
  117. package/transpiled/contract/reply.js +48 -0
  118. package/transpiled/contract/reply.js.map +1 -0
  119. package/transpiled/contract/request.d.ts +29 -0
  120. package/transpiled/contract/request.js +103 -0
  121. package/transpiled/contract/request.js.map +1 -0
  122. package/transpiled/contract/schemas.d.ts +4 -0
  123. package/transpiled/contract/schemas.js +36 -0
  124. package/transpiled/contract/schemas.js.map +1 -0
  125. package/transpiled/discovery.d.ts +12 -0
  126. package/transpiled/discovery.js +34 -0
  127. package/transpiled/discovery.js.map +1 -0
  128. package/transpiled/effect.d.ts +5 -0
  129. package/transpiled/effect.js +11 -0
  130. package/transpiled/effect.js.map +1 -0
  131. package/transpiled/emission.d.ts +8 -0
  132. package/transpiled/emission.js +14 -0
  133. package/transpiled/emission.js.map +1 -0
  134. package/transpiled/entities/changeset.d.ts +10 -0
  135. package/transpiled/entities/changeset.js +27 -0
  136. package/transpiled/entities/changeset.js.map +1 -0
  137. package/transpiled/entities/entity.d.ts +19 -0
  138. package/transpiled/entities/entity.js +109 -0
  139. package/transpiled/entities/entity.js.map +1 -0
  140. package/transpiled/entities/factory.d.ts +22 -0
  141. package/transpiled/entities/factory.js +34 -0
  142. package/transpiled/entities/factory.js.map +1 -0
  143. package/transpiled/entities/index.d.ts +4 -0
  144. package/transpiled/entities/index.js +5 -0
  145. package/transpiled/entities/index.js.map +1 -0
  146. package/transpiled/entities/newid.d.ts +1 -0
  147. package/transpiled/entities/newid.js +7 -0
  148. package/transpiled/entities/newid.js.map +1 -0
  149. package/transpiled/entities/set.d.ts +10 -0
  150. package/transpiled/entities/set.js +19 -0
  151. package/transpiled/entities/set.js.map +1 -0
  152. package/transpiled/event.d.ts +14 -0
  153. package/transpiled/event.js +38 -0
  154. package/transpiled/event.js.map +1 -0
  155. package/transpiled/exceptions.d.ts +61 -0
  156. package/transpiled/exceptions.js +93 -0
  157. package/transpiled/exceptions.js.map +1 -0
  158. package/transpiled/exposition.d.ts +18 -0
  159. package/transpiled/exposition.js +18 -0
  160. package/transpiled/exposition.js.map +1 -0
  161. package/transpiled/guard.d.ts +7 -0
  162. package/transpiled/guard.js +12 -0
  163. package/transpiled/guard.js.map +1 -0
  164. package/transpiled/index.d.ts +35 -0
  165. package/transpiled/index.js +29 -0
  166. package/transpiled/index.js.map +1 -0
  167. package/transpiled/locator.d.ts +12 -0
  168. package/transpiled/locator.js +32 -0
  169. package/transpiled/locator.js.map +1 -0
  170. package/transpiled/observation.d.ts +5 -0
  171. package/transpiled/observation.js +12 -0
  172. package/transpiled/observation.js.map +1 -0
  173. package/transpiled/operation.d.ts +49 -0
  174. package/transpiled/operation.js +78 -0
  175. package/transpiled/operation.js.map +1 -0
  176. package/transpiled/outbox/index.d.ts +1 -0
  177. package/transpiled/outbox/index.js +2 -0
  178. package/transpiled/outbox/index.js.map +1 -0
  179. package/transpiled/outbox/outbox.d.ts +46 -0
  180. package/transpiled/outbox/outbox.js +268 -0
  181. package/transpiled/outbox/outbox.js.map +1 -0
  182. package/transpiled/query/criteria.d.ts +6 -0
  183. package/transpiled/query/criteria.js +74 -0
  184. package/transpiled/query/criteria.js.map +1 -0
  185. package/transpiled/query/options.d.ts +3 -0
  186. package/transpiled/query/options.js +27 -0
  187. package/transpiled/query/options.js.map +1 -0
  188. package/transpiled/query.d.ts +9 -0
  189. package/transpiled/query.js +57 -0
  190. package/transpiled/query.js.map +1 -0
  191. package/transpiled/receiver.d.ts +20 -0
  192. package/transpiled/receiver.js +92 -0
  193. package/transpiled/receiver.js.map +1 -0
  194. package/transpiled/reflection.d.ts +9 -0
  195. package/transpiled/reflection.js +13 -0
  196. package/transpiled/reflection.js.map +1 -0
  197. package/transpiled/remote.d.ts +10 -0
  198. package/transpiled/remote.js +12 -0
  199. package/transpiled/remote.js.map +1 -0
  200. package/transpiled/state.d.ts +33 -0
  201. package/transpiled/state.js +126 -0
  202. package/transpiled/state.js.map +1 -0
  203. package/transpiled/transition.d.ts +14 -0
  204. package/transpiled/transition.js +51 -0
  205. package/transpiled/transition.js.map +1 -0
  206. package/transpiled/transmission.d.ts +8 -0
  207. package/transpiled/transmission.js +30 -0
  208. package/transpiled/transmission.js.map +1 -0
  209. package/transpiled/types/atomicity.d.ts +56 -0
  210. package/transpiled/types/atomicity.js +2 -0
  211. package/transpiled/types/atomicity.js.map +1 -0
  212. package/transpiled/types/bindings.d.ts +42 -0
  213. package/transpiled/types/bindings.js +2 -0
  214. package/transpiled/types/bindings.js.map +1 -0
  215. package/transpiled/types/bridges.d.ts +38 -0
  216. package/transpiled/types/bridges.js +2 -0
  217. package/transpiled/types/bridges.js.map +1 -0
  218. package/transpiled/types/extensions.d.ts +62 -0
  219. package/transpiled/types/extensions.js +2 -0
  220. package/transpiled/types/extensions.js.map +1 -0
  221. package/transpiled/types/index.d.ts +13 -0
  222. package/transpiled/types/index.js +8 -0
  223. package/transpiled/types/index.js.map +1 -0
  224. package/transpiled/types/message.d.ts +5 -0
  225. package/transpiled/types/message.js +2 -0
  226. package/transpiled/types/message.js.map +1 -0
  227. package/transpiled/types/operations.d.ts +15 -0
  228. package/transpiled/types/operations.js +2 -0
  229. package/transpiled/types/operations.js.map +1 -0
  230. package/transpiled/types/outbox.d.ts +28 -0
  231. package/transpiled/types/outbox.js +2 -0
  232. package/transpiled/types/outbox.js.map +1 -0
  233. package/transpiled/types/receiver.d.ts +9 -0
  234. package/transpiled/types/receiver.js +2 -0
  235. package/transpiled/types/receiver.js.map +1 -0
  236. package/transpiled/types/request.d.ts +55 -0
  237. package/transpiled/types/request.js +2 -0
  238. package/transpiled/types/request.js.map +1 -0
  239. package/transpiled/types/state.d.ts +9 -0
  240. package/transpiled/types/state.js +2 -0
  241. package/transpiled/types/state.js.map +1 -0
  242. package/transpiled/types/storages.d.ts +111 -0
  243. package/transpiled/types/storages.js +2 -0
  244. package/transpiled/types/storages.js.map +1 -0
  245. package/transpiled/unmanaged.d.ts +5 -0
  246. package/transpiled/unmanaged.js +7 -0
  247. package/transpiled/unmanaged.js.map +1 -0
  248. package/tsconfig.json +10 -0
  249. package/tsconfig.tsbuildinfo +1 -0
  250. package/src/assignment.js +0 -31
  251. package/src/call.js +0 -42
  252. package/src/cascade.js +0 -36
  253. package/src/component.js +0 -41
  254. package/src/composition.js +0 -25
  255. package/src/connector.js +0 -183
  256. package/src/context.js +0 -55
  257. package/src/contract/conditions.js +0 -21
  258. package/src/contract/index.js +0 -7
  259. package/src/contract/reply.js +0 -25
  260. package/src/contract/request.js +0 -67
  261. package/src/contract/schemas/error.yaml +0 -9
  262. package/src/contract/schemas/index.js +0 -7
  263. package/src/contract/schemas/query.yaml +0 -30
  264. package/src/discovery.js +0 -44
  265. package/src/emission.js +0 -23
  266. package/src/entities/changeset.js +0 -43
  267. package/src/entities/entity.js +0 -73
  268. package/src/entities/factory.js +0 -33
  269. package/src/entities/index.js +0 -5
  270. package/src/entities/set.js +0 -15
  271. package/src/event.js +0 -40
  272. package/src/exceptions.js +0 -100
  273. package/src/exposition.js +0 -27
  274. package/src/index.js +0 -47
  275. package/src/locator.js +0 -54
  276. package/src/observation.js +0 -20
  277. package/src/operation.js +0 -69
  278. package/src/query/criteria.js +0 -41
  279. package/src/query/options.js +0 -40
  280. package/src/query.js +0 -41
  281. package/src/receiver.js +0 -65
  282. package/src/reflection.js +0 -28
  283. package/src/remote.js +0 -17
  284. package/src/state.js +0 -99
  285. package/src/transition.js +0 -73
  286. package/src/transmission.js +0 -33
  287. package/types/bindings.d.ts +0 -40
  288. package/types/bridges.ts +0 -30
  289. package/types/component.d.ts +0 -9
  290. package/types/connector.d.ts +0 -18
  291. package/types/context.d.ts +0 -21
  292. package/types/entity.d.ts +0 -46
  293. package/types/event.d.ts +0 -11
  294. package/types/exception.d.ts +0 -4
  295. package/types/extensions.d.ts +0 -29
  296. package/types/index.ts +0 -14
  297. package/types/locator.d.ts +0 -13
  298. package/types/operations.d.ts +0 -2
  299. package/types/query.d.ts +0 -15
  300. package/types/receiver.d.ts +0 -12
  301. package/types/reflection.d.ts +0 -14
  302. package/types/request.d.ts +0 -23
  303. package/types/state.d.ts +0 -35
  304. package/types/storages.d.ts +0 -71
@@ -0,0 +1,56 @@
1
+ import { Contract } from './contract.js'
2
+ import { ResponseContractException } from '../exceptions.js'
3
+ import type { Refusal } from './contract.js'
4
+ import type { JSONSchema } from './schemas.js'
5
+
6
+ export class Reply extends Contract {
7
+ public static override Exception: Refusal =
8
+ ResponseContractException as unknown as Refusal
9
+
10
+ public static schema(output?: JSONSchema, errors?: Array<string | number>): JSONSchema {
11
+ const schema: JSONSchema = {
12
+ type: 'object',
13
+ properties: {},
14
+ additionalProperties: false
15
+ }
16
+
17
+ if (output !== undefined) {
18
+ /*
19
+ * A reply carries more than the operation declares — `VERSION` and the rest of the
20
+ * record's own fields — so what it is validated against is the declaration relaxed.
21
+ * On a copy: the declaration itself is what a component publishes when asked to
22
+ * explain, and relaxing that would publish a contract nobody wrote.
23
+ */
24
+ output = structuredClone(output)
25
+
26
+ if (output.type === 'object') output.additionalProperties = true
27
+ else if (output.type === 'array' && output.items?.type === 'object')
28
+ output.items.additionalProperties = true
29
+
30
+ schema.properties.output = output
31
+ }
32
+
33
+ /*
34
+ * An error a caller is meant to handle is one the operation states. Where none are
35
+ * stated, an error is not a reply this operation makes — it is a mistake, and the
36
+ * contract says so rather than passing an undeclared code on to whoever called.
37
+ */
38
+ schema.properties.error =
39
+ errors === undefined
40
+ ? false
41
+ : {
42
+ type: 'object',
43
+ properties: {
44
+ code: {
45
+ enum: errors
46
+ },
47
+ message: {
48
+ type: 'string'
49
+ }
50
+ },
51
+ required: ['code']
52
+ }
53
+
54
+ return schema
55
+ }
56
+ }
@@ -0,0 +1,140 @@
1
+ import * as schemas from './schemas.js'
2
+ import { RequestContractException } from '../exceptions.js'
3
+ import { Contract } from './contract.js'
4
+ import type { Schema } from '@toa.io/schemas'
5
+ import type { Refusal } from './contract.js'
6
+ import type { JSONSchema } from './schemas.js'
7
+
8
+ /** What an operation states about itself, and answers when asked to explain. */
9
+ export interface Explanation {
10
+ description?: string
11
+ input?: JSONSchema | null
12
+ output?: JSONSchema | null
13
+ errors?: Array<string | number>
14
+ }
15
+
16
+ export interface Definition extends Explanation {
17
+ type?: string
18
+ scope?: string
19
+ query?: boolean
20
+ }
21
+
22
+ /** What the operation works on, as the manifest declares it. */
23
+ export interface Entity {
24
+ properties: Record<string, JSONSchema>
25
+ required?: string[]
26
+ }
27
+
28
+ export class Request extends Contract {
29
+ public readonly discovery: Explanation = {}
30
+
31
+ public static override Exception: Refusal =
32
+ RequestContractException as unknown as Refusal
33
+
34
+ public constructor(schema: Schema, definition: Definition, entity?: Entity) {
35
+ super(schema)
36
+
37
+ for (const key of ['description', 'input', 'output', 'errors'] as const)
38
+ if (definition[key] !== undefined)
39
+ (this.discovery as Record<string, unknown>)[key] = definition[key]
40
+
41
+ /*
42
+ * An operation that states no output states an empty schema, which every reply fits —
43
+ * and which says nothing to whoever reads it. What its type answers does, so that is
44
+ * said here instead. Here and not in the definition, because the definition is what a
45
+ * reply is held to: a projection is a lawful answer, and describing one is not the same
46
+ * as requiring it.
47
+ */
48
+ if (empty(this.discovery.output)) {
49
+ const answered = answers(definition, entity)
50
+
51
+ if (answered !== undefined) this.discovery.output = answered
52
+ }
53
+ }
54
+
55
+ public static schema(
56
+ definition: Definition,
57
+ entity?: { properties: Record<string, JSONSchema> }
58
+ ): JSONSchema {
59
+ // `source` is not among these: the framework stamps it and whoever reads it takes the keys
60
+ // it knows, so holding it to a schema would only fail calls from a peer that stamps one more
61
+ const schema: JSONSchema = {
62
+ type: 'object',
63
+ properties: {
64
+ authentic: { type: 'boolean' },
65
+ task: { type: 'boolean' }
66
+ },
67
+ additionalProperties: true
68
+ }
69
+
70
+ const required: string[] = []
71
+
72
+ if (definition.input !== undefined) {
73
+ schema.properties.input = definition.input
74
+ required.push('input')
75
+ } else schema.properties.input = { type: 'null' }
76
+
77
+ if (entity === undefined) definition.query = false
78
+
79
+ if (definition.query === true) required.push('query')
80
+
81
+ if (definition.query === false) schema.properties.query = { type: 'null' }
82
+
83
+ if (definition.query !== false) {
84
+ const query = structuredClone(schemas.query)
85
+
86
+ query.properties.id = entity?.properties.id
87
+
88
+ if (definition.type === 'observation') delete query.properties.version
89
+ else delete query.properties.projection
90
+
91
+ if (definition.type !== 'observation' || definition.scope !== 'objects') {
92
+ delete query.properties.omit
93
+ delete query.properties.limit
94
+ } else if (query.required === undefined) query.required = ['limit']
95
+ else query.required.push('limit')
96
+
97
+ schema.properties.query = query
98
+ }
99
+
100
+ if (required.length > 0) schema.required = required
101
+
102
+ return schema
103
+ }
104
+ }
105
+
106
+ /**
107
+ * What an operation of this type answers, where it did not say.
108
+ *
109
+ * A transition, an observation and an assignment work on the Entity Object, so that is what
110
+ * they answer — a set of them where the scope is a set. A computation neither uses the scope
111
+ * nor produces one, and an effect answers whatever it computed, so neither is guessed at.
112
+ * A stream is not a value with a schema, and is not one either.
113
+ *
114
+ * What the entity requires is said as well. It is a description and nothing is held to it —
115
+ * an operation answering a projection says so by declaring its own output — and a shape
116
+ * whose every property reads as optional says less than it knows.
117
+ */
118
+ function answers(definition: Definition, entity?: Entity): JSONSchema | undefined {
119
+ if (entity === undefined || !ENTITY.has(definition.type as string)) return undefined
120
+
121
+ const object: JSONSchema = {
122
+ type: 'object',
123
+ properties: entity.properties,
124
+ ...(entity.required === undefined ? {} : { required: entity.required })
125
+ }
126
+
127
+ if (definition.scope === 'objects') return { type: 'array', items: object } as JSONSchema
128
+
129
+ return definition.scope === 'object' || definition.scope === 'changeset'
130
+ ? object
131
+ : undefined
132
+ }
133
+
134
+ /** The types whose subject is the Entity Object; see `documentation/design.md`. */
135
+ const ENTITY = new Set(['transition', 'observation', 'assignment'])
136
+
137
+ /** A schema that states nothing, which is what an operation saying nothing normalizes to. */
138
+ function empty(schema: JSONSchema | null | undefined): boolean {
139
+ return schema === undefined || schema === null || Object.keys(schema).length === 0
140
+ }
@@ -0,0 +1,39 @@
1
+ /** A JSON Schema, loose because it is handed to a validator and edited by the contracts. */
2
+ export type JSONSchema = Record<string, any>
3
+
4
+ export const query: JSONSchema = {
5
+ type: 'object',
6
+ properties: {
7
+ id: { type: 'string' },
8
+ ids: { type: 'array', uniqueItems: true, minItems: 1, items: { type: 'string' } },
9
+ version: { type: 'integer', minimum: 0 },
10
+ criteria: { type: 'string' },
11
+ search: { type: 'string' },
12
+ sample: { type: 'number' },
13
+ omit: { type: 'integer', minimum: 0 },
14
+ limit: { type: 'integer', minimum: 0 },
15
+ sort: {
16
+ type: 'array',
17
+ uniqueItems: true,
18
+ minItems: 1,
19
+ items: { type: 'string', pattern: '^\\w{1,32}(?::(?:asc|desc))?$' }
20
+ },
21
+ projection: {
22
+ type: 'array',
23
+ uniqueItems: true,
24
+ minItems: 1,
25
+ items: { type: 'string', not: { const: 'id' } }
26
+ },
27
+ deleted: { type: 'boolean' }
28
+ },
29
+ additionalProperties: false
30
+ }
31
+
32
+ export const error: JSONSchema = {
33
+ type: 'object',
34
+ properties: {
35
+ code: { anyOf: [{ type: 'integer' }, { type: 'string' }] },
36
+ message: { type: 'string' }
37
+ },
38
+ required: ['code']
39
+ }
@@ -0,0 +1,53 @@
1
+ import { console } from 'openspan'
2
+ import { Connector } from './connector.js'
3
+ import type { Locator } from './locator.js'
4
+
5
+ interface Lookup extends Connector {
6
+ invoke: () => Promise<any>
7
+ }
8
+
9
+ export class Discovery extends Connector {
10
+ readonly #lookup: (locator: Locator) => Promise<Lookup>
11
+ #lookups: Record<string, Promise<Lookup>> = {}
12
+
13
+ public constructor(lookup: (locator: Locator) => Promise<Lookup>) {
14
+ super()
15
+
16
+ this.#lookup = lookup
17
+ }
18
+
19
+ protected override async open(): Promise<void> {
20
+ this.#lookups = {}
21
+ }
22
+
23
+ public async lookup(locator: Locator): Promise<any> {
24
+ const id = locator.id
25
+
26
+ if (this.#lookups[id] === undefined) {
27
+ const lookup = await this.#lookup(locator)
28
+
29
+ this.#lookups[id] = Promise.resolve(lookup)
30
+ this.depends(lookup)
31
+ }
32
+
33
+ const since = Date.now()
34
+
35
+ // the wait is unbounded by design, as a dependency may still be starting,
36
+ // so repeating is the only way a component stuck on one stays visible
37
+ const warning = setInterval(() => {
38
+ const waiting = Math.round((Date.now() - since) / 1000)
39
+
40
+ console.warn('Waiting for lookup response', { component: id, waiting })
41
+ }, INTERVAL)
42
+
43
+ warning.unref()
44
+
45
+ const output = await (await this.#lookups[id]).invoke()
46
+
47
+ clearInterval(warning)
48
+
49
+ return output
50
+ }
51
+ }
52
+
53
+ const INTERVAL = 5000
@@ -0,0 +1,14 @@
1
+ import { Observation } from './observation.js'
2
+ import type { Store } from './operation.js'
3
+ import type { Entity } from './entities/entity.js'
4
+
5
+ export class Effect extends Observation {
6
+ protected override async acquire(store: Store): Promise<void> {
7
+ const { query, entity, input } = store.request
8
+
9
+ if (entity === undefined) return super.acquire(store)
10
+
11
+ store.scope = await this.scope.ensure(query as any, entity, input)
12
+ store.state = (store.scope as Entity).get()
13
+ }
14
+ }
@@ -0,0 +1,21 @@
1
+ import { Connector } from './connector.js'
2
+ import type { Event } from './event.js'
3
+ import type { Event as StateEvent } from './types/state.js'
4
+
5
+ export class Emission extends Connector {
6
+ readonly #events: Event[]
7
+
8
+ public constructor(events: Event[]) {
9
+ super()
10
+
11
+ this.#events = events
12
+
13
+ this.depends(events)
14
+ }
15
+
16
+ public async emit(event: StateEvent): Promise<void> {
17
+ const emission = this.#events.map((e) => e.emit(event))
18
+
19
+ await Promise.all(emission)
20
+ }
21
+ }
@@ -0,0 +1,37 @@
1
+ import { EntityContractException } from '../exceptions.js'
2
+ import type { Schema } from '@toa.io/schemas'
3
+ import type { Query, Record } from '../types/storages.js'
4
+
5
+ export class Changeset {
6
+ public readonly query: Query
7
+
8
+ /** the changeset schema: the entity's properties, none of them required */
9
+ readonly #schema: Schema
10
+ #state: Record | object
11
+
12
+ public constructor(schema: Schema, query: Query) {
13
+ this.query = query
14
+
15
+ this.#schema = schema
16
+ this.#state = {}
17
+ }
18
+
19
+ public get(): object {
20
+ return this.#state
21
+ }
22
+
23
+ public set(value: Record): void {
24
+ const error = this.#schema.fit(value)
25
+
26
+ if (error !== null) throw new EntityContractException(error, value)
27
+
28
+ delete (value as Partial<Record>).VERSION
29
+ value.UPDATED = Date.now()
30
+
31
+ this.#state = value
32
+ }
33
+
34
+ public export(): object {
35
+ return this.#state
36
+ }
37
+ }
@@ -0,0 +1,137 @@
1
+ import { EntityContractException, EntityGuardException } from '../exceptions.js'
2
+ import { newid } from './newid.js'
3
+ import type { Schema } from '@toa.io/schemas'
4
+ import type { Guard } from '../guard.js'
5
+ import type { Record } from '../types/storages.js'
6
+ import type { Event } from '../types/state.js'
7
+
8
+ export class Entity {
9
+ public deleted = false
10
+
11
+ readonly #schema: Schema
12
+ readonly #blank: object
13
+ readonly #guards: Guard[] | undefined
14
+ #origin: Record | null = null
15
+ #state!: Record
16
+ #mutable = true
17
+
18
+ /**
19
+ * A record makes an entity of what a storage holds; a string, or nothing at all, makes a
20
+ * blank one under that identity, out of what the component declared a record starts as.
21
+ *
22
+ * @param blank what the component declares a record holds before anything is written to it
23
+ * @param mutable whether the entity may be modified and committed
24
+ */
25
+ // eslint-disable-next-line max-params
26
+ public constructor(
27
+ schema: Schema,
28
+ blank: object,
29
+ argument?: Record | string,
30
+ guards?: Guard[],
31
+ mutable = true
32
+ ) {
33
+ this.#schema = schema
34
+ this.#blank = blank
35
+ this.#guards = guards
36
+
37
+ if (typeof argument === 'object') this.#acquire(argument, mutable)
38
+ else this.#write(this.#compose(argument ?? newid()))
39
+ }
40
+
41
+ public get(): Record {
42
+ return this.#state
43
+ }
44
+
45
+ public set(value: Record): void {
46
+ if (!this.#mutable)
47
+ throw new Error('Entity acquired by a read-only operation cannot be modified')
48
+
49
+ this.#guard(value)
50
+
51
+ const error = this.#schema.fit(value)
52
+
53
+ if (error !== null) throw new EntityContractException(error, value)
54
+
55
+ this.#revive(value)
56
+ this.#write(value)
57
+ }
58
+
59
+ public event(input?: object): Event {
60
+ return {
61
+ origin: this.#origin,
62
+ state: this.#state,
63
+ trailers: this.#state._trailers,
64
+ input
65
+ }
66
+ }
67
+
68
+ /**
69
+ * The origin is the pre-image a commit diffs the new state against. An operation that
70
+ * cannot commit has nothing to diff, so it takes the record as it came from the storage
71
+ * instead of paying for a deep copy of every record it read.
72
+ */
73
+ #acquire(record: Record, mutable: boolean): void {
74
+ this.#mutable = mutable
75
+
76
+ if (!mutable) {
77
+ this.#write(record)
78
+
79
+ return
80
+ }
81
+
82
+ this.#write(structuredClone(record))
83
+ this.#origin = record
84
+ }
85
+
86
+ /**
87
+ * A blank is not validated: what the component declared is fitted once, at boot, and the
88
+ * system properties are the runtime's own. It is copied because every entity the component
89
+ * makes is written over its own.
90
+ */
91
+ #compose(id: string): Record {
92
+ return { id, VERSION: 0, DELETED: null, ...structuredClone(this.#blank) } as Record
93
+ }
94
+
95
+ #guard(value: Record): void {
96
+ if (this.#guards === undefined) return
97
+
98
+ for (const guard of this.#guards) {
99
+ const ok = guard.fit(value, this.#origin)
100
+
101
+ if (ok === false) throw new EntityGuardException(guard.name, value)
102
+ }
103
+ }
104
+
105
+ // deletion is only expressed as a new DELETED timestamp,
106
+ // so committing over a tombstone without touching it means revival
107
+ #revive(value: Record): void {
108
+ if (this.#origin?.DELETED == null || value.DELETED !== this.#origin.DELETED) return
109
+
110
+ value.DELETED = null
111
+ this.deleted = false
112
+ }
113
+
114
+ #write(value: Record): void {
115
+ if (!('_trailers' in value))
116
+ Object.defineProperty(value, '_trailers', {
117
+ writable: false,
118
+ configurable: false,
119
+ enumerable: false,
120
+ value: {}
121
+ })
122
+
123
+ if (!('CREATED' in value)) {
124
+ value.CREATED = Date.now()
125
+ value.UPDATED ??= value.CREATED
126
+ }
127
+
128
+ if ('DELETED' in value && value.DELETED !== null) this.deleted = true
129
+
130
+ if (this.#state !== undefined) {
131
+ value.UPDATED = Date.now()
132
+ value.VERSION++
133
+ }
134
+
135
+ this.#state = value
136
+ }
137
+ }
@@ -0,0 +1,52 @@
1
+ import { newid } from './newid.js'
2
+ import { Entity } from './entity.js'
3
+ import { EntitySet } from './set.js'
4
+ import { Changeset } from './changeset.js'
5
+ import type { Schema } from '@toa.io/schemas'
6
+ import type { Guard } from '../guard.js'
7
+ import type { Query, Record } from '../types/storages.js'
8
+
9
+ /** The two schemas a component's records are held to, and what a new one starts as. */
10
+ export interface Schemas {
11
+ /** what a stored record must fit */
12
+ entity: Schema
13
+
14
+ /** what a changeset must fit: the same properties, none of them required */
15
+ changeset: Schema
16
+ }
17
+
18
+ export class Factory {
19
+ readonly #schemas: Schemas
20
+ readonly #blank: object
21
+ readonly #guards: Guard[] | undefined
22
+
23
+ public constructor(schemas: Schemas, blank: object, guards?: Guard[]) {
24
+ this.#schemas = schemas
25
+ this.#blank = blank
26
+ this.#guards = guards
27
+ }
28
+
29
+ public fit(values: object): void {
30
+ this.#schemas.entity.validate({ id: newid(), ...this.#blank, ...values }, 'Entity')
31
+ }
32
+
33
+ public init(id?: string): Entity {
34
+ return new Entity(this.#schemas.entity, this.#blank, id, this.#guards)
35
+ }
36
+
37
+ public object(record: Record, mutable = true): Entity {
38
+ return new Entity(this.#schemas.entity, this.#blank, record, this.#guards, mutable)
39
+ }
40
+
41
+ public objects(recordset: Record[], init?: string[], mutable = true): EntitySet {
42
+ const set = recordset.map((record) => this.object(record, mutable))
43
+
44
+ if (init !== undefined) for (const id of init) set.unshift(this.init(id))
45
+
46
+ return new EntitySet(set)
47
+ }
48
+
49
+ public changeset(query: Query): Changeset {
50
+ return new Changeset(this.#schemas.changeset, query)
51
+ }
52
+ }
@@ -0,0 +1,4 @@
1
+ export { Factory } from './factory.js'
2
+ export { Entity } from './entity.js'
3
+ export { EntitySet } from './set.js'
4
+ export { Changeset } from './changeset.js'
@@ -0,0 +1,9 @@
1
+ import * as uuid from 'uuid'
2
+
3
+ export function newid(): string {
4
+ const buf = Buffer.alloc(16)
5
+
6
+ uuid.v7(undefined, buf)
7
+
8
+ return buf.toString('hex')
9
+ }
@@ -0,0 +1,27 @@
1
+ import { SystemException } from '../exceptions.js'
2
+ import type { Entity } from './entity.js'
3
+ import type { Record } from '../types/storages.js'
4
+ import type { Event } from '../types/state.js'
5
+
6
+ export class EntitySet {
7
+ readonly #set: Entity[]
8
+
9
+ public constructor(set: Entity[]) {
10
+ this.#set = set
11
+ }
12
+
13
+ public get(): Record[] {
14
+ return this.#set.map((entity) => entity.get())
15
+ }
16
+
17
+ public set(values: Record[]): void {
18
+ if (values.length !== this.#set.length)
19
+ throw new SystemException('Objects array must not be modified')
20
+
21
+ values.forEach((value, index) => this.#set[index].set(value))
22
+ }
23
+
24
+ public events(input?: object): Event[] {
25
+ return this.#set.map((entity) => entity.event(input))
26
+ }
27
+ }
@@ -0,0 +1,57 @@
1
+ import { console, current, encode, type SpanOptions } from 'openspan'
2
+ import { Connector } from './connector.js'
3
+ import type { Emitter } from './types/bindings.js'
4
+ import type { Event as Bridge } from './types/bridges.js'
5
+ import type { Message } from './types/message.js'
6
+ import type { Event as StateEvent } from './types/state.js'
7
+
8
+ export interface Definition {
9
+ conditioned?: boolean
10
+ subjective?: boolean
11
+ label?: string
12
+ }
13
+
14
+ export class Event extends Connector {
15
+ readonly #emitter: Emitter
16
+ readonly #bridge: Bridge | undefined
17
+ readonly #conditioned: boolean | undefined
18
+ readonly #subjective: boolean | undefined
19
+ readonly #label: string
20
+
21
+ public constructor(definition: Definition, emitter: Emitter, bridge?: Bridge) {
22
+ super()
23
+
24
+ this.#conditioned = definition.conditioned
25
+ this.#subjective = definition.subjective
26
+ this.#label = definition.label ?? 'event'
27
+ this.#emitter = emitter
28
+ this.#bridge = bridge
29
+
30
+ this.depends(emitter)
31
+
32
+ if (bridge !== undefined) this.depends(bridge)
33
+ }
34
+
35
+ public async emit(event: StateEvent): Promise<void> {
36
+ if (this.#conditioned === false || (await this.#bridge?.condition(event)) === true) {
37
+ const payload =
38
+ this.#subjective === true ? await this.#bridge?.payload(event) : event.state
39
+
40
+ const message: Message = { payload }
41
+
42
+ const options: SpanOptions = {
43
+ name: `${this.#label} publish`,
44
+ kind: 'producer',
45
+ attributes: { 'messaging.destination.name': this.#label }
46
+ }
47
+
48
+ await console.span(options, async () => {
49
+ const context = current()
50
+
51
+ if (context !== undefined) message.telemetry = encode(context)
52
+
53
+ await this.#emitter.emit(message)
54
+ })
55
+ }
56
+ }
57
+ }