0

I am newbie to Javascript and been reading the David's Flanagan Guide to have general overview.

This code snippet made me confused. I could not realise how the Promise in loop is 'dequeued' before being 'enqueued'?

First we put all promises into AsyncQueue():

function eventStream(elt, type) {
  const q = new AsyncQueue(); // Create a queue
  elt.addEventListener(type, (e) => q.enqueue(e)); // Enqueue events
  return q;
}

Then we iterate it's values:

async function handleKeys() 
// Get a stream of keypress events and loop once for each one
  for await (const event of eventStream(document, "keypress")) {
    console.log(event.key);
  }
}

So I wonder how 'event' in 'for await' loop could have no event yet in queue? Didn't we put it before?

'For await' loop 'dequeues' 'Promise' before it is in our 'Queue'. But how? Doesn't 'for await' operator wait until next Promised is resolved?

Maybe queue - eventStream(document, "keypress") - dinamically changes? Sorry, I feel confused.

Whole code snippet:

/**
 * An asynchronously iterable queue class. Add values with enqueue()
 * and remove them with dequeue(). dequeue() returns a Promise, which
 * means that values can be dequeued before they are enqueued. The
 * class implements [Symbol.asyncIterator] and next() so that it can
 * be used with the for/await loop (which will not terminate until
 * the close() method is called.)
 */
class AsyncQueue {
  constructor() {
    // Values that have been queued but not dequeued yet are stored here
    this.values = [];
    // When Promises are dequeued before their corresponding values are
    // queued, the resolve methods for those Promises are stored here.
    this.resolvers = [];
    // Once closed, no more values can be enqueued, and no more unfulfilled
    // Promises returned.
    this.closed = false;
  }

  enqueue(value) {
    if (this.closed) {
      throw new Error("AsyncQueue closed");
    }
    if (this.resolvers.length > 0) {
      // If this value has already been promised, resolve that Promise
      const resolve = this.resolvers.shift();
      resolve(value);
    } else {
      // Otherwise, queue it up
      this.values.push(value);
    }
  }

  dequeue() {
    if (this.values.length > 0) {
      // If there is a queued value, return a resolved Promise for it
      const value = this.values.shift();
      return Promise.resolve(value);
    } else if (this.closed) {
      // If no queued values and we're closed, return a resolved
      // Promise for the "end-of-stream" marker
      return Promise.resolve(AsyncQueue.EOS);
    } else {
      // Otherwise, return an unresolved Promise,
      // queuing the resolver function for later use
      return new Promise((resolve) => {
        this.resolvers.push(resolve);
      });
    }
  }

  close() {
    // Once the queue is closed, no more values will be enqueued.
    // So resolve any pending Promises with the end-of-stream marker
    while (this.resolvers.length > 0) {
      this.resolvers.shift()(AsyncQueue.EOS);
    }
    this.closed = true;
  }

  // Define the method that makes this class asynchronously iterable
  [Symbol.asyncIterator]() {
    return this;
  }

  // Define the method that makes this an asynchronous iterator. The
  // dequeue() Promise resolves to a value or the EOS sentinel if we're
  // closed. Here, we need to return a Promise that resolves to an
  // iterator result object.
  next() {
    return this.dequeue().then((value) =>
      value === AsyncQueue.EOS
        ? { value: undefined, done: true }
        : { value: value, done: false }
    );
  }
}

// A sentinel value returned by dequeue() to mark "end of stream" when closed
AsyncQueue.EOS = Symbol("end-of-stream");

// Push events of the specified type on the specified document element
// onto an AsyncQueue object, and return the queue for use as an event stream
function eventStream(elt, type) {
  const q = new AsyncQueue(); // Create a queue
  elt.addEventListener(type, (e) => q.enqueue(e)); // Enqueue events
  return q;
}

async function handleKeys() {
  // Get a stream of keypress events and loop once for each one
  for await (const event of eventStream(document, "keypress")) {
    console.log(event.key);
  }
}

let a = handleKeys();

Thanks if anyone could explain this fundamental issue. I searched whole Internet but didn't find the explanation.

3
  • 2
    This is a pretty advanced example for someone beginning in the language. I'd recommend learning about Promises first which can be quite confusing at first. But to give you a general idea, Promises are values that are not yet calculated. When you create a promise, the code that follows doesn't stop running to wait for that value. Instead the promise has a revolve() method which is called when the value is finally calculated. I'd recommend you find an already existing implementation async queue implementing one yourself. Commented May 9, 2024 at 19:25
  • 2
    "Didn't we put it before?": no, we only registered listeners with executing addEventListener. But the callback given to those calls will only execute when the associated event is triggered (in this case a keypress). As long as that event is not triggered (i.e. you haven't pressed a key yet), the enqueue call is not made, which means the queue is empty. When you await an event from that queue you get a promise object instead. Once the queue really receives the value, it will associate it with the promise you are awaiting, and so an iteration of the await-loop is made. Commented May 9, 2024 at 19:30
  • Hi! Thank you everyone on explanations. For shedding some light on that tough example. Will be pleased if you find time little more. Does - 'eventStream' - dinamically changes its size while loop is on work? We retrieve Unresolved Promise() from 'Async queue'. In order to - if new 'event' drops into Async queue while loop is on work - it gets promptly handled with that Unresolved Promise(). Main clue is that 'eventStream' queue changes its size while 'for await' works? It can be 0 events, 1 events, again 0 events, 2 events. So 'for await' loop just trace queue while Listener works? Commented May 9, 2024 at 21:26

1 Answer 1

1

This is all async code, and while it executes top to bottom, you cannot just read it that way.

First: nothing happens until handleKeys is called. Function bodies aren't executed when they are defined, you have to call them.

Once handleKeys is called, it calls eventStream. Inside eventStream a listener is registered to listen for keypress events on the document. Remember that function bodies aren't executed when they are defined, they are executed when called, so the enqueue doesn't fire inside eventStream, it is merely registered with the document as a listener, the JS runtime will call the anonymous listener function whenever keys are pressed, and the events will be enqueued.

The for...of loop will call the AsyncQueue's [Symbol.asyncIterator] method which will yield up the queued events one by one as Promises of the key presses. The await will then "unwrap" the Promise values and the body of the for loop will be executed for each event in the queue.

On another note, and pursuant to a comment somebody else made, this is a pretty advanced example that touches on a lot of concepts: event-driven systems, async/await, Promises/Futures, well-known Symbols, iterators, data structures, callbacks... it's a lot. If you are coming to JS after extensive programming in another language where you've already gained familiarity with some of these that's one thing, but if you aren't an experienced programmer you may have stumbled accidentally onto a tutorial meant for one.

Sign up to request clarification or add additional context in comments.

2 Comments

@HarpyCutie that isn't really how the site works. It's not just about answering your question for your sake, Stack Overflow is a repository of q&a's for posterity. This could be valuable to future readers. You aren't supposed to just delete your posts.
Oh, ok ok~ Sorry for asking this. Hope my post helps someone who face the same confusion as I did.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.