@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.
Files changed (45) hide show
  1. package/LICENSE +22 -0
  2. package/components/cadence.metronome/manifest.toa.yaml +22 -28
  3. package/components/cadence.metronome/migrations/0001-system-properties.yaml +14 -0
  4. package/components/cadence.metronome/migrations/0002-indexes.yaml +5 -0
  5. package/components/cadence.metronome/operations/delay.js +1 -1
  6. package/components/cadence.metronome/operations/settle.js +2 -2
  7. package/components/cadence.metronome/types/toa.d.ts +5 -5
  8. package/package.json +6 -5
  9. package/readme.md +21 -18
  10. package/schemas/declaration.cos.yaml +0 -1
  11. package/source/Aspect.ts +19 -10
  12. package/source/Composition.ts +4 -4
  13. package/source/Dispatcher.test.ts +68 -29
  14. package/source/Dispatcher.ts +46 -23
  15. package/source/Factory.ts +13 -10
  16. package/source/Local.ts +5 -4
  17. package/source/Pulse.test.ts +48 -19
  18. package/source/Pulse.ts +30 -21
  19. package/source/const.ts +1 -1
  20. package/source/context.ts +2 -2
  21. package/source/deployment.ts +10 -5
  22. package/source/manifest.test.ts +9 -6
  23. package/source/manifest.ts +24 -11
  24. package/transpiled/Aspect.d.ts +0 -1
  25. package/transpiled/Aspect.js +11 -5
  26. package/transpiled/Aspect.js.map +1 -1
  27. package/transpiled/Composition.js.map +1 -1
  28. package/transpiled/Dispatcher.d.ts +1 -1
  29. package/transpiled/Dispatcher.js +27 -10
  30. package/transpiled/Dispatcher.js.map +1 -1
  31. package/transpiled/Factory.d.ts +1 -1
  32. package/transpiled/Factory.js.map +1 -1
  33. package/transpiled/Local.d.ts +2 -1
  34. package/transpiled/Local.js.map +1 -1
  35. package/transpiled/Pulse.d.ts +2 -1
  36. package/transpiled/Pulse.js +14 -5
  37. package/transpiled/Pulse.js.map +1 -1
  38. package/transpiled/const.js.map +1 -1
  39. package/transpiled/context.d.ts +1 -1
  40. package/transpiled/deployment.js +5 -2
  41. package/transpiled/deployment.js.map +1 -1
  42. package/transpiled/manifest.js +6 -3
  43. package/transpiled/manifest.js.map +1 -1
  44. package/tsconfig.tsbuildinfo +1 -0
  45. 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
- schema:
9
- type: object
10
- properties:
11
- lane:
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
- lane: asc
36
- due: asc
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
@@ -0,0 +1,5 @@
1
+ - index:
2
+ name: index_due
3
+ keys:
4
+ lane: asc
5
+ due: asc
@@ -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 (input, object) {
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 (_, objects) {
5
+ export function transition(_, objects) {
6
6
  const now = Date.now()
7
7
 
8
- for (const object of objects) object._deleted = now
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
- _version?: number
20
- _created?: number
21
- _updated?: number
22
- _deleted?: number | null
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.284",
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.284",
22
- "@toa.io/schemas": "1.0.0-alpha.283",
23
- "openspan": "1.0.0-alpha.277"
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: # 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
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 # once an hour
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 ({ n, i }, context) { }
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 *time*, not over the fleet. Replicas beyond `intervals`
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('mail.sender.remind', { input: { user } },
87
- { interval: 7 * 24 * 3600 * 1000, overdue: null })
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` | milliseconds the call may be late and still be made, or `null` for no bound |
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` | no bound: made whenever it can be, however late that is |
117
- | `0` | on time or never |
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 # seconds between passes over the calls waiting to be made
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,
@@ -16,6 +16,5 @@ patternProperties:
16
16
  and the operation is told which interval it is running for.
17
17
  type: integer
18
18
  minimum: 1
19
- default: 1
20
19
  required: [cycle]
21
20
  additionalProperties: false
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 (metronome: Local) {
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 (method: string, ...args: any[]): Promise<unknown> {
36
+ public async invoke(method: string, ...args: any[]): Promise<unknown> {
38
37
  switch (method) {
39
- case 'delay': return await this.delay(args[0], args[1], args[2])
40
- case 'cancel': return await this.cancel(args[0])
41
- default: throw new Error(`Delay aspect has no '${method}' method`)
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 (endpoint: string, request: object | null,
47
- options: Options): Promise<unknown> {
48
- const input: Input = { endpoint, interval: options.interval, overdue: options.overdue }
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 (id: string): Promise<unknown> {
70
+ private async cancel(id: string): Promise<unknown> {
62
71
  return await this.metronome.invoke('terminate', { query: { id }, input: null })
63
72
  }
64
73
  }
@@ -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 (): 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 (host: Host) {
23
+ public constructor(host: Host) {
24
24
  super()
25
25
 
26
26
  this.host = host
27
27
  }
28
28
 
29
- protected override async open (): Promise<void> {
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 (): Dirent[] {
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
- ({ id, lane, due, endpoint: 'tea.pots.transit', request: { input: { id } } })
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
- ({ ...properties, link: mock.fn(), connect: async () => {}, disconnect: async () => {} })
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[] => metronome.invoke.mock.calls
43
- .filter((call) => call.arguments[0] === 'enumerate')
44
- .map((call) => call.arguments[1].query)
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[] => metronome.invoke.mock.calls
48
- .filter((call) => call.arguments[0] === 'settle')
49
- .flatMap((call) => call.arguments[1].query.ids as string[])
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) => endpoint === 'enumerate' ? rows : null)
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 () => { listeners = listeners.filter((one) => one !== listener) }
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(first.criteria,
96
- `lane=in=(${range(LANES).join(',')});due<${INTERVAL};expires>0`)
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(target.invoke.mock.callCount(), 1,
140
- 'a scan reads what is overdue along with what is coming')
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(async () =>
163
- await new Promise<void>((resolve) => { release = resolve }))
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(target.invoke.mock.callCount(), 1, 'and does not send it a second time')
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 () => { throw new Error('nope') })
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(target.invoke.mock.callCount(), 2, 'the one after it is still called')
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(settled().sort(), ['a', 'b'], 'and the one that failed is settled')
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, 'lane 7 is somebody else\'s now')
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(target.invoke.mock.callCount(), 0,
251
- 'whoever holds lane 7 now is the one to make that call')
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 () => {