@toa.io/bridges.node 0.2.0-dev.0 → 0.2.1-dev.3

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 (42) hide show
  1. package/package.json +5 -3
  2. package/readme.md +103 -0
  3. package/src/algorithms/class.js +6 -0
  4. package/src/algorithms/factory.js +10 -0
  5. package/src/algorithms/function.js +10 -0
  6. package/src/algorithms/runner.js +50 -0
  7. package/src/context.js +68 -3
  8. package/src/define/.operations/define.js +16 -0
  9. package/src/define/.operations/extract.js +48 -0
  10. package/src/define/.operations/index.js +7 -0
  11. package/src/define/.operations/syntaxes/class.js +42 -0
  12. package/src/define/.operations/syntaxes/constants.js +4 -0
  13. package/src/define/.operations/syntaxes/factory.js +27 -0
  14. package/src/define/.operations/syntaxes/function.js +41 -0
  15. package/src/define/.operations/syntaxes/index.js +5 -0
  16. package/src/define/index.js +3 -6
  17. package/src/define/operations.js +11 -2
  18. package/src/event.js +8 -1
  19. package/src/factory.js +24 -4
  20. package/src/index.js +0 -1
  21. package/src/load.js +1 -1
  22. package/src/receiver.js +8 -1
  23. package/test/algorithms.runner.test.js +40 -0
  24. package/test/context.configuration.fixtures.js +21 -0
  25. package/test/context.configuration.test.js +23 -0
  26. package/test/context.origins.fixtures.js +16 -0
  27. package/test/context.origins.test.js +53 -0
  28. package/test/define.algorithms.test.js +36 -0
  29. package/test/define.operations.define.test.js +195 -0
  30. package/test/dummies/one/operations/cls.js +18 -0
  31. package/test/dummies/one/operations/fct.js +20 -0
  32. package/test/dummies/one/operations/fn.js +7 -0
  33. package/test/factory.algorithm.test.js +43 -0
  34. package/types/algorithms.d.ts +15 -0
  35. package/types/context.d.ts +21 -0
  36. package/types/define.d.ts +30 -0
  37. package/LICENSE +0 -22
  38. package/src/define/operations/definition.js +0 -40
  39. package/src/operation.js +0 -23
  40. package/test/operation.fixtures.js +0 -9
  41. package/test/operation.test.js +0 -39
  42. package/test/operations.definition.test.js +0 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/bridges.node",
3
- "version": "0.2.0-dev.0",
3
+ "version": "0.2.1-dev.3",
4
4
  "description": "Toa Node Bridge (inproc)",
5
5
  "homepage": "https://toa.io",
6
6
  "author": {
@@ -26,11 +26,13 @@
26
26
  "test": "echo \"Error: run tests from root\" && exit 1"
27
27
  },
28
28
  "dependencies": {
29
- "@babel/parser": "7.16.2",
29
+ "@toa.io/core": "*",
30
+ "@toa.io/filesystem": "*",
31
+ "@toa.io/generic": "*",
30
32
  "fast-glob": "3.2.7"
31
33
  },
32
34
  "devDependencies": {
33
35
  "clone-deep": "4.0.1"
34
36
  },
35
- "gitHead": "bee1e46d597d4f8a897fb8446bbeb5861b307d64"
37
+ "gitHead": "2be07592325b2e4dc823e81d882a4e50bf50de24"
36
38
  }
package/readme.md ADDED
@@ -0,0 +1,103 @@
1
+ # Node.js Bridge
2
+
3
+ > Currently, Node.js bridge only
4
+ > supports [CommonJS modules](https://nodejs.org/api/modules.html#modules-commonjs-modules).
5
+
6
+ ## Algorithm Definition
7
+
8
+ Operation's algorithms are defined as CommonJS modules in under `operations` directory in the
9
+ component root. Algorithm module must export a function which is Algorithm Function, Class or
10
+ Factory. Module file name without extension is an operation name (endpoint).
11
+
12
+ ### Function
13
+
14
+ ```javascript
15
+ // operations/create.js
16
+
17
+ function transition (input, object, context) {
18
+ // ...
19
+
20
+ return { output: { foo: 'bar' } }
21
+ }
22
+
23
+ exports.transition = transition
24
+ ```
25
+
26
+ Exported function's name defines operation `type` property, thus must be one of:
27
+ `transition`, `observation`, or `assignment`. Second (state) argument name must be `object`,
28
+ `objects`, or `changeset` as it defines operation's `scope`.
29
+
30
+ Following function signature defines operation of `observation` type with `objects` scope.
31
+
32
+ ```javascript
33
+ // operations/set.js
34
+
35
+ function observation (input, objects) {
36
+ // ...
37
+ }
38
+ ```
39
+
40
+ See [Operation properties](#).
41
+
42
+ ### Class
43
+
44
+ ```javascript
45
+ // operations/transit.js
46
+
47
+ class Transition {
48
+ constructor (context) {}
49
+
50
+ execute (input, object) {
51
+ // ...
52
+
53
+ return { output: { foo: 'bar' } }
54
+ }
55
+ }
56
+
57
+ exports.Transition = Transition
58
+ ```
59
+
60
+ Exported class name must be one of: `Transition`, `Observation`, or `Assignment`, as it defines
61
+ operation's `type`. Class must implement [Algorithm interface](./types/operations.d.ts).
62
+ Second (state) argument name of the `execute` method must be `object`, `objects`, or `changeset` as
63
+ it defines operation's `scope`.
64
+
65
+ ### Factory
66
+
67
+ ```javascript
68
+ class ObjectTransitionFactory {
69
+ constructor (context) {}
70
+
71
+ create () {
72
+ // ...
73
+ }
74
+ }
75
+
76
+ exports.ObjectTransitionFactory = ObjectTransitionFactory
77
+ ```
78
+
79
+ Exported class name must follow the pattern: `{Subject}{Type}Factory`, where `Subject` and `Type`
80
+ defines operation's `scope` and `type` respectively. Class must
81
+ implement [Algorithm Factory interface](#).
82
+
83
+ > Factory class name examples: `ObjectTransitionFactory`, `ObjectsObservationFactory`,
84
+ > `ChangesetAssignmentFactory`.
85
+
86
+ ### Return value
87
+
88
+ Algorithm's return value must match [UCP Response](#), that is, to be an object with either `output`
89
+ or `error` properties. If return value is a primitive or an object without neither `output`
90
+ nor `error` properties, then it is considered as the value of `output`.
91
+
92
+ Next two return values are equivalent.
93
+
94
+ ```javascript
95
+ return { ok: 1 }
96
+
97
+ return { output: { ok: 1 } }
98
+ ```
99
+
100
+ ### Storing Context
101
+
102
+ > Algorithm definition should store reference to the `context` object without copying its value
103
+ > type variables as they may change over time.
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ /** @type {toa.node.define.algorithms.Constructor} */
4
+ const create = (Class, context) => new Class(context)
5
+
6
+ exports.create = create
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ /** @type {toa.node.define.algorithms.Constructor} */
4
+ const create = (Factory, context) => {
5
+ const factory = new Factory(context)
6
+
7
+ return factory.create()
8
+ }
9
+
10
+ exports.create = create
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ /** @type {toa.node.define.algorithms.Constructor} */
4
+ const create = (func, context) => {
5
+ const run = (input, state) => func(input, state, context)
6
+
7
+ return { run }
8
+ }
9
+
10
+ exports.create = create
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const { Connector } = require('@toa.io/core')
4
+
5
+ /**
6
+ * @implements {toa.core.bridges.Algorithm}
7
+ */
8
+ class Runner extends Connector {
9
+ /** @type {toa.node.algorithms.Constructor} */
10
+ #ctor
11
+
12
+ /** @type {toa.core.bridges.Algorithm} */
13
+ #instance
14
+
15
+ /**
16
+ * @param {toa.node.algorithms.Constructor} ctor
17
+ * @param {toa.node.Context} context
18
+ */
19
+ constructor (ctor, context) {
20
+ super()
21
+
22
+ this.#ctor = ctor
23
+
24
+ this.depends(context)
25
+ }
26
+
27
+ async connection () {
28
+ this.#instance = /** @type {toa.core.bridges.Algorithm} */ this.#ctor()
29
+ }
30
+
31
+ async run (input, state) {
32
+ let reply = await this.#instance.run(input, state)
33
+
34
+ if (reply !== undefined) reply = normalize(reply)
35
+
36
+ return reply
37
+ }
38
+ }
39
+
40
+ function normalize (reply) {
41
+ const object = typeof reply === 'object'
42
+ const output = object && reply.output !== undefined
43
+ const error = object && reply.error !== undefined
44
+
45
+ if (!output && !error) reply = { output: reply }
46
+
47
+ return reply
48
+ }
49
+
50
+ exports.Runner = Runner
package/src/context.js CHANGED
@@ -1,11 +1,21 @@
1
1
  'use strict'
2
2
 
3
3
  const { Connector } = require('@toa.io/core')
4
- const { underlay } = require('@toa.io/gears')
4
+ const { underlay } = require('@toa.io/generic')
5
5
 
6
+ /**
7
+ * @implements {toa.node.Context}
8
+ */
6
9
  class Context extends Connector {
10
+ aspects
11
+ configuration
12
+ origins
13
+
7
14
  #context
8
15
 
16
+ /**
17
+ * @param {toa.core.Context} context
18
+ */
9
19
  constructor (context) {
10
20
  super()
11
21
 
@@ -14,8 +24,63 @@ class Context extends Connector {
14
24
  this.depends(context)
15
25
  }
16
26
 
17
- local = underlay(async (endpoint, request) => this.#context.apply(endpoint, request))
18
- remote = underlay(async (domain, name, endpoint, request) => this.#context.call(domain, name, endpoint, request))
27
+ async connection () {
28
+ this.aspects = this.#aspects(/** @type {toa.core.extensions.Aspect[]} */ this.#context.aspects)
29
+ }
30
+
31
+ local = underlay(async ([endpoint], [request]) => {
32
+ return this.#context.apply(endpoint, request)
33
+ })
34
+
35
+ remote = underlay(async ([namespace, name, endpoint], [request]) => {
36
+ return this.#context.call(namespace, name, endpoint, request)
37
+ })
38
+
39
+ /**
40
+ * @param {toa.core.extensions.Aspect[]} aspects
41
+ * @returns {{ [key: string]: Function}}
42
+ */
43
+ #aspects (aspects) {
44
+ const map = {}
45
+
46
+ for (const aspect of aspects) {
47
+ if (map[aspect.name] !== undefined) throw new Error(`Aspect conflict on '${aspect.name}'`)
48
+
49
+ map[aspect.name] = aspect.invoke.bind(aspect)
50
+
51
+ // well-known aspects
52
+ if (aspect.name === 'configuration') this.#configuration(aspect)
53
+ if (aspect.name === 'origins') this.origins = this.#origins(aspect)
54
+ }
55
+
56
+ return map
57
+ }
58
+
59
+ /**
60
+ * @param {toa.core.extensions.Aspect} aspect
61
+ */
62
+ #origins (aspect) {
63
+ return underlay(async (segs, args) => {
64
+ if (segs.length < 2) throw new Error(`Origins call requires at least 2 arguments, ${segs.length} given`)
65
+
66
+ const name = segs.shift()
67
+ const method = segs.pop().toUpperCase()
68
+ const path = segs.join('/')
69
+ const request = { method, ...args[0] }
70
+ const options = args[1]
71
+
72
+ return await aspect.invoke(name, path, request, options)
73
+ })
74
+ }
75
+
76
+ /**
77
+ * @param {toa.core.extensions.Aspect} aspect
78
+ */
79
+ #configuration (aspect) {
80
+ Object.defineProperty(this, 'configuration', {
81
+ get: () => aspect.invoke()
82
+ })
83
+ }
19
84
  }
20
85
 
21
86
  exports.Context = Context
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const { extract } = require('./extract')
4
+ const syntaxes = require('./syntaxes')
5
+
6
+ /**
7
+ * @param {Object} module
8
+ * @returns {toa.node.define.operations.Definition}
9
+ */
10
+ const define = (module) => {
11
+ const descriptor = extract(module)
12
+
13
+ return syntaxes[descriptor.syntax].define(descriptor)
14
+ }
15
+
16
+ exports.define = define
@@ -0,0 +1,48 @@
1
+ 'use strict'
2
+
3
+ const parser = require('@babel/parser')
4
+ const syntaxes = require('./syntaxes')
5
+
6
+ /**
7
+ * @param {Object} module
8
+ * @returns {toa.node.define.algorithms.Descriptor}
9
+ */
10
+ const extract = (module) => {
11
+ const [name, func] = find(module)
12
+ const statement = parse(func)
13
+
14
+ /** @type {toa.node.define.algorithms.Descriptor} */
15
+ const descriptor = { name, statement, syntax: undefined }
16
+
17
+ for (const [syntax, { test }] of Object.entries(syntaxes)) {
18
+ if (test(statement, name)) descriptor.syntax = /** @type {toa.node.define.algorithms.Syntax} */ syntax
19
+ }
20
+
21
+ if (descriptor.syntax === undefined) throw new Error('Exported function does not match conventions')
22
+
23
+ return descriptor
24
+ }
25
+
26
+ /**
27
+ * @param {Object} module
28
+ * @returns [string, Function]
29
+ */
30
+ const find = (module) => {
31
+ for (const [key, value] of Object.entries(module)) {
32
+ if (typeof value === 'function') return [key, value]
33
+ }
34
+
35
+ throw new Error('Module does not export function')
36
+ }
37
+
38
+ /**
39
+ * @param {Function} func
40
+ * @returns {import('@babel/types').Statement}
41
+ */
42
+ const parse = (func) => {
43
+ const file = parser.parse(func.toString())
44
+
45
+ return file.program.body[0]
46
+ }
47
+
48
+ exports.extract = extract
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { define } = require('./define')
4
+ const { extract } = require('./extract')
5
+
6
+ exports.define = define
7
+ exports.extract = extract
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ const { letters: { capitalize } } = require('@toa.io/generic')
4
+ const { types } = require('./constants')
5
+ const func = require('./function')
6
+
7
+ /** @type {toa.node.define.operations.Define} */
8
+ const define = (descriptor) => {
9
+ const declaration = /** @type {import('@babel/types').ClassDeclaration} */ descriptor.statement
10
+
11
+ descriptor.name = descriptor.name.toLowerCase()
12
+ descriptor.statement = method(declaration, 'run')
13
+
14
+ return func.define(descriptor)
15
+ }
16
+
17
+ /** @type {toa.node.define.operations.Test} */
18
+ const test = (statement, name) => {
19
+ const declaration = statement.type === 'ClassDeclaration'
20
+ const known = names.includes(name)
21
+
22
+ return declaration && known
23
+ }
24
+
25
+ /**
26
+ * @param {import('@babel/types').ClassDeclaration} statement
27
+ * @param {string} name
28
+ * @returns {import('@babel/types').Statement}
29
+ */
30
+ const method = (statement, name) => {
31
+ const methods = statement.body.body
32
+ const method = methods.find((method) => method.type === 'ClassMethod' && method.key.name === name)
33
+
34
+ if (method === undefined) throw new Error(`Method '${name}' not found`)
35
+
36
+ return /** @type {import('@babel/types').Statement} */ method
37
+ }
38
+
39
+ const names = types.map((type) => capitalize(type))
40
+
41
+ exports.define = define
42
+ exports.test = test
@@ -0,0 +1,4 @@
1
+ 'use strict'
2
+
3
+ exports.types = ['transition', 'observation', 'assignment']
4
+ exports.scopes = ['object', 'objects', 'changeset', 'none']
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ /** @type {toa.node.define.operations.Define} */
4
+ const define = (descriptor) => {
5
+ const match = descriptor.name.match(pattern)
6
+
7
+ /** @type {toa.node.define.operations.Definition} */
8
+ const definition = {}
9
+
10
+ definition.type = /** @type {typeof toa.norm.component.Operation.scope} */ match[2].toLowerCase()
11
+ definition.scope = /** @type {typeof toa.norm.component.Operation.type} */ match[1].toLowerCase()
12
+
13
+ return definition
14
+ }
15
+
16
+ /** @type {toa.node.define.operations.Test} */
17
+ const test = (statement, name) => {
18
+ const declaration = statement.type === 'ClassDeclaration'
19
+ const match = name.match(pattern) !== null
20
+
21
+ return declaration && match
22
+ }
23
+
24
+ const pattern = new RegExp('^(Objects?|Changeset|None)(Transition|Observation|Assignment)Factory$')
25
+
26
+ exports.define = define
27
+ exports.test = test
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ const { types, scopes } = require('./constants')
4
+
5
+ /** @type {toa.node.define.operations.Define} */
6
+ const define = (descriptor) => {
7
+ const { statement, name } = descriptor
8
+ const node = statement.type === 'ExpressionStatement' ? statement.expression : statement
9
+
10
+ /** @type {toa.node.define.operations.Definition} */
11
+ const definition = {}
12
+
13
+ definition.type = /** @type {typeof toa.norm.component.Operation.type} */ name
14
+
15
+ if (node.params.length > 1) definition.scope = scope(node.params[1].name)
16
+ else definition.scope = 'none'
17
+
18
+ if (node.params.length === 0) definition.input = null
19
+
20
+ return definition
21
+ }
22
+
23
+ /** @type {toa.node.define.operations.Test} */
24
+ const test = (statement, type) => {
25
+ const node = statement.type === 'ExpressionStatement' ? statement.expression : statement
26
+ const func = nodes.includes(node.type)
27
+ const known = types.includes(type)
28
+
29
+ return func && known
30
+ }
31
+
32
+ /**
33
+ * @param {string} name
34
+ * @returns {typeof toa.norm.component.Operation.scope}
35
+ */
36
+ const scope = (name) => scopes.includes(name) ? name : undefined
37
+
38
+ const nodes = ['FunctionDeclaration', 'ArrowFunctionExpression', 'ClassMethod']
39
+
40
+ exports.define = define
41
+ exports.test = test
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ exports.function = require('./function')
4
+ exports.class = require('./class')
5
+ exports.factory = require('./factory')
@@ -1,12 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { events, event } = require('./events')
4
- const { receivers, receiver } = require('./receivers')
5
- const { operation, operations } = require('./operations')
3
+ const { events } = require('./events')
4
+ const { receivers } = require('./receivers')
5
+ const { operations } = require('./operations')
6
6
 
7
- exports.event = event
8
7
  exports.events = events
9
- exports.receiver = receiver
10
8
  exports.receivers = receivers
11
- exports.operation = operation
12
9
  exports.operations = operations
@@ -1,12 +1,21 @@
1
1
  'use strict'
2
2
 
3
- const { definition } = require('./operations/definition')
4
3
  const load = require('../load')
4
+ const algorithm = require('./.operations')
5
5
 
6
+ /** @type {toa.node.define.Algorithms} */
6
7
  const operations = async (root) => {
7
8
  const modules = await load.operations(root)
8
9
 
9
- return Object.fromEntries(modules.map(([name, module]) => [name, definition(module)]))
10
+ /** @type {toa.node.define.algorithms.List} */
11
+ const algorithms = {}
12
+
13
+ for (const [name, module] of modules) algorithms[name] = algorithm.define(module)
14
+
15
+ return algorithms
10
16
  }
11
17
 
18
+ const extract = (module) => algorithm.extract(module)
19
+
12
20
  exports.operations = operations
21
+ exports.extract = extract
package/src/event.js CHANGED
@@ -1,9 +1,16 @@
1
1
  'use strict'
2
2
 
3
- class Event {
3
+ const { Connector } = require('@toa.io/core')
4
+
5
+ /**
6
+ * @implements {toa.core.bridges.Event}
7
+ */
8
+ class Event extends Connector {
4
9
  #event
5
10
 
6
11
  constructor (event) {
12
+ super()
13
+
7
14
  this.#event = event
8
15
  }
9
16
 
package/src/factory.js CHANGED
@@ -1,28 +1,48 @@
1
1
  'use strict'
2
2
 
3
3
  const load = require('./load')
4
- const { Operation } = require('./operation')
4
+ const { Runner } = require('./algorithms/runner')
5
5
  const { Event } = require('./event')
6
6
  const { Receiver } = require('./receiver')
7
7
  const { Context } = require('./context')
8
+ const { extract } = require('./define/operations')
8
9
 
10
+ /**
11
+ * @implements {toa.core.bridges.Factory}
12
+ */
9
13
  class Factory {
10
- operation (root, name, context) {
11
- const operation = load.operation(root, name)
14
+ algorithm (root, name, context) {
15
+ const module = load.operation(root, name)
12
16
  const ctx = new Context(context)
13
17
 
14
- return new Operation(operation, ctx)
18
+ return runner(module, ctx)
15
19
  }
16
20
 
17
21
  event (root, label) {
18
22
  const event = load.event(root, label)
23
+
19
24
  return new Event(event)
20
25
  }
21
26
 
22
27
  receiver (root, label) {
23
28
  const receiver = load.receiver(root, label)
29
+
24
30
  return new Receiver(receiver)
25
31
  }
26
32
  }
27
33
 
34
+ /**
35
+ * @param {Object} module
36
+ * @param {toa.node.Context} context
37
+ * @returns {Runner}
38
+ */
39
+ function runner (module, context) {
40
+ const descriptor = extract(module)
41
+ const func = module[descriptor.name]
42
+ const factory = require('./algorithms/' + descriptor.syntax)
43
+ const ctor = () => factory.create(func, context)
44
+
45
+ return new Runner(ctor, context)
46
+ }
47
+
28
48
  exports.Factory = Factory
package/src/index.js CHANGED
@@ -3,5 +3,4 @@
3
3
  const { Factory } = require('./factory')
4
4
 
5
5
  exports.define = require('./define')
6
-
7
6
  exports.Factory = Factory
package/src/load.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { basename, resolve } = require('path')
4
- const glob = require('fast-glob')
4
+ const { file: { glob } } = require('@toa.io/filesystem')
5
5
 
6
6
  const operation = (root, name) => load(root, OPERATIONS_DIRECTORY, name)
7
7
  const event = (root, name) => load(root, EVENTS_DIRECTORY, name)
package/src/receiver.js CHANGED
@@ -1,9 +1,16 @@
1
1
  'use strict'
2
2
 
3
- class Receiver {
3
+ const { Connector } = require('@toa.io/core')
4
+
5
+ /**
6
+ * @implements {toa.core.bridges.Receiver}
7
+ */
8
+ class Receiver extends Connector {
4
9
  #receiver
5
10
 
6
11
  constructor (receiver) {
12
+ super()
13
+
7
14
  this.#receiver = receiver
8
15
  }
9
16