@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,479 +0,0 @@
1
- import { it, beforeEach, afterEach, mock } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { console } from 'openspan'
4
-
5
- import { Dispatcher } from './Dispatcher.js'
6
- import { BATCH, LANES } from './const.js'
7
- import type { Local } from './Local.js'
8
- import type { atomicity } from '@toa.io/core/types'
9
-
10
- type Invoke = (endpoint: string, request: any) => Promise<unknown>
11
- type Slots = (total: number) => number[] | null
12
- type Listener = () => void
13
-
14
- const INTERVAL = 100
15
-
16
- let metronome: Local & { invoke: ReturnType<typeof mock.fn<Invoke>> }
17
- let target: Local & { invoke: ReturnType<typeof mock.fn<Invoke>> }
18
- let atom: atomicity.Atom & { slots: ReturnType<typeof mock.fn<Slots>> }
19
- let listeners: Listener[]
20
- let rows: Row[]
21
- let warn: ReturnType<typeof mock.method<typeof console, 'warn'>>
22
-
23
- interface Row {
24
- id: string
25
- lane: number
26
- due: number
27
- expires: number
28
- endpoint: string
29
- request: object | null
30
- }
31
-
32
- /** `expires` defaults to no bound at all, which is what `overdue: null` writes */
33
- const row = (id: string, due: number, lane = 0, expires = Number.MAX_SAFE_INTEGER): Row => ({
34
- id,
35
- lane,
36
- due,
37
- expires,
38
- endpoint: 'tea.pots.transit',
39
- request: { input: { id } }
40
- })
41
-
42
- const connector = (properties: object): any => ({
43
- ...properties,
44
- link: mock.fn(),
45
- connect: async () => {},
46
- disconnect: async () => {}
47
- })
48
-
49
- const advance = async (ms: number): Promise<void> => {
50
- mock.timers.tick(ms)
51
-
52
- for (let i = 0; i < 30; i++) await Promise.resolve()
53
- }
54
-
55
- /** the query one scan made */
56
- const reads = (): any[] =>
57
- metronome.invoke.mock.calls
58
- .filter((call) => call.arguments[0] === 'enumerate')
59
- .map((call) => call.arguments[1].query)
60
-
61
- /** what has been settled, flattened */
62
- const settled = (): string[] =>
63
- metronome.invoke.mock.calls
64
- .filter((call) => call.arguments[0] === 'settle')
65
- .flatMap((call) => call.arguments[1].query.ids as string[])
66
-
67
- const range = (total: number): number[] => Array.from({ length: total }, (_, i) => i)
68
-
69
- const create = (): Dispatcher => new Dispatcher(metronome, () => target, atom)
70
-
71
- beforeEach(() => {
72
- process.env.TOA_CADENCE_DISCRETENESS = String(INTERVAL)
73
- mock.timers.enable({ apis: ['setTimeout', 'setInterval', 'Date'], now: 0 })
74
-
75
- rows = []
76
- listeners = []
77
- warn = mock.method(console, 'warn', () => undefined)
78
-
79
- metronome = connector({
80
- invoke: mock.fn<Invoke>(async (endpoint: string) =>
81
- endpoint === 'enumerate' ? rows : null
82
- )
83
- })
84
-
85
- target = connector({ invoke: mock.fn<Invoke>(async () => null) })
86
-
87
- atom = connector({
88
- slots: mock.fn<Slots>((total) => range(total)),
89
- onassigned: (listener: Listener) => {
90
- listeners.push(listener)
91
- listener()
92
-
93
- return () => {
94
- listeners = listeners.filter((one) => one !== listener)
95
- }
96
- }
97
- })
98
- })
99
-
100
- afterEach(() => {
101
- mock.timers.reset()
102
- warn.mock.restore()
103
- delete process.env.TOA_CADENCE_DISCRETENESS
104
- })
105
-
106
- it('should read two intervals ahead, in the lanes it owns', async () => {
107
- const dispatcher = create()
108
-
109
- await dispatcher.connect()
110
- await advance(INTERVAL)
111
-
112
- const [first] = reads()
113
-
114
- // the first scan is the one the subscription runs as it is added, at time zero. Two
115
- // intervals, so that consecutive passes overlap; and no bound on `expires`, because an
116
- // expired row is settled rather than left where nothing will ever read it again
117
- assert.strictEqual(
118
- first.criteria,
119
- `lane=in=(${range(LANES).join(',')});due<${2 * INTERVAL}`
120
- )
121
- assert.deepStrictEqual(first.sort, ['due:asc'])
122
- })
123
-
124
- it('should reach past the next pass, so a row on its horizon is armed before it is due', async () => {
125
- rows = [row('a', INTERVAL + 10)]
126
-
127
- const dispatcher = create()
128
-
129
- await dispatcher.connect()
130
-
131
- // gone from the store before the next pass, so nothing but the scan at time zero can have
132
- // armed it — which is what reaching two intervals rather than one is for
133
- rows = []
134
-
135
- await advance(INTERVAL)
136
-
137
- assert.strictEqual(target.invoke.mock.callCount(), 0, 'armed, not yet due')
138
-
139
- await advance(10)
140
-
141
- assert.strictEqual(target.invoke.mock.callCount(), 1, 'and called at its due time')
142
- })
143
-
144
- it('should settle an expired row without calling it', async () => {
145
- rows = [row('a', -10_000, 0, -5_000)]
146
-
147
- const dispatcher = create()
148
-
149
- await dispatcher.connect()
150
- await advance(0)
151
-
152
- assert.strictEqual(target.invoke.mock.callCount(), 0, 'past the bound its caller gave it')
153
-
154
- rows = []
155
- await advance(INTERVAL)
156
-
157
- assert.deepStrictEqual(
158
- settled(),
159
- ['a'],
160
- 'and settled, or nothing would ever read it again'
161
- )
162
- })
163
-
164
- it('should settle what it has called even while it owns nothing', async () => {
165
- rows = [row('a', 10)]
166
-
167
- const dispatcher = create()
168
-
169
- await dispatcher.connect()
170
- await advance(10)
171
-
172
- assert.strictEqual(target.invoke.mock.callCount(), 1)
173
-
174
- // the lane is somebody else's now, and a row left live is one they call again
175
- rows = []
176
- atom.slots.mock.mockImplementation(() => null)
177
-
178
- await advance(INTERVAL)
179
-
180
- assert.deepStrictEqual(settled(), ['a'])
181
- })
182
-
183
- it('should say so when a scan fills its batch', async () => {
184
- rows = Array.from({ length: BATCH }, (_, i) => row(String(i), 10_000))
185
-
186
- const dispatcher = create()
187
-
188
- await dispatcher.connect()
189
- await advance(0)
190
-
191
- const warned = warn.mock.calls.map((call) => call.arguments[0] as string)
192
-
193
- assert.ok(
194
- warned.some((message) => message.includes('filled its batch')),
195
- 'the horizon is truncated and the rest arrive late'
196
- )
197
- })
198
-
199
- it('should say so when a pass is skipped because the one before it is still running', async () => {
200
- let release: () => void = () => {}
201
-
202
- metronome.invoke.mock.mockImplementation(async (endpoint: string) => {
203
- if (endpoint !== 'enumerate') return null
204
-
205
- await new Promise<void>((resolve) => {
206
- release = resolve
207
- })
208
-
209
- return []
210
- })
211
-
212
- const dispatcher = create()
213
-
214
- await dispatcher.connect()
215
- await advance(0) // the scan at time zero, which does not come back
216
-
217
- await advance(INTERVAL)
218
-
219
- const warned = warn.mock.calls.map((call) => call.arguments[0] as string)
220
-
221
- assert.ok(
222
- warned.some((message) => message.includes('has not returned')),
223
- 'a dispatcher that stops dispatching says so'
224
- )
225
-
226
- release()
227
- await advance(0)
228
- })
229
-
230
- it('should read nothing while it owns nothing', async () => {
231
- atom.slots.mock.mockImplementation(() => null)
232
-
233
- const dispatcher = create()
234
-
235
- await dispatcher.connect()
236
- await advance(INTERVAL * 3)
237
-
238
- assert.strictEqual(reads().length, 0)
239
- })
240
-
241
- it('should call at the due time and not before', async () => {
242
- rows = [row('a', 60)]
243
-
244
- const dispatcher = create()
245
-
246
- await dispatcher.connect()
247
- await advance(1)
248
-
249
- assert.strictEqual(target.invoke.mock.callCount(), 0, 'armed, not called')
250
-
251
- await advance(60)
252
-
253
- assert.strictEqual(target.invoke.mock.callCount(), 1)
254
-
255
- const [endpoint, request] = target.invoke.mock.calls[0].arguments
256
-
257
- assert.strictEqual(endpoint, 'transit')
258
- assert.deepStrictEqual(request, { input: { id: 'a' }, task: true })
259
- })
260
-
261
- it('should call one whose time has already passed at once', async () => {
262
- rows = [row('a', -10_000)]
263
-
264
- const dispatcher = create()
265
-
266
- await dispatcher.connect()
267
- await advance(0)
268
-
269
- assert.strictEqual(
270
- target.invoke.mock.callCount(),
271
- 1,
272
- 'a scan reads what is overdue along with what is coming'
273
- )
274
- })
275
-
276
- it('should arm a row once, however often a scan reads it', async () => {
277
- rows = [row('a', 10_000)]
278
-
279
- const dispatcher = create()
280
-
281
- await dispatcher.connect()
282
-
283
- // one interval at a time: a scan is one at a time, and a single jump would find the first
284
- // still running and skip the rest
285
- for (let cycle = 0; cycle < 4; cycle++) await advance(INTERVAL)
286
-
287
- assert.ok(reads().length >= 3, 'it kept scanning')
288
- assert.strictEqual(target.invoke.mock.callCount(), 0, 'and armed it only once')
289
- })
290
-
291
- it('should not arm a row again while its call is in flight', async () => {
292
- let release: () => void = () => {}
293
-
294
- rows = [row('a', 10)]
295
- target.invoke.mock.mockImplementation(
296
- async () =>
297
- await new Promise<void>((resolve) => {
298
- release = resolve
299
- })
300
- )
301
-
302
- const dispatcher = create()
303
-
304
- await dispatcher.connect()
305
- await advance(10) // the call goes out, and does not come back
306
-
307
- assert.strictEqual(target.invoke.mock.callCount(), 1)
308
-
309
- // a scan while it is in flight reads the row again, and settles what it thinks was called
310
- await advance(INTERVAL)
311
-
312
- // far enough for anything that scan armed to have fired
313
- await advance(INTERVAL)
314
-
315
- assert.strictEqual(
316
- target.invoke.mock.callCount(),
317
- 1,
318
- 'and does not send it a second time'
319
- )
320
-
321
- release()
322
- await advance(0)
323
- })
324
-
325
- it('should settle what it has called', async () => {
326
- rows = [row('a', 10)]
327
-
328
- const dispatcher = create()
329
-
330
- await dispatcher.connect()
331
- await advance(10)
332
-
333
- assert.strictEqual(target.invoke.mock.callCount(), 1)
334
-
335
- rows = []
336
- await advance(INTERVAL)
337
-
338
- assert.deepStrictEqual(settled(), ['a'])
339
- })
340
-
341
- it('should settle a row whose call failed, and keep dispatching', async () => {
342
- rows = [row('a', 10), row('b', 20)]
343
- target.invoke.mock.mockImplementationOnce(async () => {
344
- throw new Error('nope')
345
- })
346
-
347
- const dispatcher = create()
348
-
349
- await dispatcher.connect()
350
- await advance(20)
351
-
352
- assert.strictEqual(
353
- target.invoke.mock.callCount(),
354
- 2,
355
- 'the one after it is still called'
356
- )
357
-
358
- rows = []
359
- await advance(INTERVAL)
360
-
361
- assert.deepStrictEqual(
362
- settled().sort(),
363
- ['a', 'b'],
364
- 'and the one that failed is settled'
365
- )
366
- })
367
-
368
- it('should drop timers for lanes it no longer owns', async () => {
369
- rows = [row('a', 10_000, 7)]
370
-
371
- const dispatcher = create()
372
-
373
- await dispatcher.connect()
374
- await advance(INTERVAL)
375
-
376
- rows = []
377
- atom.slots.mock.mockImplementation(() => [0])
378
-
379
- for (const listener of listeners) listener()
380
-
381
- await advance(20_000)
382
-
383
- assert.strictEqual(target.invoke.mock.callCount(), 0, "lane 7 is somebody else's now")
384
- })
385
-
386
- it('should drop everything it holds when it owns nothing', async () => {
387
- rows = [row('a', 10_000, 7)]
388
-
389
- const dispatcher = create()
390
-
391
- await dispatcher.connect()
392
- await advance(INTERVAL)
393
-
394
- rows = []
395
- atom.slots.mock.mockImplementation(() => null)
396
-
397
- for (const listener of listeners) listener()
398
-
399
- await advance(20_000)
400
-
401
- assert.strictEqual(
402
- target.invoke.mock.callCount(),
403
- 0,
404
- 'whoever holds lane 7 now is the one to make that call'
405
- )
406
- })
407
-
408
- it('should scan at once when what it owns changes', async () => {
409
- atom.slots.mock.mockImplementation(() => null)
410
-
411
- const dispatcher = create()
412
-
413
- await dispatcher.connect()
414
- await advance(0)
415
-
416
- assert.strictEqual(reads().length, 0)
417
-
418
- atom.slots.mock.mockImplementation((total) => range(total))
419
-
420
- for (const listener of listeners) listener()
421
-
422
- await advance(0)
423
-
424
- assert.strictEqual(reads().length, 1, 'without waiting out an interval')
425
- })
426
-
427
- it('should give up a row a scan stops reading, so a cancellation still lands', async () => {
428
- rows = [row('a', INTERVAL + 50)]
429
-
430
- const dispatcher = create()
431
-
432
- await dispatcher.connect()
433
- await advance(1)
434
-
435
- // cancelled: the row is tombstoned, so the next scan does not read it
436
- rows = []
437
-
438
- await advance(INTERVAL) // the scan that finds it gone
439
- await advance(INTERVAL) // and past when it would have been called
440
-
441
- assert.strictEqual(target.invoke.mock.callCount(), 0, 'the timer was given up with the row')
442
- })
443
-
444
- it('should keep a row the scan still reads', async () => {
445
- rows = [row('a', INTERVAL + 50)]
446
-
447
- const dispatcher = create()
448
-
449
- await dispatcher.connect()
450
- await advance(INTERVAL)
451
-
452
- assert.strictEqual(target.invoke.mock.callCount(), 0, 'not yet due')
453
-
454
- await advance(50)
455
-
456
- assert.strictEqual(target.invoke.mock.callCount(), 1)
457
- })
458
-
459
- it('should not give up a row when the batch was filled', async () => {
460
- const held = row('held', INTERVAL + 50)
461
-
462
- rows = [held, ...Array.from({ length: BATCH - 1 }, (_, i) => row('b' + i, 10_000))]
463
-
464
- const dispatcher = create()
465
-
466
- await dispatcher.connect()
467
- await advance(1)
468
-
469
- // a full batch is a read that was cut short, so absence from the next one is not a
470
- // cancellation — these are rows it never reached, not rows nobody owes
471
- rows = Array.from({ length: BATCH }, (_, i) => row('c' + i, 20_000))
472
-
473
- await advance(INTERVAL)
474
- await advance(50)
475
-
476
- const called = target.invoke.mock.calls.map((call) => call.arguments[1].input.id)
477
-
478
- assert.deepStrictEqual(called, ['held'], 'the row it armed is still called')
479
- })