@toa.io/core 1.0.0-alpha.283 → 1.0.0-alpha.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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} +95 -51
- 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 +95 -14
- package/test/query.test.js +76 -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 -180
- 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 -37
- 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 -58
- 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 -18
- 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,29 +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
|
|
46
|
+
#timer: NodeJS.Timeout | undefined
|
|
47
|
+
#off: (() => void) | undefined
|
|
36
48
|
#pumping = false
|
|
37
49
|
#closing = false
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
// eslint-disable-next-line max-params
|
|
52
|
+
public constructor(
|
|
53
|
+
emission: Emission,
|
|
54
|
+
storage: Storage | undefined,
|
|
55
|
+
atom: Atom,
|
|
56
|
+
options: Options = {}
|
|
57
|
+
) {
|
|
40
58
|
super()
|
|
41
59
|
|
|
42
60
|
this.#emission = emission
|
|
@@ -55,21 +73,21 @@ export class Outbox extends Connector {
|
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
/** whether the storage can commit a row atomically with the entity */
|
|
58
|
-
get durable
|
|
76
|
+
public get durable(): boolean {
|
|
59
77
|
return this.#storage?.outbox !== undefined
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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.
|
|
65
83
|
*/
|
|
66
|
-
row
|
|
84
|
+
public row(event: Partial<Event>): Row {
|
|
67
85
|
return {
|
|
68
86
|
id: newid(),
|
|
69
87
|
lane: this.#lane(),
|
|
70
88
|
published: false,
|
|
71
89
|
pending: Date.now() + this.#gap,
|
|
72
|
-
event
|
|
90
|
+
event: event as Event
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
93
|
|
|
@@ -77,35 +95,54 @@ export class Outbox extends Connector {
|
|
|
77
95
|
* Hands a committed row over. Awaited by the caller only on the legacy path — with an
|
|
78
96
|
* outbox this returns at once and the broker leaves the operation's path.
|
|
79
97
|
*/
|
|
80
|
-
publish
|
|
81
|
-
|
|
82
|
-
|
|
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)
|
|
83
101
|
|
|
84
102
|
/*
|
|
85
103
|
* A publication started while the pump is closing would outlive the emitters it needs,
|
|
86
104
|
* and `comq` waits on a connection that is going rather than failing. The row is already
|
|
87
105
|
* durable, so leaving it is exactly what it is for.
|
|
88
106
|
*/
|
|
89
|
-
if (
|
|
90
|
-
this.#
|
|
107
|
+
if (
|
|
108
|
+
this.#closing ||
|
|
109
|
+
this.#defer ||
|
|
110
|
+
this.#inflight.size >= INFLIGHT ||
|
|
111
|
+
this.#published.size >= PUBLISHED
|
|
112
|
+
)
|
|
91
113
|
return
|
|
92
114
|
|
|
93
115
|
void this.#publish(row)
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
async open
|
|
118
|
+
protected override async open(): Promise<void> {
|
|
97
119
|
if (!this.durable) return
|
|
98
120
|
|
|
99
121
|
if (this.#defer)
|
|
100
|
-
console.warn(
|
|
122
|
+
console.warn(
|
|
123
|
+
'Outbox immediate publication is deferred; events are published by the pump only'
|
|
124
|
+
)
|
|
101
125
|
|
|
102
|
-
this.#timer = setInterval(() =>
|
|
126
|
+
this.#timer = setInterval(() => {
|
|
127
|
+
this.#tick()
|
|
128
|
+
}, this.#interval)
|
|
103
129
|
this.#timer.unref()
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* A lane changing hands is exactly when rows stranded in it become this replica's to
|
|
133
|
+
* publish, and the cycle would not notice for up to an interval. Being told costs a cycle
|
|
134
|
+
* that finds nothing in the usual case, where the claim arrives once and never changes.
|
|
135
|
+
*/
|
|
136
|
+
this.#off = this.#atom.onassigned(() => {
|
|
137
|
+
this.#tick()
|
|
138
|
+
})
|
|
104
139
|
}
|
|
105
140
|
|
|
106
|
-
async close
|
|
141
|
+
protected override async close(): Promise<void> {
|
|
107
142
|
this.#closing = true
|
|
108
143
|
|
|
144
|
+
this.#off?.()
|
|
145
|
+
|
|
109
146
|
if (this.#timer !== undefined) clearInterval(this.#timer)
|
|
110
147
|
|
|
111
148
|
await this.#drain()
|
|
@@ -121,9 +158,8 @@ export class Outbox extends Connector {
|
|
|
121
158
|
* it would not stop it, it would only mean the row is published twice once it lands. What
|
|
122
159
|
* bounds this instead is the in-flight cap and the drain on close.
|
|
123
160
|
*
|
|
124
|
-
* @private
|
|
125
161
|
*/
|
|
126
|
-
async #publish
|
|
162
|
+
async #publish(row: Row): Promise<void> {
|
|
127
163
|
this.#publishing.add(row.id)
|
|
128
164
|
|
|
129
165
|
const publishing = this.#emission.emit(row.event)
|
|
@@ -146,9 +182,8 @@ export class Outbox extends Connector {
|
|
|
146
182
|
* `comq` retries a publish for as long as the broker is down rather than rejecting, so an
|
|
147
183
|
* unbounded drain outlives any grace period.
|
|
148
184
|
*
|
|
149
|
-
* @private
|
|
150
185
|
*/
|
|
151
|
-
async #drain
|
|
186
|
+
async #drain(): Promise<void> {
|
|
152
187
|
if (this.#inflight.size === 0) return
|
|
153
188
|
|
|
154
189
|
await Promise.race([Promise.allSettled([...this.#inflight]), delay(DRAIN)])
|
|
@@ -158,9 +193,8 @@ export class Outbox extends Connector {
|
|
|
158
193
|
* Reads what is due, publishes it, and marks everything this process has sent — what it just
|
|
159
194
|
* published and what the immediate path published since the last cycle. One cycle at a time.
|
|
160
195
|
*
|
|
161
|
-
* @private
|
|
162
196
|
*/
|
|
163
|
-
#tick
|
|
197
|
+
#tick(): void {
|
|
164
198
|
if (this.#pumping) return
|
|
165
199
|
|
|
166
200
|
this.#pumping = true
|
|
@@ -168,29 +202,33 @@ export class Outbox extends Connector {
|
|
|
168
202
|
void this.#pump().finally(() => (this.#pumping = false))
|
|
169
203
|
}
|
|
170
204
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let
|
|
205
|
+
async #pump(): Promise<void> {
|
|
206
|
+
let page: Row[]
|
|
207
|
+
let after: string | undefined
|
|
174
208
|
|
|
175
209
|
do {
|
|
176
|
-
page = await this.#read(
|
|
210
|
+
page = await this.#read(after)
|
|
177
211
|
|
|
178
212
|
if (page.length === 0) break
|
|
179
213
|
|
|
214
|
+
// so a page is never read twice
|
|
215
|
+
after = page[page.length - 1]?.id
|
|
216
|
+
|
|
180
217
|
/*
|
|
181
218
|
* A row is unpublished in the database until a cycle marks it, so a page includes what
|
|
182
219
|
* this replica is sending right now and what a failed marking left behind. Only this
|
|
183
220
|
* process knows either.
|
|
184
221
|
*/
|
|
185
|
-
const rows = page.filter(
|
|
186
|
-
!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
|
+
)
|
|
187
225
|
|
|
188
226
|
if (rows.length > 0) {
|
|
189
227
|
console.info('Outbox recovering unpublished events', { count: rows.length })
|
|
190
228
|
|
|
191
229
|
// every row is given its chance; what the broker refused stays unpublished and comes
|
|
192
230
|
// back on a later cycle
|
|
193
|
-
await Promise.allSettled(rows.map((row) => this.#publish(row)))
|
|
231
|
+
await Promise.allSettled(rows.map(async (row) => this.#publish(row)))
|
|
194
232
|
}
|
|
195
233
|
|
|
196
234
|
// a full page is a page that may have been cut short
|
|
@@ -208,15 +246,15 @@ export class Outbox extends Connector {
|
|
|
208
246
|
* assignment arrives. Reading without an assignment would be a different guarantee, where
|
|
209
247
|
* every replica publishes every stranded row.
|
|
210
248
|
*
|
|
211
|
-
* @
|
|
212
|
-
* @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
|
|
213
250
|
*/
|
|
214
|
-
async #read
|
|
251
|
+
async #read(after?: string): Promise<Row[]> {
|
|
215
252
|
const lanes = this.#atom.slots(LANES)
|
|
216
253
|
|
|
217
254
|
if (lanes === null || lanes.length === 0) return []
|
|
218
255
|
|
|
219
|
-
return this.#storage
|
|
256
|
+
return this.#storage!
|
|
257
|
+
.outbox!.pending(lanes, Date.now(), this.#batch, after)
|
|
220
258
|
.catch((error) => {
|
|
221
259
|
console.warn('Outbox read failed', { error })
|
|
222
260
|
|
|
@@ -229,15 +267,14 @@ export class Outbox extends Connector {
|
|
|
229
267
|
* marked one by one. Ids that fail to be marked are kept and retried; a row that is never
|
|
230
268
|
* marked is simply published again, which is within the contract.
|
|
231
269
|
*
|
|
232
|
-
* @private
|
|
233
270
|
*/
|
|
234
|
-
async #mark
|
|
271
|
+
async #mark(): Promise<void> {
|
|
235
272
|
if (this.#published.size === 0) return
|
|
236
273
|
|
|
237
274
|
const ids = [...this.#published]
|
|
238
275
|
|
|
239
276
|
try {
|
|
240
|
-
await this.#storage
|
|
277
|
+
await this.#storage!.outbox!.settle(ids)
|
|
241
278
|
|
|
242
279
|
for (const id of ids) this.#published.delete(id)
|
|
243
280
|
} catch (error) {
|
|
@@ -245,14 +282,13 @@ export class Outbox extends Connector {
|
|
|
245
282
|
}
|
|
246
283
|
}
|
|
247
284
|
|
|
248
|
-
|
|
285
|
+
/**
|
|
249
286
|
* A lane this replica currently owns, so that in steady state it settles its own rows
|
|
250
287
|
* before it ever reads them. Any lane at all when it owns none: the row still has to be
|
|
251
288
|
* written, and whoever ends up owning that lane will pump it.
|
|
252
289
|
*
|
|
253
|
-
* @private
|
|
254
290
|
*/
|
|
255
|
-
#lane
|
|
291
|
+
#lane(): number {
|
|
256
292
|
const owned = this.#atom.slots(LANES)
|
|
257
293
|
|
|
258
294
|
return owned === null || owned.length === 0
|
|
@@ -261,7 +297,11 @@ export class Outbox extends Connector {
|
|
|
261
297
|
}
|
|
262
298
|
}
|
|
263
299
|
|
|
264
|
-
function number
|
|
300
|
+
function number(
|
|
301
|
+
variable: string,
|
|
302
|
+
declared: number | undefined,
|
|
303
|
+
fallback: number
|
|
304
|
+
): number {
|
|
265
305
|
if (declared !== undefined) return declared
|
|
266
306
|
|
|
267
307
|
const value = Number(process.env[variable])
|
|
@@ -269,7 +309,11 @@ function number (variable, declared, fallback) {
|
|
|
269
309
|
return Number.isNaN(value) || value <= 0 ? fallback : value
|
|
270
310
|
}
|
|
271
311
|
|
|
272
|
-
|
|
312
|
+
async function delay(ms: number): Promise<void> {
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
setTimeout(resolve, ms).unref()
|
|
315
|
+
})
|
|
316
|
+
}
|
|
273
317
|
|
|
274
318
|
/**
|
|
275
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
|
+
}
|