@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.
- package/CHANGELOG.md +1 -185
- package/package.json +19 -7
- package/source/assignment.ts +27 -0
- package/{src/call.js → source/call.ts} +16 -21
- package/{src/cascade.js → source/cascade.ts} +6 -8
- package/{src/component.js → source/component.ts} +27 -25
- package/{src/composition.js → source/composition.ts} +9 -3
- package/{src/connector.js → source/connector.ts} +36 -55
- package/source/context.ts +67 -0
- package/source/contract/contract.ts +23 -0
- package/{src/contract/reply.js → source/contract/reply.ts} +26 -19
- package/source/contract/request.ts +85 -0
- package/source/contract/schemas.ts +39 -0
- package/{src/discovery.js → source/discovery.ts} +15 -8
- package/source/effect.ts +14 -0
- package/{src/emission.js → source/emission.ts} +5 -3
- 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/{src/entities/newid.js → source/entities/newid.ts} +1 -1
- package/{src/entities/set.js → source/entities/set.ts} +8 -5
- package/source/event.ts +57 -0
- package/{src/exceptions.js → source/exceptions.ts} +52 -33
- package/source/exposition.ts +33 -0
- package/source/guard.ts +16 -0
- package/{src/index.js → source/index.ts} +8 -0
- package/{src/locator.js → source/locator.ts} +17 -30
- package/source/observation.ts +16 -0
- package/source/operation.ts +138 -0
- package/{src/outbox/outbox.js → source/outbox/outbox.ts} +87 -53
- package/source/query/criteria.ts +90 -0
- package/source/query/options.ts +35 -0
- package/{src/query.js → source/query.ts} +14 -17
- 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/{src/transmission.js → source/transmission.ts} +8 -8
- 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/{types → source/types}/index.ts +9 -16
- package/{types/message.d.ts → source/types/message.ts} +2 -1
- package/source/types/operations.ts +31 -0
- package/source/types/outbox.ts +35 -0
- package/source/types/receiver.ts +10 -0
- package/{types/request.d.ts → source/types/request.ts} +8 -9
- package/source/types/state.ts +9 -0
- package/source/types/storages.ts +129 -0
- package/source/unmanaged.ts +8 -0
- package/test/call.fixtures.js +3 -1
- package/test/call.test.js +28 -7
- package/test/component.test.js +10 -5
- package/test/connector.fixtures.js +7 -7
- package/test/connector.test.js +15 -2
- package/test/context.fixtures.js +1 -1
- package/test/context.test.js +4 -4
- package/test/contract/conditions.test.js +6 -2
- package/test/contract/contract.fixtures.js +3 -4
- package/test/contract/request.test.js +68 -34
- package/test/discovery.test.js +10 -3
- package/test/emission.test.js +16 -8
- package/test/entities/entity.fixtures.js +5 -6
- package/test/entities/entity.test.js +54 -20
- package/test/entities/factory.fixtures.js +11 -10
- package/test/entities/factory.test.js +47 -10
- package/test/entities/set.test.js +1 -1
- package/test/event.test.js +29 -11
- package/test/locator.test.js +10 -4
- package/test/outbox.test.js +49 -15
- package/test/query.test.js +69 -7
- package/test/receiver.fixtures.js +2 -2
- package/test/receiver.test.js +53 -27
- package/test/reflection.test.js +3 -3
- package/test/state.fixtures.js +10 -3
- package/test/state.test.js +77 -14
- package/test/transmission.test.js +25 -12
- 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 +143 -0
- package/transpiled/connector.js.map +1 -0
- package/transpiled/context.d.ts +18 -0
- package/transpiled/context.js +41 -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 +24 -0
- package/transpiled/contract/request.js +59 -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 +77 -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 +267 -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 +106 -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 -28
- package/src/context.js +0 -57
- package/src/contract/contract.js +0 -18
- package/src/contract/request.js +0 -77
- package/src/contract/schemas/error.yaml +0 -9
- package/src/contract/schemas/index.js +0 -11
- package/src/contract/schemas/query.yaml +0 -43
- package/src/contract/schemas/source.yaml +0 -23
- package/src/effect.js +0 -15
- package/src/entities/changeset.js +0 -36
- package/src/entities/entity.js +0 -119
- package/src/entities/factory.js +0 -40
- package/src/entities/index.js +0 -1
- package/src/event.js +0 -54
- package/src/exposition.js +0 -24
- package/src/guard.js +0 -13
- package/src/observation.js +0 -8
- package/src/operation.js +0 -95
- package/src/query/criteria.js +0 -44
- package/src/query/options.js +0 -37
- package/src/receiver.js +0 -123
- package/src/reflection.js +0 -24
- package/src/remote.js +0 -14
- package/src/state.js +0 -138
- package/src/transition.js +0 -61
- package/src/unmanaged.js +0 -7
- package/types/atomicity.d.ts +0 -79
- package/types/bindings.d.ts +0 -42
- package/types/bridges.ts +0 -33
- package/types/call.d.ts +0 -13
- package/types/component.d.ts +0 -12
- package/types/connector.d.ts +0 -18
- package/types/context.d.ts +0 -24
- 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 -73
- package/types/guard.d.ts +0 -5
- package/types/locator.d.ts +0 -13
- package/types/operations.d.ts +0 -8
- package/types/outbox.d.ts +0 -53
- package/types/query.d.ts +0 -17
- package/types/receiver.d.ts +0 -12
- package/types/reflection.d.ts +0 -14
- package/types/remote.d.ts +0 -19
- package/types/state.d.ts +0 -38
- package/types/storages.d.ts +0 -96
- /package/{src/contract/index.js → source/contract/index.ts} +0 -0
- /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 =
|
|
7
|
+
public static override Exception: Refusal =
|
|
8
|
+
ResponseContractException as unknown as Refusal
|
|
6
9
|
|
|
7
|
-
static schema
|
|
8
|
-
const schema
|
|
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 — `
|
|
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 =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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 (
|
|
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
|
|
23
|
+
public async lookup(locator: Locator): Promise<any> {
|
|
19
24
|
const id = locator.id
|
|
20
25
|
|
|
21
26
|
if (this.#lookups[id] === undefined) {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
|
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
|
|
24
|
+
public events(input?: object): Event[] {
|
|
22
25
|
return this.#set.map((entity) => entity.event(input))
|
|
23
26
|
}
|
|
24
27
|
}
|