@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/manifest.js
CHANGED
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import clone from 'clone-deep'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { basename, dirname, join } from 'node:path'
|
|
5
|
+
import { merge } from '@toa.io/generic'
|
|
6
|
+
import { component as load, revive, NORMALIZED } from '@toa.io/norm'
|
|
7
|
+
import { shipped } from '@toa.io/definitions/digest'
|
|
8
|
+
import { Locator } from '@toa.io/core'
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
const { merge } = require('@toa.io/generic')
|
|
5
|
-
const { component: load } = require('@toa.io/norm')
|
|
6
|
-
const { Locator } = require('@toa.io/core')
|
|
10
|
+
import { span } from './span.js'
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
* @type {toa.boot.Manifest}
|
|
10
|
-
*/
|
|
11
|
-
const manifest = async (path, options = {}) => {
|
|
12
|
+
export const manifest = async (path, options = {}) => {
|
|
12
13
|
options = merge(clone(options), DEFAULTS)
|
|
13
14
|
|
|
14
|
-
const manifest = await
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
for (const operation of Object.values(manifest.operations)) {
|
|
19
|
-
operation.bindings = options.bindings
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const check = (binding) => require(binding).properties?.async === true
|
|
24
|
-
const asyncBinding = options.bindings.find(check)
|
|
25
|
-
|
|
26
|
-
if (asyncBinding === undefined) throw new Error('Bindings override must contain at least one async binding')
|
|
27
|
-
|
|
28
|
-
if ('events' in manifest) {
|
|
29
|
-
for (const event of Object.values(manifest.events)) event.binding = asyncBinding
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if ('receivers' in manifest) {
|
|
33
|
-
for (const receiver of Object.values(manifest.receivers)) {
|
|
34
|
-
if (receiver.source === undefined) receiver.binding = asyncBinding
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
15
|
+
const manifest = await span(
|
|
16
|
+
{ name: `manifest ${basename(path)}`, attributes: { path } },
|
|
17
|
+
async () => await read(path)
|
|
18
|
+
)
|
|
38
19
|
|
|
39
20
|
if (manifest.extensions === undefined) manifest.extensions = {}
|
|
40
21
|
|
|
@@ -44,13 +25,76 @@ const manifest = async (path, options = {}) => {
|
|
|
44
25
|
}
|
|
45
26
|
}
|
|
46
27
|
|
|
47
|
-
if ('storage' in options && 'entity' in manifest)
|
|
28
|
+
if ('storage' in options && 'entity' in manifest)
|
|
29
|
+
manifest.entity.storage = options.storage
|
|
48
30
|
|
|
49
31
|
manifest.locator = new Locator(manifest.name, manifest.namespace)
|
|
50
32
|
|
|
51
33
|
return manifest
|
|
52
34
|
}
|
|
53
35
|
|
|
54
|
-
|
|
36
|
+
/**
|
|
37
|
+
* A manifest already normalised, where there is one, and the sources read where there is not.
|
|
38
|
+
*
|
|
39
|
+
* A component of an application's own comes with the manifest its build wrote beside it. A
|
|
40
|
+
* component an extension ships comes with none — its package is published once, long before
|
|
41
|
+
* any image is built — but `@toa.io/definitions` carries the digest of those manifests, which
|
|
42
|
+
* a deploy has read for as long as it has been unable to install the extension. A service runs
|
|
43
|
+
* them from the package, so it reads the same digest rather than reading their sources again.
|
|
44
|
+
*
|
|
45
|
+
* Only a workspace normalises, then, which is the one place a source can have changed since.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} path
|
|
48
|
+
*/
|
|
49
|
+
async function read(path) {
|
|
50
|
+
const file = join(path, NORMALIZED)
|
|
51
|
+
|
|
52
|
+
if (existsSync(file)) return revive(JSON.parse(await readFile(file, 'utf8')), path)
|
|
53
|
+
|
|
54
|
+
return (await digested(path)) ?? (await load(path))
|
|
55
|
+
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* What the digest holds for a component of an extension's own, by the directory it is in.
|
|
59
|
+
*
|
|
60
|
+
* An extension ships its components in `components/` beside its manifest and nowhere else, so
|
|
61
|
+
* that is the whole of what is looked for: anything else — an application's own component, a
|
|
62
|
+
* fixture in a workspace — is read from its sources.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} path
|
|
65
|
+
* @returns {Promise<object | undefined>}
|
|
66
|
+
*/
|
|
67
|
+
async function digested(path) {
|
|
68
|
+
const directory = dirname(path)
|
|
69
|
+
|
|
70
|
+
if (basename(directory) !== COMPONENTS) return undefined
|
|
71
|
+
|
|
72
|
+
const root = dirname(directory)
|
|
73
|
+
const manifest = join(root, MANIFEST)
|
|
74
|
+
|
|
75
|
+
if (!existsSync(manifest)) return undefined
|
|
76
|
+
|
|
77
|
+
const { name } = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
78
|
+
|
|
79
|
+
// the digest is of what Toa ships, and a package it does not define has none
|
|
80
|
+
if (name === undefined || !name.startsWith(SCOPE)) return undefined
|
|
81
|
+
|
|
82
|
+
// every component the extension ships, not those a definition deploys by default: what an
|
|
83
|
+
// annotation adds, such as the encounters of Chronicles, runs in the service all the same
|
|
84
|
+
const { labels, manifests } = shipped(name.slice(SCOPE.length)) ?? {}
|
|
85
|
+
|
|
86
|
+
if (manifests === undefined) return undefined
|
|
87
|
+
|
|
88
|
+
// a chart names a component's workload the way the digest labels it
|
|
89
|
+
const index = labels.indexOf(basename(path).replace('.', '-'))
|
|
90
|
+
|
|
91
|
+
return index === -1 ? undefined : revive(manifests[index], path)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const SCOPE = '@toa.io/'
|
|
95
|
+
|
|
96
|
+
const COMPONENTS = 'components'
|
|
97
|
+
|
|
98
|
+
const MANIFEST = 'package.json'
|
|
99
|
+
|
|
100
|
+
const DEFAULTS = {}
|
package/src/manifest.test.js
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
import { it, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { generate } from 'randomstring'
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
component: () => mockComponent()
|
|
7
|
-
}))
|
|
9
|
+
const NORMALIZED = 'manifest.toa.json'
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
let normalised = 0
|
|
12
|
+
|
|
13
|
+
const digest = {
|
|
14
|
+
labels: ['identity-basic'],
|
|
15
|
+
manifests: [{ name: 'basic', namespace: 'identity' }]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
mock.module('@toa.io/norm', {
|
|
19
|
+
namedExports: {
|
|
20
|
+
component: () => {
|
|
21
|
+
normalised++
|
|
22
|
+
|
|
23
|
+
return mockComponent()
|
|
24
|
+
},
|
|
25
|
+
revive: (declared, path) => ({ ...declared, path }),
|
|
26
|
+
NORMALIZED
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
mock.module('@toa.io/definitions/digest', {
|
|
31
|
+
namedExports: {
|
|
32
|
+
shipped: (suffix) => (suffix === 'extensions.exposition' ? digest : undefined)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const { manifest } = await import('./manifest.js')
|
|
10
37
|
|
|
11
38
|
const path = generate()
|
|
12
39
|
|
|
@@ -15,9 +42,65 @@ it('should not modify options', async () => {
|
|
|
15
42
|
|
|
16
43
|
await manifest(path, options)
|
|
17
44
|
|
|
18
|
-
|
|
45
|
+
assert.deepStrictEqual(options.extensions.length, 2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should normalize a component the build did not', async () => {
|
|
49
|
+
const before = normalised
|
|
50
|
+
|
|
51
|
+
await manifest(path)
|
|
52
|
+
|
|
53
|
+
assert.strictEqual(normalised, before + 1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should read what the build wrote, and not normalize', async () => {
|
|
57
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-manifest-test'))
|
|
58
|
+
const declared = { name: 'tasks', namespace: 'todos', operations: { compute: {} } }
|
|
59
|
+
|
|
60
|
+
await writeFile(join(root, NORMALIZED), JSON.stringify(declared))
|
|
61
|
+
|
|
62
|
+
const before = normalised
|
|
63
|
+
const read = await manifest(root)
|
|
64
|
+
|
|
65
|
+
assert.strictEqual(normalised, before, 'the component was normalized')
|
|
66
|
+
assert.strictEqual(read.path, root)
|
|
67
|
+
assert.deepStrictEqual(read.operations, declared.operations)
|
|
68
|
+
assert.strictEqual(read.locator.id, 'todos.tasks')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should read the digest for a component an extension ships, and not normalize', async () => {
|
|
72
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-manifest-test'))
|
|
73
|
+
const component = join(root, 'components', 'identity.basic')
|
|
74
|
+
|
|
75
|
+
await mkdir(component, { recursive: true })
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(root, 'package.json'),
|
|
78
|
+
JSON.stringify({ name: '@toa.io/extensions.exposition' })
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const before = normalised
|
|
82
|
+
const read = await manifest(component)
|
|
83
|
+
|
|
84
|
+
assert.strictEqual(normalised, before, 'the component was normalized')
|
|
85
|
+
assert.strictEqual(read.path, component)
|
|
86
|
+
assert.strictEqual(read.locator.id, 'identity.basic')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should normalize a component that only looks like one an extension ships', async () => {
|
|
90
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-manifest-test'))
|
|
91
|
+
const component = join(root, 'components', 'identity.basic')
|
|
92
|
+
|
|
93
|
+
await mkdir(component, { recursive: true })
|
|
94
|
+
// an application's own package, which the digest knows nothing about
|
|
95
|
+
await writeFile(join(root, 'package.json'), JSON.stringify({ name: 'acme' }))
|
|
96
|
+
|
|
97
|
+
const before = normalised
|
|
98
|
+
|
|
99
|
+
await manifest(component)
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(normalised, before + 1)
|
|
19
102
|
})
|
|
20
103
|
|
|
21
|
-
function mockComponent
|
|
104
|
+
function mockComponent() {
|
|
22
105
|
return { name: generate(), namespace: generate() }
|
|
23
106
|
}
|
package/src/map.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { gunzipSync } from 'node:zlib'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import * as core from '@toa.io/core'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What this process was given about the components it calls: the contract of the version each of
|
|
8
|
+
* them runs.
|
|
9
|
+
*
|
|
10
|
+
* Held as the file it was given rather than as what the file said, and read at every call for
|
|
11
|
+
* one: a deployment rewrites it under a running process, and a process may first call a peer
|
|
12
|
+
* long after that peer was replaced. A contract is read once per peer, so this is a read per
|
|
13
|
+
* peer.
|
|
14
|
+
*
|
|
15
|
+
* @type {string | Record<string, import('@toa.io/core').Contract> | undefined}
|
|
16
|
+
*/
|
|
17
|
+
let source
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What this process composes, which it knows from the sources it booted from. What the map
|
|
21
|
+
* states wins: two compositions of one locator run in one process while a rollout is staged,
|
|
22
|
+
* and the map is what says which of them a caller is held to.
|
|
23
|
+
*
|
|
24
|
+
* @type {Record<string, import('@toa.io/core').Contract>}
|
|
25
|
+
*/
|
|
26
|
+
let composed = {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string | Record<string, import('@toa.io/core').Contract> | undefined} value the file, or the map itself
|
|
30
|
+
*/
|
|
31
|
+
export const use = (value) => {
|
|
32
|
+
source = value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Forgets what this process composed, which is what a test does between one composition and
|
|
37
|
+
* the next: a component that is no longer composed is one nothing states.
|
|
38
|
+
*/
|
|
39
|
+
export const forget = () => {
|
|
40
|
+
composed = {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What this process composes, stated by the boot that composes it — and held to the map it was
|
|
45
|
+
* given a file of, which one `toa deploy` writes with the images it deploys: the two differ only
|
|
46
|
+
* where the map was not written again. What a test states instead is what that test means, and
|
|
47
|
+
* is not checked.
|
|
48
|
+
*
|
|
49
|
+
* @param {toa.norm.Component[]} manifests
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
export const compose = async (manifests) => {
|
|
53
|
+
const file = typeof source === 'string' ? await read(source) : undefined
|
|
54
|
+
|
|
55
|
+
for (const manifest of manifests) {
|
|
56
|
+
const stated = file?.[manifest.locator.id]
|
|
57
|
+
|
|
58
|
+
assert.ok(
|
|
59
|
+
stated === undefined || stated.version === manifest.version,
|
|
60
|
+
`'${manifest.locator.id}' is composed at a version the component map does not state. ` +
|
|
61
|
+
'Run `toa map`.'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
composed[manifest.locator.id] = core.contract.component(manifest)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What a component provides: what the map states of it, or what this process composes.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} id
|
|
72
|
+
* @returns {Promise<import('@toa.io/core').Contract | undefined>}
|
|
73
|
+
*/
|
|
74
|
+
export const contract = async (id) => {
|
|
75
|
+
if (source === undefined) return composed[id]
|
|
76
|
+
if (typeof source !== 'string') return source[id] ?? composed[id]
|
|
77
|
+
|
|
78
|
+
return (await read(source))[id] ?? composed[id]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A map is read gzipped where it is deployed and plain where it is not, so what it is is told by
|
|
83
|
+
* its first two bytes rather than by its name.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} path
|
|
86
|
+
* @returns {Promise<Record<string, import('@toa.io/core').Contract>>}
|
|
87
|
+
*/
|
|
88
|
+
async function read(path) {
|
|
89
|
+
let contents
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
contents = await readFile(path)
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
throw new Error(`Cannot read the component map '${path}'`, { cause })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (contents[0] === 0x1f && contents[1] === 0x8b) contents = gunzipSync(contents)
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
throw new Error(`The component map '${path}' is not readable gzip`, { cause })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(contents.toString('utf8'))
|
|
105
|
+
} catch (cause) {
|
|
106
|
+
throw new Error(`The component map '${path}' is not JSON`, { cause })
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/operation.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
Transition,
|
|
3
|
+
Observation,
|
|
4
|
+
Assignment,
|
|
5
|
+
Operation,
|
|
6
|
+
Query,
|
|
7
|
+
Effect,
|
|
8
|
+
Unmanaged
|
|
9
|
+
} from '@toa.io/core'
|
|
2
10
|
|
|
3
|
-
|
|
11
|
+
import * as boot from './index.js'
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
export async function operation(
|
|
14
|
+
manifest,
|
|
15
|
+
endpoint,
|
|
16
|
+
definition,
|
|
17
|
+
context,
|
|
18
|
+
scope,
|
|
19
|
+
preflight
|
|
20
|
+
) {
|
|
21
|
+
const cascade = await boot.cascade(manifest, endpoint, definition, context, preflight)
|
|
22
|
+
const reply = boot.contract.reply(definition.output, definition.errors)
|
|
10
23
|
const input = definition.input
|
|
11
|
-
const request = boot.contract.request(
|
|
24
|
+
const request = boot.contract.request(
|
|
25
|
+
{ input, stream: definition.stream },
|
|
26
|
+
manifest.entity
|
|
27
|
+
)
|
|
12
28
|
const contracts = { reply, request }
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
: new Query(manifest.entity.
|
|
29
|
+
|
|
30
|
+
const query =
|
|
31
|
+
manifest.entity === undefined ? undefined : new Query(manifest.entity.properties)
|
|
16
32
|
|
|
17
33
|
const Type = TYPES[definition.type]
|
|
18
34
|
|
|
@@ -24,7 +40,6 @@ const TYPES = {
|
|
|
24
40
|
observation: Observation,
|
|
25
41
|
assignment: Assignment,
|
|
26
42
|
computation: Operation,
|
|
27
|
-
effect:
|
|
43
|
+
effect: Effect,
|
|
44
|
+
unmanaged: Unmanaged
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
exports.operation = operation
|
package/src/outbox.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Outbox } from '@toa.io/core'
|
|
2
|
+
|
|
3
|
+
import * as boot from './index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A component with nothing to publish to has no outbox — the static check that keeps the
|
|
7
|
+
* common case free of a transaction. Its own events are one destination; an extension may
|
|
8
|
+
* contribute others, and a component that declares no event at all has an outbox where one of
|
|
9
|
+
* those is there.
|
|
10
|
+
*
|
|
11
|
+
* @param {toa.norm.Component} manifest
|
|
12
|
+
* @param {import('@toa.io/core/types').storages.Storage} [storage]
|
|
13
|
+
* @param {import('@toa.io/core').Emission} [emission]
|
|
14
|
+
* @param {import('@toa.io/core/types').outbox.Destination[]} [contributed]
|
|
15
|
+
*/
|
|
16
|
+
export const outbox = (manifest, storage, emission, contributed = []) => {
|
|
17
|
+
const destinations = emission === undefined ? contributed : [emission, ...contributed]
|
|
18
|
+
|
|
19
|
+
if (destinations.length === 0) return
|
|
20
|
+
|
|
21
|
+
return new Outbox(destinations, storage, boot.atomicity(manifest.locator.id), {})
|
|
22
|
+
}
|
package/src/rc.js
ADDED
package/src/receivers.js
CHANGED
|
@@ -1,34 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { Receiver, Locator } from '@toa.io/core'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import * as boot from './index.js'
|
|
5
|
+
import * as extensions from './extensions/index.js'
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
const extensions = require('./extensions')
|
|
7
|
-
|
|
8
|
-
const receivers = async (manifest, runtime) => {
|
|
7
|
+
export const receivers = async (manifest, component) => {
|
|
9
8
|
if (manifest.receivers === undefined) return []
|
|
10
9
|
|
|
11
10
|
const receivers = []
|
|
12
|
-
const local = await boot.remote(manifest.locator, manifest)
|
|
11
|
+
const local = await boot.remote(manifest.locator, undefined, { contract: manifest })
|
|
13
12
|
|
|
14
13
|
for (const [label, definition] of Object.entries(manifest.receivers)) {
|
|
15
|
-
const bridge = definition.bridge !== undefined ? boot.bridge.receiver(definition.bridge, manifest.path, label) : undefined
|
|
16
|
-
const receiver = new Receiver(definition, local, bridge)
|
|
17
|
-
const decorator = extensions.receiver(receiver, manifest.locator)
|
|
18
|
-
|
|
19
14
|
const locator = Locator.parse(label)
|
|
20
|
-
const transport = definition.binding ?? await resolveBinding(locator, label)
|
|
21
15
|
const source = definition.source ? Locator.parse(definition.source) : locator
|
|
22
|
-
const
|
|
16
|
+
const event = label.split('.').pop()
|
|
17
|
+
const destination = `${source.id}.${event}`
|
|
18
|
+
|
|
19
|
+
// the origin of the calls this receiver makes to its local operation
|
|
20
|
+
const origin = { namespace: source.namespace, component: source.name, event }
|
|
21
|
+
|
|
22
|
+
const bridge =
|
|
23
|
+
definition.bridge !== undefined
|
|
24
|
+
? await boot.bridge.receiver(definition.bridge, manifest.path, label)
|
|
25
|
+
: undefined
|
|
26
|
+
const receiver = new Receiver(
|
|
27
|
+
{ ...definition, label, destination, origin },
|
|
28
|
+
local,
|
|
29
|
+
bridge
|
|
30
|
+
)
|
|
31
|
+
const decorator = extensions.receiver(receiver, manifest.locator)
|
|
23
32
|
|
|
24
|
-
binding
|
|
33
|
+
const transport = definition.binding ?? (await resolveBinding(locator, label))
|
|
34
|
+
const binding = await boot.bindings.receive(
|
|
35
|
+
transport,
|
|
36
|
+
source,
|
|
37
|
+
label,
|
|
38
|
+
manifest.locator.id,
|
|
39
|
+
decorator
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
binding.depends(component)
|
|
25
43
|
receivers.push(binding)
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
return receivers
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
async function receive
|
|
49
|
+
export async function receive(label, group, callback) {
|
|
32
50
|
if (callback === undefined) {
|
|
33
51
|
callback = group
|
|
34
52
|
group = undefined
|
|
@@ -37,21 +55,33 @@ async function receive (label, group, callback) {
|
|
|
37
55
|
const locator = Locator.parse(label)
|
|
38
56
|
const transport = await resolveBinding(locator, label)
|
|
39
57
|
|
|
40
|
-
return boot.bindings.receive(transport, locator, label, group, callback)
|
|
58
|
+
return await boot.bindings.receive(transport, locator, label, group, callback)
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
/**
|
|
44
|
-
*
|
|
62
|
+
* Which transport carries an event, as the component whose event it is declares it.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('@toa.io/core').Locator} locator
|
|
45
65
|
* @param {string} label
|
|
46
66
|
* @return {Promise<string>}
|
|
47
67
|
*/
|
|
48
|
-
async function resolveBinding
|
|
68
|
+
async function resolveBinding(locator, label) {
|
|
49
69
|
const event = label.split('.').pop()
|
|
50
|
-
const
|
|
51
|
-
|
|
70
|
+
const contract = await boot.map.contract(locator.id)
|
|
71
|
+
|
|
72
|
+
assert.ok(
|
|
73
|
+
contract !== undefined,
|
|
74
|
+
`The component map states nothing of '${locator.id}', whose event '${label}' receives. ` +
|
|
75
|
+
'Run `toa map`.'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const { events, version } = contract
|
|
79
|
+
|
|
80
|
+
assert.ok(
|
|
81
|
+
events?.[event] !== undefined,
|
|
82
|
+
`'${locator.id}'${version === undefined ? '' : ` at version ${version}`} declares no ` +
|
|
83
|
+
`event '${event}', which is what '${label}' receives`
|
|
84
|
+
)
|
|
52
85
|
|
|
53
86
|
return events[event].binding
|
|
54
87
|
}
|
|
55
|
-
|
|
56
|
-
exports.receivers = receivers
|
|
57
|
-
exports.receive = receive
|
package/src/regional.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Regional } from '@toa.io/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gives every destination that carries a change to other regions the others that write to
|
|
5
|
+
* something each region keeps for itself. Neither knows what the other is: the carrier is given
|
|
6
|
+
* what they export, and hands back what arrives.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('@toa.io/core/types').outbox.Destination[]} destinations
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
export const regional = (destinations) => {
|
|
12
|
+
const regionals = destinations.filter((destination) => Regional.is(destination))
|
|
13
|
+
|
|
14
|
+
if (regionals.length === 0) return
|
|
15
|
+
|
|
16
|
+
for (const destination of destinations)
|
|
17
|
+
if (destination.carries === true) destination.regional = new Regional(regionals)
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { regional } from './regional.js'
|
|
5
|
+
|
|
6
|
+
const regionalDestination = (name) => ({
|
|
7
|
+
name,
|
|
8
|
+
export: async () => [name],
|
|
9
|
+
import: async () => undefined
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should give a carrier the regional destinations', async () => {
|
|
13
|
+
const carrier = { name: 'convergence', carries: true }
|
|
14
|
+
const events = { name: 'events' }
|
|
15
|
+
|
|
16
|
+
regional([events, carrier, regionalDestination('realtime')])
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(await carrier.regional.export({}), { realtime: ['realtime'] })
|
|
19
|
+
assert.equal('regional' in events, false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should give a carrier nothing where no destination is regional', () => {
|
|
23
|
+
const carrier = { name: 'convergence', carries: true }
|
|
24
|
+
|
|
25
|
+
regional([carrier, { name: 'events' }])
|
|
26
|
+
|
|
27
|
+
assert.equal(carrier.regional, undefined)
|
|
28
|
+
})
|
package/src/remote.js
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { Remote, contract as coreContract, exceptions } from '@toa.io/core'
|
|
2
|
+
import { remap } from '@toa.io/generic'
|
|
3
|
+
|
|
4
|
+
import * as boot from './index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('@toa.io/core').Locator} locator
|
|
8
|
+
* @param {import('@toa.io/core/types').Source} [source] the origin stamped on every call made through this remote
|
|
9
|
+
* @param {{ contract?: import('@toa.io/core').Contract }} [options]
|
|
10
|
+
* `contract` is what the caller already has — a manifest of its own is one, and so is what a
|
|
11
|
+
* tenant announced — and absent it, what the map this process was given states.
|
|
12
|
+
*/
|
|
13
|
+
export const remote = async (locator, source, options = {}) => {
|
|
14
|
+
const stated = options.contract ?? (await boot.map.contract(locator.id))
|
|
15
|
+
|
|
16
|
+
if (stated === undefined)
|
|
17
|
+
throw new exceptions.UnstatedException(
|
|
18
|
+
`Cannot call '${locator.id}': the component map states nothing of it. Run \`toa map\`.`
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// a contract states once, or not at all, what the runtime gives every component; the calls
|
|
22
|
+
// are made from the whole of it
|
|
23
|
+
const contract = coreContract.restore(stated)
|
|
24
|
+
|
|
25
|
+
// a call binds its consumers, which are loaded rather than required
|
|
26
|
+
const calls =
|
|
27
|
+
contract.operations === undefined
|
|
28
|
+
? {}
|
|
29
|
+
: await settle(
|
|
30
|
+
remap(contract.operations, (definition, endpoint) =>
|
|
31
|
+
boot.call(locator, endpoint, definition, contract.entity, source)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return new Remote(locator, calls)
|
|
36
|
+
}
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
/** An object whose values are promises, as an object of what they resolve to. */
|
|
39
|
+
async function settle(object) {
|
|
40
|
+
const entries = Object.entries(object)
|
|
41
|
+
const values = await Promise.all(entries.map(([, promise]) => promise))
|
|
21
42
|
|
|
22
|
-
return
|
|
43
|
+
return Object.fromEntries(entries.map(([key], index) => [key, values[index]]))
|
|
23
44
|
}
|
|
24
|
-
|
|
25
|
-
exports.remote = remote
|