"use strict"; const PromiseTry = require("es6-promise-try"); const isGenerator = require("is-generator-function"); const lastItem = require("../util/last-item"); const Pause = Symbol("Pause"); const isPromise = require("./is-promise"); async function asyncGeneratorContext(generatorFunction, customHook) { let generator = generatorFunction(); let lastResult = { done: false }; let nextValue; while (lastResult.done === false) { lastResult = generator.next(nextValue); if (typeof lastResult.value?.then === "function") { nextValue = await lastResult.value; } else { nextValue = customHook(lastResult.value); } } return nextValue; } module.exports = { Pause: Pause, Instruction: (instruction) => { // FIXME: Unused? return { __yieldcoreInstruction: true, instruction: instruction }; }, Internal: (generator, name) => { return { __yieldcoreInternal: true, generator: generator, name: name }; // FIXME: propagate NotEnoughInput, handle in custom handler as a retry }, create: function createYieldCore(rootGenerator, { onYieldInstruction, onReturn, onBeforeStackDecrease, onAfterStackIncrease }) { let running = false; let finished = false; let stack = []; let currentFrame; let lastValue; // FIXME: Clear on every stack change function decreaseStack(returnValue) { if (onBeforeStackDecrease != null) { onBeforeStackDecrease(stack, returnValue); } stack.pop(); currentFrame = lastItem(stack); // console.log("popping stack, new frame:", currentFrame); } function increaseStack(frame) { stack.push(frame); currentFrame = lastItem(stack); // console.log("increasing stack, new frame:", currentFrame); if (onAfterStackIncrease != null) { onAfterStackIncrease(stack); } } function insertInstruction(instruction) { increaseStack({ instruction: instruction, generator: instruction(), done: false, value: undefined, name: undefined }); } function insertInternalInstruction(generator, name) { increaseStack({ instruction: "internal", generator: generator, done: false, value: undefined, name: name // TODO: Rename this to `tag` or something instead? Doesn't have to be a name string }); } async function startLoop() { while (finished === false && running === true) { if (currentFrame.done === true) { // This is a previously completed frame; it doesn't need any further processing decreaseStack(currentFrame.value); } else { // FIXME: Catch let result = currentFrame.generator.next(lastValue); let action; if (result.done === true) { // value == return value currentFrame.done = true; currentFrame.value = result.value; let lastFrame = currentFrame; decreaseStack(result.value); if (onReturn != null && lastFrame.instruction !== "internal") { insertInternalInstruction(onReturn(result.value, lastFrame, stack), "_onReturn"); } else { action = result.value; } } else if (isGenerator(result.value)) { // TODO: Figure out better semantics for onYieldGenerator? // NOTE: Currently this hook *cannot* be a generator! // if (onYieldGenerator != null) { // onYieldGenerator(result.value, currentFrame, stack); // } insertInstruction(result.value); continue; // Proceed with the next cycle immediately } else if (isPromise(result.value)) { action = await result.value; } else if (result.value === Pause) { running = false; } else if (result.value?.__yieldcoreInternal === true) { insertInternalInstruction(result.value.generator, result.value.name); continue; // FIXME: Is this correct? } else { if (onYieldInstruction != null) { insertInternalInstruction(onYieldInstruction(result.value, currentFrame, stack), "_onYieldInstruction"); } else { // TODO: Is there ever a good reason not to have an onYieldInstruction handler? Should we just disallow that case? action = result.value; } } lastValue = action; if (stack.length === 0) { finished = true; } } } return lastValue; } // Initial frame insertInstruction(rootGenerator); return { resume: function () { if (running) { throw new Error(`Already running`); } else if (finished) { throw new Error(`Program has finished already!`); } else { running = true; return startLoop(); } } }; } };