Skip to main content

SIGNAL_JS

Constant SIGNAL_JS 

Source
pub const SIGNAL_JS: &str = "// Fine-grained reactivity for ZDeceptron.\n//\n// Dependencies are discovered at READ time, at runtime \u{2014} never from the\n// shape of the source. Spec \u{a7}5.5 requires this: Svelte documented that\n// compile-time dependency detection silently breaks when an expression is\n// extracted into a helper function, because the dependency stops being\n// visible at the declaration site. Tracking reads means refactoring cannot\n// break reactivity.\n//\n// The model is SolidJS\'s: a signal write marks its readers stale and\n// schedules them; a read inside a running computation registers an edge.\n// There is no virtual DOM and no component re-render \u{2014} a write reaches\n// exactly the bindings that read it.\n\n// **Nothing in the runtime may `for\u{2026}of`, spread, or `Array.from` a `Set`\n// or a `Map`. Call `forEach`.** That is a workaround for an engine bug and\n// not a preference: simplifying it back reintroduces a crash.\n//\n// `boa_engine` 0.21.1 \u{2014} the engine every test in this repository runs\n// emitted JavaScript through \u{2014} panics with `Object already borrowed:\n// BorrowMutError` from `builtins/set/ordered_set.rs:182`, and identically\n// from `builtins/map/ordered_map.rs:225`. `Set.prototype.values` hands its\n// iterator a lock on the set, and that lock is released only from the\n// iterator\'s `finalize` \u{2014} so an iterator that is finished with, or\n// abandoned, keeps it until the collector reaches it. `SetIterator::next`\n// then holds a *borrow* of the same set across an allocation, and a\n// collection landing on that allocation runs the pending `finalize`, which\n// borrows the set again. boa\'s own comment at the panic site reads\n// `TODO: try_downcast_mut`.\n//\n// Iterating one collection twice is therefore enough, and which program\n// crashes depends only on where the allocations happen to fall \u{2014} so it\n// presents as a bug in whatever changed last, and fixing one call site\n// only moves it to another. Restoring the spreads in this file alone, with\n// the other three left converted, still failed two `algorithms` tests but\n// no longer the same two: the crash follows the allocations, not the\n// algorithm. `forEach` is unaffected \u{2014} it keeps its lock in a Rust local\n// rather than in a collectable iterator object, and it drops the borrow\n// before calling back. 0.21.1 is the newest published `boa_engine`; when\n// there is a newer one, check whether this still bites.\n\n/** Every member of `set`, as an array: the `[...set]` the note forbids.\n *\n * The specific name is load-bearing. `zdc-bench` flattens every runtime\n * file into one scope to measure it, so a top-level name here collides\n * with one there, and the loser is called with the wrong arity rather\n * than reported \u{2014} a `snapshot` here silently became `instrument.js`\'s. */\nfunction membersOf(set) {\n  const out = [];\n  set.forEach((member) => out.push(member));\n  return out;\n}\n\n/** The computation currently running, or null at the top level. */\nlet listener = null;\n\n/** Collects disposers created inside `owned`, or null outside one. */\nlet owner = null;\n\n/** Depth of the current batch. Writes flush when it returns to zero. */\nlet batchDepth = 0;\n\nlet flushing = false;\n\n/** Computations marked stale during the current batch. */\nconst pending = new Set();\n\n/** Register a teardown with the scope `owned` opened, if any. */\nexport function onCleanup(fn) {\n  if (owner) owner.push(fn);\n}\n\n/**\n * A mutable value that tracks who reads it.\n *\n * Returns a [read, write] pair rather than an object so that reading is a\n * call \u{2014} which is what makes the dependency edge observable.\n */\nexport function signal(initial) {\n  let value = initial;\n  const readers = new Set();\n\n  function read() {\n    if (listener) {\n      readers.add(listener);\n      listener.sources.add(readers);\n    }\n    return value;\n  }\n\n  function write(next) {\n    const resolved = typeof next === \'function\' ? next(value) : next;\n    // Reference equality is the right test here: ZDeceptron values are\n    // immutable, so a structurally-equal new object is a genuine change\n    // from the language\'s point of view.\n    if (Object.is(resolved, value)) return value;\n    value = resolved;\n    // One flush per write: one at a time, a diamond ran its effect on a\n    // pair of values that never existed together.\n    batch(() => {\n      for (const reader of membersOf(readers)) invalidate(reader);\n    });\n    return value;\n  }\n\n  return [read, write];\n}\n\n/**\n * A value computed from other signals, recomputed when they change.\n *\n * This is `from` in the language. It is lazy: the body does not run until\n * something reads it, and it does not re-run until a dependency changes.\n */\nexport function derived(compute) {\n  let value;\n  let stale = true;\n  const readers = new Set();\n\n  const node = {\n    sources: new Set(),\n    run() {\n      stale = true;\n      for (const reader of membersOf(readers)) invalidate(reader);\n    },\n  };\n\n  return function read() {\n    if (listener) {\n      readers.add(listener);\n      listener.sources.add(readers);\n    }\n    if (stale) {\n      clearSources(node);\n      const previous = listener;\n      listener = node;\n      try {\n        value = compute();\n      } finally {\n        listener = previous;\n      }\n      stale = false;\n    }\n    return value;\n  };\n}\n\n/**\n * Run a function now, and again whenever anything it read changes.\n *\n * Every DOM binding is one of these, which is why an update touches only\n * the nodes that actually read the changed signal.\n */\nexport function effect(fn) {\n  // `clearSources` cannot retract a run the drain has snapshotted.\n  let live = true;\n  const node = {\n    sources: new Set(),\n    run() {\n      if (!live) return;\n      clearSources(node);\n      const previous = listener;\n      const scope = owner;\n      listener = node;\n      owner = null;\n      try {\n        fn();\n      } finally {\n        listener = previous;\n        owner = scope;\n      }\n    },\n  };\n  node.run();\n  const dispose = () => {\n    live = false;\n    clearSources(node);\n  };\n  onCleanup(dispose);\n  return dispose;\n}\n\n/**\n * Run `fn`, collecting every effect it creates so they can be torn down\n * together.\n *\n * Without this a removed list row stays subscribed to whatever it read\n * for the life of the page. It is not visible as wrong output \u{2014} a row\n * nobody writes to simply never re-runs \u{2014} which is exactly why it needs\n * an explicit mechanism rather than being noticed.\n */\nexport function owned(fn) {\n  const previous = owner;\n  const disposers = [];\n  const dispose = () => {\n    while (disposers.length > 0) disposers.pop()();\n  };\n  // Linked, so a parent reaches it: unlinked, 2000 mount/unmount cycles\n  // of three rows kept all 6000 effects live.\n  if (previous) previous.push(dispose);\n  owner = disposers;\n  try {\n    return [fn(), dispose];\n  } finally {\n    owner = previous;\n  }\n}\n\n/**\n * Apply several writes and flush once.\n *\n * An event handler is implicitly batched, so `add 1 to a` followed by\n * `set b to 2` repaints once rather than twice.\n */\nexport function batch(fn) {\n  batchDepth += 1;\n  try {\n    return fn();\n  } finally {\n    batchDepth -= 1;\n    if (batchDepth === 0) flush();\n  }\n}\n\nfunction invalidate(node) {\n  pending.add(node);\n  if (batchDepth === 0) flush();\n}\n\n/** Computations one update may run before it is a cycle. */\nconst STEP_LIMIT = 1e5;\n\nfunction flush() {\n  // A re-entrant flush is the same drain: flushing from inside a running\n  // computation cost a stack frame per link, and 200 chained bindings\n  // exhausted the budget.\n  if (flushing) return;\n  flushing = true;\n  let steps = 0;\n  // A throwing binding must not take the rest of the drain with it.\n  let failure = null;\n  try {\n    // Draining rather than iterating: a computation may invalidate another,\n    // and that one must run in the same flush or the DOM ends up showing a\n    // value that is already out of date.\n    while (pending.size > 0) {\n      const ready = membersOf(pending);\n      pending.clear();\n      for (const node of ready) {\n        if ((steps += 1) > STEP_LIMIT) {\n          throw new Error(`An update ran ${STEP_LIMIT} steps without settling.`);\n        }\n        try {\n          node.run();\n        } catch (error) {\n          failure ??= error;\n        }\n      }\n    }\n  } finally {\n    flushing = false;\n  }\n  if (failure) throw failure;\n}\n\nfunction clearSources(node) {\n  node.sources.forEach((readers) => readers.delete(node));\n  node.sources.clear();\n}\n";
Expand description

The reactivity core: signals, derived values, effects, batching.