@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/package.json +7 -9
  3. package/src/assignment.js +3 -2
  4. package/src/call.js +21 -1
  5. package/src/cascade.js +7 -5
  6. package/src/component.js +59 -15
  7. package/src/composition.js +1 -1
  8. package/src/connector.js +28 -17
  9. package/src/context.js +6 -0
  10. package/src/contract/contract.js +22 -0
  11. package/src/contract/reply.js +34 -9
  12. package/src/contract/request.js +19 -5
  13. package/src/contract/schemas/index.js +1 -0
  14. package/src/contract/schemas/query.yaml +14 -1
  15. package/src/contract/schemas/source.yaml +23 -0
  16. package/src/discovery.js +13 -7
  17. package/src/effect.js +19 -0
  18. package/src/entities/changeset.js +5 -8
  19. package/src/entities/entity.js +74 -24
  20. package/src/entities/factory.js +17 -6
  21. package/src/entities/newid.js +11 -0
  22. package/src/entities/set.js +13 -0
  23. package/src/event.js +19 -1
  24. package/src/exceptions.js +26 -19
  25. package/src/exposition.js +3 -2
  26. package/src/guard.js +17 -0
  27. package/src/index.js +8 -0
  28. package/src/observation.js +1 -9
  29. package/src/operation.js +38 -8
  30. package/src/outbox/index.js +6 -0
  31. package/src/outbox/outbox.js +301 -0
  32. package/src/query/options.js +3 -2
  33. package/src/query.js +32 -2
  34. package/src/receiver.js +73 -11
  35. package/src/remote.js +8 -7
  36. package/src/state.js +90 -47
  37. package/src/transition.js +11 -19
  38. package/src/transmission.js +12 -3
  39. package/src/unmanaged.js +11 -0
  40. package/test/component.test.js +4 -3
  41. package/test/connector.fixtures.js +8 -0
  42. package/test/connector.test.js +20 -0
  43. package/test/contract/conditions.test.js +5 -5
  44. package/test/contract/request.test.js +46 -7
  45. package/test/discovery.test.js +56 -0
  46. package/test/emission.fixtures.js +1 -2
  47. package/test/entities/entity.test.js +58 -48
  48. package/test/entities/factory.test.js +5 -3
  49. package/test/event.test.js +4 -4
  50. package/test/outbox.test.js +114 -0
  51. package/test/query.test.js +17 -0
  52. package/test/receiver.fixtures.js +1 -0
  53. package/test/state.fixtures.js +11 -8
  54. package/test/state.test.js +61 -13
  55. package/types/atomicity.d.ts +58 -0
  56. package/types/bindings.d.ts +7 -5
  57. package/types/bridges.ts +3 -0
  58. package/types/component.d.ts +4 -1
  59. package/types/context.d.ts +3 -0
  60. package/types/entity.d.ts +2 -2
  61. package/types/extensions.d.ts +7 -4
  62. package/types/index.ts +4 -1
  63. package/types/message.d.ts +1 -0
  64. package/types/operations.d.ts +6 -0
  65. package/types/outbox.d.ts +53 -0
  66. package/types/query.d.ts +2 -0
  67. package/types/remote.d.ts +18 -0
  68. package/types/request.d.ts +15 -0
  69. package/types/state.d.ts +9 -6
  70. package/types/storages.d.ts +34 -9
  71. package/src/contract/conditions.js +0 -21
@@ -7,76 +7,86 @@ beforeEach(() => {
7
7
  jest.clearAllMocks()
8
8
  })
9
9
 
10
- describe('new', () => {
11
- it('should throw on schema error', () => {
12
- const entity = new Entity(fixtures.schema)
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(() => entity.set(fixtures.failed())).toThrow()
15
+ expect(entity.get()).toEqual(state)
15
16
  })
16
17
 
17
- it('should provide state', () => {
18
- const entity = new Entity(fixtures.schema)
19
- const state = fixtures.state()
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()).toEqual(state)
22
+ expect(entity.get()).not.toBe(record)
23
+ expect(entity.event().origin).toBe(record)
24
24
  })
25
25
  })
26
26
 
27
- describe('argument', () => {
28
- it('should provide initial state if no argument passed', () => {
29
- const entity = new Entity(fixtures.schema)
30
- const defaults = fixtures.schema.defaults.mock.results[0].value
31
- const expected = {
32
- ...defaults,
33
- _version: 0
34
- }
35
-
36
- expect(entity.get()).toStrictEqual(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 set state', () => {
40
- const state = fixtures.state()
41
- const entity = new Entity(fixtures.schema, state)
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.get()).toEqual(state)
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
- it('should provide event', () => {
48
- const origin = fixtures.state()
49
- const entity = new Entity(fixtures.schema, origin)
50
- const state = entity.get()
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
- state.foo = 'new value'
53
- entity.set(state)
56
+ state.foo = 'revived'
57
+ entity.set(state)
54
58
 
55
- const event = entity.event()
59
+ expect(entity.get()._deleted).toBeNull()
60
+ expect(entity.deleted).toBe(false)
61
+ expect(entity.event().state._deleted).toBeNull()
62
+ })
56
63
 
57
- expect(event).toEqual(expect.objectContaining({
58
- state,
59
- origin,
60
- changeset: expect.objectContaining({
61
- foo: 'new value',
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
- it('should define `id` as readonly', async () => {
68
- const origin = fixtures.state()
69
- const entity = new Entity(fixtures.schema, origin)
70
- const state = entity.get()
70
+ state._deleted = timestamp
71
+ entity.set(state)
71
72
 
72
- expect(() => (state.id = 1)).toThrow('assign to read only property')
73
+ expect(entity.get()._deleted).toBe(timestamp)
74
+ expect(entity.deleted).toBe(true)
75
+ })
73
76
  })
74
77
 
75
- it('should seal id', async () => {
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
- expect(redefine).toThrow('redefine property')
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).toHaveBeenCalledWith(fixtures.schema, fixtures.entity)
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).toHaveBeenNthCalledWith(index + 1, fixtures.schema, 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
  })
@@ -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
+ })
@@ -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)
@@ -9,6 +9,7 @@ const definition = /** @type {toa.norm.component.Receiver} */ {
9
9
  }
10
10
 
11
11
  const local = /** @type {toa.core.Component} */ {
12
+ locator: { id: 'default.dummy' },
12
13
  invoke: jest.fn()
13
14
  }
14
15
 
@@ -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() }, changeset: {} }))
35
+ event: jest.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
+ 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.emitter = emitter
46
+ exports.outbox = outbox
44
47
  exports.query = query
45
48
  exports.entity = entity
46
49
  exports.initial = initial
@@ -8,7 +8,7 @@ let state
8
8
  beforeEach(() => {
9
9
  jest.clearAllMocks()
10
10
 
11
- state = new State(fixtures.storage, fixtures.factory, fixtures.emitter)
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).toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value)
19
+ expect(fixtures.factory.object)
20
+ .toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, true)
20
21
  })
21
22
 
22
- it('should provide objects', async () => {
23
- const set = await state.objects(fixtures.query)
23
+ it('should provide read-only object', async () => {
24
+ await state.object(fixtures.query, false)
24
25
 
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)
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(fixtures.initial.get.mock.results[0].value)
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 emit', async () => {
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.emitter.emit).toHaveBeenCalledWith(fixtures.entity.event.mock.results[0].value)
57
+ expect(fixtures.outbox.publish).not.toHaveBeenCalled()
40
58
  })
41
59
 
42
- it('should not emit if state has not been changed', async () => {
43
- await state.commit(fixtures.unchanged)
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
- expect(fixtures.emitter.emit).not.toHaveBeenCalled()
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
@@ -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
  }
@@ -1,9 +1,12 @@
1
1
  import { Connector } from './connector'
2
2
  import { Locator } from './locator'
3
3
  import { Request } from './request'
4
+ import { Operation } from './operations'
4
5
 
5
- export interface Component extends Connector{
6
+ export class Component extends Connector {
6
7
  locator: Locator
7
8
 
9
+ constructor (locator: Locator, operations: Record<string, Operation>)
10
+
8
11
  invoke<T = any> (endpoint: string, request: Request): Promise<T>
9
12
  }
@@ -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
  }
@@ -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