<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>
This project explores a push-pull based signal algorithm. The implementation is related to the following frontend projects:
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.
Benchmark repo: https://github.com/transitive-bullshit/js-reactivity-benchmark
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.
use library inspired by VueUseimport { 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
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
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)
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
You can reuse alien-signals’ core algorithm via createReactiveSystem() to build your own signal API. For implementation examples, see:
.get() & .set() methods like the Signals proposal)propagate and checkDirty functionsThe 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;
}
$ claude mcp add alien-signals \
-- python -m otcore.mcp_server <graph>