@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
@@ -2,7 +2,7 @@ import { console } from 'openspan'
2
2
  import { Connector, Locator } from '@toa.io/core'
3
3
  import { BATCH, DISCRETENESS, LANES, number } from './const.js'
4
4
  import type { Local } from './Local.js'
5
- import type { atomicity } from '@toa.io/core'
5
+ import type { atomicity } from '@toa.io/core/types'
6
6
 
7
7
  /**
8
8
  * Makes the calls that were put off.
@@ -37,8 +37,11 @@ export class Dispatcher extends Connector {
37
37
  private scanning = false
38
38
  private closing = false
39
39
 
40
- public constructor (metronome: Local, resolve: (locator: Locator) => Local,
41
- atom: atomicity.Atom) {
40
+ public constructor(
41
+ metronome: Local,
42
+ resolve: (locator: Locator) => Local,
43
+ atom: atomicity.Atom
44
+ ) {
42
45
  super()
43
46
 
44
47
  this.metronome = metronome
@@ -50,15 +53,19 @@ export class Dispatcher extends Connector {
50
53
  this.depends(atom)
51
54
  }
52
55
 
53
- protected override async open (): Promise<void> {
54
- this.timer = setInterval(() => { this.tick() }, this.discreteness)
56
+ protected override async open(): Promise<void> {
57
+ this.timer = setInterval(() => {
58
+ this.tick()
59
+ }, this.discreteness)
55
60
  this.timer.unref()
56
61
 
57
62
  // a lane changing hands is when its rows become this replica's, or stop being
58
- this.off = this.atom.onassigned(() => { this.adopt() })
63
+ this.off = this.atom.onassigned(() => {
64
+ this.adopt()
65
+ })
59
66
  }
60
67
 
61
- protected override async close (): Promise<void> {
68
+ protected override async close(): Promise<void> {
62
69
  this.closing = true
63
70
 
64
71
  this.off?.()
@@ -72,14 +79,18 @@ export class Dispatcher extends Connector {
72
79
  }
73
80
 
74
81
  /** One scan at a time: a slow one must not have another started underneath it. */
75
- private tick (): void {
82
+ private tick(): void {
76
83
  if (this.scanning || this.closing) return
77
84
 
78
85
  this.scanning = true
79
86
 
80
87
  void this.scan()
81
- .catch((error: unknown) => { console.error('Delayed calls scan failed', { error }) })
82
- .finally(() => { this.scanning = false })
88
+ .catch((error: unknown) => {
89
+ console.error('Delayed calls scan failed', { error })
90
+ })
91
+ .finally(() => {
92
+ this.scanning = false
93
+ })
83
94
  }
84
95
 
85
96
  /**
@@ -87,7 +98,7 @@ export class Dispatcher extends Connector {
87
98
  * whoever holds them now will arm them — and a scan runs at once rather than at the next
88
99
  * interval, so lanes just acquired are not left waiting.
89
100
  */
90
- private adopt (): void {
101
+ private adopt(): void {
91
102
  // nothing owned is nothing held: what was armed under a claim this replica no longer has
92
103
  // belongs to whoever has it now, and firing it anyway is the call twice
93
104
  this.disarm(this.atom.slots(LANES) ?? [])
@@ -96,7 +107,7 @@ export class Dispatcher extends Connector {
96
107
  }
97
108
 
98
109
  /** What is armed for a lane this replica no longer holds: whoever holds it now will arm it. */
99
- private disarm (lanes: number[]): void {
110
+ private disarm(lanes: number[]): void {
100
111
  for (const [id, { timer, lane }] of this.armed) {
101
112
  if (lanes.includes(lane)) continue
102
113
 
@@ -105,7 +116,7 @@ export class Dispatcher extends Connector {
105
116
  }
106
117
  }
107
118
 
108
- private async scan (): Promise<void> {
119
+ private async scan(): Promise<void> {
109
120
  const lanes = this.atom.slots(LANES)
110
121
 
111
122
  if (lanes === null || lanes.length === 0) return
@@ -115,14 +126,14 @@ export class Dispatcher extends Connector {
115
126
  const now = Date.now()
116
127
  const until = now + this.discreteness
117
128
 
118
- const rows = await this.metronome.invoke('enumerate', {
129
+ const rows = (await this.metronome.invoke('enumerate', {
119
130
  query: {
120
131
  // a call nothing was running to make in time has expired, and is simply not read
121
132
  criteria: `lane=in=(${lanes.join(',')});due<${until};expires>${now}`,
122
133
  sort: ['due:asc'],
123
134
  limit: BATCH
124
135
  }
125
- }) as Row[]
136
+ })) as Row[]
126
137
 
127
138
  for (const row of rows) this.arm(row)
128
139
  }
@@ -131,10 +142,15 @@ export class Dispatcher extends Connector {
131
142
  * A row already armed is left alone: a scan reads what it read before, because a row stays
132
143
  * until its call has been made.
133
144
  */
134
- private arm (row: Row): void {
145
+ private arm(row: Row): void {
135
146
  if (this.armed.has(row.id) || this.called.has(row.id)) return
136
147
 
137
- const timer = setTimeout(() => { void this.dispatch(row) }, Math.max(row.due - Date.now(), 0))
148
+ const timer = setTimeout(
149
+ () => {
150
+ void this.dispatch(row)
151
+ },
152
+ Math.max(row.due - Date.now(), 0)
153
+ )
138
154
 
139
155
  timer.unref()
140
156
  this.armed.set(row.id, { timer, lane: row.lane })
@@ -145,7 +161,7 @@ export class Dispatcher extends Connector {
145
161
  * makes the call twice, and the other order would lose it. It travels as a task, so the
146
162
  * broker holds it if the target is not there to take it.
147
163
  */
148
- private async dispatch (row: Row): Promise<void> {
164
+ private async dispatch(row: Row): Promise<void> {
149
165
  if (this.closing) {
150
166
  this.armed.delete(row.id)
151
167
 
@@ -170,20 +186,27 @@ export class Dispatcher extends Connector {
170
186
  *
171
187
  * Skipped, as a pulse skips: reported, and the row settled on the attempt either way.
172
188
  */
173
- await local.invoke(endpoint, { input: null, ...row.request, task: true })
189
+ await local
190
+ .invoke(endpoint, { input: null, ...row.request, task: true })
174
191
  .catch((error: unknown) => {
175
- console.error('Delayed call failed', { endpoint: row.endpoint, id: row.id, error })
192
+ console.error('Delayed call failed', {
193
+ endpoint: row.endpoint,
194
+ id: row.id,
195
+ error
196
+ })
176
197
  })
177
198
  // held for as long as the call is, so `armed` answers what this replica has in hand
178
199
  // rather than what it has a timer for
179
- .finally(() => { this.armed.delete(row.id) })
200
+ .finally(() => {
201
+ this.armed.delete(row.id)
202
+ })
180
203
  }
181
204
 
182
205
  /**
183
206
  * A remote is resolved once per target and held: it opens a connection, and one nothing
184
207
  * depends on would outlive the dispatcher that made it.
185
208
  */
186
- private target (locator: Locator): Local {
209
+ private target(locator: Locator): Local {
187
210
  const local = this.resolve(locator)
188
211
 
189
212
  if (!this.targets.has(local)) {
@@ -195,7 +218,7 @@ export class Dispatcher extends Connector {
195
218
  }
196
219
 
197
220
  /** One write for many rows, which is why the ids are held rather than settled one by one. */
198
- private async settle (): Promise<void> {
221
+ private async settle(): Promise<void> {
199
222
  if (this.called.size === 0) return
200
223
 
201
224
  const ids = [...this.called]
package/source/Factory.ts CHANGED
@@ -6,7 +6,7 @@ import { Dispatcher } from './Dispatcher.js'
6
6
  import { Local } from './Local.js'
7
7
  import { Pulse } from './Pulse.js'
8
8
  import type { Declaration } from './types.js'
9
- import type { extensions } from '@toa.io/core'
9
+ import type { extensions } from '@toa.io/core/types'
10
10
 
11
11
  export class Factory implements extensions.Factory {
12
12
  private readonly host: Host
@@ -14,7 +14,7 @@ export class Factory implements extensions.Factory {
14
14
  /** one per component, however much of this extension calls through it */
15
15
  private readonly locals: Record<string, Local> = {}
16
16
 
17
- public constructor (host: Host) {
17
+ public constructor(host: Host) {
18
18
  this.host = host
19
19
  }
20
20
 
@@ -25,10 +25,13 @@ export class Factory implements extensions.Factory {
25
25
  * A tenant is created before the components of its composition are, which is why nothing here
26
26
  * reaches for one.
27
27
  */
28
- public tenant (locator: Locator, declaration: object): Connector {
28
+ public tenant(locator: Locator, declaration: object): Connector {
29
29
  if (mine(locator))
30
- return new Dispatcher(this.metronome(), (target) => this.local(target),
31
- this.host.atom(locator.id))
30
+ return new Dispatcher(
31
+ this.metronome(),
32
+ (target) => this.local(target),
33
+ this.host.atom(locator.id)
34
+ )
32
35
 
33
36
  const tenant = new Connector()
34
37
  const pulses = declaration as Declaration | null
@@ -46,27 +49,27 @@ export class Factory implements extensions.Factory {
46
49
  }
47
50
 
48
51
  /** `context.delay` — for every component but the one that keeps the calls. */
49
- public aspect (locator: Locator): extensions.Aspect | extensions.Aspect[] {
52
+ public aspect(locator: Locator): extensions.Aspect | extensions.Aspect[] {
50
53
  if (mine(locator)) return []
51
54
 
52
55
  return new Aspect(this.metronome())
53
56
  }
54
57
 
55
- public service (): Connector {
58
+ public service(): Connector {
56
59
  return new Composition(this.host)
57
60
  }
58
61
 
59
- private metronome (): Local {
62
+ private metronome(): Local {
60
63
  return this.local(new Locator(COMPONENT, NAMESPACE))
61
64
  }
62
65
 
63
- private local (locator: Locator): Local {
66
+ private local(locator: Locator): Local {
64
67
  return (this.locals[locator.id] ??= new Local(this.host, locator))
65
68
  }
66
69
  }
67
70
 
68
71
  /** Whether this is a component the extension ships rather than one of the application's. */
69
- function mine (locator: Locator): boolean {
72
+ function mine(locator: Locator): boolean {
70
73
  return locator.namespace === NAMESPACE
71
74
  }
72
75
 
package/source/Local.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Connector } from '@toa.io/core'
2
2
  import type { Host } from './Factory.js'
3
- import type { Locator, Remote, Reply, Request } from '@toa.io/core'
3
+ import type { Locator, Remote } from '@toa.io/core'
4
+ import type { Reply, Request } from '@toa.io/core/types'
4
5
 
5
6
  /**
6
7
  * The component a tenant belongs to, as something to call.
@@ -14,20 +15,20 @@ export class Local extends Connector {
14
15
  private readonly locator: Locator
15
16
  private remote?: Promise<Remote>
16
17
 
17
- public constructor (host: Host, locator: Locator) {
18
+ public constructor(host: Host, locator: Locator) {
18
19
  super()
19
20
 
20
21
  this.host = host
21
22
  this.locator = locator
22
23
  }
23
24
 
24
- public async invoke (endpoint: string, request: Request): Promise<Reply> {
25
+ public async invoke(endpoint: string, request: Request): Promise<Reply> {
25
26
  this.remote ??= this.locate()
26
27
 
27
28
  return await (await this.remote).invoke(endpoint, request)
28
29
  }
29
30
 
30
- private async locate (): Promise<Remote> {
31
+ private async locate(): Promise<Remote> {
31
32
  const remote = await this.host.remote(this.locator)
32
33
 
33
34
  this.depends(remote)
@@ -3,7 +3,8 @@ import assert from 'node:assert/strict'
3
3
 
4
4
  import { Pulse } from './Pulse.js'
5
5
  import type { Local } from './Local.js'
6
- import type { Locator, Request, atomicity } from '@toa.io/core'
6
+ import type { Locator } from '@toa.io/core'
7
+ import type { Request, atomicity } from '@toa.io/core/types'
7
8
 
8
9
  type Invoke = (endpoint: string, request: Request) => Promise<unknown>
9
10
  type Slots = (total: number) => number[] | null
@@ -17,11 +18,16 @@ const DAY = 24 * HOUR
17
18
 
18
19
  const locator = { id: 'tea.pots' } as Locator
19
20
 
20
- const range = (total: number): number[] => Array.from({ length: total }, (_, index) => index)
21
+ const range = (total: number): number[] =>
22
+ Array.from({ length: total }, (_, index) => index)
21
23
 
22
24
  /** what a Pulse reaches for, with a lifecycle it can connect through */
23
- const connector = (properties: object): any =>
24
- ({ ...properties, link: mock.fn(), connect: async () => {}, disconnect: async () => {} })
25
+ const connector = (properties: object): any => ({
26
+ ...properties,
27
+ link: mock.fn(),
28
+ connect: async () => {},
29
+ disconnect: async () => {}
30
+ })
25
31
 
26
32
  const create = (cycle: number, intervals: number): Pulse =>
27
33
  new Pulse({ locator, endpoint: 'sweep', cycle, intervals }, local, atom)
@@ -37,7 +43,6 @@ const advance = async (ms: number): Promise<void> => {
37
43
  const intervals = (): number[] =>
38
44
  local.invoke.mock.calls.map((call) => (call.arguments[1].input as { i: number }).i)
39
45
 
40
-
41
46
  beforeEach(() => {
42
47
  mock.timers.enable({ apis: ['setTimeout', 'Date'], now: 0 })
43
48
 
@@ -72,7 +77,10 @@ it('should give every interval of a cycle that does not divide evenly', () => {
72
77
  now = pulse.boundary(now)
73
78
  }
74
79
 
75
- assert.deepStrictEqual([...seen].sort((a, b) => a - b), [0, 1, 2, 3, 4, 5, 6])
80
+ assert.deepStrictEqual(
81
+ [...seen].sort((a, b) => a - b),
82
+ [0, 1, 2, 3, 4, 5, 6]
83
+ )
76
84
  assert.strictEqual(now, DAY * SECOND, 'seven boundaries land exactly on the next cycle')
77
85
  })
78
86
 
@@ -130,8 +138,10 @@ it('should call once per interval, and every interval of a cycle', async () => {
130
138
 
131
139
  for (let hour = 0; hour < 24; hour++) await advance(HOUR * SECOND)
132
140
 
133
- assert.deepStrictEqual(intervals(), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
134
- 17, 18, 19, 20, 21, 22, 23, 0])
141
+ assert.deepStrictEqual(
142
+ intervals(),
143
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0]
144
+ )
135
145
  })
136
146
 
137
147
  it('should keep calling a cycle it is not splitting', async () => {
@@ -167,13 +177,17 @@ it('should not call an interval it acquired after the boundary', async () => {
167
177
 
168
178
  await advance(30 * 60 * SECOND) // and half an hour later it is
169
179
 
170
- assert.strictEqual(local.invoke.mock.callCount(), 0,
171
- 'the replica that held it at the boundary is the only one that could have called')
180
+ assert.strictEqual(
181
+ local.invoke.mock.callCount(),
182
+ 0,
183
+ 'the replica that held it at the boundary is the only one that could have called'
184
+ )
172
185
  })
173
186
 
174
187
  it('should not call an interval another replica owns', async () => {
175
188
  atom.slots.mock.mockImplementation((total) =>
176
- range(total).filter((slot) => slot % 2 === 0))
189
+ range(total).filter((slot) => slot % 2 === 0)
190
+ )
177
191
 
178
192
  const pulse = create(DAY, 24)
179
193
 
@@ -190,8 +204,12 @@ it('should not call an interval another replica owns', async () => {
190
204
  it('should not start a call while the one before has not returned', async () => {
191
205
  let release: () => void = () => {}
192
206
 
193
- local.invoke.mock.mockImplementation(async () =>
194
- await new Promise<void>((resolve) => { release = resolve }))
207
+ local.invoke.mock.mockImplementation(
208
+ async () =>
209
+ await new Promise<void>((resolve) => {
210
+ release = resolve
211
+ })
212
+ )
195
213
 
196
214
  const pulse = create(DAY, 24)
197
215
 
@@ -211,7 +229,9 @@ it('should not start a call while the one before has not returned', async () =>
211
229
  })
212
230
 
213
231
  it('should keep its cadence when a call raises', async () => {
214
- local.invoke.mock.mockImplementationOnce(async () => { throw new Error('nope') })
232
+ local.invoke.mock.mockImplementationOnce(async () => {
233
+ throw new Error('nope')
234
+ })
215
235
 
216
236
  const pulse = create(DAY, 24)
217
237
 
@@ -220,8 +240,11 @@ it('should keep its cadence when a call raises', async () => {
220
240
  await advance(HOUR * SECOND)
221
241
  await advance(HOUR * SECOND)
222
242
 
223
- assert.strictEqual(local.invoke.mock.callCount(), 2,
224
- 'the interval that raised is skipped, and the next one is called')
243
+ assert.strictEqual(
244
+ local.invoke.mock.callCount(),
245
+ 2,
246
+ 'the interval that raised is skipped, and the next one is called'
247
+ )
225
248
  })
226
249
 
227
250
  it('should wait out a cycle longer than one timer in instalments', async () => {
@@ -243,15 +266,21 @@ it('should wait for a call in flight before closing', async () => {
243
266
  let release: () => void = () => {}
244
267
  let closed = false
245
268
 
246
- local.invoke.mock.mockImplementation(async () =>
247
- await new Promise<void>((resolve) => { release = resolve }))
269
+ local.invoke.mock.mockImplementation(
270
+ async () =>
271
+ await new Promise<void>((resolve) => {
272
+ release = resolve
273
+ })
274
+ )
248
275
 
249
276
  const pulse = create(DAY, 24)
250
277
 
251
278
  await pulse.connect()
252
279
  await advance(HOUR * SECOND)
253
280
 
254
- const closing = pulse.disconnect().then(() => { closed = true })
281
+ const closing = pulse.disconnect().then(() => {
282
+ closed = true
283
+ })
255
284
 
256
285
  await advance(0)
257
286
  assert.strictEqual(closed, false, 'the call is still running')
package/source/Pulse.ts CHANGED
@@ -2,7 +2,8 @@ import { console } from 'openspan'
2
2
  import { Connector } from '@toa.io/core'
3
3
  import type { Local } from './Local.js'
4
4
  import type { SpanOptions } from 'openspan'
5
- import type { Locator, atomicity } from '@toa.io/core'
5
+ import type { Locator } from '@toa.io/core'
6
+ import type { atomicity } from '@toa.io/core/types'
6
7
 
7
8
  /**
8
9
  * A recurring call to an operation of its own component, on the cadence its manifest states.
@@ -43,7 +44,7 @@ export class Pulse extends Connector {
43
44
 
44
45
  private closing = false
45
46
 
46
- public constructor (definition: Definition, local: Local, atom: atomicity.Atom) {
47
+ public constructor(definition: Definition, local: Local, atom: atomicity.Atom) {
47
48
  super()
48
49
 
49
50
  const { locator, endpoint, cycle, intervals } = definition
@@ -75,17 +76,17 @@ export class Pulse extends Connector {
75
76
  * as large as the cycle has seconds, so their product leaves the range a double is exact in
76
77
  * for a long cycle cut finely. It is evaluated once per interval, where this costs nothing.
77
78
  */
78
- public ordinal (now: number): number {
79
- return Number(BigInt(now) * BigInt(this.n) / BigInt(this.cycle))
79
+ public ordinal(now: number): number {
80
+ return Number((BigInt(now) * BigInt(this.n)) / BigInt(this.cycle))
80
81
  }
81
82
 
82
83
  /** Which of the cycle's intervals `now` falls in. */
83
- public index (now: number): number {
84
+ public index(now: number): number {
84
85
  return this.ordinal(now) % this.n
85
86
  }
86
87
 
87
88
  /** When the interval `now` falls in gives way to the next one. */
88
- public boundary (now: number): number {
89
+ public boundary(now: number): number {
89
90
  const cycle = BigInt(this.cycle)
90
91
  const n = BigInt(this.n)
91
92
  const next = BigInt(this.ordinal(now) + 1)
@@ -94,7 +95,7 @@ export class Pulse extends Connector {
94
95
  return Number((next * cycle + n - 1n) / n)
95
96
  }
96
97
 
97
- protected override async open (): Promise<void> {
98
+ protected override async open(): Promise<void> {
98
99
  // whatever interval it is now, this replica was not there when it began, so it is not
99
100
  // this one's to call — the first call is at the next boundary
100
101
  this.fired = this.ordinal(Date.now())
@@ -102,7 +103,7 @@ export class Pulse extends Connector {
102
103
  this.arm()
103
104
  }
104
105
 
105
- protected override async close (): Promise<void> {
106
+ protected override async close(): Promise<void> {
106
107
  this.closing = true
107
108
 
108
109
  clearTimeout(this.timer)
@@ -110,11 +111,10 @@ export class Pulse extends Connector {
110
111
  // a call still running holds the remote it was made through, which is torn down after
111
112
  // this returns. Bounded, because an operation that never returns would outlive any
112
113
  // grace period and the interval is over either way
113
- if (this.firing !== undefined)
114
- await Promise.race([this.firing, delay(DRAIN)])
114
+ if (this.firing !== undefined) await Promise.race([this.firing, delay(DRAIN)])
115
115
  }
116
116
 
117
- private arm (): void {
117
+ private arm(): void {
118
118
  const now = Date.now()
119
119
 
120
120
  // a delay past this fires at once rather than late, so a long cycle is waited out in
@@ -125,7 +125,7 @@ export class Pulse extends Connector {
125
125
  this.timer.unref()
126
126
  }
127
127
 
128
- private tick (): void {
128
+ private tick(): void {
129
129
  if (this.closing) return
130
130
 
131
131
  const ordinal = this.ordinal(Date.now())
@@ -145,10 +145,12 @@ export class Pulse extends Connector {
145
145
  * Nothing queues here. A call still running when the next boundary arrives means the work
146
146
  * does not fit the gap, and starting a second one would only make that worse.
147
147
  */
148
- private async fire (i: number): Promise<void> {
148
+ private async fire(i: number): Promise<void> {
149
149
  if (this.firing !== undefined) {
150
- console.warn('Pulse skipped: the previous call has not returned',
151
- { pulse: this.label, interval: i })
150
+ console.warn('Pulse skipped: the previous call has not returned', {
151
+ pulse: this.label,
152
+ interval: i
153
+ })
152
154
 
153
155
  return
154
156
  }
@@ -156,8 +158,10 @@ export class Pulse extends Connector {
156
158
  const owned = this.atom.slots(this.n)
157
159
 
158
160
  if (owned === null) {
159
- console.warn('Pulse skipped: this replica owns nothing',
160
- { pulse: this.label, interval: i })
161
+ console.warn('Pulse skipped: this replica owns nothing', {
162
+ pulse: this.label,
163
+ interval: i
164
+ })
161
165
 
162
166
  return
163
167
  }
@@ -171,12 +175,17 @@ export class Pulse extends Connector {
171
175
  * only wanted an operation called every hour. So the interval is skipped: it is reported,
172
176
  * and the next one is called as though it had not happened.
173
177
  */
174
- const firing = console.span(this.options,
175
- async () => await this.local.invoke(this.endpoint, { input: { n: this.n, i } }))
178
+ const firing = console
179
+ .span(
180
+ this.options,
181
+ async () => await this.local.invoke(this.endpoint, { input: { n: this.n, i } })
182
+ )
176
183
  .catch((error: unknown) => {
177
184
  console.error('Pulse failed', { pulse: this.label, interval: i, error })
178
185
  })
179
- .finally(() => { this.firing = undefined })
186
+ .finally(() => {
187
+ this.firing = undefined
188
+ })
180
189
 
181
190
  this.firing = firing
182
191
 
@@ -191,7 +200,7 @@ export interface Definition {
191
200
  intervals: number
192
201
  }
193
202
 
194
- async function delay (ms: number): Promise<void> {
203
+ async function delay(ms: number): Promise<void> {
195
204
  await new Promise((resolve) => setTimeout(resolve, ms).unref())
196
205
  }
197
206
 
package/source/const.ts CHANGED
@@ -16,7 +16,7 @@ export const DISCRETENESS = 60
16
16
  export const BATCH = 200
17
17
 
18
18
  /** A number from the environment, where a deployment or a test suite states one. */
19
- export function number (variable: string, fallback: number): number {
19
+ export function number(variable: string, fallback: number): number {
20
20
  const value = Number(process.env[variable])
21
21
 
22
22
  return Number.isNaN(value) || value <= 0 ? fallback : value
package/source/context.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { Contribution } from '@toa.io/core'
1
+ import type { Contribution } from '@toa.io/core/types'
2
2
 
3
3
  /** What this extension puts on the context of a component that declares it. */
4
- export function context (): Contribution {
4
+ export function context(): Contribution {
5
5
  return {
6
6
  name: 'delay',
7
7
  type: 'Delay',
@@ -9,19 +9,24 @@ import type { Dependency, Service, Variable } from '@toa.io/operations'
9
9
  * The component that keeps delayed calls runs in a service of its own, the way the identity
10
10
  * components run in the gateway. A composition may claim it and run it in its own pods instead.
11
11
  */
12
- export function deployment (_: unknown, annotation?: Annotation | null): Dependency {
13
- schemas.annotation.validate<Annotation | null>(annotation ?? null, 'Invalid cadence annotation')
12
+ export function deployment(_: unknown, annotation?: Annotation | null): Dependency {
13
+ schemas.annotation.validate<Annotation | null>(
14
+ annotation ?? null,
15
+ 'Invalid cadence annotation'
16
+ )
14
17
 
15
18
  // stated in seconds and carried in milliseconds, so that what an application writes is what
16
19
  // the rest of a manifest is written in
17
20
  const discreteness = (annotation?.discreteness ?? DISCRETENESS) * 1000
18
- const variables: Variable[] =
19
- [{ name: 'TOA_CADENCE_DISCRETENESS', value: String(discreteness) }]
21
+ const variables: Variable[] = [
22
+ { name: 'TOA_CADENCE_DISCRETENESS', value: String(discreteness) }
23
+ ]
20
24
 
21
25
  const service: Service = {
22
26
  group: NAMESPACE,
23
27
  name: 'metronome',
24
- version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
28
+ version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
29
+ .version,
25
30
  components: components().labels,
26
31
  variables
27
32
  }
@@ -35,18 +35,21 @@ it('should answer a component that only delays calls', () => {
35
35
  })
36
36
 
37
37
  it('should refuse an undefined operation', () => {
38
- assert.throws(() => manifest({ nothing: 3600 } as any, component),
39
- /refers to undefined operation 'nothing'/)
38
+ assert.throws(
39
+ () => manifest({ nothing: 3600 } as any, component),
40
+ /refers to undefined operation 'nothing'/
41
+ )
40
42
  })
41
43
 
42
44
  it('should refuse an operation that produces no side effects', () => {
43
- assert.throws(() => manifest({ peek: 3600 } as any, component),
44
- /of the allowed types/)
45
+ assert.throws(() => manifest({ peek: 3600 } as any, component), /of the allowed types/)
45
46
  })
46
47
 
47
48
  it('should refuse an interval shorter than a second', () => {
48
- assert.throws(() => manifest({ sweep: { cycle: 60, intervals: 61 } }, component),
49
- /less than a second/)
49
+ assert.throws(
50
+ () => manifest({ sweep: { cycle: 60, intervals: 61 } }, component),
51
+ /less than a second/
52
+ )
50
53
  })
51
54
 
52
55
  it('should refuse a cycle of no time at all', () => {