@toa.io/core 1.0.0-alpha.284 → 1.0.0-alpha.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/CHANGELOG.md +0 -192
- 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,33 @@
|
|
|
1
|
+
import { Connector } from './connector.js'
|
|
2
|
+
import type { Locator } from './locator.js'
|
|
3
|
+
|
|
4
|
+
interface Manifest {
|
|
5
|
+
namespace: string
|
|
6
|
+
name: string
|
|
7
|
+
entity?: object
|
|
8
|
+
operations?: object
|
|
9
|
+
events?: object
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Exposition extends Connector {
|
|
13
|
+
public readonly locator: Locator
|
|
14
|
+
|
|
15
|
+
readonly #exposition: Partial<Manifest>
|
|
16
|
+
|
|
17
|
+
public constructor(locator: Locator, manifest: Manifest) {
|
|
18
|
+
super()
|
|
19
|
+
|
|
20
|
+
this.locator = locator
|
|
21
|
+
this.#exposition = expose(manifest)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async invoke(): Promise<{ output: Partial<Manifest> }> {
|
|
25
|
+
return { output: this.#exposition }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function expose(manifest: Manifest): Partial<Manifest> {
|
|
30
|
+
const { namespace, name, entity, operations, events } = manifest
|
|
31
|
+
|
|
32
|
+
return { namespace, name, entity, operations, events }
|
|
33
|
+
}
|
package/source/guard.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Guard as Bridge } from './types/bridges.js'
|
|
2
|
+
|
|
3
|
+
export class Guard {
|
|
4
|
+
public readonly name: string
|
|
5
|
+
|
|
6
|
+
readonly #bridge: Bridge
|
|
7
|
+
|
|
8
|
+
public constructor(name: string, bridge: Bridge) {
|
|
9
|
+
this.name = name
|
|
10
|
+
this.#bridge = bridge
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public fit(state: object, origin: object | null): boolean {
|
|
14
|
+
return this.#bridge.fit(state, origin)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -26,3 +26,11 @@ export { Guard } from './guard.js'
|
|
|
26
26
|
export * as entities from './entities/index.js'
|
|
27
27
|
export * as exceptions from './exceptions.js'
|
|
28
28
|
export * as contract from './contract/index.js'
|
|
29
|
+
|
|
30
|
+
export type { Exception } from './exceptions.js'
|
|
31
|
+
export type { Invocable } from './component.js'
|
|
32
|
+
export type { Explicable } from './remote.js'
|
|
33
|
+
export type { Scoped, Store } from './operation.js'
|
|
34
|
+
export type { Source as ReflectionSource } from './reflection.js'
|
|
35
|
+
export type { Explanation } from './contract/request.js'
|
|
36
|
+
export type { JSONSchema } from './contract/schemas.js'
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
import { concat } from '@toa.io/generic'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* @implements {toa.core.Locator}
|
|
5
|
-
*/
|
|
3
|
+
/** Where a component is, as every name derived from it. */
|
|
6
4
|
export class Locator {
|
|
7
|
-
name
|
|
8
|
-
namespace
|
|
9
|
-
|
|
10
|
-
id
|
|
11
|
-
label
|
|
12
|
-
uppercase
|
|
13
|
-
lowercase
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @param {string} name
|
|
17
|
-
* @param {string} [namespace]
|
|
18
|
-
*/
|
|
19
|
-
constructor (name, namespace) {
|
|
5
|
+
public readonly name: string
|
|
6
|
+
public readonly namespace: string | undefined
|
|
7
|
+
|
|
8
|
+
public readonly id: string
|
|
9
|
+
public readonly label: string
|
|
10
|
+
public readonly uppercase: string
|
|
11
|
+
public readonly lowercase: string
|
|
12
|
+
|
|
13
|
+
public constructor(name: string, namespace?: string) {
|
|
20
14
|
if (name === undefined) throw new TypeError('Locator name must be defined')
|
|
21
15
|
|
|
22
16
|
this.name = name
|
|
@@ -28,22 +22,15 @@ export class Locator {
|
|
|
28
22
|
this.lowercase = (concat(namespace, '_') + name).toLowerCase()
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
return concat(prefix?.toLowerCase(), '-') + this.label
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {string} string
|
|
37
|
-
* @returns {Locator}
|
|
38
|
-
*/
|
|
39
|
-
static parse (string) {
|
|
25
|
+
public static parse(string: string): Locator {
|
|
40
26
|
const [namespace, name] = string.split(DOT)
|
|
41
27
|
|
|
42
|
-
if (name === undefined)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
28
|
+
if (name === undefined) return new Locator(namespace)
|
|
29
|
+
else return new Locator(name, namespace)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public hostname(prefix?: string): string {
|
|
33
|
+
return concat(prefix?.toLowerCase(), '-') + this.label
|
|
47
34
|
}
|
|
48
35
|
}
|
|
49
36
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Operation } from './operation.js'
|
|
2
|
+
import type { Store } from './operation.js'
|
|
3
|
+
import type { Entity } from './entities/entity.js'
|
|
4
|
+
|
|
5
|
+
export class Observation extends Operation {
|
|
6
|
+
protected override async run(store: Store): Promise<void> {
|
|
7
|
+
const scope = store.scope as Entity | null
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
scope === null ||
|
|
11
|
+
(scope?.deleted === true && (store.request.query as any)?.options?.deleted !== true)
|
|
12
|
+
)
|
|
13
|
+
store.reply = null
|
|
14
|
+
else await super.run(store)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { Connector } from './connector.js'
|
|
3
|
+
import { SystemException, RequestContractException } from './exceptions.js'
|
|
4
|
+
import type { Cascade } from './cascade.js'
|
|
5
|
+
import type { State } from './state.js'
|
|
6
|
+
import type { Query as Translator } from './query.js'
|
|
7
|
+
import type { Contract } from './contract/contract.js'
|
|
8
|
+
import type { Entity } from './entities/entity.js'
|
|
9
|
+
import type { EntitySet } from './entities/set.js'
|
|
10
|
+
import type { Changeset } from './entities/changeset.js'
|
|
11
|
+
import type { scope as Scope } from './types/operations.js'
|
|
12
|
+
import type { Query, Request } from './types/request.js'
|
|
13
|
+
|
|
14
|
+
/** What an operation acquires for the algorithm to run against. */
|
|
15
|
+
export type Scoped = Entity | EntitySet | Changeset | Readable | null
|
|
16
|
+
|
|
17
|
+
/** What one invocation carries from step to step. */
|
|
18
|
+
export interface Store {
|
|
19
|
+
request: Request
|
|
20
|
+
scope?: Scoped
|
|
21
|
+
state?: any
|
|
22
|
+
reply?: any
|
|
23
|
+
/** set by a transition, which is the only operation that runs its steps again */
|
|
24
|
+
retry?: () => Promise<any>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Contracts {
|
|
28
|
+
request: Contract
|
|
29
|
+
reply: Contract
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Definition {
|
|
33
|
+
scope: Scope
|
|
34
|
+
concurrency?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Operation extends Connector {
|
|
38
|
+
public scope: State
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether what this operation acquires may be modified and committed. Only a
|
|
42
|
+
* transition commits, and only a commit needs the pre-image an entity keeps
|
|
43
|
+
* to diff the new state against.
|
|
44
|
+
*
|
|
45
|
+
* @protected
|
|
46
|
+
*/
|
|
47
|
+
protected mutable: boolean = false
|
|
48
|
+
|
|
49
|
+
readonly #cascade: Cascade
|
|
50
|
+
readonly #contracts: Contracts
|
|
51
|
+
readonly #query: Translator
|
|
52
|
+
readonly #scope: Scope
|
|
53
|
+
|
|
54
|
+
// eslint-disable-next-line max-params
|
|
55
|
+
public constructor(
|
|
56
|
+
cascade: Cascade,
|
|
57
|
+
scope: State,
|
|
58
|
+
contracts: Contracts,
|
|
59
|
+
query: Translator,
|
|
60
|
+
definition: Definition
|
|
61
|
+
) {
|
|
62
|
+
super()
|
|
63
|
+
|
|
64
|
+
this.scope = scope
|
|
65
|
+
|
|
66
|
+
this.#cascade = cascade
|
|
67
|
+
this.#contracts = contracts
|
|
68
|
+
this.#query = query
|
|
69
|
+
this.#scope = definition.scope
|
|
70
|
+
|
|
71
|
+
this.depends(cascade)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async invoke(request: Request): Promise<any> {
|
|
75
|
+
try {
|
|
76
|
+
if (request.authentic !== true) this.#contracts.request.fit(request)
|
|
77
|
+
|
|
78
|
+
// the request carries the query onward in its parsed form: what a storage is given,
|
|
79
|
+
// not what the caller sent
|
|
80
|
+
if ('query' in request)
|
|
81
|
+
request.query = this.#query.parse(request.query as Query) as any
|
|
82
|
+
|
|
83
|
+
// validate entity
|
|
84
|
+
if ('entity' in request) this.scope.fit(request.entity)
|
|
85
|
+
|
|
86
|
+
const store = { request }
|
|
87
|
+
|
|
88
|
+
return await this.process(store)
|
|
89
|
+
} catch (e) {
|
|
90
|
+
const exception = e instanceof Error ? new SystemException(e) : e
|
|
91
|
+
|
|
92
|
+
return { exception }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected async process(store: Store): Promise<any> {
|
|
97
|
+
await this.acquire(store)
|
|
98
|
+
await this.run(store)
|
|
99
|
+
await this.commit(store)
|
|
100
|
+
|
|
101
|
+
return store.reply
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected async acquire(store: Store): Promise<void> {
|
|
105
|
+
if (this.#scope === 'none') return
|
|
106
|
+
|
|
107
|
+
const scope = await this.query(store.request.query)
|
|
108
|
+
const raw = scope === null || scope instanceof Readable
|
|
109
|
+
|
|
110
|
+
store.scope = scope
|
|
111
|
+
store.state = raw ? scope : (scope as Entity).get()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected async run(store: Store): Promise<void> {
|
|
115
|
+
const { request, state } = store
|
|
116
|
+
const reply = await this.#cascade.run(request.input, state)
|
|
117
|
+
|
|
118
|
+
// validate reply only on local environments
|
|
119
|
+
if (process.env.TOA_ENV === 'local' && !(reply instanceof Readable))
|
|
120
|
+
this.#contracts.reply.fit(reply)
|
|
121
|
+
|
|
122
|
+
store.reply = reply
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected async commit(_store: Store): Promise<void> {}
|
|
126
|
+
|
|
127
|
+
protected async query(query?: Query): Promise<Scoped> {
|
|
128
|
+
if (query === undefined)
|
|
129
|
+
throw new RequestContractException('Request query is required')
|
|
130
|
+
|
|
131
|
+
const acquire = this.scope[this.#scope] as (
|
|
132
|
+
query: Query,
|
|
133
|
+
mutable?: boolean
|
|
134
|
+
) => Promise<Scoped>
|
|
135
|
+
|
|
136
|
+
return acquire.call(this.scope, query, this.mutable)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { console } from 'openspan'
|
|
2
2
|
import { Connector } from '../connector.js'
|
|
3
3
|
import { newid } from '../entities/newid.js'
|
|
4
|
+
import type { Emission } from '../emission.js'
|
|
5
|
+
import type { Atom } from '../types/atomicity.js'
|
|
6
|
+
import type { Storage } from '../types/storages.js'
|
|
7
|
+
import type { Row } from '../types/outbox.js'
|
|
8
|
+
import type { Event } from '../types/state.js'
|
|
9
|
+
|
|
10
|
+
export interface Options {
|
|
11
|
+
interval?: number
|
|
12
|
+
batch?: number
|
|
13
|
+
gap?: number
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
/**
|
|
6
17
|
* Owns the intent to publish. A row is built before the write so that the storage can commit
|
|
@@ -14,30 +25,36 @@ import { newid } from '../entities/newid.js'
|
|
|
14
25
|
* inline emission it replaces.
|
|
15
26
|
*/
|
|
16
27
|
export class Outbox extends Connector {
|
|
17
|
-
#emission
|
|
18
|
-
#storage
|
|
19
|
-
#atom
|
|
28
|
+
readonly #emission: Emission
|
|
29
|
+
readonly #storage: Storage | undefined
|
|
30
|
+
readonly #atom: Atom
|
|
20
31
|
|
|
21
|
-
#gap
|
|
22
|
-
#interval
|
|
23
|
-
#batch
|
|
24
|
-
#defer
|
|
32
|
+
readonly #gap: number
|
|
33
|
+
readonly #interval: number
|
|
34
|
+
readonly #batch: number
|
|
35
|
+
readonly #defer: boolean
|
|
25
36
|
|
|
26
37
|
/** ids this process has published, held until a cycle marks them */
|
|
27
|
-
#published = new Set()
|
|
38
|
+
readonly #published = new Set<string>()
|
|
28
39
|
|
|
29
40
|
/** in-flight publications, awaited (with a bound) on close */
|
|
30
|
-
#inflight = new Set()
|
|
41
|
+
readonly #inflight = new Set<Promise<void>>()
|
|
31
42
|
|
|
32
43
|
/** rows this replica is publishing right now, so a cycle does not pick them up again */
|
|
33
|
-
#publishing = new Set()
|
|
44
|
+
readonly #publishing = new Set<string>()
|
|
34
45
|
|
|
35
|
-
#timer
|
|
36
|
-
#off
|
|
46
|
+
#timer: NodeJS.Timeout | undefined
|
|
47
|
+
#off: (() => void) | undefined
|
|
37
48
|
#pumping = false
|
|
38
49
|
#closing = false
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
// eslint-disable-next-line max-params
|
|
52
|
+
public constructor(
|
|
53
|
+
emission: Emission,
|
|
54
|
+
storage: Storage | undefined,
|
|
55
|
+
atom: Atom,
|
|
56
|
+
options: Options = {}
|
|
57
|
+
) {
|
|
41
58
|
super()
|
|
42
59
|
|
|
43
60
|
this.#emission = emission
|
|
@@ -56,21 +73,21 @@ export class Outbox extends Connector {
|
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
/** whether the storage can commit a row atomically with the entity */
|
|
59
|
-
get durable
|
|
76
|
+
public get durable(): boolean {
|
|
60
77
|
return this.#storage?.outbox !== undefined
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
81
|
+
* An assignment's images are the write's own, so it hands over an event with neither, and
|
|
82
|
+
* the storage fills them in.
|
|
66
83
|
*/
|
|
67
|
-
row
|
|
84
|
+
public row(event: Partial<Event>): Row {
|
|
68
85
|
return {
|
|
69
86
|
id: newid(),
|
|
70
87
|
lane: this.#lane(),
|
|
71
88
|
published: false,
|
|
72
89
|
pending: Date.now() + this.#gap,
|
|
73
|
-
event
|
|
90
|
+
event: event as Event
|
|
74
91
|
}
|
|
75
92
|
}
|
|
76
93
|
|
|
@@ -78,29 +95,37 @@ export class Outbox extends Connector {
|
|
|
78
95
|
* Hands a committed row over. Awaited by the caller only on the legacy path — with an
|
|
79
96
|
* outbox this returns at once and the broker leaves the operation's path.
|
|
80
97
|
*/
|
|
81
|
-
publish
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
public publish(row: Row): Promise<void> | void {
|
|
99
|
+
// without a durable outbox this is the inline path, and the caller awaits the emission
|
|
100
|
+
if (!this.durable) return this.#emission.emit(row.event)
|
|
84
101
|
|
|
85
102
|
/*
|
|
86
103
|
* A publication started while the pump is closing would outlive the emitters it needs,
|
|
87
104
|
* and `comq` waits on a connection that is going rather than failing. The row is already
|
|
88
105
|
* durable, so leaving it is exactly what it is for.
|
|
89
106
|
*/
|
|
90
|
-
if (
|
|
91
|
-
this.#
|
|
107
|
+
if (
|
|
108
|
+
this.#closing ||
|
|
109
|
+
this.#defer ||
|
|
110
|
+
this.#inflight.size >= INFLIGHT ||
|
|
111
|
+
this.#published.size >= PUBLISHED
|
|
112
|
+
)
|
|
92
113
|
return
|
|
93
114
|
|
|
94
115
|
void this.#publish(row)
|
|
95
116
|
}
|
|
96
117
|
|
|
97
|
-
async open
|
|
118
|
+
protected override async open(): Promise<void> {
|
|
98
119
|
if (!this.durable) return
|
|
99
120
|
|
|
100
121
|
if (this.#defer)
|
|
101
|
-
console.warn(
|
|
122
|
+
console.warn(
|
|
123
|
+
'Outbox immediate publication is deferred; events are published by the pump only'
|
|
124
|
+
)
|
|
102
125
|
|
|
103
|
-
this.#timer = setInterval(() =>
|
|
126
|
+
this.#timer = setInterval(() => {
|
|
127
|
+
this.#tick()
|
|
128
|
+
}, this.#interval)
|
|
104
129
|
this.#timer.unref()
|
|
105
130
|
|
|
106
131
|
/*
|
|
@@ -108,10 +133,12 @@ export class Outbox extends Connector {
|
|
|
108
133
|
* publish, and the cycle would not notice for up to an interval. Being told costs a cycle
|
|
109
134
|
* that finds nothing in the usual case, where the claim arrives once and never changes.
|
|
110
135
|
*/
|
|
111
|
-
this.#off = this.#atom.onassigned(() =>
|
|
136
|
+
this.#off = this.#atom.onassigned(() => {
|
|
137
|
+
this.#tick()
|
|
138
|
+
})
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
async close
|
|
141
|
+
protected override async close(): Promise<void> {
|
|
115
142
|
this.#closing = true
|
|
116
143
|
|
|
117
144
|
this.#off?.()
|
|
@@ -131,9 +158,8 @@ export class Outbox extends Connector {
|
|
|
131
158
|
* it would not stop it, it would only mean the row is published twice once it lands. What
|
|
132
159
|
* bounds this instead is the in-flight cap and the drain on close.
|
|
133
160
|
*
|
|
134
|
-
* @private
|
|
135
161
|
*/
|
|
136
|
-
async #publish
|
|
162
|
+
async #publish(row: Row): Promise<void> {
|
|
137
163
|
this.#publishing.add(row.id)
|
|
138
164
|
|
|
139
165
|
const publishing = this.#emission.emit(row.event)
|
|
@@ -156,9 +182,8 @@ export class Outbox extends Connector {
|
|
|
156
182
|
* `comq` retries a publish for as long as the broker is down rather than rejecting, so an
|
|
157
183
|
* unbounded drain outlives any grace period.
|
|
158
184
|
*
|
|
159
|
-
* @private
|
|
160
185
|
*/
|
|
161
|
-
async #drain
|
|
186
|
+
async #drain(): Promise<void> {
|
|
162
187
|
if (this.#inflight.size === 0) return
|
|
163
188
|
|
|
164
189
|
await Promise.race([Promise.allSettled([...this.#inflight]), delay(DRAIN)])
|
|
@@ -168,9 +193,8 @@ export class Outbox extends Connector {
|
|
|
168
193
|
* Reads what is due, publishes it, and marks everything this process has sent — what it just
|
|
169
194
|
* published and what the immediate path published since the last cycle. One cycle at a time.
|
|
170
195
|
*
|
|
171
|
-
* @private
|
|
172
196
|
*/
|
|
173
|
-
#tick
|
|
197
|
+
#tick(): void {
|
|
174
198
|
if (this.#pumping) return
|
|
175
199
|
|
|
176
200
|
this.#pumping = true
|
|
@@ -178,29 +202,33 @@ export class Outbox extends Connector {
|
|
|
178
202
|
void this.#pump().finally(() => (this.#pumping = false))
|
|
179
203
|
}
|
|
180
204
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let
|
|
205
|
+
async #pump(): Promise<void> {
|
|
206
|
+
let page: Row[]
|
|
207
|
+
let after: string | undefined
|
|
184
208
|
|
|
185
209
|
do {
|
|
186
|
-
page = await this.#read(
|
|
210
|
+
page = await this.#read(after)
|
|
187
211
|
|
|
188
212
|
if (page.length === 0) break
|
|
189
213
|
|
|
214
|
+
// so a page is never read twice
|
|
215
|
+
after = page[page.length - 1]?.id
|
|
216
|
+
|
|
190
217
|
/*
|
|
191
218
|
* A row is unpublished in the database until a cycle marks it, so a page includes what
|
|
192
219
|
* this replica is sending right now and what a failed marking left behind. Only this
|
|
193
220
|
* process knows either.
|
|
194
221
|
*/
|
|
195
|
-
const rows = page.filter(
|
|
196
|
-
!this.#published.has(row.id) && !this.#publishing.has(row.id)
|
|
222
|
+
const rows = page.filter(
|
|
223
|
+
(row) => !this.#published.has(row.id) && !this.#publishing.has(row.id)
|
|
224
|
+
)
|
|
197
225
|
|
|
198
226
|
if (rows.length > 0) {
|
|
199
227
|
console.info('Outbox recovering unpublished events', { count: rows.length })
|
|
200
228
|
|
|
201
229
|
// every row is given its chance; what the broker refused stays unpublished and comes
|
|
202
230
|
// back on a later cycle
|
|
203
|
-
await Promise.allSettled(rows.map((row) => this.#publish(row)))
|
|
231
|
+
await Promise.allSettled(rows.map(async (row) => this.#publish(row)))
|
|
204
232
|
}
|
|
205
233
|
|
|
206
234
|
// a full page is a page that may have been cut short
|
|
@@ -218,15 +246,15 @@ export class Outbox extends Connector {
|
|
|
218
246
|
* assignment arrives. Reading without an assignment would be a different guarantee, where
|
|
219
247
|
* every replica publishes every stranded row.
|
|
220
248
|
*
|
|
221
|
-
* @
|
|
222
|
-
* @param {string} [after] the last id of the page before, so a page is never read twice
|
|
249
|
+
* @param after the last id of the page before, so a page is never read twice
|
|
223
250
|
*/
|
|
224
|
-
async #read
|
|
251
|
+
async #read(after?: string): Promise<Row[]> {
|
|
225
252
|
const lanes = this.#atom.slots(LANES)
|
|
226
253
|
|
|
227
254
|
if (lanes === null || lanes.length === 0) return []
|
|
228
255
|
|
|
229
|
-
return this.#storage
|
|
256
|
+
return this.#storage!
|
|
257
|
+
.outbox!.pending(lanes, Date.now(), this.#batch, after)
|
|
230
258
|
.catch((error) => {
|
|
231
259
|
console.warn('Outbox read failed', { error })
|
|
232
260
|
|
|
@@ -239,15 +267,14 @@ export class Outbox extends Connector {
|
|
|
239
267
|
* marked one by one. Ids that fail to be marked are kept and retried; a row that is never
|
|
240
268
|
* marked is simply published again, which is within the contract.
|
|
241
269
|
*
|
|
242
|
-
* @private
|
|
243
270
|
*/
|
|
244
|
-
async #mark
|
|
271
|
+
async #mark(): Promise<void> {
|
|
245
272
|
if (this.#published.size === 0) return
|
|
246
273
|
|
|
247
274
|
const ids = [...this.#published]
|
|
248
275
|
|
|
249
276
|
try {
|
|
250
|
-
await this.#storage
|
|
277
|
+
await this.#storage!.outbox!.settle(ids)
|
|
251
278
|
|
|
252
279
|
for (const id of ids) this.#published.delete(id)
|
|
253
280
|
} catch (error) {
|
|
@@ -255,14 +282,13 @@ export class Outbox extends Connector {
|
|
|
255
282
|
}
|
|
256
283
|
}
|
|
257
284
|
|
|
258
|
-
|
|
285
|
+
/**
|
|
259
286
|
* A lane this replica currently owns, so that in steady state it settles its own rows
|
|
260
287
|
* before it ever reads them. Any lane at all when it owns none: the row still has to be
|
|
261
288
|
* written, and whoever ends up owning that lane will pump it.
|
|
262
289
|
*
|
|
263
|
-
* @private
|
|
264
290
|
*/
|
|
265
|
-
#lane
|
|
291
|
+
#lane(): number {
|
|
266
292
|
const owned = this.#atom.slots(LANES)
|
|
267
293
|
|
|
268
294
|
return owned === null || owned.length === 0
|
|
@@ -271,7 +297,11 @@ export class Outbox extends Connector {
|
|
|
271
297
|
}
|
|
272
298
|
}
|
|
273
299
|
|
|
274
|
-
function number
|
|
300
|
+
function number(
|
|
301
|
+
variable: string,
|
|
302
|
+
declared: number | undefined,
|
|
303
|
+
fallback: number
|
|
304
|
+
): number {
|
|
275
305
|
if (declared !== undefined) return declared
|
|
276
306
|
|
|
277
307
|
const value = Number(process.env[variable])
|
|
@@ -279,7 +309,11 @@ function number (variable, declared, fallback) {
|
|
|
279
309
|
return Number.isNaN(value) || value <= 0 ? fallback : value
|
|
280
310
|
}
|
|
281
311
|
|
|
282
|
-
|
|
312
|
+
async function delay(ms: number): Promise<void> {
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
setTimeout(resolve, ms).unref()
|
|
315
|
+
})
|
|
316
|
+
}
|
|
283
317
|
|
|
284
318
|
/**
|
|
285
319
|
* Constant, never configuration: rows carry their lane, so lowering this would leave rows in
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { parse } from '@rsql/parser'
|
|
2
|
+
import { QuerySyntaxException } from '../exceptions.js'
|
|
3
|
+
import type { Node } from '../types/storages.js'
|
|
4
|
+
|
|
5
|
+
/** What a component declares about the properties a criteria may select on. */
|
|
6
|
+
export type Properties = Record<string, { type: string }>
|
|
7
|
+
|
|
8
|
+
export function criteria(expression: string, properties?: Properties): Node {
|
|
9
|
+
let ast: Node
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
ast = parse(expression) as unknown as Node
|
|
13
|
+
} catch (e) {
|
|
14
|
+
throw new QuerySyntaxException((e as Error).message)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (properties !== undefined) read(ast, properties)
|
|
18
|
+
|
|
19
|
+
return ast
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function read(node: Node, properties: Properties): void {
|
|
23
|
+
if (
|
|
24
|
+
node.type === 'COMPARISON' &&
|
|
25
|
+
node.left?.type === 'SELECTOR' &&
|
|
26
|
+
node.right?.type === 'VALUE'
|
|
27
|
+
) {
|
|
28
|
+
const selector = node.left.selector as string
|
|
29
|
+
const property = properties[selector]
|
|
30
|
+
|
|
31
|
+
if (property === undefined) {
|
|
32
|
+
throw new QuerySyntaxException(`Criteria selector '${selector}' is not defined`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const cast = CAST[property.type]
|
|
36
|
+
|
|
37
|
+
// `=in=` and `=out=` carry a list, and casting that as one value gives whatever
|
|
38
|
+
// a comma-separated string reads as
|
|
39
|
+
if (cast !== undefined)
|
|
40
|
+
node.right.value = Array.isArray(node.right.value)
|
|
41
|
+
? node.right.value.map((value: string) => cast(value, selector))
|
|
42
|
+
: cast(node.right.value as string, selector)
|
|
43
|
+
} else {
|
|
44
|
+
if (node.left !== undefined) read(node.left, properties)
|
|
45
|
+
if (node.right !== undefined) read(node.right, properties)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A criteria arrives as text, so a value is read as what the property it selects on holds. What
|
|
51
|
+
* cannot be read as that is refused rather than passed on: `parseInt` answers `NaN` for a word
|
|
52
|
+
* and `12` for `12kg`, and a storage asked to match either finds nothing and says nothing —
|
|
53
|
+
* a mistyped filter reads as an empty result.
|
|
54
|
+
*
|
|
55
|
+
* A string is left as it came, because every text is one: what a criteria may compare a string
|
|
56
|
+
* to is not the type's to say.
|
|
57
|
+
*/
|
|
58
|
+
const CAST: Record<string, (value: string, selector: string) => unknown> = {
|
|
59
|
+
number: (value, selector) => finite(value, selector, 'a number'),
|
|
60
|
+
|
|
61
|
+
integer: (value, selector) => {
|
|
62
|
+
const number = finite(value, selector, 'an integer')
|
|
63
|
+
|
|
64
|
+
if (!Number.isInteger(number)) refuse(selector, 'an integer', value)
|
|
65
|
+
|
|
66
|
+
return number
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
boolean: (value, selector) => {
|
|
70
|
+
if (value === 'true') return true
|
|
71
|
+
if (value === 'false') return false
|
|
72
|
+
|
|
73
|
+
return refuse(selector, 'a boolean', value)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function finite(value: string, selector: string, expected: string): number {
|
|
78
|
+
// `Number` reads a blank string as zero, and a criteria that says nothing says nothing
|
|
79
|
+
const number = value.trim() === '' ? Number.NaN : Number(value)
|
|
80
|
+
|
|
81
|
+
if (!Number.isFinite(number)) refuse(selector, expected, value)
|
|
82
|
+
|
|
83
|
+
return number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function refuse(selector: string, expected: string, value: string): never {
|
|
87
|
+
throw new QuerySyntaxException(
|
|
88
|
+
`Criteria selector '${selector}' takes ${expected}, and '${value}' is not one`
|
|
89
|
+
)
|
|
90
|
+
}
|