deno/core/01_core.js
Bartek Iwańczuk dcf391ffed
refactor: migrate async ops to generated wrappers (#18937)
Migrates some of existing async ops to generated wrappers introduced in
https://github.com/denoland/deno/pull/18887. As a result "core.opAsync2"
was removed.

I will follow up with more PRs that migrate all the async ops to
generated wrappers.
2023-05-01 17:40:00 +02:00

873 lines
26 KiB
JavaScript

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
"use strict";
((window) => {
const {
Array,
ArrayPrototypeFill,
ArrayPrototypeMap,
ArrayPrototypePush,
Error,
ErrorCaptureStackTrace,
MapPrototypeDelete,
MapPrototypeGet,
MapPrototypeHas,
MapPrototypeSet,
ObjectAssign,
ObjectFreeze,
ObjectFromEntries,
ObjectKeys,
Promise,
PromiseReject,
PromiseResolve,
PromisePrototypeThen,
RangeError,
ReferenceError,
ReflectHas,
ReflectApply,
SafeArrayIterator,
SafeMap,
SafePromisePrototypeFinally,
StringPrototypeSlice,
StringPrototypeSplit,
SymbolFor,
SyntaxError,
TypeError,
URIError,
setQueueMicrotask,
} = window.__bootstrap.primordials;
const { ops, asyncOps } = window.Deno.core;
const build = {
target: "unknown",
arch: "unknown",
os: "unknown",
vendor: "unknown",
env: undefined,
};
function setBuildInfo(target) {
const { 0: arch, 1: vendor, 2: os, 3: env } = StringPrototypeSplit(
target,
"-",
4,
);
build.target = target;
build.arch = arch;
build.vendor = vendor;
build.os = os;
build.env = env;
ObjectFreeze(build);
}
const errorMap = {};
// Builtin v8 / JS errors
registerErrorClass("Error", Error);
registerErrorClass("RangeError", RangeError);
registerErrorClass("ReferenceError", ReferenceError);
registerErrorClass("SyntaxError", SyntaxError);
registerErrorClass("TypeError", TypeError);
registerErrorClass("URIError", URIError);
let nextPromiseId = 1;
const promiseMap = new SafeMap();
const RING_SIZE = 4 * 1024;
const NO_PROMISE = null; // Alias to null is faster than plain nulls
const promiseRing = ArrayPrototypeFill(new Array(RING_SIZE), NO_PROMISE);
// TODO(bartlomieju): it future use `v8::Private` so it's not visible
// to users. Currently missing bindings.
const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId");
let opCallTracingEnabled = false;
const opCallTraces = new SafeMap();
function enableOpCallTracing() {
opCallTracingEnabled = true;
}
function isOpCallTracingEnabled() {
return opCallTracingEnabled;
}
function movePromise(promiseId) {
const idx = promiseId % RING_SIZE;
// Move old promise from ring to map
const oldPromise = promiseRing[idx];
if (oldPromise !== NO_PROMISE) {
const oldPromiseId = promiseId - RING_SIZE;
MapPrototypeSet(promiseMap, oldPromiseId, oldPromise);
}
return promiseRing[idx] = NO_PROMISE;
}
function setPromise(promiseId) {
const idx = promiseId % RING_SIZE;
// Move old promise from ring to map
const oldPromise = promiseRing[idx];
if (oldPromise !== NO_PROMISE) {
const oldPromiseId = promiseId - RING_SIZE;
MapPrototypeSet(promiseMap, oldPromiseId, oldPromise);
}
// Set new promise
return promiseRing[idx] = newPromise();
}
function getPromise(promiseId) {
// Check if out of ring bounds, fallback to map
const outOfBounds = promiseId < nextPromiseId - RING_SIZE;
if (outOfBounds) {
const promise = MapPrototypeGet(promiseMap, promiseId);
MapPrototypeDelete(promiseMap, promiseId);
return promise;
}
// Otherwise take from ring
const idx = promiseId % RING_SIZE;
const promise = promiseRing[idx];
promiseRing[idx] = NO_PROMISE;
return promise;
}
function newPromise() {
let resolve, reject;
const promise = new Promise((resolve_, reject_) => {
resolve = resolve_;
reject = reject_;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
}
function hasPromise(promiseId) {
// Check if out of ring bounds, fallback to map
const outOfBounds = promiseId < nextPromiseId - RING_SIZE;
if (outOfBounds) {
return MapPrototypeHas(promiseMap, promiseId);
}
// Otherwise check it in ring
const idx = promiseId % RING_SIZE;
return promiseRing[idx] != NO_PROMISE;
}
const macrotaskCallbacks = [];
const nextTickCallbacks = [];
function setMacrotaskCallback(cb) {
ArrayPrototypePush(macrotaskCallbacks, cb);
}
function setNextTickCallback(cb) {
ArrayPrototypePush(nextTickCallbacks, cb);
}
// This function has variable number of arguments. The last argument describes
// if there's a "next tick" scheduled by the Node.js compat layer. Arguments
// before last are alternating integers and any values that describe the
// responses of async ops.
function eventLoopTick() {
// First respond to all pending ops.
for (let i = 0; i < arguments.length - 1; i += 2) {
const promiseId = arguments[i];
const res = arguments[i + 1];
const promise = getPromise(promiseId);
promise.resolve(res);
}
// Drain nextTick queue if there's a tick scheduled.
if (arguments[arguments.length - 1]) {
for (let i = 0; i < nextTickCallbacks.length; i++) {
nextTickCallbacks[i]();
}
} else {
ops.op_run_microtasks();
}
// Finally drain macrotask queue.
for (let i = 0; i < macrotaskCallbacks.length; i++) {
const cb = macrotaskCallbacks[i];
while (true) {
const res = cb();
ops.op_run_microtasks();
if (res === true) {
break;
}
}
}
}
function registerErrorClass(className, errorClass) {
registerErrorBuilder(className, (msg) => new errorClass(msg));
}
function registerErrorBuilder(className, errorBuilder) {
if (typeof errorMap[className] !== "undefined") {
throw new TypeError(`Error class for "${className}" already registered`);
}
errorMap[className] = errorBuilder;
}
function buildCustomError(className, message, code) {
let error;
try {
error = errorMap[className]?.(message);
} catch (e) {
throw new Error(
`Unsable to build custom error for "${className}"\n ${e.message}`,
);
}
// Strip buildCustomError() calls from stack trace
if (typeof error == "object") {
ErrorCaptureStackTrace(error, buildCustomError);
if (code) {
error.code = code;
}
}
return error;
}
function unwrapOpError(hideFunction) {
return (res) => {
// .$err_class_name is a special key that should only exist on errors
const className = res?.$err_class_name;
if (!className) {
return res;
}
const errorBuilder = errorMap[className];
const err = errorBuilder ? errorBuilder(res.message) : new Error(
`Unregistered error class: "${className}"\n ${res.message}\n Classes of errors returned from ops should be registered via Deno.core.registerErrorClass().`,
);
// Set .code if error was a known OS error, see error_codes.rs
if (res.code) {
err.code = res.code;
}
// Strip unwrapOpResult() and errorBuilder() calls from stack trace
ErrorCaptureStackTrace(err, hideFunction);
throw err;
};
}
function unwrapOpResultNewPromise(id, res, hideFunction) {
// .$err_class_name is a special key that should only exist on errors
if (res?.$err_class_name) {
const className = res.$err_class_name;
const errorBuilder = errorMap[className];
const err = errorBuilder ? errorBuilder(res.message) : new Error(
`Unregistered error class: "${className}"\n ${res.message}\n Classes of errors returned from ops should be registered via Deno.core.registerErrorClass().`,
);
// Set .code if error was a known OS error, see error_codes.rs
if (res.code) {
err.code = res.code;
}
// Strip unwrapOpResult() and errorBuilder() calls from stack trace
ErrorCaptureStackTrace(err, hideFunction);
return PromiseReject(err);
}
const promise = PromiseResolve(res);
promise[promiseIdSymbol] = id;
return promise;
}
/*
Basic codegen.
TODO(mmastrac): automate this (handlebars?)
let s = "";
const vars = "abcdefghijklm";
for (let i = 0; i < 10; i++) {
let args = "";
for (let j = 0; j < i; j++) {
args += `${vars[j]},`;
}
s += `
case ${i}:
fn = function async_op_${i}(${args}) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, ${args});
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_${i});
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_${i});
return PromiseReject(err);
}
let promise = PromisePrototypeThen(setPromise(id), unwrapOpError(eventLoopTick));
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
`;
}
*/
// This function is called once per async stub
function asyncStub(opName, args) {
setUpAsyncStub(opName);
return ReflectApply(ops[opName], undefined, args);
}
function setUpAsyncStub(opName) {
const originalOp = asyncOps[opName];
let fn;
// The body of this switch statement can be generated using the script above.
switch (originalOp.length - 1) {
case 0:
fn = function async_op_0() {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_0);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_0);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 1:
fn = function async_op_1(a) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_1);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_1);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 2:
fn = function async_op_2(a, b) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_2);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_2);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 3:
fn = function async_op_3(a, b, c) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_3);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_3);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 4:
fn = function async_op_4(a, b, c, d) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_4);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_4);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 5:
fn = function async_op_5(a, b, c, d, e) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_5);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_5);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 6:
fn = function async_op_6(a, b, c, d, e, f) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_6);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_6);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 7:
fn = function async_op_7(a, b, c, d, e, f, g) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f, g);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_7);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_7);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 8:
fn = function async_op_8(a, b, c, d, e, f, g, h) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f, g, h);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_8);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_8);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
case 9:
fn = function async_op_9(a, b, c, d, e, f, g, h, i) {
const id = nextPromiseId++;
try {
const maybeResult = originalOp(id, a, b, c, d, e, f, g, h, i);
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, async_op_9);
}
} catch (err) {
movePromise(id);
ErrorCaptureStackTrace(err, async_op_9);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(opName, id, promise);
promise[promiseIdSymbol] = id;
return promise;
};
break;
default:
throw new Error(
`Too many arguments for async op codegen (length of ${opName} was ${
originalOp.length - 1
})`,
);
}
return (ops[opName] = fn);
}
function opAsync(name, ...args) {
const id = nextPromiseId++;
try {
const maybeResult = asyncOps[name](id, ...new SafeArrayIterator(args));
if (maybeResult !== undefined) {
movePromise(id);
return unwrapOpResultNewPromise(id, maybeResult, opAsync);
}
} catch (err) {
movePromise(id);
if (!ReflectHas(asyncOps, name)) {
return PromiseReject(new TypeError(`${name} is not a registered op`));
}
ErrorCaptureStackTrace(err, opAsync);
return PromiseReject(err);
}
let promise = PromisePrototypeThen(
setPromise(id),
unwrapOpError(eventLoopTick),
);
promise = handleOpCallTracing(name, id, promise);
promise[promiseIdSymbol] = id;
return promise;
}
function handleOpCallTracing(opName, promiseId, p) {
if (opCallTracingEnabled) {
const stack = StringPrototypeSlice(new Error().stack, 6);
MapPrototypeSet(opCallTraces, promiseId, { opName, stack });
return SafePromisePrototypeFinally(
p,
() => MapPrototypeDelete(opCallTraces, promiseId),
);
} else {
return p;
}
}
function refOp(promiseId) {
if (!hasPromise(promiseId)) {
return;
}
ops.op_ref_op(promiseId);
}
function unrefOp(promiseId) {
if (!hasPromise(promiseId)) {
return;
}
ops.op_unref_op(promiseId);
}
function resources() {
return ObjectFromEntries(ops.op_resources());
}
function metrics() {
const { 0: aggregate, 1: perOps } = ops.op_metrics();
aggregate.ops = ObjectFromEntries(ArrayPrototypeMap(
ops.op_op_names(),
(opName, opId) => [opName, perOps[opId]],
));
return aggregate;
}
let reportExceptionCallback = undefined;
// Used to report errors thrown from functions passed to `queueMicrotask()`.
// The callback will be passed the thrown error. For example, you can use this
// to dispatch an error event to the global scope.
// In other words, set the implementation for
// https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception
function setReportExceptionCallback(cb) {
if (typeof cb != "function") {
throw new TypeError("expected a function");
}
reportExceptionCallback = cb;
}
function queueMicrotask(cb) {
if (typeof cb != "function") {
throw new TypeError("expected a function");
}
return ops.op_queue_microtask(() => {
try {
cb();
} catch (error) {
if (reportExceptionCallback) {
reportExceptionCallback(error);
} else {
throw error;
}
}
});
}
// Some "extensions" rely on "BadResource" and "Interrupted" errors in the
// JS code (eg. "deno_net") so they are provided in "Deno.core" but later
// reexported on "Deno.errors"
class BadResource extends Error {
constructor(msg) {
super(msg);
this.name = "BadResource";
}
}
const BadResourcePrototype = BadResource.prototype;
class Interrupted extends Error {
constructor(msg) {
super(msg);
this.name = "Interrupted";
}
}
const InterruptedPrototype = Interrupted.prototype;
const promiseHooks = [
[], // init
[], // before
[], // after
[], // resolve
];
function setPromiseHooks(init, before, after, resolve) {
const hooks = [init, before, after, resolve];
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
// Skip if no callback was provided for this hook type.
if (hook == null) {
continue;
}
// Verify that the type of `hook` is a function.
if (typeof hook !== "function") {
throw new TypeError(`Expected function at position ${i}`);
}
// Add the hook to the list.
ArrayPrototypePush(promiseHooks[i], hook);
}
const wrappedHooks = ArrayPrototypeMap(promiseHooks, (hooks) => {
switch (hooks.length) {
case 0:
return undefined;
case 1:
return hooks[0];
case 2:
return create2xHookWrapper(hooks[0], hooks[1]);
case 3:
return create3xHookWrapper(hooks[0], hooks[1], hooks[2]);
default:
return createHookListWrapper(hooks);
}
// The following functions are used to create wrapper functions that call
// all the hooks in a list of a certain length. The reason to use a
// function that creates a wrapper is to minimize the number of objects
// captured in the closure.
function create2xHookWrapper(hook1, hook2) {
return function (promise, parent) {
hook1(promise, parent);
hook2(promise, parent);
};
}
function create3xHookWrapper(hook1, hook2, hook3) {
return function (promise, parent) {
hook1(promise, parent);
hook2(promise, parent);
hook3(promise, parent);
};
}
function createHookListWrapper(hooks) {
return function (promise, parent) {
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
hook(promise, parent);
}
};
}
});
ops.op_set_promise_hooks(
wrappedHooks[0],
wrappedHooks[1],
wrappedHooks[2],
wrappedHooks[3],
);
}
// Eagerly initialize ops for snapshot purposes
for (const opName of new SafeArrayIterator(ObjectKeys(asyncOps))) {
setUpAsyncStub(opName);
}
function generateAsyncOpHandler(/* opNames... */) {
const fastOps = {};
for (const opName of new SafeArrayIterator(arguments)) {
if (ops[opName] === undefined) {
throw new Error(`Unknown or disabled op '${opName}'`);
}
if (asyncOps[opName] !== undefined) {
fastOps[opName] = setUpAsyncStub(opName);
} else {
fastOps[opName] = ops[opName];
}
}
return fastOps;
}
const {
op_close: close,
op_try_close: tryClose,
op_read: read,
op_read_all: readAll,
op_write: write,
op_write_all: writeAll,
op_read_sync: readSync,
op_write_sync: writeSync,
op_shutdown: shutdown,
} = generateAsyncOpHandler(
"op_close",
"op_try_close",
"op_read",
"op_read_all",
"op_write",
"op_write_all",
"op_read_sync",
"op_write_sync",
"op_shutdown",
);
// Extra Deno.core.* exports
const core = ObjectAssign(globalThis.Deno.core, {
asyncStub,
generateAsyncOpHandler,
opAsync,
resources,
metrics,
registerErrorBuilder,
registerErrorClass,
buildCustomError,
eventLoopTick,
BadResource,
BadResourcePrototype,
Interrupted,
InterruptedPrototype,
enableOpCallTracing,
isOpCallTracingEnabled,
opCallTraces,
refOp,
unrefOp,
setReportExceptionCallback,
setPromiseHooks,
close,
tryClose,
read,
readAll,
write,
writeAll,
readSync,
writeSync,
shutdown,
print: (msg, isErr) => ops.op_print(msg, isErr),
setMacrotaskCallback,
setNextTickCallback,
runMicrotasks: () => ops.op_run_microtasks(),
hasTickScheduled: () => ops.op_has_tick_scheduled(),
setHasTickScheduled: (bool) => ops.op_set_has_tick_scheduled(bool),
evalContext: (
source,
specifier,
) => ops.op_eval_context(source, specifier),
createHostObject: () => ops.op_create_host_object(),
encode: (text) => ops.op_encode(text),
decode: (buffer) => ops.op_decode(buffer),
serialize: (
value,
options,
errorCallback,
) => ops.op_serialize(value, options, errorCallback),
deserialize: (buffer, options) => ops.op_deserialize(buffer, options),
getPromiseDetails: (promise) => ops.op_get_promise_details(promise),
getProxyDetails: (proxy) => ops.op_get_proxy_details(proxy),
isProxy: (value) => ops.op_is_proxy(value),
memoryUsage: () => ops.op_memory_usage(),
setWasmStreamingCallback: (fn) => ops.op_set_wasm_streaming_callback(fn),
abortWasmStreaming: (
rid,
error,
) => ops.op_abort_wasm_streaming(rid, error),
destructureError: (error) => ops.op_destructure_error(error),
opNames: () => ops.op_op_names(),
eventLoopHasMoreWork: () => ops.op_event_loop_has_more_work(),
setPromiseRejectCallback: (fn) => ops.op_set_promise_reject_callback(fn),
byteLength: (str) => ops.op_str_byte_length(str),
build,
setBuildInfo,
});
ObjectAssign(globalThis.__bootstrap, { core });
const internals = {};
ObjectAssign(globalThis.__bootstrap, { internals });
ObjectAssign(globalThis.Deno, { core });
// Direct bindings on `globalThis`
ObjectAssign(globalThis, { queueMicrotask });
setQueueMicrotask(queueMicrotask);
})(globalThis);