@bytecodealliance/jco
Version:
JavaScript tooling for working with WebAssembly Components
1,581 lines (1,311 loc) • 251 kB
JavaScript
"use jco";import { environment, exit as exit$1, stderr, stdin, stdout, terminalInput, terminalOutput, terminalStderr, terminalStdin, terminalStdout } from '@bytecodealliance/preview2-shim/cli';
import { preopens, types } from '@bytecodealliance/preview2-shim/filesystem';
import { error, streams } from '@bytecodealliance/preview2-shim/io';
import { random } from '@bytecodealliance/preview2-shim/random';
const { getEnvironment } = environment;
getEnvironment._isHostProvided = true;
if (getEnvironment=== undefined) {
const err = new Error("unexpectedly undefined local import 'getEnvironment', was 'getEnvironment' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { exit } = exit$1;
exit._isHostProvided = true;
if (exit=== undefined) {
const err = new Error("unexpectedly undefined local import 'exit', was 'exit' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getStderr } = stderr;
getStderr._isHostProvided = true;
if (getStderr=== undefined) {
const err = new Error("unexpectedly undefined local import 'getStderr', was 'getStderr' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getStdin } = stdin;
getStdin._isHostProvided = true;
if (getStdin=== undefined) {
const err = new Error("unexpectedly undefined local import 'getStdin', was 'getStdin' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getStdout } = stdout;
getStdout._isHostProvided = true;
if (getStdout=== undefined) {
const err = new Error("unexpectedly undefined local import 'getStdout', was 'getStdout' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { TerminalInput } = terminalInput;
TerminalInput._isHostProvided = true;
if (TerminalInput=== undefined) {
const err = new Error("unexpectedly undefined local import 'TerminalInput', was 'TerminalInput' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { TerminalOutput } = terminalOutput;
TerminalOutput._isHostProvided = true;
if (TerminalOutput=== undefined) {
const err = new Error("unexpectedly undefined local import 'TerminalOutput', was 'TerminalOutput' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getTerminalStderr } = terminalStderr;
getTerminalStderr._isHostProvided = true;
if (getTerminalStderr=== undefined) {
const err = new Error("unexpectedly undefined local import 'getTerminalStderr', was 'getTerminalStderr' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getTerminalStdin } = terminalStdin;
getTerminalStdin._isHostProvided = true;
if (getTerminalStdin=== undefined) {
const err = new Error("unexpectedly undefined local import 'getTerminalStdin', was 'getTerminalStdin' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getTerminalStdout } = terminalStdout;
getTerminalStdout._isHostProvided = true;
if (getTerminalStdout=== undefined) {
const err = new Error("unexpectedly undefined local import 'getTerminalStdout', was 'getTerminalStdout' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getDirectories } = preopens;
getDirectories._isHostProvided = true;
if (getDirectories=== undefined) {
const err = new Error("unexpectedly undefined local import 'getDirectories', was 'getDirectories' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { Descriptor,
DirectoryEntryStream,
filesystemErrorCode } = types;
Descriptor._isHostProvided = true;
if (Descriptor=== undefined) {
const err = new Error("unexpectedly undefined local import 'Descriptor', was 'Descriptor' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
DirectoryEntryStream._isHostProvided = true;
if (DirectoryEntryStream=== undefined) {
const err = new Error("unexpectedly undefined local import 'DirectoryEntryStream', was 'DirectoryEntryStream' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
filesystemErrorCode._isHostProvided = true;
if (filesystemErrorCode=== undefined) {
const err = new Error("unexpectedly undefined local import 'filesystemErrorCode', was 'filesystemErrorCode' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { Error: Error$1 } = error;
Error$1._isHostProvided = true;
if (Error$1=== undefined) {
const err = new Error("unexpectedly undefined local import 'Error$1', was 'Error' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { InputStream,
OutputStream } = streams;
InputStream._isHostProvided = true;
if (InputStream=== undefined) {
const err = new Error("unexpectedly undefined local import 'InputStream', was 'InputStream' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
OutputStream._isHostProvided = true;
if (OutputStream=== undefined) {
const err = new Error("unexpectedly undefined local import 'OutputStream', was 'OutputStream' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
const { getRandomBytes } = random;
getRandomBytes._isHostProvided = true;
if (getRandomBytes=== undefined) {
const err = new Error("unexpectedly undefined local import 'getRandomBytes', was 'getRandomBytes' available at instantiation?");
console.error("ERROR:", err.toString());
throw err;
}
let dv = new DataView(new ArrayBuffer());
const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer);
const toUint64 = val => BigInt.asUintN(64, BigInt(val));
function toUint32(val) {
return val >>> 0;
}
const TEXT_DECODER_UTF8 = new TextDecoder();
const TEXT_ENCODER_UTF8 = new TextEncoder();
function _utf8AllocateAndEncode(s, realloc, memory) {
if (typeof s !== 'string') {
throw new TypeError('expected a string, received [' + typeof s + ']');
}
if (s.length === 0) { return { ptr: 1, len: 0 }; }
let buf = TEXT_ENCODER_UTF8.encode(s);
let ptr = realloc(0, 0, 1, buf.length);
new Uint8Array(memory.buffer).set(buf, ptr);
return { ptr, len: buf.length, codepoints: [...s].length };
}
const T_FLAG = 1 << 30;
function rscTableCreateOwn (table, rep) {
const free = table[0] & ~T_FLAG;
if (free === 0) {
table.push(0);
table.push(rep | T_FLAG);
return (table.length >> 1) - 1;
}
table[0] = table[free << 1];
table[free << 1] = 0;
table[(free << 1) + 1] = rep | T_FLAG;
return free;
}
function rscTableRemove (table, handle) {
const scope = table[handle << 1];
const val = table[(handle << 1) + 1];
const own = (val & T_FLAG) !== 0;
const rep = val & ~T_FLAG;
if (val === 0 || (scope & T_FLAG) !== 0) throw new TypeError('Invalid handle');
table[handle << 1] = table[0] | T_FLAG;
table[0] = handle | T_FLAG;
return { rep, scope, own };
}
let curResourceBorrows = [];
function getCurrentTask(componentIdx) {
if (componentIdx === undefined || componentIdx === null) {
throw new Error('missing/invalid component instance index [' + componentIdx + '] while getting current task');
}
const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
if (tasks === undefined) { return undefined; }
if (tasks.length === 0) { return undefined; }
return tasks[tasks.length - 1];
}
function createNewCurrentTask(args) {
_debugLog('[createNewCurrentTask()] args', args);
const {
componentIdx,
isAsync,
entryFnName,
parentSubtaskID,
callbackFnName,
getCallbackFn,
getParamsFn,
stringEncoding,
errHandling,
getCalleeParamsFn,
resultPtr,
callingWasmExport,
} = args;
if (componentIdx === undefined || componentIdx === null) {
throw new Error('missing/invalid component instance index while starting task');
}
const taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
const callbackFn = getCallbackFn ? getCallbackFn() : null;
const newTask = new AsyncTask({
componentIdx,
isAsync,
entryFnName,
callbackFn,
callbackFnName,
stringEncoding,
getCalleeParamsFn,
resultPtr,
errHandling,
});
const newTaskID = newTask.id();
const newTaskMeta = { id: newTaskID, componentIdx, task: newTask };
ASYNC_CURRENT_TASK_IDS.push(newTaskID);
ASYNC_CURRENT_COMPONENT_IDXS.push(componentIdx);
if (!taskMetas) {
ASYNC_TASKS_BY_COMPONENT_IDX.set(componentIdx, [newTaskMeta]);
} else {
taskMetas.push(newTaskMeta);
}
return [newTask, newTaskID];
}
function endCurrentTask(componentIdx, taskID) {
componentIdx ??= ASYNC_CURRENT_COMPONENT_IDXS.at(-1);
taskID ??= ASYNC_CURRENT_TASK_IDS.at(-1);
_debugLog('[endCurrentTask()] args', { componentIdx, taskID });
if (componentIdx === undefined || componentIdx === null) {
throw new Error('missing/invalid component instance index while ending current task');
}
const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
if (!tasks || !Array.isArray(tasks)) {
throw new Error('missing/invalid tasks for component instance while ending task');
}
if (tasks.length == 0) {
throw new Error('no current task(s) for component instance while ending task');
}
if (taskID) {
const last = tasks[tasks.length - 1];
if (last.id !== taskID) {
// throw new Error('current task does not match expected task ID');
return;
}
}
ASYNC_CURRENT_TASK_IDS.pop();
ASYNC_CURRENT_COMPONENT_IDXS.pop();
const taskMeta = tasks.pop();
return taskMeta.task;
}
const ASYNC_TASKS_BY_COMPONENT_IDX = new Map();
const ASYNC_CURRENT_TASK_IDS = [];
const ASYNC_CURRENT_COMPONENT_IDXS = [];
class AsyncTask {
static _ID = 0n;
static State = {
INITIAL: 'initial',
CANCELLED: 'cancelled',
CANCEL_PENDING: 'cancel-pending',
CANCEL_DELIVERED: 'cancel-delivered',
RESOLVED: 'resolved',
}
static BlockResult = {
CANCELLED: 'block.cancelled',
NOT_CANCELLED: 'block.not-cancelled',
}
#id;
#componentIdx;
#state;
#isAsync;
#entryFnName = null;
#subtasks = [];
#onResolveHandlers = [];
#completionPromise = null;
#memoryIdx = null;
#callbackFn = null;
#callbackFnName = null;
#postReturnFn = null;
#getCalleeParamsFn = null;
#stringEncoding = null;
#parentSubtask = null;
#needsExclusiveLock = false;
#errHandling;
#backpressurePromise;
#backpressureWaiters = 0n;
#returnLowerFns = null;
cancelled = false;
requested = false;
alwaysTaskReturn = false;
returnCalls = 0;
storage = [0, 0];
borrowedHandles = {};
awaitableResume = null;
awaitableCancel = null;
constructor(opts) {
this.#id = ++AsyncTask._ID;
if (opts?.componentIdx === undefined) {
throw new TypeError('missing component id during task creation');
}
this.#componentIdx = opts.componentIdx;
this.#state = AsyncTask.State.INITIAL;
this.#isAsync = opts?.isAsync ?? false;
this.#entryFnName = opts.entryFnName;
const {
promise: completionPromise,
resolve: resolveCompletionPromise,
reject: rejectCompletionPromise,
} = promiseWithResolvers();
this.#completionPromise = completionPromise;
this.#onResolveHandlers.push((results) => {
resolveCompletionPromise(results);
})
if (opts.callbackFn) { this.#callbackFn = opts.callbackFn; }
if (opts.callbackFnName) { this.#callbackFnName = opts.callbackFnName; }
if (opts.getCalleeParamsFn) { this.#getCalleeParamsFn = opts.getCalleeParamsFn; }
if (opts.stringEncoding) { this.#stringEncoding = opts.stringEncoding; }
if (opts.parentSubtask) { this.#parentSubtask = opts.parentSubtask; }
this.#needsExclusiveLock = this.isSync() || !this.hasCallback();
if (opts.errHandling) { this.#errHandling = opts.errHandling; }
}
taskState() { return this.#state; }
id() { return this.#id; }
componentIdx() { return this.#componentIdx; }
isAsync() { return this.#isAsync; }
entryFnName() { return this.#entryFnName; }
completionPromise() { return this.#completionPromise; }
isAsync() { return this.#isAsync; }
isSync() { return !this.isAsync(); }
getErrHandling() { return this.#errHandling; }
hasCallback() { return this.#callbackFn !== null; }
setReturnMemoryIdx(idx) { this.#memoryIdx = idx; }
getReturnMemoryIdx() { return this.#memoryIdx; }
setReturnLowerFns(fns) { this.#returnLowerFns = fns; }
getReturnLowerFns() { return this.#returnLowerFns; }
setParentSubtask(subtask) {
if (!subtask || !(subtask instanceof AsyncSubtask)) { return }
if (this.#parentSubtask) { throw new Error('parent subtask can only be set once'); }
this.#parentSubtask = subtask;
}
getParentSubtask() { return this.#parentSubtask; }
// TODO(threads): this is very inefficient, we can pass along a root task,
// and ideally do not need this once thread support is in place
getRootTask() {
let currentSubtask = this.getParentSubtask();
let task = this;
while (currentSubtask) {
task = currentSubtask.getParentTask();
currentSubtask = task.getParentSubtask();
}
return task;
}
setPostReturnFn(f) {
if (!f) { return; }
if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); }
this.#postReturnFn = f;
}
setCallbackFn(f, name) {
if (!f) { return; }
if (this.#callbackFn) { throw new Error('callback fn can only be set once'); }
this.#callbackFn = f;
this.#callbackFnName = name;
}
getCallbackFnName() {
if (!this.#callbackFnName) { return undefined; }
return this.#callbackFnName;
}
runCallbackFn(...args) {
if (!this.#callbackFn) { throw new Error('on callback function has been set for task'); }
return this.#callbackFn.apply(null, args);
}
getCalleeParams() {
if (!this.#getCalleeParamsFn) { throw new Error('missing/invalid getCalleeParamsFn'); }
return this.#getCalleeParamsFn();
}
mayEnter(task) {
const cstate = getOrCreateAsyncState(this.#componentIdx);
if (cstate.hasBackpressure()) {
_debugLog('[AsyncTask#mayEnter()] disallowed due to backpressure', { taskID: this.#id });
return false;
}
if (!cstate.callingSyncImport()) {
_debugLog('[AsyncTask#mayEnter()] disallowed due to sync import call', { taskID: this.#id });
return false;
}
const callingSyncExportWithSyncPending = cstate.callingSyncExport && !task.isAsync;
if (!callingSyncExportWithSyncPending) {
_debugLog('[AsyncTask#mayEnter()] disallowed due to sync export w/ sync pending', { taskID: this.#id });
return false;
}
return true;
}
async enter() {
_debugLog('[AsyncTask#enter()] args', { taskID: this.#id });
const cstate = getOrCreateAsyncState(this.#componentIdx);
if (this.isSync()) { return true; }
if (cstate.hasBackpressure()) {
cstate.addBackpressureWaiter();
const result = await this.waitUntil({
readyFn: () => !cstate.hasBackpressure(),
cancellable: true,
});
cstate.removeBackpressureWaiter();
if (result === AsyncTask.BlockResult.CANCELLED) {
this.cancel();
return false;
}
}
if (this.needsExclusiveLock()) { cstate.exclusiveLock(); }
return true;
}
isRunning() {
return this.#state !== AsyncTask.State.RESOLVED;
}
async waitUntil(opts) {
const { readyFn, waitableSetRep, cancellable } = opts;
_debugLog('[AsyncTask#waitUntil()] args', { taskID: this.#id, waitableSetRep, cancellable });
const state = getOrCreateAsyncState(this.#componentIdx);
const wset = state.waitableSets.get(waitableSetRep);
let event;
wset.incrementNumWaiting();
const keepGoing = await this.suspendUntil({
readyFn: () => {
const hasPendingEvent = wset.hasPendingEvent();
return readyFn() && hasPendingEvent;
},
cancellable,
});
if (keepGoing) {
event = wset.getPendingEvent();
} else {
event = {
code: ASYNC_EVENT_CODE.TASK_CANCELLED,
index: 0,
result: 0,
};
}
wset.decrementNumWaiting();
return event;
}
async onBlock(awaitable) {
_debugLog('[AsyncTask#onBlock()] args', { taskID: this.#id, awaitable });
if (!(awaitable instanceof Awaitable)) {
throw new Error('invalid awaitable during onBlock');
}
// Build a promise that this task can await on which resolves when it is awoken
const { promise, resolve, reject } = promiseWithResolvers();
this.awaitableResume = () => {
_debugLog('[AsyncTask] resuming after onBlock', { taskID: this.#id });
resolve();
};
this.awaitableCancel = (err) => {
_debugLog('[AsyncTask] rejecting after onBlock', { taskID: this.#id, err });
reject(err);
};
// Park this task/execution to be handled later
const state = getOrCreateAsyncState(this.#componentIdx);
state.parkTaskOnAwaitable({ awaitable, task: this });
try {
await promise;
return AsyncTask.BlockResult.NOT_CANCELLED;
} catch (err) {
// rejection means task cancellation
return AsyncTask.BlockResult.CANCELLED;
}
}
async asyncOnBlock(awaitable) {
_debugLog('[AsyncTask#asyncOnBlock()] args', { taskID: this.#id, awaitable });
if (!(awaitable instanceof Awaitable)) {
throw new Error('invalid awaitable during onBlock');
}
// TODO: watch for waitable AND cancellation
// TODO: if it WAS cancelled:
// - return true
// - only once per subtask
// - do not wait on the scheduler
// - control flow should go to the subtask (only once)
// - Once subtask blocks/resolves, reqlinquishControl() will tehn resolve request_cancel_end (without scheduler lock release)
// - control flow goes back to request_cancel
//
// Subtask cancellation should work similarly to an async import call -- runs sync up until
// the subtask blocks or resolves
//
throw new Error('AsyncTask#asyncOnBlock() not yet implemented');
}
async yieldUntil(opts) {
const { readyFn, cancellable } = opts;
_debugLog('[AsyncTask#yield()] args', { taskID: this.#id, cancellable });
const keepGoing = await this.suspendUntil({ readyFn, cancellable });
if (!keepGoing) {
return {
code: ASYNC_EVENT_CODE.TASK_CANCELLED,
index: 0,
result: 0,
};
}
return {
code: ASYNC_EVENT_CODE.NONE,
index: 0,
result: 0,
};
}
async suspendUntil(opts) {
const { cancellable, readyFn } = opts;
_debugLog('[AsyncTask#suspendUntil()] args', { cancellable });
const pendingCancelled = this.deliverPendingCancel({ cancellable });
if (pendingCancelled) { return false; }
const completed = await this.immediateSuspendUntil({ readyFn, cancellable });
return completed;
}
// TODO(threads): equivalent to thread.suspend_until()
async immediateSuspendUntil(opts) {
const { cancellable, readyFn } = opts;
_debugLog('[AsyncTask#immediateSuspendUntil()] args', { cancellable, readyFn });
const ready = readyFn();
if (ready && !ASYNC_DETERMINISM && _coinFlip()) {
return true;
}
const cstate = getOrCreateAsyncState(this.#componentIdx);
cstate.addPendingTask(this);
const keepGoing = await this.immediateSuspend({ cancellable, readyFn });
return keepGoing;
}
async immediateSuspend(opts) { // NOTE: equivalent to thread.suspend()
// TODO(threads): store readyFn on the thread
const { cancellable, readyFn } = opts;
_debugLog('[AsyncTask#immediateSuspend()] args', { cancellable, readyFn });
const pendingCancelled = this.deliverPendingCancel({ cancellable });
if (pendingCancelled) { return false; }
const cstate = getOrCreateAsyncState(this.#componentIdx);
setTimeout(() => cstate.tick(), 0);
const taskWait = await cstate.suspendTask({ task: this, readyFn });
const keepGoing = await taskWait;
return keepGoing;
}
deliverPendingCancel(opts) {
const { cancellable } = opts;
_debugLog('[AsyncTask#deliverPendingCancel()] args', { cancellable });
if (cancellable && this.#state === AsyncTask.State.PENDING_CANCEL) {
this.#state = Task.State.CANCEL_DELIVERED;
return true;
}
return false;
}
isCancelled() { return this.cancelled }
cancel() {
_debugLog('[AsyncTask#cancel()] args', { });
if (!this.taskState() !== AsyncTask.State.CANCEL_DELIVERED) {
throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] invalid task state for cancellation`);
}
if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); }
this.cancelled = true;
this.onResolve(new Error('cancelled'));
this.#state = AsyncTask.State.RESOLVED;
}
onResolve(taskValue) {
for (const f of this.#onResolveHandlers) {
try {
f(taskValue);
} catch (err) {
console.error("error during task resolve handler", err);
throw err;
}
}
if (this.#postReturnFn) {
_debugLog('[AsyncTask#onResolve()] running post return ', {
componentIdx: this.#componentIdx,
taskID: this.#id,
});
this.#postReturnFn();
}
}
registerOnResolveHandler(f) {
this.#onResolveHandlers.push(f);
}
resolve(results) {
_debugLog('[AsyncTask#resolve()] args', {
results,
componentIdx: this.#componentIdx,
taskID: this.#id,
});
if (this.#state === AsyncTask.State.RESOLVED) {
throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] is already resolved (did you forget to wait for an import?)`);
}
if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); }
switch (results.length) {
case 0:
this.onResolve(undefined);
break;
case 1:
this.onResolve(results[0]);
break;
default:
throw new Error('unexpected number of results');
}
this.#state = AsyncTask.State.RESOLVED;
}
exit() {
_debugLog('[AsyncTask#exit()] args', { });
// TODO: ensure there is only one task at a time (scheduler.lock() functionality)
if (this.#state !== AsyncTask.State.RESOLVED) {
// TODO(fix): only fused, manually specified post returns seem to break this invariant,
// as the TaskReturn trampoline is not activated it seems.
//
// see: test/p3/ported/wasmtime/component-async/post-return.js
//
// We *should* be able to upgrade this to be more strict and throw at some point,
// which may involve rewriting the upstream test to surface task return manually somehow.
//
//throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] exited without resolution`);
_debugLog('[AsyncTask#exit()] task exited without resolution', {
componentIdx: this.#componentIdx,
taskID: this.#id,
subtask: this.getParentSubtask(),
subtaskID: this.getParentSubtask()?.id(),
});
this.#state = AsyncTask.State.RESOLVED;
}
if (this.borrowedHandles > 0) {
throw new Error('task [${this.#id}] exited without clearing borrowed handles');
}
const state = getOrCreateAsyncState(this.#componentIdx);
if (!state) { throw new Error('missing async state for component [' + this.#componentIdx + ']'); }
if (!this.#isAsync && !state.inSyncExportCall) {
throw new Error('sync task must be run from components known to be in a sync export call');
}
state.inSyncExportCall = false;
if (this.needsExclusiveLock() && !state.isExclusivelyLocked()) {
throw new Error('task [' + this.#id + '] exit: component [' + this.#componentIdx + '] should have been exclusively locked');
}
state.exclusiveRelease();
}
needsExclusiveLock() { return this.#needsExclusiveLock; }
createSubtask(args) {
_debugLog('[AsyncTask#createSubtask()] args', args);
const { componentIdx, childTask, callMetadata } = args;
const newSubtask = new AsyncSubtask({
componentIdx,
childTask,
parentTask: this,
callMetadata,
});
this.#subtasks.push(newSubtask);
return newSubtask;
}
getLatestSubtask() { return this.#subtasks.at(-1); }
currentSubtask() {
_debugLog('[AsyncTask#currentSubtask()]');
if (this.#subtasks.length === 0) { return undefined; }
return this.#subtasks.at(-1);
}
endCurrentSubtask() {
_debugLog('[AsyncTask#endCurrentSubtask()]');
if (this.#subtasks.length === 0) { throw new Error('cannot end current subtask: no current subtask'); }
const subtask = this.#subtasks.pop();
subtask.drop();
return subtask;
}
}
function unpackCallbackResult(result) {
_debugLog('[unpackCallbackResult()] args', { result });
if (!(_typeCheckValidI32(result))) { throw new Error('invalid callback return value [' + result + '], not a valid i32'); }
const eventCode = result & 0xF;
if (eventCode < 0 || eventCode > 3) {
throw new Error('invalid async return value [' + eventCode + '], outside callback code range');
}
if (result < 0 || result >= 2**32) { throw new Error('invalid callback result'); }
// TODO: table max length check?
const waitableSetRep = result >> 4;
return [eventCode, waitableSetRep];
}
function _lowerImport(args, exportFn) {
const params = [...arguments].slice(2);
_debugLog('[_lowerImport()] args', { args, params, exportFn });
const {
functionIdx,
componentIdx,
isAsync,
paramLiftFns,
resultLowerFns,
metadata,
memoryIdx,
getMemoryFn,
getReallocFn,
} = args;
const parentTaskMeta = getCurrentTask(componentIdx);
const parentTask = parentTaskMeta?.task;
if (!parentTask) { throw new Error('missing parent task during lower of import'); }
const cstate = getOrCreateAsyncState(componentIdx);
const subtask = parentTask.createSubtask({
componentIdx,
parentTask,
callMetadata: {
memoryIdx,
memory: getMemoryFn(),
realloc: getReallocFn(),
resultPtr: params[0],
}
});
parentTask.setReturnMemoryIdx(memoryIdx);
const rep = cstate.subtasks.insert(subtask);
subtask.setRep(rep);
subtask.setOnProgressFn(() => {
subtask.setPendingEventFn(() => {
if (subtask.resolved()) { subtask.deliverResolve(); }
return {
code: ASYNC_EVENT_CODE.SUBTASK,
index: rep,
result: subtask.getStateNumber(),
}
});
});
// Set up a handler on subtask completion to lower results from the call into the caller's memory region.
subtask.registerOnResolveHandler((res) => {
_debugLog('[_lowerImport()] handling subtask result', { res, subtaskID: subtask.id() });
const { memory, resultPtr, realloc } = subtask.getCallMetadata();
resultLowerFns[0]({ componentIdx, memory, realloc, vals: [res], storagePtr: resultPtr });
});
const subtaskState = subtask.getStateNumber();
if (subtaskState < 0 || subtaskState > 2**5) {
throw new Error('invalid subtask state, out of valid range');
}
// NOTE: we must wait a bit before calling the export function,
// to ensure the subtask state is not modified before the lower call return
//
// TODO: we should trigger via subtask state changing, rather than a static wait?
setTimeout(async () => {
try {
_debugLog('[_lowerImport()] calling lowered import', { exportFn, params });
exportFn.apply(null, params);
const task = subtask.getChildTask();
task.registerOnResolveHandler((res) => {
_debugLog('[_lowerImport()] cascading subtask completion', {
childTaskID: task.id(),
subtaskID: subtask.id(),
parentTaskID: parentTask.id(),
});
subtask.onResolve(res);
cstate.tick();
});
} catch (err) {
console.error("post-lower import fn error:", err);
throw err;
}
}, 100);
return Number(subtask.waitableRep()) << 4 | subtaskState;
}
function _liftFlatU8(ctx) {
_debugLog('[_liftFlatU8()] args', { ctx });
let val;
if (ctx.useDirectParams) {
if (ctx.params.length === 0) { throw new Error('expected at least a single i32 argument'); }
val = ctx.params[0];
ctx.params = ctx.params.slice(1);
} else {
if (ctx.storageLen < ctx.storagePtr + 1) { throw new Error('not enough storage remaining for lift'); }
val = new DataView(ctx.memory.buffer).getUint8(ctx.storagePtr);
ctx.storagePtr += 1;
ctx.storageLen -= 1;
}
return [val, ctx];
}
function _liftFlatU16(ctx) {
_debugLog('[_liftFlatU16()] args', { ctx });
let val;
if (ctx.useDirectParams) {
if (params.length === 0) { throw new Error('expected at least a single i32 argument'); }
val = ctx.params[0];
ctx.params = ctx.params.slice(1);
} else {
if (ctx.storageLen < ctx.storagePtr + 2) { throw new Error('not enough storage remaining for lift'); }
val = new DataView(ctx.memory.buffer).getUint16(ctx.storagePtr);
ctx.storagePtr += 2;
ctx.storageLen -= 2;
}
return [val, ctx];
}
function _liftFlatU32(ctx) {
_debugLog('[_liftFlatU32()] args', { ctx });
let val;
if (ctx.useDirectParams) {
if (ctx.params.length === 0) { throw new Error('expected at least a single i34 argument'); }
val = ctx.params[0];
ctx.params = ctx.params.slice(1);
} else {
if (ctx.storageLen < ctx.storagePtr + 4) { throw new Error('not enough storage remaining for lift'); }
val = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr);
ctx.storagePtr += 4;
ctx.storageLen -= 4;
}
return [val, ctx];
}
function _liftFlatU64(ctx) {
_debugLog('[_liftFlatU64()] args', { ctx });
let val;
if (ctx.useDirectParams) {
if (ctx.params.length === 0) { throw new Error('expected at least one single i64 argument'); }
if (typeof ctx.params[0] !== 'bigint') { throw new Error('expected bigint'); }
val = ctx.params[0];
ctx.params = ctx.params.slice(1);
} else {
if (ctx.storageLen < ctx.storagePtr + 8) { throw new Error('not enough storage remaining for lift'); }
val = new DataView(ctx.memory.buffer).getUint64(ctx.storagePtr);
ctx.storagePtr += 8;
ctx.storageLen -= 8;
}
return [val, ctx];
}
function _liftFlatStringUTF8(ctx) {
_debugLog('[_liftFlatStringUTF8()] args', { ctx });
let val;
if (ctx.useDirectParams) {
if (ctx.params.length < 2) { throw new Error('expected at least two u32 arguments'); }
const offset = ctx.params[0];
if (!Number.isSafeInteger(offset)) { throw new Error('invalid offset'); }
const len = ctx.params[1];
if (!Number.isSafeInteger(len)) { throw new Error('invalid len'); }
val = TEXT_DECODER_UTF8.decode(new DataView(ctx.memory.buffer, offset, len));
ctx.params = ctx.params.slice(2);
} else {
const start = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr, params[0], true);
const codeUnits = new DataView(memory.buffer).getUint32(ctx.storagePtr, params[0] + 4, true);
val = TEXT_DECODER_UTF8.decode(new Uint8Array(ctx.memory.buffer, start, codeUnits));
ctx.storagePtr += codeUnits;
ctx.storageLen -= codeUnits;
}
return [val, ctx];
}
function _liftFlatVariant(casesAndLiftFns) {
return function _liftFlatVariantInner(ctx) {
_debugLog('[_liftFlatVariant()] args', { ctx });
const origUseParams = ctx.useDirectParams;
let caseIdx;
if (casesAndLiftFns.length < 256) {
let discriminantByteLen = 1;
const [idx, newCtx] = _liftFlatU8(ctx);
caseIdx = idx;
ctx = newCtx;
} else if (casesAndLiftFns.length > 256 && discriminantByteLen < 65536) {
discriminantByteLen = 2;
const [idx, newCtx] = _liftFlatU16(ctx);
caseIdx = idx;
ctx = newCtx;
} else if (casesAndLiftFns.length > 65536 && discriminantByteLen < 4_294_967_296) {
discriminantByteLen = 4;
const [idx, newCtx] = _liftFlatU32(ctx);
caseIdx = idx;
ctx = newCtx;
} else {
throw new Error('unsupported number of cases [' + casesAndLIftFns.legnth + ']');
}
const [ tag, liftFn, size32, alignment32 ] = casesAndLiftFns[caseIdx];
let val;
if (liftFn === null) {
val = { tag };
return [val, ctx];
}
const [newVal, newCtx] = liftFn(ctx);
ctx = newCtx;
val = { tag, val: newVal };
return [val, ctx];
}
}
function _liftFlatList(elemLiftFn, alignment32, knownLen) {
function _liftFlatListInner(ctx) {
_debugLog('[_liftFlatList()] args', { ctx });
let metaPtr;
let dataPtr;
let len;
if (ctx.useDirectParams) {
if (knownLen) {
dataPtr = _liftFlatU32(ctx);
} else {
metaPtr = _liftFlatU32(ctx);
}
} else {
if (knownLen) {
dataPtr = _liftFlatU32(ctx);
} else {
metaPtr = _liftFlatU32(ctx);
}
}
if (metaPtr) {
if (dataPtr !== undefined) { throw new Error('both meta and data pointers should not be set yet'); }
if (ctx.useDirectParams) {
ctx.useDirectParams = false;
ctx.storagePtr = metaPtr;
ctx.storageLen = 8;
dataPtr = _liftFlatU32(ctx);
len = _liftFlatU32(ctx);
ctx.useDirectParams = true;
ctx.storagePtr = null;
ctx.storageLen = null;
} else {
dataPtr = _liftFlatU32(ctx);
len = _liftFlatU32(ctx);
}
}
const val = [];
for (var i = 0; i < len; i++) {
ctx.storagePtr = Math.ceil(ctx.storagePtr / alignment32) * alignment32;
const [res, nextCtx] = elemLiftFn(ctx);
val.push(res);
ctx = nextCtx;
}
return [val, ctx];
}
}
function _liftFlatFlags(cases) {
return function _liftFlatFlagsInner(ctx) {
_debugLog('[_liftFlatFlags()] args', { ctx });
throw new Error('flat lift for flags not yet implemented!');
}
}
function _liftFlatResult(casesAndLiftFns) {
return function _liftFlatResultInner(ctx) {
_debugLog('[_liftFlatResult()] args', { ctx });
return _liftFlatVariant(casesAndLiftFns)(ctx);
}
}
function _liftFlatOwn(componentTableIdx, size, memory, vals, storagePtr, storageLen) {
_debugLog('[_liftFlatOwn()] args', { size, memory, vals, storagePtr, storageLen });
throw new Error('flat lift for owned resources not yet implemented!');
}
function _lowerFlatU8(ctx) {
_debugLog('[_lowerFlatU8()] args', ctx);
const { memory, realloc, vals, storagePtr, storageLen } = ctx;
if (vals.length !== 1) {
throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)');
}
if (vals[0] > 255 || vals[0] < 0) { throw new Error('invalid value for core value representing u8'); }
if (!memory) { throw new Error("missing memory for lower"); }
new DataView(memory.buffer).setUint32(storagePtr, vals[0], true);
return 1;
}
function _lowerFlatU16(memory, vals, storagePtr, storageLen) {
_debugLog('[_lowerFlatU16()] args', { memory, vals, storagePtr, storageLen });
if (vals.length !== 1) {
throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)');
}
if (vals[0] > 65_535 || vals[0] < 0) { throw new Error('invalid value for core value representing u16'); }
new DataView(memory.buffer).setUint16(storagePtr, vals[0], true);
return 2;
}
function _lowerFlatU32(ctx) {
_debugLog('[_lowerFlatU32()] args', ctx);
const { memory, realloc, vals, storagePtr, storageLen } = ctx;
if (vals.length !== 1) { throw new Error('expected single value to lower, got (' + vals.length + ')'); }
if (vals[0] > 4_294_967_295 || vals[0] < 0) { throw new Error('invalid value for core value representing u32'); }
// TODO(fix): fix misaligned writes properly
const rem = ctx.storagePtr % 4;
if (rem !== 0) { ctx.storagePtr += (4 - rem); }
new DataView(memory.buffer).setUint32(storagePtr, vals[0], true);
return 4;
}
function _lowerFlatU64(memory, vals, storagePtr, storageLen) {
_debugLog('[_lowerFlatU64()] args', { memory, vals, storagePtr, storageLen });
if (vals.length !== 1) { throw new Error('unexpected number of core vals'); }
if (vals[0] > 18_446_744_073_709_551_615n || vals[0] < 0n) { throw new Error('invalid value for core value representing u64'); }
new DataView(memory.buffer).setBigUint64(storagePtr, vals[0], true);
return 8;
}
function _lowerFlatRecord(fieldMetas) {
return (size, memory, vals, storagePtr, storageLen) => {
const params = [...arguments].slice(5);
_debugLog('[_lowerFlatRecord()] args', {
size,
memory,
vals,
storagePtr,
storageLen,
params,
fieldMetas
});
const [start] = vals;
if (storageLen !== undefined && size !== undefined && size > storageLen) {
throw new Error('not enough storage remaining for record flat lower');
}
const data = new Uint8Array(memory.buffer, start, size);
new Uint8Array(memory.buffer, storagePtr, size).set(data);
return data.byteLength;
}
}
function _lowerFlatVariant(metadata, extra) {
const { discriminantSizeBytes, lowerMetas } = metadata;
return function _lowerFlatVariantInner(ctx) {
_debugLog('[_lowerFlatVariant()] args', ctx);
const { memory, realloc, vals, storageLen, componentIdx } = ctx;
let storagePtr = ctx.storagePtr;
const { tag, val } = vals[0];
const variant = lowerMetas.find(vm => vm.tag === tag);
if (!variant) { throw new Error(`missing/invalid variant, no tag matches [${tag}] (options were ${variantMetas.map(vm => vm.tag)})`); }
if (!variant.discriminant) { throw new Error(`missing/invalid discriminant for variant [${variant}]`); }
let bytesWritten;
let discriminantLowerArgs = { memory, realloc, vals: [variant.discriminant], storagePtr, componentIdx }
switch (discriminantSizeBytes) {
case 1:
bytesWritten = _lowerFlatU8(discriminantLowerArgs);
break;
case 2:
bytesWritten = _lowerFlatU16(discriminantLowerArgs);
break;
case 4:
bytesWritten = _lowerFlatU32(discriminantLowerArgs);
break;
default:
throw new Error(`unexpected discriminant size bytes [${discriminantSizeBytes}]`);
}
if (bytesWritten !== discriminantSizeBytes) {
throw new Error("unexpectedly wrote more bytes than discriminant");
}
storagePtr += bytesWritten;
bytesWritten += variant.lowerFn({ memory, realloc, vals: [val], storagePtr, storageLen, componentIdx });
return bytesWritten;
}
}
function _lowerFlatList(size, memory, vals, storagePtr, storageLen) {
_debugLog('[_lowerFlatList()] args', { size, memory, vals, storagePtr, storageLen });
let [start, len] = vals;
const totalSizeBytes = len * size;
if (storageLen !== undefined && totalSizeBytes > storageLen) {
throw new Error('not enough storage remaining for list flat lower');
}
const data = new Uint8Array(memory.buffer, start, totalSizeBytes);
new Uint8Array(memory.buffer, storagePtr, totalSizeBytes).set(data);
return data.byteLength;
}
function _lowerFlatEnum(size, memory, vals, storagePtr, storageLen) {
_debugLog('[_lowerFlatEnum()] args', { size, memory, vals, storagePtr, storageLen });
let [start] = vals;
if (storageLen !== undefined && size !== undefined && size > storageLen) {
throw new Error('not enough storage remaining for enum flat lower');
}
const data = new Uint8Array(memory.buffer, start, size);
new Uint8Array(memory.buffer, storagePtr, size).set(data);
return data.byteLength;
}
function _lowerFlatOption(size, memory, vals, storagePtr, storageLen) {
_debugLog('[_lowerFlatOption()] args', { size, memory, vals, storagePtr, storageLen });
let [start] = vals;
if (storageLen !== undefined && size !== undefined && size > storageLen) {
throw new Error('not enough storage remaining for option flat lower');
}
const data = new Uint8Array(memory.buffer, start, size);
new Uint8Array(memory.buffer, storagePtr, size).set(data);
return data.byteLength;
}
function _lowerFlatResult(lowerMetas) {
const invalidTag = lowerMetas.find(t => t.tag !== 'ok' && t.tag !== 'error')
if (invalidTag) { throw new Error(`invalid variant tag [${invalidTag}] found for result`); }
return function _lowerFlatResultInner() {
_debugLog('[_lowerFlatResult()] args', { lowerMetas });
let lowerFn = _lowerFlatVariant({ discriminantSizeBytes: 1, lowerMetas }, { forResult: true });
return lowerFn.apply(null, arguments);
};
}
function _lowerFlatOwn(size, memory, vals, storagePtr, storageLen) {
_debugLog('[_lowerFlatOwn()] args', { size, memory, vals, storagePtr, storageLen });
throw new Error('flat lower for owned resources not yet implemented!');
}
const ASYNC_STATE = new Map();
function getOrCreateAsyncState(componentIdx, init) {
if (!ASYNC_STATE.has(componentIdx)) {
const newState = new ComponentAsyncState({ componentIdx });
ASYNC_STATE.set(componentIdx, newState);
}
return ASYNC_STATE.get(componentIdx);
}
class ComponentAsyncState {
static EVENT_HANDLER_EVENTS = [ 'backpressure-change' ];
#componentIdx;
#callingAsyncImport = false;
#syncImportWait = promiseWithResolvers();
#locked = false;
#parkedTasks = new Map();
#suspendedTasksByTaskID = new Map();
#suspendedTaskIDs = [];
#pendingTasks = [];
#errored = null;
#backpressure = 0;
#backpressureWaiters = 0n;
#handlerMap = new Map();
#nextHandlerID = 0n;
mayLeave = true;
waitableSets;
waitables;
subtasks;
constructor(args) {
this.#componentIdx = args.componentIdx;
this.waitableSets = new RepTable({ target: `component [${this.#componentIdx}] waitable sets` });
this.waitables = new RepTable({ target: `component [${this.#componentIdx}] waitables` });
this.subtasks = new RepTable({ target: `component [${this.#componentIdx}] subtasks` });
};
componentIdx() { return this.#componentIdx; }
errored() { return this.#errored !== null; }
setErrored(err) {
_debugLog('[ComponentAsyncState#setErrored()] component errored', { err, componentIdx: this.#componentIdx });
if (this.#errored) { return; }
if (!err) {
err = new Error('error elswehere (see other component instance error)')
err.componentIdx = this.#componentIdx;
}
this.#errored = err;
}
callingSyncImport(val) {
if (val === undefined) { return this.#callingAsyncImport; }
if (typeof val !== 'boolean') { throw new TypeError('invalid setting for async import'); }
const prev = this.#callingAsyncImport;
this.#callingAsyncImport = val;
if (prev === true && this.#callingAsyncImport === false) {
this.#notifySyncImportEnd();
}
}
#notifySyncImportEnd() {
const existing = this.#syncImportWait;
this.#syncImportWait = promiseWithResolvers();
existing.resolve();
}
async waitForSyncImportCallEnd() {
await this.#syncImportWait.promise;
}
setBackpressure(v) { this.#backpressure = v; }
getBackpressure(v) { return this.#backpressure; }
incrementBackpressure() {
const newValue = this.getBackpressure() + 1;
if (newValue > 2**16) { throw new Error("invalid backpressure value, overflow"); }
this.setBackpressure(newValue);
}
decrementBackpressure() {
this.setBackpressure(Math.max(0, this.getBackpressure() - 1));
}
hasBackpressure() { return this.#backpressure > 0; }
waitForBackpressure() {
let backpressureCleared = false;
const cstate = this;
cstate.addBackpressureWaiter();
const handlerID = this.registerHandler({
event: 'backpressure-change',
fn: (bp) => {
if (bp === 0) {
cstate.removeHandler(handlerID);
backpressureCleared = true;
}
}
});
return new Promise((resolve) => {
const interval = setInterval(() => {
if (backpressureCleared) { return; }
clearInterval(interval);
cstate.removeBackpressureWaiter();
resolve(null);
}, 0);
});
}
registerHandler(args) {
const { event, fn } = args;
if (!event) { throw new Error("missing handler event"); }
if (!fn) { throw new Error("missing handler fn"); }
if (!ComponentAsyncState.EVENT_HANDLER_EVENTS.includes(event)) {
throw new Error(`unrecognized event handler [${event}]`);
}
const handlerID = this.#nextHandlerID++;
let handlers = this.#handlerMap.get(event);
if (!handlers) {
handlers = [];
this.#handlerMap.set(event, handlers)
}
handlers.push({ id: handlerID, fn, event });
return handlerID;
}
removeHandler(args) {
const { event, handlerID } = args;
const registeredHandlers = this.#handlerMap.get(event);
if (!registeredHandlers) { return; }
const found = registeredHandlers.find(h => h.id === handlerID);
if (!found) { return; }
this.#handlerMap.set(event, this.#handlerMap.get(event).filter(h => h.id !== handlerID));
}
getBackpressureWaiters() { return this.#backpressureWaiters; }
addBackpressureWaiter() { this.#backpressureWaiters++; }
removeBackpressureWaiter() {
this.#backpressureWaiters--;
if (this.#backpressureWaiters < 0) {
throw new Error("unexepctedly negative number of backpressure waiters");
}
}
parkTaskOnAwaitable(args) {
if (!args.awaitable) { throw new TypeError('missing awaitable when trying to park'); }
if (!args.task) { throw new TypeError('missing task when trying to park'); }
const { awaitable, task } = args;
let taskList = this.#parkedTasks.get(awaitable.id());
if (!taskList) {
taskList = [];
this.#parkedTasks.set(awaitable.id(), taskList);
}
taskList.push(task);
this.wakeNextTaskForAwaitable(awaitable);
}
wakeNextTaskForAwaitable(awaitable) {
if (!awaitable) { throw new TypeError('missing awaitable when waking next task'); }
const awaitableID = awaitable.id();
const taskList = this.#parkedTasks.get(awaitableID);
if (!taskList || taskList.length === 0) {
_debugLog('[ComponentAsyncState] no tasks waiting for awaitable', { awaitableID: awaitable.id() });
return;
}
let task = taskList.shift(); // todo(perf)
if (!task) { throw new Error('no task in parked list despite previous check'); }
if (!task.awaitableResume) {
throw new Error('task ready due to awaitable is missing resume', { taskID: task.id(), awaitableID });
}
task.awaitableResume();
}
// TODO: we might want to check for pre-locked status here
exclusiveLock() {
this.#locked = true;
}
exclusiveRelease() {
_debugLog('[ComponentAsyncState#exclusiveRelease()] releasing', {
locked: this.#locked,
componentIdx: this.#componentIdx,
});
this.#locked = false
}
isExclusivelyLocked() { return this.#locked === true; }
#getSuspendedTaskMeta(taskID) {
return this.#suspendedTasksByTaskID.get(taskID);
}
#removeSuspendedTaskMeta(taskID) {
_debugLog('[ComponentAsyncState#removeSuspendedTaskMeta()] removing suspended task', { taskID });
const idx = this.#suspendedTaskIDs.findIndex(t => t === taskID);
const meta = this.#suspendedTasksByTaskID.get(taskID);
this.#suspendedTaskIDs[idx] = null;
this.#suspendedTasksByTaskID.delete(taskID);
return meta;
}
#addSuspendedTaskMeta(meta) {
if (!meta) { throw new Error('missing task meta'); }
const taskID = meta.taskID;
this.#suspendedTasksByTaskID.set(taskID, meta);
this.#suspendedTaskIDs.push(taskID);
if (this.#suspendedTasksByTaskID.size < this.#suspendedTaskIDs.length - 10) {
this.#suspendedTaskIDs = this.#suspendedTaskIDs.filter(t => t !== null);
}
}
suspendTask(args) {
// TODO(threads): readyFn is normally on the thread
const { task, readyFn } = args;
const taskID = task.id();
_debugLog('[ComponentAsyncState#suspendTask()]', { taskID });
if (this.#getSuspendedTaskMeta(taskID)) {
throw new Error('task [' + taskID + '] already suspended');
}
const { promise, resolve } = Promise.withResolvers();
this.#addSuspendedTaskMeta({
task,
taskID,
readyFn,
resume: () => {
_debugLog('[ComponentAsyncState#suspendTask()] resuming suspended task', { taskID });
// TODO(threads): it's thread cancellation we should be checking for below, not task
resolve(!task.isCancelled());
},
});
return promise;
}
resumeTaskByID(taskID) {
const meta = this.#removeSuspendedTaskMeta(taskID);
if (!meta) { return; }
if (meta.taskID !== taskID) { throw new Error('task ID does not match'); }
meta.resume();
}
tick() {
_debugLog('[ComponentAsyncState#tick()]', { suspendedTaskIDs: this.#suspendedTaskIDs });
let resumedTask = false;
for (const taskID of this.#suspendedTaskIDs.filter(t => t !== null)) {
const meta = this.#suspendedTasksByTaskID.get(taskID);
if (!meta || !meta.readyFn) {
throw new Error('missing/invalid task despite ID [' + taskID + '] being present');
}
if (!meta.readyFn()) { continue; }
resumedTask = true;
this.resumeTaskByID(taskID);
}
return resumedTask;
}
addPendingTask(task) {
this.#pendingTasks.push(task);
}
}
function _prepareCall(
memoryIdx,
getMemoryFn,
startFn,
returnFn,
callerInstanceIdx,
calleeInstanceIdx,
taskReturnTypeIdx,
isCalleeAsyncInt,
stringEncoding,
resultCountOrAsync,
) {
_debugLog('[_prepareCall()]', {
callerInstanceIdx,
calleeInstanceIdx,
taskReturnTypeIdx,
isCalleeAsyncInt,
stringEncoding,
resultCountOrAsync,
});
const argArray = [...arguments];
// Since Rust will happily