@toa.io/boot 1.0.0-alpha.29 → 1.0.0-alpha.291

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 (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/package.json +10 -8
  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/index.js +5 -13
  11. package/src/bindings/produce.js +29 -18
  12. package/src/bindings/receive.js +3 -6
  13. package/src/bridge.js +22 -16
  14. package/src/call.js +6 -9
  15. package/src/cascade.js +16 -10
  16. package/src/component.js +79 -32
  17. package/src/composition.js +104 -15
  18. package/src/context.js +7 -8
  19. package/src/contract.js +11 -12
  20. package/src/discovery.js +29 -18
  21. package/src/emission.js +21 -14
  22. package/src/env.js +19 -12
  23. package/src/events.js +27 -0
  24. package/src/extensions/aspects.js +4 -8
  25. package/src/extensions/component.js +4 -8
  26. package/src/extensions/context.js +4 -8
  27. package/src/extensions/emitter.js +7 -9
  28. package/src/extensions/index.js +9 -19
  29. package/src/extensions/instances.js +2 -4
  30. package/src/extensions/load.js +7 -7
  31. package/src/extensions/manage.js +15 -0
  32. package/src/extensions/receiver.js +5 -9
  33. package/src/extensions/resolve.js +36 -11
  34. package/src/extensions/storage.js +4 -8
  35. package/src/extensions/tenants.js +5 -9
  36. package/src/guards.js +20 -0
  37. package/src/host.js +15 -0
  38. package/src/index.js +48 -32
  39. package/src/manifest.js +25 -16
  40. package/src/manifest.test.js +7 -8
  41. package/src/operation.js +25 -13
  42. package/src/outbox.js +17 -0
  43. package/src/rc.js +5 -0
  44. package/src/receivers.js +35 -21
  45. package/src/remote.js +38 -17
  46. package/src/span.js +19 -0
  47. package/src/storage.js +35 -14
  48. package/src/storage.test.js +57 -0
  49. package/types/bindings.d.ts +6 -2
  50. package/types/index.d.ts +19 -10
  51. package/types/manifest.d.ts +5 -4
  52. package/src/exposition.js +0 -17
@@ -1,24 +1,113 @@
1
- 'use strict'
1
+ import { console } from 'openspan'
2
+ import { Composition } from '@toa.io/core'
3
+ import { version } from '@toa.io/runtime'
4
+ import { environment } from '@toa.io/generic'
2
5
 
3
- const { Composition } = require('@toa.io/core')
6
+ import * as boot from './index.js'
7
+ import { span } from './span.js'
4
8
 
5
- const boot = require('./index')
9
+ export async function composition(paths, options) {
10
+ // what a test set after this package loaded goes to the store now, before a module of a
11
+ // component is imported: nothing of it is left in `process.env` for that module to read
12
+ environment.absorb()
6
13
 
7
- async function composition (paths, options) {
8
14
  options = Object.assign({}, options)
9
15
 
10
- const manifests = await Promise.all(paths.map((path) => boot.manifest(path, options)))
11
- const tenants = await Promise.all(manifests.map(boot.extensions.tenants))
12
- const expositions = await Promise.all(manifests.map(boot.discovery.expose))
13
- const components = await Promise.all(manifests.map(boot.component))
16
+ return span('boot composition', async () => {
17
+ const manifests = await span(
18
+ 'load manifests',
19
+ async () => await Promise.all(paths.map((path) => boot.manifest(path, options)))
20
+ )
14
21
 
15
- const producers = components.map((component, index) =>
16
- boot.bindings.produce(component, manifests[index].operations))
22
+ console.info('Starting composition', {
23
+ runtime: version,
24
+ components: manifests.map((manifest) => manifest.locator.id)
25
+ })
17
26
 
18
- const receivers = await Promise.all(components.map((component, index) =>
19
- boot.receivers(manifests[index], component)))
27
+ const tenants = await span(
28
+ 'create tenants',
29
+ async () => await Promise.all(manifests.map(boot.extensions.tenants))
30
+ )
20
31
 
21
- return new Composition(expositions.flat(), producers.flat(), receivers.flat(), tenants.flat())
22
- }
32
+ const expositions = await span(
33
+ 'expose discovery',
34
+ async () => await Promise.all(manifests.map(boot.discovery.expose))
35
+ )
36
+
37
+ try {
38
+ const components = await span(
39
+ 'create components',
40
+ async () => await Promise.all(manifests.map(boot.component))
41
+ )
42
+
43
+ const groups = await Promise.all(
44
+ components.map(
45
+ async (component, index) =>
46
+ await boot.bindings.produce(component, manifests[index].operations)
47
+ )
48
+ )
49
+
50
+ const receivers = await span(
51
+ 'create receivers',
52
+ async () =>
53
+ await Promise.all(
54
+ components.map((component, index) =>
55
+ boot.receivers(manifests[index], component)
56
+ )
57
+ )
58
+ )
59
+
60
+ const producers = []
61
+ const settles = []
62
+
63
+ for (let i = 0; i < components.length; i++) {
64
+ const { local, other } = groups[i]
65
+ const settle = components[i].settle
66
+ const serving = [...local, ...other]
67
+
68
+ producers.push(...serving)
69
+
70
+ /*
71
+ * A receiver turns an event into a call to an operation of its own component, and
72
+ * that call is served by these — the loop producer first, the broker's if it has
73
+ * already gone. They are siblings under the composition otherwise, so a receiver
74
+ * still draining a delivery would be left calling something already torn down.
75
+ */
76
+ // one at a time: `depends` given an array links the group it makes rather than its
77
+ // members, and it is the members whose teardown has to wait for this one
78
+ for (const receiver of receivers[i])
79
+ for (const producer of serving) receiver.depends(producer)
23
80
 
24
- exports.composition = composition
81
+ if (settle === undefined) continue
82
+
83
+ if (local.length > 0) settle.depends(local)
84
+
85
+ for (const producer of other) producer.depends(settle)
86
+
87
+ for (const receiver of receivers[i]) receiver.depends(settle)
88
+
89
+ settles.push(settle)
90
+ }
91
+
92
+ const composition = new Composition(
93
+ expositions.flat(),
94
+ producers.concat(settles),
95
+ receivers.flat(),
96
+ tenants.flat()
97
+ )
98
+
99
+ /*
100
+ * A lookup that is never answered holds a connection open, and whoever
101
+ * awaits it has nothing to disconnect — the remote it would belong to does
102
+ * not exist yet. The composition owns the process's discovery instead, so
103
+ * an unanswered lookup cannot outlive it.
104
+ */
105
+ composition.depends(await boot.discovery.discovery())
106
+
107
+ return boot.extensions.manage(composition)
108
+ } catch (exception) {
109
+ await Promise.all(expositions.flat().map((connector) => connector.disconnect(true)))
110
+ throw exception
111
+ }
112
+ })
113
+ }
package/src/context.js CHANGED
@@ -1,13 +1,14 @@
1
- 'use strict'
1
+ import { Context, Locator } from '@toa.io/core'
2
2
 
3
- const { Context, Locator } = require('@toa.io/core')
3
+ import * as boot from './index.js'
4
4
 
5
- const boot = require('./index')
6
-
7
- const context = async (manifest) => {
8
- const local = await boot.remote(manifest.locator, manifest)
5
+ export const context = async (manifest) => {
6
+ const local = await boot.remote(manifest.locator, undefined, manifest)
9
7
  const aspects = boot.extensions.aspects(manifest)
10
8
 
9
+ // a system aspect: it is there in every component, declared by none of them
10
+ aspects.push(boot.atom(manifest.locator.id))
11
+
11
12
  const lookup = async (namespace, name) => {
12
13
  const locator = new Locator(name, namespace)
13
14
  const remote = await boot.remote(locator)
@@ -21,5 +22,3 @@ const context = async (manifest) => {
21
22
 
22
23
  return boot.extensions.context(context)
23
24
  }
24
-
25
- exports.context = context
package/src/contract.js CHANGED
@@ -1,21 +1,20 @@
1
- 'use strict'
1
+ import { contract } from '@toa.io/core'
2
+ import * as schemas from '@toa.io/schemas'
2
3
 
3
- const { contract: { Request, Reply } } = require('@toa.io/core')
4
- const { Schema } = require('@toa.io/schema')
4
+ const { Request, Reply } = contract
5
5
 
6
- const request = (definition, entity) => {
6
+ export const request = (definition, entity) => {
7
7
  const request = Request.schema(definition, entity)
8
- const schema = new Schema(request, { removeAdditional: true }) // soft inputs
8
+ const schema = schemas.schema(request)
9
9
 
10
- return new Request(schema)
10
+ return new Request(schema, definition, entity)
11
11
  }
12
12
 
13
- const reply = (output, error) => {
14
- const reply = Reply.schema(output, error)
15
- const schema = new Schema(reply) // outputs strict
13
+ export const reply = (output, errors) => {
14
+ const reply = Reply.schema(output, errors)
15
+
16
+ // a reply is read, not shaped: a value that only fits once coerced does not fit
17
+ const schema = schemas.schema(reply, { coerceTypes: false })
16
18
 
17
19
  return new Reply(schema)
18
20
  }
19
-
20
- exports.request = request
21
- exports.reply = reply
package/src/discovery.js CHANGED
@@ -1,35 +1,49 @@
1
- 'use strict'
1
+ import { Discovery, Exposition } from '@toa.io/core'
2
2
 
3
- const { Discovery, Exposition } = require('@toa.io/core')
4
-
5
- const boot = require('./index')
3
+ import * as boot from './index.js'
6
4
 
7
5
  let promise
8
6
  let instance = null
9
7
 
10
- const discovery = async () => {
11
- if (instance === null) {
12
- instance = new Discovery(lookup)
13
- promise = instance.connect()
8
+ /**
9
+ * The process's discovery, for as long as it is connected.
10
+ *
11
+ * A disconnected one is replaced rather than reused. A lookup becomes a dependency of the
12
+ * discovery that made it and nothing takes one back, so an instance carried across a
13
+ * disconnection would reconnect every call it has ever made beside the ones the composition now
14
+ * booting asks for — and every disconnection after that would walk all of them again.
15
+ */
16
+ export const discovery = async () => {
17
+ while (true) {
18
+ if (instance === null) {
19
+ instance = new Discovery(lookup)
20
+ promise = instance.connect()
21
+ }
22
+
23
+ const current = instance
24
+
25
+ await promise
26
+
27
+ if (current.connected) return current
28
+
29
+ // whoever replaced it first wins, and the loop reads what they left
30
+ if (instance === current) instance = null
14
31
  }
15
-
16
- await promise
17
-
18
- return instance
19
32
  }
20
33
 
21
34
  const lookup = async (locator) => {
22
- const call = boot.call(locator, ENDPOINT, { bindings: BINDINGS })
35
+ const call = await boot.call(locator, ENDPOINT, { bindings: BINDINGS })
23
36
 
24
37
  await call.connect()
25
38
 
26
39
  return call
27
40
  }
28
41
 
29
- const expose = async (manifest) => {
42
+ export const expose = async (manifest) => {
30
43
  const exposition = new Exposition(manifest.locator, manifest)
31
44
  const operations = { [ENDPOINT]: { bindings: BINDINGS } }
32
- const producers = boot.bindings.produce(exposition, operations)
45
+ const { local, other } = await boot.bindings.produce(exposition, operations)
46
+ const producers = local.concat(other)
33
47
 
34
48
  await Promise.all(producers.map((producer) => producer.connect()))
35
49
 
@@ -38,6 +52,3 @@ const expose = async (manifest) => {
38
52
 
39
53
  const BINDINGS = ['@toa.io/bindings.amqp']
40
54
  const ENDPOINT = '.lookup'
41
-
42
- exports.discovery = discovery
43
- exports.expose = expose
package/src/emission.js CHANGED
@@ -1,22 +1,29 @@
1
- 'use strict'
1
+ import { Emission, Event } from '@toa.io/core'
2
2
 
3
- const { Emission, Event } = 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 emission = (definitions, locator, context) => {
6
+ export const emission = async (definitions, locator, context) => {
9
7
  if (definitions === undefined) return
10
8
 
11
- const events = Object.entries(definitions).map(([label, definition]) => {
12
- const emitter = boot.bindings.emit(definition.binding, locator, label)
13
- const decorator = extensions.emitter(emitter, label)
14
- const bridge = boot.bridge.event(definition.bridge, definition.path, label, context)
9
+ const events = await Promise.all(
10
+ Object.entries(definitions).map(async ([label, definition]) => {
11
+ const emitter = await boot.bindings.emit(definition.binding, locator, label)
12
+ const decorator = extensions.emitter(emitter, label, locator)
13
+ const bridge = await boot.bridge.event(
14
+ definition.bridge,
15
+ definition.path,
16
+ label,
17
+ context
18
+ )
15
19
 
16
- return new Event(definition, decorator, bridge)
17
- })
20
+ return new Event(
21
+ { ...definition, label: `${locator.id}.${label}` },
22
+ decorator,
23
+ bridge
24
+ )
25
+ })
26
+ )
18
27
 
19
28
  return new Emission(events)
20
29
  }
21
-
22
- exports.emission = emission
package/src/env.js CHANGED
@@ -1,15 +1,22 @@
1
- 'use strict'
1
+ import { readFileSync } from 'node:fs'
2
+ import dotenv from 'dotenv'
3
+ import { environment, findUp } from '@toa.io/generic'
2
4
 
3
- const { file } = require('@toa.io/filesystem')
4
-
5
- async function setup () {
6
- const path = await file.dot('env')
7
-
8
- if (path !== undefined) require('dotenv').config({ path })
5
+ /*
6
+ * A local run reads what `toa env` wrote. What the file says goes to the environment store
7
+ * and never into `process.env`: a `.env` is where a local run keeps its secrets, and a
8
+ * component's code must not find them. A variable already set is left alone, as `dotenv`
9
+ * left it, so what the environment says still wins — and reading the file costs nothing
10
+ * where there is none: a deployment has no `.env` to find.
11
+ *
12
+ * It used to be read only where `TOA_ENV` was unset, taking that one variable as proof the
13
+ * environment had been injected. Setting it for any other reason then dropped the file
14
+ * whole, and every variable in it with it.
15
+ */
16
+ export function load(path) {
17
+ environment.absorbEntries(dotenv.parse(readFileSync(path, 'utf8')))
9
18
  }
10
19
 
11
- if (!('TOA_ENV' in process.env)) {
12
- (async () => {
13
- await setup()
14
- })()
15
- }
20
+ const path = findUp('.env')
21
+
22
+ if (path !== undefined) load(path)
package/src/events.js ADDED
@@ -0,0 +1,27 @@
1
+ import { environment } from '@toa.io/generic'
2
+
3
+ /**
4
+ * The events of this component that something consumes. An event nobody consumes is not
5
+ * published, so it gets no emitter and no exchange, and a component none of whose events are
6
+ * consumed gets no emission — and therefore no outbox and no transaction.
7
+ *
8
+ * The deployment computes this from the whole context. Without one — a local run, `toa mono`,
9
+ * a composition booted in a test — the variable is absent and every event is published.
10
+ *
11
+ * @param {toa.norm.Component} manifest
12
+ * @returns {toa.norm.Events | undefined}
13
+ */
14
+ export const events = (manifest) => {
15
+ if (manifest.events === undefined) return
16
+
17
+ const value = environment.get(VARIABLE + manifest.locator.uppercase)
18
+
19
+ if (value === undefined) return manifest.events
20
+
21
+ const consumed = new Set(value.split(' ').filter(Boolean))
22
+ const entries = Object.entries(manifest.events).filter(([label]) => consumed.has(label))
23
+
24
+ return entries.length === 0 ? undefined : Object.fromEntries(entries)
25
+ }
26
+
27
+ const VARIABLE = 'TOA_EVENTS_'
@@ -1,18 +1,16 @@
1
- 'use strict'
2
-
3
- const { resolve } = require('./resolve')
1
+ import { instance } from './resolve.js'
4
2
 
5
3
  /**
6
4
  * @param {toa.norm.Component} manifest
7
- * @returns {toa.core.extensions.Aspect[]}
5
+ * @returns {import('@toa.io/core/types').extensions.Aspect[]}
8
6
  */
9
- const aspects = (manifest) => {
7
+ export const aspects = (manifest) => {
10
8
  const aspects = []
11
9
 
12
10
  if (manifest.extensions === undefined) return aspects
13
11
 
14
12
  for (const [name, declaration] of Object.entries(manifest.extensions)) {
15
- const factory = resolve(name, manifest.path)
13
+ const factory = instance(name, manifest.path)
16
14
 
17
15
  if (factory.aspect === undefined) continue
18
16
 
@@ -24,5 +22,3 @@ const aspects = (manifest) => {
24
22
 
25
23
  return aspects
26
24
  }
27
-
28
- exports.aspects = aspects
@@ -1,12 +1,10 @@
1
- 'use strict'
2
-
3
- const { instances } = require('./instances')
1
+ import { instances } from './instances.js'
4
2
 
5
3
  /**
6
- * @param {toa.core.Component} component
7
- * @returns {toa.core.Component}
4
+ * @param {import('@toa.io/core').Component} component
5
+ * @returns {import('@toa.io/core').Component}
8
6
  */
9
- const component = (component) => {
7
+ export const component = (component) => {
10
8
  let decorated = component
11
9
 
12
10
  for (const factory of Object.values(instances)) {
@@ -15,5 +13,3 @@ const component = (component) => {
15
13
 
16
14
  return decorated
17
15
  }
18
-
19
- exports.component = component
@@ -1,12 +1,10 @@
1
- 'use strict'
2
-
3
- const { instances } = require('./instances')
1
+ import { instances } from './instances.js'
4
2
 
5
3
  /**
6
- * @param {toa.core.Context} component
7
- * @returns {toa.core.Context}
4
+ * @param {import('@toa.io/core').Context} component
5
+ * @returns {import('@toa.io/core').Context}
8
6
  */
9
- const context = (component) => {
7
+ export const context = (component) => {
10
8
  let decorated = component
11
9
 
12
10
  for (const factory of Object.values(instances)) {
@@ -15,5 +13,3 @@ const context = (component) => {
15
13
 
16
14
  return decorated
17
15
  }
18
-
19
- exports.context = context
@@ -1,20 +1,18 @@
1
- 'use strict'
2
-
3
- const { instances } = require('./instances')
1
+ import { instances } from './instances.js'
4
2
 
5
3
  /**
6
- * @param {toa.core.bindings.Emitter} emitter
4
+ * @param {import('@toa.io/core/types').bindings.Emitter} emitter
7
5
  * @param {string} label
8
- * @returns {toa.core.bindings.Emitter}
6
+ * @param {import('@toa.io/core').Locator} locator
7
+ * @returns {import('@toa.io/core/types').bindings.Emitter}
9
8
  */
10
- const emitter = (emitter, label) => {
9
+ export const emitter = (emitter, label, locator) => {
11
10
  let decorated = emitter
12
11
 
13
12
  for (const factory of Object.values(instances)) {
14
- if (factory.emitter !== undefined) decorated = factory.emitter(decorated, label)
13
+ if (factory.emitter !== undefined)
14
+ decorated = factory.emitter(decorated, label, locator)
15
15
  }
16
16
 
17
17
  return decorated
18
18
  }
19
-
20
- exports.emitter = emitter
@@ -1,19 +1,9 @@
1
- 'use strict'
2
-
3
- const { aspects } = require('./aspects')
4
- const { component } = require('./component')
5
- const { context } = require('./context')
6
- const { emitter } = require('./emitter')
7
- const { load } = require('./load')
8
- const { receiver } = require('./receiver')
9
- const { storage } = require('./storage')
10
- const { tenants } = require('./tenants')
11
-
12
- exports.aspects = aspects
13
- exports.component = component
14
- exports.context = context
15
- exports.emitter = emitter
16
- exports.load = load
17
- exports.receiver = receiver
18
- exports.storage = storage
19
- exports.tenants = tenants
1
+ export { aspects } from './aspects.js'
2
+ export { component } from './component.js'
3
+ export { context } from './context.js'
4
+ export { emitter } from './emitter.js'
5
+ export { load } from './load.js'
6
+ export { manage } from './manage.js'
7
+ export { receiver } from './receiver.js'
8
+ export { storage } from './storage.js'
9
+ export { tenants } from './tenants.js'
@@ -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
+ }