@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.
- package/CHANGELOG.md +475 -0
- package/package.json +11 -8
- package/src/atom.js +41 -0
- package/src/atomicity.js +11 -0
- package/src/bindings/broadcast.js +3 -6
- package/src/bindings/constants.js +1 -3
- package/src/bindings/consume.js +18 -6
- package/src/bindings/emit.js +3 -6
- package/src/bindings/factory.js +5 -9
- package/src/bindings/inbound.js +11 -0
- package/src/bindings/index.js +7 -13
- package/src/bindings/outbound.js +10 -0
- package/src/bindings/produce.js +62 -17
- package/src/bindings/receive.js +3 -6
- package/src/bridge.js +22 -16
- package/src/call.js +29 -9
- package/src/cascade.js +16 -10
- package/src/component.js +101 -32
- package/src/composition.js +115 -15
- package/src/context.js +7 -8
- package/src/contract.js +11 -12
- package/src/emission.js +21 -14
- package/src/env.js +19 -12
- package/src/events.js +27 -0
- package/src/extensions/aspects.js +4 -8
- package/src/extensions/component.js +4 -8
- package/src/extensions/context.js +4 -8
- package/src/extensions/destinations.js +27 -0
- package/src/extensions/emitter.js +7 -9
- package/src/extensions/index.js +10 -19
- package/src/extensions/instances.js +2 -4
- package/src/extensions/load.js +7 -7
- package/src/extensions/manage.js +15 -0
- package/src/extensions/receiver.js +5 -9
- package/src/extensions/resolve.js +62 -13
- package/src/extensions/storage.js +6 -9
- package/src/extensions/tenants.js +5 -9
- package/src/guards.js +20 -0
- package/src/host.js +29 -0
- package/src/inbox.js +12 -0
- package/src/index.js +29 -34
- package/src/manifest.js +80 -36
- package/src/manifest.test.js +91 -8
- package/src/map.js +108 -0
- package/src/operation.js +29 -14
- package/src/outbox.js +22 -0
- package/src/rc.js +5 -0
- package/src/receivers.js +53 -23
- package/src/regional.js +18 -0
- package/src/regional.test.js +28 -0
- package/src/remote.js +41 -22
- package/src/rendering.js +50 -0
- package/src/span.js +19 -0
- package/src/storage.js +50 -16
- package/src/storage.test.js +57 -0
- package/src/workload.js +283 -0
- package/src/workload.test.js +323 -0
- package/types/bindings.d.ts +6 -2
- package/types/index.d.ts +62 -10
- package/types/manifest.d.ts +5 -4
- package/src/discovery.js +0 -43
- package/src/exposition.js +0 -17
package/src/rendering.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Event, Rendering } from '@toa.io/core'
|
|
2
|
+
|
|
3
|
+
import * as boot from './index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gives every destination that names the events it renders a `Rendering` of them, made with the
|
|
7
|
+
* component's own bridges. Every event of the manifest may be named, including those nothing
|
|
8
|
+
* consumes and are therefore not published: a destination writes somewhere of its own.
|
|
9
|
+
*
|
|
10
|
+
* @param {toa.norm.Component} manifest
|
|
11
|
+
* @param {import('@toa.io/core/types').outbox.Destination[]} destinations
|
|
12
|
+
* @param {import('@toa.io/core').Context} context
|
|
13
|
+
* @returns {Promise<void>}
|
|
14
|
+
*/
|
|
15
|
+
export const rendering = async (manifest, destinations, context) => {
|
|
16
|
+
for (const destination of destinations) {
|
|
17
|
+
if (destination.renders === undefined) continue
|
|
18
|
+
|
|
19
|
+
const events = new Map()
|
|
20
|
+
|
|
21
|
+
for (const label of destination.renders) {
|
|
22
|
+
const definition = manifest.events?.[label]
|
|
23
|
+
|
|
24
|
+
if (definition === undefined)
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Component '${manifest.locator.id}' has no event '${label}' to render for ` +
|
|
27
|
+
`'${destination.name}'`
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const bridge = await boot.bridge.event(
|
|
31
|
+
definition.bridge,
|
|
32
|
+
definition.path,
|
|
33
|
+
label,
|
|
34
|
+
context
|
|
35
|
+
)
|
|
36
|
+
const event = new Event(
|
|
37
|
+
{ ...definition, label: `${manifest.locator.id}.${label}` },
|
|
38
|
+
null,
|
|
39
|
+
bridge
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
events.set(label, event)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rendering = new Rendering(events)
|
|
46
|
+
|
|
47
|
+
destination.rendering = rendering
|
|
48
|
+
destination.depends(rendering)
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/span.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { console, traces } from 'openspan'
|
|
2
|
+
import { environment } from '@toa.io/generic'
|
|
3
|
+
|
|
4
|
+
const enabled = environment.get('TOA_BOOT_TRACE') === '1'
|
|
5
|
+
|
|
6
|
+
// Tracing is off until an exporter is configured, and the CLI opens its own span before
|
|
7
|
+
// any extension gets to configure one — so asking for a boot trace turns the console
|
|
8
|
+
// exporter on here. Extensions keep it: see `development()` in the telemetry factory.
|
|
9
|
+
if (enabled) traces({ exporters: { console: {} } })
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a span if boot tracing is enabled (TOA_BOOT_TRACE=1), otherwise just runs the task
|
|
13
|
+
*
|
|
14
|
+
* @param {string | object} options
|
|
15
|
+
* @param {() => Promise<any>} task
|
|
16
|
+
* @returns {Promise<any>}
|
|
17
|
+
*/
|
|
18
|
+
export const span = async (options, task) =>
|
|
19
|
+
enabled ? console.span(options, task) : task()
|
package/src/storage.js
CHANGED
|
@@ -1,26 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import * as extensions from './extensions/index.js'
|
|
4
|
+
|
|
5
|
+
// import.meta.resolve takes no paths, and the connector is resolved against the
|
|
6
|
+
// component that names it
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {toa.norm.Component} manifest
|
|
11
|
+
* @param {boolean} outbox whether this component publishes anything, and so needs a place to
|
|
12
|
+
* commit it with the entity
|
|
13
|
+
* @param {boolean} inbox whether any of its operations declares `once`, and so needs a place to
|
|
14
|
+
* record the call it commits
|
|
15
|
+
*/
|
|
16
|
+
export const storage = async (manifest, outbox, inbox) => {
|
|
7
17
|
if (manifest.entity === undefined) return
|
|
8
18
|
|
|
9
|
-
const Factory = load(manifest)
|
|
19
|
+
const Factory = await load(manifest)
|
|
10
20
|
|
|
11
|
-
/** @type {toa.core.storages.Factory} */
|
|
21
|
+
/** @type {import('@toa.io/core/types').storages.Factory} */
|
|
12
22
|
const factory = new Factory()
|
|
13
|
-
const storage = factory.storage(manifest.locator, manifest.entity)
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
const storage = factory.storage(manifest.locator, manifest.entity, { outbox, inbox })
|
|
24
|
+
|
|
25
|
+
// a component whose structure nothing will make must not start with the structure it lacks;
|
|
26
|
+
// what it inherits was written for descendants that store, and one that stores nothing has
|
|
27
|
+
// nothing to convert
|
|
28
|
+
const own = manifest.entity.migrations?.some(
|
|
29
|
+
(migration) => migration.prototype === undefined
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if (own === true && storage.migrates !== true)
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Component '${manifest.locator.id}' declares migrations, ` +
|
|
35
|
+
`which storage '${manifest.entity.storage}' does not apply`
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* Refused here rather than degraded: a storage that cannot record a call would run every
|
|
40
|
+
* duplicate of it, which is what declaring `once` asked not to happen. Whether the deployment
|
|
41
|
+
* can commit one is the storage's own to refuse, once it is connected.
|
|
42
|
+
*/
|
|
43
|
+
if (inbox && storage.claims !== true)
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Component '${manifest.locator.id}' declares 'once', ` +
|
|
46
|
+
`which storage '${manifest.entity.storage}' does not provide`
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return extensions.storage(storage, manifest.locator)
|
|
16
50
|
}
|
|
17
51
|
|
|
18
|
-
function load
|
|
52
|
+
async function load(component) {
|
|
19
53
|
const reference = component.entity.storage
|
|
20
|
-
const path = require.resolve(reference, {
|
|
21
|
-
|
|
54
|
+
const path = require.resolve(reference, {
|
|
55
|
+
paths: [component.path, import.meta.dirname]
|
|
56
|
+
})
|
|
57
|
+
const { Factory } = await import(pathToFileURL(path).href)
|
|
22
58
|
|
|
23
59
|
return Factory
|
|
24
60
|
}
|
|
25
|
-
|
|
26
|
-
exports.storage = storage
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { Locator } from '@toa.io/core'
|
|
5
|
+
|
|
6
|
+
import { storage } from './storage.js'
|
|
7
|
+
|
|
8
|
+
const manifest = (entity) => ({
|
|
9
|
+
locator: new Locator('one', 'test'),
|
|
10
|
+
path: import.meta.dirname,
|
|
11
|
+
entity
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const schema = { properties: {} }
|
|
15
|
+
|
|
16
|
+
it('should not create a storage for a component that stores nothing', async () => {
|
|
17
|
+
assert.equal(await storage(manifest(undefined), false), undefined)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should refuse migrations a storage does not apply', async () => {
|
|
21
|
+
const entity = {
|
|
22
|
+
schema,
|
|
23
|
+
storage: '@toa.io/storages.null',
|
|
24
|
+
migrations: [{ id: '0001', steps: [] }]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await assert.rejects(
|
|
28
|
+
storage(manifest(entity), false),
|
|
29
|
+
/declares migrations, which storage '@toa.io\/storages.null' does not apply/
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should accept inherited migrations a storage does not apply', async () => {
|
|
34
|
+
const entity = {
|
|
35
|
+
schema,
|
|
36
|
+
storage: '@toa.io/storages.null',
|
|
37
|
+
migrations: [{ id: 'system:0001', steps: [], prototype: 'system' }]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
assert.notEqual(await storage(manifest(entity), false), undefined)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should accept a storage with no migrations', async () => {
|
|
44
|
+
const entity = { schema, storage: '@toa.io/storages.null' }
|
|
45
|
+
|
|
46
|
+
assert.notEqual(await storage(manifest(entity), false), undefined)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should accept migrations a storage applies', async () => {
|
|
50
|
+
const entity = {
|
|
51
|
+
schema,
|
|
52
|
+
storage: '@toa.io/storages.mongodb',
|
|
53
|
+
migrations: [{ id: '0001', steps: [] }]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
assert.notEqual(await storage(manifest(entity), false), undefined)
|
|
57
|
+
})
|
package/src/workload.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { console } from 'openspan'
|
|
2
|
+
import { Connector, Gate, deliveries, halting } from '@toa.io/core'
|
|
3
|
+
import { PREDEFINED } from '@toa.io/definitions'
|
|
4
|
+
|
|
5
|
+
import { resolve } from './extensions/resolve.js'
|
|
6
|
+
import { host } from './host.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A process, as a connector.
|
|
10
|
+
*
|
|
11
|
+
* What it holds is in two parts. Its *residents* are what the extensions keep in every process
|
|
12
|
+
* whatever it runs — the readiness probe is the one there is — and they connect before
|
|
13
|
+
* everything else and go last. The rest is what the command built.
|
|
14
|
+
*
|
|
15
|
+
* Which is what a process had no name for before: `toa compose`, `toa serve` and `toa mono`
|
|
16
|
+
* each assembled a root out of a bare `Connector` and their own arithmetic, and an extension
|
|
17
|
+
* with something to put in a process had nowhere to put it.
|
|
18
|
+
*/
|
|
19
|
+
export class Workload extends Connector {
|
|
20
|
+
/** @type {(workload: Workload) => Promise<import('@toa.io/core').Connector>} */
|
|
21
|
+
#build
|
|
22
|
+
|
|
23
|
+
/** @type {import('@toa.io/core/types').extensions.Resident[]} */
|
|
24
|
+
#residents = []
|
|
25
|
+
|
|
26
|
+
/** @type {import('@toa.io/core').Gate[]} */
|
|
27
|
+
#gates = []
|
|
28
|
+
|
|
29
|
+
/** @type {NodeJS.Timeout | null} */
|
|
30
|
+
#timer = null
|
|
31
|
+
|
|
32
|
+
#halting = false
|
|
33
|
+
|
|
34
|
+
#quiesced = false
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {(workload: Workload) => Promise<import('@toa.io/core').Connector>} build
|
|
38
|
+
* what the command runs, called once
|
|
39
|
+
*/
|
|
40
|
+
constructor(build) {
|
|
41
|
+
super()
|
|
42
|
+
|
|
43
|
+
this.#build = build
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A part of this tree a halt takes down and builds again. Handed to extensions as
|
|
48
|
+
* `Host.gate`, and used by the handlers for what the command runs.
|
|
49
|
+
*
|
|
50
|
+
* @param {() => Promise<import('@toa.io/core').Connector>} build
|
|
51
|
+
* @returns {import('@toa.io/core').Gate}
|
|
52
|
+
*/
|
|
53
|
+
gate(build) {
|
|
54
|
+
const gate = new Gate(build)
|
|
55
|
+
|
|
56
|
+
this.#gates.push(gate)
|
|
57
|
+
|
|
58
|
+
return gate
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether what a halt takes down is up. False from the moment a halt begins until the rebuild
|
|
63
|
+
* has landed, so whoever watches one watches the thing itself rather than a clock beside it.
|
|
64
|
+
*/
|
|
65
|
+
running() {
|
|
66
|
+
return !this.#halting && this.#gates.every((gate) => gate.holding())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether this process is quiet: it does nothing of its own accord, and holds everything it
|
|
71
|
+
* had open. What a halt does before it decides whether to go down.
|
|
72
|
+
*/
|
|
73
|
+
quiescent() {
|
|
74
|
+
return this.#quiesced
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stops what this process does of its own accord and holds open everything it has: the
|
|
79
|
+
* gateway answers `503`, the clocks stop, and each component gets its `pause`. Nothing
|
|
80
|
+
* closes, so nothing has to be built again to undo it.
|
|
81
|
+
*
|
|
82
|
+
* Awaited, unlike `stop`: whoever asks for this goes on to watch what the deployment does
|
|
83
|
+
* next, and that is only worth watching once this process has gone quiet.
|
|
84
|
+
*/
|
|
85
|
+
async quiesce() {
|
|
86
|
+
if (this.#halting || this.#quiesced || !this.connected) return
|
|
87
|
+
|
|
88
|
+
this.#quiesced = true
|
|
89
|
+
|
|
90
|
+
await this.halt()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Undoes a quiesce. Nothing was taken down, so nothing is built again. */
|
|
94
|
+
async cancel() {
|
|
95
|
+
if (this.#halting || !this.#quiesced) return
|
|
96
|
+
|
|
97
|
+
this.#quiesced = false
|
|
98
|
+
|
|
99
|
+
await this.restore()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stops this process for `seconds`, then builds it again.
|
|
104
|
+
*
|
|
105
|
+
* Returns before anything closes, on the next tick rather than in the caller's own: whoever
|
|
106
|
+
* asks for this is a consumer callback more often than not, and a connection waits for the
|
|
107
|
+
* message being handled before it closes, so a teardown awaited inside that callback would be
|
|
108
|
+
* waiting on the callback that is waiting on it.
|
|
109
|
+
*
|
|
110
|
+
* @param {number} seconds
|
|
111
|
+
*/
|
|
112
|
+
stop(seconds) {
|
|
113
|
+
if (this.#halting || !this.connected) return
|
|
114
|
+
|
|
115
|
+
this.#halting = true
|
|
116
|
+
|
|
117
|
+
setImmediate(() => void this.#cycle(seconds))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async open() {
|
|
121
|
+
/*
|
|
122
|
+
* Connected here rather than declared as dependencies, because what they are is read from
|
|
123
|
+
* the extensions and a connector's dependencies are walked before `open` runs. The probe
|
|
124
|
+
* goes up before the build so that it answers while the build is still happening.
|
|
125
|
+
*/
|
|
126
|
+
this.#residents = await residents(this)
|
|
127
|
+
|
|
128
|
+
for (const resident of this.#residents) {
|
|
129
|
+
this.depends(resident)
|
|
130
|
+
|
|
131
|
+
await resident.connect()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const root = await this.#build(this)
|
|
135
|
+
|
|
136
|
+
this.depends(root)
|
|
137
|
+
|
|
138
|
+
await root.connect()
|
|
139
|
+
|
|
140
|
+
for (const resident of this.#residents) await resident.complete?.()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async close() {
|
|
144
|
+
if (this.#timer !== null) {
|
|
145
|
+
clearTimeout(this.#timer)
|
|
146
|
+
this.#timer = null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {number} seconds
|
|
152
|
+
*/
|
|
153
|
+
async #cycle(seconds) {
|
|
154
|
+
/*
|
|
155
|
+
* The window is measured from here rather than from where the teardown ends.
|
|
156
|
+
*
|
|
157
|
+
* A teardown takes as long as what it drains, so a process that started counting afterwards
|
|
158
|
+
* would be down for a window beginning where everyone else's ended. Counting from the signal
|
|
159
|
+
* means every process comes back at one instant however long its own teardown took, and one
|
|
160
|
+
* whose teardown outran the window comes back at once with nothing left to wait for.
|
|
161
|
+
*/
|
|
162
|
+
const resumesAt = Date.now() + seconds * 1000
|
|
163
|
+
|
|
164
|
+
halting.begin()
|
|
165
|
+
|
|
166
|
+
console.warn('Halting', { seconds })
|
|
167
|
+
|
|
168
|
+
this.#quiesced = true
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* Quiet first, then down. A source stopped before the drain begins is work that never
|
|
172
|
+
* starts, so what the teardown waits for is only what was already in hand — and a component
|
|
173
|
+
* gets its `pause` while it is still whole, which is the one moment it can release what the
|
|
174
|
+
* runtime cannot see.
|
|
175
|
+
*/
|
|
176
|
+
await this.halt()
|
|
177
|
+
|
|
178
|
+
/*
|
|
179
|
+
* Whoever called this stop saw the deployment still, and this process was not: what it is
|
|
180
|
+
* handling has written no edge yet. Nothing is abandoned — the teardown waits for every
|
|
181
|
+
* one of these — but it is the one thing that makes a halt take longer than it says, so
|
|
182
|
+
* it is said out loud.
|
|
183
|
+
*/
|
|
184
|
+
const inflight = deliveries.inflight()
|
|
185
|
+
|
|
186
|
+
if (inflight > 0) console.warn('Halting with deliveries in flight', { inflight })
|
|
187
|
+
|
|
188
|
+
await this.#down(seconds)
|
|
189
|
+
|
|
190
|
+
const remaining = resumesAt - Date.now()
|
|
191
|
+
|
|
192
|
+
if (remaining <= 0) {
|
|
193
|
+
// said out loud: for as long as it took, this process was neither halted nor working
|
|
194
|
+
console.warn('Halted late, the teardown outran the window', {
|
|
195
|
+
seconds,
|
|
196
|
+
over: Math.round(-remaining / 1000)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return await this.#resume()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.warn('Halted', { seconds })
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* Referenced, unlike every other timer the runtime keeps: a halted process holds nothing
|
|
206
|
+
* at all — no connection, no listening socket that is not unreferenced — so this is the
|
|
207
|
+
* only thing left to keep it alive. Unreferenced, the process would exit here rather than
|
|
208
|
+
* come back, and whatever restarts it would bring up a process that never heard of the
|
|
209
|
+
* halt. `close` clears it, so it holds nothing open against a shutdown.
|
|
210
|
+
*/
|
|
211
|
+
this.#timer = setTimeout(() => void this.#resume(), remaining)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* In reverse: a gate an extension made is below the one that holds what the command runs.
|
|
216
|
+
*
|
|
217
|
+
* @param {number} [seconds] how long they stay down, where they are coming back
|
|
218
|
+
*/
|
|
219
|
+
async #down(seconds) {
|
|
220
|
+
for (const gate of [...this.#gates].reverse()) await gate.down(seconds)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Building again is building, and nothing about it is special: the same function of the same
|
|
225
|
+
* arguments, against the same infrastructure, as at boot. So it is tried once, and a failure
|
|
226
|
+
* ends the process the way a boot failure ends it. Anything else would be a second, weaker
|
|
227
|
+
* path to being up, one that could paper over a rebuild that does not work, which is precisely
|
|
228
|
+
* what there is to find out.
|
|
229
|
+
*/
|
|
230
|
+
async #resume() {
|
|
231
|
+
this.#timer = null
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
for (const gate of this.#gates) await gate.up()
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('Resume failed', { message: error?.message })
|
|
237
|
+
|
|
238
|
+
await this.disconnect().catch((failure) => {
|
|
239
|
+
console.error('Shutdown after a failed resume failed', {
|
|
240
|
+
message: failure?.message
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
process.exit(1)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this.restore()
|
|
248
|
+
|
|
249
|
+
this.#quiesced = false
|
|
250
|
+
this.#halting = false
|
|
251
|
+
|
|
252
|
+
halting.end()
|
|
253
|
+
|
|
254
|
+
console.info('Resumed')
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* What the extensions this process has loaded keep in it.
|
|
260
|
+
*
|
|
261
|
+
* The predefined ones are loaded here rather than left to whichever component happens to
|
|
262
|
+
* reference them: what a resident answers for is the process, and a process that runs no
|
|
263
|
+
* component of its own must have one all the same.
|
|
264
|
+
*
|
|
265
|
+
* @returns {Promise<import('@toa.io/core/types').extensions.Resident[]>}
|
|
266
|
+
*/
|
|
267
|
+
async function residents(workload) {
|
|
268
|
+
const { instances } = await import('./extensions/instances.js')
|
|
269
|
+
|
|
270
|
+
await Promise.all(Object.keys(PREDEFINED).map(async (name) => await resolve(name)))
|
|
271
|
+
|
|
272
|
+
const residents = []
|
|
273
|
+
|
|
274
|
+
for (const factory of Object.values(instances)) {
|
|
275
|
+
if (factory.resident === undefined) continue
|
|
276
|
+
|
|
277
|
+
const resident = await factory.resident(host(workload))
|
|
278
|
+
|
|
279
|
+
if (resident !== null && resident !== undefined) residents.push(resident)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return residents
|
|
283
|
+
}
|