From d14c2ee05f7c6cb720ef6721433df5351ab2f4b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:59:13 -0500 Subject: [PATCH] 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. --- packages/runtime/src/scr_async_dyn.c | 70 +++++++++++++++---- .../corpus/7111-als-bind-snapshot-options.ts | 7 ++ .../corpus/7112-als-bind-snapshot-dynamic.mjs | 43 ++++++++++++ 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index c47626557..58a4ac3f8 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -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; } diff --git a/tests/corpus/7111-als-bind-snapshot-options.ts b/tests/corpus/7111-als-bind-snapshot-options.ts index d1c6dbbf2..cad93cd5c 100644 --- a/tests/corpus/7111-als-bind-snapshot-options.ts +++ b/tests/corpus/7111-als-bind-snapshot-options.ts @@ -48,6 +48,13 @@ const unnamed = new AsyncLocalStorage(); console.log(JSON.stringify(unnamed.name), unnamed.getStore()); const empty = new AsyncLocalStorage({}); 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(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())); diff --git a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs index 22e3238b8..1e20aefd3 100644 --- a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs +++ b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs @@ -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) =>