@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.
- package/CHANGELOG.md +168 -0
- package/package.json +8 -10
- package/src/assignment.js +5 -8
- package/src/call.js +35 -9
- package/src/cascade.js +9 -11
- package/src/component.js +61 -21
- package/src/composition.js +3 -7
- package/src/connector.js +29 -22
- package/src/context.js +8 -6
- package/src/contract/contract.js +18 -0
- package/src/contract/index.js +2 -7
- package/src/contract/reply.js +36 -15
- package/src/contract/request.js +20 -10
- package/src/contract/schemas/index.js +9 -5
- package/src/contract/schemas/query.yaml +14 -1
- package/src/contract/schemas/source.yaml +23 -0
- package/src/discovery.js +15 -13
- package/src/effect.js +15 -0
- package/src/emission.js +2 -6
- package/src/entities/changeset.js +7 -14
- package/src/entities/entity.js +75 -29
- package/src/entities/factory.js +21 -14
- package/src/entities/index.js +1 -5
- package/src/entities/newid.js +9 -0
- package/src/entities/set.js +13 -4
- package/src/event.js +21 -7
- package/src/exceptions.js +53 -39
- package/src/exposition.js +5 -8
- package/src/guard.js +13 -0
- package/src/index.js +28 -47
- package/src/locator.js +2 -6
- package/src/observation.js +3 -15
- package/src/operation.js +40 -14
- package/src/outbox/index.js +1 -0
- package/src/outbox/outbox.js +296 -0
- package/src/query/criteria.js +3 -7
- package/src/query/options.js +5 -8
- package/src/query.js +36 -7
- package/src/receiver.js +76 -18
- package/src/reflection.js +2 -6
- package/src/remote.js +9 -12
- package/src/state.js +89 -50
- package/src/transition.js +13 -25
- package/src/transmission.js +15 -10
- package/src/unmanaged.js +7 -0
- package/test/call.fixtures.js +8 -12
- package/test/call.test.js +21 -9
- package/test/component.fixtures.js +5 -8
- package/test/component.test.js +22 -10
- package/test/connector.fixtures.js +10 -8
- package/test/connector.test.js +59 -38
- package/test/context.fixtures.js +8 -12
- package/test/context.test.js +18 -7
- package/test/contract/conditions.test.js +11 -9
- package/test/contract/contract.fixtures.js +12 -16
- package/test/contract/request.test.js +84 -34
- package/test/discovery.test.js +64 -0
- package/test/emission.fixtures.js +6 -10
- package/test/emission.test.js +29 -12
- package/test/entities/entity.fixtures.js +7 -11
- package/test/entities/entity.test.js +70 -49
- package/test/entities/factory.fixtures.js +22 -10
- package/test/entities/factory.test.js +29 -15
- package/test/entities/set.fixtures.js +6 -8
- package/test/entities/set.test.js +6 -5
- package/test/event.fixtures.js +9 -14
- package/test/event.test.js +57 -31
- package/test/locator.test.js +25 -24
- package/test/outbox.test.js +121 -0
- package/test/query.fixtures.js +1 -5
- package/test/query.test.js +42 -14
- package/test/receiver.fixtures.js +9 -12
- package/test/receiver.test.js +45 -22
- package/test/reflection.test.js +7 -6
- package/test/state.fixtures.js +25 -30
- package/test/state.test.js +72 -19
- package/test/transmission.fixtures.js +4 -6
- package/test/transmission.test.js +33 -15
- package/types/atomicity.d.ts +58 -0
- package/types/bindings.d.ts +8 -6
- package/types/bridges.ts +5 -2
- package/types/component.d.ts +7 -4
- package/types/context.d.ts +6 -3
- package/types/entity.d.ts +4 -4
- package/types/event.d.ts +1 -1
- package/types/extensions.d.ts +35 -10
- package/types/index.ts +16 -13
- package/types/message.d.ts +1 -0
- package/types/operations.d.ts +6 -0
- package/types/outbox.d.ts +53 -0
- package/types/query.d.ts +2 -0
- package/types/receiver.d.ts +2 -2
- package/types/reflection.d.ts +1 -1
- package/types/remote.d.ts +18 -0
- package/types/request.d.ts +16 -1
- package/types/state.d.ts +11 -8
- package/types/storages.d.ts +36 -11
- package/src/contract/conditions.js +0 -21
package/test/receiver.test.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
import clone from 'clone-deep'
|
|
6
|
+
import { generate } from 'randomstring'
|
|
7
|
+
import { merge } from '@toa.io/generic'
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
import { Connector } from '../src/connector.js'
|
|
8
10
|
|
|
9
|
-
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
+
}
|
package/test/reflection.test.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { generate } from 'randomstring'
|
|
5
|
+
import { Reflection, Connector } from '../src/index.js'
|
|
5
6
|
|
|
6
7
|
it('should export', () => {
|
|
7
|
-
|
|
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
|
-
|
|
24
|
+
assert.ok(reflection instanceof Connector)
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
it('should reflect', async () => {
|
|
27
28
|
await reflection.connect()
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
assert.deepStrictEqual(reflection.value, value)
|
|
30
31
|
})
|
package/test/state.fixtures.js
CHANGED
|
@@ -1,47 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
import { mock } from 'node:test'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { generate } from 'randomstring'
|
|
4
4
|
|
|
5
|
-
const storage = {
|
|
5
|
+
export const storage = {
|
|
6
6
|
name: 'dummy',
|
|
7
|
-
get:
|
|
8
|
-
find:
|
|
9
|
-
add:
|
|
10
|
-
set:
|
|
11
|
-
store:
|
|
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:
|
|
16
|
-
objects:
|
|
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:
|
|
23
|
-
event:
|
|
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:
|
|
35
|
+
event: mock.fn(() => ({ state: { [generate()]: generate() } }))
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
package/test/state.test.js
CHANGED
|
@@ -1,46 +1,99 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
11
|
+
resetCalls()
|
|
10
12
|
|
|
11
|
-
state = new State(fixtures.storage, fixtures.factory, fixtures.
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
23
|
-
|
|
24
|
+
it('should provide read-only object', async () => {
|
|
25
|
+
await state.object(fixtures.query, false)
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
42
|
+
it('should publish the row', async () => {
|
|
37
43
|
await state.commit(fixtures.entity)
|
|
38
44
|
|
|
39
|
-
|
|
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
|
|
43
|
-
|
|
49
|
+
it('should not publish if the write did not happen', async () => {
|
|
50
|
+
fixtures.storage.store.mock.mockImplementationOnce(() => false)
|
|
44
51
|
|
|
45
|
-
|
|
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
|
-
|
|
1
|
+
import { mock } from 'node:test'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { generate } from 'randomstring'
|
|
4
4
|
|
|
5
5
|
const binding = (index) => ({
|
|
6
|
-
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
|
-
|
|
1
|
+
import { it, beforeEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import { Connector } from '../src/connector.js'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
43
|
+
assert.strictEqual(result, await fixtures.bindings[0].request.mock.calls[0].result)
|
|
36
44
|
|
|
37
45
|
result = await transmission.request({ pick: 1 })
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/types/bindings.d.ts
CHANGED
|
@@ -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
|
}
|
package/types/component.d.ts
CHANGED
|
@@ -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
|
|
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/context.d.ts
CHANGED
|
@@ -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