@toa.io/boot 1.0.0-alpha.34 → 1.0.0-alpha.341

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/package.json +11 -8
  3. package/src/atom.js +41 -0
  4. package/src/atomicity.js +11 -0
  5. package/src/bindings/broadcast.js +3 -6
  6. package/src/bindings/constants.js +1 -3
  7. package/src/bindings/consume.js +18 -6
  8. package/src/bindings/emit.js +3 -6
  9. package/src/bindings/factory.js +5 -9
  10. package/src/bindings/inbound.js +11 -0
  11. package/src/bindings/index.js +7 -13
  12. package/src/bindings/outbound.js +10 -0
  13. package/src/bindings/produce.js +62 -17
  14. package/src/bindings/receive.js +3 -6
  15. package/src/bridge.js +22 -16
  16. package/src/call.js +29 -9
  17. package/src/cascade.js +16 -10
  18. package/src/component.js +101 -32
  19. package/src/composition.js +115 -15
  20. package/src/context.js +7 -8
  21. package/src/contract.js +11 -12
  22. package/src/emission.js +21 -14
  23. package/src/env.js +19 -12
  24. package/src/events.js +27 -0
  25. package/src/extensions/aspects.js +4 -8
  26. package/src/extensions/component.js +4 -8
  27. package/src/extensions/context.js +4 -8
  28. package/src/extensions/destinations.js +27 -0
  29. package/src/extensions/emitter.js +7 -9
  30. package/src/extensions/index.js +10 -19
  31. package/src/extensions/instances.js +2 -4
  32. package/src/extensions/load.js +7 -7
  33. package/src/extensions/manage.js +15 -0
  34. package/src/extensions/receiver.js +5 -9
  35. package/src/extensions/resolve.js +62 -13
  36. package/src/extensions/storage.js +6 -9
  37. package/src/extensions/tenants.js +5 -9
  38. package/src/guards.js +20 -0
  39. package/src/host.js +29 -0
  40. package/src/inbox.js +12 -0
  41. package/src/index.js +29 -34
  42. package/src/manifest.js +80 -36
  43. package/src/manifest.test.js +91 -8
  44. package/src/map.js +108 -0
  45. package/src/operation.js +29 -14
  46. package/src/outbox.js +22 -0
  47. package/src/rc.js +5 -0
  48. package/src/receivers.js +53 -23
  49. package/src/regional.js +18 -0
  50. package/src/regional.test.js +28 -0
  51. package/src/remote.js +41 -22
  52. package/src/rendering.js +50 -0
  53. package/src/span.js +19 -0
  54. package/src/storage.js +50 -16
  55. package/src/storage.test.js +57 -0
  56. package/src/workload.js +283 -0
  57. package/src/workload.test.js +323 -0
  58. package/types/bindings.d.ts +6 -2
  59. package/types/index.d.ts +62 -10
  60. package/types/manifest.d.ts +5 -4
  61. package/src/discovery.js +0 -43
  62. package/src/exposition.js +0 -17
@@ -0,0 +1,323 @@
1
+ import { describe, it, before, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { Connector } from '@toa.io/core'
4
+ import { environment, timeout } from '@toa.io/generic'
5
+
6
+ /*
7
+ * The probe binds a port and the halt listener reaches a broker, and this suite is about
8
+ * neither: what it exercises is the workload's own lifecycle, so the extensions that would
9
+ * contribute a resident are off. `TOA_INTROSPECTION` is unset, so introspection's is `null`.
10
+ */
11
+ environment.set('TOA_TELEMETRY_READY', 'false')
12
+
13
+ let Workload
14
+
15
+ before(async () => {
16
+ ;({ Workload } = await import('./workload.js'))
17
+ })
18
+
19
+ let sequence
20
+
21
+ beforeEach(() => {
22
+ sequence = []
23
+ })
24
+
25
+ class TestConnector extends Connector {
26
+ #label
27
+
28
+ constructor(label) {
29
+ super()
30
+ this.#label = label
31
+ }
32
+
33
+ async open() {
34
+ sequence.push(`+${this.#label}`)
35
+ }
36
+
37
+ async close() {
38
+ sequence.push(`-${this.#label}`)
39
+ }
40
+
41
+ async dispose() {
42
+ sequence.push(`*${this.#label}`)
43
+ }
44
+ }
45
+
46
+ /** One that has something to stop doing, which most connectors have not. */
47
+ class QuietConnector extends Connector {
48
+ #label
49
+
50
+ constructor(label) {
51
+ super()
52
+ this.#label = label
53
+ }
54
+
55
+ async pause() {
56
+ sequence.push(`~${this.#label}`)
57
+ }
58
+
59
+ async unpause() {
60
+ sequence.push(`^${this.#label}`)
61
+ }
62
+ }
63
+
64
+ /** Waits for a condition the halt reaches on its own timers. */
65
+ async function until(condition, limit = 2000) {
66
+ const deadline = Date.now() + limit
67
+
68
+ while (Date.now() < deadline) {
69
+ if (condition()) return
70
+
71
+ await timeout(5)
72
+ }
73
+
74
+ assert.fail('condition was not reached')
75
+ }
76
+
77
+ describe('Workload', () => {
78
+ it('should connect what the command built', async () => {
79
+ const workload = new Workload(async () => new TestConnector('a'))
80
+
81
+ await workload.connect()
82
+
83
+ assert.deepStrictEqual(sequence, ['+a'])
84
+
85
+ await workload.disconnect()
86
+ })
87
+
88
+ it('should take down what a gate holds, and leave the rest connected', async () => {
89
+ let built = 0
90
+
91
+ const workload = new Workload(async (workload) => {
92
+ const gate = workload.gate(async () => {
93
+ built++
94
+
95
+ return new TestConnector('gated')
96
+ })
97
+ const root = new Connector()
98
+
99
+ root.depends([gate, new TestConnector('above')])
100
+
101
+ return root
102
+ })
103
+
104
+ await workload.connect()
105
+
106
+ assert.deepStrictEqual(sequence.sort(), ['+above', '+gated'])
107
+
108
+ sequence = []
109
+
110
+ // a hundredth of a second, so the window is over before the assertions are
111
+ workload.stop(0.01)
112
+
113
+ // `-gated` and not `-above`: what a gate holds is closed, and nothing else is. The window is
114
+ // over by the time a poll sees it, and may be over by the time it is seen: what follows is
115
+ // the rebuild, asserted below
116
+ await until(() => sequence.includes('*gated'))
117
+
118
+ assert.deepStrictEqual(sequence.slice(0, 2), ['-gated', '*gated'])
119
+
120
+ await until(() => built === 2)
121
+
122
+ assert.deepStrictEqual(sequence, ['-gated', '*gated', '+gated'])
123
+
124
+ await workload.disconnect()
125
+ })
126
+
127
+ it('should build again rather than reconnect', async () => {
128
+ const held = []
129
+
130
+ const workload = new Workload(async (workload) =>
131
+ workload.gate(async () => {
132
+ const connector = new TestConnector('a')
133
+
134
+ held.push(connector)
135
+
136
+ return connector
137
+ })
138
+ )
139
+
140
+ await workload.connect()
141
+
142
+ workload.stop(0.01)
143
+
144
+ await until(() => held.length === 2)
145
+
146
+ assert.notEqual(held[0], held[1])
147
+
148
+ await workload.disconnect()
149
+ })
150
+
151
+ it('should return before anything is closed', async () => {
152
+ const workload = new Workload(async (workload) =>
153
+ workload.gate(async () => new TestConnector('a'))
154
+ )
155
+
156
+ await workload.connect()
157
+
158
+ sequence = []
159
+ workload.stop(0.01)
160
+
161
+ // the caller is a consumer callback: the teardown must not have begun under it
162
+ assert.deepStrictEqual(sequence, [])
163
+
164
+ await until(() => sequence.includes('+a'))
165
+ await workload.disconnect()
166
+ })
167
+
168
+ it('should make one cycle of two overlapping halts', async () => {
169
+ let built = 0
170
+
171
+ const workload = new Workload(async (workload) =>
172
+ workload.gate(async () => {
173
+ built++
174
+
175
+ return new TestConnector('a')
176
+ })
177
+ )
178
+
179
+ await workload.connect()
180
+
181
+ workload.stop(0.01)
182
+ workload.stop(0.01)
183
+
184
+ await until(() => built === 2)
185
+ await timeout(50)
186
+
187
+ assert.equal(built, 2)
188
+
189
+ await workload.disconnect()
190
+ })
191
+
192
+ it('should not halt what is not connected', async () => {
193
+ const workload = new Workload(async () => new TestConnector('a'))
194
+
195
+ workload.stop(0.01)
196
+
197
+ await timeout(30)
198
+
199
+ assert.deepStrictEqual(sequence, [])
200
+ })
201
+
202
+ it('should quiesce what it holds, closing nothing', async () => {
203
+ const workload = new Workload(async (workload) =>
204
+ workload.gate(async () => new QuietConnector('a'))
205
+ )
206
+
207
+ await workload.connect()
208
+
209
+ sequence = []
210
+
211
+ await workload.quiesce()
212
+
213
+ assert.deepStrictEqual(sequence, ['~a'])
214
+ assert.equal(workload.quiescent(), true)
215
+
216
+ // nothing was taken down, so the process is still working as far as anyone watching it
217
+ assert.equal(workload.running(), true)
218
+
219
+ await workload.disconnect()
220
+ })
221
+
222
+ it('should undo a quiesce without building anything', async () => {
223
+ let built = 0
224
+
225
+ const workload = new Workload(async (workload) =>
226
+ workload.gate(async () => {
227
+ built++
228
+
229
+ return new QuietConnector('a')
230
+ })
231
+ )
232
+
233
+ await workload.connect()
234
+ await workload.quiesce()
235
+
236
+ sequence = []
237
+
238
+ await workload.cancel()
239
+
240
+ assert.deepStrictEqual(sequence, ['^a'])
241
+ assert.equal(workload.quiescent(), false)
242
+ assert.equal(built, 1)
243
+
244
+ await workload.disconnect()
245
+ })
246
+
247
+ it('should take down a process that is already quiet', async () => {
248
+ const workload = new Workload(async (workload) =>
249
+ workload.gate(async () => {
250
+ const root = new Connector()
251
+
252
+ root.depends([new QuietConnector('q'), new TestConnector('a')])
253
+
254
+ return root
255
+ })
256
+ )
257
+
258
+ await workload.connect()
259
+ await workload.quiesce()
260
+
261
+ sequence = []
262
+ workload.stop(0.01)
263
+
264
+ await until(() => sequence.includes('+a'))
265
+
266
+ // quieted once, on the quiesce, and not again on the way down
267
+ assert.deepStrictEqual(sequence, ['-a', '*a', '+a'])
268
+
269
+ await workload.disconnect()
270
+ })
271
+
272
+ it('should not let a cancel undo a stop', async () => {
273
+ const workload = new Workload(async (workload) =>
274
+ workload.gate(async () => new TestConnector('a'))
275
+ )
276
+
277
+ await workload.connect()
278
+
279
+ sequence = []
280
+ workload.stop(0.01)
281
+
282
+ await workload.cancel()
283
+
284
+ // it goes down and comes back: a cancel is for a quiesce, and this is past one
285
+ await until(() => sequence.includes('+a'))
286
+
287
+ assert.deepStrictEqual(sequence, ['-a', '*a', '+a'])
288
+
289
+ await workload.disconnect()
290
+ })
291
+
292
+ it('should keep the process alive while it is halted', async () => {
293
+ const workload = new Workload(async (workload) =>
294
+ workload.gate(async () => new TestConnector('a'))
295
+ )
296
+
297
+ await workload.connect()
298
+
299
+ // long enough that the process would have to be held open for it
300
+ workload.stop(10)
301
+
302
+ await until(() => sequence.includes('*a'))
303
+
304
+ // a halted process holds nothing else: unreferenced, this timer would let it exit
305
+ assert.ok(
306
+ process.getActiveResourcesInfo().includes('Timeout'),
307
+ 'nothing keeps the process alive until the rebuild'
308
+ )
309
+
310
+ await workload.disconnect()
311
+ })
312
+
313
+ it('should take the gates down on its own disconnection', async () => {
314
+ const workload = new Workload(async (workload) =>
315
+ workload.gate(async () => new TestConnector('a'))
316
+ )
317
+
318
+ await workload.connect()
319
+ await workload.disconnect()
320
+
321
+ assert.deepStrictEqual(sequence, ['+a', '-a', '*a'])
322
+ })
323
+ })
@@ -1,3 +1,7 @@
1
- import type { bindings } from '@toa.io/core'
1
+ import type { bindings } from '@toa.io/core/types'
2
2
 
3
- export function broadcast<T> (name: string, group?: string, binding?: string): bindings.Broadcast<T>
3
+ export function broadcast<T>(
4
+ name: string,
5
+ group?: string,
6
+ binding?: string
7
+ ): bindings.Broadcast<T>
package/types/index.d.ts CHANGED
@@ -1,21 +1,73 @@
1
- import * as core from '@toa.io/core'
2
- import * as composition from './composition'
1
+ import type * as core from '@toa.io/core'
2
+ import type * as types from '@toa.io/core/types'
3
+ import type * as norm from '@toa.io/norm/types'
4
+ import * as composition from './composition.d.ts'
3
5
 
4
- export * as bindings from './bindings'
6
+ export * as bindings from './bindings.d.ts'
5
7
 
6
- export async function composition (paths: string[], options?: composition.Options): Promise<core.Connector>
8
+ export async function manifest(
9
+ path: string,
10
+ options?: composition.Options
11
+ ): Promise<norm.Manifest>
7
12
 
8
- export async function remote (locator: core.Locator): Promise<core.Component>
13
+ /** What this process was given about the components it calls. */
14
+ export namespace map {
15
+ /** the file to read it from, or the map itself */
16
+ function use(value: string | Record<string, core.Contract> | undefined): void
9
17
 
10
- export async function receive<T = any> (
18
+ /** what this process composes, stated by the boot that composes it */
19
+ function compose(manifests: norm.Manifest[]): Promise<void>
20
+
21
+ /** forgets what this process composed */
22
+ function forget(): void
23
+
24
+ function contract(id: string): Promise<core.Contract | undefined>
25
+ }
26
+
27
+ export async function composition(
28
+ paths: string[],
29
+ options?: composition.Options
30
+ ): Promise<core.Connector>
31
+
32
+ export async function remote(
33
+ locator: core.Locator,
34
+ source?: types.Source
35
+ ): Promise<core.Remote>
36
+
37
+ export async function receive<T = any>(
11
38
  label: string,
12
- receiver: Receiver,
39
+ receiver: Receiver
13
40
  ): Promise<core.Connector>
14
41
 
15
- export async function receive<T = any> (
42
+ export async function receive<T = any>(
16
43
  label: string,
17
44
  group: string | undefined,
18
- receiver: Receiver,
45
+ receiver: Receiver
19
46
  ): Promise<core.Connector>
20
47
 
21
- type Receiver = { receive: (message: core.Message<T>) => void | Promise<void> }
48
+ type Receiver = { receive: (message: types.Message<T>) => void | Promise<void> }
49
+
50
+ export function host(): types.extensions.Host
51
+
52
+ /** A process, as a connector: what the command built, and what the extensions keep in one. */
53
+ export class Workload extends core.Connector {
54
+ constructor(build: (workload: Workload) => Promise<core.Connector>)
55
+
56
+ /** A part of this process a halt takes down and builds again. */
57
+ gate(build: () => Promise<core.Connector>): core.Gate
58
+
59
+ /** Whether what a halt takes down is up. */
60
+ running(): boolean
61
+
62
+ /** Whether this process is quiet: it does nothing of its own accord and holds everything. */
63
+ quiescent(): boolean
64
+
65
+ /** Stops what this process does of its own accord, holding open everything it has. */
66
+ quiesce(): Promise<void>
67
+
68
+ /** Undoes a quiesce. */
69
+ cancel(): Promise<void>
70
+
71
+ /** Stops this process for `seconds`, then builds it again. */
72
+ stop(seconds: number): void
73
+ }
@@ -1,8 +1,9 @@
1
1
  import * as _norm from '@toa.io/norm/types'
2
- import * as _composition from './composition'
2
+ import * as _composition from './composition.d.ts'
3
3
 
4
4
  declare namespace toa.boot {
5
-
6
- type Manifest = (path: string, options?: _composition.Options) => Promise<_norm.Component>
7
-
5
+ type Manifest = (
6
+ path: string,
7
+ options?: _composition.Options
8
+ ) => Promise<_norm.Component>
8
9
  }
package/src/discovery.js DELETED
@@ -1,43 +0,0 @@
1
- 'use strict'
2
-
3
- const { Discovery, Exposition } = require('@toa.io/core')
4
-
5
- const boot = require('./index')
6
-
7
- let promise
8
- let instance = null
9
-
10
- const discovery = async () => {
11
- if (instance === null) {
12
- instance = new Discovery(lookup)
13
- promise = instance.connect()
14
- }
15
-
16
- await promise
17
-
18
- return instance
19
- }
20
-
21
- const lookup = async (locator) => {
22
- const call = boot.call(locator, ENDPOINT, { bindings: BINDINGS })
23
-
24
- await call.connect()
25
-
26
- return call
27
- }
28
-
29
- const expose = async (manifest) => {
30
- const exposition = new Exposition(manifest.locator, manifest)
31
- const operations = { [ENDPOINT]: { bindings: BINDINGS } }
32
- const producers = boot.bindings.produce(exposition, operations)
33
-
34
- await Promise.all(producers.map((producer) => producer.connect()))
35
-
36
- return producers
37
- }
38
-
39
- const BINDINGS = ['@toa.io/bindings.amqp']
40
- const ENDPOINT = '.lookup'
41
-
42
- exports.discovery = discovery
43
- exports.expose = expose
package/src/exposition.js DELETED
@@ -1,17 +0,0 @@
1
- 'use strict'
2
-
3
- const { Exposition, Locator } = require('@toa.io/core')
4
-
5
- const boot = require('./index')
6
-
7
- const exposition = async (manifest) => {
8
- const locator = new Locator('', '')
9
- const exposition = new Exposition(locator, manifest)
10
- const producers = boot.bindings.expose(exposition)
11
-
12
- await Promise.all(producers.map((producer) => producer.connect()))
13
-
14
- return producers
15
- }
16
-
17
- exports.exposition = exposition