endo icon indicating copy to clipboard operation
endo copied to clipboard

Investigation for Node v24 AsyncLocalStorage compatibility issue

Open jcorbin opened this issue 1 month ago • 2 comments

I've managed to at least turn a couple of banal type programming bugs into a perhaps more diagnostic semantic failure of the test:

  • looks like _enable is no longer a thing, but the new class code does have a disable method
  • not sure how the kResourceStore property setter used to get called, but looks like it doesn't anymore...
  • ...so all those getStoreMap(this).get // or set or whatever sites that were pledged a non-undefined blew up

This gets us to "at least now it's just our own expectation that's failing, rather than the test crashing" like:

  ✘ [fail]: async_hooks › AsyncLocalStorage patch
  ✔ async_hooks › async_hooks Promise patch (616ms)
  ✔ bundle › Can be bundled (594ms)
  ─

  async_hooks › AsyncLocalStorage patch

  Difference (- actual, + expected):

  - undefined
  + 1

  [object Object]
    at packages/init/test/async_hooks.test.js:103:7
    at async packages/init/test/async_hooks.test.js:88:3

  ─

  1 test failed

jcorbin avatar Nov 21 '25 16:11 jcorbin

For reference that debug print currently spits:

WAT AsyncLocalStorage {} { _propagate: [Function: _propagate] } [class AsyncLocalStorage] class AsyncLocalStorage {
  #defaultValue = undefined;
  #name = undefined;

  /**
   * @typedef {object} AsyncLocalStorageOptions
   * @property {any} [defaultValue] - The default value to use when no value is set.
   * @property {string} [name] - The name of the storage.
   */
  /**
   * @param {AsyncLocalStorageOptions} [options]
   */
  constructor(options = {}) {
    validateObject(options, 'options');
    this.#defaultValue = options.defaultValue;

    if (options.name !== undefined) {
      this.#name = `${options.name}`;
    }
  }

  /** @type {string} */
  get name() { return this.#name || ''; }

  static bind(fn) {
    return AsyncResource.bind(fn);
  }

  static snapshot() {
    return AsyncLocalStorage.bind((cb, ...args) => cb(...args));
  }

  disable() {
    AsyncContextFrame.disable(this);
  }

  enterWith(data) {
    const frame = new AsyncContextFrame(this, data);
    AsyncContextFrame.set(frame);
  }

  run(data, fn, ...args) {
    const prior = this.getStore();
    if (ObjectIs(prior, data)) {
      return ReflectApply(fn, null, args);
    }
    this.enterWith(data);
    try {
      return ReflectApply(fn, null, args);
    } finally {
      this.enterWith(prior);
    }
  }

  exit(fn, ...args) {
    return this.run(undefined, fn, ...args);
  }

  getStore() {
    const frame = AsyncContextFrame.current();
    if (!frame?.has(this)) {
      return this.#defaultValue;
    }
    return frame?.get(this);
  }
}

jcorbin avatar Nov 21 '25 16:11 jcorbin

I recently heard that async_hooks no longer gets triggered by starting the debugger, so we may be able to simply nuke the async_hooks patch out of existence.

Opened https://github.com/endojs/endo/issues/3012

mhofman avatar Nov 25 '25 22:11 mhofman