@toa.io/extensions.cadence 1.0.0-alpha.291 → 1.0.0-alpha.293

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 (47) hide show
  1. package/package.json +15 -6
  2. package/transpiled/Dispatcher.js +5 -2
  3. package/transpiled/Dispatcher.js.map +1 -1
  4. package/transpiled/Factory.js +1 -1
  5. package/transpiled/Factory.js.map +1 -1
  6. package/transpiled/index.d.ts +2 -5
  7. package/transpiled/index.js +2 -4
  8. package/transpiled/index.js.map +1 -1
  9. package/CHANGELOG.md +0 -37
  10. package/schemas/annotation.cos.yaml +0 -9
  11. package/schemas/declaration.cos.yaml +0 -20
  12. package/source/Aspect.ts +0 -73
  13. package/source/Composition.ts +0 -47
  14. package/source/Dispatcher.test.ts +0 -479
  15. package/source/Dispatcher.ts +0 -334
  16. package/source/Factory.ts +0 -76
  17. package/source/Local.ts +0 -40
  18. package/source/Pulse.test.ts +0 -303
  19. package/source/Pulse.ts +0 -211
  20. package/source/const.ts +0 -31
  21. package/source/context.ts +0 -10
  22. package/source/deployment.ts +0 -38
  23. package/source/index.ts +0 -8
  24. package/source/manifest.test.ts +0 -61
  25. package/source/manifest.ts +0 -64
  26. package/source/schemas.ts +0 -10
  27. package/source/types.ts +0 -41
  28. package/transpiled/const.d.ts +0 -21
  29. package/transpiled/const.js +0 -26
  30. package/transpiled/const.js.map +0 -1
  31. package/transpiled/context.d.ts +0 -3
  32. package/transpiled/context.js +0 -9
  33. package/transpiled/context.js.map +0 -1
  34. package/transpiled/deployment.d.ts +0 -9
  35. package/transpiled/deployment.js +0 -29
  36. package/transpiled/deployment.js.map +0 -1
  37. package/transpiled/manifest.d.ts +0 -11
  38. package/transpiled/manifest.js +0 -42
  39. package/transpiled/manifest.js.map +0 -1
  40. package/transpiled/schemas.d.ts +0 -4
  41. package/transpiled/schemas.js +0 -7
  42. package/transpiled/schemas.js.map +0 -1
  43. package/transpiled/types.d.ts +0 -34
  44. package/transpiled/types.js +0 -2
  45. package/transpiled/types.js.map +0 -1
  46. package/tsconfig.json +0 -13
  47. package/tsconfig.tsbuildinfo +0 -1
@@ -1,303 +0,0 @@
1
- import { it, beforeEach, afterEach, mock } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { Pulse } from './Pulse.js'
5
- import type { Local } from './Local.js'
6
- import type { Locator } from '@toa.io/core'
7
- import type { Request, atomicity } from '@toa.io/core/types'
8
-
9
- type Invoke = (endpoint: string, request: Request) => Promise<unknown>
10
- type Slots = (total: number) => number[] | null
11
-
12
- let local: Local & { invoke: ReturnType<typeof mock.fn<Invoke>> }
13
- let atom: atomicity.Atom & { slots: ReturnType<typeof mock.fn<Slots>> }
14
-
15
- const SECOND = 1000
16
- const HOUR = 3600
17
- const DAY = 24 * HOUR
18
-
19
- const locator = { id: 'tea.pots' } as Locator
20
-
21
- const range = (total: number): number[] =>
22
- Array.from({ length: total }, (_, index) => index)
23
-
24
- /** what a Pulse reaches for, with a lifecycle it can connect through */
25
- const connector = (properties: object): any => ({
26
- ...properties,
27
- link: mock.fn(),
28
- connect: async () => {},
29
- disconnect: async () => {}
30
- })
31
-
32
- const create = (cycle: number, intervals: number): Pulse =>
33
- new Pulse({ locator, endpoint: 'sweep', cycle, intervals }, local, atom)
34
-
35
- /** the timers the tick arms are the only ones, so a tick is a jump and its microtasks */
36
- const advance = async (ms: number): Promise<void> => {
37
- mock.timers.tick(ms)
38
-
39
- for (let i = 0; i < 20; i++) await Promise.resolve()
40
- }
41
-
42
- /** which interval each call was told it was for */
43
- const intervals = (): number[] =>
44
- local.invoke.mock.calls.map((call) => (call.arguments[1].input as { i: number }).i)
45
-
46
- beforeEach(() => {
47
- mock.timers.enable({ apis: ['setTimeout', 'Date'], now: 0 })
48
-
49
- local = connector({ invoke: mock.fn<Invoke>(async () => undefined) })
50
-
51
- // one replica of one: every slot of the cycle is its own
52
- atom = connector({ slots: mock.fn<Slots>((total) => range(total)) })
53
- })
54
-
55
- afterEach(() => {
56
- mock.timers.reset()
57
- })
58
-
59
- it('should split a cycle into intervals', () => {
60
- const pulse = create(DAY, 24)
61
-
62
- assert.strictEqual(pulse.index(0), 0)
63
- assert.strictEqual(pulse.index(HOUR * SECOND - 1), 0)
64
- assert.strictEqual(pulse.index(HOUR * SECOND), 1)
65
- assert.strictEqual(pulse.index(23 * HOUR * SECOND), 23)
66
- assert.strictEqual(pulse.index(DAY * SECOND), 0)
67
- })
68
-
69
- it('should give every interval of a cycle that does not divide evenly', () => {
70
- const pulse = create(DAY, 7)
71
- const seen = new Set<number>()
72
-
73
- let now = 0
74
-
75
- for (let step = 0; step < 7; step++) {
76
- seen.add(pulse.index(now))
77
- now = pulse.boundary(now)
78
- }
79
-
80
- assert.deepStrictEqual(
81
- [...seen].sort((a, b) => a - b),
82
- [0, 1, 2, 3, 4, 5, 6]
83
- )
84
- assert.strictEqual(now, DAY * SECOND, 'seven boundaries land exactly on the next cycle')
85
- })
86
-
87
- it('should not drift across cycles', () => {
88
- const pulse = create(DAY, 7)
89
-
90
- let now = 0
91
-
92
- for (let step = 0; step < 7 * 100; step++) now = pulse.boundary(now)
93
-
94
- assert.strictEqual(now, 100 * DAY * SECOND)
95
- })
96
-
97
- it('should treat the interval it starts in as already handled', async () => {
98
- const pulse = create(DAY, 24)
99
-
100
- mock.timers.setTime(30 * 60 * SECOND) // half past the first hour
101
-
102
- await pulse.connect()
103
- await advance(0)
104
-
105
- assert.strictEqual(local.invoke.mock.callCount(), 0)
106
- })
107
-
108
- it('should call at the boundary', async () => {
109
- const pulse = create(DAY, 24)
110
-
111
- mock.timers.setTime(30 * 60 * SECOND)
112
-
113
- await pulse.connect()
114
-
115
- await advance(30 * 60 * SECOND - 1)
116
- assert.strictEqual(local.invoke.mock.callCount(), 0, 'not a millisecond early')
117
-
118
- await advance(1)
119
- assert.strictEqual(local.invoke.mock.callCount(), 1)
120
- })
121
-
122
- it('should tell the operation which interval it is', async () => {
123
- const pulse = create(DAY, 24)
124
-
125
- await pulse.connect()
126
- await advance(HOUR * SECOND)
127
-
128
- const [endpoint, request] = local.invoke.mock.calls[0].arguments
129
-
130
- assert.strictEqual(endpoint, 'sweep')
131
- assert.deepStrictEqual(request, { input: { n: 24, i: 1 } })
132
- })
133
-
134
- it('should call once per interval, and every interval of a cycle', async () => {
135
- const pulse = create(DAY, 24)
136
-
137
- await pulse.connect()
138
-
139
- for (let hour = 0; hour < 24; hour++) await advance(HOUR * SECOND)
140
-
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
- )
145
- })
146
-
147
- it('should keep calling a cycle it is not splitting', async () => {
148
- const pulse = create(HOUR, 1)
149
-
150
- await pulse.connect()
151
-
152
- for (let cycle = 0; cycle < 3; cycle++) await advance(HOUR * SECOND)
153
-
154
- assert.deepStrictEqual(intervals(), [0, 0, 0])
155
- })
156
-
157
- it('should not call while this replica owns nothing', async () => {
158
- atom.slots.mock.mockImplementation(() => null)
159
-
160
- const pulse = create(DAY, 24)
161
-
162
- await pulse.connect()
163
- await advance(HOUR * SECOND)
164
-
165
- assert.strictEqual(local.invoke.mock.callCount(), 0)
166
- })
167
-
168
- it('should not call an interval it acquired after the boundary', async () => {
169
- atom.slots.mock.mockImplementation(() => [])
170
-
171
- const pulse = create(DAY, 24)
172
-
173
- await pulse.connect()
174
- await advance(HOUR * SECOND) // the boundary passes, and the interval is not this one's
175
-
176
- atom.slots.mock.mockImplementation((total) => range(total))
177
-
178
- await advance(30 * 60 * SECOND) // and half an hour later it is
179
-
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
- )
185
- })
186
-
187
- it('should not call an interval another replica owns', async () => {
188
- atom.slots.mock.mockImplementation((total) =>
189
- range(total).filter((slot) => slot % 2 === 0)
190
- )
191
-
192
- const pulse = create(DAY, 24)
193
-
194
- await pulse.connect()
195
-
196
- for (let hour = 0; hour < 24; hour++) await advance(HOUR * SECOND)
197
-
198
- const called = intervals()
199
-
200
- assert.strictEqual(called.length, 12)
201
- assert.ok(called.every((i) => i % 2 === 0))
202
- })
203
-
204
- it('should not start a call while the one before has not returned', async () => {
205
- let release: () => void = () => {}
206
-
207
- local.invoke.mock.mockImplementation(
208
- async () =>
209
- await new Promise<void>((resolve) => {
210
- release = resolve
211
- })
212
- )
213
-
214
- const pulse = create(DAY, 24)
215
-
216
- await pulse.connect()
217
-
218
- await advance(HOUR * SECOND)
219
- await advance(HOUR * SECOND)
220
-
221
- assert.strictEqual(local.invoke.mock.callCount(), 1)
222
-
223
- release()
224
- await advance(0) // the call settles, and only then is the pulse free again
225
-
226
- await advance(HOUR * SECOND)
227
-
228
- assert.strictEqual(local.invoke.mock.callCount(), 2)
229
- })
230
-
231
- it('should keep its cadence when a call raises', async () => {
232
- local.invoke.mock.mockImplementationOnce(async () => {
233
- throw new Error('nope')
234
- })
235
-
236
- const pulse = create(DAY, 24)
237
-
238
- await pulse.connect()
239
-
240
- await advance(HOUR * SECOND)
241
- await advance(HOUR * SECOND)
242
-
243
- assert.strictEqual(
244
- local.invoke.mock.callCount(),
245
- 2,
246
- 'the interval that raised is skipped, and the next one is called'
247
- )
248
- })
249
-
250
- it('should wait out a cycle longer than one timer in instalments', async () => {
251
- const YEAR = 365 * DAY
252
-
253
- const pulse = create(YEAR, 1)
254
-
255
- await pulse.connect()
256
-
257
- // more than `setTimeout` takes at once, and nothing is due yet
258
- await advance(2 ** 31)
259
- assert.strictEqual(local.invoke.mock.callCount(), 0)
260
-
261
- await advance(YEAR * SECOND)
262
- assert.strictEqual(local.invoke.mock.callCount(), 1)
263
- })
264
-
265
- it('should wait for a call in flight before closing', async () => {
266
- let release: () => void = () => {}
267
- let closed = false
268
-
269
- local.invoke.mock.mockImplementation(
270
- async () =>
271
- await new Promise<void>((resolve) => {
272
- release = resolve
273
- })
274
- )
275
-
276
- const pulse = create(DAY, 24)
277
-
278
- await pulse.connect()
279
- await advance(HOUR * SECOND)
280
-
281
- const closing = pulse.disconnect().then(() => {
282
- closed = true
283
- })
284
-
285
- await advance(0)
286
- assert.strictEqual(closed, false, 'the call is still running')
287
-
288
- release()
289
- await advance(0)
290
- await closing
291
-
292
- assert.strictEqual(closed, true)
293
- })
294
-
295
- it('should stop calling once closed', async () => {
296
- const pulse = create(DAY, 24)
297
-
298
- await pulse.connect()
299
- await pulse.disconnect()
300
- await advance(HOUR * SECOND)
301
-
302
- assert.strictEqual(local.invoke.mock.callCount(), 0)
303
- })
package/source/Pulse.ts DELETED
@@ -1,211 +0,0 @@
1
- import { console } from 'openspan'
2
- import { Connector } from '@toa.io/core'
3
- import type { Local } from './Local.js'
4
- import type { SpanOptions } from 'openspan'
5
- import type { Locator } from '@toa.io/core'
6
- import type { atomicity } from '@toa.io/core/types'
7
-
8
- /**
9
- * A recurring call to an operation of its own component, on the cadence its manifest states.
10
- *
11
- * Nothing is stored. The cycle is split into `n` intervals and the current one is a function of
12
- * the clock, so every replica computes the same `i` without talking to any other; the one that
13
- * owns `i` is the one that calls. An interval no replica was alive for is not made up
14
- * afterwards — the operation is expected to select by what is still due rather than by where
15
- * the cycle has reached.
16
- */
17
- export class Pulse extends Connector {
18
- private readonly endpoint: string
19
- private readonly label: string
20
-
21
- /** milliseconds one whole cycle takes */
22
- private readonly cycle: number
23
-
24
- /** intervals the cycle is split into */
25
- private readonly n: number
26
-
27
- private readonly local: Local
28
- private readonly atom: atomicity.Atom
29
-
30
- /** span options, built once */
31
- private readonly options: SpanOptions
32
-
33
- /**
34
- * The ordinal of the interval this process has already handled, so an early wake-up is not a
35
- * second call. An ordinal and not an index: with one interval per cycle the index never
36
- * changes, and comparing it would suppress every call after the first.
37
- */
38
- private fired = 0
39
-
40
- private timer?: NodeJS.Timeout
41
-
42
- /** the call in flight, awaited on close */
43
- private firing?: Promise<unknown>
44
-
45
- private closing = false
46
-
47
- public constructor(definition: Definition, local: Local, atom: atomicity.Atom) {
48
- super()
49
-
50
- const { locator, endpoint, cycle, intervals } = definition
51
-
52
- this.endpoint = endpoint
53
- this.label = `${locator.id}.${endpoint}`
54
- this.cycle = cycle * 1000
55
- this.n = intervals
56
-
57
- this.local = local
58
- this.atom = atom
59
-
60
- this.depends(local)
61
- this.depends(atom)
62
-
63
- this.options = {
64
- name: `${this.label} pulse`,
65
- kind: 'producer',
66
- service: locator.id
67
- }
68
- }
69
-
70
- /**
71
- * How many intervals have passed since the epoch, `now` included. Against the cycle rather
72
- * than by adding up gaps, so a cycle that does not divide evenly into its intervals neither
73
- * drifts nor accumulates error.
74
- *
75
- * The arithmetic is exact rather than double: `now` is milliseconds since 1970 and `n` may be
76
- * as large as the cycle has seconds, so their product leaves the range a double is exact in
77
- * for a long cycle cut finely. It is evaluated once per interval, where this costs nothing.
78
- */
79
- public ordinal(now: number): number {
80
- return Number((BigInt(now) * BigInt(this.n)) / BigInt(this.cycle))
81
- }
82
-
83
- /** Which of the cycle's intervals `now` falls in. */
84
- public index(now: number): number {
85
- return this.ordinal(now) % this.n
86
- }
87
-
88
- /** When the interval `now` falls in gives way to the next one. */
89
- public boundary(now: number): number {
90
- const cycle = BigInt(this.cycle)
91
- const n = BigInt(this.n)
92
- const next = BigInt(this.ordinal(now) + 1)
93
-
94
- // the earliest millisecond whose ordinal is the next one
95
- return Number((next * cycle + n - 1n) / n)
96
- }
97
-
98
- protected override async open(): Promise<void> {
99
- // whatever interval it is now, this replica was not there when it began, so it is not
100
- // this one's to call — the first call is at the next boundary
101
- this.fired = this.ordinal(Date.now())
102
-
103
- this.arm()
104
- }
105
-
106
- protected override async close(): Promise<void> {
107
- this.closing = true
108
-
109
- clearTimeout(this.timer)
110
-
111
- // a call still running holds the remote it was made through, which is torn down after
112
- // this returns. Bounded, because an operation that never returns would outlive any
113
- // grace period and the interval is over either way
114
- if (this.firing !== undefined) await Promise.race([this.firing, delay(DRAIN)])
115
- }
116
-
117
- private arm(): void {
118
- const now = Date.now()
119
-
120
- // a delay past this fires at once rather than late, so a long cycle is waited out in
121
- // instalments; the wake-up finds the same interval and simply arms again
122
- const wait = Math.min(this.boundary(now) - now, MAX_DELAY)
123
-
124
- this.timer = setTimeout(() => this.tick(), wait)
125
- this.timer.unref()
126
- }
127
-
128
- private tick(): void {
129
- if (this.closing) return
130
-
131
- const ordinal = this.ordinal(Date.now())
132
-
133
- // a timer may wake a millisecond before the boundary it was set for, and a cycle longer
134
- // than one timer wakes several times on the way — both are the interval already handled
135
- if (ordinal !== this.fired) {
136
- this.fired = ordinal
137
-
138
- void this.fire(ordinal % this.n)
139
- }
140
-
141
- this.arm()
142
- }
143
-
144
- /**
145
- * Nothing queues here. A call still running when the next boundary arrives means the work
146
- * does not fit the gap, and starting a second one would only make that worse.
147
- */
148
- private async fire(i: number): Promise<void> {
149
- if (this.firing !== undefined) {
150
- console.warn('Pulse skipped: the previous call has not returned', {
151
- pulse: this.label,
152
- interval: i
153
- })
154
-
155
- return
156
- }
157
-
158
- const owned = this.atom.slots(this.n)
159
-
160
- if (owned === null) {
161
- console.warn('Pulse skipped: this replica owns nothing', {
162
- pulse: this.label,
163
- interval: i
164
- })
165
-
166
- return
167
- }
168
-
169
- if (!owned.includes(i)) return
170
-
171
- /*
172
- * A pulse calls and waits, so an operation that raised comes back as an exception here —
173
- * the runtime carries it across and rethrows it in this process. What to do about one is a
174
- * choice with no right answer, and asking would put the whole of it in front of anyone who
175
- * only wanted an operation called every hour. So the interval is skipped: it is reported,
176
- * and the next one is called as though it had not happened.
177
- */
178
- const firing = console
179
- .span(
180
- this.options,
181
- async () => await this.local.invoke(this.endpoint, { input: { n: this.n, i } })
182
- )
183
- .catch((error: unknown) => {
184
- console.error('Pulse failed', { pulse: this.label, interval: i, error })
185
- })
186
- .finally(() => {
187
- this.firing = undefined
188
- })
189
-
190
- this.firing = firing
191
-
192
- await firing
193
- }
194
- }
195
-
196
- export interface Definition {
197
- locator: Locator
198
- endpoint: string
199
- cycle: number
200
- intervals: number
201
- }
202
-
203
- async function delay(ms: number): Promise<void> {
204
- await new Promise((resolve) => setTimeout(resolve, ms).unref())
205
- }
206
-
207
- /** `setTimeout` fires immediately past this, so a longer wait is taken in instalments */
208
- const MAX_DELAY = 2 ** 31 - 1
209
-
210
- /** how long a call in flight is waited for on close */
211
- const DRAIN = 10_000
package/source/const.ts DELETED
@@ -1,31 +0,0 @@
1
- import { environment } from '@toa.io/generic'
2
-
3
- /** The component the extension ships to hold delayed calls. */
4
- export const NAMESPACE = 'cadence'
5
- export const COMPONENT = 'metronome'
6
-
7
- /**
8
- * Constant, never configuration: a row carries its lane, so lowering this would leave rows in
9
- * lanes nobody reads any more. It is also the ceiling on replicas dispatching, and a power of
10
- * two so that the common replica counts divide evenly. The outbox says the same of its own.
11
- *
12
- * A scan asks for its lanes as a set and sorts on `due`, which is the index's second key, so
13
- * the plan that serves it explodes the set into one scan per lane and merges them in order.
14
- * MongoDB stops exploding past `internalQueryMaxScansToExplode`, 200 by default, and falls
15
- * back to sorting the whole result in memory — so this staying well under that is not
16
- * incidental.
17
- */
18
- export const LANES = 128
19
-
20
- /** seconds between passes over the calls waiting to be made, and how far ahead each reaches */
21
- export const DISCRETENESS = 60
22
-
23
- /** rows one scan brings back */
24
- export const BATCH = 200
25
-
26
- /** A number from the environment, where a deployment or a test suite states one. */
27
- export function number(variable: string, fallback: number): number {
28
- const value = Number(environment.get(variable))
29
-
30
- return Number.isNaN(value) || value <= 0 ? fallback : value
31
- }
package/source/context.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { Contribution } from '@toa.io/core/types'
2
-
3
- /** What this extension puts on the context of a component that declares it. */
4
- export function context(): Contribution {
5
- return {
6
- name: 'delay',
7
- type: 'Delay',
8
- imports: { '@toa.io/extensions.cadence': ['Delay'] }
9
- }
10
- }
@@ -1,38 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { components } from './Composition.js'
3
- import { DISCRETENESS, NAMESPACE } from './const.js'
4
- import * as schemas from './schemas.js'
5
- import type { Annotation } from './types.js'
6
- import type { Dependency, Service, Variable } from '@toa.io/operations'
7
-
8
- /**
9
- * The component that keeps delayed calls runs in a service of its own, the way the identity
10
- * components run in the gateway. A composition may claim it and run it in its own pods instead.
11
- */
12
- export function deployment(_: unknown, annotation?: Annotation | null): Dependency {
13
- schemas.annotation.validate<Annotation | null>(
14
- annotation ?? null,
15
- 'Invalid cadence annotation'
16
- )
17
-
18
- // stated in seconds and carried in milliseconds, so that what an application writes is what
19
- // the rest of a manifest is written in
20
- const discreteness = (annotation?.discreteness ?? DISCRETENESS) * 1000
21
- const variables: Variable[] = [
22
- { name: 'TOA_CADENCE_DISCRETENESS', value: String(discreteness) }
23
- ]
24
-
25
- const service: Service = {
26
- group: NAMESPACE,
27
- name: 'metronome',
28
- version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
29
- .version,
30
- components: components().labels,
31
- variables
32
- }
33
-
34
- return { services: [service] }
35
- }
36
-
37
- /** The extension is deployed for the sake of `delay`, which nothing has to declare to use. */
38
- export const standalone = true
package/source/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export { Factory } from './Factory.js'
2
- export { manifest } from './manifest.js'
3
- export { components } from './Composition.js'
4
- export { deployment, standalone } from './deployment.js'
5
- export { context } from './context.js'
6
- export { LANES } from './const.js'
7
-
8
- export type { Declaration, Delay, Pulse } from './types.js'
@@ -1,61 +0,0 @@
1
- import { it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { manifest } from './manifest.js'
5
- import type { Manifest } from '@toa.io/norm'
6
-
7
- const component = {
8
- operations: {
9
- sweep: { type: 'effect' },
10
- gather: { type: 'transition' },
11
- peek: { type: 'observation' }
12
- }
13
- } as unknown as Manifest
14
-
15
- it('should normalize a declaration', () => {
16
- const declaration = manifest({ sweep: { cycle: 86400, intervals: 24 } }, component)
17
-
18
- assert.deepStrictEqual(declaration, { sweep: { cycle: 86400, intervals: 24 } })
19
- })
20
-
21
- it('should split a cycle into one interval by default', () => {
22
- const declaration = manifest({ sweep: { cycle: 3600 } } as any, component)
23
-
24
- assert.deepStrictEqual(declaration, { sweep: { cycle: 3600, intervals: 1 } })
25
- })
26
-
27
- it('should expand the shorthand', () => {
28
- const declaration = manifest({ sweep: 3600 } as any, component)
29
-
30
- assert.deepStrictEqual(declaration, { sweep: { cycle: 3600, intervals: 1 } })
31
- })
32
-
33
- it('should answer a component that only delays calls', () => {
34
- assert.deepStrictEqual(manifest(null, component), {})
35
- })
36
-
37
- it('should refuse an undefined operation', () => {
38
- assert.throws(
39
- () => manifest({ nothing: 3600 } as any, component),
40
- /refers to undefined operation 'nothing'/
41
- )
42
- })
43
-
44
- it('should refuse an operation that produces no side effects', () => {
45
- assert.throws(() => manifest({ peek: 3600 } as any, component), /of the allowed types/)
46
- })
47
-
48
- it('should refuse an interval shorter than a second', () => {
49
- assert.throws(
50
- () => manifest({ sweep: { cycle: 60, intervals: 61 } }, component),
51
- /less than a second/
52
- )
53
- })
54
-
55
- it('should refuse a cycle of no time at all', () => {
56
- assert.throws(() => manifest({ sweep: { cycle: 0, intervals: 1 } }, component))
57
- })
58
-
59
- it('should refuse what the declaration does not describe', () => {
60
- assert.throws(() => manifest({ sweep: { cycle: 60, every: 3 } } as any, component))
61
- })