@toa.io/core 1.0.0-alpha.27 → 1.0.0-alpha.270

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 (63) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/package.json +7 -9
  3. package/src/call.js +21 -1
  4. package/src/cascade.js +7 -5
  5. package/src/component.js +59 -15
  6. package/src/composition.js +1 -1
  7. package/src/connector.js +28 -17
  8. package/src/context.js +6 -0
  9. package/src/contract/contract.js +22 -0
  10. package/src/contract/reply.js +26 -9
  11. package/src/contract/request.js +19 -5
  12. package/src/contract/schemas/index.js +1 -0
  13. package/src/contract/schemas/query.yaml +14 -1
  14. package/src/contract/schemas/source.yaml +23 -0
  15. package/src/discovery.js +13 -7
  16. package/src/effect.js +19 -0
  17. package/src/entities/changeset.js +5 -8
  18. package/src/entities/entity.js +76 -24
  19. package/src/entities/factory.js +17 -6
  20. package/src/entities/newid.js +11 -0
  21. package/src/entities/set.js +13 -0
  22. package/src/event.js +19 -1
  23. package/src/exceptions.js +26 -19
  24. package/src/exposition.js +3 -2
  25. package/src/guard.js +17 -0
  26. package/src/index.js +6 -0
  27. package/src/observation.js +1 -9
  28. package/src/operation.js +38 -8
  29. package/src/query/options.js +3 -2
  30. package/src/query.js +32 -2
  31. package/src/receiver.js +73 -11
  32. package/src/remote.js +8 -7
  33. package/src/state.js +74 -35
  34. package/src/transition.js +11 -19
  35. package/src/transmission.js +12 -3
  36. package/src/unmanaged.js +11 -0
  37. package/test/component.test.js +4 -3
  38. package/test/connector.fixtures.js +8 -0
  39. package/test/connector.test.js +20 -0
  40. package/test/contract/conditions.test.js +5 -5
  41. package/test/contract/request.test.js +46 -7
  42. package/test/discovery.test.js +56 -0
  43. package/test/entities/entity.test.js +54 -40
  44. package/test/entities/factory.test.js +5 -3
  45. package/test/event.test.js +4 -4
  46. package/test/query.test.js +17 -0
  47. package/test/receiver.fixtures.js +1 -0
  48. package/test/state.test.js +13 -12
  49. package/types/bindings.d.ts +7 -5
  50. package/types/bridges.ts +3 -0
  51. package/types/component.d.ts +4 -1
  52. package/types/context.d.ts +3 -0
  53. package/types/entity.d.ts +2 -2
  54. package/types/extensions.d.ts +7 -4
  55. package/types/index.ts +2 -1
  56. package/types/message.d.ts +1 -0
  57. package/types/operations.d.ts +6 -0
  58. package/types/query.d.ts +2 -0
  59. package/types/remote.d.ts +18 -0
  60. package/types/request.d.ts +15 -0
  61. package/types/state.d.ts +2 -2
  62. package/types/storages.d.ts +11 -9
  63. package/src/contract/conditions.js +0 -21
package/CHANGELOG.md ADDED
@@ -0,0 +1,81 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **core:** apply the input default on a remote call ([341699b](https://github.com/toa-io/toa/commit/341699b26c9299963a898368671633a9b3fb97ed))
12
+
13
+
14
+ ### Performance Improvements
15
+
16
+ * **core:** do not snapshot a record an operation cannot commit ([a5e695b](https://github.com/toa-io/toa/commit/a5e695b0b1d4b00a91e40336deecd476c9646c89))
17
+ * **core:** keep the parsed criteria of a query ([7efc694](https://github.com/toa-io/toa/commit/7efc694814c2a071ae7e48dbf1c6014db6025a9f))
18
+ * stop rebuilding per-call values that never change ([4202504](https://github.com/toa-io/toa/commit/4202504c33fb9ef9694f9995f3d0397d3a186438))
19
+
20
+
21
+
22
+
23
+
24
+ # [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **core:** say a connector is disconnected once it has closed, not before ([d67b48e](https://github.com/toa-io/toa/commit/d67b48eaafaacd5c635d8b066413f84b8e6a3771))
30
+
31
+
32
+
33
+
34
+
35
+ # [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
36
+
37
+
38
+ ### Features
39
+
40
+ * **bridges.node:** add the dispose run command phase ([00df715](https://github.com/toa-io/toa/commit/00df7158f638b976ab09d4007102a4ead3c696c3))
41
+
42
+
43
+
44
+
45
+
46
+ # [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
47
+
48
+ **Note:** Version bump only for package @toa.io/core
49
+
50
+
51
+
52
+
53
+
54
+ # [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
55
+
56
+
57
+ ### Features
58
+
59
+ * **core:** identify the origin of a call with `request.source` ([9da6b66](https://github.com/toa-io/toa/commit/9da6b66df852690351a1e82c7b8cc176fd89774a))
60
+
61
+
62
+
63
+
64
+
65
+ # [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
66
+
67
+
68
+ ### Features
69
+
70
+ * **runtime:** split component RC into preflight and settle ([0d648b3](https://github.com/toa-io/toa/commit/0d648b314e4aac226c254bf01a86affa04b59b34))
71
+
72
+
73
+
74
+
75
+
76
+ # [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
77
+
78
+
79
+ ### Bug Fixes
80
+
81
+ * **core:** lift tombstone on commit when transition leaves it untouched ([38c0d15](https://github.com/toa-io/toa/commit/38c0d15e946c3e2d559d4b18aecc33e0fda2471a))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/core",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.270",
4
4
  "description": "Toa Core",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -21,13 +21,11 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@rsql/parser": "1.2.4",
24
- "@toa.io/console": "1.0.0-alpha.27",
25
- "@toa.io/generic": "1.0.0-alpha.27",
26
- "@toa.io/yaml": "1.0.0-alpha.27",
27
- "error-value": "0.3.0"
24
+ "@toa.io/generic": "1.0.0-alpha.254",
25
+ "@toa.io/yaml": "1.0.0-alpha.254",
26
+ "error-value": "0.3.0",
27
+ "openspan": "1.0.0-alpha.270",
28
+ "uuid": "11.1.1"
28
29
  },
29
- "devDependencies": {
30
- "clone-deep": "4.0.1"
31
- },
32
- "gitHead": "9bc4bdb919688c5272791020746f70d51a520750"
30
+ "gitHead": "94400bdd411b4c25074ffcf58184580ddd060268"
33
31
  }
package/src/call.js CHANGED
@@ -1,28 +1,44 @@
1
1
  'use strict'
2
2
 
3
3
  const { Readable } = require('node:stream')
4
+ const { current, encode } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
6
  const { Err } = require('error-value')
6
7
 
7
8
  class Call extends Connector {
8
9
  #transmitter
9
10
  #contract
11
+ #source
10
12
 
11
- constructor (transmitter, contract) {
13
+ constructor (transmitter, contract, source) {
12
14
  super()
13
15
 
14
16
  this.#transmitter = transmitter
15
17
  this.#contract = contract
18
+ this.#source = source
16
19
 
17
20
  this.depends(transmitter)
18
21
  }
19
22
 
20
23
  async invoke (request = {}) {
24
+ // the caller may have attributed the call itself, as the node bridge does
25
+ if (this.#source !== undefined)
26
+ request.source ??= this.#source
27
+
28
+ // fitting first lets the input schema supply its default;
29
+ // an operation that takes no input still has to send an explicit null
21
30
  this.#contract.fit(request)
22
31
 
32
+ request.input ??= null
33
+
23
34
  // avoid validation on the recipient's side
24
35
  request.authentic = true
25
36
 
37
+ const context = current()
38
+
39
+ if (context !== undefined)
40
+ request.telemetry = encode(context)
41
+
26
42
  const reply = await this.#transmitter.request(request)
27
43
 
28
44
  if (reply === null) return null
@@ -37,6 +53,10 @@ class Call extends Connector {
37
53
  return reply.output
38
54
  }
39
55
  }
56
+
57
+ explain () {
58
+ return this.#contract.discovery
59
+ }
40
60
  }
41
61
 
42
62
  exports.Call = Call
package/src/cascade.js CHANGED
@@ -1,19 +1,21 @@
1
1
  'use strict'
2
2
 
3
- const { merge } = require('@toa.io/generic')
4
3
  const { Connector } = require('./connector')
5
4
 
6
5
  class Cascade extends Connector {
7
- #bridges
6
+ // #bridges
8
7
  #last
9
8
 
10
- constructor (bridges) {
9
+ constructor (bridges, preflight) {
11
10
  super()
12
11
 
13
- this.#bridges = bridges
12
+ // this.#bridges = bridges
14
13
  this.#last = bridges[bridges.length - 1]
15
14
 
16
- this.depends(bridges)
15
+ if (preflight === undefined)
16
+ this.depends(bridges)
17
+ else
18
+ this.depends(bridges).depends(preflight)
17
19
  }
18
20
 
19
21
  async run (...args) {
package/src/component.js CHANGED
@@ -1,40 +1,84 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const assert = require('node:assert')
4
+ const { console, current, decode, run } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
- const { NotImplementedException } = require('./exceptions')
6
6
 
7
- /**
8
- * @implements {toa.core.Component}
9
- */
10
7
  class Component extends Connector {
11
8
  locator
12
9
 
13
- #operations
10
+ /** @protected */
11
+ operations
12
+
13
+ /** @protected */
14
+ kind = 'server'
15
+
16
+ /** @type {Record<string, object>} span options per endpoint */
17
+ #spans = {}
14
18
 
15
19
  constructor (locator, operations) {
16
20
  super()
17
21
 
18
22
  this.locator = locator
19
- this.#operations = operations
23
+ this.operations = operations
20
24
 
21
25
  Object.values(operations).forEach((operation) => this.depends(operation))
22
26
  }
23
27
 
24
- async open () {
25
- console.info(`Runtime '${this.locator.id}' connected`)
28
+ async invoke (endpoint, request) {
29
+ if (!(endpoint in this.operations))
30
+ // `assert.fail`, not `assert.ok`: the message is built only when it is needed
31
+ assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
32
+
33
+ // if the request carries no telemetry, the trace starts here
34
+ const remote = request?.telemetry === undefined ? null : decode(request.telemetry)
35
+ const task = () => this.process(endpoint, request)
36
+
37
+ if (remote === null)
38
+ return task()
39
+ else
40
+ return run(remote, task)
26
41
  }
27
42
 
28
- async dispose () {
29
- console.info(`Runtime '${this.locator.id}' disconnected`)
43
+ /** @private */
44
+ async process (endpoint, request) {
45
+ return console.span(this.span(endpoint), async () => {
46
+ const reply = await this.operations[endpoint].invoke(request)
47
+
48
+ if (reply?.exception !== undefined) {
49
+ current().status = 'error'
50
+
51
+ console.error('Failed to execute operation', {
52
+ endpoint: `${this.locator.id}.${endpoint}`,
53
+ exception: reply.exception
54
+ })
55
+ }
56
+
57
+ return reply
58
+ })
30
59
  }
31
60
 
32
- async invoke (endpoint, request) {
33
- if (!(endpoint in this.#operations)) {
34
- throw new NotImplementedException(`Endpoint '${endpoint}' not found in '${this.locator.id}'`)
61
+ /**
62
+ * The span of an endpoint never changes, so it is built once. Not in the constructor:
63
+ * `kind` is a field of the subclass, and those are assigned after this one is built.
64
+ *
65
+ * @private
66
+ */
67
+ span (endpoint) {
68
+ let options = this.#spans[endpoint]
69
+
70
+ if (options === undefined) {
71
+ options = { name: `${this.locator.id}.${endpoint}`, kind: this.kind }
72
+
73
+ // the server span is emitted by the component itself, while the client span
74
+ // belongs to the calling service and inherits it from the context
75
+ if (this.kind === 'server')
76
+ options.service = this.locator.id
77
+
78
+ this.#spans[endpoint] = options
35
79
  }
36
80
 
37
- return this.#operations[endpoint].invoke(request)
81
+ return options
38
82
  }
39
83
  }
40
84
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const { console } = require('openspan')
4
4
  const { Connector } = require('./connector')
5
5
 
6
6
  class Composition extends Connector {
package/src/connector.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
4
- const { newid } = require('@toa.io/generic')
3
+ const { console } = require('openspan')
5
4
 
6
5
  /**
7
6
  * Abstract connections hierarchy
@@ -26,7 +25,7 @@ class Connector {
26
25
  connected = false
27
26
 
28
27
  constructor () {
29
- this.id = this.constructor.name + '#' + newid().substring(0, 8)
28
+ this.id = this.constructor.name + '#' + Math.random().toString(36).substring(2, 8)
30
29
  }
31
30
 
32
31
  /**
@@ -86,12 +85,18 @@ class Connector {
86
85
 
87
86
  this.#disconnecting = undefined
88
87
 
89
- console.debug(`Connecting '${this.id}' with ${this.#dependencies.length} dependencies...`)
90
-
91
- this.#connecting = (async () => {
88
+ const work = async () => {
92
89
  await Promise.all(this.#dependencies.map((connector) => connector.connect()))
93
90
  await this.open()
94
- })()
91
+ }
92
+
93
+ // anonymous grouping nodes are not worth a span
94
+ this.#connecting = TRACE_BOOT && this.constructor.name !== 'Connector'
95
+ ? console.span({
96
+ name: `connect ${this.constructor.name}`,
97
+ attributes: { id: this.locator?.id ?? this.id }
98
+ }, work)
99
+ : work()
95
100
 
96
101
  try {
97
102
  await this.#connecting
@@ -100,8 +105,6 @@ class Connector {
100
105
  await this.disconnect(true)
101
106
  throw e
102
107
  }
103
-
104
- console.debug(`Connector '${this.id}' connected`)
105
108
  }
106
109
 
107
110
  /**
@@ -115,7 +118,11 @@ class Connector {
115
118
  * @returns {Promise<void>}
116
119
  */
117
120
  async disconnect (interrupt) {
118
- if (interrupt !== true) await this.#connecting
121
+ // a connector that has not finished connecting has nothing to close, and
122
+ // awaiting a connection that may never settle outlives any grace period
123
+ const pending = interrupt === true || this.connected === false
124
+
125
+ if (!pending) await this.#connecting
119
126
 
120
127
  if (this.#disconnecting) return this.#disconnecting
121
128
 
@@ -123,19 +130,24 @@ class Connector {
123
130
 
124
131
  if (linked && interrupt !== true) return
125
132
 
126
- this.connected = false
127
- this.#connecting = undefined
128
-
129
133
  this.#disconnecting = (async () => {
130
134
  const start = +new Date()
131
135
 
132
136
  const interval = setInterval(() => {
133
137
  const delay = +new Date() - start
134
138
 
135
- if (delay > DELAY) console.warn(`Connector ${this.id} still disconnecting (${delay})`)
139
+ if (delay > DELAY)
140
+ console.warn(`Connector ${this.id} still disconnecting (${delay})`)
136
141
  }, DELAY)
137
142
 
138
- if (interrupt !== true) await this.close()
143
+ if (!pending) await this.close()
144
+
145
+ // said once the closing is done rather than when it starts: a dependant that is
146
+ // still closing is still using what it depends on, and the check above reads this
147
+ // of every dependant. Saying it early lets the first to start tear down a shared
148
+ // dependency under the others.
149
+ this.connected = false
150
+ this.#connecting = undefined
139
151
 
140
152
  clearInterval(interval)
141
153
 
@@ -145,8 +157,6 @@ class Connector {
145
157
  })()
146
158
 
147
159
  await this.#disconnecting
148
-
149
- console.debug(`Connector '${this.id}' disconnected`)
150
160
  }
151
161
 
152
162
  async reconnect () {
@@ -179,5 +189,6 @@ class Connector {
179
189
  }
180
190
 
181
191
  const DELAY = 5000
192
+ const TRACE_BOOT = process.env.TOA_BOOT_TRACE === '1'
182
193
 
183
194
  exports.Connector = Connector
package/src/context.js CHANGED
@@ -6,7 +6,10 @@ const { Connector } = require('./connector')
6
6
  * @implements {toa.core.Context}
7
7
  */
8
8
  class Context extends Connector {
9
+ env
10
+ name
9
11
  aspects
12
+ locator
10
13
 
11
14
  #local
12
15
  #discover
@@ -15,7 +18,10 @@ class Context extends Connector {
15
18
  constructor (local, discover, aspects = []) {
16
19
  super()
17
20
 
21
+ this.env = process.env.TOA_ENV
22
+ this.name = process.env.TOA_CONTEXT
18
23
  this.aspects = aspects
24
+ this.locator = local?.locator
19
25
 
20
26
  this.#local = local
21
27
  this.#discover = discover
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const { SystemException } = require('../exceptions')
4
+
5
+ class Contract {
6
+ schema
7
+
8
+ constructor (schema) {
9
+ this.schema = schema
10
+ }
11
+
12
+ fit (value) {
13
+ const error = this.schema.fit(value)
14
+
15
+ if (error !== null)
16
+ throw new this.constructor.Exception(error, value)
17
+ }
18
+
19
+ static Exception = SystemException
20
+ }
21
+
22
+ exports.Contract = Contract
@@ -1,22 +1,39 @@
1
1
  'use strict'
2
2
 
3
3
  const schemas = require('./schemas')
4
- const { Conditions } = require('./conditions')
4
+ const { Contract } = require('./contract')
5
5
  const { ResponseContractException } = require('../exceptions')
6
6
 
7
- class Reply extends Conditions {
7
+ class Reply extends Contract {
8
8
  static Exception = ResponseContractException
9
9
 
10
- /**
11
- * @returns {toa.schema.JSON}
12
- */
13
- static schema (output, error) {
10
+ static schema (output, errors) {
14
11
  const schema = { type: 'object', properties: {}, additionalProperties: false }
15
12
 
16
- if (output !== undefined) schema.properties.output = output
13
+ if (output !== undefined) {
14
+ if (output.type === 'object')
15
+ output.additionalProperties = true
16
+ else if (output.type === 'array' && output.items?.type === 'object')
17
+ output.items.additionalProperties = true
17
18
 
18
- if (error !== undefined) schema.properties.error = error
19
- else schema.properties.error = schemas.error
19
+ schema.properties.output = output
20
+ }
21
+
22
+ if (errors !== undefined)
23
+ schema.properties.error = {
24
+ type: 'object',
25
+ properties: {
26
+ code: {
27
+ enum: errors
28
+ },
29
+ message: {
30
+ type: 'string'
31
+ }
32
+ },
33
+ required: ['code']
34
+ }
35
+ else
36
+ schema.properties.error = schemas.error
20
37
 
21
38
  return schema
22
39
  }
@@ -2,9 +2,20 @@
2
2
 
3
3
  const schemas = require('./schemas')
4
4
  const { RequestContractException } = require('../exceptions')
5
- const { Conditions } = require('./conditions')
5
+ const { Contract } = require('./contract')
6
+
7
+ class Request extends Contract {
8
+ /** @readonly */
9
+ discovery = {}
10
+
11
+ constructor (schema, definition) {
12
+ super(schema)
13
+
14
+ for (const key of ['input', 'output', 'errors'])
15
+ if (definition[key] !== undefined)
16
+ this.discovery[key] = definition[key]
17
+ }
6
18
 
7
- class Request extends Conditions {
8
19
  static Exception = RequestContractException
9
20
 
10
21
  /**
@@ -13,7 +24,11 @@ class Request extends Conditions {
13
24
  static schema (definition, entity) {
14
25
  const schema = {
15
26
  type: 'object',
16
- properties: { authentic: { type: 'boolean' } },
27
+ properties: {
28
+ authentic: { type: 'boolean' },
29
+ task: { type: 'boolean' },
30
+ source: structuredClone(schemas.source)
31
+ },
17
32
  additionalProperties: true
18
33
  }
19
34
 
@@ -22,9 +37,8 @@ class Request extends Conditions {
22
37
  if (definition.input !== undefined) {
23
38
  schema.properties.input = definition.input
24
39
  required.push('input')
25
- } else {
40
+ } else
26
41
  schema.properties.input = { type: 'null' }
27
- }
28
42
 
29
43
  if (entity === undefined)
30
44
  definition.query = false
@@ -5,3 +5,4 @@ const { load } = require('@toa.io/yaml')
5
5
 
6
6
  exports.query = load.sync(resolve(__dirname, './query.yaml'))
7
7
  exports.error = load.sync(resolve(__dirname, './error.yaml'))
8
+ exports.source = load.sync(resolve(__dirname, './source.yaml'))
@@ -1,11 +1,22 @@
1
1
  type: object
2
2
  properties:
3
- id: { }
3
+ id:
4
+ type: string
5
+ ids:
6
+ type: array
7
+ uniqueItems: true
8
+ minItems: 1
9
+ items:
10
+ type: string
4
11
  version:
5
12
  type: integer
6
13
  minimum: 0
7
14
  criteria:
8
15
  type: string
16
+ search:
17
+ type: string
18
+ sample:
19
+ type: number
9
20
  omit:
10
21
  type: integer
11
22
  minimum: 0
@@ -27,4 +38,6 @@ properties:
27
38
  type: string
28
39
  not:
29
40
  const: id
41
+ deleted:
42
+ type: boolean
30
43
  additionalProperties: false
@@ -0,0 +1,23 @@
1
+ # Origin of a call, see toa.core.Source.
2
+ # Deliberately permissive on required properties: the contract throws on a mismatch,
3
+ # and a strict union would turn a partially stamped source into a failed business call.
4
+ # `additionalProperties: false` combined with `removeAdditional` is what matters here —
5
+ # it strips whatever a peer puts in, bounding the cardinality of the resulting map.
6
+ type: object
7
+ properties:
8
+ namespace:
9
+ type: string
10
+ maxLength: 64
11
+ component:
12
+ type: string
13
+ maxLength: 64
14
+ operation:
15
+ type: string
16
+ maxLength: 64
17
+ event:
18
+ type: string
19
+ maxLength: 64
20
+ service:
21
+ type: string
22
+ maxLength: 64
23
+ additionalProperties: false
package/src/discovery.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const { console } = require('openspan')
4
4
  const { Connector } = require('./connector')
5
5
 
6
6
  class Discovery extends Connector {
@@ -25,20 +25,26 @@ class Discovery extends Connector {
25
25
  this.depends(this.#lookups[id])
26
26
  }
27
27
 
28
- console.debug(`Sending lookup request to '${id}'`)
28
+ const since = Date.now()
29
29
 
30
- const warning = () => console.warn(`Waiting for lookup response from '${id}'...`)
31
- const timeout = setTimeout(warning, TIMEOUT)
30
+ // the wait is unbounded by design, as a dependency may still be starting,
31
+ // so repeating is the only way a component stuck on one stays visible
32
+ const warning = setInterval(() => {
33
+ const waiting = Math.round((Date.now() - since) / 1000)
34
+
35
+ console.warn('Waiting for lookup response', { component: id, waiting })
36
+ }, INTERVAL)
37
+
38
+ warning.unref()
32
39
 
33
40
  const output = await this.#lookups[id].invoke()
34
41
 
35
- console.debug(`Lookup response from '${id}' received`)
36
- clearTimeout(timeout)
42
+ clearInterval(warning)
37
43
 
38
44
  return output
39
45
  }
40
46
  }
41
47
 
42
- const TIMEOUT = 5000
48
+ const INTERVAL = 5000
43
49
 
44
50
  exports.Discovery = Discovery
package/src/effect.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const { Observation } = require('./observation')
4
+
5
+ class Effect extends Observation {
6
+
7
+ async acquire (store) {
8
+ const { query, entity, input } = store.request
9
+
10
+ if (entity === undefined)
11
+ return super.acquire(store)
12
+
13
+ store.scope = await this.scope.ensure(query, entity, input)
14
+ store.state = store.scope.get()
15
+ }
16
+
17
+ }
18
+
19
+ exports.Effect = Effect