MCPcopy
hub / github.com/stackblitz/alien-signals

github.com/stackblitz/alien-signals @v3.2.1 sqlite

repository ↗ · DeepWiki ↗ · release v3.2.1 ↗
54 symbols 143 edges 12 files 0 documented · 0%
README
<img src="https://github.com/stackblitz/alien-signals/raw/v3.2.1/assets/logo.png" width="250">









<a href="https://npmjs.com/package/alien-signals"><img src="https://badgen.net/npm/v/alien-signals" alt="npm package"></a>
<a href="https://deepwiki.com/stackblitz/alien-signals"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>

alien-signals

This project explores a push-pull based signal algorithm. The implementation is related to the following frontend projects:

  • Propagation algorithm of Vue 3
  • Preact’s double-linked-list approach (https://preactjs.com/blog/signal-boosting/)
  • Inner effects scheduling of Svelte
  • Graph-coloring approach of Reactively (https://milomg.dev/2022-12-01/reactivity)

We impose some constraints (such as not using Array/Set/Map and disallowing function recursion in the algorithmic core) to ensure performance. We found that under these conditions, maintaining algorithmic simplicity offers more significant improvements than complex scheduling strategies.

I wrote the reactivity code for both Vue and alien-signals. Below is a benchmark comparison against Vue 3.4 and other frameworks. The core algorithm has since been ported back to Vue 3.6.

Image

Benchmark repo: https://github.com/transitive-bullshit/js-reactivity-benchmark

Background

I spent considerable time optimizing Vue 3.4’s reactivity system, gaining experience along the way. Since Vue 3.5 switched to a pull-based algorithm similar to Preact, I decided to continue researching a push-pull based implementation in a separate project. The algorithm is used in Vue language tools for incremental AST parsing and virtual code generation.

Other Language Implementations

Derived Projects

Adoption

  • vuejs/core: The core algorithm has been ported to v3.6 (PR: https://github.com/vuejs/core/pull/12349)
  • statelyai/xstate: The core algorithm has been ported to implement the atom architecture (PR: https://github.com/statelyai/xstate/pull/5250)
  • flamrdevs/xignal: Infrastructure for the reactive system
  • vuejs/language-tools: Used in the language-core package for virtual code generation
  • unuse: A framework-agnostic use library inspired by VueUse

Usage

Basic APIs

import { signal, computed, effect } from 'alien-signals';

const count = signal(1);
const doubleCount = computed(() => count() * 2);

effect(() => {
  console.log(`Count is: ${count()}`);
}); // Console: Count is: 1

console.log(doubleCount()); // 2

count(2); // Console: Count is: 2

console.log(doubleCount()); // 4

Effect Scope

import { signal, effect, effectScope } from 'alien-signals';

const count = signal(1);

const stopScope = effectScope(() => {
  effect(() => {
    console.log(`Count in scope: ${count()}`);
  }); // Console: Count in scope: 1
});

count(2); // Console: Count in scope: 2

stopScope();

count(3); // No console output

Nested Effects

Effects can be nested inside other effects. When the outer effect re-runs, inner effects from the previous run are automatically cleaned up, and new inner effects are created if needed. The system ensures proper execution order — outer effects always run before their inner effects:

import { signal, effect } from 'alien-signals';

const show = signal(true);
const count = signal(1);

effect(() => {
  if (show()) {
    // This inner effect is created when show() is true
    effect(() => {
      console.log(`Count is: ${count()}`);
    });
  }
}); // Console: Count is: 1

count(2); // Console: Count is: 2

// When show becomes false, the inner effect is cleaned up
show(false); // No output

count(3); // No output (inner effect no longer exists)

Manual Triggering

The trigger() function allows you to manually trigger updates for downstream dependencies when you've directly mutated a signal's value without using the signal setter:

import { signal, computed, trigger } from 'alien-signals';

const arr = signal<number[]>([]);
const length = computed(() => arr().length);

console.log(length()); // 0

// Direct mutation doesn't automatically trigger updates
arr().push(1);
console.log(length()); // Still 0

// Manually trigger updates
trigger(arr);
console.log(length()); // 1

You can also trigger multiple signals at once:

import { signal, computed, trigger } from 'alien-signals';

const src1 = signal<number[]>([]);
const src2 = signal<number[]>([]);
const total = computed(() => src1().length + src2().length);

src1().push(1);
src2().push(2);

trigger(() => {
  src1();
  src2();
});

console.log(total()); // 2

Creating Your Own Surface API

You can reuse alien-signals’ core algorithm via createReactiveSystem() to build your own signal API. For implementation examples, see:

About propagate and checkDirty functions

The actual implementations of propagate and checkDirty in system.ts replace recursive calls with iterative stack-based traversal for performance. The recursive versions below are equivalent and easier to follow — useful as a reference when porting to other languages where the iterative optimization may not help.

propagate

function propagate(link: Link, innerWrite: boolean): void {
    do {
        const sub = link.sub;

        let flags = sub.flags;

        if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending))) {
            sub.flags = flags | ReactiveFlags.Pending;
            if (innerWrite) {
                sub.flags |= ReactiveFlags.Recursed;
            }
        } else if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))) {
            flags = ReactiveFlags.None;
        } else if (!(flags & ReactiveFlags.RecursedCheck)) {
            sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending;
        } else if (!(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) && isValidLink(link, sub)) {
            sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending;
            flags &= ReactiveFlags.Mutable;
        } else {
            flags = ReactiveFlags.None;
        }

        if (flags & ReactiveFlags.Watching) {
            notify(sub);
        }

        if (flags & ReactiveFlags.Mutable) {
            const subSubs = sub.subs;
            if (subSubs !== undefined) {
                propagate(subSubs, innerWrite);
            }
        }

        link = link.nextSub!;
    } while (link !== undefined);
}

checkDirty

function checkDirty(link: Link, sub: ReactiveNode): boolean {
    do {
        const dep = link.dep;
        const depFlags = dep.flags;

        if (sub.flags & ReactiveFlags.Dirty) {
            return true;
        } else if ((depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) === (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) {
            if (update(dep)) {
                const subs = dep.subs!;
                if (subs.nextSub !== undefined) {
                    shallowPropagate(subs);
                }
                return true;
            }
        } else if ((depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) === (ReactiveFlags.Mutable | ReactiveFlags.Pending)) {
            if (checkDirty(dep.deps!, dep)) {
                if (update(dep)) {
                    const subs = dep.subs!;
                    if (subs.nextSub !== undefined) {
                        shallowPropagate(subs);
                    }
                    return true;
                }
            } else {
                dep.flags = depFlags & ~ReactiveFlags.Pending;
            }
        }

        link = link.nextDep!;
    } while (link !== undefined);

    return false;
}

Extension points exported contracts — how you extend this code

EffectScopeNode (Interface)
(no doc)
src/index.ts
ReactiveNode (Interface)
(no doc)
src/system.ts
EffectNode (Interface)
(no doc)
src/index.ts
Link (Interface)
(no doc)
src/system.ts
ComputedNode (Interface)
(no doc)
src/index.ts
Stack (Interface)
(no doc)
src/system.ts
SignalNode (Interface)
(no doc)
src/index.ts

Core symbols most depended-on inside this repo

effect
called by 40
src/index.ts
signal
called by 19
src/index.ts
computed
called by 9
src/index.ts
setActiveSub
called by 8
src/index.ts
unlink
called by 6
src/system.ts
effectScope
called by 5
src/index.ts
trigger
called by 5
src/index.ts
shallowPropagate
called by 5
src/system.ts

Shape

Function 45
Interface 7
Enum 1
Method 1

Languages

TypeScript100%

Modules by API surface

src/index.ts33 symbols
src/system.ts11 symbols
tests/conformance.spec.ts6 symbols
tsslint.config.ts3 symbols
build.js1 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

Dependencies from manifests, versioned

@tsslint/clilatest · 1×
@tsslint/configlatest · 1×
mitatalatest · 1×
reactive-framework-test-suite0.0.2 · 1×
rolldownlatest · 1×
typescriptlatest · 1×
vitestlatest · 1×

For agents

$ claude mcp add alien-signals \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact