@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,11 +1,9 @@
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 set = [
6
- { get: jest.fn(() => ({ [generate()]: generate() })) },
7
- { get: jest.fn(() => ({ [generate()]: generate() })) },
8
- { get: jest.fn(() => ({ [generate()]: generate() })) }
5
+ export const set = [
6
+ { get: mock.fn(() => ({ [generate()]: generate() })) },
7
+ { get: mock.fn(() => ({ [generate()]: generate() })) },
8
+ { get: mock.fn(() => ({ [generate()]: generate() })) }
9
9
  ]
10
-
11
- exports.set = set
@@ -1,12 +1,13 @@
1
- 'use strict'
1
+ import { it } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { EntitySet } = require('../../src/entities/set')
4
- const fixtures = require('./set.fixtures')
4
+ import { EntitySet } from '../../src/entities/set.js'
5
+ import * as fixtures from './set.fixtures.js'
5
6
 
6
7
  it('should provide state', () => {
7
8
  const set = new EntitySet(fixtures.set)
8
9
  const state = set.get()
9
- const expected = fixtures.set.map((entity) => entity.get.mock.results[0].value)
10
+ const expected = fixtures.set.map((entity) => entity.get.mock.calls[0].result)
10
11
 
11
- expect(state).toStrictEqual(expected)
12
+ assert.deepStrictEqual(state, expected)
12
13
  })
@@ -1,29 +1,24 @@
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 definition = {
5
+ export const definition = {
6
6
  conditioned: true,
7
7
  subjective: true
8
8
  }
9
9
 
10
10
  // noinspection JSCheckFunctionSignatures
11
- const bridge = {
12
- condition: jest.fn(async (origin) => !origin.falsy),
13
- payload: jest.fn(async () => ({ [generate()]: generate() }))
11
+ export const bridge = {
12
+ condition: mock.fn(async (origin) => !origin.falsy),
13
+ payload: mock.fn(async () => ({ [generate()]: generate() }))
14
14
  }
15
15
 
16
- const binding = {
17
- emit: jest.fn()
16
+ export const binding = {
17
+ emit: mock.fn()
18
18
  }
19
19
 
20
- const event = {
20
+ export const event = {
21
21
  origin: { [generate()]: generate() },
22
22
  state: { [generate()]: generate() },
23
23
  changeset: { [generate()]: generate() }
24
24
  }
25
-
26
- exports.bridge = bridge
27
- exports.binding = binding
28
- exports.definition = definition
29
- exports.event = event
@@ -1,32 +1,41 @@
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')
5
+ import clone from 'clone-deep'
4
6
 
5
- jest.mock('../src/connector')
6
-
7
- const { Connector } = require('../src/connector')
8
- const { Event } = require('../src/event')
9
- const fixtures = require('./event.fixtures')
7
+ import { Connector } from '../src/connector.js'
8
+ import { Event } from '../src/event.js'
9
+ import * as fixtures from './event.fixtures.js'
10
10
 
11
11
  let event, emit
12
12
 
13
+ // the fixtures are not connectors, so the dependency is recorded rather than linked
14
+ const depends = mock.method(Connector.prototype, 'depends', () => undefined)
15
+
16
+ const dependencies = (instance) => depends.mock.calls
17
+ .filter((call) => call.this === instance)
18
+ .map((call) => call.arguments[0])
19
+
13
20
  beforeEach(() => {
14
- jest.clearAllMocks()
21
+ resetCalls()
22
+ depends.mock.resetCalls()
15
23
 
16
24
  event = new Event(fixtures.definition, fixtures.binding, fixtures.bridge)
17
25
  emit = () => event.emit(fixtures.event)
18
26
  })
19
27
 
20
28
  it('should depend on binding', () => {
21
- expect(event).toBeInstanceOf(Connector)
22
- expect(Connector.mock.instances[0].depends).toHaveBeenCalledWith(fixtures.binding)
29
+ assert.ok(event instanceof Connector)
30
+ assert.ok(dependencies(event).some((one) => isDeepStrictEqual(one, fixtures.binding)))
23
31
  })
24
32
 
25
33
  it('should depend on bridge if provided', () => {
26
- expect(Connector.mock.instances[0].depends).toHaveBeenCalledWith(fixtures.bridge)
34
+ assert.ok(dependencies(event).some((one) => isDeepStrictEqual(one, fixtures.bridge)))
35
+
36
+ const bridgeless = new Event(fixtures.definition, fixtures.binding)
27
37
 
28
- event = new Event(fixtures.definition, fixtures.binding)
29
- expect(Connector.mock.instances[1].depends).not.toHaveBeenCalledWith(fixtures.bridge)
38
+ assert.ok(!dependencies(bridgeless).some((one) => isDeepStrictEqual(one, fixtures.bridge)))
30
39
  })
31
40
 
32
41
  describe('condition', () => {
@@ -34,15 +43,14 @@ describe('condition', () => {
34
43
  it('should call condition', async () => {
35
44
  await emit()
36
45
 
37
- expect(fixtures.bridge.condition).toHaveBeenCalledWith(fixtures.event)
46
+ assert.ok(fixtures.bridge.condition.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], fixtures.event)))
38
47
  })
39
48
 
40
49
  it('should emit if condition returns true', async () => {
41
50
  await emit()
42
51
 
43
- const payload = await fixtures.bridge.payload.mock.results[0].value
44
- const message = { payload }
45
- expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
52
+ const payload = await fixtures.bridge.payload.mock.calls[0].result
53
+ assertEmitted(payload)
46
54
  })
47
55
 
48
56
  it('should not emit if condition returns false', async () => {
@@ -52,7 +60,7 @@ describe('condition', () => {
52
60
 
53
61
  await event.emit(origin, fixtures.event.changeset, fixtures.event.state)
54
62
 
55
- expect(fixtures.binding.emit).not.toHaveBeenCalled()
63
+ assert.strictEqual(fixtures.binding.emit.mock.callCount(), 0)
56
64
  })
57
65
  })
58
66
 
@@ -68,12 +76,10 @@ describe('condition', () => {
68
76
  it('should not call condition', async () => {
69
77
  await event.emit(fixtures.event.origin, fixtures.event.changeset, fixtures.event.state)
70
78
 
71
- expect(fixtures.bridge.condition).not.toHaveBeenCalledWith()
79
+ assert.ok(!(fixtures.bridge.condition.mock.calls.some((call) => call.arguments.length === 0)))
72
80
 
73
- const payload = await fixtures.bridge.payload.mock.results[0].value
74
- const message = { payload }
75
-
76
- expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
81
+ const payload = await fixtures.bridge.payload.mock.calls[0].result
82
+ assertEmitted(payload)
77
83
  })
78
84
  })
79
85
  })
@@ -83,10 +89,8 @@ describe('payload', () => {
83
89
  it('should emit payload', async () => {
84
90
  await emit()
85
91
 
86
- const payload = await fixtures.bridge.payload.mock.results[0].value
87
- const message = { payload }
88
-
89
- expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
92
+ const payload = await fixtures.bridge.payload.mock.calls[0].result
93
+ assertEmitted(payload)
90
94
  })
91
95
  })
92
96
 
@@ -103,16 +107,38 @@ describe('payload', () => {
103
107
  it('should not call payload', async () => {
104
108
  await emit()
105
109
 
106
- expect(fixtures.bridge.payload).not.toHaveBeenCalled()
110
+ assert.strictEqual(fixtures.bridge.payload.mock.callCount(), 0)
107
111
  })
108
112
 
109
113
  it('should return state as payload', async () => {
110
114
  await emit()
111
115
 
112
116
  const payload = fixtures.event.state
113
- const message = { payload }
114
-
115
- expect(fixtures.binding.emit).toHaveBeenCalledWith(message)
117
+ assertEmitted(payload)
116
118
  })
117
119
  })
118
120
  })
121
+
122
+ function resetCalls (target = [assert, clone, fixtures, depends, dependencies], seen = new Set()) {
123
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
124
+
125
+ seen.add(target)
126
+
127
+ for (const value of Object.values(target))
128
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
129
+ else resetCalls(value, seen)
130
+ }
131
+
132
+ /** The message carries the payload and a telemetry token generated per emission. */
133
+ function assertEmitted (payload) {
134
+ const emitted = fixtures.binding.emit.mock.calls
135
+ .map((call) => call.arguments[0])
136
+ .filter((message) => isDeepStrictEqual(message.payload, payload))
137
+
138
+ assert.notStrictEqual(emitted.length, 0)
139
+
140
+ for (const message of emitted) {
141
+ assert.strictEqual(typeof message.telemetry, 'string')
142
+ assert.deepStrictEqual(Object.keys(message).sort(), ['payload', 'telemetry'])
143
+ }
144
+ }
@@ -1,7 +1,8 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { generate } = require('randomstring')
4
- const { Locator } = require('../src/locator')
4
+ import { generate } from 'randomstring'
5
+ import { Locator } from '../src/locator.js'
5
6
 
6
7
  /** @type {string} */
7
8
  let name
@@ -20,37 +21,37 @@ beforeEach(() => {
20
21
  })
21
22
 
22
23
  it('should expose name and namespace', () => {
23
- expect(locator.name).toStrictEqual(name)
24
- expect(locator.namespace).toStrictEqual(namespace)
24
+ assert.deepStrictEqual(locator.name, name)
25
+ assert.deepStrictEqual(locator.namespace, namespace)
25
26
  })
26
27
 
27
28
  it('should expose id, label', () => {
28
29
  const id = locator.namespace + '.' + locator.name
29
30
  const label = locator.namespace.toLowerCase() + '-' + locator.name.toLowerCase()
30
31
 
31
- expect(locator.id).toStrictEqual(id)
32
- expect(locator.label).toStrictEqual(label)
32
+ assert.deepStrictEqual(locator.id, id)
33
+ assert.deepStrictEqual(locator.label, label)
33
34
  })
34
35
 
35
36
  it('should expose uppercase', () => {
36
- expect(locator.uppercase).toStrictEqual((locator.namespace + '_' + locator.name).toUpperCase())
37
+ assert.deepStrictEqual(locator.uppercase, (locator.namespace + '_' + locator.name).toUpperCase())
37
38
  })
38
39
 
39
40
  it('should throw if name is undefined', () => {
40
- expect(() => new Locator(undefined, namespace)).toThrow(TypeError)
41
+ assert.throws(() => new Locator(undefined, namespace), TypeError)
41
42
 
42
43
  // noinspection JSCheckFunctionSignatures
43
- expect(() => new Locator()).toThrow(TypeError)
44
+ assert.throws(() => new Locator(), TypeError)
44
45
  })
45
46
 
46
47
  it('should expose host', () => {
47
- expect(locator.hostname()).toStrictEqual((namespace + '-' + name).toLowerCase())
48
+ assert.deepStrictEqual(locator.hostname(), (namespace + '-' + name).toLowerCase())
48
49
  })
49
50
 
50
51
  it('should expose host with given prefix', () => {
51
52
  const prefix = generate()
52
53
 
53
- expect(locator.hostname(prefix)).toStrictEqual((prefix + '-' + namespace + '-' + name).toLowerCase())
54
+ assert.deepStrictEqual(locator.hostname(prefix), (prefix + '-' + namespace + '-' + name).toLowerCase())
54
55
  })
55
56
 
56
57
  describe('global', () => {
@@ -61,28 +62,28 @@ describe('global', () => {
61
62
  it('should not throw', () => undefined)
62
63
 
63
64
  it('should expose id', () => {
64
- expect(locator.id).toStrictEqual(name)
65
+ assert.deepStrictEqual(locator.id, name)
65
66
  })
66
67
 
67
68
  it('should expose label', () => {
68
- expect(locator.label).toStrictEqual(name.toLowerCase())
69
+ assert.deepStrictEqual(locator.label, name.toLowerCase())
69
70
  })
70
71
 
71
72
  it('should expose uppercase', () => {
72
- expect(locator.uppercase).toStrictEqual(name.toUpperCase())
73
+ assert.deepStrictEqual(locator.uppercase, name.toUpperCase())
73
74
  })
74
75
 
75
76
  it('should expose hostname', () => {
76
77
  const type = generate()
77
78
 
78
- expect(locator.hostname(type)).toStrictEqual((type + '-' + name).toLowerCase())
79
- expect(locator.hostname()).toStrictEqual(name.toLowerCase())
79
+ assert.deepStrictEqual(locator.hostname(type), (type + '-' + name).toLowerCase())
80
+ assert.deepStrictEqual(locator.hostname(), name.toLowerCase())
80
81
  })
81
82
  })
82
83
 
83
84
  describe('parse', () => {
84
85
  it('should be', async () => {
85
- expect(Locator.parse).toBeInstanceOf(Function)
86
+ assert.ok(Locator.parse instanceof Function)
86
87
  })
87
88
 
88
89
  const namespace = generate()
@@ -92,22 +93,22 @@ describe('parse', () => {
92
93
  it('should parse id', async () => {
93
94
  const locator = Locator.parse(id)
94
95
 
95
- expect(locator.namespace).toStrictEqual(namespace)
96
- expect(locator.name).toStrictEqual(name)
96
+ assert.deepStrictEqual(locator.namespace, namespace)
97
+ assert.deepStrictEqual(locator.name, name)
97
98
  })
98
99
 
99
100
  it('should parse endpoint', async () => {
100
101
  const endpoint = `${id}.${generate()}`
101
102
  const locator = Locator.parse(endpoint)
102
103
 
103
- expect(locator.namespace).toStrictEqual(namespace)
104
- expect(locator.name).toStrictEqual(name)
104
+ assert.deepStrictEqual(locator.namespace, namespace)
105
+ assert.deepStrictEqual(locator.name, name)
105
106
  })
106
107
 
107
108
  it('should parse token as global', async () => {
108
109
  const locator = Locator.parse(name)
109
110
 
110
- expect(locator.namespace).toBeUndefined()
111
- expect(locator.name).toStrictEqual(name)
111
+ assert.strictEqual(locator.namespace, undefined)
112
+ assert.deepStrictEqual(locator.name, name)
112
113
  })
113
114
  })
@@ -0,0 +1,121 @@
1
+ import { it, beforeEach, afterEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { Outbox } from '../src/outbox/index.js'
5
+
6
+ let emission, storage, atom, outbox
7
+
8
+ const BATCH = 4
9
+
10
+ /** rows as the storage hands them back, ids ascending like the uuid v7 they are */
11
+ const page = (from, count) =>
12
+ Array.from({ length: count }, (_, i) => ({
13
+ id: String(from + i).padStart(4, '0'),
14
+ event: { state: {} }
15
+ }))
16
+
17
+ beforeEach(() => {
18
+ resetCalls()
19
+ mock.timers.enable()
20
+
21
+ emission = { emit: mock.fn(async () => undefined), link: mock.fn() }
22
+
23
+ storage = {
24
+ link: mock.fn(),
25
+ outbox: {
26
+ pending: mock.fn(async () => []),
27
+ settle: mock.fn(async () => undefined)
28
+ }
29
+ }
30
+
31
+ atom = { slots: mock.fn(() => [0]), link: mock.fn() }
32
+ outbox = new Outbox(emission, storage, atom, { interval: 1000, batch: BATCH })
33
+ })
34
+
35
+ afterEach(() => {
36
+ mock.timers.reset()
37
+ })
38
+
39
+ /** one cycle, and everything it awaited */
40
+ const cycle = async () => {
41
+ mock.timers.tick(1000)
42
+
43
+ for (let i = 0; i < 20; i++) await Promise.resolve()
44
+ }
45
+
46
+ it('should read nothing more when the first page is short', async () => {
47
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(0, 3))
48
+
49
+ await outbox.open()
50
+ await cycle()
51
+
52
+ assert.strictEqual(storage.outbox.pending.mock.callCount(), 1)
53
+ assert.strictEqual(emission.emit.mock.callCount(), 3)
54
+ })
55
+
56
+ it('should keep reading while a page comes back full', async () => {
57
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(0, BATCH), storage.outbox.pending.mock.callCount())
58
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(BATCH, BATCH), storage.outbox.pending.mock.callCount() + 1)
59
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(2 * BATCH, 5), storage.outbox.pending.mock.callCount() + 2)
60
+
61
+ await outbox.open()
62
+ await cycle()
63
+
64
+ assert.strictEqual(storage.outbox.pending.mock.callCount(), 3)
65
+ assert.strictEqual(emission.emit.mock.callCount(), 2 * BATCH + 5)
66
+ })
67
+
68
+ it('should continue each page from the id the one before ended on', async () => {
69
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(0, BATCH), storage.outbox.pending.mock.callCount())
70
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(BATCH, 1), storage.outbox.pending.mock.callCount() + 1)
71
+
72
+ await outbox.open()
73
+ await cycle()
74
+
75
+ const [, second] = storage.outbox.pending.mock.calls
76
+
77
+ assert.strictEqual(storage.outbox.pending.mock.calls[0].arguments[3], undefined)
78
+ assert.deepStrictEqual(second.arguments[3], String(BATCH - 1).padStart(4, '0'))
79
+ })
80
+
81
+ it('should mark what it published, once, after the last page', async () => {
82
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(0, BATCH), storage.outbox.pending.mock.callCount())
83
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(BATCH, 2), storage.outbox.pending.mock.callCount() + 1)
84
+
85
+ await outbox.open()
86
+ await cycle()
87
+
88
+ assert.strictEqual(storage.outbox.settle.mock.callCount(), 1)
89
+ assert.strictEqual(storage.outbox.settle.mock.calls[0].arguments[0].length, BATCH + 2)
90
+ })
91
+
92
+ it('should not publish a row it has published and not yet marked', async () => {
93
+ storage.outbox.settle.mock.mockImplementationOnce(async () => { throw new Error('mongo is out') })
94
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(0, 2), storage.outbox.pending.mock.callCount())
95
+ storage.outbox.pending.mock.mockImplementationOnce(async () => page(0, 2), storage.outbox.pending.mock.callCount() + 1)
96
+
97
+ await outbox.open()
98
+ await cycle()
99
+ await cycle()
100
+
101
+ assert.strictEqual(emission.emit.mock.callCount(), 2)
102
+ })
103
+
104
+ it('should read nothing while it owns no slots', async () => {
105
+ atom.slots.mock.mockImplementation(() => null)
106
+
107
+ await outbox.open()
108
+ await cycle()
109
+
110
+ assert.strictEqual(storage.outbox.pending.mock.callCount(), 0)
111
+ })
112
+
113
+ function resetCalls (target = [assert, BATCH, page, cycle], seen = new Set()) {
114
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
115
+
116
+ seen.add(target)
117
+
118
+ for (const value of Object.values(target))
119
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
120
+ else resetCalls(value, seen)
121
+ }
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- const samples = {
1
+ export const samples = {
4
2
  simple: {
5
3
  query: {
6
4
  criteria: 'name==Eddie'
@@ -96,5 +94,3 @@ const samples = {
96
94
  }
97
95
  }
98
96
  }
99
-
100
- exports.samples = samples
@@ -1,10 +1,11 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { Query } = require('../src/query')
4
- const fixtures = require('./query.fixtures')
4
+ import { Query } from '../src/query.js'
5
+ import * as fixtures from './query.fixtures.js'
5
6
 
6
7
  beforeEach(() => {
7
- jest.clearAllMocks()
8
+ resetCalls()
8
9
  })
9
10
 
10
11
  describe('criteria', () => {
@@ -12,34 +13,51 @@ describe('criteria', () => {
12
13
  const instance = new Query(fixtures.samples.simple.properties)
13
14
  const query = instance.parse({})
14
15
 
15
- expect(query.criteria).toBeUndefined()
16
+ assert.strictEqual(query.criteria, undefined)
16
17
  })
17
18
 
18
19
  it('should parse criteria', () => {
19
20
  const instance = new Query(fixtures.samples.simple.properties)
20
21
  const query = instance.parse(fixtures.samples.simple.query)
21
22
 
22
- expect(query.criteria).toEqual(fixtures.samples.simple.parsed.criteria)
23
+ assert.deepStrictEqual(query.criteria, fixtures.samples.simple.parsed.criteria)
24
+ })
25
+
26
+ it('should keep a parsed criteria', () => {
27
+ const instance = new Query(fixtures.samples.simple.properties)
28
+
29
+ const first = instance.parse(fixtures.samples.simple.query).criteria
30
+ const second = instance.parse(fixtures.samples.simple.query).criteria
31
+
32
+ assert.strictEqual(second, first)
33
+ })
34
+
35
+ it('should not keep an invalid criteria', () => {
36
+ const instance = new Query(fixtures.samples.simple.properties)
37
+ const query = { criteria: 'nonexistent==1' }
38
+
39
+ assert.throws(() => instance.parse(query))
40
+ assert.throws(() => instance.parse(query))
23
41
  })
24
42
 
25
43
  it('should parse criteria with type coercion', () => {
26
44
  const instance = new Query(fixtures.samples.extended.properties)
27
45
  const query = instance.parse(fixtures.samples.extended.query)
28
46
 
29
- expect(query.criteria).toEqual(fixtures.samples.extended.parsed.criteria)
47
+ assert.deepStrictEqual(query.criteria, fixtures.samples.extended.parsed.criteria)
30
48
  })
31
49
 
32
50
  it('should throw on unknown properties', () => {
33
51
  const instance = new Query(fixtures.samples.simple.properties)
34
52
 
35
- expect(() => instance.parse({ criteria: 'lastname==Johnson' })).toThrow(/not defined/)
53
+ assert.throws(() => instance.parse({ criteria: 'lastname==Johnson' }), (error) => /not defined/.test(error.message))
36
54
  })
37
55
 
38
56
  it('should parse id', () => {
39
57
  const instance = new Query(fixtures.samples.id.properties)
40
58
  const query = instance.parse({ ...fixtures.samples.id.query })
41
59
 
42
- expect(query).toStrictEqual(fixtures.samples.id.parsed)
60
+ assert.deepStrictEqual(query, fixtures.samples.id.parsed)
43
61
  })
44
62
  })
45
63
 
@@ -49,7 +67,7 @@ describe('options', () => {
49
67
  it('should not throw if no options', () => {
50
68
  const query = instance.parse({})
51
69
 
52
- expect(query.options).toBeUndefined()
70
+ assert.strictEqual(query.options, undefined)
53
71
  })
54
72
 
55
73
  describe('omit, limit', () => {
@@ -57,7 +75,7 @@ describe('options', () => {
57
75
  const input = { omit: 1, limit: 1 }
58
76
  const query = instance.parse(input)
59
77
 
60
- expect(query.options).toStrictEqual(input)
78
+ assert.deepStrictEqual(query.options, input)
61
79
  })
62
80
  })
63
81
 
@@ -66,13 +84,13 @@ describe('options', () => {
66
84
  const sort = ['a', 'b:desc', 'c']
67
85
  const query = instance.parse({ sort })
68
86
 
69
- expect(query.options.sort).toStrictEqual([['a', 'asc'], ['b', 'desc'], ['c', 'asc']])
87
+ assert.deepStrictEqual(query.options.sort, [['a', 'asc'], ['b', 'desc'], ['c', 'asc']])
70
88
  })
71
89
 
72
90
  it('should throw on unknown properties', () => {
73
91
  const sort = ['d:asc']
74
92
 
75
- expect(() => instance.parse({ sort })).toThrow(/not defined/)
93
+ assert.throws(() => instance.parse({ sort }), (error) => /not defined/.test(error.message))
76
94
  })
77
95
  })
78
96
 
@@ -80,7 +98,17 @@ describe('options', () => {
80
98
  it('should throw on unknown properties', () => {
81
99
  const projection = ['a', 'b', 'c', 'd']
82
100
 
83
- expect(() => instance.parse({ projection })).toThrow(/not defined/)
101
+ assert.throws(() => instance.parse({ projection }), (error) => /not defined/.test(error.message))
84
102
  })
85
103
  })
86
104
  })
105
+
106
+ function resetCalls (target = [assert, fixtures], seen = new Set()) {
107
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
108
+
109
+ seen.add(target)
110
+
111
+ for (const value of Object.values(target))
112
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
113
+ else resetCalls(value, seen)
114
+ }
@@ -1,23 +1,20 @@
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 definition = /** @type {toa.norm.component.Receiver} */ {
5
+ export const definition = /** @type {toa.norm.component.Receiver} */ {
6
6
  operation: generate(),
7
7
  conditioned: false,
8
8
  adaptive: false
9
9
  }
10
10
 
11
- const local = /** @type {toa.core.Component} */ {
12
- invoke: jest.fn()
11
+ export const local = /** @type {toa.core.Component} */ {
12
+ locator: { id: 'default.dummy' },
13
+ invoke: mock.fn()
13
14
  }
14
15
 
15
16
  // noinspection JSCheckFunctionSignatures
16
- const bridge = /** @type {toa.core.bridges.Event} */ {
17
- condition: jest.fn(async (payload) => !(payload.reject === true)),
18
- request: jest.fn(async () => ({ input: generate() }))
17
+ export const bridge = /** @type {toa.core.bridges.Event} */ {
18
+ condition: mock.fn(async (payload) => !(payload.reject === true)),
19
+ request: mock.fn(async () => ({ input: generate() }))
19
20
  }
20
-
21
- exports.definition = definition
22
- exports.local = local
23
- exports.bridge = bridge