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

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 (71) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/package.json +7 -9
  3. package/src/assignment.js +3 -2
  4. package/src/call.js +21 -1
  5. package/src/cascade.js +7 -5
  6. package/src/component.js +59 -15
  7. package/src/composition.js +1 -1
  8. package/src/connector.js +28 -17
  9. package/src/context.js +6 -0
  10. package/src/contract/contract.js +22 -0
  11. package/src/contract/reply.js +34 -9
  12. package/src/contract/request.js +19 -5
  13. package/src/contract/schemas/index.js +1 -0
  14. package/src/contract/schemas/query.yaml +14 -1
  15. package/src/contract/schemas/source.yaml +23 -0
  16. package/src/discovery.js +13 -7
  17. package/src/effect.js +19 -0
  18. package/src/entities/changeset.js +5 -8
  19. package/src/entities/entity.js +74 -24
  20. package/src/entities/factory.js +17 -6
  21. package/src/entities/newid.js +11 -0
  22. package/src/entities/set.js +13 -0
  23. package/src/event.js +19 -1
  24. package/src/exceptions.js +26 -19
  25. package/src/exposition.js +3 -2
  26. package/src/guard.js +17 -0
  27. package/src/index.js +8 -0
  28. package/src/observation.js +1 -9
  29. package/src/operation.js +38 -8
  30. package/src/outbox/index.js +6 -0
  31. package/src/outbox/outbox.js +301 -0
  32. package/src/query/options.js +3 -2
  33. package/src/query.js +32 -2
  34. package/src/receiver.js +73 -11
  35. package/src/remote.js +8 -7
  36. package/src/state.js +90 -47
  37. package/src/transition.js +11 -19
  38. package/src/transmission.js +12 -3
  39. package/src/unmanaged.js +11 -0
  40. package/test/component.test.js +4 -3
  41. package/test/connector.fixtures.js +8 -0
  42. package/test/connector.test.js +20 -0
  43. package/test/contract/conditions.test.js +5 -5
  44. package/test/contract/request.test.js +46 -7
  45. package/test/discovery.test.js +56 -0
  46. package/test/emission.fixtures.js +1 -2
  47. package/test/entities/entity.test.js +58 -48
  48. package/test/entities/factory.test.js +5 -3
  49. package/test/event.test.js +4 -4
  50. package/test/outbox.test.js +114 -0
  51. package/test/query.test.js +17 -0
  52. package/test/receiver.fixtures.js +1 -0
  53. package/test/state.fixtures.js +11 -8
  54. package/test/state.test.js +61 -13
  55. package/types/atomicity.d.ts +58 -0
  56. package/types/bindings.d.ts +7 -5
  57. package/types/bridges.ts +3 -0
  58. package/types/component.d.ts +4 -1
  59. package/types/context.d.ts +3 -0
  60. package/types/entity.d.ts +2 -2
  61. package/types/extensions.d.ts +7 -4
  62. package/types/index.ts +4 -1
  63. package/types/message.d.ts +1 -0
  64. package/types/operations.d.ts +6 -0
  65. package/types/outbox.d.ts +53 -0
  66. package/types/query.d.ts +2 -0
  67. package/types/remote.d.ts +18 -0
  68. package/types/request.d.ts +15 -0
  69. package/types/state.d.ts +9 -6
  70. package/types/storages.d.ts +34 -9
  71. package/src/contract/conditions.js +0 -21
@@ -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
@@ -1,10 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- merge,
5
- overwrite,
6
- newid
7
- } = require('@toa.io/generic')
8
3
  const { EntityContractException } = require('../exceptions')
9
4
 
10
5
  class Changeset {
@@ -18,7 +13,7 @@ class Changeset {
18
13
  this.query = query
19
14
 
20
15
  this.#schema = schema
21
- this.#state = schema.system()
16
+ this.#state = {}
22
17
  }
23
18
 
24
19
  get () {
@@ -26,10 +21,12 @@ class Changeset {
26
21
  }
27
22
 
28
23
  set (value) {
29
- const error = this.#schema.adapt(value)
24
+ const error = this.#schema.match(value)
30
25
 
31
- if (error !== null) throw new EntityContractException(error)
26
+ if (error !== null)
27
+ throw new EntityContractException(error, value)
32
28
 
29
+ delete value._version
33
30
  value._updated = Date.now()
34
31
 
35
32
  this.#state = value
@@ -1,24 +1,36 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- difference,
5
- newid
6
- } = require('@toa.io/generic')
7
-
8
- const { EntityContractException } = require('../exceptions')
3
+ const { EntityContractException, EntityGuardException } = require('../exceptions')
4
+ const { newid } = require('./newid')
9
5
 
10
6
  class Entity {
7
+ deleted = false
11
8
  #schema
9
+ #guards
12
10
  #origin = null
13
11
  #state
12
+ #mutable = true
14
13
 
15
- constructor (schema, argument) {
14
+ /**
15
+ * @param {boolean} [mutable] whether the entity may be modified and committed
16
+ */
17
+ constructor (schema, argument, guards, mutable = true) {
16
18
  this.#schema = schema
19
+ this.#guards = guards
17
20
 
18
21
  if (typeof argument === 'object') {
19
- const object = structuredClone(argument)
20
- this.set(object)
21
- this.#origin = argument
22
+ /*
23
+ * The origin is the pre-image a commit diffs the new state against. An operation
24
+ * that cannot commit has nothing to diff, so it takes the record as it came from
25
+ * the storage instead of paying for a deep copy of every record it read.
26
+ */
27
+ this.#mutable = mutable
28
+
29
+ if (mutable) {
30
+ this.#set(structuredClone(argument))
31
+ this.#origin = argument
32
+ } else
33
+ this.#set(argument)
22
34
  } else {
23
35
  const id = argument === undefined ? newid() : argument
24
36
  this.#init(id)
@@ -29,37 +41,75 @@ class Entity {
29
41
  return this.#state
30
42
  }
31
43
 
32
- set (value) {
33
- const error = this.#schema.fit(value)
44
+ set (value, optional = false) {
45
+ if (!this.#mutable)
46
+ throw new Error('Entity acquired by a read-only operation cannot be modified')
47
+
48
+ if (!optional)
49
+ this.#guard(value)
34
50
 
35
- if (error !== null) throw new EntityContractException(error)
51
+ const error = optional ? this.#schema.fitOptional(value) : this.#schema.fit(value)
36
52
 
53
+ if (error !== null)
54
+ throw new EntityContractException(error, value)
55
+
56
+ this.#revive(value)
37
57
  this.#set(value)
38
58
  }
39
59
 
40
- event () {
60
+ event (input = undefined) {
41
61
  return {
42
62
  origin: this.#origin,
43
63
  state: this.#state,
44
- changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state)
64
+ trailers: this.#state._trailers,
65
+ input
45
66
  }
46
67
  }
47
68
 
48
69
  #init (id) {
49
- const value = {
50
- ...this.#schema.defaults({ id }),
51
- _version: 0,
52
- _created: Date.now()
70
+ const value = { id, _version: 0 }
71
+
72
+ this.set(value, true)
73
+ }
74
+
75
+ #guard (value) {
76
+ if (this.#guards === undefined)
77
+ return
78
+
79
+ for (const guard of this.#guards) {
80
+ const ok = guard.fit(value, this.#origin)
81
+
82
+ if (ok === false)
83
+ throw new EntityGuardException(guard.name, value)
53
84
  }
85
+ }
54
86
 
55
- this.#set(value)
87
+ // deletion is only expressed as a new _deleted timestamp,
88
+ // so committing over a tombstone without touching it means revival
89
+ #revive (value) {
90
+ if (this.#origin?._deleted == null || value._deleted !== this.#origin._deleted)
91
+ return
92
+
93
+ value._deleted = null
94
+ this.deleted = false
56
95
  }
57
96
 
58
97
  #set (value) {
59
- Object.defineProperty(value, 'id', {
60
- writable: false,
61
- configurable: false
62
- })
98
+ if (!('_trailers' in value))
99
+ Object.defineProperty(value, '_trailers', {
100
+ writable: false,
101
+ configurable: false,
102
+ enumerable: false,
103
+ value: {}
104
+ })
105
+
106
+ if (!('_created' in value)) {
107
+ value._created = Date.now()
108
+ value._updated ??= value._created
109
+ }
110
+
111
+ if ('_deleted' in value && value._deleted !== null)
112
+ this.deleted = true
63
113
 
64
114
  if (this.#state !== undefined) {
65
115
  value._updated = Date.now()
@@ -1,26 +1,37 @@
1
1
  'use strict'
2
2
 
3
+ const { newid } = require('./newid')
3
4
  const { Entity } = require('./entity')
4
5
  const { EntitySet } = require('./set')
5
6
  const { Changeset } = require('./changeset')
6
7
 
7
8
  class Factory {
8
9
  #schema
10
+ #guards
9
11
 
10
- constructor (schema) {
12
+ constructor (schema, guards) {
11
13
  this.#schema = schema
14
+ this.#guards = guards
15
+ }
16
+
17
+ fit (values) {
18
+ this.#schema.validate({ id: newid(), ...values }, 'Entity')
12
19
  }
13
20
 
14
21
  init (id) {
15
- return new Entity(this.#schema, id)
22
+ return new Entity(this.#schema, id, this.#guards)
16
23
  }
17
24
 
18
- object (record) {
19
- return new Entity(this.#schema, record)
25
+ object (record, mutable = true) {
26
+ return new Entity(this.#schema, record, this.#guards, mutable)
20
27
  }
21
28
 
22
- objects (recordset) {
23
- const set = recordset.map((record) => this.object(record))
29
+ objects (recordset, init, mutable = true) {
30
+ const set = recordset.map((record) => this.object(record, mutable))
31
+
32
+ if (init !== undefined)
33
+ for (const id of init)
34
+ set.unshift(this.init(id))
24
35
 
25
36
  return new EntitySet(set)
26
37
  }
@@ -0,0 +1,11 @@
1
+ const uuid = require('uuid')
2
+
3
+ function newid () {
4
+ const buf = Buffer.alloc(16)
5
+
6
+ uuid.v7(undefined, buf)
7
+
8
+ return buf.toString('hex')
9
+ }
10
+
11
+ module.exports = { newid }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { SystemException } = require("../exceptions")
4
+
3
5
  class EntitySet {
4
6
  #set
5
7
 
@@ -10,6 +12,17 @@ class EntitySet {
10
12
  get () {
11
13
  return this.#set.map((entity) => entity.get())
12
14
  }
15
+
16
+ set (values) {
17
+ if (values.length !== this.#set.length)
18
+ throw new SystemException('Objects array must not be modified')
19
+
20
+ values.forEach((value, index) => this.#set[index].set(value))
21
+ }
22
+
23
+ events (input = undefined) {
24
+ return this.#set.map((entity) => entity.event(input))
25
+ }
13
26
  }
14
27
 
15
28
  exports.EntitySet = EntitySet
package/src/event.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { console, current, encode } = require('openspan')
3
4
  const { Connector } = require('./connector')
4
5
 
5
6
  /**
@@ -12,11 +13,15 @@ class Event extends Connector {
12
13
  #conditioned
13
14
  #subjective
14
15
 
16
+ /** @type {string} */
17
+ #label
18
+
15
19
  constructor (definition, emitter, bridge = undefined) {
16
20
  super()
17
21
 
18
22
  this.#conditioned = definition.conditioned
19
23
  this.#subjective = definition.subjective
24
+ this.#label = definition.label ?? 'event'
20
25
  this.#emitter = emitter
21
26
  this.#bridge = bridge
22
27
 
@@ -32,7 +37,20 @@ class Event extends Connector {
32
37
  /** @type {toa.core.Message} */
33
38
  const message = { payload }
34
39
 
35
- await this.#emitter.emit(message)
40
+ const options = {
41
+ name: `${this.#label} publish`,
42
+ kind: 'producer',
43
+ attributes: { 'messaging.destination.name': this.#label }
44
+ }
45
+
46
+ await console.span(options, async () => {
47
+ const context = current()
48
+
49
+ if (context !== undefined)
50
+ message.telemetry = encode(context)
51
+
52
+ await this.#emitter.emit(message)
53
+ })
36
54
  }
37
55
  }
38
56
  }
package/src/exceptions.js CHANGED
@@ -4,7 +4,6 @@ const { swap } = require('@toa.io/generic')
4
4
 
5
5
  const codes = {
6
6
  System: 0,
7
- NotImplemented: 10,
8
7
 
9
8
  Contract: 200,
10
9
  RequestSyntax: 201,
@@ -12,6 +11,7 @@ const codes = {
12
11
  RequestConflict: 203,
13
12
  ResponseContract: 211,
14
13
  EntityContract: 212,
14
+ EntityGuard: 213,
15
15
  QuerySyntax: 221,
16
16
 
17
17
  State: 300,
@@ -32,9 +32,12 @@ class Exception {
32
32
  code
33
33
  message
34
34
 
35
- constructor (code, message) {
35
+ constructor (code, message, cause) {
36
36
  this.code = code
37
37
  this.message = message
38
+
39
+ if (cause !== undefined)
40
+ this.cause = cause
38
41
  }
39
42
  }
40
43
 
@@ -49,31 +52,30 @@ class SystemException extends Exception {
49
52
  }
50
53
 
51
54
  class ContractException extends Exception {
52
- keyword
53
- property
54
- schema
55
- path
56
-
57
- constructor (code, error) {
58
- super(code || codes.Contract, error.message)
59
-
60
- this.keyword = error.keyword
61
- this.property = error.property
62
- this.schema = error.schema
63
- this.path = error.path
55
+ constructor (code, error, cause) {
56
+ super(code || codes.Contract, typeof error === 'string' ? error : error?.message, cause)
57
+
58
+ if (typeof error === 'object' && error !== null)
59
+ for (const k of ['keyword', 'property', 'schema', 'path', 'params'])
60
+ if (k in error)
61
+ this[k] = error[k]
64
62
  }
65
63
  }
66
64
 
67
65
  class RequestContractException extends ContractException {
68
- constructor (error) { super(codes.RequestContract, error) }
66
+ constructor (error, cause) { super(codes.RequestContract, error, cause) }
69
67
  }
70
68
 
71
69
  class ResponseContractException extends ContractException {
72
- constructor (error) { super(codes.ResponseContract, error) }
70
+ constructor (error, cause) { super(codes.ResponseContract, error, cause) }
73
71
  }
74
72
 
75
73
  class EntityContractException extends ContractException {
76
- constructor (error) { super(codes.EntityContract, error) }
74
+ constructor (error, cause) { super(codes.EntityContract, error, cause) }
75
+ }
76
+
77
+ class EntityGuardException extends ContractException {
78
+ constructor (name, cause) { super(codes.EntityGuard, name, cause) }
77
79
  }
78
80
 
79
81
  // #region exports
@@ -82,14 +84,19 @@ exports.SystemException = SystemException
82
84
  exports.RequestContractException = RequestContractException
83
85
  exports.ResponseContractException = ResponseContractException
84
86
  exports.EntityContractException = EntityContractException
87
+ exports.EntityGuardException = EntityGuardException
85
88
 
86
89
  for (const [name, code] of Object.entries(codes)) {
87
90
  const classname = name + 'Exception'
88
91
 
89
92
  if (exports[classname] === undefined) {
90
93
  exports[classname] = class extends Exception {
91
- constructor (message) {
92
- super(code, message ?? classname)
94
+ constructor (message, cause) {
95
+ message = message
96
+ ? `${classname}: ${message}`
97
+ : classname
98
+
99
+ super(code, message ?? classname, cause)
93
100
  }
94
101
  }
95
102
  }
package/src/exposition.js CHANGED
@@ -20,8 +20,9 @@ class Exposition extends Connector {
20
20
  }
21
21
 
22
22
  const expose = (manifest) => {
23
- const { namespace, name, operations, events, entity } = manifest
24
- return { namespace, name, operations, events, entity }
23
+ const { namespace, name, entity, operations, events } = manifest
24
+
25
+ return { namespace, name, entity, operations, events }
25
26
  }
26
27
 
27
28
  exports.Exposition = Exposition
package/src/guard.js ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ class Guard {
4
+ name
5
+ #bridge
6
+
7
+ constructor (name, bridge) {
8
+ this.name = name
9
+ this.#bridge = bridge
10
+ }
11
+
12
+ fit (state, origin) {
13
+ return this.#bridge.fit(state, origin)
14
+ }
15
+ }
16
+
17
+ exports.Guard = Guard
package/src/index.js CHANGED
@@ -5,11 +5,13 @@ const { Composition } = require('./composition')
5
5
  const { Connector } = require('./connector')
6
6
  const { Context } = require('./context')
7
7
  const { Discovery } = require('./discovery')
8
+ const { Effect } = require('./effect')
8
9
  const { Emission } = require('./emission')
9
10
  const { Event } = require('./event')
10
11
  const { Exposition } = require('./exposition')
11
12
  const { Locator } = require('./locator')
12
13
  const { Observation } = require('./observation')
14
+ const { Outbox } = require('./outbox')
13
15
  const { Operation } = require('./operation')
14
16
  const { Query } = require('./query')
15
17
  const { Receiver } = require('./receiver')
@@ -19,6 +21,8 @@ const { Component } = require('./component')
19
21
  const { State } = require('./state')
20
22
  const { Transition } = require('./transition')
21
23
  const { Transmission } = require('./transmission')
24
+ const { Unmanaged } = require('./unmanaged')
25
+ const { Guard } = require('./guard')
22
26
 
23
27
  exports.entities = require('./entities')
24
28
  exports.exceptions = require('./exceptions')
@@ -32,11 +36,13 @@ exports.Composition = Composition
32
36
  exports.Connector = Connector
33
37
  exports.Context = Context
34
38
  exports.Discovery = Discovery
39
+ exports.Effect = Effect
35
40
  exports.Emission = Emission
36
41
  exports.Event = Event
37
42
  exports.Exposition = Exposition
38
43
  exports.Locator = Locator
39
44
  exports.Observation = Observation
45
+ exports.Outbox = Outbox
40
46
  exports.Operation = Operation
41
47
  exports.Query = Query
42
48
  exports.Receiver = Receiver
@@ -45,3 +51,5 @@ exports.Remote = Remote
45
51
  exports.State = State
46
52
  exports.Transition = Transition
47
53
  exports.Transmission = Transmission
54
+ exports.Unmanaged = Unmanaged
55
+ exports.Guard = Guard
@@ -3,16 +3,8 @@
3
3
  const { Operation } = require('./operation')
4
4
 
5
5
  class Observation extends Operation {
6
- async acquire (store) {
7
- const scope = await this.query(store.request.query)
8
- const state = scope === null ? null : scope.get()
9
-
10
- store.scope = scope
11
- store.state = state
12
- }
13
-
14
6
  async run (store) {
15
- if (store.scope === null) store.reply = null
7
+ if (store.scope === null || (store.scope?.deleted === true && store.request.query?.options?.deleted !== true)) store.reply = null
16
8
  else await super.run(store)
17
9
  }
18
10
  }
package/src/operation.js CHANGED
@@ -1,11 +1,21 @@
1
1
  'use strict'
2
2
 
3
3
  const { Connector } = require('./connector')
4
- const { SystemException } = require('./exceptions')
4
+ const { SystemException, RequestContractException } = require('./exceptions')
5
+ const { Readable } = require('node:stream')
5
6
 
6
7
  class Operation extends Connector {
7
8
  scope
8
9
 
10
+ /**
11
+ * Whether what this operation acquires may be modified and committed. Only a
12
+ * transition commits, and only a commit needs the pre-image an entity keeps
13
+ * to diff the new state against.
14
+ *
15
+ * @protected
16
+ */
17
+ mutable = false
18
+
9
19
  #cascade
10
20
  #contracts
11
21
  #query
@@ -26,8 +36,15 @@ class Operation extends Connector {
26
36
 
27
37
  async invoke (request) {
28
38
  try {
29
- if (request.authentic !== true) this.#contracts.request.fit(request)
30
- if ('query' in request) request.query = this.#query.parse(request.query)
39
+ if (request.authentic !== true)
40
+ this.#contracts.request.fit(request)
41
+
42
+ if ('query' in request)
43
+ request.query = this.#query.parse(request.query)
44
+
45
+ // validate entity
46
+ if ('entity' in request)
47
+ this.scope.fit(request.entity)
31
48
 
32
49
  const store = { request }
33
50
 
@@ -47,14 +64,24 @@ class Operation extends Connector {
47
64
  return store.reply
48
65
  }
49
66
 
50
- async acquire () {}
67
+ async acquire (store) {
68
+ if (this.#scope === 'none')
69
+ return
70
+
71
+ const scope = await this.query(store.request.query)
72
+ const raw = scope === null || scope instanceof Readable
73
+
74
+ store.scope = scope
75
+ store.state = raw ? scope : scope.get()
76
+ }
51
77
 
52
78
  async run (store) {
53
79
  const { request, state } = store
54
- // noinspection UnnecessaryLocalVariableJS
55
- const reply = await this.#cascade.run(request.input, state) || {}
80
+ const reply = await this.#cascade.run(request.input, state)
56
81
 
57
- // this.#contracts.reply.fit(reply)
82
+ // validate reply only on local environments
83
+ if (process.env.TOA_ENV === 'local' && !(reply instanceof Readable))
84
+ this.#contracts.reply.fit(reply)
58
85
 
59
86
  store.reply = reply
60
87
  }
@@ -62,7 +89,10 @@ class Operation extends Connector {
62
89
  async commit () {}
63
90
 
64
91
  async query (query) {
65
- return this.scope[this.#scope](query)
92
+ if (query === undefined)
93
+ throw new RequestContractException('Request query is required')
94
+
95
+ return this.scope[this.#scope](query, this.mutable)
66
96
  }
67
97
  }
68
98
 
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ const { Outbox, LANES } = require('./outbox')
4
+
5
+ exports.Outbox = Outbox
6
+ exports.LANES = LANES