@toa.io/core 1.0.0-alpha.27 → 1.0.0-alpha.272
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +137 -0
- package/package.json +7 -9
- package/src/assignment.js +3 -2
- package/src/call.js +21 -1
- package/src/cascade.js +7 -5
- package/src/component.js +59 -15
- package/src/composition.js +1 -1
- package/src/connector.js +28 -17
- package/src/context.js +6 -0
- package/src/contract/contract.js +22 -0
- package/src/contract/reply.js +34 -9
- package/src/contract/request.js +19 -5
- package/src/contract/schemas/index.js +1 -0
- package/src/contract/schemas/query.yaml +14 -1
- package/src/contract/schemas/source.yaml +23 -0
- package/src/discovery.js +13 -7
- package/src/effect.js +19 -0
- package/src/entities/changeset.js +5 -8
- package/src/entities/entity.js +74 -24
- package/src/entities/factory.js +17 -6
- package/src/entities/newid.js +11 -0
- package/src/entities/set.js +13 -0
- package/src/event.js +19 -1
- package/src/exceptions.js +26 -19
- package/src/exposition.js +3 -2
- package/src/guard.js +17 -0
- package/src/index.js +8 -0
- package/src/observation.js +1 -9
- package/src/operation.js +38 -8
- package/src/outbox/index.js +6 -0
- package/src/outbox/outbox.js +301 -0
- package/src/query/options.js +3 -2
- package/src/query.js +32 -2
- package/src/receiver.js +73 -11
- package/src/remote.js +8 -7
- package/src/state.js +90 -47
- package/src/transition.js +11 -19
- package/src/transmission.js +12 -3
- package/src/unmanaged.js +11 -0
- package/test/component.test.js +4 -3
- package/test/connector.fixtures.js +8 -0
- package/test/connector.test.js +20 -0
- package/test/contract/conditions.test.js +5 -5
- package/test/contract/request.test.js +46 -7
- package/test/discovery.test.js +56 -0
- package/test/emission.fixtures.js +1 -2
- package/test/entities/entity.test.js +58 -48
- package/test/entities/factory.test.js +5 -3
- package/test/event.test.js +4 -4
- package/test/outbox.test.js +114 -0
- package/test/query.test.js +17 -0
- package/test/receiver.fixtures.js +1 -0
- package/test/state.fixtures.js +11 -8
- package/test/state.test.js +61 -13
- package/types/atomicity.d.ts +58 -0
- package/types/bindings.d.ts +7 -5
- package/types/bridges.ts +3 -0
- package/types/component.d.ts +4 -1
- package/types/context.d.ts +3 -0
- package/types/entity.d.ts +2 -2
- package/types/extensions.d.ts +7 -4
- package/types/index.ts +4 -1
- 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/remote.d.ts +18 -0
- package/types/request.d.ts +15 -0
- package/types/state.d.ts +9 -6
- package/types/storages.d.ts +34 -9
- package/src/contract/conditions.js +0 -21
|
@@ -7,76 +7,86 @@ beforeEach(() => {
|
|
|
7
7
|
jest.clearAllMocks()
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
describe('
|
|
11
|
-
it('should
|
|
12
|
-
const
|
|
10
|
+
describe('argument', () => {
|
|
11
|
+
it('should set state', () => {
|
|
12
|
+
const state = fixtures.state()
|
|
13
|
+
const entity = new Entity(fixtures.schema, state)
|
|
13
14
|
|
|
14
|
-
expect(
|
|
15
|
+
expect(entity.get()).toEqual(state)
|
|
15
16
|
})
|
|
16
17
|
|
|
17
|
-
it('should
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
entity.set(state)
|
|
18
|
+
it('should snapshot the record it may commit', () => {
|
|
19
|
+
const record = fixtures.state()
|
|
20
|
+
const entity = new Entity(fixtures.schema, record)
|
|
22
21
|
|
|
23
|
-
expect(entity.get()).
|
|
22
|
+
expect(entity.get()).not.toBe(record)
|
|
23
|
+
expect(entity.event().origin).toBe(record)
|
|
24
24
|
})
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
describe('
|
|
28
|
-
it('should
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
expect(entity.get()).toStrictEqual(expect.objectContaining(expected))
|
|
27
|
+
describe('read-only', () => {
|
|
28
|
+
it('should take the record as it came', () => {
|
|
29
|
+
const record = fixtures.state()
|
|
30
|
+
const entity = new Entity(fixtures.schema, record, undefined, false)
|
|
31
|
+
|
|
32
|
+
// no pre-image to diff against, hence no copy of it
|
|
33
|
+
expect(entity.get()).toBe(record)
|
|
37
34
|
})
|
|
38
35
|
|
|
39
|
-
it('should
|
|
40
|
-
const
|
|
41
|
-
const entity = new Entity(fixtures.schema,
|
|
36
|
+
it('should still report a tombstone', () => {
|
|
37
|
+
const record = { ...fixtures.state(), _deleted: Date.now() }
|
|
38
|
+
const entity = new Entity(fixtures.schema, record, undefined, false)
|
|
42
39
|
|
|
43
|
-
expect(entity.
|
|
40
|
+
expect(entity.deleted).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should refuse to be modified', () => {
|
|
44
|
+
const entity = new Entity(fixtures.schema, fixtures.state(), undefined, false)
|
|
45
|
+
|
|
46
|
+
expect(() => entity.set(entity.get())).toThrow('read-only')
|
|
44
47
|
})
|
|
45
48
|
})
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
describe('tombstone', () => {
|
|
51
|
+
it('should lift tombstone when transition leaves _deleted untouched', () => {
|
|
52
|
+
const origin = fixtures.state()
|
|
53
|
+
const entity = new Entity(fixtures.schema, origin)
|
|
54
|
+
const state = entity.get()
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
state.foo = 'revived'
|
|
57
|
+
entity.set(state)
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
expect(entity.get()._deleted).toBeNull()
|
|
60
|
+
expect(entity.deleted).toBe(false)
|
|
61
|
+
expect(entity.event().state._deleted).toBeNull()
|
|
62
|
+
})
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
state,
|
|
59
|
-
origin
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
_version: 1
|
|
63
|
-
})
|
|
64
|
-
}))
|
|
65
|
-
})
|
|
64
|
+
it('should keep tombstone written by transition', () => {
|
|
65
|
+
const origin = { ...fixtures.state(), _deleted: null }
|
|
66
|
+
const entity = new Entity(fixtures.schema, origin)
|
|
67
|
+
const state = entity.get()
|
|
68
|
+
const timestamp = Date.now()
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const entity = new Entity(fixtures.schema, origin)
|
|
70
|
-
const state = entity.get()
|
|
70
|
+
state._deleted = timestamp
|
|
71
|
+
entity.set(state)
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
expect(entity.get()._deleted).toBe(timestamp)
|
|
74
|
+
expect(entity.deleted).toBe(true)
|
|
75
|
+
})
|
|
73
76
|
})
|
|
74
77
|
|
|
75
|
-
it('should
|
|
78
|
+
it('should provide event', () => {
|
|
76
79
|
const origin = fixtures.state()
|
|
77
80
|
const entity = new Entity(fixtures.schema, origin)
|
|
78
81
|
const state = entity.get()
|
|
79
|
-
const redefine = () => Object.defineProperty(state, 'id', { writable: true })
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
state.foo = 'new value'
|
|
84
|
+
entity.set(state)
|
|
85
|
+
|
|
86
|
+
const event = entity.event()
|
|
87
|
+
|
|
88
|
+
expect(event).toEqual(expect.objectContaining({ state, origin }))
|
|
89
|
+
expect(event.state.foo).toBe('new value')
|
|
90
|
+
expect(event.state._version).toBe(1)
|
|
91
|
+
expect(event.origin.foo).not.toBe('new value')
|
|
82
92
|
})
|
|
@@ -23,14 +23,15 @@ it('should create initial', () => {
|
|
|
23
23
|
const initial = factory.init(id)
|
|
24
24
|
|
|
25
25
|
expect(initial).toBeInstanceOf(mock.Entity)
|
|
26
|
-
expect(initial.constructor).toHaveBeenCalledWith(fixtures.schema, id)
|
|
26
|
+
expect(initial.constructor).toHaveBeenCalledWith(fixtures.schema, id, expect.any(Function))
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('should create instance', () => {
|
|
30
30
|
const object = factory.object(fixtures.entity)
|
|
31
31
|
|
|
32
32
|
expect(object).toBeInstanceOf(mock.Entity)
|
|
33
|
-
expect(object.constructor)
|
|
33
|
+
expect(object.constructor)
|
|
34
|
+
.toHaveBeenCalledWith(fixtures.schema, fixtures.entity, expect.any(Function), true)
|
|
34
35
|
})
|
|
35
36
|
|
|
36
37
|
it('should create set', () => {
|
|
@@ -39,7 +40,8 @@ it('should create set', () => {
|
|
|
39
40
|
expect(objects).toBeInstanceOf(mock.EntitySet)
|
|
40
41
|
|
|
41
42
|
const instances = fixtures.set.map((entity, index) => {
|
|
42
|
-
expect(mock.Entity)
|
|
43
|
+
expect(mock.Entity)
|
|
44
|
+
.toHaveBeenNthCalledWith(index + 1, fixtures.schema, entity, expect.any(Function), true)
|
|
43
45
|
|
|
44
46
|
return mock.Entity.mock.instances[index]
|
|
45
47
|
})
|
package/test/event.test.js
CHANGED
|
@@ -41,7 +41,7 @@ describe('condition', () => {
|
|
|
41
41
|
await emit()
|
|
42
42
|
|
|
43
43
|
const payload = await fixtures.bridge.payload.mock.results[0].value
|
|
44
|
-
const message = { payload }
|
|
44
|
+
const message = { payload, telemetry: expect.any(String) }
|
|
45
45
|
expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
|
|
46
46
|
})
|
|
47
47
|
|
|
@@ -71,7 +71,7 @@ describe('condition', () => {
|
|
|
71
71
|
expect(fixtures.bridge.condition).not.toHaveBeenCalledWith()
|
|
72
72
|
|
|
73
73
|
const payload = await fixtures.bridge.payload.mock.results[0].value
|
|
74
|
-
const message = { payload }
|
|
74
|
+
const message = { payload, telemetry: expect.any(String) }
|
|
75
75
|
|
|
76
76
|
expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
|
|
77
77
|
})
|
|
@@ -84,7 +84,7 @@ describe('payload', () => {
|
|
|
84
84
|
await emit()
|
|
85
85
|
|
|
86
86
|
const payload = await fixtures.bridge.payload.mock.results[0].value
|
|
87
|
-
const message = { payload }
|
|
87
|
+
const message = { payload, telemetry: expect.any(String) }
|
|
88
88
|
|
|
89
89
|
expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
|
|
90
90
|
})
|
|
@@ -110,7 +110,7 @@ describe('payload', () => {
|
|
|
110
110
|
await emit()
|
|
111
111
|
|
|
112
112
|
const payload = fixtures.event.state
|
|
113
|
-
const message = { payload }
|
|
113
|
+
const message = { payload, telemetry: expect.any(String) }
|
|
114
114
|
|
|
115
115
|
expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
|
|
116
116
|
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Outbox } = require('../src/outbox')
|
|
4
|
+
|
|
5
|
+
let emission, storage, atom, outbox
|
|
6
|
+
|
|
7
|
+
const BATCH = 4
|
|
8
|
+
|
|
9
|
+
/** rows as the storage hands them back, ids ascending like the uuid v7 they are */
|
|
10
|
+
const page = (from, count) =>
|
|
11
|
+
Array.from({ length: count }, (_, i) => ({
|
|
12
|
+
id: String(from + i).padStart(4, '0'),
|
|
13
|
+
event: { state: {} }
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks()
|
|
18
|
+
jest.useFakeTimers()
|
|
19
|
+
|
|
20
|
+
emission = { emit: jest.fn(async () => undefined), link: jest.fn() }
|
|
21
|
+
|
|
22
|
+
storage = {
|
|
23
|
+
link: jest.fn(),
|
|
24
|
+
outbox: {
|
|
25
|
+
pending: jest.fn(async () => []),
|
|
26
|
+
settle: jest.fn(async () => undefined)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
atom = { slots: jest.fn(() => [0]), link: jest.fn() }
|
|
31
|
+
outbox = new Outbox(emission, storage, atom, { interval: 1000, batch: BATCH })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.useRealTimers()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/** one cycle, and everything it awaited */
|
|
39
|
+
const cycle = async () => {
|
|
40
|
+
jest.advanceTimersByTime(1000)
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < 20; i++) await Promise.resolve()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('should read nothing more when the first page is short', async () => {
|
|
46
|
+
storage.outbox.pending.mockResolvedValueOnce(page(0, 3))
|
|
47
|
+
|
|
48
|
+
await outbox.open()
|
|
49
|
+
await cycle()
|
|
50
|
+
|
|
51
|
+
expect(storage.outbox.pending).toHaveBeenCalledTimes(1)
|
|
52
|
+
expect(emission.emit).toHaveBeenCalledTimes(3)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should keep reading while a page comes back full', async () => {
|
|
56
|
+
storage.outbox.pending
|
|
57
|
+
.mockResolvedValueOnce(page(0, BATCH))
|
|
58
|
+
.mockResolvedValueOnce(page(BATCH, BATCH))
|
|
59
|
+
.mockResolvedValueOnce(page(2 * BATCH, 5))
|
|
60
|
+
|
|
61
|
+
await outbox.open()
|
|
62
|
+
await cycle()
|
|
63
|
+
|
|
64
|
+
expect(storage.outbox.pending).toHaveBeenCalledTimes(3)
|
|
65
|
+
expect(emission.emit).toHaveBeenCalledTimes(2 * BATCH + 5)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should continue each page from the id the one before ended on', async () => {
|
|
69
|
+
storage.outbox.pending
|
|
70
|
+
.mockResolvedValueOnce(page(0, BATCH))
|
|
71
|
+
.mockResolvedValueOnce(page(BATCH, 1))
|
|
72
|
+
|
|
73
|
+
await outbox.open()
|
|
74
|
+
await cycle()
|
|
75
|
+
|
|
76
|
+
const [, second] = storage.outbox.pending.mock.calls
|
|
77
|
+
|
|
78
|
+
expect(storage.outbox.pending.mock.calls[0][3]).toBeUndefined()
|
|
79
|
+
expect(second[3]).toStrictEqual(String(BATCH - 1).padStart(4, '0'))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should mark what it published, once, after the last page', async () => {
|
|
83
|
+
storage.outbox.pending
|
|
84
|
+
.mockResolvedValueOnce(page(0, BATCH))
|
|
85
|
+
.mockResolvedValueOnce(page(BATCH, 2))
|
|
86
|
+
|
|
87
|
+
await outbox.open()
|
|
88
|
+
await cycle()
|
|
89
|
+
|
|
90
|
+
expect(storage.outbox.settle).toHaveBeenCalledTimes(1)
|
|
91
|
+
expect(storage.outbox.settle.mock.calls[0][0]).toHaveLength(BATCH + 2)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should not publish a row it has published and not yet marked', async () => {
|
|
95
|
+
storage.outbox.settle.mockRejectedValueOnce(new Error('mongo is out'))
|
|
96
|
+
storage.outbox.pending
|
|
97
|
+
.mockResolvedValueOnce(page(0, 2))
|
|
98
|
+
.mockResolvedValueOnce(page(0, 2))
|
|
99
|
+
|
|
100
|
+
await outbox.open()
|
|
101
|
+
await cycle()
|
|
102
|
+
await cycle()
|
|
103
|
+
|
|
104
|
+
expect(emission.emit).toHaveBeenCalledTimes(2)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should read nothing while it owns no slots', async () => {
|
|
108
|
+
atom.slots.mockReturnValue(null)
|
|
109
|
+
|
|
110
|
+
await outbox.open()
|
|
111
|
+
await cycle()
|
|
112
|
+
|
|
113
|
+
expect(storage.outbox.pending).not.toHaveBeenCalled()
|
|
114
|
+
})
|
package/test/query.test.js
CHANGED
|
@@ -22,6 +22,23 @@ describe('criteria', () => {
|
|
|
22
22
|
expect(query.criteria).toEqual(fixtures.samples.simple.parsed.criteria)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
it('should keep a parsed criteria', () => {
|
|
26
|
+
const instance = new Query(fixtures.samples.simple.properties)
|
|
27
|
+
|
|
28
|
+
const first = instance.parse(fixtures.samples.simple.query).criteria
|
|
29
|
+
const second = instance.parse(fixtures.samples.simple.query).criteria
|
|
30
|
+
|
|
31
|
+
expect(second).toBe(first)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should not keep an invalid criteria', () => {
|
|
35
|
+
const instance = new Query(fixtures.samples.simple.properties)
|
|
36
|
+
const query = { criteria: 'nonexistent==1' }
|
|
37
|
+
|
|
38
|
+
expect(() => instance.parse(query)).toThrow()
|
|
39
|
+
expect(() => instance.parse(query)).toThrow()
|
|
40
|
+
})
|
|
41
|
+
|
|
25
42
|
it('should parse criteria with type coercion', () => {
|
|
26
43
|
const instance = new Query(fixtures.samples.extended.properties)
|
|
27
44
|
const query = instance.parse(fixtures.samples.extended.query)
|
package/test/state.fixtures.js
CHANGED
|
@@ -8,7 +8,10 @@ const storage = {
|
|
|
8
8
|
find: jest.fn(() => ([{ id: generate() }])),
|
|
9
9
|
add: jest.fn(() => true),
|
|
10
10
|
set: jest.fn(() => true),
|
|
11
|
-
store: jest.fn(() => true)
|
|
11
|
+
store: jest.fn(() => true),
|
|
12
|
+
massStore: jest.fn(() => true),
|
|
13
|
+
upsert: jest.fn(() => ({ id: generate() })),
|
|
14
|
+
ensure: jest.fn((query, properties, state) => state)
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const factory = {
|
|
@@ -20,9 +23,7 @@ const query = generate()
|
|
|
20
23
|
|
|
21
24
|
const entity = {
|
|
22
25
|
get: jest.fn(() => ({ [generate()]: generate() })),
|
|
23
|
-
event: jest.fn(() => ({
|
|
24
|
-
state: { [generate()]: generate() }, changeset: { [generate()]: generate() }
|
|
25
|
-
}))
|
|
26
|
+
event: jest.fn(() => ({ state: { [generate()]: generate() } }))
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const initial = {
|
|
@@ -31,16 +32,18 @@ const initial = {
|
|
|
31
32
|
|
|
32
33
|
const unchanged = {
|
|
33
34
|
...entity,
|
|
34
|
-
event: jest.fn(() => ({ state: { [generate()]: generate() }
|
|
35
|
+
event: jest.fn(() => ({ state: { [generate()]: generate() } }))
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// a legacy outbox: no storage capability, so `publish` emits inline
|
|
39
|
+
const outbox = {
|
|
40
|
+
row: jest.fn((event) => ({ id: generate(), lane: 0, published: false, pending: 0, event })),
|
|
41
|
+
publish: jest.fn()
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
exports.storage = storage
|
|
42
45
|
exports.factory = factory
|
|
43
|
-
exports.
|
|
46
|
+
exports.outbox = outbox
|
|
44
47
|
exports.query = query
|
|
45
48
|
exports.entity = entity
|
|
46
49
|
exports.initial = initial
|
package/test/state.test.js
CHANGED
|
@@ -8,7 +8,7 @@ let state
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
jest.clearAllMocks()
|
|
10
10
|
|
|
11
|
-
state = new State(fixtures.storage, fixtures.factory, fixtures.
|
|
11
|
+
state = new State(fixtures.storage, fixtures.factory, fixtures.outbox)
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
it('should provide object', async () => {
|
|
@@ -16,31 +16,79 @@ it('should provide object', async () => {
|
|
|
16
16
|
|
|
17
17
|
expect(fixtures.storage.get).toHaveBeenCalledWith(fixtures.query)
|
|
18
18
|
expect(entity).toStrictEqual(fixtures.factory.object.mock.results[0].value)
|
|
19
|
-
expect(fixtures.factory.object)
|
|
19
|
+
expect(fixtures.factory.object)
|
|
20
|
+
.toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, true)
|
|
20
21
|
})
|
|
21
22
|
|
|
22
|
-
it('should provide
|
|
23
|
-
|
|
23
|
+
it('should provide read-only object', async () => {
|
|
24
|
+
await state.object(fixtures.query, false)
|
|
24
25
|
|
|
25
|
-
expect(fixtures.
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
expect(fixtures.factory.object)
|
|
27
|
+
.toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should provide read-only objects', async () => {
|
|
31
|
+
await state.objects(fixtures.query, false)
|
|
32
|
+
|
|
33
|
+
expect(fixtures.factory.objects)
|
|
34
|
+
.toHaveBeenCalledWith(fixtures.storage.find.mock.results[0].value, undefined, false)
|
|
28
35
|
})
|
|
29
36
|
|
|
30
37
|
it('should store entity', async () => {
|
|
31
38
|
await state.commit(fixtures.initial)
|
|
32
39
|
|
|
33
|
-
expect(fixtures.storage.store).toHaveBeenCalledWith(
|
|
40
|
+
expect(fixtures.storage.store).toHaveBeenCalledWith(
|
|
41
|
+
fixtures.initial.get.mock.results[0].value,
|
|
42
|
+
fixtures.outbox.row.mock.results[0].value)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should publish the row', async () => {
|
|
46
|
+
await state.commit(fixtures.entity)
|
|
47
|
+
|
|
48
|
+
expect(fixtures.outbox.row).toHaveBeenCalledWith(fixtures.entity.event.mock.results[0].value)
|
|
49
|
+
expect(fixtures.outbox.publish).toHaveBeenCalledWith(fixtures.outbox.row.mock.results[0].value)
|
|
34
50
|
})
|
|
35
51
|
|
|
36
|
-
it('should
|
|
52
|
+
it('should not publish if the write did not happen', async () => {
|
|
53
|
+
fixtures.storage.store.mockImplementationOnce(() => false)
|
|
54
|
+
|
|
37
55
|
await state.commit(fixtures.entity)
|
|
38
56
|
|
|
39
|
-
expect(fixtures.
|
|
57
|
+
expect(fixtures.outbox.publish).not.toHaveBeenCalled()
|
|
40
58
|
})
|
|
41
59
|
|
|
42
|
-
it('should
|
|
43
|
-
|
|
60
|
+
it('should build the row before the write', async () => {
|
|
61
|
+
// the storage commits the row in the same transaction, so it must already exist
|
|
62
|
+
fixtures.storage.store.mockImplementationOnce((_, row) => {
|
|
63
|
+
expect(row).toBeDefined()
|
|
64
|
+
|
|
65
|
+
return true
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect.assertions(1)
|
|
69
|
+
|
|
70
|
+
await state.commit(fixtures.entity)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('assignment', () => {
|
|
74
|
+
const changeset = { query: 'q', export: () => ({ foo: 1 }) }
|
|
75
|
+
|
|
76
|
+
it('should pass the row to upsert and publish it', async () => {
|
|
77
|
+
const result = await state.apply(changeset, { foo: 1 })
|
|
78
|
+
|
|
79
|
+
expect(fixtures.storage.upsert).toHaveBeenCalledWith(
|
|
80
|
+
changeset.query, { foo: 1 }, fixtures.outbox.row.mock.results[0].value)
|
|
81
|
+
|
|
82
|
+
expect(fixtures.outbox.publish).toHaveBeenCalledWith(fixtures.outbox.row.mock.results[0].value)
|
|
83
|
+
expect(result).toStrictEqual(fixtures.storage.upsert.mock.results[0].value)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should fill the state a storage without the outbox left alone', async () => {
|
|
87
|
+
await state.apply(changeset, { foo: 1 })
|
|
88
|
+
|
|
89
|
+
const row = fixtures.outbox.row.mock.results[0].value
|
|
44
90
|
|
|
45
|
-
|
|
91
|
+
expect(row.event.state).toStrictEqual(fixtures.storage.upsert.mock.results[0].value)
|
|
92
|
+
expect(row.event.input).toStrictEqual({ foo: 1 })
|
|
93
|
+
})
|
|
46
94
|
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// noinspection ES6UnusedImports
|
|
2
|
+
|
|
3
|
+
import { Connector } from './connector'
|
|
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
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
|
package/types/bridges.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Request, Reply } from './request'
|
|
2
2
|
import type { Context } from './context'
|
|
3
|
+
import type { Connector } from './connector'
|
|
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
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
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import * as _request from './request'
|
|
2
|
+
import { Locator } from './locator'
|
|
2
3
|
import * as _reply from './reply'
|
|
3
4
|
import * as _extensions from './extensions'
|
|
4
5
|
import * as _connector from './connector'
|
|
@@ -6,6 +7,8 @@ import * as _connector from './connector'
|
|
|
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
|
@@ -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/extensions.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ import * as _component from './component'
|
|
|
3
3
|
import * as _context from './context'
|
|
4
4
|
import * as _storages from './storages'
|
|
5
5
|
import * as _bindings from './bindings'
|
|
6
|
+
import { Manifest } from '@toa.io/norm'
|
|
6
7
|
|
|
7
|
-
export interface Factory{
|
|
8
|
-
tenant? (locator: _core.Locator, manifest: object): _core.Connector
|
|
8
|
+
export interface Factory {
|
|
9
|
+
tenant? (locator: _core.Locator, manifest: object, component: Manifest): _core.Connector
|
|
9
10
|
|
|
10
11
|
aspect? (locator: _core.Locator, manifest: object | null): Aspect | Aspect[]
|
|
11
12
|
|
|
@@ -15,14 +16,16 @@ export interface Factory{
|
|
|
15
16
|
|
|
16
17
|
context? (context: _context.Context): _context.Context
|
|
17
18
|
|
|
19
|
+
manage? (composition: _core.Connector): _core.Connector
|
|
20
|
+
|
|
18
21
|
storage? (storage: _storages.Storage): _storages.Storage
|
|
19
22
|
|
|
20
|
-
emitter? (emitter: _bindings.Emitter, label: string): _bindings.Emitter
|
|
23
|
+
emitter? (emitter: _bindings.Emitter, label: string, locator: _core.Locator): _bindings.Emitter
|
|
21
24
|
|
|
22
25
|
receiver? (receiver: _core.Receiver, locator: _core.Locator): _core.Receiver
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export interface Aspect extends _core.Connector{
|
|
28
|
+
export interface Aspect extends _core.Connector {
|
|
26
29
|
name: string
|
|
27
30
|
|
|
28
31
|
invoke (...args: any[]): any
|