@toa.io/core 1.0.0-alpha.284 → 1.0.0-alpha.287
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -185
- package/package.json +19 -7
- package/source/assignment.ts +27 -0
- package/{src/call.js → source/call.ts} +16 -21
- package/{src/cascade.js → source/cascade.ts} +6 -8
- package/{src/component.js → source/component.ts} +27 -25
- package/{src/composition.js → source/composition.ts} +9 -3
- package/{src/connector.js → source/connector.ts} +36 -55
- package/source/context.ts +67 -0
- package/source/contract/contract.ts +23 -0
- package/{src/contract/reply.js → source/contract/reply.ts} +26 -19
- package/source/contract/request.ts +85 -0
- package/source/contract/schemas.ts +39 -0
- package/{src/discovery.js → source/discovery.ts} +15 -8
- package/source/effect.ts +14 -0
- package/{src/emission.js → source/emission.ts} +5 -3
- package/source/entities/changeset.ts +37 -0
- package/source/entities/entity.ts +137 -0
- package/source/entities/factory.ts +52 -0
- package/source/entities/index.ts +4 -0
- package/{src/entities/newid.js → source/entities/newid.ts} +1 -1
- package/{src/entities/set.js → source/entities/set.ts} +8 -5
- package/source/event.ts +57 -0
- package/{src/exceptions.js → source/exceptions.ts} +52 -33
- package/source/exposition.ts +33 -0
- package/source/guard.ts +16 -0
- package/{src/index.js → source/index.ts} +8 -0
- package/{src/locator.js → source/locator.ts} +17 -30
- package/source/observation.ts +16 -0
- package/source/operation.ts +138 -0
- package/{src/outbox/outbox.js → source/outbox/outbox.ts} +87 -53
- package/source/query/criteria.ts +90 -0
- package/source/query/options.ts +35 -0
- package/{src/query.js → source/query.ts} +14 -17
- package/source/receiver.ts +120 -0
- package/source/reflection.ts +20 -0
- package/source/remote.ts +20 -0
- package/source/state.ts +179 -0
- package/source/transition.ts +79 -0
- package/{src/transmission.js → source/transmission.ts} +8 -8
- package/source/types/atomicity.ts +63 -0
- package/source/types/bindings.ts +59 -0
- package/source/types/bridges.ts +54 -0
- package/source/types/extensions.ts +89 -0
- package/{types → source/types}/index.ts +9 -16
- package/{types/message.d.ts → source/types/message.ts} +2 -1
- package/source/types/operations.ts +31 -0
- package/source/types/outbox.ts +35 -0
- package/source/types/receiver.ts +10 -0
- package/{types/request.d.ts → source/types/request.ts} +8 -9
- package/source/types/state.ts +9 -0
- package/source/types/storages.ts +129 -0
- package/source/unmanaged.ts +8 -0
- package/test/call.fixtures.js +3 -1
- package/test/call.test.js +28 -7
- package/test/component.test.js +10 -5
- package/test/connector.fixtures.js +7 -7
- package/test/connector.test.js +15 -2
- package/test/context.fixtures.js +1 -1
- package/test/context.test.js +4 -4
- package/test/contract/conditions.test.js +6 -2
- package/test/contract/contract.fixtures.js +3 -4
- package/test/contract/request.test.js +68 -34
- package/test/discovery.test.js +10 -3
- package/test/emission.test.js +16 -8
- package/test/entities/entity.fixtures.js +5 -6
- package/test/entities/entity.test.js +54 -20
- package/test/entities/factory.fixtures.js +11 -10
- package/test/entities/factory.test.js +47 -10
- package/test/entities/set.test.js +1 -1
- package/test/event.test.js +29 -11
- package/test/locator.test.js +10 -4
- package/test/outbox.test.js +49 -15
- package/test/query.test.js +69 -7
- package/test/receiver.fixtures.js +2 -2
- package/test/receiver.test.js +53 -27
- package/test/reflection.test.js +3 -3
- package/test/state.fixtures.js +10 -3
- package/test/state.test.js +77 -14
- package/test/transmission.test.js +25 -12
- package/transpiled/assignment.d.ts +6 -0
- package/transpiled/assignment.js +20 -0
- package/transpiled/assignment.js.map +1 -0
- package/transpiled/call.d.ts +10 -0
- package/transpiled/call.js +52 -0
- package/transpiled/call.js.map +1 -0
- package/transpiled/cascade.d.ts +7 -0
- package/transpiled/cascade.js +28 -0
- package/transpiled/cascade.js.map +1 -0
- package/transpiled/component.d.ts +15 -0
- package/transpiled/component.js +61 -0
- package/transpiled/component.js.map +1 -0
- package/transpiled/composition.d.ts +6 -0
- package/transpiled/composition.js +23 -0
- package/transpiled/composition.js.map +1 -0
- package/transpiled/connector.d.ts +47 -0
- package/transpiled/connector.js +143 -0
- package/transpiled/connector.js.map +1 -0
- package/transpiled/context.d.ts +18 -0
- package/transpiled/context.js +41 -0
- package/transpiled/context.js.map +1 -0
- package/transpiled/contract/contract.d.ts +10 -0
- package/transpiled/contract/contract.js +14 -0
- package/transpiled/contract/contract.js.map +1 -0
- package/transpiled/contract/index.d.ts +2 -0
- package/transpiled/contract/index.js +3 -0
- package/transpiled/contract/index.js.map +1 -0
- package/transpiled/contract/reply.d.ts +7 -0
- package/transpiled/contract/reply.js +48 -0
- package/transpiled/contract/reply.js.map +1 -0
- package/transpiled/contract/request.d.ts +24 -0
- package/transpiled/contract/request.js +59 -0
- package/transpiled/contract/request.js.map +1 -0
- package/transpiled/contract/schemas.d.ts +4 -0
- package/transpiled/contract/schemas.js +36 -0
- package/transpiled/contract/schemas.js.map +1 -0
- package/transpiled/discovery.d.ts +12 -0
- package/transpiled/discovery.js +34 -0
- package/transpiled/discovery.js.map +1 -0
- package/transpiled/effect.d.ts +5 -0
- package/transpiled/effect.js +11 -0
- package/transpiled/effect.js.map +1 -0
- package/transpiled/emission.d.ts +8 -0
- package/transpiled/emission.js +14 -0
- package/transpiled/emission.js.map +1 -0
- package/transpiled/entities/changeset.d.ts +10 -0
- package/transpiled/entities/changeset.js +27 -0
- package/transpiled/entities/changeset.js.map +1 -0
- package/transpiled/entities/entity.d.ts +19 -0
- package/transpiled/entities/entity.js +109 -0
- package/transpiled/entities/entity.js.map +1 -0
- package/transpiled/entities/factory.d.ts +22 -0
- package/transpiled/entities/factory.js +34 -0
- package/transpiled/entities/factory.js.map +1 -0
- package/transpiled/entities/index.d.ts +4 -0
- package/transpiled/entities/index.js +5 -0
- package/transpiled/entities/index.js.map +1 -0
- package/transpiled/entities/newid.d.ts +1 -0
- package/transpiled/entities/newid.js +7 -0
- package/transpiled/entities/newid.js.map +1 -0
- package/transpiled/entities/set.d.ts +10 -0
- package/transpiled/entities/set.js +19 -0
- package/transpiled/entities/set.js.map +1 -0
- package/transpiled/event.d.ts +14 -0
- package/transpiled/event.js +38 -0
- package/transpiled/event.js.map +1 -0
- package/transpiled/exceptions.d.ts +61 -0
- package/transpiled/exceptions.js +93 -0
- package/transpiled/exceptions.js.map +1 -0
- package/transpiled/exposition.d.ts +18 -0
- package/transpiled/exposition.js +18 -0
- package/transpiled/exposition.js.map +1 -0
- package/transpiled/guard.d.ts +7 -0
- package/transpiled/guard.js +12 -0
- package/transpiled/guard.js.map +1 -0
- package/transpiled/index.d.ts +35 -0
- package/transpiled/index.js +29 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/locator.d.ts +12 -0
- package/transpiled/locator.js +32 -0
- package/transpiled/locator.js.map +1 -0
- package/transpiled/observation.d.ts +5 -0
- package/transpiled/observation.js +12 -0
- package/transpiled/observation.js.map +1 -0
- package/transpiled/operation.d.ts +49 -0
- package/transpiled/operation.js +77 -0
- package/transpiled/operation.js.map +1 -0
- package/transpiled/outbox/index.d.ts +1 -0
- package/transpiled/outbox/index.js +2 -0
- package/transpiled/outbox/index.js.map +1 -0
- package/transpiled/outbox/outbox.d.ts +46 -0
- package/transpiled/outbox/outbox.js +267 -0
- package/transpiled/outbox/outbox.js.map +1 -0
- package/transpiled/query/criteria.d.ts +6 -0
- package/transpiled/query/criteria.js +74 -0
- package/transpiled/query/criteria.js.map +1 -0
- package/transpiled/query/options.d.ts +3 -0
- package/transpiled/query/options.js +27 -0
- package/transpiled/query/options.js.map +1 -0
- package/transpiled/query.d.ts +9 -0
- package/transpiled/query.js +57 -0
- package/transpiled/query.js.map +1 -0
- package/transpiled/receiver.d.ts +20 -0
- package/transpiled/receiver.js +92 -0
- package/transpiled/receiver.js.map +1 -0
- package/transpiled/reflection.d.ts +9 -0
- package/transpiled/reflection.js +13 -0
- package/transpiled/reflection.js.map +1 -0
- package/transpiled/remote.d.ts +10 -0
- package/transpiled/remote.js +12 -0
- package/transpiled/remote.js.map +1 -0
- package/transpiled/state.d.ts +33 -0
- package/transpiled/state.js +126 -0
- package/transpiled/state.js.map +1 -0
- package/transpiled/transition.d.ts +14 -0
- package/transpiled/transition.js +51 -0
- package/transpiled/transition.js.map +1 -0
- package/transpiled/transmission.d.ts +8 -0
- package/transpiled/transmission.js +30 -0
- package/transpiled/transmission.js.map +1 -0
- package/transpiled/types/atomicity.d.ts +56 -0
- package/transpiled/types/atomicity.js +2 -0
- package/transpiled/types/atomicity.js.map +1 -0
- package/transpiled/types/bindings.d.ts +42 -0
- package/transpiled/types/bindings.js +2 -0
- package/transpiled/types/bindings.js.map +1 -0
- package/transpiled/types/bridges.d.ts +38 -0
- package/transpiled/types/bridges.js +2 -0
- package/transpiled/types/bridges.js.map +1 -0
- package/transpiled/types/extensions.d.ts +62 -0
- package/transpiled/types/extensions.js +2 -0
- package/transpiled/types/extensions.js.map +1 -0
- package/transpiled/types/index.d.ts +13 -0
- package/transpiled/types/index.js +8 -0
- package/transpiled/types/index.js.map +1 -0
- package/transpiled/types/message.d.ts +5 -0
- package/transpiled/types/message.js +2 -0
- package/transpiled/types/message.js.map +1 -0
- package/transpiled/types/operations.d.ts +15 -0
- package/transpiled/types/operations.js +2 -0
- package/transpiled/types/operations.js.map +1 -0
- package/transpiled/types/outbox.d.ts +28 -0
- package/transpiled/types/outbox.js +2 -0
- package/transpiled/types/outbox.js.map +1 -0
- package/transpiled/types/receiver.d.ts +9 -0
- package/transpiled/types/receiver.js +2 -0
- package/transpiled/types/receiver.js.map +1 -0
- package/transpiled/types/request.d.ts +55 -0
- package/transpiled/types/request.js +2 -0
- package/transpiled/types/request.js.map +1 -0
- package/transpiled/types/state.d.ts +9 -0
- package/transpiled/types/state.js +2 -0
- package/transpiled/types/state.js.map +1 -0
- package/transpiled/types/storages.d.ts +106 -0
- package/transpiled/types/storages.js +2 -0
- package/transpiled/types/storages.js.map +1 -0
- package/transpiled/unmanaged.d.ts +5 -0
- package/transpiled/unmanaged.js +7 -0
- package/transpiled/unmanaged.js.map +1 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/src/assignment.js +0 -28
- package/src/context.js +0 -57
- package/src/contract/contract.js +0 -18
- package/src/contract/request.js +0 -77
- package/src/contract/schemas/error.yaml +0 -9
- package/src/contract/schemas/index.js +0 -11
- package/src/contract/schemas/query.yaml +0 -43
- package/src/contract/schemas/source.yaml +0 -23
- package/src/effect.js +0 -15
- package/src/entities/changeset.js +0 -36
- package/src/entities/entity.js +0 -119
- package/src/entities/factory.js +0 -40
- package/src/entities/index.js +0 -1
- package/src/event.js +0 -54
- package/src/exposition.js +0 -24
- package/src/guard.js +0 -13
- package/src/observation.js +0 -8
- package/src/operation.js +0 -95
- package/src/query/criteria.js +0 -44
- package/src/query/options.js +0 -37
- package/src/receiver.js +0 -123
- package/src/reflection.js +0 -24
- package/src/remote.js +0 -14
- package/src/state.js +0 -138
- package/src/transition.js +0 -61
- package/src/unmanaged.js +0 -7
- package/types/atomicity.d.ts +0 -79
- package/types/bindings.d.ts +0 -42
- package/types/bridges.ts +0 -33
- package/types/call.d.ts +0 -13
- package/types/component.d.ts +0 -12
- package/types/connector.d.ts +0 -18
- package/types/context.d.ts +0 -24
- package/types/entity.d.ts +0 -46
- package/types/event.d.ts +0 -11
- package/types/exception.d.ts +0 -4
- package/types/extensions.d.ts +0 -73
- package/types/guard.d.ts +0 -5
- package/types/locator.d.ts +0 -13
- package/types/operations.d.ts +0 -8
- package/types/outbox.d.ts +0 -53
- package/types/query.d.ts +0 -17
- package/types/receiver.d.ts +0 -12
- package/types/reflection.d.ts +0 -14
- package/types/remote.d.ts +0 -19
- package/types/state.d.ts +0 -38
- package/types/storages.d.ts +0 -96
- /package/{src/contract/index.js → source/contract/index.ts} +0 -0
- /package/{src/outbox/index.js → source/outbox/index.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { QuerySyntaxException } from '../exceptions.js'
|
|
2
|
+
import type { Options } from '../types/storages.js'
|
|
3
|
+
import type { Properties } from './criteria.js'
|
|
4
|
+
|
|
5
|
+
export function options(given: Record<string, any>, properties: Properties): Options {
|
|
6
|
+
if (given.sort !== undefined) given.sort = sort(given.sort, properties)
|
|
7
|
+
|
|
8
|
+
if (given.projection !== undefined) projection(given.projection, properties)
|
|
9
|
+
|
|
10
|
+
return given as Options
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sort(sort: string[], properties: Properties): Array<[string, string]> {
|
|
14
|
+
const result: Array<[string, string]> = []
|
|
15
|
+
|
|
16
|
+
for (const sorting of sort) {
|
|
17
|
+
const [property, direction] = sorting.split(':')
|
|
18
|
+
|
|
19
|
+
if (properties[property] === undefined)
|
|
20
|
+
throw new QuerySyntaxException(`Sort property '${property}' is not defined`)
|
|
21
|
+
|
|
22
|
+
result.push([property, direction ?? 'asc'])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function projection(projection: string[], properties: Properties): void {
|
|
29
|
+
for (const property of projection)
|
|
30
|
+
if (properties[property] === undefined)
|
|
31
|
+
throw new QuerySyntaxException(`Projection property '${property}' is not defined`)
|
|
32
|
+
|
|
33
|
+
for (const property of ['VERSION', 'CREATED', 'UPDATED', 'DELETED'])
|
|
34
|
+
if (!projection.includes(property)) projection.push(property)
|
|
35
|
+
}
|
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import { empty } from '@toa.io/generic'
|
|
2
2
|
import * as criteria from './query/criteria.js'
|
|
3
3
|
import * as options from './query/options.js'
|
|
4
|
+
import type { Properties } from './query/criteria.js'
|
|
5
|
+
import type { Node, Options, Query as Parsed } from './types/storages.js'
|
|
6
|
+
import type { Query as Requested } from './types/request.js'
|
|
4
7
|
|
|
5
8
|
const parse = { ...criteria, ...options }
|
|
6
9
|
|
|
10
|
+
/** Translates the query a request carries into the one a storage is given. */
|
|
7
11
|
export class Query {
|
|
8
|
-
#properties
|
|
9
|
-
#system
|
|
12
|
+
readonly #properties: Properties
|
|
10
13
|
|
|
11
|
-
/**
|
|
12
|
-
#asts = new Map()
|
|
14
|
+
/** parsed criteria by their expression */
|
|
15
|
+
readonly #asts = new Map<string, Node>()
|
|
13
16
|
|
|
14
|
-
constructor
|
|
17
|
+
public constructor(properties: Properties) {
|
|
15
18
|
this.#properties = properties
|
|
16
|
-
this.#system = Object.keys(properties).filter((key) => properties[key].system === true)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @returns {toa.core.storages.Query}
|
|
22
|
-
*/
|
|
23
|
-
parse (query) {
|
|
24
|
-
/** @type {toa.core.storages.Query} */
|
|
25
|
-
const result = {}
|
|
21
|
+
public parse(query: Requested): Parsed {
|
|
22
|
+
const result: Parsed = {}
|
|
26
23
|
const { id, ids, version, criteria, search, ...rest } = query
|
|
27
24
|
|
|
28
25
|
const options = this.#options(rest)
|
|
@@ -37,10 +34,10 @@ export class Query {
|
|
|
37
34
|
return result
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
#options
|
|
41
|
-
if (empty(
|
|
37
|
+
#options(given: Record<string, any>): Options | undefined {
|
|
38
|
+
if (empty(given)) return undefined
|
|
42
39
|
|
|
43
|
-
return parse.options(
|
|
40
|
+
return parse.options(given, this.#properties)
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/**
|
|
@@ -52,7 +49,7 @@ export class Query {
|
|
|
52
49
|
* Expressions come from the client, hence the bound. An invalid one throws before it
|
|
53
50
|
* reaches the cache.
|
|
54
51
|
*/
|
|
55
|
-
#criteria
|
|
52
|
+
#criteria(criteria: string): Node {
|
|
56
53
|
const known = this.#asts.get(criteria)
|
|
57
54
|
|
|
58
55
|
if (known !== undefined) return known
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { console, decode, run, type SpanOptions } from 'openspan'
|
|
2
|
+
import { add } from '@toa.io/generic'
|
|
3
|
+
import { Connector } from './connector.js'
|
|
4
|
+
import type { Component } from './component.js'
|
|
5
|
+
import type { Receiver as Bridge } from './types/bridges.js'
|
|
6
|
+
import type { Message } from './types/message.js'
|
|
7
|
+
import type { Request, Source } from './types/request.js'
|
|
8
|
+
|
|
9
|
+
export interface Definition {
|
|
10
|
+
conditioned?: boolean
|
|
11
|
+
adaptive?: boolean
|
|
12
|
+
operation: string
|
|
13
|
+
label?: string
|
|
14
|
+
destination?: string
|
|
15
|
+
arguments?: unknown[]
|
|
16
|
+
origin?: Source
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Receiver extends Connector {
|
|
20
|
+
readonly #conditioned: boolean | undefined
|
|
21
|
+
readonly #adaptive: boolean | undefined
|
|
22
|
+
readonly #endpoint: string
|
|
23
|
+
readonly #label: string
|
|
24
|
+
readonly #destination: string
|
|
25
|
+
readonly #arguments: unknown[] | undefined
|
|
26
|
+
readonly #origin: Source | undefined
|
|
27
|
+
readonly #local: Component
|
|
28
|
+
readonly #bridge: Bridge | undefined
|
|
29
|
+
readonly #delivery: SpanOptions
|
|
30
|
+
readonly #processing: SpanOptions
|
|
31
|
+
|
|
32
|
+
public constructor(definition: Definition, local: Component, bridge?: Bridge) {
|
|
33
|
+
super()
|
|
34
|
+
|
|
35
|
+
const { conditioned, adaptive, operation } = definition
|
|
36
|
+
|
|
37
|
+
this.#conditioned = conditioned
|
|
38
|
+
this.#adaptive = adaptive
|
|
39
|
+
this.#endpoint = operation
|
|
40
|
+
this.#label = definition.label ?? operation
|
|
41
|
+
this.#destination = definition.destination ?? this.#label
|
|
42
|
+
this.#arguments = definition.arguments
|
|
43
|
+
this.#origin = definition.origin
|
|
44
|
+
|
|
45
|
+
this.#local = local
|
|
46
|
+
this.#bridge = bridge
|
|
47
|
+
|
|
48
|
+
this.depends(local)
|
|
49
|
+
if (bridge !== undefined) this.depends(bridge)
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* The delivery span is created on behalf of the messaging destination, so that
|
|
53
|
+
* each consumer forms its own complete producer/consumer pair, and service graphs
|
|
54
|
+
* display fan-out correctly: producer -> destination -> each consumer
|
|
55
|
+
* (Tempo pairs spans one-to-one, thus multiple consumers can not pair
|
|
56
|
+
* with a single producer span, see grafana/tempo#5408)
|
|
57
|
+
*/
|
|
58
|
+
this.#delivery = {
|
|
59
|
+
name: `${this.#label} deliver`,
|
|
60
|
+
kind: 'producer',
|
|
61
|
+
service: this.#destination,
|
|
62
|
+
attributes: { 'messaging.destination.name': this.#destination }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.#processing = {
|
|
66
|
+
name: `${this.#label} process`,
|
|
67
|
+
kind: 'consumer',
|
|
68
|
+
service: local.locator.id,
|
|
69
|
+
attributes: { 'messaging.destination.name': this.#destination }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @hot */
|
|
74
|
+
public async receive(message: Message): Promise<void> {
|
|
75
|
+
const { payload, telemetry, ...extensions } = message
|
|
76
|
+
|
|
77
|
+
if (this.#conditioned === true && (await this.#bridge?.condition(payload)) === false)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
const request = await this.#request(payload)
|
|
81
|
+
|
|
82
|
+
add(request, extensions)
|
|
83
|
+
|
|
84
|
+
// set after `add`, so that a message field can not spoof the origin
|
|
85
|
+
if (this.#origin !== undefined) request.source = this.#origin
|
|
86
|
+
|
|
87
|
+
// continue the trace from the producer span
|
|
88
|
+
const remote = telemetry === undefined ? null : decode(telemetry)
|
|
89
|
+
const task = async (): Promise<void> => {
|
|
90
|
+
await this.#process(request)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (remote === null) await task()
|
|
94
|
+
else await run(remote, task)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async #process(request: Request): Promise<void> {
|
|
98
|
+
return console.span(this.#delivery, async () =>
|
|
99
|
+
console.span(this.#processing, async () => {
|
|
100
|
+
try {
|
|
101
|
+
await this.#local.invoke(this.#endpoint, request)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Receiver error', {
|
|
104
|
+
component: this.#local.locator.id,
|
|
105
|
+
endpoint: this.#endpoint,
|
|
106
|
+
error
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async #request(payload: object): Promise<Request> {
|
|
116
|
+
return this.#adaptive === true
|
|
117
|
+
? await this.#bridge!.request(payload, ...(this.#arguments ?? []))
|
|
118
|
+
: { input: payload }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Connector } from './connector.js'
|
|
2
|
+
|
|
3
|
+
/** What a reflection reads, once, as it connects. */
|
|
4
|
+
export type Source<T = any> = () => Promise<T>
|
|
5
|
+
|
|
6
|
+
export class Reflection<T = any> extends Connector {
|
|
7
|
+
public value: T | undefined
|
|
8
|
+
|
|
9
|
+
readonly #source: Source<T>
|
|
10
|
+
|
|
11
|
+
public constructor(source: Source<T>) {
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
this.#source = source
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected override async open(): Promise<void> {
|
|
18
|
+
this.value = await this.#source()
|
|
19
|
+
}
|
|
20
|
+
}
|
package/source/remote.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { Component } from './component.js'
|
|
3
|
+
import type { Invocable } from './component.js'
|
|
4
|
+
|
|
5
|
+
/** A call knows what its endpoint declares, which is what an explanation is read from. */
|
|
6
|
+
export interface Explicable extends Invocable {
|
|
7
|
+
explain(): any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Remote extends Component<Explicable> {
|
|
11
|
+
protected override kind = 'client' as const
|
|
12
|
+
|
|
13
|
+
public explain(endpoint: string): any {
|
|
14
|
+
if (!(endpoint in this.operations))
|
|
15
|
+
// `assert.fail`, not `assert.ok`: the message is built only when it is needed
|
|
16
|
+
assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
|
|
17
|
+
|
|
18
|
+
return this.operations[endpoint].explain()
|
|
19
|
+
}
|
|
20
|
+
}
|