@toa.io/core 1.0.0-alpha.28 → 1.0.0-alpha.282

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 (98) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/package.json +8 -10
  3. package/src/assignment.js +5 -8
  4. package/src/call.js +35 -9
  5. package/src/cascade.js +9 -11
  6. package/src/component.js +61 -21
  7. package/src/composition.js +3 -7
  8. package/src/connector.js +29 -22
  9. package/src/context.js +8 -6
  10. package/src/contract/contract.js +18 -0
  11. package/src/contract/index.js +2 -7
  12. package/src/contract/reply.js +36 -15
  13. package/src/contract/request.js +20 -10
  14. package/src/contract/schemas/index.js +9 -5
  15. package/src/contract/schemas/query.yaml +14 -1
  16. package/src/contract/schemas/source.yaml +23 -0
  17. package/src/discovery.js +15 -13
  18. package/src/effect.js +15 -0
  19. package/src/emission.js +2 -6
  20. package/src/entities/changeset.js +7 -14
  21. package/src/entities/entity.js +75 -29
  22. package/src/entities/factory.js +21 -14
  23. package/src/entities/index.js +1 -5
  24. package/src/entities/newid.js +9 -0
  25. package/src/entities/set.js +13 -4
  26. package/src/event.js +21 -7
  27. package/src/exceptions.js +53 -39
  28. package/src/exposition.js +5 -8
  29. package/src/guard.js +13 -0
  30. package/src/index.js +28 -47
  31. package/src/locator.js +2 -6
  32. package/src/observation.js +3 -15
  33. package/src/operation.js +40 -14
  34. package/src/outbox/index.js +1 -0
  35. package/src/outbox/outbox.js +296 -0
  36. package/src/query/criteria.js +3 -7
  37. package/src/query/options.js +5 -8
  38. package/src/query.js +36 -7
  39. package/src/receiver.js +76 -18
  40. package/src/reflection.js +2 -6
  41. package/src/remote.js +9 -12
  42. package/src/state.js +89 -50
  43. package/src/transition.js +13 -25
  44. package/src/transmission.js +15 -10
  45. package/src/unmanaged.js +7 -0
  46. package/test/call.fixtures.js +8 -12
  47. package/test/call.test.js +21 -9
  48. package/test/component.fixtures.js +5 -8
  49. package/test/component.test.js +22 -10
  50. package/test/connector.fixtures.js +10 -8
  51. package/test/connector.test.js +59 -38
  52. package/test/context.fixtures.js +8 -12
  53. package/test/context.test.js +18 -7
  54. package/test/contract/conditions.test.js +11 -9
  55. package/test/contract/contract.fixtures.js +12 -16
  56. package/test/contract/request.test.js +84 -34
  57. package/test/discovery.test.js +64 -0
  58. package/test/emission.fixtures.js +6 -10
  59. package/test/emission.test.js +29 -12
  60. package/test/entities/entity.fixtures.js +7 -11
  61. package/test/entities/entity.test.js +70 -49
  62. package/test/entities/factory.fixtures.js +22 -10
  63. package/test/entities/factory.test.js +29 -15
  64. package/test/entities/set.fixtures.js +6 -8
  65. package/test/entities/set.test.js +6 -5
  66. package/test/event.fixtures.js +9 -14
  67. package/test/event.test.js +57 -31
  68. package/test/locator.test.js +25 -24
  69. package/test/outbox.test.js +121 -0
  70. package/test/query.fixtures.js +1 -5
  71. package/test/query.test.js +42 -14
  72. package/test/receiver.fixtures.js +9 -12
  73. package/test/receiver.test.js +45 -22
  74. package/test/reflection.test.js +7 -6
  75. package/test/state.fixtures.js +25 -30
  76. package/test/state.test.js +72 -19
  77. package/test/transmission.fixtures.js +4 -6
  78. package/test/transmission.test.js +33 -15
  79. package/types/atomicity.d.ts +58 -0
  80. package/types/bindings.d.ts +8 -6
  81. package/types/bridges.ts +5 -2
  82. package/types/component.d.ts +7 -4
  83. package/types/context.d.ts +6 -3
  84. package/types/entity.d.ts +4 -4
  85. package/types/event.d.ts +1 -1
  86. package/types/extensions.d.ts +35 -10
  87. package/types/index.ts +16 -13
  88. package/types/message.d.ts +1 -0
  89. package/types/operations.d.ts +6 -0
  90. package/types/outbox.d.ts +53 -0
  91. package/types/query.d.ts +2 -0
  92. package/types/receiver.d.ts +2 -2
  93. package/types/reflection.d.ts +1 -1
  94. package/types/remote.d.ts +18 -0
  95. package/types/request.d.ts +16 -1
  96. package/types/state.d.ts +11 -8
  97. package/types/storages.d.ts +36 -11
  98. package/src/contract/conditions.js +0 -21
@@ -1,15 +1,22 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- const clone = require('clone-deep')
4
- const { generate } = require('randomstring')
5
- const { merge } = require('@toa.io/generic')
5
+ import clone from 'clone-deep'
6
+ import { generate } from 'randomstring'
7
+ import { merge } from '@toa.io/generic'
6
8
 
7
- jest.mock('../src/connector')
9
+ import { Connector } from '../src/connector.js'
8
10
 
9
- const { Connector } = /** @type {{ Connector: jest.Mock<toa.core.Connector>}} */ require('../src/connector')
11
+ // the fixtures are not connectors, so a dependency is recorded rather than linked
12
+ const depends = mock.method(Connector.prototype, 'depends', () => undefined)
10
13
 
11
- const { Receiver } = require('../src/receiver')
12
- const fixtures = require('./receiver.fixtures')
14
+ const dependencies = (instance) => depends.mock.calls
15
+ .filter((call) => call.this === instance)
16
+ .map((call) => call.arguments[0])
17
+
18
+ import { Receiver } from '../src/receiver.js'
19
+ import * as fixtures from './receiver.fixtures.js'
13
20
 
14
21
  /** @type {toa.core.Receiver} */
15
22
  let receiver
@@ -18,15 +25,16 @@ let receiver
18
25
  let definition
19
26
 
20
27
  beforeEach(() => {
21
- jest.clearAllMocks()
28
+ resetCalls()
29
+ depends.mock.resetCalls()
22
30
 
23
31
  definition = clone(fixtures.definition)
24
32
  receiver = new Receiver(fixtures.definition, fixtures.local, fixtures.bridge)
25
33
  })
26
34
 
27
35
  it('should depend on local, bridge', () => {
28
- expect(Connector.mock.instances[0].depends).toHaveBeenCalledWith(fixtures.local)
29
- expect(Connector.mock.instances[0].depends).toHaveBeenCalledWith(fixtures.bridge)
36
+ assert.ok(dependencies(receiver).some((one) => isDeepStrictEqual(one, fixtures.local)))
37
+ assert.ok(dependencies(receiver).some((one) => isDeepStrictEqual(one, fixtures.bridge)))
30
38
  })
31
39
 
32
40
  it('should apply', async () => {
@@ -34,11 +42,12 @@ it('should apply', async () => {
34
42
 
35
43
  await receiver.receive({ payload })
36
44
 
37
- expect(fixtures.local.invoke).toHaveBeenCalledWith(definition.operation, { input: payload })
45
+ assert.ok(fixtures.local.invoke.mock.calls.some((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], definition.operation) && isDeepStrictEqual(call.arguments[1], { input: payload })))
38
46
  })
39
47
 
40
- it.each([[false], [true]])('should pass UI extensions (adaptive: %s)', async (adaptive) => {
41
- jest.clearAllMocks()
48
+ for (const [adaptive] of [[false], [true]])
49
+ it(`should pass UI extensions (adaptive: ${adaptive})`, async () => {
50
+ resetCalls()
42
51
 
43
52
  definition.adaptive = adaptive
44
53
  receiver = new Receiver(definition, fixtures.local, fixtures.bridge)
@@ -49,12 +58,12 @@ it.each([[false], [true]])('should pass UI extensions (adaptive: %s)', async (ad
49
58
 
50
59
  await receiver.receive(message)
51
60
 
52
- const request = adaptive ? await fixtures.bridge.request.mock.results[0].value : { input: payload }
61
+ const request = adaptive ? await fixtures.bridge.request.mock.calls[0].result : { input: payload }
53
62
  const expected = merge(clone(request), extension)
54
63
 
55
- const argument = fixtures.local.invoke.mock.calls[0][1]
64
+ const argument = fixtures.local.invoke.mock.calls[0].arguments[1]
56
65
 
57
- expect(argument).toStrictEqual(expected)
66
+ assert.deepStrictEqual(argument, expected)
58
67
  })
59
68
 
60
69
  describe('conditioned', () => {
@@ -67,15 +76,15 @@ describe('conditioned', () => {
67
76
  const payload = { foo: 'bar' }
68
77
  await receiver.receive({ payload })
69
78
 
70
- expect(fixtures.bridge.condition).toHaveBeenCalledWith(payload)
71
- expect(fixtures.local.invoke).toHaveBeenCalledWith(definition.operation, { input: payload })
79
+ assert.ok(fixtures.bridge.condition.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], payload)))
80
+ assert.ok(fixtures.local.invoke.mock.calls.some((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], definition.operation) && isDeepStrictEqual(call.arguments[1], { input: payload })))
72
81
  })
73
82
 
74
83
  it('should not apply if condition is false', async () => {
75
84
  const payload = { reject: true }
76
85
  await receiver.receive({ payload })
77
86
 
78
- expect(fixtures.local.invoke).not.toHaveBeenCalled()
87
+ assert.strictEqual(fixtures.local.invoke.mock.callCount(), 0)
79
88
  })
80
89
  })
81
90
 
@@ -89,7 +98,21 @@ describe('adaptive', () => {
89
98
  const payload = { reject: true }
90
99
  await receiver.receive({ payload })
91
100
 
92
- expect(fixtures.local.invoke)
93
- .toHaveBeenCalledWith(definition.operation, await fixtures.bridge.request.mock.results[0].value)
101
+ const request = await fixtures.bridge.request.mock.calls[0].result
102
+
103
+ assert.ok(fixtures.local.invoke.mock.calls.some((call) =>
104
+ call.arguments.length === 2 &&
105
+ isDeepStrictEqual(call.arguments[0], definition.operation) &&
106
+ isDeepStrictEqual(call.arguments[1], request)))
94
107
  })
95
108
  })
109
+
110
+ function resetCalls (target = [assert, clone, fixtures], seen = new Set()) {
111
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
112
+
113
+ seen.add(target)
114
+
115
+ for (const value of Object.values(target))
116
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
117
+ else resetCalls(value, seen)
118
+ }
@@ -1,10 +1,11 @@
1
- 'use strict'
1
+ import { it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { generate } = require('randomstring')
4
- const { Reflection, Connector } = require('../')
4
+ import { generate } from 'randomstring'
5
+ import { Reflection, Connector } from '../src/index.js'
5
6
 
6
7
  it('should export', () => {
7
- expect(Reflection).toBeDefined()
8
+ assert.notStrictEqual(Reflection, undefined)
8
9
  })
9
10
 
10
11
  /** @type {toa.core.Reflection<string>} */
@@ -20,11 +21,11 @@ beforeEach(() => {
20
21
  })
21
22
 
22
23
  it('should be a Connector', () => {
23
- expect(reflection).toBeInstanceOf(Connector)
24
+ assert.ok(reflection instanceof Connector)
24
25
  })
25
26
 
26
27
  it('should reflect', async () => {
27
28
  await reflection.connect()
28
29
 
29
- expect(reflection.value).toStrictEqual(value)
30
+ assert.deepStrictEqual(reflection.value, value)
30
31
  })
@@ -1,47 +1,42 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const { generate } = require('randomstring')
3
+ import { generate } from 'randomstring'
4
4
 
5
- const storage = {
5
+ export const storage = {
6
6
  name: 'dummy',
7
- get: jest.fn(() => ({ id: generate() })),
8
- find: jest.fn(() => ([{ id: generate() }])),
9
- add: jest.fn(() => true),
10
- set: jest.fn(() => true),
11
- store: jest.fn(() => true)
7
+ get: mock.fn(() => ({ id: generate() })),
8
+ find: mock.fn(() => ([{ id: generate() }])),
9
+ add: mock.fn(() => true),
10
+ set: mock.fn(() => true),
11
+ store: mock.fn(() => true),
12
+ massStore: mock.fn(() => true),
13
+ upsert: mock.fn(() => ({ id: generate() })),
14
+ ensure: mock.fn((query, properties, state) => state)
12
15
  }
13
16
 
14
- const factory = {
15
- object: jest.fn(() => ({ [generate()]: generate() })),
16
- objects: jest.fn(() => ({ [generate()]: generate() }))
17
+ export const factory = {
18
+ object: mock.fn(() => ({ [generate()]: generate() })),
19
+ objects: mock.fn(() => ({ [generate()]: generate() }))
17
20
  }
18
21
 
19
- const query = generate()
22
+ export const query = generate()
20
23
 
21
- const entity = {
22
- get: jest.fn(() => ({ [generate()]: generate() })),
23
- event: jest.fn(() => ({
24
- state: { [generate()]: generate() }, changeset: { [generate()]: generate() }
25
- }))
24
+ export const entity = {
25
+ get: mock.fn(() => ({ [generate()]: generate() })),
26
+ event: mock.fn(() => ({ state: { [generate()]: generate() } }))
26
27
  }
27
28
 
28
- const initial = {
29
+ export const initial = {
29
30
  initial: true, ...entity
30
31
  }
31
32
 
32
- const unchanged = {
33
+ export const unchanged = {
33
34
  ...entity,
34
- event: jest.fn(() => ({ state: { [generate()]: generate() }, changeset: {} }))
35
+ event: mock.fn(() => ({ state: { [generate()]: generate() } }))
35
36
  }
36
37
 
37
- const emitter = {
38
- emit: jest.fn()
38
+ // a legacy outbox: no storage capability, so `publish` emits inline
39
+ export const outbox = {
40
+ row: mock.fn((event) => ({ id: generate(), lane: 0, published: false, pending: 0, event })),
41
+ publish: mock.fn()
39
42
  }
40
-
41
- exports.storage = storage
42
- exports.factory = factory
43
- exports.emitter = emitter
44
- exports.query = query
45
- exports.entity = entity
46
- exports.initial = initial
47
- exports.unchanged = unchanged
@@ -1,46 +1,99 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- const { State } = require('../src/state')
4
- const fixtures = require('./state.fixtures')
5
+ import { State } from '../src/state.js'
6
+ import * as fixtures from './state.fixtures.js'
5
7
 
6
8
  let state
7
9
 
8
10
  beforeEach(() => {
9
- jest.clearAllMocks()
11
+ resetCalls()
10
12
 
11
- state = new State(fixtures.storage, fixtures.factory, fixtures.emitter)
13
+ state = new State(fixtures.storage, fixtures.factory, fixtures.outbox)
12
14
  })
13
15
 
14
16
  it('should provide object', async () => {
15
17
  const entity = await state.object(fixtures.query)
16
18
 
17
- expect(fixtures.storage.get).toHaveBeenCalledWith(fixtures.query)
18
- expect(entity).toStrictEqual(fixtures.factory.object.mock.results[0].value)
19
- expect(fixtures.factory.object).toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value)
19
+ assert.ok(fixtures.storage.get.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], fixtures.query)))
20
+ assert.deepStrictEqual(entity, fixtures.factory.object.mock.calls[0].result)
21
+ assert.ok(fixtures.factory.object.mock.calls.some((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], fixtures.storage.get.mock.calls[0].result) && isDeepStrictEqual(call.arguments[1], true)))
20
22
  })
21
23
 
22
- it('should provide objects', async () => {
23
- const set = await state.objects(fixtures.query)
24
+ it('should provide read-only object', async () => {
25
+ await state.object(fixtures.query, false)
24
26
 
25
- expect(fixtures.storage.find).toHaveBeenCalledWith(fixtures.query)
26
- expect(set).toStrictEqual(fixtures.factory.objects.mock.results[0].value)
27
- expect(fixtures.factory.objects).toHaveBeenCalledWith(fixtures.storage.find.mock.results[0].value)
27
+ assert.ok(fixtures.factory.object.mock.calls.some((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], fixtures.storage.get.mock.calls[0].result) && isDeepStrictEqual(call.arguments[1], false)))
28
+ })
29
+
30
+ it('should provide read-only objects', async () => {
31
+ await state.objects(fixtures.query, false)
32
+
33
+ assert.ok(fixtures.factory.objects.mock.calls.some((call) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], fixtures.storage.find.mock.calls[0].result) && isDeepStrictEqual(call.arguments[1], undefined) && isDeepStrictEqual(call.arguments[2], false)))
28
34
  })
29
35
 
30
36
  it('should store entity', async () => {
31
37
  await state.commit(fixtures.initial)
32
38
 
33
- expect(fixtures.storage.store).toHaveBeenCalledWith(fixtures.initial.get.mock.results[0].value)
39
+ assert.ok(fixtures.storage.store.mock.calls.some((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], fixtures.initial.get.mock.calls[0].result) && isDeepStrictEqual(call.arguments[1], fixtures.outbox.row.mock.calls[0].result)))
34
40
  })
35
41
 
36
- it('should emit', async () => {
42
+ it('should publish the row', async () => {
37
43
  await state.commit(fixtures.entity)
38
44
 
39
- expect(fixtures.emitter.emit).toHaveBeenCalledWith(fixtures.entity.event.mock.results[0].value)
45
+ assert.ok(fixtures.outbox.row.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], fixtures.entity.event.mock.calls[0].result)))
46
+ assert.ok(fixtures.outbox.publish.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], fixtures.outbox.row.mock.calls[0].result)))
40
47
  })
41
48
 
42
- it('should not emit if state has not been changed', async () => {
43
- await state.commit(fixtures.unchanged)
49
+ it('should not publish if the write did not happen', async () => {
50
+ fixtures.storage.store.mock.mockImplementationOnce(() => false)
44
51
 
45
- expect(fixtures.emitter.emit).not.toHaveBeenCalled()
52
+ await state.commit(fixtures.entity)
53
+
54
+ assert.strictEqual(fixtures.outbox.publish.mock.callCount(), 0)
46
55
  })
56
+
57
+ it('should build the row before the write', async () => {
58
+ // the storage commits the row in the same transaction, so it must already exist
59
+ fixtures.storage.store.mock.mockImplementationOnce((_, row) => {
60
+ assert.notStrictEqual(row, undefined)
61
+
62
+ return true
63
+ })
64
+
65
+
66
+ await state.commit(fixtures.entity)
67
+ })
68
+
69
+ describe('assignment', () => {
70
+ const changeset = { query: 'q', export: () => ({ foo: 1 }) }
71
+
72
+ it('should pass the row to upsert and publish it', async () => {
73
+ const result = await state.apply(changeset, { foo: 1 })
74
+
75
+ assert.ok(fixtures.storage.upsert.mock.calls.some((call) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], changeset.query) && isDeepStrictEqual(call.arguments[1], { foo: 1 }) && isDeepStrictEqual(call.arguments[2], fixtures.outbox.row.mock.calls[0].result)))
76
+
77
+ assert.ok(fixtures.outbox.publish.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], fixtures.outbox.row.mock.calls[0].result)))
78
+ assert.deepStrictEqual(result, fixtures.storage.upsert.mock.calls[0].result)
79
+ })
80
+
81
+ it('should fill the state a storage without the outbox left alone', async () => {
82
+ await state.apply(changeset, { foo: 1 })
83
+
84
+ const row = fixtures.outbox.row.mock.calls[0].result
85
+
86
+ assert.deepStrictEqual(row.event.state, fixtures.storage.upsert.mock.calls[0].result)
87
+ assert.deepStrictEqual(row.event.input, { foo: 1 })
88
+ })
89
+ })
90
+
91
+ function resetCalls (target = [assert, fixtures], seen = new Set()) {
92
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
93
+
94
+ seen.add(target)
95
+
96
+ for (const value of Object.values(target))
97
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
98
+ else resetCalls(value, seen)
99
+ }
@@ -1,15 +1,13 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const { generate } = require('randomstring')
3
+ import { generate } from 'randomstring'
4
4
 
5
5
  const binding = (index) => ({
6
- request: jest.fn(async (request) => {
6
+ request: mock.fn(async (request) => {
7
7
  if (request?.pick !== undefined && request.pick !== index) return false
8
8
 
9
9
  return { output: generate() }
10
10
  })
11
11
  })
12
12
 
13
- const bindings = [0, 1, 2, 3, 4].map(binding)
14
-
15
- exports.bindings = bindings
13
+ export const bindings = [0, 1, 2, 3, 4].map(binding)
@@ -1,16 +1,24 @@
1
- 'use strict'
1
+ import { it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- jest.mock('../src/connector')
5
+ import { Connector } from '../src/connector.js'
4
6
 
5
- const { Connector } = require('../src/connector')
6
- const { Transmission } = require('../src/transmission')
7
- const { codes } = require('../src/exceptions')
8
- const fixtures = require('./transmission.fixtures')
7
+ // the fixtures are not connectors, so a dependency is recorded rather than linked
8
+ const depends = mock.method(Connector.prototype, 'depends', () => undefined)
9
+
10
+ const dependencies = (instance) => depends.mock.calls
11
+ .filter((call) => call.this === instance)
12
+ .map((call) => call.arguments[0])
13
+ import { Transmission } from '../src/transmission.js'
14
+ import { codes } from '../src/exceptions.js'
15
+ import * as fixtures from './transmission.fixtures.js'
9
16
 
10
17
  let transmission
11
18
 
12
19
  beforeEach(() => {
13
- jest.clearAllMocks()
20
+ resetCalls()
21
+ depends.mock.resetCalls()
14
22
  })
15
23
 
16
24
  beforeEach(() => {
@@ -18,29 +26,39 @@ beforeEach(() => {
18
26
  })
19
27
 
20
28
  it('should be instance of Connector depending on bindings', () => {
21
- expect(transmission).toBeInstanceOf(Connector)
22
- expect(Connector.mock.instances[0].depends).toHaveBeenCalledWith(fixtures.bindings)
29
+ assert.ok(transmission instanceof Connector)
30
+ assert.ok(dependencies(transmission).some((one) => isDeepStrictEqual(one, fixtures.bindings)))
23
31
  })
24
32
 
25
33
  it('should pass arguments and return value', async () => {
26
34
  const request = { foo: 'bar' }
27
35
  const result = await transmission.request(request)
28
36
 
29
- expect(fixtures.bindings[0].request).toHaveBeenCalledWith(request)
30
- expect(result).toBe(await fixtures.bindings[0].request.mock.results[0].value)
37
+ assert.ok(fixtures.bindings[0].request.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], request)))
38
+ assert.strictEqual(result, await fixtures.bindings[0].request.mock.calls[0].result)
31
39
  })
32
40
 
33
41
  it('should pick bindings sequentially', async () => {
34
42
  let result = await transmission.request()
35
- expect(result).toBe(await fixtures.bindings[0].request.mock.results[0].value)
43
+ assert.strictEqual(result, await fixtures.bindings[0].request.mock.calls[0].result)
36
44
 
37
45
  result = await transmission.request({ pick: 1 })
38
- expect(result).toBe(await fixtures.bindings[1].request.mock.results[0]?.value)
46
+ assert.strictEqual(result, await fixtures.bindings[1].request.mock.calls[0]?.result)
39
47
  })
40
48
 
41
49
  it('should throw exception if none succeeded', async () => {
42
- await expect(transmission.request({ pick: 5 })).rejects.toMatchObject({ code: codes.Transmission })
50
+ await assert.rejects(transmission.request({ pick: 5 }), (error) => { assert.partialDeepStrictEqual(error, { code: codes.Transmission }); return true })
43
51
 
44
52
  fixtures.bindings.forEach((binding) =>
45
- expect(binding.request).toHaveBeenCalled())
53
+ assert.ok(binding.request.mock.callCount() > 0))
46
54
  })
55
+
56
+ function resetCalls (target = [assert, depends, dependencies, fixtures], seen = new Set()) {
57
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
58
+
59
+ seen.add(target)
60
+
61
+ for (const value of Object.values(target))
62
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
63
+ else resetCalls(value, seen)
64
+ }
@@ -0,0 +1,58 @@
1
+ // noinspection ES6UnusedImports
2
+
3
+ import { Connector } from './connector.js'
4
+
5
+ declare namespace toa.core {
6
+
7
+ namespace atomicity {
8
+
9
+ /**
10
+ * What one group of replicas decides together, in one place. The decisions here are the
11
+ * ones processes cannot arrange by talking to each other: they need a single arbiter and a
12
+ * step indivisible from its point of view.
13
+ */
14
+ interface Atom extends Connector {
15
+ /**
16
+ * An exclusive claim on slots of `0..total`: while this replica holds one, no other
17
+ * replica of the group does. Answered from memory, so it costs nothing to ask.
18
+ *
19
+ * `null` while this replica owns nothing — after a restart, during a rollout, or while
20
+ * coordination is unreachable. Whoever asks must be able to stand down: acting on a
21
+ * claim that cannot be supported is a different guarantee, not a degraded one.
22
+ */
23
+ slots (total: number): number[] | null
24
+
25
+ /**
26
+ * Debt the group has run up under each key, in milliseconds. Every call adds its own
27
+ * deltas and reads back where the group stands, so a replica reports what it alone has
28
+ * spent and still decides on what all of them have.
29
+ *
30
+ * Rejects where there is nothing to arbitrate through.
31
+ */
32
+ meter (keys: string[], deltas: number[]): Promise<number[]>
33
+
34
+ /**
35
+ * Runs `routine` holding `keys`, and while it holds them no other replica of the group
36
+ * does. Waits for as long as it takes to acquire them.
37
+ *
38
+ * The lease is extended for as long as the routine runs. An extension that fails aborts
39
+ * the signal the routine is given, which is the only way it learns it no longer holds
40
+ * what it is working under.
41
+ *
42
+ * Rejects where there is nothing to arbitrate through.
43
+ */
44
+ lock<T> (keys: string | string[],
45
+ routine: (signal: AbortSignal, context: unknown) => Promise<T>): Promise<T>
46
+ }
47
+
48
+ interface Factory {
49
+ /** @param group what the replicas deciding together have in common */
50
+ atom (group: string, options?: object): Atom
51
+ }
52
+
53
+ }
54
+
55
+ }
56
+
57
+ export type Atom = toa.core.atomicity.Atom
58
+ export type Factory = toa.core.atomicity.Factory
@@ -1,26 +1,28 @@
1
- import * as _core from './index'
1
+ import * as _core from './index.js'
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
package/types/bridges.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { Request, Reply } from './request'
2
- import type { Context } from './context'
1
+ import type { Request, Reply } from './request.js'
2
+ import type { Context } from './context.js'
3
+ import type { Connector } from './connector.js'
3
4
 
4
5
  export interface Algorithm {
5
6
  mount: (context?: Context) => Promise<void>
@@ -27,4 +28,6 @@ export interface Factory {
27
28
  event?: (path: string, label: string) => Event
28
29
 
29
30
  receiver?: (path: string, label: string) => Receiver
31
+
32
+ rc?: (path: string, context: Context) => Promise<{ preflight?: Connector, settle?: Connector, dispose?: Connector } | undefined>
30
33
  }
@@ -1,9 +1,12 @@
1
- import { Connector } from './connector'
2
- import { Locator } from './locator'
3
- import { Request } from './request'
1
+ import { Connector } from './connector.js'
2
+ import { Locator } from './locator.js'
3
+ import { Request } from './request.js'
4
+ import { Operation } from './operations.js'
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
  }
@@ -1,11 +1,14 @@
1
- import * as _request from './request'
1
+ import * as _request from './request.js'
2
+ import { Locator } from './locator.js'
2
3
  import * as _reply from './reply'
3
- import * as _extensions from './extensions'
4
- import * as _connector from './connector'
4
+ import * as _extensions from './extensions.js'
5
+ import * as _connector from './connector.js'
5
6
 
6
7
  export interface Context extends _connector.Connector{
7
8
  aspects: _extensions.Aspect[]
8
9
 
10
+ locator: Locator
11
+
9
12
  /**
10
13
  * Calls local endpoint
11
14
  */
package/types/entity.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import * as _event from './event'
2
- import * as _storages from './storages'
1
+ import * as _event from './event.js'
2
+ import * as _storages from './storages.js'
3
3
 
4
4
  declare namespace toa.core {
5
5
 
@@ -8,9 +8,9 @@ declare namespace toa.core {
8
8
  interface Factory {
9
9
  init(id: string): Entity
10
10
 
11
- object(record: Object): Entity
11
+ object(record: Object, mutable?: boolean): Entity
12
12
 
13
- objects(recordset: Object[]): Entity[]
13
+ objects(recordset: Object[], init?: string[], mutable?: boolean): Entity[]
14
14
 
15
15
  changeset(query: _storages.Query): Changeset
16
16
  }
package/types/event.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as _state from './state'
1
+ import * as _state from './state.js'
2
2
 
3
3
  declare namespace toa.core {
4
4