Read AsyncLocalStorage options as JavaScript does and call snapshot callbacks without a receiver

new AsyncLocalStorage(options) read defaultValue and name as own data
properties, so inherited values and getters were ignored, and a name
that was not a string reached the compiled code unconverted. The options
are now read with a full property get (prototypes, getters, proxies,
the snapshot of a typed record or class instance) in Node's order:
defaultValue once, name to see whether it is defined and again for its
value. The name is kept as String(name); a Symbol throws Node's
TypeError, and an exception from a getter propagates. A class instance
whose class itself declares one of the two members, which a capsule does
not expose to the runtime, is refused with a "not supported yet" error.

A snapshot's runner called its callback with the runner's own receiver;
Node applies the callback to an undefined receiver.
This commit is contained in:
Chris Tate
2026-10-22 09:59:13 -05:00
parent fa98044a5c
commit d14c2ee05f
3 changed files with 107 additions and 13 deletions
+57 -13
View File
@@ -71,17 +71,61 @@ double scr_als_new(void) {
return ++scr_als_counter;
}
/* options[key] as JavaScript reads it (+1, or NULL with the exception
* pending): inherited properties and getters included. A class instance
* reads its own fields through its snapshot; a member its class declares
* (a getter or a method) has no runtime view here and is refused. */
static ScrDyn *scr_als_option(ScrDyn *options, const char *key, size_t key_len) {
ScrStr *name = scr_str_new(key, key_len);
if (options->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_has(options, name)) {
char message[160];
snprintf(message, sizeof message,
"AsyncLocalStorage options whose class declares '%s' are not supported yet", key);
scr_str_release(name);
scr_throw_error_msg(SCR_ERR_ERROR, message, strlen(message));
return NULL;
}
ScrDyn *key_value = scr_dyn_new_str(name);
scr_str_release(name);
ScrDyn *value = scr_dyn_reflect_get(options, key_value, options);
scr_dyn_release(key_value);
if (value == NULL && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined());
return value;
}
/* new AsyncLocalStorage(options): Node validates that options is an
* object, then keeps options.defaultValue and options.name. NaN with the
* exception pending when options is not an object. */
* object, then reads options.defaultValue once and options.name twice
* (whether it is defined, then its value), keeping String(name). NaN with
* the exception pending when options is not an object or a read throws. */
double scr_als_new_options(const ScrDyn *options) {
if (options->kind == SCR_DYN_UNDEF) return scr_als_new();
/* A typed record or class instance reads through its snapshot. */
ScrDyn *snapshot = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL;
if (snapshot) options = snapshot;
if (options->kind != SCR_DYN_OBJ) {
if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_TYPED_REF && options->kind != SCR_DYN_PROXY) {
scr_dyn_arg_type_fail("options", "of type object", options);
scr_dyn_release(snapshot);
return NAN;
}
ScrDyn *value = scr_als_option((ScrDyn *)options, "defaultValue", 12);
if (!value) return NAN;
ScrDyn *name = scr_als_option((ScrDyn *)options, "name", 4);
if (name && name->kind != SCR_DYN_UNDEF) {
scr_dyn_release(name);
name = scr_als_option((ScrDyn *)options, "name", 4);
if (name && name->kind == SCR_DYN_SYMBOL) {
static const char message[] = "Cannot convert a Symbol value to a string";
scr_dyn_release(name);
name = NULL;
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
} else if (name) {
ScrStr *text = scr_dyn_string_coerce_js(name);
scr_dyn_release(name);
name = text ? scr_dyn_new_str(text) : NULL;
scr_str_release(text);
}
} else {
scr_dyn_release(name);
name = scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined());
}
if (!name) {
scr_dyn_release(value);
return NAN;
}
double id = scr_als_new();
@@ -94,12 +138,9 @@ double scr_als_new_options(const ScrDyn *options) {
scr_als_options = grown;
scr_als_options_len = len;
}
ScrDyn *value = scr_dyn_obj_get(options, "defaultValue", 12);
ScrDyn *name = scr_dyn_obj_get(options, "name", 4);
scr_als_options[index].default_value = scr_dyn_retain(value ? value : scr_dyn_undefined());
scr_als_options[index].name =
name && name->kind != SCR_DYN_UNDEF ? scr_dyn_retain(name) : NULL;
scr_dyn_release(snapshot);
scr_als_options[index].default_value = value; /* the read's +1 moves */
scr_als_options[index].name = name->kind != SCR_DYN_UNDEF ? name : NULL;
if (name->kind == SCR_DYN_UNDEF) scr_dyn_release(name);
return id;
}
@@ -235,7 +276,10 @@ static ScrDyn *scr_als_snapshot_call(ScrClosure *self, ScrDyn *const *args, size
ScrAlsCtx **slot = SCR_ALS_SLOT();
ScrAlsCtx *prev = *slot;
*slot = scr_box_get_ref(self->caps[0]); /* the slot owns this reference */
/* Node applies cb to an undefined receiver, whatever the runner's own. */
scr_dyn_this_push_dyn(scr_dyn_undefined());
ScrDyn *result = scr_dyn_call(cb, args + 1, argc - 1, "callback");
scr_dyn_this_pop();
scr_als_restore(prev);
return result;
}
@@ -48,6 +48,13 @@ const unnamed = new AsyncLocalStorage<number>();
console.log(JSON.stringify(unnamed.name), unnamed.getStore());
const empty = new AsyncLocalStorage<number>({});
console.log(JSON.stringify(empty.name), empty.getStore());
// A class instance's fields are options too.
class StoreOptions {
defaultValue = 7;
name = "from-class";
}
const fromClass = new AsyncLocalStorage<number>(new StoreOptions());
console.log(fromClass.name, fromClass.getStore());
// Bound functions and snapshots carry the context across asynchronous work.
const later = store.run("timer", () => AsyncLocalStorage.bind(() => store.getStore()));
@@ -43,6 +43,49 @@ console.log(named.name, named.getStore());
named.run(5, () => console.log(named.getStore()));
console.log(new AsyncLocalStorage(undefined).name === "");
// The options are read as JavaScript reads properties: inherited values
// and getters count, defaultValue is read once and name twice, and the
// name becomes a string.
const inherited = new AsyncLocalStorage(Object.create({ defaultValue: "inherited", name: "proto" }));
console.log(inherited.getStore(), inherited.name);
const reads = [];
const viaGetters = new AsyncLocalStorage({
get defaultValue() {
reads.push("defaultValue");
return "getter";
},
get name() {
reads.push("name");
return "getter-name";
},
});
console.log(viaGetters.getStore(), viaGetters.name, reads.join(","));
const coerced = [5, null, { toString: () => "object" }].map((name) => new AsyncLocalStorage({ name }).name);
console.log(JSON.stringify(coerced));
for (const options of [
{
get defaultValue() {
throw new Error("from a getter");
},
},
{ name: Symbol("s") },
]) {
try {
new AsyncLocalStorage(options);
console.log("constructed");
} catch (error) {
console.log(error.name, error.message);
}
}
// A snapshot calls its callback with an undefined receiver, also when the
// snapshot itself is called as a method.
const kind = function () {
return this === undefined ? "undefined" : typeof this;
};
const holder = { snap, tag: "holder" };
console.log(snap(kind), holder.snap(kind), snap.call({ tag: "receiver" }, kind));
// An untyped callback runs in the stores of its call.
store.run("readFile", () =>
readFile(import.meta.filename, "utf8", (error, text) =>