@toa.io/core 1.0.0-alpha.292 → 1.0.0-alpha.299
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 +10 -4
- package/transpiled/call.js +12 -0
- package/transpiled/call.js.map +1 -1
- package/transpiled/component.js +31 -4
- package/transpiled/component.js.map +1 -1
- package/transpiled/context.d.ts +5 -0
- package/transpiled/context.js +6 -0
- package/transpiled/context.js.map +1 -1
- package/transpiled/emission.d.ts +4 -2
- package/transpiled/emission.js +4 -2
- package/transpiled/emission.js.map +1 -1
- package/transpiled/entities/entity.js +34 -6
- package/transpiled/entities/entity.js.map +1 -1
- package/transpiled/entities/factory.d.ts +5 -0
- package/transpiled/entities/factory.js +6 -2
- package/transpiled/entities/factory.js.map +1 -1
- package/transpiled/event.d.ts +2 -2
- package/transpiled/event.js +6 -1
- package/transpiled/event.js.map +1 -1
- package/transpiled/exceptions.d.ts +19 -0
- package/transpiled/exceptions.js +74 -2
- package/transpiled/exceptions.js.map +1 -1
- package/transpiled/index.d.ts +2 -1
- package/transpiled/index.js +2 -1
- package/transpiled/index.js.map +1 -1
- package/transpiled/{outbox/outbox.d.ts → outbox.d.ts} +6 -7
- package/transpiled/outbox.js +424 -0
- package/transpiled/outbox.js.map +1 -0
- package/transpiled/query/options.js +1 -1
- package/transpiled/query/options.js.map +1 -1
- package/transpiled/receiver.js +14 -1
- package/transpiled/receiver.js.map +1 -1
- package/transpiled/remote.js +2 -3
- package/transpiled/remote.js.map +1 -1
- package/transpiled/state.d.ts +1 -1
- package/transpiled/trail.d.ts +49 -0
- package/transpiled/trail.js +98 -0
- package/transpiled/trail.js.map +1 -0
- package/transpiled/types/bindings.d.ts +22 -0
- package/transpiled/types/extensions.d.ts +13 -2
- package/transpiled/types/message.d.ts +5 -0
- package/transpiled/types/outbox.d.ts +29 -3
- package/transpiled/types/request.d.ts +5 -0
- package/transpiled/types/storages.d.ts +20 -0
- package/CHANGELOG.md +0 -43
- package/source/assignment.ts +0 -27
- package/source/call.ts +0 -63
- package/source/cascade.ts +0 -32
- package/source/component.ts +0 -83
- package/source/composition.ts +0 -27
- package/source/connector.ts +0 -217
- package/source/context.ts +0 -68
- package/source/contract/contract.ts +0 -23
- package/source/contract/index.ts +0 -2
- package/source/contract/reply.ts +0 -56
- package/source/contract/request.ts +0 -140
- package/source/contract/schemas.ts +0 -39
- package/source/discovery.ts +0 -53
- package/source/effect.ts +0 -14
- package/source/emission.ts +0 -21
- package/source/entities/changeset.ts +0 -37
- package/source/entities/entity.ts +0 -137
- package/source/entities/factory.ts +0 -52
- package/source/entities/index.ts +0 -4
- package/source/entities/newid.ts +0 -9
- package/source/entities/set.ts +0 -27
- package/source/event.ts +0 -57
- package/source/exceptions.ts +0 -133
- package/source/exposition.ts +0 -33
- package/source/guard.ts +0 -16
- package/source/index.ts +0 -36
- package/source/locator.ts +0 -37
- package/source/observation.ts +0 -16
- package/source/operation.ts +0 -139
- package/source/outbox/index.ts +0 -1
- package/source/outbox/outbox.ts +0 -341
- package/source/query/criteria.ts +0 -90
- package/source/query/options.ts +0 -35
- package/source/query.ts +0 -67
- package/source/receiver.ts +0 -120
- package/source/reflection.ts +0 -20
- package/source/remote.ts +0 -20
- package/source/state.ts +0 -179
- package/source/transition.ts +0 -79
- package/source/transmission.ts +0 -38
- package/source/types/atomicity.ts +0 -63
- package/source/types/bindings.ts +0 -59
- package/source/types/bridges.ts +0 -54
- package/source/types/extensions.ts +0 -89
- package/source/types/index.ts +0 -14
- package/source/types/message.ts +0 -5
- package/source/types/operations.ts +0 -31
- package/source/types/outbox.ts +0 -35
- package/source/types/receiver.ts +0 -10
- package/source/types/request.ts +0 -54
- package/source/types/state.ts +0 -9
- package/source/types/storages.ts +0 -134
- package/source/unmanaged.ts +0 -8
- package/test/call.fixtures.js +0 -25
- package/test/call.test.js +0 -79
- package/test/component.fixtures.js +0 -18
- package/test/component.test.js +0 -57
- package/test/connector.fixtures.js +0 -56
- package/test/connector.test.js +0 -266
- package/test/context.fixtures.js +0 -14
- package/test/context.test.js +0 -40
- package/test/contract/conditions.test.js +0 -32
- package/test/contract/contract.fixtures.js +0 -28
- package/test/contract/request.test.js +0 -196
- package/test/discovery.test.js +0 -71
- package/test/emission.fixtures.js +0 -13
- package/test/emission.test.js +0 -60
- package/test/entities/entity.fixtures.js +0 -24
- package/test/entities/entity.test.js +0 -137
- package/test/entities/factory.fixtures.js +0 -31
- package/test/entities/factory.test.js +0 -99
- package/test/entities/set.fixtures.js +0 -9
- package/test/entities/set.test.js +0 -13
- package/test/event.fixtures.js +0 -24
- package/test/event.test.js +0 -162
- package/test/locator.test.js +0 -120
- package/test/outbox.test.js +0 -202
- package/test/query.fixtures.js +0 -96
- package/test/query.test.js +0 -183
- package/test/receiver.fixtures.js +0 -20
- package/test/receiver.test.js +0 -144
- package/test/reflection.test.js +0 -31
- package/test/state.fixtures.js +0 -49
- package/test/state.test.js +0 -162
- package/test/transmission.fixtures.js +0 -13
- package/test/transmission.test.js +0 -77
- package/transpiled/outbox/index.d.ts +0 -1
- package/transpiled/outbox/index.js +0 -2
- package/transpiled/outbox/index.js.map +0 -1
- package/transpiled/outbox/outbox.js +0 -268
- package/transpiled/outbox/outbox.js.map +0 -1
- package/tsconfig.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
package/source/connector.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { console } from 'openspan'
|
|
2
|
-
import { environment } from '@toa.io/generic'
|
|
3
|
-
import type { Locator } from './locator.js'
|
|
4
|
-
|
|
5
|
-
/** Abstract connections hierarchy */
|
|
6
|
-
export class Connector {
|
|
7
|
-
#dependencies: Connector[] = []
|
|
8
|
-
#links: Connector[] = []
|
|
9
|
-
#connecting: Promise<void> | undefined
|
|
10
|
-
#disconnecting: Promise<void> | undefined
|
|
11
|
-
#discarded: boolean = false
|
|
12
|
-
|
|
13
|
-
public readonly id: string
|
|
14
|
-
public connected: boolean = false
|
|
15
|
-
|
|
16
|
-
public constructor() {
|
|
17
|
-
this.id = this.constructor.name + '#' + Math.random().toString(36).substring(2, 8)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Creates a dependency and backlink with another Connector or a set of Connectors
|
|
22
|
-
*
|
|
23
|
-
* See .connect() and .disconnect()
|
|
24
|
-
*
|
|
25
|
-
*/
|
|
26
|
-
public depends(connector: Connector | Connector[]): Connector {
|
|
27
|
-
let next: Connector
|
|
28
|
-
|
|
29
|
-
if (connector instanceof Array) {
|
|
30
|
-
if (connector.length === 0) throw new Error('Connectors array must not be empty')
|
|
31
|
-
|
|
32
|
-
if (connector.length > 1) {
|
|
33
|
-
next = new Connector()
|
|
34
|
-
|
|
35
|
-
for (const item of connector) {
|
|
36
|
-
this.#dependencies.push(item)
|
|
37
|
-
item.depends(next)
|
|
38
|
-
}
|
|
39
|
-
} else next = connector[0]
|
|
40
|
-
} else next = connector
|
|
41
|
-
|
|
42
|
-
this.#dependencies.push(next)
|
|
43
|
-
next.link(this)
|
|
44
|
-
|
|
45
|
-
// See .#discard()
|
|
46
|
-
if (this.#disconnecting !== undefined) next.#discard()
|
|
47
|
-
|
|
48
|
-
return next
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Disconnects, now and again if a connection is still to land.
|
|
53
|
-
*
|
|
54
|
-
* A dependency taken on after its dependant was disconnected — a lookup that returns
|
|
55
|
-
* once the teardown walk has gone by — is one nothing is left to take down, as the
|
|
56
|
-
* walk that would have reached it is over. One that stays connected holds open
|
|
57
|
-
* everything it depends on with it: a dependency reads `connected` of every dependant
|
|
58
|
-
* before it closes, and a dependant that is connected for good is a dependency that
|
|
59
|
-
* never closes.
|
|
60
|
-
*/
|
|
61
|
-
#discard(): void {
|
|
62
|
-
this.#discarded = true
|
|
63
|
-
|
|
64
|
-
// one that is up already, or on its way, is taken down here; one that is not is
|
|
65
|
-
// taken down by the connection when it comes, and until then there is nothing to do
|
|
66
|
-
if (this.#connecting !== undefined) void this.#drop()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Creates a backlink to another Connector
|
|
71
|
-
*
|
|
72
|
-
* See .connect() and .disconnect()
|
|
73
|
-
*
|
|
74
|
-
*/
|
|
75
|
-
public link(connector: Connector): void {
|
|
76
|
-
this.#links.push(connector)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Connects dependants then self
|
|
81
|
-
*
|
|
82
|
-
* In case of exception disconnects with current connection interruption
|
|
83
|
-
*
|
|
84
|
-
* Method is idempotent
|
|
85
|
-
*
|
|
86
|
-
*/
|
|
87
|
-
public async connect(): Promise<void> {
|
|
88
|
-
if (this.#connecting) return this.#connecting
|
|
89
|
-
|
|
90
|
-
this.#disconnecting = undefined
|
|
91
|
-
|
|
92
|
-
const work = async () => {
|
|
93
|
-
await Promise.all(this.#dependencies.map((connector) => connector.connect()))
|
|
94
|
-
await this.open()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// anonymous grouping nodes are not worth a span
|
|
98
|
-
const connecting =
|
|
99
|
-
TRACE_BOOT && this.constructor.name !== 'Connector'
|
|
100
|
-
? console.span(
|
|
101
|
-
{
|
|
102
|
-
name: `connect ${this.constructor.name}`,
|
|
103
|
-
// a connector that has a locator is named by it; a grouping node has none
|
|
104
|
-
attributes: { id: (this as { locator?: Locator }).locator?.id ?? this.id }
|
|
105
|
-
},
|
|
106
|
-
work
|
|
107
|
-
)
|
|
108
|
-
: work()
|
|
109
|
-
|
|
110
|
-
this.#connecting = connecting
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await connecting
|
|
114
|
-
|
|
115
|
-
/*
|
|
116
|
-
* A disconnection that did not wait for this connection emptied `#connecting` to
|
|
117
|
-
* say so. Reporting `connected` now would report it of a connector nothing takes
|
|
118
|
-
* down again — its `#disconnecting` has settled, so a later call returns at once —
|
|
119
|
-
* and what it depends on reads that of every dependant before it closes.
|
|
120
|
-
*/
|
|
121
|
-
if (this.#connecting === connecting) this.connected = true
|
|
122
|
-
} catch (e) {
|
|
123
|
-
await this.disconnect(true)
|
|
124
|
-
throw e
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// a connection asked for before this one was discarded, landing after. See .#discard()
|
|
128
|
-
if (this.#discarded) await this.#drop()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Disconnects a discarded connector, saying so rather than throwing at whoever is not waiting. */
|
|
132
|
-
async #drop(): Promise<void> {
|
|
133
|
-
this.#discarded = false
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await this.disconnect()
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error('Discarded connector failed to disconnect', { id: this.id, error })
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Disconnects self then dependants
|
|
144
|
-
*
|
|
145
|
-
* Does nothing if there are connected linked Connectors
|
|
146
|
-
*
|
|
147
|
-
* Method is idempotent
|
|
148
|
-
*
|
|
149
|
-
*/
|
|
150
|
-
public async disconnect(interrupt?: boolean): Promise<void> {
|
|
151
|
-
// a connector that has not finished connecting has nothing to close, and
|
|
152
|
-
// awaiting a connection that may never settle outlives any grace period
|
|
153
|
-
const pending = interrupt === true || this.connected === false
|
|
154
|
-
|
|
155
|
-
if (!pending) await this.#connecting
|
|
156
|
-
|
|
157
|
-
if (this.#disconnecting) return this.#disconnecting
|
|
158
|
-
|
|
159
|
-
const linked = this.#links.reduce((acc, parent) => acc || parent.connected, false)
|
|
160
|
-
|
|
161
|
-
if (linked && interrupt !== true) return
|
|
162
|
-
|
|
163
|
-
this.#disconnecting = (async () => {
|
|
164
|
-
const start = +new Date()
|
|
165
|
-
|
|
166
|
-
const interval = setInterval(() => {
|
|
167
|
-
const delay = +new Date() - start
|
|
168
|
-
|
|
169
|
-
if (delay > DELAY)
|
|
170
|
-
console.warn(`Connector ${this.id} still disconnecting (${delay})`)
|
|
171
|
-
}, DELAY)
|
|
172
|
-
|
|
173
|
-
if (!pending) await this.close()
|
|
174
|
-
|
|
175
|
-
// said once the closing is done rather than when it starts: a dependant that is
|
|
176
|
-
// still closing is still using what it depends on, and the check above reads this
|
|
177
|
-
// of every dependant. Saying it early lets the first to start tear down a shared
|
|
178
|
-
// dependency under the others.
|
|
179
|
-
this.connected = false
|
|
180
|
-
this.#connecting = undefined
|
|
181
|
-
|
|
182
|
-
clearInterval(interval)
|
|
183
|
-
|
|
184
|
-
await Promise.all(this.#dependencies.map((connector) => connector.disconnect()))
|
|
185
|
-
|
|
186
|
-
await this.dispose()
|
|
187
|
-
})()
|
|
188
|
-
|
|
189
|
-
await this.#disconnecting
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
public async reconnect(): Promise<void> {
|
|
193
|
-
await this.disconnect()
|
|
194
|
-
await this.connect()
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
public debug(node: Record<string, any> = {}): Record<string, any> {
|
|
198
|
-
node[this.id] = { connected: this.connected }
|
|
199
|
-
|
|
200
|
-
if (this.#dependencies.length > 0)
|
|
201
|
-
for (const connector of this.#dependencies) connector.debug?.(node[this.id])
|
|
202
|
-
|
|
203
|
-
return node
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Called on connection */
|
|
207
|
-
protected open(): Promise<void> | void {}
|
|
208
|
-
|
|
209
|
-
/** Called on disconnection */
|
|
210
|
-
protected close(): Promise<void> | void {}
|
|
211
|
-
|
|
212
|
-
/** Called after self and dependants disconnection is complete */
|
|
213
|
-
protected dispose(): Promise<void> | void {}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const DELAY = 5000
|
|
217
|
-
const TRACE_BOOT = environment.get('TOA_BOOT_TRACE') === '1'
|
package/source/context.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { Connector } from './connector.js'
|
|
2
|
-
import { environment } from '@toa.io/generic'
|
|
3
|
-
import type { Locator } from './locator.js'
|
|
4
|
-
import type { Component } from './component.js'
|
|
5
|
-
import type { Remote } from './remote.js'
|
|
6
|
-
import type { Aspect } from './types/extensions.js'
|
|
7
|
-
import type { Request } from './types/request.js'
|
|
8
|
-
|
|
9
|
-
type Discover = (namespace: string, name: string) => Promise<Remote>
|
|
10
|
-
|
|
11
|
-
export class Context extends Connector {
|
|
12
|
-
public readonly env: string | undefined
|
|
13
|
-
public readonly name: string | undefined
|
|
14
|
-
public readonly aspects: Aspect[]
|
|
15
|
-
public readonly locator: Locator
|
|
16
|
-
|
|
17
|
-
readonly #local: Component
|
|
18
|
-
readonly #discover: Discover
|
|
19
|
-
readonly #remotes: Record<string, Promise<Remote>> = {}
|
|
20
|
-
|
|
21
|
-
public constructor(local: Component, discover: Discover, aspects: Aspect[] = []) {
|
|
22
|
-
super()
|
|
23
|
-
|
|
24
|
-
this.env = environment.get('TOA_ENV')
|
|
25
|
-
this.name = environment.get('TOA_CONTEXT')
|
|
26
|
-
this.aspects = aspects
|
|
27
|
-
this.locator = local?.locator
|
|
28
|
-
|
|
29
|
-
this.#local = local
|
|
30
|
-
this.#discover = discover
|
|
31
|
-
|
|
32
|
-
this.depends(local)
|
|
33
|
-
|
|
34
|
-
if (aspects.length > 0) this.depends(aspects)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public async apply(endpoint: string, request: Request): Promise<any> {
|
|
38
|
-
return this.#local.invoke(endpoint, request)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// eslint-disable-next-line max-params
|
|
42
|
-
public async call(
|
|
43
|
-
namespace: string,
|
|
44
|
-
name: string,
|
|
45
|
-
endpoint: string,
|
|
46
|
-
request: Request
|
|
47
|
-
): Promise<any> {
|
|
48
|
-
const remote = await this.#remote(namespace, name)
|
|
49
|
-
|
|
50
|
-
return remote.invoke(endpoint, request)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async #remote(namespace: string, name: string): Promise<Remote> {
|
|
54
|
-
const key = namespace + '.' + name
|
|
55
|
-
|
|
56
|
-
this.#remotes[key] ??= this.#connect(namespace, name)
|
|
57
|
-
|
|
58
|
-
return this.#remotes[key]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async #connect(namespace: string, name: string): Promise<Remote> {
|
|
62
|
-
const remote = await this.#discover(namespace, name)
|
|
63
|
-
|
|
64
|
-
this.depends(remote)
|
|
65
|
-
|
|
66
|
-
return remote
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|
package/source/contract/index.ts
DELETED
package/source/contract/reply.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Contract } from './contract.js'
|
|
2
|
-
import { ResponseContractException } from '../exceptions.js'
|
|
3
|
-
import type { Refusal } from './contract.js'
|
|
4
|
-
import type { JSONSchema } from './schemas.js'
|
|
5
|
-
|
|
6
|
-
export class Reply extends Contract {
|
|
7
|
-
public static override Exception: Refusal =
|
|
8
|
-
ResponseContractException as unknown as Refusal
|
|
9
|
-
|
|
10
|
-
public static schema(output?: JSONSchema, errors?: Array<string | number>): JSONSchema {
|
|
11
|
-
const schema: JSONSchema = {
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {},
|
|
14
|
-
additionalProperties: false
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (output !== undefined) {
|
|
18
|
-
/*
|
|
19
|
-
* A reply carries more than the operation declares — `VERSION` and the rest of the
|
|
20
|
-
* record's own fields — so what it is validated against is the declaration relaxed.
|
|
21
|
-
* On a copy: the declaration itself is what a component publishes when asked to
|
|
22
|
-
* explain, and relaxing that would publish a contract nobody wrote.
|
|
23
|
-
*/
|
|
24
|
-
output = structuredClone(output)
|
|
25
|
-
|
|
26
|
-
if (output.type === 'object') output.additionalProperties = true
|
|
27
|
-
else if (output.type === 'array' && output.items?.type === 'object')
|
|
28
|
-
output.items.additionalProperties = true
|
|
29
|
-
|
|
30
|
-
schema.properties.output = output
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/*
|
|
34
|
-
* An error a caller is meant to handle is one the operation states. Where none are
|
|
35
|
-
* stated, an error is not a reply this operation makes — it is a mistake, and the
|
|
36
|
-
* contract says so rather than passing an undeclared code on to whoever called.
|
|
37
|
-
*/
|
|
38
|
-
schema.properties.error =
|
|
39
|
-
errors === undefined
|
|
40
|
-
? false
|
|
41
|
-
: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
code: {
|
|
45
|
-
enum: errors
|
|
46
|
-
},
|
|
47
|
-
message: {
|
|
48
|
-
type: 'string'
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
required: ['code']
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return schema
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import * as schemas from './schemas.js'
|
|
2
|
-
import { RequestContractException } from '../exceptions.js'
|
|
3
|
-
import { Contract } from './contract.js'
|
|
4
|
-
import type { Schema } from '@toa.io/schemas'
|
|
5
|
-
import type { Refusal } from './contract.js'
|
|
6
|
-
import type { JSONSchema } from './schemas.js'
|
|
7
|
-
|
|
8
|
-
/** What an operation states about itself, and answers when asked to explain. */
|
|
9
|
-
export interface Explanation {
|
|
10
|
-
description?: string
|
|
11
|
-
input?: JSONSchema | null
|
|
12
|
-
output?: JSONSchema | null
|
|
13
|
-
errors?: Array<string | number>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface Definition extends Explanation {
|
|
17
|
-
type?: string
|
|
18
|
-
scope?: string
|
|
19
|
-
query?: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** What the operation works on, as the manifest declares it. */
|
|
23
|
-
export interface Entity {
|
|
24
|
-
properties: Record<string, JSONSchema>
|
|
25
|
-
required?: string[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class Request extends Contract {
|
|
29
|
-
public readonly discovery: Explanation = {}
|
|
30
|
-
|
|
31
|
-
public static override Exception: Refusal =
|
|
32
|
-
RequestContractException as unknown as Refusal
|
|
33
|
-
|
|
34
|
-
public constructor(schema: Schema, definition: Definition, entity?: Entity) {
|
|
35
|
-
super(schema)
|
|
36
|
-
|
|
37
|
-
for (const key of ['description', 'input', 'output', 'errors'] as const)
|
|
38
|
-
if (definition[key] !== undefined)
|
|
39
|
-
(this.discovery as Record<string, unknown>)[key] = definition[key]
|
|
40
|
-
|
|
41
|
-
/*
|
|
42
|
-
* An operation that states no output states an empty schema, which every reply fits —
|
|
43
|
-
* and which says nothing to whoever reads it. What its type answers does, so that is
|
|
44
|
-
* said here instead. Here and not in the definition, because the definition is what a
|
|
45
|
-
* reply is held to: a projection is a lawful answer, and describing one is not the same
|
|
46
|
-
* as requiring it.
|
|
47
|
-
*/
|
|
48
|
-
if (empty(this.discovery.output)) {
|
|
49
|
-
const answered = answers(definition, entity)
|
|
50
|
-
|
|
51
|
-
if (answered !== undefined) this.discovery.output = answered
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
public static schema(
|
|
56
|
-
definition: Definition,
|
|
57
|
-
entity?: { properties: Record<string, JSONSchema> }
|
|
58
|
-
): JSONSchema {
|
|
59
|
-
// `source` is not among these: the framework stamps it and whoever reads it takes the keys
|
|
60
|
-
// it knows, so holding it to a schema would only fail calls from a peer that stamps one more
|
|
61
|
-
const schema: JSONSchema = {
|
|
62
|
-
type: 'object',
|
|
63
|
-
properties: {
|
|
64
|
-
authentic: { type: 'boolean' },
|
|
65
|
-
task: { type: 'boolean' }
|
|
66
|
-
},
|
|
67
|
-
additionalProperties: true
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const required: string[] = []
|
|
71
|
-
|
|
72
|
-
if (definition.input !== undefined) {
|
|
73
|
-
schema.properties.input = definition.input
|
|
74
|
-
required.push('input')
|
|
75
|
-
} else schema.properties.input = { type: 'null' }
|
|
76
|
-
|
|
77
|
-
if (entity === undefined) definition.query = false
|
|
78
|
-
|
|
79
|
-
if (definition.query === true) required.push('query')
|
|
80
|
-
|
|
81
|
-
if (definition.query === false) schema.properties.query = { type: 'null' }
|
|
82
|
-
|
|
83
|
-
if (definition.query !== false) {
|
|
84
|
-
const query = structuredClone(schemas.query)
|
|
85
|
-
|
|
86
|
-
query.properties.id = entity?.properties.id
|
|
87
|
-
|
|
88
|
-
if (definition.type === 'observation') delete query.properties.version
|
|
89
|
-
else delete query.properties.projection
|
|
90
|
-
|
|
91
|
-
if (definition.type !== 'observation' || definition.scope !== 'objects') {
|
|
92
|
-
delete query.properties.omit
|
|
93
|
-
delete query.properties.limit
|
|
94
|
-
} else if (query.required === undefined) query.required = ['limit']
|
|
95
|
-
else query.required.push('limit')
|
|
96
|
-
|
|
97
|
-
schema.properties.query = query
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (required.length > 0) schema.required = required
|
|
101
|
-
|
|
102
|
-
return schema
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* What an operation of this type answers, where it did not say.
|
|
108
|
-
*
|
|
109
|
-
* A transition, an observation and an assignment work on the Entity Object, so that is what
|
|
110
|
-
* they answer — a set of them where the scope is a set. A computation neither uses the scope
|
|
111
|
-
* nor produces one, and an effect answers whatever it computed, so neither is guessed at.
|
|
112
|
-
* A stream is not a value with a schema, and is not one either.
|
|
113
|
-
*
|
|
114
|
-
* What the entity requires is said as well. It is a description and nothing is held to it —
|
|
115
|
-
* an operation answering a projection says so by declaring its own output — and a shape
|
|
116
|
-
* whose every property reads as optional says less than it knows.
|
|
117
|
-
*/
|
|
118
|
-
function answers(definition: Definition, entity?: Entity): JSONSchema | undefined {
|
|
119
|
-
if (entity === undefined || !ENTITY.has(definition.type as string)) return undefined
|
|
120
|
-
|
|
121
|
-
const object: JSONSchema = {
|
|
122
|
-
type: 'object',
|
|
123
|
-
properties: entity.properties,
|
|
124
|
-
...(entity.required === undefined ? {} : { required: entity.required })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (definition.scope === 'objects') return { type: 'array', items: object } as JSONSchema
|
|
128
|
-
|
|
129
|
-
return definition.scope === 'object' || definition.scope === 'changeset'
|
|
130
|
-
? object
|
|
131
|
-
: undefined
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** The types whose subject is the Entity Object; see `documentation/design.md`. */
|
|
135
|
-
const ENTITY = new Set(['transition', 'observation', 'assignment'])
|
|
136
|
-
|
|
137
|
-
/** A schema that states nothing, which is what an operation saying nothing normalizes to. */
|
|
138
|
-
function empty(schema: JSONSchema | null | undefined): boolean {
|
|
139
|
-
return schema === undefined || schema === null || Object.keys(schema).length === 0
|
|
140
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
package/source/discovery.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { console } from 'openspan'
|
|
2
|
-
import { Connector } from './connector.js'
|
|
3
|
-
import type { Locator } from './locator.js'
|
|
4
|
-
|
|
5
|
-
interface Lookup extends Connector {
|
|
6
|
-
invoke: () => Promise<any>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class Discovery extends Connector {
|
|
10
|
-
readonly #lookup: (locator: Locator) => Promise<Lookup>
|
|
11
|
-
#lookups: Record<string, Promise<Lookup>> = {}
|
|
12
|
-
|
|
13
|
-
public constructor(lookup: (locator: Locator) => Promise<Lookup>) {
|
|
14
|
-
super()
|
|
15
|
-
|
|
16
|
-
this.#lookup = lookup
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
protected override async open(): Promise<void> {
|
|
20
|
-
this.#lookups = {}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public async lookup(locator: Locator): Promise<any> {
|
|
24
|
-
const id = locator.id
|
|
25
|
-
|
|
26
|
-
if (this.#lookups[id] === undefined) {
|
|
27
|
-
const lookup = await this.#lookup(locator)
|
|
28
|
-
|
|
29
|
-
this.#lookups[id] = Promise.resolve(lookup)
|
|
30
|
-
this.depends(lookup)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const since = Date.now()
|
|
34
|
-
|
|
35
|
-
// the wait is unbounded by design, as a dependency may still be starting,
|
|
36
|
-
// so repeating is the only way a component stuck on one stays visible
|
|
37
|
-
const warning = setInterval(() => {
|
|
38
|
-
const waiting = Math.round((Date.now() - since) / 1000)
|
|
39
|
-
|
|
40
|
-
console.warn('Waiting for lookup response', { component: id, waiting })
|
|
41
|
-
}, INTERVAL)
|
|
42
|
-
|
|
43
|
-
warning.unref()
|
|
44
|
-
|
|
45
|
-
const output = await (await this.#lookups[id]).invoke()
|
|
46
|
-
|
|
47
|
-
clearInterval(warning)
|
|
48
|
-
|
|
49
|
-
return output
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const INTERVAL = 5000
|
package/source/effect.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|
package/source/emission.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Connector } from './connector.js'
|
|
2
|
-
import type { Event } from './event.js'
|
|
3
|
-
import type { Event as StateEvent } from './types/state.js'
|
|
4
|
-
|
|
5
|
-
export class Emission extends Connector {
|
|
6
|
-
readonly #events: Event[]
|
|
7
|
-
|
|
8
|
-
public constructor(events: Event[]) {
|
|
9
|
-
super()
|
|
10
|
-
|
|
11
|
-
this.#events = events
|
|
12
|
-
|
|
13
|
-
this.depends(events)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public async emit(event: StateEvent): Promise<void> {
|
|
17
|
-
const emission = this.#events.map((e) => e.emit(event))
|
|
18
|
-
|
|
19
|
-
await Promise.all(emission)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
}
|