@toa.io/boot 1.0.0-alpha.30 → 1.0.0-alpha.300
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 +124 -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 +8 -8
- 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 +40 -18
- package/src/bindings/receive.js +3 -6
- package/src/bridge.js +22 -16
- package/src/call.js +8 -9
- package/src/cascade.js +16 -10
- package/src/component.js +85 -32
- package/src/composition.js +104 -15
- package/src/context.js +7 -8
- package/src/contract.js +11 -12
- package/src/discovery.js +29 -18
- 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 +36 -11
- package/src/extensions/storage.js +6 -9
- package/src/extensions/tenants.js +5 -9
- package/src/guards.js +20 -0
- package/src/host.js +17 -0
- package/src/inbox.js +10 -0
- package/src/index.js +52 -32
- package/src/manifest.js +78 -36
- package/src/manifest.test.js +85 -8
- package/src/operation.js +25 -13
- package/src/outbox.js +22 -0
- package/src/rc.js +5 -0
- package/src/receivers.js +35 -21
- package/src/remote.js +38 -17
- package/src/span.js +19 -0
- package/src/storage.js +50 -16
- package/src/storage.test.js +57 -0
- package/src/workload.js +84 -0
- package/types/bindings.d.ts +6 -2
- package/types/index.d.ts +24 -10
- package/types/manifest.d.ts +5 -4
- package/src/exposition.js +0 -17
package/src/extensions/load.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { resolve } = require('./resolve')
|
|
1
|
+
import { resolve } from './resolve.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* @param {toa.norm.Component} manifest
|
|
7
5
|
*/
|
|
8
|
-
const load = (manifest) => {
|
|
6
|
+
export const load = async (manifest) => {
|
|
9
7
|
if (manifest.extensions === undefined) return
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
await Promise.all(
|
|
10
|
+
Object.keys(manifest.extensions).map(
|
|
11
|
+
async (name) => await resolve(name, manifest.path)
|
|
12
|
+
)
|
|
13
|
+
)
|
|
12
14
|
}
|
|
13
|
-
|
|
14
|
-
exports.load = load
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { instances } from './instances.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import('@toa.io/core').Connector} composition
|
|
5
|
+
* @returns {import('@toa.io/core').Connector}
|
|
6
|
+
*/
|
|
7
|
+
export const manage = (composition) => {
|
|
8
|
+
let managed = composition
|
|
9
|
+
|
|
10
|
+
for (const factory of Object.values(instances)) {
|
|
11
|
+
if (factory.manage !== undefined) managed = factory.manage(managed)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return managed
|
|
15
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { instances } = require('./instances')
|
|
1
|
+
import { instances } from './instances.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
* @param {toa.core.Receiver} receiver
|
|
7
|
-
* @param {toa.core.Locator} locator
|
|
8
|
-
* @returns {toa.core.Receiver}
|
|
4
|
+
* @param {import('@toa.io/core/types').Receiver} receiver
|
|
5
|
+
* @param {import('@toa.io/core').Locator} locator
|
|
6
|
+
* @returns {import('@toa.io/core/types').Receiver}
|
|
9
7
|
*/
|
|
10
|
-
const receiver = (receiver, locator) => {
|
|
8
|
+
export const receiver = (receiver, locator) => {
|
|
11
9
|
let decorated = receiver
|
|
12
10
|
|
|
13
11
|
for (const factory of Object.values(instances)) {
|
|
@@ -16,5 +14,3 @@ const receiver = (receiver, locator) => {
|
|
|
16
14
|
|
|
17
15
|
return decorated
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
exports.receiver = receiver
|
|
@@ -1,27 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { find } from '@toa.io/generic'
|
|
4
|
+
import * as boot from '../../src/index.js'
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
const boot = require('../..')
|
|
6
|
+
import { instances } from './instances.js'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
// what `find` returns is a package directory, and a module is loaded by file
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
|
|
11
|
+
// an extension is loaded once, and what is remembered while it loads is the
|
|
12
|
+
// promise, so two components cannot each make a factory
|
|
13
|
+
const loading = {}
|
|
7
14
|
|
|
8
15
|
/**
|
|
16
|
+
* Loads an extension and remembers its factory.
|
|
17
|
+
*
|
|
9
18
|
* @param {string} reference
|
|
10
19
|
* @param {string} base
|
|
11
|
-
* @returns {toa.core.extensions.Factory}
|
|
20
|
+
* @returns {Promise<import('@toa.io/core/types').extensions.Factory>}
|
|
12
21
|
*/
|
|
13
|
-
const resolve = (reference, base = process.cwd()) => {
|
|
22
|
+
export const resolve = async (reference, base = process.cwd()) => {
|
|
14
23
|
const path = find(reference, base)
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
loading[path] ??= create(path)
|
|
26
|
+
instances[path] = await loading[path]
|
|
17
27
|
|
|
18
28
|
return instances[path]
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The factory of an extension already loaded. Everything that decorates what a
|
|
33
|
+
* component is made of runs after its extensions are loaded, and stays
|
|
34
|
+
* synchronous.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} reference
|
|
37
|
+
* @param {string} base
|
|
38
|
+
* @returns {import('@toa.io/core/types').extensions.Factory}
|
|
39
|
+
*/
|
|
40
|
+
export const instance = (reference, base = process.cwd()) => {
|
|
41
|
+
const factory = instances[find(reference, base)]
|
|
42
|
+
|
|
43
|
+
if (factory === undefined) throw new Error(`Extension '${reference}' is not loaded`)
|
|
23
44
|
|
|
24
|
-
return
|
|
45
|
+
return factory
|
|
25
46
|
}
|
|
26
47
|
|
|
27
|
-
|
|
48
|
+
const create = async (path) => {
|
|
49
|
+
const { Factory } = await import(pathToFileURL(require.resolve(path)).href)
|
|
50
|
+
|
|
51
|
+
return new Factory(boot.host())
|
|
52
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { instances } = require('./instances')
|
|
1
|
+
import { instances } from './instances.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
* @param {toa.core.Storage} storage
|
|
7
|
-
* @
|
|
4
|
+
* @param {import('@toa.io/core/types').storages.Storage} storage
|
|
5
|
+
* @param {import('@toa.io/core').Locator} locator whose storage it is
|
|
6
|
+
* @returns {import('@toa.io/core/types').storages.Storage}
|
|
8
7
|
*/
|
|
9
|
-
const storage = (storage) => {
|
|
8
|
+
export const storage = (storage, locator) => {
|
|
10
9
|
let decorated = storage
|
|
11
10
|
|
|
12
11
|
for (const factory of Object.values(instances)) {
|
|
13
|
-
if (factory.storage !== undefined) decorated = factory.storage(decorated)
|
|
12
|
+
if (factory.storage !== undefined) decorated = factory.storage(decorated, locator)
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
return decorated
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
exports.storage = storage
|
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { resolve } = require('./resolve')
|
|
1
|
+
import { resolve } from './resolve.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* @param {toa.norm.Component} manifest
|
|
7
|
-
* @returns {toa.core.Connector[]}
|
|
5
|
+
* @returns {import('@toa.io/core').Connector[]}
|
|
8
6
|
*/
|
|
9
|
-
const tenants = (manifest) => {
|
|
7
|
+
export const tenants = async (manifest) => {
|
|
10
8
|
const tenants = []
|
|
11
9
|
|
|
12
10
|
if (manifest.extensions === undefined) return tenants
|
|
13
11
|
|
|
14
12
|
for (const [name, declaration] of Object.entries(manifest.extensions)) {
|
|
15
|
-
const factory = resolve(name, manifest.path)
|
|
13
|
+
const factory = await resolve(name, manifest.path)
|
|
16
14
|
|
|
17
15
|
if (factory.tenant === undefined) continue
|
|
18
16
|
|
|
19
|
-
const tenant = factory.tenant(manifest.locator, declaration)
|
|
17
|
+
const tenant = await factory.tenant(manifest.locator, declaration, manifest)
|
|
20
18
|
|
|
21
19
|
tenants.push(tenant)
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
return tenants
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
exports.tenants = tenants
|
package/src/guards.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Guard } from '@toa.io/core'
|
|
2
|
+
import * as boot from './index.js'
|
|
3
|
+
|
|
4
|
+
export async function guards(manifest, context) {
|
|
5
|
+
if (manifest.guards === undefined) return
|
|
6
|
+
|
|
7
|
+
const entries = Object.entries(manifest.guards)
|
|
8
|
+
|
|
9
|
+
return await Promise.all(
|
|
10
|
+
entries.map(async ([name, guard]) => {
|
|
11
|
+
const bridge = await load(guard, name, context)
|
|
12
|
+
|
|
13
|
+
return new Guard(name, bridge)
|
|
14
|
+
})
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function load(guard, name, context) {
|
|
19
|
+
return await boot.bridge.guard(guard.bridge, guard.path, name, context)
|
|
20
|
+
}
|
package/src/host.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as boot from './index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the process hosting an extension provides to it. The counterpart of a component's
|
|
5
|
+
* context: an extension reaches the core through this and through nothing else.
|
|
6
|
+
*
|
|
7
|
+
* @returns {import('@toa.io/core/types').extensions.Host}
|
|
8
|
+
*/
|
|
9
|
+
export const host = () => ({
|
|
10
|
+
remote: boot.remote,
|
|
11
|
+
broadcast: boot.bindings.broadcast,
|
|
12
|
+
composition: boot.composition,
|
|
13
|
+
receive: boot.receive,
|
|
14
|
+
atom: boot.atomicity,
|
|
15
|
+
outbound: boot.bindings.outbound,
|
|
16
|
+
inbound: boot.bindings.inbound
|
|
17
|
+
})
|
package/src/inbox.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether any operation of this component declares `once`, which is what decides that a place
|
|
3
|
+
* to record a call is made at all. Local to the manifest, unlike the events something consumes,
|
|
4
|
+
* so there is no variable a deployment computes.
|
|
5
|
+
*
|
|
6
|
+
* @param {toa.norm.Component} manifest
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export const inbox = (manifest) =>
|
|
10
|
+
Object.values(manifest.operations ?? {}).some((definition) => definition.once === true)
|
package/src/index.js
CHANGED
|
@@ -1,36 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
// the environment is read at module scope elsewhere, so it is loaded first
|
|
2
|
+
import { load as env } from './env.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import * as bindings from './bindings/index.js'
|
|
5
|
+
import * as bridge from './bridge.js'
|
|
6
|
+
import * as contract from './contract.js'
|
|
7
|
+
import * as discovery from './discovery.js'
|
|
8
|
+
import * as extensions from './extensions/index.js'
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
import { call } from './call.js'
|
|
11
|
+
import { cascade } from './cascade.js'
|
|
12
|
+
import { component } from './component.js'
|
|
13
|
+
import { composition } from './composition.js'
|
|
14
|
+
import { context } from './context.js'
|
|
15
|
+
import { emission } from './emission.js'
|
|
16
|
+
import { events } from './events.js'
|
|
17
|
+
import { inbox } from './inbox.js'
|
|
18
|
+
import { outbox } from './outbox.js'
|
|
19
|
+
import { atomicity } from './atomicity.js'
|
|
20
|
+
import { atom } from './atom.js'
|
|
21
|
+
import { manifest } from './manifest.js'
|
|
22
|
+
import { operation } from './operation.js'
|
|
23
|
+
import { receivers, receive } from './receivers.js'
|
|
24
|
+
import { remote } from './remote.js'
|
|
25
|
+
import { storage } from './storage.js'
|
|
26
|
+
import { guards } from './guards.js'
|
|
27
|
+
import { host } from './host.js'
|
|
28
|
+
import { rc } from './rc.js'
|
|
29
|
+
import { Workload } from './workload.js'
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
export {
|
|
32
|
+
call,
|
|
33
|
+
env,
|
|
34
|
+
cascade,
|
|
35
|
+
component,
|
|
36
|
+
composition,
|
|
37
|
+
context,
|
|
38
|
+
emission,
|
|
39
|
+
events,
|
|
40
|
+
inbox,
|
|
41
|
+
outbox,
|
|
42
|
+
atomicity,
|
|
43
|
+
atom,
|
|
44
|
+
host,
|
|
45
|
+
manifest,
|
|
46
|
+
operation,
|
|
47
|
+
receivers,
|
|
48
|
+
receive,
|
|
49
|
+
remote,
|
|
50
|
+
storage,
|
|
51
|
+
guards,
|
|
52
|
+
rc,
|
|
53
|
+
Workload
|
|
54
|
+
}
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
exports.cascade = cascade
|
|
26
|
-
exports.component = component
|
|
27
|
-
exports.composition = composition
|
|
28
|
-
exports.context = context
|
|
29
|
-
exports.emission = emission
|
|
30
|
-
exports.exposition = exposition
|
|
31
|
-
exports.manifest = manifest
|
|
32
|
-
exports.operation = operation
|
|
33
|
-
exports.receivers = receivers
|
|
34
|
-
exports.receive = receive
|
|
35
|
-
exports.remote = remote
|
|
36
|
-
exports.storage = storage
|
|
56
|
+
export { bindings, bridge, contract, discovery, extensions }
|
package/src/manifest.js
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
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, definition, revive, NORMALIZED } from '@toa.io/norm'
|
|
7
|
+
import { Locator } from '@toa.io/core'
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
const { merge } = require('@toa.io/generic')
|
|
5
|
-
const { component: load } = require('@toa.io/norm')
|
|
6
|
-
const { Locator } = require('@toa.io/core')
|
|
9
|
+
import { span } from './span.js'
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
* @type {toa.boot.Manifest}
|
|
10
|
-
*/
|
|
11
|
-
const manifest = async (path, options = {}) => {
|
|
11
|
+
export const manifest = async (path, options = {}) => {
|
|
12
12
|
options = merge(clone(options), DEFAULTS)
|
|
13
13
|
|
|
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
|
-
}
|
|
14
|
+
const manifest = await span(
|
|
15
|
+
{ name: `manifest ${basename(path)}`, attributes: { path } },
|
|
16
|
+
async () => await read(path)
|
|
17
|
+
)
|
|
38
18
|
|
|
39
19
|
if (manifest.extensions === undefined) manifest.extensions = {}
|
|
40
20
|
|
|
@@ -44,13 +24,75 @@ const manifest = async (path, options = {}) => {
|
|
|
44
24
|
}
|
|
45
25
|
}
|
|
46
26
|
|
|
47
|
-
if ('storage' in options && 'entity' in manifest)
|
|
27
|
+
if ('storage' in options && 'entity' in manifest)
|
|
28
|
+
manifest.entity.storage = options.storage
|
|
48
29
|
|
|
49
30
|
manifest.locator = new Locator(manifest.name, manifest.namespace)
|
|
50
31
|
|
|
51
32
|
return manifest
|
|
52
33
|
}
|
|
53
34
|
|
|
54
|
-
|
|
35
|
+
/**
|
|
36
|
+
* A manifest already normalised, where there is one, and the sources read where there is not.
|
|
37
|
+
*
|
|
38
|
+
* A component of an application's own comes with the manifest its build wrote beside it. A
|
|
39
|
+
* component an extension ships comes with none — its package is published once, long before
|
|
40
|
+
* any image is built — but `@toa.io/definitions` carries the digest of those manifests, which
|
|
41
|
+
* a deploy has read for as long as it has been unable to install the extension. A service runs
|
|
42
|
+
* them from the package, so it reads the same digest rather than reading their sources again.
|
|
43
|
+
*
|
|
44
|
+
* Only a workspace normalises, then, which is the one place a source can have changed since.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} path
|
|
47
|
+
*/
|
|
48
|
+
async function read(path) {
|
|
49
|
+
const file = join(path, NORMALIZED)
|
|
50
|
+
|
|
51
|
+
if (existsSync(file)) return revive(JSON.parse(await readFile(file, 'utf8')), path)
|
|
52
|
+
|
|
53
|
+
return (await digested(path)) ?? (await load(path))
|
|
54
|
+
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* What the digest holds for a component of an extension's own, by the directory it is in.
|
|
58
|
+
*
|
|
59
|
+
* An extension ships its components in `components/` beside its manifest and nowhere else, so
|
|
60
|
+
* that is the whole of what is looked for: anything else — an application's own component, a
|
|
61
|
+
* fixture in a workspace — is read from its sources.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} path
|
|
64
|
+
* @returns {Promise<object | undefined>}
|
|
65
|
+
*/
|
|
66
|
+
async function digested(path) {
|
|
67
|
+
const directory = dirname(path)
|
|
68
|
+
|
|
69
|
+
if (basename(directory) !== COMPONENTS) return undefined
|
|
70
|
+
|
|
71
|
+
const root = dirname(directory)
|
|
72
|
+
const manifest = join(root, MANIFEST)
|
|
73
|
+
|
|
74
|
+
if (!existsSync(manifest)) return undefined
|
|
75
|
+
|
|
76
|
+
const { name } = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
77
|
+
|
|
78
|
+
// the digest is of what Toa ships, and a package it does not define has none
|
|
79
|
+
if (name === undefined || !name.startsWith(SCOPE)) return undefined
|
|
80
|
+
|
|
81
|
+
const { module } = await definition(name)
|
|
82
|
+
const { labels, manifests } = module.components?.() ?? {}
|
|
83
|
+
|
|
84
|
+
if (manifests === undefined) return undefined
|
|
85
|
+
|
|
86
|
+
// a chart names a component's workload the way the digest labels it
|
|
87
|
+
const index = labels.indexOf(basename(path).replace('.', '-'))
|
|
88
|
+
|
|
89
|
+
return index === -1 ? undefined : revive(manifests[index], path)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SCOPE = '@toa.io/'
|
|
93
|
+
|
|
94
|
+
const COMPONENTS = 'components'
|
|
95
|
+
|
|
96
|
+
const MANIFEST = 'package.json'
|
|
97
|
+
|
|
98
|
+
const DEFAULTS = {}
|
package/src/manifest.test.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
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 = { labels: ['identity-basic'], manifests: [{ name: 'basic', namespace: 'identity' }] }
|
|
14
|
+
|
|
15
|
+
mock.module('@toa.io/norm', {
|
|
16
|
+
namedExports: {
|
|
17
|
+
component: () => {
|
|
18
|
+
normalised++
|
|
19
|
+
|
|
20
|
+
return mockComponent()
|
|
21
|
+
},
|
|
22
|
+
definition: async (reference) => ({
|
|
23
|
+
module: reference === '@toa.io/extensions.exposition' ? { components: () => digest } : {}
|
|
24
|
+
}),
|
|
25
|
+
revive: (declared, path) => ({ ...declared, path }),
|
|
26
|
+
NORMALIZED
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const { manifest } = await import('./manifest.js')
|
|
10
31
|
|
|
11
32
|
const path = generate()
|
|
12
33
|
|
|
@@ -15,9 +36,65 @@ it('should not modify options', async () => {
|
|
|
15
36
|
|
|
16
37
|
await manifest(path, options)
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
assert.deepStrictEqual(options.extensions.length, 2)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should normalize a component the build did not', async () => {
|
|
43
|
+
const before = normalised
|
|
44
|
+
|
|
45
|
+
await manifest(path)
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(normalised, before + 1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should read what the build wrote, and not normalize', async () => {
|
|
51
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-manifest-test'))
|
|
52
|
+
const declared = { name: 'tasks', namespace: 'todos', operations: { compute: {} } }
|
|
53
|
+
|
|
54
|
+
await writeFile(join(root, NORMALIZED), JSON.stringify(declared))
|
|
55
|
+
|
|
56
|
+
const before = normalised
|
|
57
|
+
const read = await manifest(root)
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(normalised, before, 'the component was normalized')
|
|
60
|
+
assert.strictEqual(read.path, root)
|
|
61
|
+
assert.deepStrictEqual(read.operations, declared.operations)
|
|
62
|
+
assert.strictEqual(read.locator.id, 'todos.tasks')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should read the digest for a component an extension ships, and not normalize', async () => {
|
|
66
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-manifest-test'))
|
|
67
|
+
const component = join(root, 'components', 'identity.basic')
|
|
68
|
+
|
|
69
|
+
await mkdir(component, { recursive: true })
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(root, 'package.json'),
|
|
72
|
+
JSON.stringify({ name: '@toa.io/extensions.exposition' })
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const before = normalised
|
|
76
|
+
const read = await manifest(component)
|
|
77
|
+
|
|
78
|
+
assert.strictEqual(normalised, before, 'the component was normalized')
|
|
79
|
+
assert.strictEqual(read.path, component)
|
|
80
|
+
assert.strictEqual(read.locator.id, 'identity.basic')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should normalize a component that only looks like one an extension ships', async () => {
|
|
84
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-manifest-test'))
|
|
85
|
+
const component = join(root, 'components', 'identity.basic')
|
|
86
|
+
|
|
87
|
+
await mkdir(component, { recursive: true })
|
|
88
|
+
// an application's own package, which the digest knows nothing about
|
|
89
|
+
await writeFile(join(root, 'package.json'), JSON.stringify({ name: 'acme' }))
|
|
90
|
+
|
|
91
|
+
const before = normalised
|
|
92
|
+
|
|
93
|
+
await manifest(component)
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(normalised, before + 1)
|
|
19
96
|
})
|
|
20
97
|
|
|
21
|
-
function mockComponent
|
|
98
|
+
function mockComponent() {
|
|
22
99
|
return { name: generate(), namespace: generate() }
|
|
23
100
|
}
|
package/src/operation.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
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
24
|
const request = boot.contract.request({ input }, manifest.entity)
|
|
12
25
|
const contracts = { reply, request }
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
: new Query(manifest.entity.
|
|
26
|
+
|
|
27
|
+
const query =
|
|
28
|
+
manifest.entity === undefined ? undefined : new Query(manifest.entity.properties)
|
|
16
29
|
|
|
17
30
|
const Type = TYPES[definition.type]
|
|
18
31
|
|
|
@@ -24,7 +37,6 @@ const TYPES = {
|
|
|
24
37
|
observation: Observation,
|
|
25
38
|
assignment: Assignment,
|
|
26
39
|
computation: Operation,
|
|
27
|
-
effect:
|
|
40
|
+
effect: Effect,
|
|
41
|
+
unmanaged: Unmanaged
|
|
28
42
|
}
|
|
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
|
+
}
|