@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.
- package/package.json +15 -6
- package/transpiled/Dispatcher.js +5 -2
- package/transpiled/Dispatcher.js.map +1 -1
- package/transpiled/Factory.js +1 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/index.d.ts +2 -5
- package/transpiled/index.js +2 -4
- package/transpiled/index.js.map +1 -1
- package/CHANGELOG.md +0 -37
- package/schemas/annotation.cos.yaml +0 -9
- package/schemas/declaration.cos.yaml +0 -20
- package/source/Aspect.ts +0 -73
- package/source/Composition.ts +0 -47
- package/source/Dispatcher.test.ts +0 -479
- package/source/Dispatcher.ts +0 -334
- package/source/Factory.ts +0 -76
- package/source/Local.ts +0 -40
- package/source/Pulse.test.ts +0 -303
- package/source/Pulse.ts +0 -211
- package/source/const.ts +0 -31
- package/source/context.ts +0 -10
- package/source/deployment.ts +0 -38
- package/source/index.ts +0 -8
- package/source/manifest.test.ts +0 -61
- package/source/manifest.ts +0 -64
- package/source/schemas.ts +0 -10
- package/source/types.ts +0 -41
- package/transpiled/const.d.ts +0 -21
- package/transpiled/const.js +0 -26
- package/transpiled/const.js.map +0 -1
- package/transpiled/context.d.ts +0 -3
- package/transpiled/context.js +0 -9
- package/transpiled/context.js.map +0 -1
- package/transpiled/deployment.d.ts +0 -9
- package/transpiled/deployment.js +0 -29
- package/transpiled/deployment.js.map +0 -1
- package/transpiled/manifest.d.ts +0 -11
- package/transpiled/manifest.js +0 -42
- package/transpiled/manifest.js.map +0 -1
- package/transpiled/schemas.d.ts +0 -4
- package/transpiled/schemas.js +0 -7
- package/transpiled/schemas.js.map +0 -1
- package/transpiled/types.d.ts +0 -34
- package/transpiled/types.js +0 -2
- package/transpiled/types.js.map +0 -1
- package/tsconfig.json +0 -13
- 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
|
-
})
|