You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
4.4 KiB
JavaScript
169 lines
4.4 KiB
JavaScript
"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();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|