Skip to content

Bad interaction between Generators.input and serializable variable reevaluation #376

@tomlarkworthy

Description

@tomlarkworthy

Commit
fb80e6a
Introduce setting a generator before running the next version in Runtime 6.0.0

However, this can clash with the common pattern of using Generator.input to extract a logical signal from a UI element (e.g. viewof)

If the UI is initialized to undefined the Generator never resolves. If the UI is then updated to expose a value, then the generator not resolving prevents it from ever generating a value, even though the UI form might be updating values and dispatching fresh events.

I noticed this because Plot can initialised with undefined values for pointer events and you might fill in data later, but then all the pointer values never update.

node.js reproduction (you have to toggle the runtime in the package.json, item get stuck in pending for runtime 6.0.0). Not sure where the best fix lies, maybe Generator.input can be fixed rather than mess with runtime? Or maybe runtime should have a timeout for long running Generators?

Further discussion on the feature was here

{
  "name": "generator-bug-repro",
  "type": "module",
  "scripts": {
    "test": "node index.mjs"
  },
  "dependencies": {
    "//":"or set to runtime version 6 when it breaks",
    "@observablehq/runtime": "5", 
    "@observablehq/stdlib": "^5.8.8"
  }
}
// Generator bug repro - comparing Observable runtime v5.9.9 vs v6.0.0
// Run with: npm run test
// (after changing the runtime dependency version in package.json)

import { Runtime } from "@observablehq/runtime";
import { Library } from "@observablehq/stdlib";

const version = process.env.RUNTIME_VERSION || "unknown";
console.log(`Using Observable runtime v${version}\n`);

// Use the real stdlib Generators
const library = new Library();
const Generators = library.Generators;

// Minimal mock input element compatible with Generators.input
// (stdlib Inputs.input needs DOM, so we create a simple EventTarget-based mock)
function createMockInput(initialValue) {
  const listeners = new Map();
  return {
    value: initialValue,
    addEventListener(type, fn) {
      if (!listeners.has(type)) listeners.set(type, []);
      listeners.get(type).push(fn);
    },
    removeEventListener(type, fn) {
      const arr = listeners.get(type);
      if (arr) {
        const idx = arr.indexOf(fn);
        if (idx >= 0) arr.splice(idx, 1);
      }
    },
    dispatchEvent(event) {
      const arr = listeners.get(event.type);
      if (arr) arr.forEach(fn => fn(event));
    }
  };
}

// Simple observer that logs to console
function observer(name) {
  return {
    pending: () => console.log(`[${name}] pending`),
    fulfilled: (value) => console.log(`[${name}] fulfilled:`, value),
    rejected: (error) => console.log(`[${name}] rejected:`, error)
  };
}

// Define the notebook
function define(runtime, observer) {
  const main = runtime.module();

  // Provide builtins - using REAL Generators.input from stdlib
  main.builtin("Generators", Generators);
  main.builtin("createMockInput", createMockInput);

  // viewof trigger - initial value "tick", changes to "tock" after 200ms
  main.variable(observer("viewof trigger")).define("viewof trigger", ["createMockInput"], (createMockInput) => {
    console.log("  _trigger function running");
    const view = createMockInput("tick");
    setTimeout(() => {
      console.log("  trigger setTimeout firing -> tock");
      view.value = "tock";
      view.dispatchEvent({ type: "input" });
    }, 200);
    return view;
  });

  // trigger - generator that yields viewof trigger's value (using real Generators.input)
  main.variable(observer("trigger")).define("trigger", ["Generators", "viewof trigger"], (G, _) => {
    console.log("  trigger generator starting");
    return G.input(_);
  });

  // viewof item - depends on trigger, initial undefined, dispatches "foo" after delay
  let itemRunCount = 0;
  main.variable(observer("viewof item")).define("viewof item", ["trigger", "createMockInput"], (trigger, createMockInput) => {
    itemRunCount++;
    const runNum = itemRunCount;
    console.log(`  _item function running #${runNum} (trigger = ${trigger})`);
    const view = createMockInput(undefined);

    // Only set timeout on the SECOND run (after trigger changes to "tock")
    if (trigger === "tock") {
      setTimeout(() => {
        console.log(`  item setTimeout #${runNum} firing -> foo`);
        view.value = "foo";
        view.dispatchEvent({ type: "input" });
      }, 500);
    }
    return view;
  });

  // item - generator that yields viewof item's value (using real Generators.input)
  main.variable(observer("item")).define("item", ["Generators", "viewof item"], (G, _) => {
    console.log("  item generator starting");
    return G.input(_);
  });

  // Display cell that shows item's value
  main.variable(observer("display")).define("display", ["item"], (item) => {
    console.log("  display function running (item =", item, ")");
    return item;
  });

  return main;
}

// Run the notebook
console.log("Starting notebook...\n");
const runtime = new Runtime();
runtime.module(define, observer);

// Keep the process alive to see all the async behavior
setTimeout(() => {
  console.log("\n--- Test complete (2.5s timeout) ---");
  process.exit(0);
}, 2500);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions