@toa.io/core 1.0.0-alpha.5 → 1.0.0-alpha.50

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/core",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.50",
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,13 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@rsql/parser": "1.2.4",
24
- "@toa.io/console": "1.0.0-alpha.5",
25
- "@toa.io/generic": "1.0.0-alpha.5",
26
- "@toa.io/yaml": "1.0.0-alpha.5",
24
+ "@toa.io/console": "1.0.0-alpha.50",
25
+ "@toa.io/generic": "1.0.0-alpha.50",
26
+ "@toa.io/yaml": "1.0.0-alpha.50",
27
27
  "error-value": "0.3.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "clone-deep": "4.0.1"
31
31
  },
32
- "gitHead": "1e4bb4ac28a6dddff1f4b8c5be7224fcdc47b847"
32
+ "gitHead": "514269d4b481150c8cd0db2e5971da6e9fe80ad9"
33
33
  }
package/src/assignment.js CHANGED
@@ -9,13 +9,22 @@ class Assignment extends Operation {
9
9
  }
10
10
 
11
11
  async commit (store) {
12
- const { scope, state, reply } = store
12
+ const {
13
+ scope,
14
+ state,
15
+ reply
16
+ } = store
13
17
 
14
18
  if (reply.error !== undefined) return
15
19
 
16
20
  scope.set(state)
17
21
 
18
- await this.scope.apply(scope)
22
+ const output = await this.scope.apply(scope)
23
+
24
+ // assignment returns new state by default
25
+ if (store.reply.output === undefined) {
26
+ store.reply.output = output
27
+ }
19
28
  }
20
29
  }
21
30
 
package/src/call.js CHANGED
@@ -37,6 +37,10 @@ class Call extends Connector {
37
37
  return reply.output
38
38
  }
39
39
  }
40
+
41
+ explain () {
42
+ return this.#contract.discovery
43
+ }
40
44
  }
41
45
 
42
46
  exports.Call = Call
package/src/cascade.js CHANGED
@@ -1,16 +1,15 @@
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
9
  constructor (bridges) {
11
10
  super()
12
11
 
13
- this.#bridges = bridges
12
+ // this.#bridges = bridges
14
13
  this.#last = bridges[bridges.length - 1]
15
14
 
16
15
  this.depends(bridges)
package/src/component.js CHANGED
@@ -1,40 +1,28 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
4
3
  const { Connector } = require('./connector')
5
- const { NotImplementedException } = require('./exceptions')
4
+ const assert = require('node:assert')
6
5
 
7
- /**
8
- * @implements {toa.core.Component}
9
- */
10
6
  class Component extends Connector {
11
7
  locator
12
8
 
13
- #operations
9
+ /** @protected */
10
+ operations
14
11
 
15
12
  constructor (locator, operations) {
16
13
  super()
17
14
 
18
15
  this.locator = locator
19
- this.#operations = operations
16
+ this.operations = operations
20
17
 
21
18
  Object.values(operations).forEach((operation) => this.depends(operation))
22
19
  }
23
20
 
24
- async open () {
25
- console.info(`Runtime '${this.locator.id}' connected`)
26
- }
27
-
28
- async dispose () {
29
- console.info(`Runtime '${this.locator.id}' disconnected`)
30
- }
31
-
32
21
  async invoke (endpoint, request) {
33
- if (!(endpoint in this.#operations)) {
34
- throw new NotImplementedException(`Endpoint '${endpoint}' not found in '${this.locator.id}'`)
35
- }
22
+ assert.ok(endpoint in this.operations,
23
+ `Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
36
24
 
37
- return this.#operations[endpoint].invoke(request)
25
+ return this.operations[endpoint].invoke(request)
38
26
  }
39
27
  }
40
28
 
@@ -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)
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,7 @@ 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: { authentic: { type: 'boolean' }, task: { type: 'boolean' } },
17
28
  additionalProperties: true
18
29
  }
19
30
 
@@ -22,9 +33,8 @@ class Request extends Conditions {
22
33
  if (definition.input !== undefined) {
23
34
  schema.properties.input = definition.input
24
35
  required.push('input')
25
- } else {
36
+ } else
26
37
  schema.properties.input = { type: 'null' }
27
- }
28
38
 
29
39
  if (entity === undefined)
30
40
  definition.query = false
@@ -18,14 +18,13 @@ properties:
18
18
  minItems: 1
19
19
  items:
20
20
  type: string
21
- pattern: ^[a-zA-Z]+([-a-zA-Z0-9]*[a-zA-Z0-9]+)?(:(asc|desc))?$
21
+ pattern: ^\w{1,32}(?::(?:asc|desc))?$
22
22
  projection:
23
23
  type: array
24
24
  uniqueItems: true
25
25
  minItems: 1
26
26
  items:
27
27
  type: string
28
- pattern: ^([a-zA-Z]+([-a-zA-Z0-9]*[a-zA-Z0-9]+)?)$
29
28
  not:
30
29
  const: id
31
30
  additionalProperties: false
package/src/discovery.js CHANGED
@@ -25,14 +25,11 @@ class Discovery extends Connector {
25
25
  this.depends(this.#lookups[id])
26
26
  }
27
27
 
28
- console.debug(`Sending lookup request to '${id}'`)
29
-
30
28
  const warning = () => console.warn(`Waiting for lookup response from '${id}'...`)
31
29
  const timeout = setTimeout(warning, TIMEOUT)
32
30
 
33
31
  const output = await this.#lookups[id].invoke()
34
32
 
35
- console.debug(`Lookup response from '${id}' received`)
36
33
  clearTimeout(timeout)
37
34
 
38
35
  return output
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 } = store.request
9
+
10
+ if (entity === undefined)
11
+ return super.acquire(store)
12
+
13
+ store.scope = await this.scope.ensure(query, entity)
14
+ store.state = store.scope.get()
15
+ }
16
+
17
+ }
18
+
19
+ exports.Effect = Effect
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const { merge, overwrite, newid } = require('@toa.io/generic')
4
3
  const { EntityContractException } = require('../exceptions')
5
4
 
6
5
  class Changeset {
@@ -14,7 +13,7 @@ class Changeset {
14
13
  this.query = query
15
14
 
16
15
  this.#schema = schema
17
- this.#state = schema.system()
16
+ this.#state = {}
18
17
  }
19
18
 
20
19
  get () {
@@ -22,25 +21,19 @@ class Changeset {
22
21
  }
23
22
 
24
23
  set (value) {
25
- const error = this.#schema.adapt(value)
24
+ const error = this.#schema.fitOptional(value)
26
25
 
27
- if (error !== null) throw new EntityContractException(error)
26
+ if (error !== null)
27
+ throw new EntityContractException(error)
28
+
29
+ delete value._version
30
+ value._updated = Date.now()
28
31
 
29
32
  this.#state = value
30
33
  }
31
34
 
32
35
  export () {
33
- const changeset = this.#state
34
- const result = { changeset }
35
- const insert = merge({ id: newid() }, changeset)
36
- const error = this.#schema.fit(insert)
37
-
38
- if (error === null) {
39
- delete insert.id
40
- result.insert = overwrite(insert, changeset)
41
- }
42
-
43
- return result
36
+ return this.#state
44
37
  }
45
38
  }
46
39
 
@@ -1,10 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- difference,
5
- newid
6
- } = require('@toa.io/generic')
7
-
3
+ const { difference, newid } = require('@toa.io/generic')
8
4
  const { EntityContractException } = require('../exceptions')
9
5
 
10
6
  class Entity {
@@ -29,10 +25,11 @@ class Entity {
29
25
  return this.#state
30
26
  }
31
27
 
32
- set (value) {
33
- const error = this.#schema.fit(value)
28
+ set (value, optional = false) {
29
+ const error = optional ? this.#schema.fitOptional(value) : this.#schema.fit(value)
34
30
 
35
- if (error !== null) throw new EntityContractException(error)
31
+ if (error !== null)
32
+ throw new EntityContractException(error)
36
33
 
37
34
  this.#set(value)
38
35
  }
@@ -41,26 +38,31 @@ class Entity {
41
38
  return {
42
39
  origin: this.#origin,
43
40
  state: this.#state,
44
- changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state)
41
+ changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state),
42
+ trailers: this.#state._trailers
45
43
  }
46
44
  }
47
45
 
48
46
  #init (id) {
49
- const value = {
50
- ...this.#schema.defaults({ id }),
51
- _version: 0
52
- }
47
+ const value = { id, _version: 0 }
53
48
 
54
- this.#set(value)
49
+ this.set(value, true)
55
50
  }
56
51
 
57
52
  #set (value) {
58
- Object.defineProperty(value, 'id', {
59
- writable: false,
60
- configurable: false
61
- })
53
+ if (!('_trailers' in value))
54
+ Object.defineProperty(value, '_trailers', {
55
+ writable: false,
56
+ configurable: false,
57
+ enumerable: false,
58
+ value: {}
59
+ })
60
+
61
+ if (!('_created' in value))
62
+ value._created = Date.now()
62
63
 
63
64
  if (this.#state !== undefined) {
65
+ value._updated = Date.now()
64
66
  value._version++
65
67
  }
66
68
 
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { newid } = require('@toa.io/generic')
4
+
3
5
  const { Entity } = require('./entity')
4
6
  const { EntitySet } = require('./set')
5
7
  const { Changeset } = require('./changeset')
@@ -11,6 +13,10 @@ class Factory {
11
13
  this.#schema = schema
12
14
  }
13
15
 
16
+ fit (values) {
17
+ this.#schema.validate({ id: newid(), ...values }, 'Entity')
18
+ }
19
+
14
20
  init (id) {
15
21
  return new Entity(this.#schema, id)
16
22
  }
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,
@@ -19,6 +18,7 @@ const codes = {
19
18
  StatePrecondition: 303,
20
19
  StateConcurrency: 304,
21
20
  StateInitialization: 305,
21
+ Duplicate: 306,
22
22
 
23
23
  Communication: 400,
24
24
  Transmission: 401
@@ -48,18 +48,13 @@ class SystemException extends Exception {
48
48
  }
49
49
 
50
50
  class ContractException extends Exception {
51
- keyword
52
- property
53
- schema
54
- path
55
-
56
51
  constructor (code, error) {
57
- super(code || codes.Contract, error.message)
52
+ super(code || codes.Contract, typeof error === 'string' ? error : error?.message)
58
53
 
59
- this.keyword = error.keyword
60
- this.property = error.property
61
- this.schema = error.schema
62
- this.path = error.path
54
+ if (typeof error === 'object' && error !== null)
55
+ for (const k of ['keyword', 'property', 'schema', 'path', 'params'])
56
+ if (k in error)
57
+ this[k] = error[k]
63
58
  }
64
59
  }
65
60
 
@@ -88,7 +83,7 @@ for (const [name, code] of Object.entries(codes)) {
88
83
  if (exports[classname] === undefined) {
89
84
  exports[classname] = class extends Exception {
90
85
  constructor (message) {
91
- super(code, message || classname)
86
+ super(code, message ?? classname)
92
87
  }
93
88
  }
94
89
  }
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/index.js CHANGED
@@ -5,6 +5,7 @@ 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')
@@ -32,6 +33,7 @@ exports.Composition = Composition
32
33
  exports.Connector = Connector
33
34
  exports.Context = Context
34
35
  exports.Discovery = Discovery
36
+ exports.Effect = Effect
35
37
  exports.Emission = Emission
36
38
  exports.Event = Event
37
39
  exports.Exposition = Exposition
package/src/locator.js CHANGED
@@ -12,6 +12,7 @@ class Locator {
12
12
  id
13
13
  label
14
14
  uppercase
15
+ lowercase
15
16
 
16
17
  /**
17
18
  * @param {string} name
@@ -26,6 +27,7 @@ class Locator {
26
27
  this.id = concat(namespace, '.') + name
27
28
  this.label = (concat(namespace, '-') + name).toLowerCase()
28
29
  this.uppercase = (concat(namespace, '_') + name).toUpperCase()
30
+ this.lowercase = (concat(namespace, '_') + name).toLowerCase()
29
31
  }
30
32
 
31
33
  hostname (prefix) {
@@ -39,8 +41,11 @@ class Locator {
39
41
  static parse (string) {
40
42
  const [namespace, name] = string.split(DOT)
41
43
 
42
- if (name === undefined) return new Locator(namespace)
43
- else return new Locator(name, namespace)
44
+ if (name === undefined) {
45
+ return new Locator(namespace)
46
+ } else {
47
+ return new Locator(name, namespace)
48
+ }
44
49
  }
45
50
  }
46
51
 
@@ -3,14 +3,6 @@
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
7
  if (store.scope === null) store.reply = null
16
8
  else await super.run(store)
package/src/operation.js CHANGED
@@ -1,7 +1,8 @@
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
@@ -26,13 +27,22 @@ class Operation extends Connector {
26
27
 
27
28
  async invoke (request) {
28
29
  try {
29
- if (request.authentic !== true) this.#contracts.request.fit(request)
30
- if ('query' in request) request.query = this.#query.parse(request.query)
30
+ if (request.authentic !== true)
31
+ this.#contracts.request.fit(request)
32
+
33
+ if ('query' in request)
34
+ request.query = this.#query.parse(request.query)
35
+
36
+ // validate entity
37
+ if ('entity' in request)
38
+ this.scope.fit(request.entity)
31
39
 
32
40
  const store = { request }
33
41
 
34
42
  return await this.process(store)
35
43
  } catch (e) {
44
+ console.error(e)
45
+
36
46
  const exception = e instanceof Error ? new SystemException(e) : e
37
47
 
38
48
  return { exception }
@@ -47,14 +57,24 @@ class Operation extends Connector {
47
57
  return store.reply
48
58
  }
49
59
 
50
- async acquire () {}
60
+ async acquire (store) {
61
+ if (this.#scope === 'none')
62
+ return
63
+
64
+ const scope = await this.query(store.request.query)
65
+ const raw = scope === null || scope instanceof Readable
66
+
67
+ store.scope = scope
68
+ store.state = raw ? scope : scope.get()
69
+ }
51
70
 
52
71
  async run (store) {
53
72
  const { request, state } = store
54
- // noinspection UnnecessaryLocalVariableJS
55
- const reply = await this.#cascade.run(request.input, state) || {}
73
+ const reply = await this.#cascade.run(request.input, state)
56
74
 
57
- // this.#contracts.reply.fit(reply)
75
+ // validate reply only on local environments
76
+ if (process.env.TOA_ENV === 'local')
77
+ this.#contracts.reply.fit(reply)
58
78
 
59
79
  store.reply = reply
60
80
  }
@@ -62,6 +82,9 @@ class Operation extends Connector {
62
82
  async commit () {}
63
83
 
64
84
  async query (query) {
85
+ if (query === undefined)
86
+ throw new RequestContractException('Request query is required')
87
+
65
88
  return this.scope[this.#scope](query)
66
89
  }
67
90
  }
package/src/receiver.js CHANGED
@@ -16,18 +16,13 @@ class Receiver extends Connector {
16
16
  /** @type {string} */
17
17
  #endpoint
18
18
 
19
- /** @type {toa.core.Component} */
19
+ /** @type {unknown[]} */
20
+ #arguments
21
+
20
22
  #local
21
23
 
22
- /** @type {toa.core.bridges.Receiver} */
23
24
  #bridge
24
25
 
25
- /**
26
- *
27
- * @param {toa.norm.component.Receiver} definition
28
- * @param {toa.core.Component} local
29
- * @param {toa.core.bridges.Receiver} bridge
30
- */
31
26
  constructor (definition, local, bridge) {
32
27
  super()
33
28
 
@@ -36,6 +31,7 @@ class Receiver extends Connector {
36
31
  this.#conditioned = conditioned
37
32
  this.#adaptive = adaptive
38
33
  this.#endpoint = operation
34
+ this.#arguments = definition.arguments
39
35
 
40
36
  this.#local = local
41
37
  this.#bridge = bridge
@@ -58,7 +54,7 @@ class Receiver extends Connector {
58
54
  }
59
55
 
60
56
  async #request (payload) {
61
- return this.#adaptive ? await this.#bridge.request(payload) : { input: payload }
57
+ return this.#adaptive ? await this.#bridge.request(payload, ...(this.#arguments ?? [])) : { input: payload }
62
58
  }
63
59
  }
64
60
 
package/src/remote.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { console } = require('@toa.io/console')
4
4
 
5
+ const assert = require('node:assert')
5
6
  const { Component } = require('./component')
6
7
 
7
8
  class Remote extends Component {
@@ -12,6 +13,13 @@ class Remote extends Component {
12
13
  async dispose () {
13
14
  console.info(`Remote '${this.locator.id}' disconnected`)
14
15
  }
16
+
17
+ explain (endpoint) {
18
+ assert.ok(endpoint in this.operations,
19
+ `Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
20
+
21
+ return this.operations[endpoint].explain()
22
+ }
15
23
  }
16
24
 
17
25
  exports.Remote = Remote
package/src/state.js CHANGED
@@ -1,62 +1,79 @@
1
1
  'use strict'
2
2
 
3
- const { empty } = require('@toa.io/generic')
3
+ const { empty, newid } = require('@toa.io/generic')
4
+ const { StatePreconditionException, StateNotFoundException } = require('./exceptions')
4
5
 
5
- const {
6
- StatePreconditionException,
7
- StateNotFoundException,
8
- StateInitializationException
9
- } = require('./exceptions')
10
-
11
- /**
12
- * @implements {toa.core.State}
13
- */
14
6
  class State {
15
- /** @type {toa.core.Storage} */
7
+ #associated
16
8
  #storage
17
-
18
- /** @type {toa.core.entity.Factory} */
19
- #entity
9
+ #entities
20
10
  #emission
21
- #dependent
22
11
 
23
- constructor (storage, entity, emission, dependent) {
12
+ constructor (storage, entity, emission, associated) {
24
13
  this.#storage = storage
25
- this.#entity = entity
14
+ this.#entities = entity
26
15
  this.#emission = emission
27
- this.#dependent = dependent === true
16
+ this.#associated = associated === true
28
17
  }
29
18
 
30
19
  init (id) {
31
- return this.#entity.init(id)
20
+ return this.#entities.init(id)
21
+ }
22
+
23
+ fit (values) {
24
+ return this.#entities.fit(values)
32
25
  }
33
26
 
34
27
  async object (query) {
35
28
  const record = await this.#storage.get(query)
36
29
 
37
30
  if (record === null) {
38
- if (this.#dependent && query.id !== undefined && query.version === undefined) return this.init(query.id)
39
- else if (query.version !== undefined) throw new StatePreconditionException()
40
- }
41
-
42
- if (record === null) return null
43
- else return this.#entity.object(record)
31
+ if (this.#associated && query.id !== undefined && query.version === undefined)
32
+ return this.init(query.id)
33
+ else if (query.version !== undefined)
34
+ throw new StatePreconditionException()
35
+
36
+ return null
37
+ } else
38
+ return this.#entities.object(record)
44
39
  }
45
40
 
46
41
  async objects (query) {
47
42
  const recordset = await this.#storage.find(query)
48
43
 
49
- return this.#entity.objects(recordset)
44
+ return this.#entities.objects(recordset)
45
+ }
46
+
47
+ async stream (query) {
48
+ return this.#storage.stream(query)
50
49
  }
51
50
 
52
51
  changeset (query) {
53
- return this.#entity.changeset(query)
52
+ return this.#entities.changeset(query)
54
53
  }
55
54
 
56
55
  none () {
57
56
  return null
58
57
  }
59
58
 
59
+ async ensure (query, properties) {
60
+ const object = this.#entities.init()
61
+ const blank = object.get()
62
+
63
+ Object.assign(blank, properties)
64
+
65
+ object.set(blank)
66
+
67
+ const record = await this.#storage.ensure(query, properties, object.get())
68
+
69
+ if (record.id !== blank.id) // exists
70
+ return this.#entities.object(record)
71
+
72
+ await this.#emission.emit(object.event())
73
+
74
+ return object
75
+ }
76
+
60
77
  async commit (state) {
61
78
  const event = state.event()
62
79
 
@@ -68,30 +85,34 @@ class State {
68
85
  ok = await this.#storage.store(object)
69
86
 
70
87
  // #20
71
- await this.#emission.emit(event)
88
+ if (ok === true) {
89
+ await this.#emission.emit(event)
90
+ }
72
91
  }
73
92
 
74
93
  return ok
75
94
  }
76
95
 
77
96
  async apply (state) {
78
- const { changeset, insert } = state.export()
79
-
80
- let upsert
81
-
82
- if (this.#dependent && state.query.id !== undefined && state.query.version === undefined) {
83
- upsert = insert
84
- }
97
+ const changeset = state.export()
85
98
 
86
- const result = await this.#storage.upsert(state.query, changeset, upsert)
99
+ const result = await this.#storage.upsert(state.query, changeset)
87
100
 
88
101
  if (result === null) {
89
- if (state.query.version !== undefined) throw new StatePreconditionException()
90
- else throw new StateNotFoundException()
102
+ if (state.query.version !== undefined) {
103
+ throw new StatePreconditionException()
104
+ } else {
105
+ throw new StateNotFoundException()
106
+ }
107
+ } else {
108
+ // same as above
109
+ await this.#emission.emit({
110
+ changeset,
111
+ state: result
112
+ })
91
113
  }
92
114
 
93
- // same as above
94
- await this.#emission.emit({ changeset, state: result })
115
+ return result
95
116
  }
96
117
  }
97
118
 
package/src/transition.js CHANGED
@@ -3,7 +3,10 @@
3
3
  const { retry } = require('@toa.io/generic')
4
4
 
5
5
  const { Operation } = require('./operation')
6
- const { StateConcurrencyException, StateNotFoundException } = require('./exceptions')
6
+ const {
7
+ StateConcurrencyException,
8
+ StateNotFoundException
9
+ } = require('./exceptions')
7
10
 
8
11
  class Transition extends Operation {
9
12
  #concurrency
@@ -23,7 +26,9 @@ class Transition extends Operation {
23
26
 
24
27
  store.scope = request.query ? await this.query(request.query) : this.scope.init()
25
28
 
26
- if (store.scope === null) throw new StateNotFoundException()
29
+ if (store.scope === null) {
30
+ throw new StateNotFoundException()
31
+ }
27
32
 
28
33
  store.state = store.scope.get()
29
34
  }
@@ -35,11 +40,13 @@ class Transition extends Operation {
35
40
 
36
41
  scope.set(state)
37
42
 
38
- const ok = await this.scope.commit(scope)
43
+ const result = await this.scope.commit(scope)
39
44
 
40
- if (ok !== true) {
41
- if (this.#concurrency === 'retry') retry()
42
- else throw new StateConcurrencyException()
45
+ if (result === false) {
46
+ if (this.#concurrency === 'retry')
47
+ return retry()
48
+ else
49
+ throw new StateConcurrencyException()
43
50
  }
44
51
  }
45
52
 
@@ -18,13 +18,22 @@ class Transmission extends Connector {
18
18
  let i = 0
19
19
 
20
20
  while (reply === false && i < this.#bindings.length) {
21
- reply = await this.#bindings[i].request(request)
21
+ const binding = this.#bindings[i]
22
+
22
23
  i++
24
+
25
+ if (request?.task === true) {
26
+ if (binding.task === undefined)
27
+ continue
28
+
29
+ await binding.task(request)
30
+ reply = null
31
+ } else
32
+ reply = await binding.request(request)
23
33
  }
24
34
 
25
- if (reply === false) {
35
+ if (reply === false)
26
36
  throw new TransmissionException(`All (${this.#bindings.length}) bindings rejected.`)
27
- }
28
37
 
29
38
  return reply
30
39
  }
@@ -3,6 +3,7 @@
3
3
  const { Component } = require('../src/component')
4
4
  const { codes } = require('../src/exceptions')
5
5
  const fixtures = require('./component.fixtures')
6
+ const { AssertionError } = require('node:assert')
6
7
 
7
8
  describe('Invocations', () => {
8
9
  const name = ['foo', 'bar'][Math.floor(2 * Math.random())]
@@ -21,7 +22,7 @@ describe('Invocations', () => {
21
22
 
22
23
  it('should throw on unknown invocation name', async () => {
23
24
  await expect(() => component.invoke('baz'))
24
- .rejects.toMatchObject({ code: codes.NotImplemented })
25
+ .rejects.toThrow(AssertionError)
25
26
  })
26
27
 
27
28
  it('should invoke input and query', async () => {
@@ -2,19 +2,19 @@
2
2
 
3
3
  const { generate } = require('randomstring')
4
4
 
5
- const { Conditions } = require('../../src/contract/conditions')
5
+ const { Contract } = require('../../src/contract/contract')
6
6
  const fixtures = require('./contract.fixtures')
7
7
 
8
- let conditions
8
+ let contract
9
9
 
10
10
  beforeEach(() => {
11
- conditions = new Conditions(fixtures.schema)
11
+ contract = new Contract(fixtures.schema)
12
12
  })
13
13
 
14
14
  it('should fit value', () => {
15
15
  const value = { foo: generate() }
16
16
 
17
- conditions.fit(value)
17
+ contract.fit(value)
18
18
 
19
19
  expect(fixtures.schema.fit).toHaveBeenCalledWith(value)
20
20
  })
@@ -22,5 +22,5 @@ it('should fit value', () => {
22
22
  it('should throw on invalid value', () => {
23
23
  const value = { invalid: true }
24
24
 
25
- expect(() => conditions.fit(value)).toThrow()
25
+ expect(() => contract.fit(value)).toThrow()
26
26
  })
@@ -3,10 +3,10 @@
3
3
  const clone = require('clone-deep')
4
4
  const { generate } = require('randomstring')
5
5
 
6
- jest.mock('../../src/contract/conditions')
6
+ jest.mock('../../src/contract/contract')
7
7
 
8
8
  const { Request } = require('../../src/contract/request')
9
- const { Conditions } = require('../../src/contract/conditions')
9
+ const { Contract } = require('../../src/contract/contract')
10
10
  const fixtures = require('./contract.fixtures')
11
11
 
12
12
  let contract
@@ -14,14 +14,14 @@ let contract
14
14
  beforeEach(() => {
15
15
  jest.clearAllMocks()
16
16
 
17
- contract = new Request(fixtures.schema)
17
+ contract = new Request(fixtures.schema, {})
18
18
  })
19
19
 
20
20
  const dummy = { schema: { properties: {} } }
21
21
 
22
22
  it('should extend Conditions', () => {
23
- expect(contract).toBeInstanceOf(Conditions)
24
- expect(Conditions).toHaveBeenCalledWith(fixtures.schema)
23
+ expect(contract).toBeInstanceOf(Contract)
24
+ expect(Contract).toHaveBeenCalledWith(fixtures.schema)
25
25
  })
26
26
 
27
27
  it('should fit request', () => {
@@ -29,7 +29,7 @@ it('should fit request', () => {
29
29
 
30
30
  contract.fit(request)
31
31
 
32
- expect(Conditions.mock.instances[0].fit).toHaveBeenCalledWith(request)
32
+ expect(Contract.mock.instances[0].fit).toHaveBeenCalledWith(request)
33
33
  })
34
34
 
35
35
  describe('schema', () => {
@@ -59,7 +59,7 @@ describe('schema', () => {
59
59
 
60
60
  it('should not contain query if declaration.query is false', () => {
61
61
  schema.properties.query = { type: 'null' }
62
- expect(Request.schema({ query: false }, dummy)).toStrictEqual(schema)
62
+ expect(Request.schema({ query: false }, dummy)).toMatchObject(schema)
63
63
  })
64
64
 
65
65
  it('should require query if declaration.query is true', () => {
@@ -7,35 +7,7 @@ beforeEach(() => {
7
7
  jest.clearAllMocks()
8
8
  })
9
9
 
10
- describe('new', () => {
11
- it('should throw on schema error', () => {
12
- const entity = new Entity(fixtures.schema)
13
-
14
- expect(() => entity.set(fixtures.failed())).toThrow()
15
- })
16
-
17
- it('should provide state', () => {
18
- const entity = new Entity(fixtures.schema)
19
- const state = fixtures.state()
20
-
21
- entity.set(state)
22
-
23
- expect(entity.get()).toEqual(state)
24
- })
25
- })
26
-
27
10
  describe('argument', () => {
28
- it('should provide initial state if no argument passed', () => {
29
- const entity = new Entity(fixtures.schema)
30
- const defaults = fixtures.schema.defaults.mock.results[0].value
31
- const expected = {
32
- ...defaults,
33
- _version: 0
34
- }
35
-
36
- expect(entity.get()).toStrictEqual(expected)
37
- })
38
-
39
11
  it('should set state', () => {
40
12
  const state = fixtures.state()
41
13
  const entity = new Entity(fixtures.schema, state)
@@ -54,29 +26,12 @@ it('should provide event', () => {
54
26
 
55
27
  const event = entity.event()
56
28
 
57
- expect(event).toEqual({
29
+ expect(event).toEqual(expect.objectContaining({
58
30
  state,
59
31
  origin,
60
- changeset: {
32
+ changeset: expect.objectContaining({
61
33
  foo: 'new value',
62
34
  _version: 1
63
- }
64
- })
65
- })
66
-
67
- it('should define `id` as readonly', async () => {
68
- const origin = fixtures.state()
69
- const entity = new Entity(fixtures.schema, origin)
70
- const state = entity.get()
71
-
72
- expect(() => (state.id = 1)).toThrow('assign to read only property')
73
- })
74
-
75
- it('should seal id', async () => {
76
- const origin = fixtures.state()
77
- const entity = new Entity(fixtures.schema, origin)
78
- const state = entity.get()
79
- const redefine = () => Object.defineProperty(state, 'id', { writable: true })
80
-
81
- expect(redefine).toThrow('redefine property')
35
+ })
36
+ }))
82
37
  })
@@ -1,26 +1,28 @@
1
1
  import * as _core from './index'
2
2
 
3
- declare namespace toa.core.bindings{
3
+ declare namespace toa.core.bindings {
4
4
 
5
5
  type Properties = {
6
6
  async?: boolean
7
7
  }
8
8
 
9
- interface Consumer extends _core.Connector{
9
+ interface Consumer extends _core.Connector {
10
10
  request (request: Request): Promise<_core.Reply>
11
+
12
+ task (request: Request): Promise<void>
11
13
  }
12
14
 
13
- interface Emitter extends _core.Connector{
15
+ interface Emitter extends _core.Connector {
14
16
  emit (message: _core.Message): Promise<void>
15
17
  }
16
18
 
17
- interface Broadcast<L> extends _core.Connector{
19
+ interface Broadcast<L> extends _core.Connector {
18
20
  transmit<T> (label: L, payload: T): Promise<void>
19
21
 
20
22
  receive<T> (label: L, callback: (payload: T) => void | Promise<void>): Promise<void>
21
23
  }
22
24
 
23
- interface Factory{
25
+ interface Factory {
24
26
  producer? (locator: _core.Locator, endpoints: Array<string>, producer: _core.Component): _core.Connector
25
27
 
26
28
  consumer? (locator: _core.Locator, endpoint: string): Consumer
@@ -1,9 +1,12 @@
1
1
  import { Connector } from './connector'
2
2
  import { Locator } from './locator'
3
3
  import { Request } from './request'
4
+ import { Operation } from './operations'
4
5
 
5
- export interface Component extends Connector{
6
+ export class Component extends Connector {
6
7
  locator: Locator
7
8
 
9
+ constructor (locator: Locator, operations: Record<string, Operation>)
10
+
8
11
  invoke<T = any> (endpoint: string, request: Request): Promise<T>
9
12
  }
package/types/index.ts CHANGED
@@ -5,6 +5,7 @@ export * as bridges from './bridges'
5
5
  export * as operations from './operations'
6
6
 
7
7
  export type { Component } from './component'
8
+ export type { Remote } from './remote'
8
9
  export { Connector } from './connector'
9
10
  export type { Context } from './context'
10
11
  export type { Exception } from './exception'
@@ -1,10 +1,11 @@
1
- export class Locator {
1
+ export class Locator{
2
2
  public readonly name: string
3
3
  public readonly namespace: string
4
4
 
5
5
  public readonly id: string
6
6
  public readonly label: string
7
7
  public readonly uppercase: string
8
+ public readonly lowercase: string
8
9
 
9
10
  constructor (name: string, namespace?: string)
10
11
 
@@ -1,2 +1,8 @@
1
+ import { Request } from './request'
2
+
1
3
  export type type = 'transition' | 'observation' | 'assignment' | 'computation' | 'effect'
2
4
  export type scope = 'object' | 'objects' | 'changeset'
5
+
6
+ export class Operation {
7
+ invoke<T = any> (request: Request): Promise<T>
8
+ }
@@ -0,0 +1,11 @@
1
+ import { Component } from './component'
2
+
3
+ export class Remote extends Component {
4
+ explain: (endpoint: string) => Explanation
5
+ }
6
+
7
+ interface Explanation {
8
+ input: object | null
9
+ output: object | null
10
+ errors?: string[]
11
+ }
@@ -14,6 +14,7 @@ export interface Request {
14
14
  input?: any
15
15
  query?: Query
16
16
  authentic?: boolean
17
+ task?: boolean
17
18
  }
18
19
 
19
20
  export interface Reply {
@@ -1,21 +0,0 @@
1
- 'use strict'
2
-
3
- const { SystemException } = require('../exceptions')
4
-
5
- class Conditions {
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) throw new this.constructor.Exception(error)
16
- }
17
-
18
- static Exception = SystemException
19
- }
20
-
21
- exports.Conditions = Conditions