@toa.io/extensions.cadence 1.0.0-alpha.284 → 1.0.0-alpha.286
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/LICENSE +22 -0
- package/components/cadence.metronome/manifest.toa.yaml +22 -28
- package/components/cadence.metronome/migrations/0001-system-properties.yaml +14 -0
- package/components/cadence.metronome/migrations/0002-indexes.yaml +5 -0
- package/components/cadence.metronome/operations/delay.js +1 -1
- package/components/cadence.metronome/operations/settle.js +2 -2
- package/components/cadence.metronome/types/toa.d.ts +5 -5
- package/package.json +6 -5
- package/readme.md +21 -18
- package/schemas/declaration.cos.yaml +0 -1
- package/source/Aspect.ts +19 -10
- package/source/Composition.ts +4 -4
- package/source/Dispatcher.test.ts +68 -29
- package/source/Dispatcher.ts +46 -23
- package/source/Factory.ts +13 -10
- package/source/Local.ts +5 -4
- package/source/Pulse.test.ts +48 -19
- package/source/Pulse.ts +30 -21
- package/source/const.ts +1 -1
- package/source/context.ts +2 -2
- package/source/deployment.ts +10 -5
- package/source/manifest.test.ts +9 -6
- package/source/manifest.ts +24 -11
- package/transpiled/Aspect.d.ts +0 -1
- package/transpiled/Aspect.js +11 -5
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Dispatcher.d.ts +1 -1
- package/transpiled/Dispatcher.js +27 -10
- package/transpiled/Dispatcher.js.map +1 -1
- package/transpiled/Factory.d.ts +1 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Local.d.ts +2 -1
- package/transpiled/Local.js.map +1 -1
- package/transpiled/Pulse.d.ts +2 -1
- package/transpiled/Pulse.js +14 -5
- package/transpiled/Pulse.js.map +1 -1
- package/transpiled/const.js.map +1 -1
- package/transpiled/context.d.ts +1 -1
- package/transpiled/deployment.js +5 -2
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/manifest.js +6 -3
- package/transpiled/manifest.js.map +1 -1
- package/tsconfig.tsbuildinfo +1 -0
- package/CHANGELOG.md +0 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2020-present Artem Gurtovoi
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -5,35 +5,29 @@ name: metronome
|
|
|
5
5
|
cadence: ~
|
|
6
6
|
|
|
7
7
|
entity:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
description: Which dispatcher takes this row, as the outbox partitions its lanes
|
|
13
|
-
type: integer
|
|
14
|
-
due:
|
|
15
|
-
description: When the call is to be made, in milliseconds since the epoch
|
|
16
|
-
type: integer
|
|
17
|
-
expires:
|
|
18
|
-
description: >
|
|
19
|
-
After this the call is not made at all. A scan bounds on it, so a call nothing was
|
|
20
|
-
running to make in time is a row nobody reads, rather than an event anything sees.
|
|
21
|
-
type: integer
|
|
22
|
-
endpoint:
|
|
23
|
-
description: What to call, as `namespace.component.operation`
|
|
24
|
-
type: string
|
|
25
|
-
request:
|
|
26
|
-
description: The request to make, as it was handed over
|
|
27
|
-
type: object
|
|
28
|
-
required:
|
|
29
|
-
- lane
|
|
30
|
-
- due
|
|
31
|
-
- expires
|
|
32
|
-
- endpoint
|
|
33
|
-
index:
|
|
8
|
+
properties:
|
|
9
|
+
lane:
|
|
10
|
+
description: Which dispatcher takes this row, as the outbox partitions its lanes
|
|
11
|
+
type: integer
|
|
34
12
|
due:
|
|
35
|
-
|
|
36
|
-
|
|
13
|
+
description: When the call is to be made, in milliseconds since the epoch
|
|
14
|
+
type: integer
|
|
15
|
+
expires:
|
|
16
|
+
description: >
|
|
17
|
+
After this the call is not made at all. A scan bounds on it, so a call nothing was
|
|
18
|
+
running to make in time is a row nobody reads, rather than an event anything sees.
|
|
19
|
+
type: integer
|
|
20
|
+
endpoint:
|
|
21
|
+
description: What to call, as `namespace.component.operation`
|
|
22
|
+
type: string
|
|
23
|
+
request:
|
|
24
|
+
description: The request to make, as it was handed over
|
|
25
|
+
type: object
|
|
26
|
+
required:
|
|
27
|
+
- lane
|
|
28
|
+
- due
|
|
29
|
+
- expires
|
|
30
|
+
- endpoint
|
|
37
31
|
|
|
38
32
|
# `enumerate` reads what is due and `terminate` cancels one; both come from the prototype.
|
|
39
33
|
operations:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 1.0.0-alpha.286 renamed the four system properties of a record. A database written before it
|
|
2
|
+
# holds them under the old names, where nothing reads them: such a record is never created,
|
|
3
|
+
# never updated, never deleted, and at version zero.
|
|
4
|
+
#
|
|
5
|
+
# `$rename` leaves a record that has none of them alone, so this is a no-op for a component
|
|
6
|
+
# that has only ever run this release.
|
|
7
|
+
- update:
|
|
8
|
+
filter: {}
|
|
9
|
+
update:
|
|
10
|
+
$rename:
|
|
11
|
+
_version: VERSION
|
|
12
|
+
_created: CREATED
|
|
13
|
+
_updated: UPDATED
|
|
14
|
+
_deleted: DELETED
|
|
@@ -8,7 +8,7 @@ import { LANES } from '@toa.io/extensions.cadence'
|
|
|
8
8
|
* row has no immediate path to settle, so nothing is gained, and an even spread is what the
|
|
9
9
|
* dispatchers want.
|
|
10
10
|
*/
|
|
11
|
-
export function transition
|
|
11
|
+
export function transition(input, object) {
|
|
12
12
|
const due = Date.now() + input.interval
|
|
13
13
|
|
|
14
14
|
object.lane = Math.floor(Math.random() * LANES)
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The rows whose calls have been made. They are tombstoned rather than removed: the record of
|
|
3
3
|
* what was asked for outlives the call, and a query answers only what is still owed.
|
|
4
4
|
*/
|
|
5
|
-
export function transition
|
|
5
|
+
export function transition(_, objects) {
|
|
6
6
|
const now = Date.now()
|
|
7
7
|
|
|
8
|
-
for (const object of objects) object.
|
|
8
|
+
for (const object of objects) object.DELETED = now
|
|
9
9
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Written by `toa types`. Every run rewrites it.
|
|
2
2
|
// What a manifest does not state belongs in a file of your own.
|
|
3
3
|
|
|
4
|
-
import type { Query } from '@toa.io/core'
|
|
4
|
+
import type { Query } from '@toa.io/core/types'
|
|
5
5
|
import type { Readable } from 'node:stream'
|
|
6
6
|
|
|
7
7
|
export interface Entity {
|
|
@@ -16,10 +16,10 @@ export interface Entity {
|
|
|
16
16
|
/** The request to make, as it was handed over */
|
|
17
17
|
request?: Record<string, unknown>
|
|
18
18
|
id: string
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
VERSION?: number
|
|
20
|
+
CREATED?: number
|
|
21
|
+
UPDATED?: number
|
|
22
|
+
DELETED?: number | null
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export type DelayInput = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.cadence",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.286",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Schedule",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
22
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
23
|
-
"openspan": "1.0.0-alpha.
|
|
21
|
+
"@toa.io/core": "1.0.0-alpha.286",
|
|
22
|
+
"@toa.io/schemas": "1.0.0-alpha.286",
|
|
23
|
+
"openspan": "1.0.0-alpha.286"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"transpile": "tsc"
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
"gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6"
|
|
28
29
|
}
|
package/readme.md
CHANGED
|
@@ -14,9 +14,9 @@ at all.
|
|
|
14
14
|
```yaml
|
|
15
15
|
# manifest.toa.yaml
|
|
16
16
|
cadence:
|
|
17
|
-
sweep:
|
|
18
|
-
cycle: 86400
|
|
19
|
-
intervals: 24
|
|
17
|
+
sweep: # an operation of this component
|
|
18
|
+
cycle: 86400 # seconds one whole cycle takes
|
|
19
|
+
intervals: 24 # what it is split into, so one call an hour
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The gap between calls is `cycle / intervals`, derived rather than declared, so the two cannot
|
|
@@ -26,13 +26,13 @@ declares:
|
|
|
26
26
|
```yaml
|
|
27
27
|
# manifest.toa.yaml
|
|
28
28
|
cadence:
|
|
29
|
-
sweep: 3600
|
|
29
|
+
sweep: 3600 # once an hour
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
The operation is called with the cycle and the interval as its input:
|
|
33
33
|
|
|
34
34
|
```javascript
|
|
35
|
-
async function sweep
|
|
35
|
+
async function sweep({ n, i }, context) {}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
`n` is `intervals` and `i` is which of them this call is for, from `0` to `n - 1`. They are there
|
|
@@ -77,22 +77,25 @@ either answer put to everyone who wanted an operation called every hour is a wor
|
|
|
77
77
|
picking one.
|
|
78
78
|
|
|
79
79
|
`intervals` is not a replica count. With `intervals: 24` and three replicas each makes eight of
|
|
80
|
-
the day's calls: the cycle is spread over
|
|
80
|
+
the day's calls: the cycle is spread over _time_, not over the fleet. Replicas beyond `intervals`
|
|
81
81
|
make none.
|
|
82
82
|
|
|
83
83
|
## Delay
|
|
84
84
|
|
|
85
85
|
```javascript
|
|
86
|
-
const id = await context.delay(
|
|
87
|
-
|
|
86
|
+
const id = await context.delay(
|
|
87
|
+
'mail.sender.remind',
|
|
88
|
+
{ input: { user } },
|
|
89
|
+
{ interval: 7 * 24 * 3600 * 1000, overdue: null }
|
|
90
|
+
)
|
|
88
91
|
|
|
89
92
|
await context.delay.cancel(id)
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
| `interval` | milliseconds from now
|
|
95
|
-
| `overdue`
|
|
95
|
+
| | |
|
|
96
|
+
| ---------- | --------------------------------------------------------------------------- |
|
|
97
|
+
| `interval` | milliseconds from now |
|
|
98
|
+
| `overdue` | milliseconds the call may be late and still be made, or `null` for no bound |
|
|
96
99
|
|
|
97
100
|
The call is made once the delay has passed, and waits for the target where it is not there to
|
|
98
101
|
take it. The id it answers cancels it, and `cancel` raises where the id was never issued.
|
|
@@ -111,11 +114,11 @@ cadence: ~
|
|
|
111
114
|
ordinary path has: a call is made at the moment it is due. How much is left of that in practice
|
|
112
115
|
depends on how coarsely the calls are looked over — see [Discreteness](#discreteness).
|
|
113
116
|
|
|
114
|
-
|
|
|
115
|
-
|
|
116
|
-
| `null`
|
|
117
|
-
| `0`
|
|
118
|
-
| a number | made if no more than that many milliseconds late
|
|
117
|
+
| | |
|
|
118
|
+
| -------- | ------------------------------------------------------- |
|
|
119
|
+
| `null` | no bound: made whenever it can be, however late that is |
|
|
120
|
+
| `0` | on time or never |
|
|
121
|
+
| a number | made if no more than that many milliseconds late |
|
|
119
122
|
|
|
120
123
|
**There is no default, and it is stated on every call.** Only the caller knows whether a late
|
|
121
124
|
call is still the right call — an unpaid order expires on time or not at all, a report is wanted
|
|
@@ -179,7 +182,7 @@ may no longer be one it accepts. Either way the attempt is spent.
|
|
|
179
182
|
```yaml
|
|
180
183
|
# context.toa.yaml
|
|
181
184
|
cadence:
|
|
182
|
-
discreteness: 60
|
|
185
|
+
discreteness: 60 # seconds between passes over the calls waiting to be made
|
|
183
186
|
```
|
|
184
187
|
|
|
185
188
|
`discreteness` is how often the calls waiting to be made are looked over. It defaults to 60,
|
package/source/Aspect.ts
CHANGED
|
@@ -17,14 +17,13 @@ interface Input {
|
|
|
17
17
|
* handing over the call it should have scheduled loses the call, and there is no way for this
|
|
18
18
|
* to know. Where the semantics allow it, hand the call over first.
|
|
19
19
|
*
|
|
20
|
-
* @implements {toa.core.extensions.Aspect}
|
|
21
20
|
*/
|
|
22
21
|
export class Aspect extends Connector {
|
|
23
22
|
public readonly name = 'delay'
|
|
24
23
|
|
|
25
24
|
private readonly metronome: Local
|
|
26
25
|
|
|
27
|
-
public constructor
|
|
26
|
+
public constructor(metronome: Local) {
|
|
28
27
|
super()
|
|
29
28
|
|
|
30
29
|
this.metronome = metronome
|
|
@@ -34,18 +33,28 @@ export class Aspect extends Connector {
|
|
|
34
33
|
|
|
35
34
|
// named rather than forwarded, so that the connector's lifecycle is not reachable from an
|
|
36
35
|
// algorithm — the same reason the atom aspect names its own
|
|
37
|
-
public async invoke
|
|
36
|
+
public async invoke(method: string, ...args: any[]): Promise<unknown> {
|
|
38
37
|
switch (method) {
|
|
39
|
-
case 'delay':
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
case 'delay':
|
|
39
|
+
return await this.delay(args[0], args[1], args[2])
|
|
40
|
+
case 'cancel':
|
|
41
|
+
return await this.cancel(args[0])
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Delay aspect has no '${method}' method`)
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/** Answers the id the call was given, which is what cancels it. */
|
|
46
|
-
private async delay
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
private async delay(
|
|
49
|
+
endpoint: string,
|
|
50
|
+
request: object | null,
|
|
51
|
+
options: Options
|
|
52
|
+
): Promise<unknown> {
|
|
53
|
+
const input: Input = {
|
|
54
|
+
endpoint,
|
|
55
|
+
interval: options.interval,
|
|
56
|
+
overdue: options.overdue
|
|
57
|
+
}
|
|
49
58
|
|
|
50
59
|
// a call that takes no request carries none, rather than a null one
|
|
51
60
|
if (request !== null && request !== undefined) input.request = request
|
|
@@ -58,7 +67,7 @@ export class Aspect extends Connector {
|
|
|
58
67
|
* nothing to terminate and raises; one whose call has already been made has a row still, and
|
|
59
68
|
* terminating it again is not an error.
|
|
60
69
|
*/
|
|
61
|
-
private async cancel
|
|
70
|
+
private async cancel(id: string): Promise<unknown> {
|
|
62
71
|
return await this.metronome.invoke('terminate', { query: { id }, input: null })
|
|
63
72
|
}
|
|
64
73
|
}
|
package/source/Composition.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Dirent } from 'node:fs'
|
|
|
5
5
|
import type { Host } from './Factory.js'
|
|
6
6
|
|
|
7
7
|
/** The components this extension ships, which run inside its own service. */
|
|
8
|
-
export function components
|
|
8
|
+
export function components(): Components {
|
|
9
9
|
const labels: string[] = []
|
|
10
10
|
const paths: string[] = []
|
|
11
11
|
|
|
@@ -20,13 +20,13 @@ export function components (): Components {
|
|
|
20
20
|
export class Composition extends Connector {
|
|
21
21
|
private readonly host: Host
|
|
22
22
|
|
|
23
|
-
public constructor
|
|
23
|
+
public constructor(host: Host) {
|
|
24
24
|
super()
|
|
25
25
|
|
|
26
26
|
this.host = host
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
protected override async open
|
|
29
|
+
protected override async open(): Promise<void> {
|
|
30
30
|
const composition = await this.host.composition(components().paths)
|
|
31
31
|
|
|
32
32
|
await composition.connect()
|
|
@@ -35,7 +35,7 @@ export class Composition extends Connector {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function entries
|
|
38
|
+
function entries(): Dirent[] {
|
|
39
39
|
return readdirSync(ROOT, { withFileTypes: true }).filter((entry) => entry.isDirectory())
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import { Dispatcher } from './Dispatcher.js'
|
|
5
5
|
import { LANES } from './const.js'
|
|
6
6
|
import type { Local } from './Local.js'
|
|
7
|
-
import type { atomicity } from '@toa.io/core'
|
|
7
|
+
import type { atomicity } from '@toa.io/core/types'
|
|
8
8
|
|
|
9
9
|
type Invoke = (endpoint: string, request: any) => Promise<unknown>
|
|
10
10
|
type Slots = (total: number) => number[] | null
|
|
@@ -26,11 +26,20 @@ interface Row {
|
|
|
26
26
|
request: object | null
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const row = (id: string, due: number, lane = 0): Row =>
|
|
30
|
-
|
|
29
|
+
const row = (id: string, due: number, lane = 0): Row => ({
|
|
30
|
+
id,
|
|
31
|
+
lane,
|
|
32
|
+
due,
|
|
33
|
+
endpoint: 'tea.pots.transit',
|
|
34
|
+
request: { input: { id } }
|
|
35
|
+
})
|
|
31
36
|
|
|
32
|
-
const connector = (properties: object): any =>
|
|
33
|
-
|
|
37
|
+
const connector = (properties: object): any => ({
|
|
38
|
+
...properties,
|
|
39
|
+
link: mock.fn(),
|
|
40
|
+
connect: async () => {},
|
|
41
|
+
disconnect: async () => {}
|
|
42
|
+
})
|
|
34
43
|
|
|
35
44
|
const advance = async (ms: number): Promise<void> => {
|
|
36
45
|
mock.timers.tick(ms)
|
|
@@ -39,20 +48,20 @@ const advance = async (ms: number): Promise<void> => {
|
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/** the query one scan made */
|
|
42
|
-
const reads = (): any[] =>
|
|
43
|
-
.
|
|
44
|
-
|
|
51
|
+
const reads = (): any[] =>
|
|
52
|
+
metronome.invoke.mock.calls
|
|
53
|
+
.filter((call) => call.arguments[0] === 'enumerate')
|
|
54
|
+
.map((call) => call.arguments[1].query)
|
|
45
55
|
|
|
46
56
|
/** what has been settled, flattened */
|
|
47
|
-
const settled = (): string[] =>
|
|
48
|
-
.
|
|
49
|
-
|
|
57
|
+
const settled = (): string[] =>
|
|
58
|
+
metronome.invoke.mock.calls
|
|
59
|
+
.filter((call) => call.arguments[0] === 'settle')
|
|
60
|
+
.flatMap((call) => call.arguments[1].query.ids as string[])
|
|
50
61
|
|
|
51
62
|
const range = (total: number): number[] => Array.from({ length: total }, (_, i) => i)
|
|
52
63
|
|
|
53
|
-
const create = (): Dispatcher =>
|
|
54
|
-
new Dispatcher(metronome, () => target, atom)
|
|
55
|
-
|
|
64
|
+
const create = (): Dispatcher => new Dispatcher(metronome, () => target, atom)
|
|
56
65
|
|
|
57
66
|
beforeEach(() => {
|
|
58
67
|
process.env.TOA_CADENCE_DISCRETENESS = String(INTERVAL)
|
|
@@ -62,7 +71,9 @@ beforeEach(() => {
|
|
|
62
71
|
listeners = []
|
|
63
72
|
|
|
64
73
|
metronome = connector({
|
|
65
|
-
invoke: mock.fn<Invoke>(async (endpoint: string) =>
|
|
74
|
+
invoke: mock.fn<Invoke>(async (endpoint: string) =>
|
|
75
|
+
endpoint === 'enumerate' ? rows : null
|
|
76
|
+
)
|
|
66
77
|
})
|
|
67
78
|
|
|
68
79
|
target = connector({ invoke: mock.fn<Invoke>(async () => null) })
|
|
@@ -73,7 +84,9 @@ beforeEach(() => {
|
|
|
73
84
|
listeners.push(listener)
|
|
74
85
|
listener()
|
|
75
86
|
|
|
76
|
-
return () => {
|
|
87
|
+
return () => {
|
|
88
|
+
listeners = listeners.filter((one) => one !== listener)
|
|
89
|
+
}
|
|
77
90
|
}
|
|
78
91
|
})
|
|
79
92
|
})
|
|
@@ -92,8 +105,10 @@ it('should read what is due within the interval, in the lanes it owns', async ()
|
|
|
92
105
|
const [first] = reads()
|
|
93
106
|
|
|
94
107
|
// the first scan is the one the subscription runs as it is added, at time zero
|
|
95
|
-
assert.strictEqual(
|
|
96
|
-
|
|
108
|
+
assert.strictEqual(
|
|
109
|
+
first.criteria,
|
|
110
|
+
`lane=in=(${range(LANES).join(',')});due<${INTERVAL};expires>0`
|
|
111
|
+
)
|
|
97
112
|
assert.deepStrictEqual(first.sort, ['due:asc'])
|
|
98
113
|
})
|
|
99
114
|
|
|
@@ -136,8 +151,11 @@ it('should call one whose time has already passed at once', async () => {
|
|
|
136
151
|
await dispatcher.connect()
|
|
137
152
|
await advance(0)
|
|
138
153
|
|
|
139
|
-
assert.strictEqual(
|
|
140
|
-
|
|
154
|
+
assert.strictEqual(
|
|
155
|
+
target.invoke.mock.callCount(),
|
|
156
|
+
1,
|
|
157
|
+
'a scan reads what is overdue along with what is coming'
|
|
158
|
+
)
|
|
141
159
|
})
|
|
142
160
|
|
|
143
161
|
it('should arm a row once, however often a scan reads it', async () => {
|
|
@@ -159,8 +177,12 @@ it('should not arm a row again while its call is in flight', async () => {
|
|
|
159
177
|
let release: () => void = () => {}
|
|
160
178
|
|
|
161
179
|
rows = [row('a', 10)]
|
|
162
|
-
target.invoke.mock.mockImplementation(
|
|
163
|
-
|
|
180
|
+
target.invoke.mock.mockImplementation(
|
|
181
|
+
async () =>
|
|
182
|
+
await new Promise<void>((resolve) => {
|
|
183
|
+
release = resolve
|
|
184
|
+
})
|
|
185
|
+
)
|
|
164
186
|
|
|
165
187
|
const dispatcher = create()
|
|
166
188
|
|
|
@@ -175,7 +197,11 @@ it('should not arm a row again while its call is in flight', async () => {
|
|
|
175
197
|
// far enough for anything that scan armed to have fired
|
|
176
198
|
await advance(INTERVAL)
|
|
177
199
|
|
|
178
|
-
assert.strictEqual(
|
|
200
|
+
assert.strictEqual(
|
|
201
|
+
target.invoke.mock.callCount(),
|
|
202
|
+
1,
|
|
203
|
+
'and does not send it a second time'
|
|
204
|
+
)
|
|
179
205
|
|
|
180
206
|
release()
|
|
181
207
|
await advance(0)
|
|
@@ -199,19 +225,29 @@ it('should settle what it has called', async () => {
|
|
|
199
225
|
|
|
200
226
|
it('should settle a row whose call failed, and keep dispatching', async () => {
|
|
201
227
|
rows = [row('a', 10), row('b', 20)]
|
|
202
|
-
target.invoke.mock.mockImplementationOnce(async () => {
|
|
228
|
+
target.invoke.mock.mockImplementationOnce(async () => {
|
|
229
|
+
throw new Error('nope')
|
|
230
|
+
})
|
|
203
231
|
|
|
204
232
|
const dispatcher = create()
|
|
205
233
|
|
|
206
234
|
await dispatcher.connect()
|
|
207
235
|
await advance(20)
|
|
208
236
|
|
|
209
|
-
assert.strictEqual(
|
|
237
|
+
assert.strictEqual(
|
|
238
|
+
target.invoke.mock.callCount(),
|
|
239
|
+
2,
|
|
240
|
+
'the one after it is still called'
|
|
241
|
+
)
|
|
210
242
|
|
|
211
243
|
rows = []
|
|
212
244
|
await advance(INTERVAL)
|
|
213
245
|
|
|
214
|
-
assert.deepStrictEqual(
|
|
246
|
+
assert.deepStrictEqual(
|
|
247
|
+
settled().sort(),
|
|
248
|
+
['a', 'b'],
|
|
249
|
+
'and the one that failed is settled'
|
|
250
|
+
)
|
|
215
251
|
})
|
|
216
252
|
|
|
217
253
|
it('should drop timers for lanes it no longer owns', async () => {
|
|
@@ -229,7 +265,7 @@ it('should drop timers for lanes it no longer owns', async () => {
|
|
|
229
265
|
|
|
230
266
|
await advance(20_000)
|
|
231
267
|
|
|
232
|
-
assert.strictEqual(target.invoke.mock.callCount(), 0,
|
|
268
|
+
assert.strictEqual(target.invoke.mock.callCount(), 0, "lane 7 is somebody else's now")
|
|
233
269
|
})
|
|
234
270
|
|
|
235
271
|
it('should drop everything it holds when it owns nothing', async () => {
|
|
@@ -247,8 +283,11 @@ it('should drop everything it holds when it owns nothing', async () => {
|
|
|
247
283
|
|
|
248
284
|
await advance(20_000)
|
|
249
285
|
|
|
250
|
-
assert.strictEqual(
|
|
251
|
-
|
|
286
|
+
assert.strictEqual(
|
|
287
|
+
target.invoke.mock.callCount(),
|
|
288
|
+
0,
|
|
289
|
+
'whoever holds lane 7 now is the one to make that call'
|
|
290
|
+
)
|
|
252
291
|
})
|
|
253
292
|
|
|
254
293
|
it('should scan at once when what it owns changes', async () => {
|