Skip to content

Commit 6cfd134

Browse files
committed
Buffer emissions for async iterable integrity
1 parent 4997384 commit 6cfd134

2 files changed

Lines changed: 99 additions & 3 deletions

File tree

packages/polling/src/poll.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,21 @@ export class Poll<T = any, U = any, V extends string = 'standby'>
142142
* Return an async iterator that yields every tick.
143143
*/
144144
async *[Symbol.asyncIterator](): AsyncIterableIterator<IPoll.State<T, U, V>> {
145-
while (!this.isDisposed) {
146-
yield this.state;
147-
await this.tick.catch(() => undefined);
145+
const queue: IPoll.State<T, U, V>[] = [this.state];
146+
const enqueue = (_: unknown, state: IPoll.State<T, U, V>) => {
147+
queue.push(state);
148+
};
149+
this.ticked.connect(enqueue);
150+
try {
151+
while (!this.isDisposed) {
152+
if (queue.length) {
153+
yield queue.shift()!;
154+
} else {
155+
await this.tick.catch(() => undefined);
156+
}
157+
}
158+
} finally {
159+
this.ticked.disconnect(enqueue);
148160
}
149161
}
150162

packages/polling/tests/src/poll.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,90 @@ describe('Poll', () => {
296296
}
297297
expect(ticker.join(' ')).to.equal(expected);
298298
});
299+
300+
it('should not drop rapid schedule calls', async () => {
301+
poll = new Poll<number>({
302+
auto: false,
303+
factory: () => Promise.resolve(0),
304+
frequency: { interval: Poll.NEVER },
305+
name: '@lumino/polling:Poll#[Symbol.asyncIterator]-4'
306+
});
307+
void poll.start();
308+
await poll.tick;
309+
310+
const payloads = [1, 2, 3, 4, 5];
311+
const collected: number[] = [];
312+
const consumption = (async () => {
313+
for await (const state of poll) {
314+
if (
315+
state.phase === 'resolved' &&
316+
typeof state.payload === 'number' &&
317+
state.payload > 0
318+
) {
319+
collected.push(state.payload as number);
320+
}
321+
if (collected.length === payloads.length) {
322+
break;
323+
}
324+
}
325+
})();
326+
327+
// Rapidly schedule 5 payloads synchronously.
328+
for (const payload of payloads) {
329+
void poll.schedule({ payload, phase: 'resolved' });
330+
}
331+
332+
await consumption;
333+
expect(collected).to.deep.equal(payloads);
334+
});
335+
336+
it('should stop yielding after disposal', async () => {
337+
poll = new Poll({
338+
auto: false,
339+
factory: () => Promise.resolve(),
340+
frequency: { interval: Poll.NEVER },
341+
name: '@lumino/polling:Poll#[Symbol.asyncIterator]-5'
342+
});
343+
void poll.start();
344+
await poll.tick;
345+
346+
// Schedule several ticks then dispose immediately.
347+
void poll.schedule({ payload: 'a', phase: 'resolved' });
348+
void poll.schedule({ payload: 'b', phase: 'resolved' });
349+
void poll.schedule({ payload: 'c', phase: 'resolved' });
350+
poll.dispose();
351+
352+
const collected: string[] = [];
353+
for await (const state of poll) {
354+
collected.push(state.phase);
355+
}
356+
// Iterator exits immediately — no states yielded from a disposed poll.
357+
expect(collected).to.deep.equal([]);
358+
});
359+
360+
it('should work identically for one-tick-at-a-time polling', async () => {
361+
const total = 5;
362+
let i = 0;
363+
poll = new Poll({
364+
auto: false,
365+
factory: async () => ++i,
366+
frequency: { interval: Poll.IMMEDIATE },
367+
name: '@lumino/polling:Poll#[Symbol.asyncIterator]-6'
368+
});
369+
const ticker: IPoll.Phase<any>[] = [];
370+
void poll.start();
371+
for await (const state of poll) {
372+
ticker.push(state.phase);
373+
if (i >= total) {
374+
poll.dispose();
375+
break;
376+
}
377+
}
378+
// Should see started, then resolved for each factory call.
379+
expect(ticker[0]).to.equal('started');
380+
expect(ticker.slice(1).every(p => p === 'resolved')).to.equal(true);
381+
expect(ticker.length).to.be.greaterThan(1);
382+
});
299383
});
300384

301385
describe('#tick', () => {

0 commit comments

Comments
 (0)