@toa.io/boot 1.0.0-alpha.3 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/package.json +12 -11
  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 +8 -8
  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 +40 -18
  14. package/src/bindings/receive.js +3 -6
  15. package/src/bridge.js +22 -16
  16. package/src/call.js +8 -9
  17. package/src/cascade.js +16 -10
  18. package/src/component.js +83 -25
  19. package/src/composition.js +104 -15
  20. package/src/context.js +7 -8
  21. package/src/contract.js +11 -12
  22. package/src/discovery.js +29 -18
  23. package/src/emission.js +21 -14
  24. package/src/env.js +19 -12
  25. package/src/events.js +27 -0
  26. package/src/extensions/aspects.js +4 -8
  27. package/src/extensions/component.js +4 -8
  28. package/src/extensions/context.js +4 -8
  29. package/src/extensions/destinations.js +27 -0
  30. package/src/extensions/emitter.js +7 -9
  31. package/src/extensions/index.js +10 -19
  32. package/src/extensions/instances.js +2 -4
  33. package/src/extensions/load.js +7 -7
  34. package/src/extensions/manage.js +15 -0
  35. package/src/extensions/receiver.js +5 -9
  36. package/src/extensions/resolve.js +36 -11
  37. package/src/extensions/storage.js +6 -9
  38. package/src/extensions/tenants.js +5 -9
  39. package/src/guards.js +20 -0
  40. package/src/host.js +17 -0
  41. package/src/inbox.js +10 -0
  42. package/src/index.js +52 -32
  43. package/src/manifest.js +80 -34
  44. package/src/manifest.test.js +85 -8
  45. package/src/operation.js +25 -13
  46. package/src/outbox.js +22 -0
  47. package/src/rc.js +5 -0
  48. package/src/receivers.js +35 -21
  49. package/src/remote.js +38 -17
  50. package/src/span.js +19 -0
  51. package/src/storage.js +51 -32
  52. package/src/storage.test.js +57 -0
  53. package/src/workload.js +84 -0
  54. package/types/bindings.d.ts +6 -2
  55. package/types/index.d.ts +24 -10
  56. package/types/manifest.d.ts +5 -4
  57. package/src/exposition.js +0 -17
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
1
  /**
4
- * @type {{ [key: string]: toa.core.extensions.Factory}}
2
+ * @type {{ [key: string]: import('@toa.io/core/types').extensions.Factory}}
5
3
  */
6
- exports.instances = {}
4
+ export const instances = {}
@@ -1,14 +1,14 @@
1
- 'use strict'
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
- for (const name of Object.keys(manifest.extensions)) resolve(name, manifest.path)
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
- 'use strict'
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
- 'use strict'
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
- const { directory: { find } } = require('@toa.io/filesystem')
4
- const boot = require('../..')
6
+ import { instances } from './instances.js'
5
7
 
6
- const { instances } = require('./instances')
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
- if (instances[path] === undefined) instances[path] = create(path)
25
+ loading[path] ??= create(path)
26
+ instances[path] = await loading[path]
17
27
 
18
28
  return instances[path]
19
29
  }
20
30
 
21
- const create = (path) => {
22
- const { Factory } = require(path)
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 new Factory(boot)
45
+ return factory
25
46
  }
26
47
 
27
- exports.resolve = resolve
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
- 'use strict'
2
-
3
- const { instances } = require('./instances')
1
+ import { instances } from './instances.js'
4
2
 
5
3
  /**
6
- * @param {toa.core.Storage} storage
7
- * @returns {toa.core.Storage}
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
- 'use strict'
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
- 'use strict'
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
- require('./env')
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
- const { call } = require('./call')
6
- const { cascade } = require('./cascade')
7
- const { component } = require('./component')
8
- const { composition } = require('./composition')
9
- const { context } = require('./context')
10
- const { emission } = require('./emission')
11
- const { exposition } = require('./exposition')
12
- const { manifest } = require('./manifest')
13
- const { operation } = require('./operation')
14
- const { receivers, receive } = require('./receivers')
15
- const { remote } = require('./remote')
16
- const { storage } = require('./storage')
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
- exports.bindings = require('./bindings')
19
- exports.bridge = require('./bridge')
20
- exports.contract = require('./contract')
21
- exports.discovery = require('./discovery')
22
- exports.extensions = require('./extensions')
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
- exports.call = call
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,52 +1,98 @@
1
- 'use strict'
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
- const clone = require('clone-deep')
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'
10
+
11
+ export const manifest = async (path, options = {}) => {
12
+ options = merge(clone(options), DEFAULTS)
13
+
14
+ const manifest = await span(
15
+ { name: `manifest ${basename(path)}`, attributes: { path } },
16
+ async () => await read(path)
17
+ )
18
+
19
+ if (manifest.extensions === undefined) manifest.extensions = {}
20
+
21
+ if (options.extensions !== undefined) {
22
+ for (const extension of options.extensions) {
23
+ if (!(extension in manifest.extensions)) manifest.extensions[extension] = null
24
+ }
25
+ }
26
+
27
+ if ('storage' in options && 'entity' in manifest)
28
+ manifest.entity.storage = options.storage
29
+
30
+ manifest.locator = new Locator(manifest.name, manifest.namespace)
31
+
32
+ return manifest
33
+ }
7
34
 
8
35
  /**
9
- * @type {toa.boot.Manifest}
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
10
47
  */
11
- const manifest = async (path, options = {}) => {
12
- options = merge(clone(options), DEFAULTS)
48
+ async function read(path) {
49
+ const file = join(path, NORMALIZED)
13
50
 
14
- const manifest = await load(path)
51
+ if (existsSync(file)) return revive(JSON.parse(await readFile(file, 'utf8')), path)
15
52
 
16
- if (options?.bindings !== undefined) {
17
- if ('operations' in manifest)
18
- for (const operation of Object.values(manifest.operations))
19
- operation.bindings = options.bindings
53
+ return (await digested(path)) ?? (await load(path))
54
+ }
20
55
 
21
- const check = (binding) => require(binding).properties?.async === true
22
- const asyncBinding = options.bindings.find(check)
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)
23
68
 
24
- if (asyncBinding === undefined) throw new Error('Bindings override must contain at least one async binding')
69
+ if (basename(directory) !== COMPONENTS) return undefined
25
70
 
26
- if ('events' in manifest)
27
- for (const event of Object.values(manifest.events)) event.binding = asyncBinding
71
+ const root = dirname(directory)
72
+ const manifest = join(root, MANIFEST)
28
73
 
29
- if ('receivers' in manifest)
30
- for (const receiver of Object.values(manifest.receivers))
31
- if (receiver.source === undefined) receiver.binding = asyncBinding
32
- }
74
+ if (!existsSync(manifest)) return undefined
33
75
 
34
- if (manifest.extensions === undefined) manifest.extensions = {}
76
+ const { name } = JSON.parse(readFileSync(manifest, 'utf8'))
35
77
 
36
- // add `null` manifests
37
- for (const extension of options.extensions) {
38
- if (!(extension in manifest.extensions)) manifest.extensions[extension] = null
39
- }
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
40
80
 
41
- if ('storage' in options && 'entity' in manifest) manifest.entity.storage = options.storage
81
+ const { module } = await definition(name)
82
+ const { labels, manifests } = module.components?.() ?? {}
42
83
 
43
- manifest.locator = new Locator(manifest.name, manifest.namespace)
84
+ if (manifests === undefined) return undefined
44
85
 
45
- return manifest
46
- }
86
+ // a chart names a component's workload the way the digest labels it
87
+ const index = labels.indexOf(basename(path).replace('.', '-'))
47
88
 
48
- const DEFAULTS = {
49
- extensions: ['@toa.io/extensions.sampling']
89
+ return index === -1 ? undefined : revive(manifests[index], path)
50
90
  }
51
91
 
52
- exports.manifest = manifest
92
+ const SCOPE = '@toa.io/'
93
+
94
+ const COMPONENTS = 'components'
95
+
96
+ const MANIFEST = 'package.json'
97
+
98
+ const DEFAULTS = {}
@@ -1,12 +1,33 @@
1
- 'use strict'
1
+ import { it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { generate } = require('randomstring')
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
- jest.mock('@toa.io/norm', () => ({
6
- component: () => mockComponent()
7
- }))
9
+ const NORMALIZED = 'manifest.toa.json'
8
10
 
9
- const { manifest } = require('./manifest')
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
- expect(options.extensions.length).toStrictEqual(2)
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
- 'use strict'
1
+ import {
2
+ Transition,
3
+ Observation,
4
+ Assignment,
5
+ Operation,
6
+ Query,
7
+ Effect,
8
+ Unmanaged
9
+ } from '@toa.io/core'
2
10
 
3
- const { Transition, Observation, Assignment, Operation, Query } = require('@toa.io/core')
11
+ import * as boot from './index.js'
4
12
 
5
- const boot = require('./index')
6
-
7
- const operation = (manifest, endpoint, definition, context, scope) => {
8
- const cascade = boot.cascade(manifest, endpoint, definition, context)
9
- const reply = boot.contract.reply(definition.output, definition.error)
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
- const query = manifest.entity === undefined
14
- ? undefined
15
- : new Query(manifest.entity.schema.properties)
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: Operation
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
+ }
package/src/rc.js ADDED
@@ -0,0 +1,5 @@
1
+ import * as boot from './index.js'
2
+
3
+ export async function rc(manifest, context) {
4
+ return boot.bridge.rc(manifest.bridge, manifest.path, context)
5
+ }