@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,85 @@
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
+ export class Request extends Contract {
23
+ public readonly discovery: Explanation = {}
24
+
25
+ public static override Exception: Refusal =
26
+ RequestContractException as unknown as Refusal
27
+
28
+ public constructor(schema: Schema, definition: Definition) {
29
+ super(schema)
30
+
31
+ for (const key of ['description', 'input', 'output', 'errors'] as const)
32
+ if (definition[key] !== undefined)
33
+ (this.discovery as Record<string, unknown>)[key] = definition[key]
34
+ }
35
+
36
+ public static schema(
37
+ definition: Definition,
38
+ entity?: { properties: Record<string, JSONSchema> }
39
+ ): JSONSchema {
40
+ // `source` is not among these: the framework stamps it and whoever reads it takes the keys
41
+ // it knows, so holding it to a schema would only fail calls from a peer that stamps one more
42
+ const schema: JSONSchema = {
43
+ type: 'object',
44
+ properties: {
45
+ authentic: { type: 'boolean' },
46
+ task: { type: 'boolean' }
47
+ },
48
+ additionalProperties: true
49
+ }
50
+
51
+ const required: string[] = []
52
+
53
+ if (definition.input !== undefined) {
54
+ schema.properties.input = definition.input
55
+ required.push('input')
56
+ } else schema.properties.input = { type: 'null' }
57
+
58
+ if (entity === undefined) definition.query = false
59
+
60
+ if (definition.query === true) required.push('query')
61
+
62
+ if (definition.query === false) schema.properties.query = { type: 'null' }
63
+
64
+ if (definition.query !== false) {
65
+ const query = structuredClone(schemas.query)
66
+
67
+ query.properties.id = entity?.properties.id
68
+
69
+ if (definition.type === 'observation') delete query.properties.version
70
+ else delete query.properties.projection
71
+
72
+ if (definition.type !== 'observation' || definition.scope !== 'objects') {
73
+ delete query.properties.omit
74
+ delete query.properties.limit
75
+ } else if (query.required === undefined) query.required = ['limit']
76
+ else query.required.push('limit')
77
+
78
+ schema.properties.query = query
79
+ }
80
+
81
+ if (required.length > 0) schema.required = required
82
+
83
+ return schema
84
+ }
85
+ }
@@ -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
+ }
@@ -1,26 +1,33 @@
1
1
  import { console } from 'openspan'
2
2
  import { Connector } from './connector.js'
3
+ import type { Locator } from './locator.js'
4
+
5
+ interface Lookup extends Connector {
6
+ invoke: () => Promise<any>
7
+ }
3
8
 
4
9
  export class Discovery extends Connector {
5
- #lookup
6
- #lookups
10
+ readonly #lookup: (locator: Locator) => Promise<Lookup>
11
+ #lookups: Record<string, Promise<Lookup>> = {}
7
12
 
8
- constructor (lookup) {
13
+ public constructor(lookup: (locator: Locator) => Promise<Lookup>) {
9
14
  super()
10
15
 
11
16
  this.#lookup = lookup
12
17
  }
13
18
 
14
- async open () {
19
+ protected override async open(): Promise<void> {
15
20
  this.#lookups = {}
16
21
  }
17
22
 
18
- async lookup (locator) {
23
+ public async lookup(locator: Locator): Promise<any> {
19
24
  const id = locator.id
20
25
 
21
26
  if (this.#lookups[id] === undefined) {
22
- this.#lookups[id] = await this.#lookup(locator)
23
- this.depends(this.#lookups[id])
27
+ const lookup = await this.#lookup(locator)
28
+
29
+ this.#lookups[id] = Promise.resolve(lookup)
30
+ this.depends(lookup)
24
31
  }
25
32
 
26
33
  const since = Date.now()
@@ -35,7 +42,7 @@ export class Discovery extends Connector {
35
42
 
36
43
  warning.unref()
37
44
 
38
- const output = await this.#lookups[id].invoke()
45
+ const output = await (await this.#lookups[id]).invoke()
39
46
 
40
47
  clearInterval(warning)
41
48
 
@@ -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
+ }
@@ -1,9 +1,11 @@
1
1
  import { Connector } from './connector.js'
2
+ import type { Event } from './event.js'
3
+ import type { Event as StateEvent } from './types/state.js'
2
4
 
3
5
  export class Emission extends Connector {
4
- #events
6
+ readonly #events: Event[]
5
7
 
6
- constructor (events) {
8
+ public constructor(events: Event[]) {
7
9
  super()
8
10
 
9
11
  this.#events = events
@@ -11,7 +13,7 @@ export class Emission extends Connector {
11
13
  this.depends(events)
12
14
  }
13
15
 
14
- async emit (event) {
16
+ public async emit(event: StateEvent): Promise<void> {
15
17
  const emission = this.#events.map((e) => e.emit(event))
16
18
 
17
19
  await Promise.all(emission)
@@ -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'
@@ -1,6 +1,6 @@
1
1
  import * as uuid from 'uuid'
2
2
 
3
- export function newid () {
3
+ export function newid(): string {
4
4
  const buf = Buffer.alloc(16)
5
5
 
6
6
  uuid.v7(undefined, buf)
@@ -1,24 +1,27 @@
1
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'
2
5
 
3
6
  export class EntitySet {
4
- #set
7
+ readonly #set: Entity[]
5
8
 
6
- constructor (set) {
9
+ public constructor(set: Entity[]) {
7
10
  this.#set = set
8
11
  }
9
12
 
10
- get () {
13
+ public get(): Record[] {
11
14
  return this.#set.map((entity) => entity.get())
12
15
  }
13
16
 
14
- set (values) {
17
+ public set(values: Record[]): void {
15
18
  if (values.length !== this.#set.length)
16
19
  throw new SystemException('Objects array must not be modified')
17
20
 
18
21
  values.forEach((value, index) => this.#set[index].set(value))
19
22
  }
20
23
 
21
- events (input = undefined) {
24
+ public events(input?: object): Event[] {
22
25
  return this.#set.map((entity) => entity.event(input))
23
26
  }
24
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
+ }
@@ -1,4 +1,5 @@
1
1
  import { swap } from '@toa.io/generic'
2
+ import type { SchemaError } from '@toa.io/schemas'
2
3
 
3
4
  export const codes = {
4
5
  System: 0,
@@ -23,79 +24,97 @@ export const codes = {
23
24
  Transmission: 401
24
25
  }
25
26
 
26
- /**
27
- * @implements {toa.core.Exception}
28
- */
29
27
  export class Exception {
30
- code
31
- message
28
+ public readonly code: number
29
+ public readonly message: string
30
+ public cause?: unknown;
32
31
 
33
- constructor (code, message, cause) {
32
+ /** what a contract exception copies off the schema error it refused with */
33
+ [key: string]: unknown
34
+
35
+ public constructor(code: number, message: string, cause?: unknown) {
34
36
  this.code = code
35
37
  this.message = message
36
38
 
37
- if (cause !== undefined)
38
- this.cause = cause
39
+ if (cause !== undefined) this.cause = cause
39
40
  }
40
41
  }
41
42
 
42
43
  export class SystemException extends Exception {
43
- stack
44
+ public readonly stack?: string
44
45
 
45
- constructor (error) {
46
- super(codes.System, error.message)
46
+ public constructor(error: Error | string) {
47
+ super(codes.System, typeof error === 'string' ? error : error.message)
47
48
 
48
- if (error.stack !== undefined) this.stack = error.stack
49
+ if (typeof error !== 'string' && error.stack !== undefined) this.stack = error.stack
49
50
  }
50
51
  }
51
52
 
52
53
  export class ContractException extends Exception {
53
- constructor (code, error, cause) {
54
- super(code || codes.Contract, typeof error === 'string' ? error : error?.message, cause)
54
+ public constructor(
55
+ code: number | undefined,
56
+ error: SchemaError | string | null,
57
+ cause?: unknown
58
+ ) {
59
+ super(
60
+ code ?? codes.Contract,
61
+ typeof error === 'string' ? error : (error?.message ?? ''),
62
+ cause
63
+ )
55
64
 
56
65
  if (typeof error === 'object' && error !== null)
57
- for (const k of ['keyword', 'property', 'schema', 'path', 'params'])
58
- if (k in error)
59
- this[k] = error[k]
66
+ for (const k of ['keyword', 'property', 'schema', 'path', 'params'] as const)
67
+ if (k in error) this[k] = (error as unknown as Record<string, unknown>)[k]
60
68
  }
61
69
  }
62
70
 
63
71
  export class RequestContractException extends ContractException {
64
- constructor (error, cause) { super(codes.RequestContract, error, cause) }
72
+ public constructor(error: SchemaError | string, cause?: unknown) {
73
+ super(codes.RequestContract, error, cause)
74
+ }
65
75
  }
66
76
 
67
77
  export class ResponseContractException extends ContractException {
68
- constructor (error, cause) { super(codes.ResponseContract, error, cause) }
78
+ public constructor(error: SchemaError | string, cause?: unknown) {
79
+ super(codes.ResponseContract, error, cause)
80
+ }
69
81
  }
70
82
 
71
83
  export class EntityContractException extends ContractException {
72
- constructor (error, cause) { super(codes.EntityContract, error, cause) }
84
+ public constructor(error: SchemaError | string, cause?: unknown) {
85
+ super(codes.EntityContract, error, cause)
86
+ }
73
87
  }
74
88
 
75
89
  export class EntityGuardException extends ContractException {
76
- constructor (name, cause) { super(codes.EntityGuard, name, cause) }
90
+ public constructor(name: string, cause?: unknown) {
91
+ super(codes.EntityGuard, name, cause)
92
+ }
77
93
  }
78
94
 
79
95
  // #region exports
80
96
 
81
-
82
-
83
-
84
-
85
-
86
-
87
97
  // a module's exports are static, so the ones that follow a code are named rather
88
98
  // than generated onto the namespace
89
- function derive (name) {
99
+ type Derived = new (message?: string, cause?: unknown) => Exception
100
+
101
+ function derive(name: keyof typeof codes): Derived {
90
102
  const classname = name + 'Exception'
91
103
 
92
- return class extends Exception {
93
- constructor (message, cause) {
94
- super(codes[name], message ? `${classname}: ${message}` : classname, cause)
104
+ const derived = class extends Exception {
105
+ public constructor(message?: string, cause?: unknown) {
106
+ super(
107
+ codes[name],
108
+ message === undefined ? classname : `${classname}: ${message}`,
109
+ cause
110
+ )
95
111
  }
96
-
97
- static name = classname
98
112
  }
113
+
114
+ // the name is the class's own, and a class expression has none worth reporting
115
+ Object.defineProperty(derived, 'name', { value: classname })
116
+
117
+ return derived
99
118
  }
100
119
 
101
120
  export const RequestSyntaxException = derive('RequestSyntax')