Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions resources/IterableEventQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,21 @@ export class IterableEventQueue<Event extends object = any> extends EventEmitter
return new Promise((resolve) => {
if (!this.queue || this.queue.length === 0) resolve(true);
else {
this.once('drained', () => resolve(true));
this.currentDrainResolver = resolve;
// The queue can also empty through paths that never emit 'drained' (the on('data')
// attach loop and the resolveNext bypass), so a waiter relying on the event alone
// can hang forever on an already-empty queue. Poll as a fallback wakeup.
const settle = (drained: boolean) => {
clearInterval(poll);
this.removeListener('drained', onDrained);
resolve(drained);
};
const onDrained = () => settle(true);
this.once('drained', onDrained);
this.currentDrainResolver = settle;
const poll = setInterval(() => {
if (!this.queue || this.queue.length === 0) settle(true);
}, 100);
poll.unref?.();
if (!this.drainCloseListener) {
this.drainCloseListener = true;
this.on('close', () => {
Expand Down
3 changes: 3 additions & 0 deletions resources/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ export function makeTable(options) {
static get expirationMS() {
return expirationMs;
}
static get evictionMS() {
return evictionMs;
}
static dbisDB = dbisDb;
static schemaDefined = schemaDefined;
/**
Expand Down
10 changes: 10 additions & 0 deletions unitTests/resources/IterableEventQueue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@ describe('IterableEventQueue', () => {
q.off('close', close);
assert.equal(q.hasDataListeners, true, "removing a 'close' listener leaves data state intact");
});

it('waitForDrain resolves when the on(data) attach loop empties the queue without emitting drained', async () => {
const q = new IterableEventQueue();
q.send({ n: 1 });
const drained = q.waitForDrain();
q.on('data', () => {}); // attach loop drains the buffered queue synchronously, no 'drained' emit
const result = await Promise.race([drained, new Promise((r) => setTimeout(() => r('hung'), 1000))]);
assert.equal(result, true, 'waiter must observe the queue emptying through the attach path');
assert.equal(q.listenerCount('drained'), 0, 'poll-path settle must remove the drained listener');
});
});
Loading