@toa.io/core 1.0.0-alpha.29 → 1.0.0-alpha.292
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.
- package/CHANGELOG.md +43 -0
- package/package.json +22 -12
- package/source/assignment.ts +27 -0
- package/source/call.ts +63 -0
- package/source/cascade.ts +32 -0
- package/source/component.ts +83 -0
- package/source/composition.ts +27 -0
- package/source/connector.ts +217 -0
- package/source/context.ts +68 -0
- package/source/contract/contract.ts +23 -0
- package/source/contract/index.ts +2 -0
- package/source/contract/reply.ts +56 -0
- package/source/contract/request.ts +140 -0
- package/source/contract/schemas.ts +39 -0
- package/source/discovery.ts +53 -0
- package/source/effect.ts +14 -0
- package/source/emission.ts +21 -0
- package/source/entities/changeset.ts +37 -0
- package/source/entities/entity.ts +137 -0
- package/source/entities/factory.ts +52 -0
- package/source/entities/index.ts +4 -0
- package/source/entities/newid.ts +9 -0
- package/source/entities/set.ts +27 -0
- package/source/event.ts +57 -0
- package/source/exceptions.ts +133 -0
- package/source/exposition.ts +33 -0
- package/source/guard.ts +16 -0
- package/source/index.ts +36 -0
- package/source/locator.ts +37 -0
- package/source/observation.ts +16 -0
- package/source/operation.ts +139 -0
- package/source/outbox/index.ts +1 -0
- package/source/outbox/outbox.ts +341 -0
- package/source/query/criteria.ts +90 -0
- package/source/query/options.ts +35 -0
- package/source/query.ts +67 -0
- package/source/receiver.ts +120 -0
- package/source/reflection.ts +20 -0
- package/source/remote.ts +20 -0
- package/source/state.ts +179 -0
- package/source/transition.ts +79 -0
- package/source/transmission.ts +38 -0
- package/source/types/atomicity.ts +63 -0
- package/source/types/bindings.ts +59 -0
- package/source/types/bridges.ts +54 -0
- package/source/types/extensions.ts +89 -0
- package/source/types/index.ts +14 -0
- package/{types/message.d.ts → source/types/message.ts} +2 -0
- package/source/types/operations.ts +31 -0
- package/source/types/outbox.ts +35 -0
- package/source/types/receiver.ts +10 -0
- package/source/types/request.ts +54 -0
- package/source/types/state.ts +9 -0
- package/source/types/storages.ts +134 -0
- package/source/unmanaged.ts +8 -0
- package/test/call.fixtures.js +10 -12
- package/test/call.test.js +42 -9
- package/test/component.fixtures.js +5 -8
- package/test/component.test.js +27 -10
- package/test/connector.fixtures.js +29 -13
- package/test/connector.test.js +128 -38
- package/test/context.fixtures.js +8 -12
- package/test/context.test.js +19 -8
- package/test/contract/conditions.test.js +15 -9
- package/test/contract/contract.fixtures.js +11 -16
- package/test/contract/request.test.js +130 -46
- package/test/discovery.test.js +71 -0
- package/test/emission.fixtures.js +6 -10
- package/test/emission.test.js +37 -12
- package/test/entities/entity.fixtures.js +11 -16
- package/test/entities/entity.test.js +105 -50
- package/test/entities/factory.fixtures.js +26 -13
- package/test/entities/factory.test.js +68 -17
- package/test/entities/set.fixtures.js +6 -8
- package/test/entities/set.test.js +6 -5
- package/test/event.fixtures.js +9 -14
- package/test/event.test.js +78 -34
- package/test/locator.test.js +32 -25
- package/test/outbox.test.js +202 -0
- package/test/query.fixtures.js +1 -5
- package/test/query.test.js +111 -14
- package/test/receiver.fixtures.js +9 -12
- package/test/receiver.test.js +80 -31
- package/test/reflection.test.js +9 -8
- package/test/state.fixtures.js +33 -31
- package/test/state.test.js +135 -19
- package/test/transmission.fixtures.js +4 -6
- package/test/transmission.test.js +47 -16
- package/transpiled/assignment.d.ts +6 -0
- package/transpiled/assignment.js +20 -0
- package/transpiled/assignment.js.map +1 -0
- package/transpiled/call.d.ts +10 -0
- package/transpiled/call.js +52 -0
- package/transpiled/call.js.map +1 -0
- package/transpiled/cascade.d.ts +7 -0
- package/transpiled/cascade.js +28 -0
- package/transpiled/cascade.js.map +1 -0
- package/transpiled/component.d.ts +15 -0
- package/transpiled/component.js +61 -0
- package/transpiled/component.js.map +1 -0
- package/transpiled/composition.d.ts +6 -0
- package/transpiled/composition.js +23 -0
- package/transpiled/composition.js.map +1 -0
- package/transpiled/connector.d.ts +47 -0
- package/transpiled/connector.js +185 -0
- package/transpiled/connector.js.map +1 -0
- package/transpiled/context.d.ts +18 -0
- package/transpiled/context.js +42 -0
- package/transpiled/context.js.map +1 -0
- package/transpiled/contract/contract.d.ts +10 -0
- package/transpiled/contract/contract.js +14 -0
- package/transpiled/contract/contract.js.map +1 -0
- package/transpiled/contract/index.d.ts +2 -0
- package/transpiled/contract/index.js +3 -0
- package/transpiled/contract/index.js.map +1 -0
- package/transpiled/contract/reply.d.ts +7 -0
- package/transpiled/contract/reply.js +48 -0
- package/transpiled/contract/reply.js.map +1 -0
- package/transpiled/contract/request.d.ts +29 -0
- package/transpiled/contract/request.js +103 -0
- package/transpiled/contract/request.js.map +1 -0
- package/transpiled/contract/schemas.d.ts +4 -0
- package/transpiled/contract/schemas.js +36 -0
- package/transpiled/contract/schemas.js.map +1 -0
- package/transpiled/discovery.d.ts +12 -0
- package/transpiled/discovery.js +34 -0
- package/transpiled/discovery.js.map +1 -0
- package/transpiled/effect.d.ts +5 -0
- package/transpiled/effect.js +11 -0
- package/transpiled/effect.js.map +1 -0
- package/transpiled/emission.d.ts +8 -0
- package/transpiled/emission.js +14 -0
- package/transpiled/emission.js.map +1 -0
- package/transpiled/entities/changeset.d.ts +10 -0
- package/transpiled/entities/changeset.js +27 -0
- package/transpiled/entities/changeset.js.map +1 -0
- package/transpiled/entities/entity.d.ts +19 -0
- package/transpiled/entities/entity.js +109 -0
- package/transpiled/entities/entity.js.map +1 -0
- package/transpiled/entities/factory.d.ts +22 -0
- package/transpiled/entities/factory.js +34 -0
- package/transpiled/entities/factory.js.map +1 -0
- package/transpiled/entities/index.d.ts +4 -0
- package/transpiled/entities/index.js +5 -0
- package/transpiled/entities/index.js.map +1 -0
- package/transpiled/entities/newid.d.ts +1 -0
- package/transpiled/entities/newid.js +7 -0
- package/transpiled/entities/newid.js.map +1 -0
- package/transpiled/entities/set.d.ts +10 -0
- package/transpiled/entities/set.js +19 -0
- package/transpiled/entities/set.js.map +1 -0
- package/transpiled/event.d.ts +14 -0
- package/transpiled/event.js +38 -0
- package/transpiled/event.js.map +1 -0
- package/transpiled/exceptions.d.ts +61 -0
- package/transpiled/exceptions.js +93 -0
- package/transpiled/exceptions.js.map +1 -0
- package/transpiled/exposition.d.ts +18 -0
- package/transpiled/exposition.js +18 -0
- package/transpiled/exposition.js.map +1 -0
- package/transpiled/guard.d.ts +7 -0
- package/transpiled/guard.js +12 -0
- package/transpiled/guard.js.map +1 -0
- package/transpiled/index.d.ts +35 -0
- package/transpiled/index.js +29 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/locator.d.ts +12 -0
- package/transpiled/locator.js +32 -0
- package/transpiled/locator.js.map +1 -0
- package/transpiled/observation.d.ts +5 -0
- package/transpiled/observation.js +12 -0
- package/transpiled/observation.js.map +1 -0
- package/transpiled/operation.d.ts +49 -0
- package/transpiled/operation.js +78 -0
- package/transpiled/operation.js.map +1 -0
- package/transpiled/outbox/index.d.ts +1 -0
- package/transpiled/outbox/index.js +2 -0
- package/transpiled/outbox/index.js.map +1 -0
- package/transpiled/outbox/outbox.d.ts +46 -0
- package/transpiled/outbox/outbox.js +268 -0
- package/transpiled/outbox/outbox.js.map +1 -0
- package/transpiled/query/criteria.d.ts +6 -0
- package/transpiled/query/criteria.js +74 -0
- package/transpiled/query/criteria.js.map +1 -0
- package/transpiled/query/options.d.ts +3 -0
- package/transpiled/query/options.js +27 -0
- package/transpiled/query/options.js.map +1 -0
- package/transpiled/query.d.ts +9 -0
- package/transpiled/query.js +57 -0
- package/transpiled/query.js.map +1 -0
- package/transpiled/receiver.d.ts +20 -0
- package/transpiled/receiver.js +92 -0
- package/transpiled/receiver.js.map +1 -0
- package/transpiled/reflection.d.ts +9 -0
- package/transpiled/reflection.js +13 -0
- package/transpiled/reflection.js.map +1 -0
- package/transpiled/remote.d.ts +10 -0
- package/transpiled/remote.js +12 -0
- package/transpiled/remote.js.map +1 -0
- package/transpiled/state.d.ts +33 -0
- package/transpiled/state.js +126 -0
- package/transpiled/state.js.map +1 -0
- package/transpiled/transition.d.ts +14 -0
- package/transpiled/transition.js +51 -0
- package/transpiled/transition.js.map +1 -0
- package/transpiled/transmission.d.ts +8 -0
- package/transpiled/transmission.js +30 -0
- package/transpiled/transmission.js.map +1 -0
- package/transpiled/types/atomicity.d.ts +56 -0
- package/transpiled/types/atomicity.js +2 -0
- package/transpiled/types/atomicity.js.map +1 -0
- package/transpiled/types/bindings.d.ts +42 -0
- package/transpiled/types/bindings.js +2 -0
- package/transpiled/types/bindings.js.map +1 -0
- package/transpiled/types/bridges.d.ts +38 -0
- package/transpiled/types/bridges.js +2 -0
- package/transpiled/types/bridges.js.map +1 -0
- package/transpiled/types/extensions.d.ts +62 -0
- package/transpiled/types/extensions.js +2 -0
- package/transpiled/types/extensions.js.map +1 -0
- package/transpiled/types/index.d.ts +13 -0
- package/transpiled/types/index.js +8 -0
- package/transpiled/types/index.js.map +1 -0
- package/transpiled/types/message.d.ts +5 -0
- package/transpiled/types/message.js +2 -0
- package/transpiled/types/message.js.map +1 -0
- package/transpiled/types/operations.d.ts +15 -0
- package/transpiled/types/operations.js +2 -0
- package/transpiled/types/operations.js.map +1 -0
- package/transpiled/types/outbox.d.ts +28 -0
- package/transpiled/types/outbox.js +2 -0
- package/transpiled/types/outbox.js.map +1 -0
- package/transpiled/types/receiver.d.ts +9 -0
- package/transpiled/types/receiver.js +2 -0
- package/transpiled/types/receiver.js.map +1 -0
- package/transpiled/types/request.d.ts +55 -0
- package/transpiled/types/request.js +2 -0
- package/transpiled/types/request.js.map +1 -0
- package/transpiled/types/state.d.ts +9 -0
- package/transpiled/types/state.js +2 -0
- package/transpiled/types/state.js.map +1 -0
- package/transpiled/types/storages.d.ts +111 -0
- package/transpiled/types/storages.js +2 -0
- package/transpiled/types/storages.js.map +1 -0
- package/transpiled/unmanaged.d.ts +5 -0
- package/transpiled/unmanaged.js +7 -0
- package/transpiled/unmanaged.js.map +1 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/src/assignment.js +0 -31
- package/src/call.js +0 -42
- package/src/cascade.js +0 -36
- package/src/component.js +0 -41
- package/src/composition.js +0 -25
- package/src/connector.js +0 -183
- package/src/context.js +0 -55
- package/src/contract/conditions.js +0 -21
- package/src/contract/index.js +0 -7
- package/src/contract/reply.js +0 -25
- package/src/contract/request.js +0 -67
- package/src/contract/schemas/error.yaml +0 -9
- package/src/contract/schemas/index.js +0 -7
- package/src/contract/schemas/query.yaml +0 -30
- package/src/discovery.js +0 -44
- package/src/emission.js +0 -23
- package/src/entities/changeset.js +0 -43
- package/src/entities/entity.js +0 -73
- package/src/entities/factory.js +0 -33
- package/src/entities/index.js +0 -5
- package/src/entities/set.js +0 -15
- package/src/event.js +0 -40
- package/src/exceptions.js +0 -100
- package/src/exposition.js +0 -27
- package/src/index.js +0 -47
- package/src/locator.js +0 -54
- package/src/observation.js +0 -20
- package/src/operation.js +0 -69
- package/src/query/criteria.js +0 -41
- package/src/query/options.js +0 -40
- package/src/query.js +0 -41
- package/src/receiver.js +0 -65
- package/src/reflection.js +0 -28
- package/src/remote.js +0 -17
- package/src/state.js +0 -99
- package/src/transition.js +0 -73
- package/src/transmission.js +0 -33
- package/types/bindings.d.ts +0 -40
- package/types/bridges.ts +0 -30
- package/types/component.d.ts +0 -9
- package/types/connector.d.ts +0 -18
- package/types/context.d.ts +0 -21
- package/types/entity.d.ts +0 -46
- package/types/event.d.ts +0 -11
- package/types/exception.d.ts +0 -4
- package/types/extensions.d.ts +0 -29
- package/types/index.ts +0 -14
- package/types/locator.d.ts +0 -13
- package/types/operations.d.ts +0 -2
- package/types/query.d.ts +0 -15
- package/types/receiver.d.ts +0 -12
- package/types/reflection.d.ts +0 -14
- package/types/request.d.ts +0 -23
- package/types/state.d.ts +0 -35
- 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
|
package/source/effect.ts
ADDED
|
@@ -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,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
|
+
}
|
package/source/event.ts
ADDED
|
@@ -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
|
+
}
|