Add AsyncDisposableStack

new AsyncDisposableStack() compiles to a native stack handle with use,
adopt, defer, move, disposeAsync, disposed and [Symbol.asyncDispose], and
works with `await using`. disposeAsync runs the resources in reverse order
and awaits each result as Node does: the next resource runs one microtask
turn after the result settles, a callback that throws is not awaited, and
use() of null or undefined costs one await at the end when nothing else
was awaited. Failures chain into a SuppressedError, and an invalid receiver
rejects the returned promise.

use() of a class instance calls its [Symbol.asyncDispose] method, or its
[Symbol.dispose] method when it has no async one. The stack lives in a new
runtime unit, scr_async_disposable.c, linked when a program uses both
symbols and checked-dynamic promises; it shares DisposableStack's storage
through scr_disposable.h. Library mode refuses it with the rest of the
checked-dynamic async surface.
This commit is contained in:
Chris Tate
2026-10-18 00:57:21 -05:00
parent 8077a9bfff
commit 7eeca8eb43
19 changed files with 692 additions and 98 deletions
@@ -1 +1 @@
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `AsyncDisposableStack`, are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws.
- **Resource disposal:** Top-level and switch-clause `using` declarations are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws. `AsyncDisposableStack` supports `use`, `adopt`, `defer`, `move`, `disposeAsync`, `disposed` and `[Symbol.asyncDispose]`, and works with `await using`; `disposeAsync` awaits each disposal's result on the microtask turns Node uses, and `use` of a class instance calls its `[Symbol.asyncDispose]` method, or its `[Symbol.dispose]` method when it has no async one. In library mode `AsyncDisposableStack` is unsupported.
@@ -1328,6 +1328,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.usingDispose": "scr_dyn_using_dispose",
"disposable.stackNew": "scr_disposable_stack_new",
"disposable.stackIs": "scr_disposable_stack_is",
"disposable.asyncStackNew": "scr_async_disposable_stack_new",
"disposable.asyncStackIs": "scr_async_disposable_stack_is",
"disposable.stackUseCallback": "scr_disposable_stack_use_callback",
"dyn.reflectSet": "scr_dyn_reflect_set",
"dyn.reflectDefine": "scr_dyn_reflect_define",
@@ -538,6 +538,8 @@ export async function compileCInternal(
// their exact size class.
...(opts.emitter || opts.workers || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []),
...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []),
// AsyncDisposableStack calls into both scr_symbol.c and scr_async_dyn.c.
...(opts.symbol && opts.dynAsync ? [rt(join(rtDir, "scr_async_disposable.c"))] : []),
...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []),
...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []),
// The legacy url unit references the regex, querystring and dyn-async
@@ -5,44 +5,66 @@ import { newFnCtx } from "../function-context.js";
import { BOOL, DYN, VOID, type IrExpr, type IrType } from "../../../ir/ir.js";
import { varRef } from "../../../ir/build.js";
/** Whether `node`'s static type is the standard library's DisposableStack. */
function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean {
type StackClass = "DisposableStack" | "AsyncDisposableStack";
const STACK_CLASSES: readonly StackClass[] = ["DisposableStack", "AsyncDisposableStack"];
/** The standard-library stack class `node`'s static type names, or null. */
function stdlibStackClassOf(lowerer: Lowerer, node: ts.Expression): StackClass | null {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true;
const name = STACK_CLASSES.find((cls) => cls === symbol?.name);
if (name && symbol && lowerer.isStdlibSymbol(symbol)) return name;
}
return false;
return null;
}
/** `new DisposableStack()`: a native stack handle (scr_symbol.c). Its
* members are function values and getters of the handle, reached through
* the checked-dynamic method and property paths. Null for other
* constructors. */
/** `new DisposableStack()` and `new AsyncDisposableStack()`: native stack
* handles (scr_symbol.c, scr_async_disposable.c). Their members are
* function values and getters of the handle, reached through the
* checked-dynamic method and property paths. Null for other constructors. */
export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null;
if (!ts.isIdentifier(expr.expression)) return null;
const text = expr.expression.text;
const name = STACK_CLASSES.find((cls) => cls === text);
if (!name) return null;
const symbol = lowerer.resolveValueSymbol(expr.expression);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
if ((expr.arguments ?? []).length !== 0) return null;
return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) };
return {
kind: "libCall",
fn: name === "DisposableStack" ? "disposable.stackNew" : "disposable.asyncStackNew",
args: [],
type: DYN,
loc: locOf(expr),
};
}
/** `x instanceof DisposableStack`, or null when the right side is not the
* standard library's DisposableStack. */
/** `x instanceof DisposableStack` and `x instanceof AsyncDisposableStack`,
* or null when the right side is neither standard-library class. */
export function lowerDisposableStackInstanceOf(
lowerer: Lowerer,
expr: ts.BinaryExpression,
): IrExpr | null {
if (!lowerer.isStdlibGlobal(expr.right, "DisposableStack")) return null;
const name = STACK_CLASSES.find((cls) => lowerer.isStdlibGlobal(expr.right, cls));
if (!name) return null;
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return { kind: "libCall", fn: "disposable.stackIs", args: [value], type: BOOL, loc: locOf(expr) };
return {
kind: "libCall",
fn: name === "DisposableStack" ? "disposable.stackIs" : "disposable.asyncStackIs",
args: [value],
type: BOOL,
loc: locOf(expr),
};
}
/** `stack.use(value)` where `value` is a class instance. The dyn view of a
* class instance has no methods, so the stack receives a function that
* calls the class's [Symbol.dispose] method on the value, and the call
* answers the value at its static type. Other values take the
* checked-dynamic method path, which reads the value's [Symbol.dispose].
* Null when the call is not that form. */
* calls the class's [Symbol.dispose] method on the value (on an
* AsyncDisposableStack, its [Symbol.asyncDispose] method when it has one,
* whose promise the disposal awaits), and the call answers the value at
* its static type. Other values take the checked-dynamic method path,
* which reads the value's methods. Null when the call is not that form. */
export function lowerDisposableStackUseCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -52,7 +74,8 @@ export function lowerDisposableStackUseCall(
if (access.name.text !== "use" || call.arguments.length !== 1) return null;
const argNode = call.arguments[0]!;
if (ts.isSpreadElement(argNode)) return null;
if (!isStdlibDisposableStack(lowerer, access.expression)) return null;
const stackClass = stdlibStackClassOf(lowerer, access.expression);
if (!stackClass) return null;
const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
if (valueT?.kind === "union") {
const arms = lowerer.unions.get(valueT.unionId)?.arms ?? [];
@@ -69,34 +92,41 @@ export function lowerDisposableStackUseCall(
const className = valueT.className;
const info = lowerer.classes.get(className);
if (!info) lowerer.flushDeferredClass(className);
const found = info ? lowerer.findMethodOn(info, "sym:dispose") : null;
const asyncFound =
stackClass === "AsyncDisposableStack" && info
? lowerer.findMethodOn(info, "sym:asyncDispose")
: null;
const member = asyncFound ? "sym:asyncDispose" : "sym:dispose";
const found = asyncFound ?? (info ? lowerer.findMethodOn(info, "sym:dispose") : null);
if (!found) return null;
const memberText = asyncFound ? "[Symbol.asyncDispose]" : "[Symbol.dispose]";
if (found.sig.params.length !== 0) {
lowerer.unsupported("SC1090", argNode, "[Symbol.dispose] methods with parameters");
lowerer.unsupported("SC1090", argNode, `${memberText} methods with parameters`);
}
if (asyncFound && found.sig.ret.kind !== "promise") {
lowerer.unsupported("SC1090", argNode, `${memberText} methods whose result is not a Promise`);
}
const helperRet: IrType = asyncFound ? found.sig.ret : VOID;
const stack = lowerer.lowerExpr(access.expression);
const value = lowerer.lowerExpr(argNode);
const key = `disposableStack.use:${className}`;
const key = `disposableStack.use:${member}:${className}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%disposableStack.use.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, helper);
const context = newFnCtx(false, null, null, VOID);
const context = newFnCtx(false, null, null, helperRet);
lowerer.fnStack.push(context);
try {
const param = lowerer.declareHiddenLocal("value", valueT);
const self = varRef(param.id, valueT, loc);
const call = lowerer.accessorCall(className, member, self, [], found.sig.ret, loc);
lowerer.liftedFns.push({
name: helper,
params: [{ localId: param.id, name: "value", type: valueT }],
returnType: VOID,
returnType: helperRet,
locals: context.locals,
body: [
{
kind: "exprStmt",
expr: lowerer.accessorCall(className, "sym:dispose", self, [], found.sig.ret, loc),
loc,
},
asyncFound ? { kind: "return", value: call, loc } : { kind: "exprStmt", expr: call, loc },
],
loc,
});
@@ -106,7 +136,7 @@ export function lowerDisposableStackUseCall(
}
const stackLocal = lowerer.declareHiddenLocal("%disposableStack", DYN);
const valueLocal = lowerer.declareHiddenLocal("%disposableValue", valueT);
const fnT: IrType = { kind: "func", params: [valueT], ret: VOID };
const fnT: IrType = { kind: "func", params: [valueT], ret: helperRet };
const dispose: IrExpr = { kind: "closure", fnName: helper, captures: [], type: fnT, loc };
return {
kind: "seqExpr",
@@ -2377,13 +2377,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// representation and consumed by the console method lowerings. The
// stdlib Disposable (events.addAbortListener's result) and
// AsyncDisposable are checked objects carrying their Symbol.dispose or
// Symbol.asyncDispose method. A DisposableStack is a native handle whose
// methods are function values of its checked-dynamic view.
// Symbol.asyncDispose method. DisposableStack and AsyncDisposableStack
// are native handles whose methods are function values of their
// checked-dynamic views.
if (
(psym?.name === "Console" ||
psym?.name === "Disposable" ||
psym?.name === "AsyncDisposable" ||
psym?.name === "DisposableStack") &&
psym?.name === "DisposableStack" ||
psym?.name === "AsyncDisposableStack") &&
checker
.declarationsOf(psym)
.some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))
@@ -3214,12 +3214,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* `await using`. Both throw Node's TypeErrors or the method's error. */
"dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN },
"dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN },
/** DisposableStack (scr_symbol.c): a new stack handle; whether a value is
* one; and `stack.use(value)` for a class instance, registering the
* compiler's disposal function for it. The last throws Node's
* ReferenceError on a disposed stack. */
/** DisposableStack (scr_symbol.c) and AsyncDisposableStack
* (scr_async_disposable.c): a new stack handle; whether a value is one;
* and `stack.use(value)` for a class instance on either stack,
* registering the compiler's disposal function for it. The last throws
* Node's ReferenceError on a disposed stack. */
"disposable.stackNew": { argTypes: [], result: DYN },
"disposable.stackIs": { argTypes: [DYN], result: BOOL },
"disposable.asyncStackNew": { argTypes: [], result: DYN },
"disposable.asyncStackIs": { argTypes: [DYN], result: BOOL },
"disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL },
"dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL },
"dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL },
@@ -43,6 +43,7 @@ export interface RuntimeFeatures {
}
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"disposable.asyncStackNew",
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5902-async-disposable-stack.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5903-async-disposable-stack-js.js"
],
"diags": []
}
@@ -80,6 +80,8 @@ const optional = [
["scr_events_emitter.c", "emitter"],
["scr_dyn_handle.c", any("emitter", "netEffective")],
["scr_symbol.c", "symbol"],
// AsyncDisposableStack calls into both scr_symbol.c and scr_async_dyn.c.
["scr_async_disposable.c", all("symbol", "dynAsync")],
["scr_bigint_assert.c", all("assert", "bigint")],
["scr_qs.c", "qs"],
// The legacy url unit lowercases and normalizes through the regex unit
@@ -70,6 +70,8 @@ const OPTIONAL = [
["scr_events_emitter.c", "emitter"],
["scr_dyn_handle.c", any("emitter", "netEffective")],
["scr_symbol.c", "symbol"],
// AsyncDisposableStack calls into both scr_symbol.c and scr_async_dyn.c.
["scr_async_disposable.c", all("symbol", "dynAsync")],
["scr_bigint_assert.c", all("assert", "bigint")],
["scr_qs.c", "qs"],
// The legacy url unit lowercases and normalizes through the regex unit
+276
View File
@@ -0,0 +1,276 @@
/* AsyncDisposableStack: the DisposableStack storage (scr_disposable.h)
* behind its own handle, with disposeAsync in place of dispose.
* disposeAsync runs the entries in reverse order and awaits each result as
* an async function would: the next entry runs one microtask turn after
* the result settles, a callback that throws is not awaited, and an absent
* entry (use() of null or undefined) costs one await at the end when no
* other entry was awaited. Failures chain into a SuppressedError as in
* DisposableStack, and the returned promise settles after the last entry.
* Linked with scr_symbol.c and scr_async_dyn.c, whose functions it calls. */
#include "scr_runtime.h"
#include "scr_disposable.h"
#include <stdlib.h>
#include <string.h>
typedef struct {
ScrDisposableStack *storage; /* the entries, moved out of the stack */
size_t next; /* entries [0, next) are still to run */
ScrCaught *error; /* the disposal error so far, or NULL */
bool needs_await, has_awaited;
ScrPromise *result; /* the promise disposeAsync returned */
ScrPromise *awaited; /* the result being awaited */
} ScrAdsRun;
static void scr_ads_step(ScrAdsRun *run);
static void scr_ads_fail(ScrAdsRun *run) {
if (run->error) scr_exc_suppress(run->error);
run->error = scr_exc_take();
}
static void scr_ads_resume(ScrFiber *self, void *opaque) {
(void)self;
ScrAdsRun *run = opaque;
run->has_awaited = true;
ScrDyn *value = scr_await_dyn(run->awaited);
scr_promise_release(run->awaited);
run->awaited = NULL;
if (value) scr_dyn_release(value);
else scr_ads_fail(run);
scr_ads_step(run);
}
/* Await `value` (moved in): a promise is awaited itself, any other value
* through a fulfilled promise, as `await` takes one turn for either. */
static void scr_ads_await(ScrAdsRun *run, ScrDyn *value) {
ScrPromise *p;
if (value->kind == SCR_DYN_PROMISE) {
p = scr_promise_retain(value->v.promise);
} else {
p = scr_promise_new();
scr_promise_fulfill_ref(p, scr_dyn_retain(value), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
}
scr_dyn_release(value);
scr_promise_mark_handled(p);
run->awaited = p;
ScrPromise *waiter = scr_async_spawn_after(p, scr_ads_resume, run);
scr_promise_release(waiter);
}
static void scr_ads_finish(ScrAdsRun *run) {
if (run->error) {
scr_rethrow(run->error);
scr_caught_release(run->error);
scr_promise_reject_pending(run->result);
} else {
scr_promise_resolve_dyn(run->result, scr_dyn_retain(scr_dyn_undefined()));
}
scr_promise_release(run->result);
scr_ds_release(run->storage);
free(run);
}
static void scr_ads_step(ScrAdsRun *run) {
while (run->next > 0) {
ScrDisposableResource *r = &run->storage->items[--run->next];
if (r->kind == SCR_DS_ABSENT) {
run->needs_await = true;
continue;
}
ScrDyn *result;
if (r->kind == SCR_DS_USE) {
result = scr_dyn_using_dispose(r->method, r->value);
} else {
scr_dyn_this_push_dyn(scr_dyn_undefined());
result = r->kind == SCR_DS_ADOPT ? scr_dyn_call(r->method, &r->value, 1, "onDisposeAsync")
: scr_dyn_call(r->method, NULL, 0, "onDisposeAsync");
scr_dyn_this_pop();
}
scr_dyn_release(r->value);
scr_dyn_release(r->method);
r->value = r->method = NULL;
r->kind = SCR_DS_ABSENT;
if (!result) {
scr_ads_fail(run);
continue;
}
scr_ads_await(run, result);
return;
}
if (run->needs_await && !run->has_awaited) {
run->needs_await = false;
scr_ads_await(run, scr_dyn_retain(scr_dyn_undefined()));
return;
}
scr_ads_finish(run);
}
static ScrDyn *scr_ads_dispose_async(const ScrDyn *self) {
ScrPromise *p = scr_promise_new();
ScrDyn *boxed = scr_dyn_new_promise(p);
ScrDisposableStack *s = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK
? self->v.handle.ptr : NULL;
if (!s) {
/* An invalid receiver rejects the promise instead of throwing. */
static const char message[] = "Receiver is not an AsyncDisposableStack";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
scr_promise_reject_pending(p);
} else if (s->disposed) {
scr_promise_resolve_dyn(p, scr_dyn_retain(scr_dyn_undefined()));
} else {
ScrAdsRun *run = calloc(1, sizeof *run);
if (!run) scr_trap("scriptc: out of memory\n");
run->storage = scr_ds_storage_new();
scr_ds_move(run->storage, s);
run->next = run->storage->len;
run->result = scr_promise_retain(p);
scr_ads_step(run);
}
scr_promise_release(p);
return boxed;
}
static SCR_TL ScrDyn *scr_ads_method_values[6];
static const char *const scr_ads_method_names[] = {"use", "adopt", "defer", "move", "disposeAsync", "disposed"};
static const uint32_t scr_ads_method_lengths[] = {1, 2, 1, 0, 0, 0};
enum { SCR_ADS_M_USE, SCR_ADS_M_ADOPT, SCR_ADS_M_DEFER, SCR_ADS_M_MOVE, SCR_ADS_M_DISPOSE, SCR_ADS_M_DISPOSED };
static ScrDisposableStack *scr_ads_alloc(void);
static ScrDyn *scr_ads_box(ScrDisposableStack *s);
static ScrDyn *scr_ads_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
unsigned index = (unsigned)scr_box_get_f64(clo->caps[0]);
ScrDyn *self = scr_dyn_this_get();
const ScrDyn *arg0 = argc > 0 ? args[0] : scr_dyn_undefined();
const ScrDyn *arg1 = argc > 1 ? args[1] : scr_dyn_undefined();
const ScrDynHandleTag tag = SCR_DYNH_ASYNC_DISPOSABLE_STACK;
ScrDyn *out = NULL;
ScrDisposableStack *s;
switch (index) {
case SCR_ADS_M_USE:
if (!(s = scr_ds_receiver(self, tag, "use", false, true))) break;
if (arg0->kind == SCR_DYN_NULL || arg0->kind == SCR_DYN_UNDEF) {
scr_ds_push(s, NULL, NULL, SCR_DS_ABSENT);
} else {
ScrDyn *method = scr_dyn_using_method(arg0, true);
if (!method) break;
scr_ds_push(s, scr_dyn_retain((ScrDyn *)arg0), method, SCR_DS_USE);
}
out = scr_dyn_retain((ScrDyn *)arg0);
break;
case SCR_ADS_M_ADOPT:
if (!(s = scr_ds_receiver(self, tag, "adopt", false, true)) || !scr_ds_callable(arg1)) break;
scr_ds_push(s, scr_dyn_retain((ScrDyn *)arg0), scr_dyn_retain((ScrDyn *)arg1), SCR_DS_ADOPT);
out = scr_dyn_retain((ScrDyn *)arg0);
break;
case SCR_ADS_M_DEFER:
if (!(s = scr_ds_receiver(self, tag, "defer", false, true)) || !scr_ds_callable(arg0)) break;
scr_ds_push(s, NULL, scr_dyn_retain((ScrDyn *)arg0), SCR_DS_DEFER);
out = scr_dyn_retain(scr_dyn_undefined());
break;
case SCR_ADS_M_MOVE: {
if (!(s = scr_ds_receiver(self, tag, "move", false, true))) break;
ScrDisposableStack *moved = scr_ads_alloc();
scr_ds_move(moved, s);
out = scr_ads_box(moved);
scr_ds_release(moved);
break;
}
default:
out = scr_ads_dispose_async(self);
break;
}
scr_dyn_release(self);
return out;
}
static ScrDyn *scr_ads_method_value(unsigned index) {
if (!scr_ads_method_values[index]) {
ScrClosure *clo = scr_closure_new(NULL, 1);
clo->caps[0] = scr_box_new(SCR_BOX_F64);
scr_box_set_f64(clo->caps[0], (double)index);
scr_ads_method_values[index] = scr_dyn_new_func(clo, scr_ads_method_call, scr_ads_method_lengths[index],
"", scr_ads_method_names[index]);
}
return scr_dyn_retain(scr_ads_method_values[index]);
}
static ScrDyn *scr_ads_get(void *ptr, const char *key, size_t len) {
ScrDisposableStack *s = ptr;
if (len == 8 && memcmp(key, "disposed", 8) == 0) return scr_dyn_new_bool(s->disposed);
for (unsigned i = 0; i < SCR_ADS_M_DISPOSED; i++) {
if (strlen(scr_ads_method_names[i]) == len && memcmp(key, scr_ads_method_names[i], len) == 0)
return scr_ads_method_value(i);
}
return NULL;
}
static bool scr_ads_set(void *ptr, const char *key, size_t len, const ScrDyn *value) {
(void)ptr;
(void)key;
(void)len;
(void)value;
return false;
}
/* [Symbol.asyncDispose] is disposeAsync itself, as in Node. */
static ScrDyn *scr_ads_symbol_get(void *ptr, const ScrDyn *key) {
(void)ptr;
if (key->kind != SCR_DYN_SYMBOL) return NULL;
ScrStr *name = scr_str_new("asyncDispose", 12);
ScrSym *async_dispose = scr_sym_well_known(name);
scr_str_release(name);
bool match = key->v.symbol.value == async_dispose;
scr_sym_release(async_dispose);
return match ? scr_ads_method_value(SCR_ADS_M_DISPOSE) : NULL;
}
static ScrDyn *scr_ads_inspect_view(void *ptr) {
(void)ptr;
return scr_dyn_new_obj();
}
static void scr_ads_teardown(void) {
for (size_t i = 0; i < sizeof scr_ads_method_values / sizeof scr_ads_method_values[0]; i++) {
scr_dyn_release(scr_ads_method_values[i]);
scr_ads_method_values[i] = NULL;
}
}
static const ScrDynHandleOps scr_ads_ops = {
.cls = "AsyncDisposableStack",
.retain = scr_ds_retain,
.release = scr_ds_release,
.get = scr_ads_get,
.set = scr_ads_set,
.inspect_view = scr_ads_inspect_view,
.json_empty_object = true,
.inspect_view_empty = true,
.symbol_get = scr_ads_symbol_get,
};
static ScrDisposableStack *scr_ads_alloc(void) {
static SCR_TL bool installed;
if (!installed) {
installed = true;
scr_dyn_handle_install(SCR_DYNH_ASYNC_DISPOSABLE_STACK, &scr_ads_ops);
scr_atexit(scr_ads_teardown);
}
return scr_ds_storage_new();
}
static ScrDyn *scr_ads_box(ScrDisposableStack *s) {
return scr_dyn_new_handle(s, SCR_DYNH_ASYNC_DISPOSABLE_STACK);
}
ScrDyn *scr_async_disposable_stack_new(void) {
ScrDisposableStack *s = scr_ads_alloc();
ScrDyn *out = scr_ads_box(s);
scr_ds_release(s);
return out;
}
bool scr_async_disposable_stack_is(const ScrDyn *value) {
return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK;
}
+51
View File
@@ -0,0 +1,51 @@
#ifndef SCR_DISPOSABLE_H
#define SCR_DISPOSABLE_H
/* Storage shared by the DisposableStack handle (scr_symbol.c) and the
* AsyncDisposableStack handle (scr_async_disposable.c). A stack is a
* collector-headered object behind a traced dyn handle: its resources hold
* callbacks that can capture the stack itself. Each entry records how
* disposal calls it: `use` calls the value's dispose method with the value
* as `this`, `adopt` calls the callback with the value, `defer` calls the
* callback with no arguments, and an absent entry (an AsyncDisposableStack
* use() of null or undefined) calls nothing. */
#include "scr_runtime.h"
enum { SCR_DS_USE, SCR_DS_ADOPT, SCR_DS_DEFER, SCR_DS_ABSENT };
typedef struct {
ScrDyn *value; /* the resource (use, adopt); NULL for defer and absent */
ScrDyn *method; /* NULL for absent */
unsigned kind;
/* Which members the last trace visited: the collector frees those
* itself, and gcfree must not read them afterwards. */
bool value_traced, method_traced;
} ScrDisposableResource;
typedef struct {
size_t rc;
bool disposed;
ScrDisposableResource *items;
size_t len, cap;
} ScrDisposableStack;
/* A new, empty stack (+1). */
ScrDisposableStack *scr_ds_storage_new(void);
void *scr_ds_retain(void *stack);
void scr_ds_release(void *stack);
/* Moves value and method in. */
void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind);
/* Moves `from`'s entries to `to` and marks `from` disposed. */
void scr_ds_move(ScrDisposableStack *to, ScrDisposableStack *from);
/* V8's rendering of a value in a TypeError. */
void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value);
/* Whether `value` is callable; false with V8's TypeError pending. */
bool scr_ds_callable(const ScrDyn *value);
/* The stack behind `self` when it has handle tag `tag`, or NULL with
* Node's TypeError (an incompatible receiver of <class>.prototype.<name>)
* or ReferenceError (a disposed stack, when `live` is required) pending. */
ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, ScrDynHandleTag tag, const char *name, bool getter,
bool live);
#endif
+6
View File
@@ -169,6 +169,7 @@ typedef enum {
SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */
SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */
SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */
SCR_DYNH_ASYNC_DISPOSABLE_STACK, /* AsyncDisposableStack (scr_async_disposable.c) */
SCR_DYNH_COUNT,
} ScrDynHandleTag;
@@ -633,6 +634,11 @@ bool scr_disposable_stack_is(const ScrDyn *value);
bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value);
bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose);
bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose);
/* AsyncDisposableStack (scr_async_disposable.c): a new stack (+1) and
* whether a value is one. scr_disposable_stack_use_callback also registers
* on an AsyncDisposableStack, whose disposal awaits the function's result. */
ScrDyn *scr_async_disposable_stack_new(void);
bool scr_async_disposable_stack_is(const ScrDyn *value);
void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored);
void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key);
bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key);
+4 -2
View File
@@ -3872,6 +3872,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) {
d->v.handle.tag = tag;
d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET ||
tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT || tag == SCR_DYNH_DISPOSABLE_STACK ||
tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK ||
((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) &&
((ScrMap *)h)->traced != 0);
return d;
@@ -5406,7 +5407,8 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET ||
d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP ||
d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_BLOB ||
d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) {
d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK ||
d->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "[object ");
@@ -5992,7 +5994,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET ||
d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP ||
d->v.handle.tag == SCR_DYNH_BLOB || d->v.handle.tag == SCR_DYNH_FILE ||
d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) {
d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK || d->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "[object ");
+53 -58
View File
@@ -12,6 +12,7 @@
* always after scr_init registered the RC audit, so atexit's LIFO order
* runs it before the audit and the audited string counts stay exact. */
#include "scr_runtime.h"
#include "scr_disposable.h"
#include <stdio.h>
#include <stdlib.h>
@@ -459,31 +460,10 @@ ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value) {
}
/* ── DisposableStack ───────────────────────────────────────────────────
* The stack is a collector-headered object behind a traced dyn handle: its
* resources hold callbacks that can capture the stack itself. Each entry
* records how disposal calls it: `use` calls the value's dispose method
* with the value as `this`, `adopt` calls the callback with the value, and
* `defer` calls the callback with no arguments. Disposal runs the entries
* in reverse order; each later failure becomes a SuppressedError over the
* error so far, and a single failure is rethrown as it was thrown. */
enum { SCR_DS_USE, SCR_DS_ADOPT, SCR_DS_DEFER };
typedef struct {
ScrDyn *value; /* the resource (use, adopt); NULL for defer */
ScrDyn *method;
unsigned kind;
/* Which members the last trace visited: the collector frees those
* itself, and gcfree must not read them afterwards. */
bool value_traced, method_traced;
} ScrDisposableResource;
typedef struct {
size_t rc;
bool disposed;
ScrDisposableResource *items;
size_t len, cap;
} ScrDisposableStack;
* Storage and entries are described in scr_disposable.h. Disposal runs
* the entries in reverse order; each later failure becomes a
* SuppressedError over the error so far, and a single failure is rethrown
* as it was thrown. */
static bool scr_ds_traced(const ScrDyn *value) {
return value && value->kind > SCR_DYN_STR && value->kind != SCR_DYN_UNDEF;
}
@@ -521,12 +501,12 @@ static void scr_ds_destroy(void *ptr) {
scr_ds_gcfree(ptr);
}
static void *scr_ds_retain(void *ptr) {
void *scr_ds_retain(void *ptr) {
((ScrDisposableStack *)ptr)->rc++;
return ptr;
}
static void scr_ds_release(void *ptr) {
void scr_ds_release(void *ptr) {
ScrDisposableStack *s = ptr;
if (!s) return;
if (--s->rc == 0) {
@@ -535,7 +515,7 @@ static void scr_ds_release(void *ptr) {
} else scr_cyc_released(s);
}
static void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind) {
void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind) {
if (s->len == s->cap) {
size_t cap = s->cap ? s->cap * 2 : 4;
ScrDisposableResource *items = realloc(s->items, cap * sizeof *items);
@@ -548,7 +528,7 @@ static void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, un
/* V8's rendering of a value in a TypeError: primitives by ToString,
* arrays as [object Array], other objects as #<Constructor>. */
static void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) {
void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) {
switch (value->kind) {
case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); return;
case SCR_DYN_NULL: scr_jb_puts(b, "null"); return;
@@ -577,22 +557,24 @@ static void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) {
}
}
static ScrDisposableStack *scr_ds_of(const ScrDyn *value) {
return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK
? value->v.handle.ptr : NULL;
static ScrDisposableStack *scr_ds_of(const ScrDyn *value, ScrDynHandleTag tag) {
return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == tag ? value->v.handle.ptr : NULL;
}
bool scr_disposable_stack_is(const ScrDyn *value) { return scr_ds_of(value) != NULL; }
bool scr_disposable_stack_is(const ScrDyn *value) {
return scr_ds_of(value, SCR_DYNH_DISPOSABLE_STACK) != NULL;
}
/* The receiver of DisposableStack.prototype.<name>, or NULL with Node's
* TypeError (an incompatible receiver) or ReferenceError (a disposed
* stack, when `live` is required) pending. */
static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name, bool getter, bool live) {
ScrDisposableStack *s = scr_ds_of(self);
ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, ScrDynHandleTag tag, const char *name, bool getter,
bool live) {
ScrDisposableStack *s = scr_ds_of(self, tag);
const char *cls = tag == SCR_DYNH_DISPOSABLE_STACK ? "DisposableStack" : "AsyncDisposableStack";
ScrJsonBuf message;
if (!s) {
scr_jb_init(&message);
scr_jb_puts(&message, getter ? "Method get DisposableStack.prototype." : "Method DisposableStack.prototype.");
scr_jb_puts(&message, getter ? "Method get " : "Method ");
scr_jb_puts(&message, cls);
scr_jb_puts(&message, ".prototype.");
scr_jb_puts(&message, name);
scr_jb_puts(&message, " called on incompatible receiver ");
scr_ds_put_value(&message, self);
@@ -600,8 +582,11 @@ static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name,
return NULL;
}
if (live && s->disposed) {
/* V8 names the base class at the end for both stack classes. */
scr_jb_init(&message);
scr_jb_puts(&message, "Cannot call DisposableStack.prototype.");
scr_jb_puts(&message, "Cannot call ");
scr_jb_puts(&message, cls);
scr_jb_puts(&message, ".prototype.");
scr_jb_puts(&message, name);
scr_jb_puts(&message, " on an already-disposed DisposableStack");
scr_throw_error(SCR_ERR_REFERENCE, scr_jb_finish(&message));
@@ -610,7 +595,7 @@ static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name,
return s;
}
static bool scr_ds_callable(const ScrDyn *value) {
bool scr_ds_callable(const ScrDyn *value) {
if (scr_using_typeof_is(value, "function", 8)) return true;
ScrJsonBuf message;
scr_jb_init(&message);
@@ -621,7 +606,7 @@ static bool scr_ds_callable(const ScrDyn *value) {
}
bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value) {
ScrDisposableStack *s = scr_ds_receiver(stack, "use", false, true);
ScrDisposableStack *s = scr_ds_receiver(stack, SCR_DYNH_DISPOSABLE_STACK, "use", false, true);
if (!s) return false;
if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) return true;
ScrDyn *method = scr_dyn_using_method(value, false);
@@ -631,21 +616,23 @@ bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value) {
}
bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose) {
ScrDisposableStack *s = scr_ds_receiver(stack, "use", false, true);
ScrDynHandleTag tag = stack->kind == SCR_DYN_HANDLE && stack->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK
? SCR_DYNH_ASYNC_DISPOSABLE_STACK : SCR_DYNH_DISPOSABLE_STACK;
ScrDisposableStack *s = scr_ds_receiver(stack, tag, "use", false, true);
if (!s) return false;
scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain((ScrDyn *)dispose), SCR_DS_ADOPT);
return true;
}
bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose) {
ScrDisposableStack *s = scr_ds_receiver(stack, "adopt", false, true);
ScrDisposableStack *s = scr_ds_receiver(stack, SCR_DYNH_DISPOSABLE_STACK, "adopt", false, true);
if (!s || !scr_ds_callable(on_dispose)) return false;
scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain((ScrDyn *)on_dispose), SCR_DS_ADOPT);
return true;
}
static bool scr_disposable_stack_defer(const ScrDyn *stack, const ScrDyn *on_dispose) {
ScrDisposableStack *s = scr_ds_receiver(stack, "defer", false, true);
ScrDisposableStack *s = scr_ds_receiver(stack, SCR_DYNH_DISPOSABLE_STACK, "defer", false, true);
if (!s || !scr_ds_callable(on_dispose)) return false;
scr_ds_push(s, NULL, scr_dyn_retain((ScrDyn *)on_dispose), SCR_DS_DEFER);
return true;
@@ -714,21 +701,16 @@ static ScrDyn *scr_ds_method_call(ScrClosure *clo, ScrDyn *const *args, size_t a
break;
case SCR_DS_M_MOVE: {
/* The new stack takes the entries; this one becomes disposed. */
ScrDisposableStack *s = scr_ds_receiver(self, "move", false, true);
ScrDisposableStack *s = scr_ds_receiver(self, SCR_DYNH_DISPOSABLE_STACK, "move", false, true);
if (!s) break;
ScrDisposableStack *moved = scr_ds_alloc();
moved->items = s->items;
moved->len = s->len;
moved->cap = s->cap;
s->items = NULL;
s->len = s->cap = 0;
s->disposed = true;
scr_ds_move(moved, s);
out = scr_ds_box(moved);
scr_ds_release(moved);
break;
}
default: {
ScrDisposableStack *s = scr_ds_receiver(self, "dispose", false, false);
ScrDisposableStack *s = scr_ds_receiver(self, SCR_DYNH_DISPOSABLE_STACK, "dispose", false, false);
if (s) out = scr_ds_dispose(s);
break;
}
@@ -803,6 +785,23 @@ static const ScrDynHandleOps scr_ds_ops = {
.symbol_get = scr_ds_symbol_get,
};
ScrDisposableStack *scr_ds_storage_new(void) {
static ScrCycType cyc_type = {&scr_ds_trace, &scr_ds_gcfree, SCR_CYC_KIND_NONE, 0};
ScrDisposableStack *s = scr_cyc_alloc(sizeof *s, &cyc_type);
s->rc = 1;
scr_obj_alloc_note();
return s;
}
void scr_ds_move(ScrDisposableStack *to, ScrDisposableStack *from) {
to->items = from->items;
to->len = from->len;
to->cap = from->cap;
from->items = NULL;
from->len = from->cap = 0;
from->disposed = true;
}
static ScrDisposableStack *scr_ds_alloc(void) {
static SCR_TL bool installed;
if (!installed) {
@@ -810,11 +809,7 @@ static ScrDisposableStack *scr_ds_alloc(void) {
scr_dyn_handle_install(SCR_DYNH_DISPOSABLE_STACK, &scr_ds_ops);
scr_atexit(scr_ds_teardown);
}
static ScrCycType cyc_type = {&scr_ds_trace, &scr_ds_gcfree, SCR_CYC_KIND_NONE, 0};
ScrDisposableStack *s = scr_cyc_alloc(sizeof *s, &cyc_type);
s->rc = 1;
scr_obj_alloc_note();
return s;
return scr_ds_storage_new();
}
static ScrDyn *scr_ds_box(ScrDisposableStack *s) { return scr_dyn_new_handle(s, SCR_DYNH_DISPOSABLE_STACK); }
+156
View File
@@ -0,0 +1,156 @@
// AsyncDisposableStack: disposeAsync interleaved with a microtask ticker (each
// awaited result, a throwing callback, absent resources, failures), class
// resources with [Symbol.asyncDispose] or [Symbol.dispose], await using over
// a stack, move, SuppressedError chains, and stacks whose callbacks capture
// the stack. Node.js prints the same.
const log: string[] = [];
async function ticker(label: string, n: number): Promise<void> {
for (let i = 0; i < n; i++) {
log.push(label + i);
await null;
}
}
async function scenario(name: string, build: (s: AsyncDisposableStack) => void): Promise<void> {
log.length = 0;
const s = new AsyncDisposableStack();
build(s);
const tick = ticker("t", 14);
const p = s.disposeAsync().then(
() => log.push("resolved"),
(e: unknown) => log.push("rejected " + (e instanceof Error ? e.message : String(e))),
);
log.push("sync end");
await p;
await tick;
console.log(name + ": " + log.join(" "));
}
async function timings(): Promise<void> {
await scenario("empty", () => {});
await scenario("one sync defer", (s) => s.defer(() => { log.push("d"); }));
await scenario("one async defer", (s) => s.defer(async () => { log.push("d"); }));
await scenario("two defers", (s) => {
s.defer(() => { log.push("d1"); });
s.defer(() => { log.push("d2"); });
});
await scenario("sync use", (s) => { s.use({ [Symbol.dispose]() { log.push("u"); } }); });
await scenario("async use", (s) => { s.use({ async [Symbol.asyncDispose]() { log.push("u"); } }); });
await scenario("null use", (s) => { s.use(null); });
await scenario("null then defer", (s) => { s.defer(() => { log.push("d"); }); s.use(null); });
await scenario("defer then null", (s) => { s.use(undefined); s.defer(() => { log.push("d"); }); });
await scenario("adopt", (s) => { s.adopt(3, (v) => { log.push("a" + v); }); });
await scenario("slow async", (s) => s.defer(async () => { log.push("d start"); await null; await null; log.push("d end"); }));
await scenario("throws", (s) => s.defer(() => { throw new Error("boom"); }));
await scenario("rejects", (s) => s.defer(async () => { throw new Error("boom"); }));
await scenario("two failures", (s) => {
s.defer(() => { throw new Error("first"); });
s.defer(async () => { throw new Error("second"); });
});
await scenario("returns promise", (s) => s.defer(() => Promise.resolve()));
await scenario("already disposed", (s) => { void s.disposeAsync(); });
}
const events: string[] = [];
class AsyncResource {
readonly name: string;
constructor(name: string) {
this.name = name;
}
async [Symbol.asyncDispose](): Promise<void> {
events.push("async start " + this.name);
await null;
events.push("async end " + this.name);
}
}
class SyncResource {
readonly name: string;
constructor(name: string) {
this.name = name;
}
[Symbol.dispose](): void {
events.push("sync " + this.name);
}
}
class Both extends SyncResource {
async [Symbol.asyncDispose](): Promise<void> {
events.push("both async " + this.name);
}
}
class FailingAsync {
async [Symbol.asyncDispose](): Promise<void> {
throw new Error("async failure");
}
}
async function eventTicker(n: number): Promise<void> {
for (let i = 0; i < n; i++) {
events.push("t" + i);
await null;
}
}
async function main(): Promise<void> {
const tick = eventTicker(10);
{
await using stack = new AsyncDisposableStack();
const a = stack.use(new AsyncResource("a"));
stack.use(new SyncResource("s"));
stack.use(new Both("b"));
stack.adopt(new SyncResource("adopted"), async (r) => {
events.push("adopt " + r.name);
});
stack.defer(() => {
events.push("defer");
});
events.push("got " + a.name + " " + stack.disposed);
}
events.push("after block");
await tick;
console.log(events.join("\n"));
events.length = 0;
const outer = new AsyncDisposableStack();
outer.use(new SyncResource("moved"));
const moved = outer.move();
console.log(outer.disposed, moved.disposed, moved instanceof AsyncDisposableStack, moved instanceof DisposableStack);
await moved.disposeAsync();
await moved.disposeAsync();
console.log(events.join(","), moved.disposed);
const failing = new AsyncDisposableStack();
failing.use(new FailingAsync());
failing.defer(() => {
throw new TypeError("sync failure");
});
failing.use(new FailingAsync());
try {
await failing.disposeAsync();
} catch (e) {
const chain: string[] = [];
let cur: unknown = e;
while (cur instanceof SuppressedError) {
chain.push((cur.error as Error).message);
cur = cur.suppressed;
}
chain.push((cur as Error).message);
console.log(chain.join(" <- "));
}
try {
await using s = new AsyncDisposableStack();
s.defer(async () => {
throw new Error("disposal");
});
throw new Error("body");
} catch (e) {
if (e instanceof SuppressedError) console.log(e.name, (e.error as Error).message, (e.suppressed as Error).message);
}
for (let i = 0; i < 3; i++) {
const cyc = new AsyncDisposableStack();
cyc.defer(() => console.log("never", cyc.disposed));
}
const shown = new AsyncDisposableStack();
console.log(shown, String(shown), JSON.stringify(shown), typeof shown.disposeAsync);
const sync = new DisposableStack();
sync.defer(() => console.log("sync stack inside async"));
await using wrapper = new AsyncDisposableStack();
wrapper.use(sync);
console.log("end of main");
}
void timings().then(() => main());
@@ -0,0 +1,48 @@
// AsyncDisposableStack from JavaScript: untyped resources and callbacks, the
// order of async and sync disposals against a microtask ticker, detached
// methods, and Node's TypeError, ReferenceError and rejection texts. Node.js
// prints the same.
const messages = new AsyncDisposableStack();
const t = (f) => { try { const r = f(); console.log("ok", r); } catch (e) { console.log(e.name + ": " + e.message); } };
const { use, defer, move, disposeAsync, adopt } = messages;
t(() => use(null));
t(() => defer.call({}, () => {}));
t(() => move.call(5));
disposeAsync.call({}).catch((e) => console.log("rejected", e.name + ": " + e.message));
t(() => messages.adopt(1, 2));
t(() => messages.use(5));
t(() => messages.use({}));
console.log(messages.disposeAsync.name, messages.disposeAsync.length, messages[Symbol.asyncDispose] === messages.disposeAsync, messages.use.length, messages.adopt.length, messages.defer.length, messages.move.length, typeof messages.dispose);
console.log(String(messages), JSON.stringify(messages), messages);
messages.disposeAsync().then(() => { t(() => messages.defer(() => {})); t(() => messages.move()); console.log(messages.disposed); });
const m = new AsyncDisposableStack(); m.defer(() => console.log("moved ran")); const n = m.move(); console.log(m.disposed, n.disposed, n instanceof AsyncDisposableStack, n instanceof DisposableStack); n.disposeAsync();
const order = [];
async function orderTicker(n) { for (let i = 0; i < n; i++) { order.push("t" + i); await null; } }
async function main() {
const tick = orderTicker(12);
const s = new AsyncDisposableStack();
s.defer(() => { order.push("sync defer"); });
s.defer(async () => { order.push("async defer start"); await null; order.push("async defer end"); });
s.use({ [Symbol.dispose]() { order.push("sync use"); } });
s.use({ async [Symbol.asyncDispose]() { order.push("async use"); } });
s.adopt(1, (v) => { order.push("adopt " + v); });
s.use(null);
const p = s.disposeAsync();
order.push("returned " + (typeof p.then));
await p;
order.push("done " + s.disposed);
await s.disposeAsync();
order.push("again");
await tick;
console.log(order.join("\n"));
const e = new AsyncDisposableStack();
e.defer(async () => { throw new Error("a"); });
e.defer(() => { throw new Error("b"); });
try { await e.disposeAsync(); } catch (err) { console.log(err.name, err.error.message, err.suppressed.message); }
try { e.use({}); } catch (err) { console.log(err.name, err.message); }
const f = new AsyncDisposableStack(); void 0; console.log(typeof f.dispose, typeof f[Symbol.asyncDispose], String(f));
try { new AsyncDisposableStack().use(3); } catch (err) { console.log(err.message); }
try { new AsyncDisposableStack().use({}); } catch (err) { console.log(err.message); }
console.log(new AsyncDisposableStack());
}
main();
+4
View File
@@ -1849,6 +1849,8 @@
"5859-disposable-stack-js.js": 3100,
"5882-string-decoder-runtime-encoding.ts": 2541,
"5883-os-set-priority.ts": 2380,
"5902-async-disposable-stack.ts": 3200,
"5903-async-disposable-stack-js.js": 3100,
"5904-async-generator-completion.ts": 2950,
"5905-array-from-async-generator.ts": 2750,
"5906-array-from-async-generator-js.js": 2950,
@@ -4496,6 +4498,8 @@
"5859-disposable-stack-js.js": 3600,
"5882-string-decoder-runtime-encoding.ts": 3854,
"5883-os-set-priority.ts": 3417,
"5902-async-disposable-stack.ts": 3550,
"5903-async-disposable-stack-js.js": 3500,
"5904-async-generator-completion.ts": 3800,
"5905-array-from-async-generator.ts": 3500,
"5906-array-from-async-generator-js.js": 3750,