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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/CHANGELOG.md +1 -185
  2. package/package.json +19 -7
  3. package/source/assignment.ts +27 -0
  4. package/{src/call.js → source/call.ts} +16 -21
  5. package/{src/cascade.js → source/cascade.ts} +6 -8
  6. package/{src/component.js → source/component.ts} +27 -25
  7. package/{src/composition.js → source/composition.ts} +9 -3
  8. package/{src/connector.js → source/connector.ts} +36 -55
  9. package/source/context.ts +67 -0
  10. package/source/contract/contract.ts +23 -0
  11. package/{src/contract/reply.js → source/contract/reply.ts} +26 -19
  12. package/source/contract/request.ts +85 -0
  13. package/source/contract/schemas.ts +39 -0
  14. package/{src/discovery.js → source/discovery.ts} +15 -8
  15. package/source/effect.ts +14 -0
  16. package/{src/emission.js → source/emission.ts} +5 -3
  17. package/source/entities/changeset.ts +37 -0
  18. package/source/entities/entity.ts +137 -0
  19. package/source/entities/factory.ts +52 -0
  20. package/source/entities/index.ts +4 -0
  21. package/{src/entities/newid.js → source/entities/newid.ts} +1 -1
  22. package/{src/entities/set.js → source/entities/set.ts} +8 -5
  23. package/source/event.ts +57 -0
  24. package/{src/exceptions.js → source/exceptions.ts} +52 -33
  25. package/source/exposition.ts +33 -0
  26. package/source/guard.ts +16 -0
  27. package/{src/index.js → source/index.ts} +8 -0
  28. package/{src/locator.js → source/locator.ts} +17 -30
  29. package/source/observation.ts +16 -0
  30. package/source/operation.ts +138 -0
  31. package/{src/outbox/outbox.js → source/outbox/outbox.ts} +87 -53
  32. package/source/query/criteria.ts +90 -0
  33. package/source/query/options.ts +35 -0
  34. package/{src/query.js → source/query.ts} +14 -17
  35. package/source/receiver.ts +120 -0
  36. package/source/reflection.ts +20 -0
  37. package/source/remote.ts +20 -0
  38. package/source/state.ts +179 -0
  39. package/source/transition.ts +79 -0
  40. package/{src/transmission.js → source/transmission.ts} +8 -8
  41. package/source/types/atomicity.ts +63 -0
  42. package/source/types/bindings.ts +59 -0
  43. package/source/types/bridges.ts +54 -0
  44. package/source/types/extensions.ts +89 -0
  45. package/{types → source/types}/index.ts +9 -16
  46. package/{types/message.d.ts → source/types/message.ts} +2 -1
  47. package/source/types/operations.ts +31 -0
  48. package/source/types/outbox.ts +35 -0
  49. package/source/types/receiver.ts +10 -0
  50. package/{types/request.d.ts → source/types/request.ts} +8 -9
  51. package/source/types/state.ts +9 -0
  52. package/source/types/storages.ts +129 -0
  53. package/source/unmanaged.ts +8 -0
  54. package/test/call.fixtures.js +3 -1
  55. package/test/call.test.js +28 -7
  56. package/test/component.test.js +10 -5
  57. package/test/connector.fixtures.js +7 -7
  58. package/test/connector.test.js +15 -2
  59. package/test/context.fixtures.js +1 -1
  60. package/test/context.test.js +4 -4
  61. package/test/contract/conditions.test.js +6 -2
  62. package/test/contract/contract.fixtures.js +3 -4
  63. package/test/contract/request.test.js +68 -34
  64. package/test/discovery.test.js +10 -3
  65. package/test/emission.test.js +16 -8
  66. package/test/entities/entity.fixtures.js +5 -6
  67. package/test/entities/entity.test.js +54 -20
  68. package/test/entities/factory.fixtures.js +11 -10
  69. package/test/entities/factory.test.js +47 -10
  70. package/test/entities/set.test.js +1 -1
  71. package/test/event.test.js +29 -11
  72. package/test/locator.test.js +10 -4
  73. package/test/outbox.test.js +49 -15
  74. package/test/query.test.js +69 -7
  75. package/test/receiver.fixtures.js +2 -2
  76. package/test/receiver.test.js +53 -27
  77. package/test/reflection.test.js +3 -3
  78. package/test/state.fixtures.js +10 -3
  79. package/test/state.test.js +77 -14
  80. package/test/transmission.test.js +25 -12
  81. package/transpiled/assignment.d.ts +6 -0
  82. package/transpiled/assignment.js +20 -0
  83. package/transpiled/assignment.js.map +1 -0
  84. package/transpiled/call.d.ts +10 -0
  85. package/transpiled/call.js +52 -0
  86. package/transpiled/call.js.map +1 -0
  87. package/transpiled/cascade.d.ts +7 -0
  88. package/transpiled/cascade.js +28 -0
  89. package/transpiled/cascade.js.map +1 -0
  90. package/transpiled/component.d.ts +15 -0
  91. package/transpiled/component.js +61 -0
  92. package/transpiled/component.js.map +1 -0
  93. package/transpiled/composition.d.ts +6 -0
  94. package/transpiled/composition.js +23 -0
  95. package/transpiled/composition.js.map +1 -0
  96. package/transpiled/connector.d.ts +47 -0
  97. package/transpiled/connector.js +143 -0
  98. package/transpiled/connector.js.map +1 -0
  99. package/transpiled/context.d.ts +18 -0
  100. package/transpiled/context.js +41 -0
  101. package/transpiled/context.js.map +1 -0
  102. package/transpiled/contract/contract.d.ts +10 -0
  103. package/transpiled/contract/contract.js +14 -0
  104. package/transpiled/contract/contract.js.map +1 -0
  105. package/transpiled/contract/index.d.ts +2 -0
  106. package/transpiled/contract/index.js +3 -0
  107. package/transpiled/contract/index.js.map +1 -0
  108. package/transpiled/contract/reply.d.ts +7 -0
  109. package/transpiled/contract/reply.js +48 -0
  110. package/transpiled/contract/reply.js.map +1 -0
  111. package/transpiled/contract/request.d.ts +24 -0
  112. package/transpiled/contract/request.js +59 -0
  113. package/transpiled/contract/request.js.map +1 -0
  114. package/transpiled/contract/schemas.d.ts +4 -0
  115. package/transpiled/contract/schemas.js +36 -0
  116. package/transpiled/contract/schemas.js.map +1 -0
  117. package/transpiled/discovery.d.ts +12 -0
  118. package/transpiled/discovery.js +34 -0
  119. package/transpiled/discovery.js.map +1 -0
  120. package/transpiled/effect.d.ts +5 -0
  121. package/transpiled/effect.js +11 -0
  122. package/transpiled/effect.js.map +1 -0
  123. package/transpiled/emission.d.ts +8 -0
  124. package/transpiled/emission.js +14 -0
  125. package/transpiled/emission.js.map +1 -0
  126. package/transpiled/entities/changeset.d.ts +10 -0
  127. package/transpiled/entities/changeset.js +27 -0
  128. package/transpiled/entities/changeset.js.map +1 -0
  129. package/transpiled/entities/entity.d.ts +19 -0
  130. package/transpiled/entities/entity.js +109 -0
  131. package/transpiled/entities/entity.js.map +1 -0
  132. package/transpiled/entities/factory.d.ts +22 -0
  133. package/transpiled/entities/factory.js +34 -0
  134. package/transpiled/entities/factory.js.map +1 -0
  135. package/transpiled/entities/index.d.ts +4 -0
  136. package/transpiled/entities/index.js +5 -0
  137. package/transpiled/entities/index.js.map +1 -0
  138. package/transpiled/entities/newid.d.ts +1 -0
  139. package/transpiled/entities/newid.js +7 -0
  140. package/transpiled/entities/newid.js.map +1 -0
  141. package/transpiled/entities/set.d.ts +10 -0
  142. package/transpiled/entities/set.js +19 -0
  143. package/transpiled/entities/set.js.map +1 -0
  144. package/transpiled/event.d.ts +14 -0
  145. package/transpiled/event.js +38 -0
  146. package/transpiled/event.js.map +1 -0
  147. package/transpiled/exceptions.d.ts +61 -0
  148. package/transpiled/exceptions.js +93 -0
  149. package/transpiled/exceptions.js.map +1 -0
  150. package/transpiled/exposition.d.ts +18 -0
  151. package/transpiled/exposition.js +18 -0
  152. package/transpiled/exposition.js.map +1 -0
  153. package/transpiled/guard.d.ts +7 -0
  154. package/transpiled/guard.js +12 -0
  155. package/transpiled/guard.js.map +1 -0
  156. package/transpiled/index.d.ts +35 -0
  157. package/transpiled/index.js +29 -0
  158. package/transpiled/index.js.map +1 -0
  159. package/transpiled/locator.d.ts +12 -0
  160. package/transpiled/locator.js +32 -0
  161. package/transpiled/locator.js.map +1 -0
  162. package/transpiled/observation.d.ts +5 -0
  163. package/transpiled/observation.js +12 -0
  164. package/transpiled/observation.js.map +1 -0
  165. package/transpiled/operation.d.ts +49 -0
  166. package/transpiled/operation.js +77 -0
  167. package/transpiled/operation.js.map +1 -0
  168. package/transpiled/outbox/index.d.ts +1 -0
  169. package/transpiled/outbox/index.js +2 -0
  170. package/transpiled/outbox/index.js.map +1 -0
  171. package/transpiled/outbox/outbox.d.ts +46 -0
  172. package/transpiled/outbox/outbox.js +267 -0
  173. package/transpiled/outbox/outbox.js.map +1 -0
  174. package/transpiled/query/criteria.d.ts +6 -0
  175. package/transpiled/query/criteria.js +74 -0
  176. package/transpiled/query/criteria.js.map +1 -0
  177. package/transpiled/query/options.d.ts +3 -0
  178. package/transpiled/query/options.js +27 -0
  179. package/transpiled/query/options.js.map +1 -0
  180. package/transpiled/query.d.ts +9 -0
  181. package/transpiled/query.js +57 -0
  182. package/transpiled/query.js.map +1 -0
  183. package/transpiled/receiver.d.ts +20 -0
  184. package/transpiled/receiver.js +92 -0
  185. package/transpiled/receiver.js.map +1 -0
  186. package/transpiled/reflection.d.ts +9 -0
  187. package/transpiled/reflection.js +13 -0
  188. package/transpiled/reflection.js.map +1 -0
  189. package/transpiled/remote.d.ts +10 -0
  190. package/transpiled/remote.js +12 -0
  191. package/transpiled/remote.js.map +1 -0
  192. package/transpiled/state.d.ts +33 -0
  193. package/transpiled/state.js +126 -0
  194. package/transpiled/state.js.map +1 -0
  195. package/transpiled/transition.d.ts +14 -0
  196. package/transpiled/transition.js +51 -0
  197. package/transpiled/transition.js.map +1 -0
  198. package/transpiled/transmission.d.ts +8 -0
  199. package/transpiled/transmission.js +30 -0
  200. package/transpiled/transmission.js.map +1 -0
  201. package/transpiled/types/atomicity.d.ts +56 -0
  202. package/transpiled/types/atomicity.js +2 -0
  203. package/transpiled/types/atomicity.js.map +1 -0
  204. package/transpiled/types/bindings.d.ts +42 -0
  205. package/transpiled/types/bindings.js +2 -0
  206. package/transpiled/types/bindings.js.map +1 -0
  207. package/transpiled/types/bridges.d.ts +38 -0
  208. package/transpiled/types/bridges.js +2 -0
  209. package/transpiled/types/bridges.js.map +1 -0
  210. package/transpiled/types/extensions.d.ts +62 -0
  211. package/transpiled/types/extensions.js +2 -0
  212. package/transpiled/types/extensions.js.map +1 -0
  213. package/transpiled/types/index.d.ts +13 -0
  214. package/transpiled/types/index.js +8 -0
  215. package/transpiled/types/index.js.map +1 -0
  216. package/transpiled/types/message.d.ts +5 -0
  217. package/transpiled/types/message.js +2 -0
  218. package/transpiled/types/message.js.map +1 -0
  219. package/transpiled/types/operations.d.ts +15 -0
  220. package/transpiled/types/operations.js +2 -0
  221. package/transpiled/types/operations.js.map +1 -0
  222. package/transpiled/types/outbox.d.ts +28 -0
  223. package/transpiled/types/outbox.js +2 -0
  224. package/transpiled/types/outbox.js.map +1 -0
  225. package/transpiled/types/receiver.d.ts +9 -0
  226. package/transpiled/types/receiver.js +2 -0
  227. package/transpiled/types/receiver.js.map +1 -0
  228. package/transpiled/types/request.d.ts +55 -0
  229. package/transpiled/types/request.js +2 -0
  230. package/transpiled/types/request.js.map +1 -0
  231. package/transpiled/types/state.d.ts +9 -0
  232. package/transpiled/types/state.js +2 -0
  233. package/transpiled/types/state.js.map +1 -0
  234. package/transpiled/types/storages.d.ts +106 -0
  235. package/transpiled/types/storages.js +2 -0
  236. package/transpiled/types/storages.js.map +1 -0
  237. package/transpiled/unmanaged.d.ts +5 -0
  238. package/transpiled/unmanaged.js +7 -0
  239. package/transpiled/unmanaged.js.map +1 -0
  240. package/tsconfig.json +10 -0
  241. package/tsconfig.tsbuildinfo +1 -0
  242. package/src/assignment.js +0 -28
  243. package/src/context.js +0 -57
  244. package/src/contract/contract.js +0 -18
  245. package/src/contract/request.js +0 -77
  246. package/src/contract/schemas/error.yaml +0 -9
  247. package/src/contract/schemas/index.js +0 -11
  248. package/src/contract/schemas/query.yaml +0 -43
  249. package/src/contract/schemas/source.yaml +0 -23
  250. package/src/effect.js +0 -15
  251. package/src/entities/changeset.js +0 -36
  252. package/src/entities/entity.js +0 -119
  253. package/src/entities/factory.js +0 -40
  254. package/src/entities/index.js +0 -1
  255. package/src/event.js +0 -54
  256. package/src/exposition.js +0 -24
  257. package/src/guard.js +0 -13
  258. package/src/observation.js +0 -8
  259. package/src/operation.js +0 -95
  260. package/src/query/criteria.js +0 -44
  261. package/src/query/options.js +0 -37
  262. package/src/receiver.js +0 -123
  263. package/src/reflection.js +0 -24
  264. package/src/remote.js +0 -14
  265. package/src/state.js +0 -138
  266. package/src/transition.js +0 -61
  267. package/src/unmanaged.js +0 -7
  268. package/types/atomicity.d.ts +0 -79
  269. package/types/bindings.d.ts +0 -42
  270. package/types/bridges.ts +0 -33
  271. package/types/call.d.ts +0 -13
  272. package/types/component.d.ts +0 -12
  273. package/types/connector.d.ts +0 -18
  274. package/types/context.d.ts +0 -24
  275. package/types/entity.d.ts +0 -46
  276. package/types/event.d.ts +0 -11
  277. package/types/exception.d.ts +0 -4
  278. package/types/extensions.d.ts +0 -73
  279. package/types/guard.d.ts +0 -5
  280. package/types/locator.d.ts +0 -13
  281. package/types/operations.d.ts +0 -8
  282. package/types/outbox.d.ts +0 -53
  283. package/types/query.d.ts +0 -17
  284. package/types/receiver.d.ts +0 -12
  285. package/types/reflection.d.ts +0 -14
  286. package/types/remote.d.ts +0 -19
  287. package/types/state.d.ts +0 -38
  288. package/types/storages.d.ts +0 -96
  289. /package/{src/contract/index.js → source/contract/index.ts} +0 -0
  290. /package/{src/outbox/index.js → source/outbox/index.ts} +0 -0
@@ -0,0 +1,67 @@
1
+ import { Connector } from './connector.js'
2
+ import type { Locator } from './locator.js'
3
+ import type { Component } from './component.js'
4
+ import type { Remote } from './remote.js'
5
+ import type { Aspect } from './types/extensions.js'
6
+ import type { Request } from './types/request.js'
7
+
8
+ type Discover = (namespace: string, name: string) => Promise<Remote>
9
+
10
+ export class Context extends Connector {
11
+ public readonly env: string | undefined
12
+ public readonly name: string | undefined
13
+ public readonly aspects: Aspect[]
14
+ public readonly locator: Locator
15
+
16
+ readonly #local: Component
17
+ readonly #discover: Discover
18
+ readonly #remotes: Record<string, Promise<Remote>> = {}
19
+
20
+ public constructor(local: Component, discover: Discover, aspects: Aspect[] = []) {
21
+ super()
22
+
23
+ this.env = process.env.TOA_ENV
24
+ this.name = process.env.TOA_CONTEXT
25
+ this.aspects = aspects
26
+ this.locator = local?.locator
27
+
28
+ this.#local = local
29
+ this.#discover = discover
30
+
31
+ this.depends(local)
32
+
33
+ if (aspects.length > 0) this.depends(aspects)
34
+ }
35
+
36
+ public async apply(endpoint: string, request: Request): Promise<any> {
37
+ return this.#local.invoke(endpoint, request)
38
+ }
39
+
40
+ // eslint-disable-next-line max-params
41
+ public async call(
42
+ namespace: string,
43
+ name: string,
44
+ endpoint: string,
45
+ request: Request
46
+ ): Promise<any> {
47
+ const remote = await this.#remote(namespace, name)
48
+
49
+ return remote.invoke(endpoint, request)
50
+ }
51
+
52
+ async #remote(namespace: string, name: string): Promise<Remote> {
53
+ const key = namespace + '.' + name
54
+
55
+ this.#remotes[key] ??= this.#connect(namespace, name)
56
+
57
+ return this.#remotes[key]
58
+ }
59
+
60
+ async #connect(namespace: string, name: string): Promise<Remote> {
61
+ const remote = await this.#discover(namespace, name)
62
+
63
+ this.depends(remote)
64
+
65
+ return remote
66
+ }
67
+ }
@@ -0,0 +1,23 @@
1
+ import { SystemException } from '../exceptions.js'
2
+ import type { Schema, SchemaError } from '@toa.io/schemas'
3
+ import type { Exception } from '../exceptions.js'
4
+
5
+ /** What a contract throws with, stated by the subclass. */
6
+ export type Refusal = new (error: SchemaError, cause?: unknown) => Exception
7
+
8
+ export class Contract {
9
+ public readonly schema: Schema
10
+
11
+ public static Exception: Refusal = SystemException as unknown as Refusal
12
+
13
+ public constructor(schema: Schema) {
14
+ this.schema = schema
15
+ }
16
+
17
+ public fit(value: unknown): void {
18
+ const error = this.schema.fit(value)
19
+
20
+ if (error !== null)
21
+ throw new (this.constructor as typeof Contract).Exception(error, value)
22
+ }
23
+ }
@@ -1,23 +1,29 @@
1
1
  import { Contract } from './contract.js'
2
2
  import { ResponseContractException } from '../exceptions.js'
3
+ import type { Refusal } from './contract.js'
4
+ import type { JSONSchema } from './schemas.js'
3
5
 
4
6
  export class Reply extends Contract {
5
- static Exception = ResponseContractException
7
+ public static override Exception: Refusal =
8
+ ResponseContractException as unknown as Refusal
6
9
 
7
- static schema (output, errors) {
8
- const schema = { type: 'object', properties: {}, additionalProperties: false }
10
+ public static schema(output?: JSONSchema, errors?: Array<string | number>): JSONSchema {
11
+ const schema: JSONSchema = {
12
+ type: 'object',
13
+ properties: {},
14
+ additionalProperties: false
15
+ }
9
16
 
10
17
  if (output !== undefined) {
11
18
  /*
12
- * A reply carries more than the operation declares — `_version` and the rest of the
19
+ * A reply carries more than the operation declares — `VERSION` and the rest of the
13
20
  * record's own fields — so what it is validated against is the declaration relaxed.
14
21
  * On a copy: the declaration itself is what a component publishes when asked to
15
22
  * explain, and relaxing that would publish a contract nobody wrote.
16
23
  */
17
24
  output = structuredClone(output)
18
25
 
19
- if (output.type === 'object')
20
- output.additionalProperties = true
26
+ if (output.type === 'object') output.additionalProperties = true
21
27
  else if (output.type === 'array' && output.items?.type === 'object')
22
28
  output.items.additionalProperties = true
23
29
 
@@ -29,20 +35,21 @@ export class Reply extends Contract {
29
35
  * stated, an error is not a reply this operation makes — it is a mistake, and the
30
36
  * contract says so rather than passing an undeclared code on to whoever called.
31
37
  */
32
- schema.properties.error = errors === undefined
33
- ? false
34
- : {
35
- type: 'object',
36
- properties: {
37
- code: {
38
- enum: errors
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
+ }
39
50
  },
40
- message: {
41
- type: 'string'
42
- }
43
- },
44
- required: ['code']
45
- }
51
+ required: ['code']
52
+ }
46
53
 
47
54
  return schema
48
55
  }
@@ -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
  }