Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Last active August 3, 2023 10:12
Show Gist options
  • Save sebinsua/c171d7ce7e9511b38d962e89ed17ebb6 to your computer and use it in GitHub Desktop.
Save sebinsua/c171d7ce7e9511b38d962e89ed17ebb6 to your computer and use it in GitHub Desktop.
Playing around with `AsyncIterable`s
#!/usr/bin/env ts-node
type ValueFn = (index?: number) => any | Promise<any>;
const identity = v => v;
async function* range(start: number, end: number, value: ValueFn = identity) {
let index = start;
while (index < end) {
yield await value(index);
index++;
}
}
type AllIterators<TValue> =
| Iterable<TValue>
| IterableIterator<TValue>
| AsyncIterableIterator<TValue>;
function wait(wait: number): Promise<void> {
return new Promise(resolve => setTimeout(() => resolve(), wait));
}
function every(ms: number) {
return async function*<TValue>(
generator: AllIterators<TValue>
): AsyncIterableIterator<TValue> {
for await (let v of generator) {
await wait(ms);
yield v;
}
};
}
function between(start: number, _end: number): Promise<number> {
const end = _end !== Infinity ? _end : Number.MAX_SAFE_INTEGER;
return new Promise((resolve, reject) =>
resolve(Math.floor(start + Math.random() * (end - start + 1)))
);
}
async function start() {
for await (let value of every(100)(
range(0, Infinity, () => between(1, 10))
)) {
console.log("value", value);
}
}
start();
@sebinsua
Copy link
Author

sebinsua commented Jul 4, 2019

@sebinsua
Copy link
Author

sebinsua commented Jul 4, 2019

As above, you can create an infinite generator, but it can't be reused once it has completed, and unfortunately it appears to move into the completed [[GeneratorState]] if you do things like break from a loop.

However, if you wrap a generator with a function like tiptoe (see below) then the inner generator will not move into a completed state.

#!/usr/bin/env ts-node

type ValueFn = (index?: number) => any | Promise<any>;

const identity = <TValue>(v: TValue): TValue => v;

async function* range(start: number, end: number, value: ValueFn = identity) {
  let index = start;
  while (index < end) {
    yield await value(index);
    index++;
  }
}

type AllIterators<TValue> =
  | Iterable<TValue>
  | IterableIterator<TValue>
  | AsyncIterableIterator<TValue>;

const isAsyncIterator = <TValue>(
  iterator: any
): iterator is AsyncIterableIterator<TValue> => {
  return Symbol.asyncIterator in iterator;
};

const isIterator = <TValue>(
  iterator: any
): iterator is IterableIterator<TValue> => {
  return Symbol.iterator in iterator;
};

async function* tiptoe<TValue>(iterator: AllIterators<TValue>) {
  while (true) {
    if (isAsyncIterator(iterator)) {
      const { value, done } = await iterator.next();
      if (done) {
        break;
      }
      yield value;
    } else if (isIterator(iterator)) {
      const { value, done } = iterator.next();
      if (done) {
        break;
      }
      yield value;
    } else if (Array.isArray(iterator)) {
      for (let value of iterator) {
        yield value;
      }
      break;
    }
  }
}

async function start() {
  const it = range(1, Infinity);
  for await (let v of tiptoe(it)) {
    console.log("first run, value", v);
    if (v === 50) {
      break;
    }
  }

  for await (let v of tiptoe(it)) {
    console.log("second run, value", v);
    if (v === 100) {
      break;
    }
  }
}

start();

@sebinsua
Copy link
Author

sebinsua commented Jul 4, 2019

@sebinsua
Copy link
Author

sebinsua commented Aug 3, 2023

A better approach that tiptoe might be to wrap a yield with a try-finally and to continue in the finally block, see: https://twitter.com/acutmore/status/1686839231971205120?s=61&t=ZeNB37SiaxuBAxOl6pUE4Q

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment