Skip to content

Instantly share code, notes, and snippets.

@mmocny
Last active February 4, 2021 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmocny/ca03707dd3524ca8bb89260a37d033fb to your computer and use it in GitHub Desktop.
Save mmocny/ca03707dd3524ca8bb89260a37d033fb to your computer and use it in GitHub Desktop.
Performance Observer to Async Iterator

Performance Observer to Async Iterator

Background: Iterators, Performance Observer, and Async Iterators

function* range() {
   yield 1;
   yield 2;
   yield 3;
}

for (let i of range()) {
  console.log(i);
}

async function* async_range() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

for await (let i of async_range()) {
  console.log(i);
}

Could we adopt this pattern for:

new PerformanceObserver(lst => {
        lst.getEntries().forEach(entry => { console.log(entry); });
    }).observe({type: "layout-shift", buffered: true});

First Attempt: Wrap PO results a promise

async function *PO2it(args) {
  let resolve;

  new PerformanceObserver(l => {
    resolve(l.getEntries());
  }).observe(args);


  for (;;) {
    const entries = await new Promise(resolve_ => {
      resolve = resolve_;
    });
    for (let entry of entries) {
      yield entry;
    }
  }
}

for await (let entry of PO2it({type: 'layout-shift', buffered: true})) {
  console.log(entry);
}

Second Attempt: Wrap PO results a promise

// From: https://github.com/tc39/proposal-async-iteration/issues/99

function deferred() {
  const def = {}
  def.promise = new Promise((resolve, reject) => {
    def.resolve = resolve
    def.reject = reject
  })
  return def
}

class AsyncQueue {
  constructor(initializer) {
    // TODO(mmocny): Can probably replace the { type, value } by just storing promises in queue
    this.queue = []
    this.waiting = []

    initializer({
      next: value => {
        if (this.waiting.length > 0) {
          const consumer = this.waiting.shift()
          consumer.resolve({
            done: false,
            value
          });
          return;
        }
        this.queue.push(Promise.resolve({
          done: false,
          value
        }));
      },
      throw: error => {
        if (this.waiting.length > 0) {
          const consumer = this.waiting.shift()
          consumer.reject(error)
          return;
        }
        this.queue.push(Promise.reject(item.value));
      },
      return: value => {
        if (this.waiting.length > 0) {
          const consumer = this.waiting.shift()
          consumer.resolve({
            done: true,
            value
          });
          return;
        }
        this.queue.push(Promise.resolve({
          done: true,
          value
        }));
      }
    })
  }

  next() {
    if (this.queue.length > 0) {
      // If there are items available then simply put them
      // into the queue
      const item = this.queue.shift()
      if (item.type === 'error') {
        return Promise.reject(item.value)
      } else {
        return Promise.resolve({
          done: item.type === 'return',
          value: item.value
        })
      }
    } else {
      // If there's nothing available then simply
      // give back a Promise immediately for when a value eventually
      // comes in
      const def = deferred()
      this.waiting.push(def)
      return def.promise
    }
  }

  [Symbol.asyncIterator]() {
    return this
  }
}



// Usage:
function PO2it(args) {
  return new AsyncQueue(produce => {
      new PerformanceObserver(lst => {
         lst.getEntries().forEach(entry => { produce.next(entry); });
      }).observe(args);
  });
}

for await (let entry of PO2it({type: 'layout-shift', buffered: true})) {
  console.log(entry);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment