@toa.io/boot 1.0.0-alpha.28 → 1.0.0-alpha.282

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 (50) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/package.json +10 -8
  3. package/src/atom.js +37 -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 +5 -8
  8. package/src/bindings/emit.js +3 -6
  9. package/src/bindings/factory.js +5 -9
  10. package/src/bindings/index.js +5 -13
  11. package/src/bindings/produce.js +32 -19
  12. package/src/bindings/receive.js +3 -6
  13. package/src/bridge.js +23 -16
  14. package/src/call.js +6 -9
  15. package/src/cascade.js +6 -10
  16. package/src/component.js +56 -32
  17. package/src/composition.js +81 -16
  18. package/src/context.js +7 -8
  19. package/src/contract.js +9 -12
  20. package/src/discovery.js +7 -11
  21. package/src/emission.js +10 -14
  22. package/src/env.js +4 -5
  23. package/src/events.js +25 -0
  24. package/src/exposition.js +3 -7
  25. package/src/extensions/aspects.js +3 -7
  26. package/src/extensions/component.js +2 -6
  27. package/src/extensions/context.js +2 -6
  28. package/src/extensions/emitter.js +4 -7
  29. package/src/extensions/index.js +9 -19
  30. package/src/extensions/instances.js +1 -3
  31. package/src/extensions/load.js +6 -8
  32. package/src/extensions/manage.js +16 -0
  33. package/src/extensions/receiver.js +4 -7
  34. package/src/extensions/resolve.js +36 -11
  35. package/src/extensions/storage.js +2 -6
  36. package/src/extensions/tenants.js +4 -8
  37. package/src/guards.js +19 -0
  38. package/src/host.js +15 -0
  39. package/src/index.js +28 -32
  40. package/src/manifest.js +19 -14
  41. package/src/manifest.test.js +7 -7
  42. package/src/operation.js +7 -9
  43. package/src/outbox.js +17 -0
  44. package/src/rc.js +5 -0
  45. package/src/receivers.js +21 -18
  46. package/src/remote.js +28 -10
  47. package/src/span.js +17 -0
  48. package/src/storage.js +19 -13
  49. package/types/index.d.ts +7 -5
  50. package/types/manifest.d.ts +1 -1
package/src/index.js CHANGED
@@ -1,36 +1,32 @@
1
- 'use strict'
1
+ // the environment is read at module scope elsewhere, so it is loaded first
2
+ import './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 { outbox } from './outbox.js'
18
+ import { atomicity } from './atomicity.js'
19
+ import { atom } from './atom.js'
20
+ import { exposition } from './exposition.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'
17
29
 
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')
30
+ export { call, cascade, component, composition, context, emission, events, outbox, atomicity, atom, exposition, host, manifest, operation, receivers, receive, remote, storage, guards, rc }
23
31
 
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
32
+ export { bindings, bridge, contract, discovery, extensions }
package/src/manifest.js CHANGED
@@ -1,17 +1,16 @@
1
- 'use strict'
1
+ import clone from 'clone-deep'
2
+ import { basename } from 'node:path'
3
+ import { merge } from '@toa.io/generic'
4
+ import { component as load } from '@toa.io/norm'
5
+ import { Locator } from '@toa.io/core'
2
6
 
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')
7
+ import { span } from './span.js'
7
8
 
8
- /**
9
- * @type {toa.boot.Manifest}
10
- */
11
- const manifest = async (path, options = {}) => {
9
+ export const manifest = async (path, options = {}) => {
12
10
  options = merge(clone(options), DEFAULTS)
13
11
 
14
- const manifest = await load(path)
12
+ const manifest = await span({ name: `manifest ${basename(path)}`, attributes: { path } },
13
+ async () => await load(path))
15
14
 
16
15
  if (options?.bindings !== undefined) {
17
16
  if ('operations' in manifest) {
@@ -20,8 +19,16 @@ const manifest = async (path, options = {}) => {
20
19
  }
21
20
  }
22
21
 
23
- const check = (binding) => require(binding).properties?.async === true
24
- const asyncBinding = options.bindings.find(check)
22
+ let asyncBinding
23
+
24
+ for (const binding of options.bindings) {
25
+ const { properties } = await import(binding)
26
+
27
+ if (properties?.async === true) {
28
+ asyncBinding = binding
29
+ break
30
+ }
31
+ }
25
32
 
26
33
  if (asyncBinding === undefined) throw new Error('Bindings override must contain at least one async binding')
27
34
 
@@ -52,5 +59,3 @@ const manifest = async (path, options = {}) => {
52
59
  }
53
60
 
54
61
  const DEFAULTS = {}
55
-
56
- exports.manifest = manifest
@@ -1,12 +1,12 @@
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 { generate } from 'randomstring'
4
5
 
5
- jest.mock('@toa.io/norm', () => ({
6
- component: () => mockComponent()
7
- }))
6
+ mock.module('@toa.io/norm', { namedExports: { component: () => mockComponent() } })
7
+
8
+ const { manifest } = await import('./manifest.js')
8
9
 
9
- const { manifest } = require('./manifest')
10
10
 
11
11
  const path = generate()
12
12
 
@@ -15,7 +15,7 @@ it('should not modify options', async () => {
15
15
 
16
16
  await manifest(path, options)
17
17
 
18
- expect(options.extensions.length).toStrictEqual(2)
18
+ assert.deepStrictEqual(options.extensions.length, 2)
19
19
  })
20
20
 
21
21
  function mockComponent () {
package/src/operation.js CHANGED
@@ -1,15 +1,14 @@
1
- 'use strict'
1
+ import { Transition, Observation, Assignment, Operation, Query, Effect, Unmanaged } from '@toa.io/core'
2
2
 
3
- const { Transition, Observation, Assignment, Operation, Query } = require('@toa.io/core')
3
+ import * as boot from './index.js'
4
4
 
5
- const boot = require('./index')
6
-
7
- const operation = (manifest, endpoint, definition, context, scope) => {
8
- const cascade = boot.cascade(manifest, endpoint, definition, context)
5
+ export async function operation (manifest, endpoint, definition, context, scope, preflight) {
6
+ const cascade = await boot.cascade(manifest, endpoint, definition, context, preflight)
9
7
  const reply = boot.contract.reply(definition.output, definition.error)
10
8
  const input = definition.input
11
9
  const request = boot.contract.request({ input }, manifest.entity)
12
10
  const contracts = { reply, request }
11
+
13
12
  const query = manifest.entity === undefined
14
13
  ? undefined
15
14
  : new Query(manifest.entity.schema.properties)
@@ -24,7 +23,6 @@ const TYPES = {
24
23
  observation: Observation,
25
24
  assignment: Assignment,
26
25
  computation: Operation,
27
- effect: Operation
26
+ effect: Effect,
27
+ unmanaged: Unmanaged
28
28
  }
29
-
30
- exports.operation = operation
package/src/outbox.js ADDED
@@ -0,0 +1,17 @@
1
+ import { Outbox } from '@toa.io/core'
2
+
3
+ import * as boot from './index.js'
4
+
5
+ /**
6
+ * A component with no declared events has nothing to publish and therefore no outbox — the
7
+ * static check that keeps the common case free of a transaction.
8
+ *
9
+ * @param {toa.norm.Component} manifest
10
+ * @param {toa.core.Storage} [storage]
11
+ * @param {toa.core.Emission} [emission]
12
+ */
13
+ export const outbox = (manifest, storage, emission) => {
14
+ if (emission === undefined) return
15
+
16
+ return new Outbox(emission, storage, boot.atomicity(manifest.locator.id), {})
17
+ }
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
+ }
package/src/receivers.js CHANGED
@@ -1,34 +1,40 @@
1
- 'use strict'
1
+ import { Receiver, Locator } from '@toa.io/core'
2
2
 
3
- const { Receiver, Locator } = require('@toa.io/core')
3
+ import * as boot from './index.js'
4
+ import * as extensions from './extensions/index.js'
4
5
 
5
- const boot = require('./index')
6
- const extensions = require('./extensions')
7
-
8
- const receivers = async (manifest, runtime) => {
6
+ export const receivers = async (manifest, component) => {
9
7
  if (manifest.receivers === undefined) return []
10
8
 
11
9
  const receivers = []
12
- const local = await boot.remote(manifest.locator, manifest)
10
+ const local = await boot.remote(manifest.locator, undefined, manifest)
13
11
 
14
12
  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)
13
+ const locator = Locator.parse(label)
14
+ const source = definition.source ? Locator.parse(definition.source) : locator
15
+ const event = label.split('.').pop()
16
+ const destination = `${source.id}.${event}`
17
+
18
+ // the origin of the calls this receiver makes to its local operation
19
+ const origin = { namespace: source.namespace, component: source.name, event }
20
+
21
+ const bridge = definition.bridge !== undefined
22
+ ? await boot.bridge.receiver(definition.bridge, manifest.path, label)
23
+ : undefined
24
+ const receiver = new Receiver({ ...definition, label, destination, origin }, local, bridge)
17
25
  const decorator = extensions.receiver(receiver, manifest.locator)
18
26
 
19
- const locator = Locator.parse(label)
20
27
  const transport = definition.binding ?? await resolveBinding(locator, label)
21
- const source = definition.source ? Locator.parse(definition.source) : locator
22
- const binding = boot.bindings.receive(transport, source, label, manifest.locator.id, decorator)
28
+ const binding = await boot.bindings.receive(transport, source, label, manifest.locator.id, decorator)
23
29
 
24
- binding.depends(runtime)
30
+ binding.depends(component)
25
31
  receivers.push(binding)
26
32
  }
27
33
 
28
34
  return receivers
29
35
  }
30
36
 
31
- async function receive (label, group, callback) {
37
+ export async function receive (label, group, callback) {
32
38
  if (callback === undefined) {
33
39
  callback = group
34
40
  group = undefined
@@ -37,7 +43,7 @@ async function receive (label, group, callback) {
37
43
  const locator = Locator.parse(label)
38
44
  const transport = await resolveBinding(locator, label)
39
45
 
40
- return boot.bindings.receive(transport, locator, label, group, callback)
46
+ return await boot.bindings.receive(transport, locator, label, group, callback)
41
47
  }
42
48
 
43
49
  /**
@@ -52,6 +58,3 @@ async function resolveBinding (locator, label) {
52
58
 
53
59
  return events[event].binding
54
60
  }
55
-
56
- exports.receivers = receivers
57
- exports.receive = receive
package/src/remote.js CHANGED
@@ -1,25 +1,43 @@
1
- 'use strict'
1
+ import { console } from 'openspan'
2
+ import { Remote } from '@toa.io/core'
3
+ import { remap } from '@toa.io/generic'
2
4
 
3
- const { Remote } = require('@toa.io/core')
4
- const { remap } = require('@toa.io/generic')
5
+ import * as boot from './index.js'
5
6
 
6
- const boot = require('./index')
7
+ /**
8
+ * @param {toa.core.Locator} locator
9
+ * @param {toa.core.Source} [source] the origin stamped on every call made through this remote
10
+ * @param {toa.norm.Component} [manifest] skips discovery when the manifest is already known
11
+ */
12
+ export const remote = async (locator, source, manifest) => {
13
+ let discovery
7
14
 
8
- const remote = async (locator, manifest = undefined) => {
9
- const discovery = await boot.discovery.discovery()
15
+ if (manifest === undefined) {
16
+ console.debug('Lookup', { locator: locator.id })
10
17
 
11
- if (manifest === undefined) manifest = await discovery.lookup(locator)
18
+ discovery = await boot.discovery.discovery(locator)
19
+ manifest = await discovery.lookup(locator)
20
+ }
12
21
 
22
+ // a call binds its consumers, which are loaded rather than required
13
23
  const calls = manifest.operations === undefined
14
24
  ? {}
15
- : remap(manifest.operations, (definition, endpoint) => boot.call(locator, endpoint, definition, manifest.entity))
25
+ : await settle(remap(manifest.operations,
26
+ (definition, endpoint) => boot.call(locator, endpoint, definition, manifest.entity, source)))
16
27
 
17
28
  const remote = new Remote(locator, calls)
18
29
 
19
30
  // ensure discovery shutdown
20
- remote.depends(discovery)
31
+ if (discovery !== undefined)
32
+ remote.depends(discovery)
21
33
 
22
34
  return remote
23
35
  }
24
36
 
25
- exports.remote = remote
37
+ /** An object whose values are promises, as an object of what they resolve to. */
38
+ async function settle (object) {
39
+ const entries = Object.entries(object)
40
+ const values = await Promise.all(entries.map(([, promise]) => promise))
41
+
42
+ return Object.fromEntries(entries.map(([key], index) => [key, values[index]]))
43
+ }
package/src/span.js ADDED
@@ -0,0 +1,17 @@
1
+ import { console, traces } from 'openspan'
2
+
3
+ const enabled = process.env.TOA_BOOT_TRACE === '1'
4
+
5
+ // Tracing is off until an exporter is configured, and the CLI opens its own span before
6
+ // any extension gets to configure one — so asking for a boot trace turns the console
7
+ // exporter on here. Extensions keep it: see `development()` in the telemetry factory.
8
+ if (enabled) traces({ exporters: { console: {} } })
9
+
10
+ /**
11
+ * Creates a span if boot tracing is enabled (TOA_BOOT_TRACE=1), otherwise just runs the task
12
+ *
13
+ * @param {string | object} options
14
+ * @param {() => Promise<any>} task
15
+ * @returns {Promise<any>}
16
+ */
17
+ export const span = async (options, task) => enabled ? console.span(options, task) : task()
package/src/storage.js CHANGED
@@ -1,26 +1,32 @@
1
- 'use strict'
2
-
3
- const { join } = require('node:path')
4
- const extensions = require('./extensions')
5
-
6
- const storage = (manifest) => {
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
+ */
14
+ export const storage = async (manifest, outbox) => {
7
15
  if (manifest.entity === undefined) return
8
16
 
9
- const Factory = load(manifest)
17
+ const Factory = await load(manifest)
10
18
 
11
19
  /** @type {toa.core.storages.Factory} */
12
20
  const factory = new Factory()
13
- const storage = factory.storage(manifest.locator, manifest.entity)
21
+ const storage = factory.storage(manifest.locator, manifest.entity, { outbox })
14
22
 
15
23
  return extensions.storage(storage)
16
24
  }
17
25
 
18
- function load (component) {
26
+ async function load (component) {
19
27
  const reference = component.entity.storage
20
- const path = require.resolve(reference, { paths: [component.path, __dirname] })
21
- const { Factory } = require(path)
28
+ const path = require.resolve(reference, { paths: [component.path, import.meta.dirname] })
29
+ const { Factory } = await import(pathToFileURL(path).href)
22
30
 
23
31
  return Factory
24
32
  }
25
-
26
- exports.storage = storage
package/types/index.d.ts CHANGED
@@ -1,21 +1,23 @@
1
1
  import * as core from '@toa.io/core'
2
- import * as composition from './composition'
2
+ import * as composition from './composition.js'
3
3
 
4
- export * as bindings from './bindings'
4
+ export * as bindings from './bindings.js'
5
5
 
6
6
  export async function composition (paths: string[], options?: composition.Options): Promise<core.Connector>
7
7
 
8
- export async function remote (locator: core.Locator): Promise<core.Component>
8
+ export async function remote (locator: core.Locator, source?: core.Source): Promise<core.Remote>
9
9
 
10
10
  export async function receive<T = any> (
11
11
  label: string,
12
- receiver: Receiver,
12
+ receiver: Receiver
13
13
  ): Promise<core.Connector>
14
14
 
15
15
  export async function receive<T = any> (
16
16
  label: string,
17
17
  group: string | undefined,
18
- receiver: Receiver,
18
+ receiver: Receiver
19
19
  ): Promise<core.Connector>
20
20
 
21
21
  type Receiver = { receive: (message: core.Message<T>) => void | Promise<void> }
22
+
23
+ export function host (): core.extensions.Host
@@ -1,5 +1,5 @@
1
1
  import * as _norm from '@toa.io/norm/types'
2
- import * as _composition from './composition'
2
+ import * as _composition from './composition.js'
3
3
 
4
4
  declare namespace toa.boot {
5
5