first commit

This commit is contained in:
Artem Anufrij
2026-10-07 18:56:27 +02:00
commit 2a8a60b725
7768 changed files with 1550851 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-present Johnson Chu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+266
View File
@@ -0,0 +1,266 @@
<p align="center">
<img src="assets/logo.png" width="250"><br>
<p>
<p align="center">
<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>
</p>
# 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](https://github.com/stackblitz/alien-signals/blob/master/src/system.ts)) 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](https://github.com/vuejs/core/pull/12349).
<img width="1210" alt="Image" src="https://github.com/user-attachments/assets/88448f6d-4034-4389-89aa-9edf3da77254" />
> Benchmark repo: https://github.com/transitive-bullshit/js-reactivity-benchmark
## Background
I spent considerable time [optimizing Vue 3.4’s reactivity system](https://github.com/vuejs/core/pull/5912), gaining experience along the way. Since Vue 3.5 [switched to a pull-based algorithm similar to Preact](https://github.com/vuejs/core/pull/10397), 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
- **Dart:** [medz/alien-signals-dart](https://github.com/medz/alien-signals-dart)
- **Dart:** [void-signals/void_signals](https://github.com/void-signals/void_signals)
- **Lua:** [YanqingXu/alien-signals-in-lua](https://github.com/YanqingXu/alien-signals-in-lua)
- **Lua 5.4:** [xuhuanzy/alien-signals-lua](https://github.com/xuhuanzy/alien-signals-lua)
- **Luau:** [Nicell/alien-signals-luau](https://github.com/Nicell/alien-signals-luau)
- **Java:** [CTRL-Neo-Studios/java-alien-signals](https://github.com/CTRL-Neo-Studios/java-alien-signals)
- **C#:** [CTRL-Neo-Studios/csharp-alien-signals](https://github.com/CTRL-Neo-Studios/csharp-alien-signals)
- **Go:** [delaneyj/alien-signals-go](https://github.com/delaneyj/alien-signals-go)
- **Rust:** [wuzekang/samara-signals](https://github.com/wuzekang/samara/tree/main/crates/signals)
- **Rust:** [ohkami-rs/alien-signals-rs](https://github.com/ohkami-rs/alien-signals-rs)
## Derived Projects
- [Rajaniraiyn/react-alien-signals](https://github.com/Rajaniraiyn/react-alien-signals): React bindings for the alien-signals API
- [CCherry07/alien-deepsignals](https://github.com/CCherry07/alien-deepsignals): Use alien-signals with the interface of a plain JavaScript object
- [hunghg255/reactjs-signal](https://github.com/hunghg255/reactjs-signal): Share Store State with Signal Pattern
- [gn8-ai/universe-alien-signals](https://github.com/gn8-ai/universe-alien-signals): Enables simple use of the Alien Signals state management system in modern frontend frameworks
- [WebReflection/alien-signals](https://github.com/WebReflection/alien-signals): Preact signals like API and a class based approach for easy brand check
- [@lift-html/alien](https://github.com/JLarky/lift-html/tree/main/packages/alien): Integrating alien-signals into lift-html
- [ilha](https://github.com/ilhajs/ilha): A tiny web UI library built around the islands architecture
## Adoption
- [vuejs/core](https://github.com/vuejs/core): The core algorithm has been ported to v3.6 (PR: https://github.com/vuejs/core/pull/12349)
- [statelyai/xstate](https://github.com/statelyai/xstate): The core algorithm has been ported to implement the atom architecture (PR: https://github.com/statelyai/xstate/pull/5250)
- [flamrdevs/xignal](https://github.com/flamrdevs/xignal): Infrastructure for the reactive system
- [vuejs/language-tools](https://github.com/vuejs/language-tools): Used in the language-core package for virtual code generation
- [unuse](https://github.com/un-ts/unuse): A framework-agnostic `use` library inspired by `VueUse`
## Usage
#### Basic APIs
```ts
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
```ts
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:
```ts
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:
```ts
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:
```ts
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:
- [Starter template](https://github.com/johnsoncodehk/alien-signals-starter) (implements `.get()` & `.set()` methods like the [Signals proposal](https://github.com/tc39/proposal-signals))
- [stackblitz/alien-signals/src/index.ts](https://github.com/stackblitz/alien-signals/blob/master/src/index.ts)
- [proposal-signals/signal-polyfill#44](https://github.com/proposal-signals/signal-polyfill/pull/44)
## About `propagate` and `checkDirty` functions
The actual implementations of `propagate` and `checkDirty` in [system.ts](https://github.com/stackblitz/alien-signals/blob/master/src/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.
<details>
<summary><code>propagate</code></summary>
```ts
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);
}
```
</details>
<details>
<summary><code>checkDirty</code></summary>
```ts
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;
}
```
</details>
+386
View File
@@ -0,0 +1,386 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getActiveSub = getActiveSub;
exports.setActiveSub = setActiveSub;
exports.getBatchDepth = getBatchDepth;
exports.startBatch = startBatch;
exports.endBatch = endBatch;
exports.isSignal = isSignal;
exports.isComputed = isComputed;
exports.isEffect = isEffect;
exports.isEffectScope = isEffectScope;
exports.signal = signal;
exports.computed = computed;
exports.effect = effect;
exports.effectScope = effectScope;
exports.trigger = trigger;
const system_js_1 = require("./system.cjs");
const HasChildEffect = 64;
let cycle = 0;
let runDepth = 0;
let batchDepth = 0;
let notifyIndex = 0;
let queuedLength = 0;
let activeSub;
const queued = [];
const { link, unlink, propagate, checkDirty, shallowPropagate, } = (0, system_js_1.createReactiveSystem)({
update(node) {
if ('getter' in node) {
return updateComputed(node);
}
if ('currentValue' in node) {
return updateSignal(node);
}
node.flags = 1;
return true;
},
notify(effect) {
let insertIndex = queuedLength;
let firstInsertedIndex = insertIndex;
do {
queued[insertIndex++] = effect;
effect.flags &= ~2;
effect = effect.subs?.sub;
if (effect === undefined || !(effect.flags & 2)) {
break;
}
} while (true);
queuedLength = insertIndex;
while (firstInsertedIndex < --insertIndex) {
const left = queued[firstInsertedIndex];
queued[firstInsertedIndex++] = queued[insertIndex];
queued[insertIndex] = left;
}
},
unwatched(node) {
if ('getter' in node) {
if (node.depsTail !== undefined) {
node.flags = 1 | 16;
disposeAllDepsInReverse(node);
}
}
else if ('currentValue' in node) {
}
else if ('fn' in node) {
effectOper.call(node);
}
else {
effectScopeOper.call(node);
}
},
});
function getActiveSub() {
return activeSub;
}
function setActiveSub(sub) {
const prevSub = activeSub;
activeSub = sub;
return prevSub;
}
function getBatchDepth() {
return batchDepth;
}
function startBatch() {
++batchDepth;
}
function endBatch() {
if (!--batchDepth) {
flush();
}
}
function isSignal(fn) {
return fn.name === 'bound ' + signalOper.name;
}
function isComputed(fn) {
return fn.name === 'bound ' + computedOper.name;
}
function isEffect(fn) {
return fn.name === 'bound ' + effectOper.name;
}
function isEffectScope(fn) {
return fn.name === 'bound ' + effectScopeOper.name;
}
function signal(initialValue) {
return signalOper.bind({
currentValue: initialValue,
pendingValue: initialValue,
subs: undefined,
subsTail: undefined,
flags: 1,
});
}
function computed(getter) {
return computedOper.bind({
value: undefined,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: 0,
getter: getter,
});
}
function effect(fn) {
const e = {
fn,
cleanup: undefined,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: 2 | 4,
};
const prevSub = setActiveSub(e);
if (prevSub !== undefined) {
link(e, prevSub, 0);
prevSub.flags |= HasChildEffect;
}
try {
++runDepth;
e.cleanup = e.fn();
}
finally {
--runDepth;
activeSub = prevSub;
e.flags &= ~4;
}
return effectOper.bind(e);
}
function effectScope(fn) {
const e = {
deps: undefined,
depsTail: undefined,
subs: undefined,
subsTail: undefined,
flags: 1,
};
const prevSub = setActiveSub(e);
if (prevSub !== undefined) {
link(e, prevSub, 0);
prevSub.flags |= HasChildEffect;
}
try {
fn();
}
finally {
activeSub = prevSub;
}
return effectScopeOper.bind(e);
}
function trigger(fn) {
const sub = {
deps: undefined,
depsTail: undefined,
flags: 2,
};
const prevSub = setActiveSub(sub);
try {
fn();
}
finally {
activeSub = prevSub;
sub.flags = 0;
let link = sub.deps;
while (link !== undefined) {
const dep = link.dep;
link = unlink(link, sub);
const subs = dep.subs;
if (subs !== undefined) {
propagate(subs, !!runDepth);
shallowPropagate(subs);
}
}
if (!batchDepth) {
flush();
}
}
}
function updateComputed(c) {
if (c.flags & HasChildEffect) {
let link = c.depsTail;
while (link !== undefined) {
const prev = link.prevDep;
const dep = link.dep;
if (!('getter' in dep) && !('currentValue' in dep)) {
unlink(link, c);
}
link = prev;
}
}
c.depsTail = undefined;
c.flags = 1 | 4;
const prevSub = setActiveSub(c);
try {
++cycle;
const oldValue = c.value;
return oldValue !== (c.value = c.getter(oldValue));
}
finally {
activeSub = prevSub;
c.flags &= ~4;
purgeDeps(c);
}
}
function updateSignal(s) {
s.flags = 1;
return s.currentValue !== (s.currentValue = s.pendingValue);
}
function run(e) {
const flags = e.flags;
if (flags & 16
|| (flags & 32
&& checkDirty(e.deps, e))) {
if (flags & HasChildEffect) {
let link = e.depsTail;
while (link !== undefined) {
const prev = link.prevDep;
const dep = link.dep;
if (!('getter' in dep) && !('currentValue' in dep)) {
unlink(link, e);
}
link = prev;
}
}
if (e.cleanup) {
runCleanup(e);
if (!e.flags) {
return;
}
}
e.depsTail = undefined;
e.flags = 2 | 4;
const prevSub = setActiveSub(e);
try {
++cycle;
++runDepth;
e.cleanup = e.fn();
}
finally {
--runDepth;
activeSub = prevSub;
e.flags &= ~4;
purgeDeps(e);
}
}
else if (e.deps !== undefined) {
e.flags = 2 | (flags & HasChildEffect);
}
}
function flush() {
try {
while (notifyIndex < queuedLength) {
const effect = queued[notifyIndex];
queued[notifyIndex++] = undefined;
run(effect);
}
}
finally {
while (notifyIndex < queuedLength) {
const effect = queued[notifyIndex];
queued[notifyIndex++] = undefined;
effect.flags |= 2 | 8;
}
notifyIndex = 0;
queuedLength = 0;
}
}
function computedOper() {
const flags = this.flags;
if (flags & 16
|| (flags & 32
&& (checkDirty(this.deps, this)
|| (this.flags = flags & ~32, false)))) {
if (updateComputed(this)) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
}
else if (!flags) {
this.flags = 1 | 4;
const prevSub = setActiveSub(this);
try {
this.value = this.getter();
}
finally {
activeSub = prevSub;
this.flags &= ~4;
}
}
const sub = activeSub;
if (sub !== undefined) {
link(this, sub, cycle);
}
return this.value;
}
function signalOper(...value) {
if (value.length) {
if (this.pendingValue !== (this.pendingValue = value[0])) {
this.flags = 1 | 16;
const subs = this.subs;
if (subs !== undefined) {
propagate(subs, !!runDepth);
if (!batchDepth) {
flush();
}
}
}
}
else {
if (this.flags & 16) {
if (updateSignal(this)) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
}
const sub = activeSub;
if (sub !== undefined) {
link(this, sub, cycle);
}
return this.currentValue;
}
}
function runCleanup(e) {
const cleanup = e.cleanup;
e.cleanup = undefined;
const prevSub = activeSub;
activeSub = undefined;
try {
cleanup();
}
finally {
activeSub = prevSub;
}
}
function effectOper() {
effectScopeOper.call(this);
if (this.cleanup) {
runCleanup(this);
}
}
function effectScopeOper() {
this.flags = 0;
disposeAllDepsInReverse(this);
const sub = this.subs;
if (sub !== undefined) {
unlink(sub);
}
}
function disposeAllDepsInReverse(sub) {
let link = sub.depsTail;
while (link !== undefined) {
const prev = link.prevDep;
unlink(link, sub);
link = prev;
}
}
function purgeDeps(sub) {
const depsTail = sub.depsTail;
let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps;
while (dep !== undefined) {
dep = unlink(dep, sub);
}
}
+228
View File
@@ -0,0 +1,228 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReactiveFlags = void 0;
exports.createReactiveSystem = createReactiveSystem;
exports.ReactiveFlags = {
None: 0,
Mutable: 1,
Watching: 2,
RecursedCheck: 4,
Recursed: 8,
Dirty: 16,
Pending: 32,
};
function createReactiveSystem({ update, notify, unwatched, }) {
return {
link,
unlink,
propagate,
checkDirty,
shallowPropagate,
};
function link(dep, sub, version) {
const prevDep = sub.depsTail;
if (prevDep !== undefined && prevDep.dep === dep) {
return;
}
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps;
if (nextDep !== undefined && nextDep.dep === dep) {
nextDep.version = version;
sub.depsTail = nextDep;
return;
}
const prevSub = dep.subsTail;
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub) {
return;
}
const newLink = sub.depsTail
= dep.subsTail
= {
version,
dep,
sub,
prevDep,
nextDep,
prevSub,
nextSub: undefined,
};
if (nextDep !== undefined) {
nextDep.prevDep = newLink;
}
if (prevDep !== undefined) {
prevDep.nextDep = newLink;
}
else {
sub.deps = newLink;
}
if (prevSub !== undefined) {
prevSub.nextSub = newLink;
}
else {
dep.subs = newLink;
}
}
function unlink(link, sub = link.sub) {
const { dep, prevDep, nextDep, nextSub, prevSub } = link;
if (nextDep !== undefined) {
nextDep.prevDep = prevDep;
}
else {
sub.depsTail = prevDep;
}
if (prevDep !== undefined) {
prevDep.nextDep = nextDep;
}
else {
sub.deps = nextDep;
}
if (nextSub !== undefined) {
nextSub.prevSub = prevSub;
}
else {
dep.subsTail = prevSub;
}
if (prevSub !== undefined) {
prevSub.nextSub = nextSub;
}
else if ((dep.subs = nextSub) === undefined) {
unwatched(dep);
}
return nextDep;
}
function propagate(link, innerWrite) {
let next = link.nextSub;
let stack;
top: do {
const sub = link.sub;
let flags = sub.flags;
if (!(flags & (4 | 8 | 16 | 32))) {
sub.flags = flags | 32;
if (innerWrite) {
sub.flags |= 8;
}
}
else if (!(flags & (4 | 8))) {
flags = 0;
}
else if (!(flags & 4)) {
sub.flags = (flags & ~8) | 32;
}
else if (!(flags & (16 | 32)) && isValidLink(link, sub)) {
sub.flags = flags | (8 | 32);
flags &= 1;
}
else {
flags = 0;
}
if (flags & 2) {
notify(sub);
}
if (flags & 1) {
const subSubs = sub.subs;
if (subSubs !== undefined) {
const nextSub = (link = subSubs).nextSub;
if (nextSub !== undefined) {
stack = { value: next, prev: stack };
next = nextSub;
}
continue;
}
}
if ((link = next) !== undefined) {
next = link.nextSub;
continue;
}
while (stack !== undefined) {
link = stack.value;
stack = stack.prev;
if (link !== undefined) {
next = link.nextSub;
continue top;
}
}
break;
} while (true);
}
function checkDirty(link, sub) {
let stack;
let checkDepth = 0;
let dirty = false;
top: do {
const dep = link.dep;
const flags = dep.flags;
if (sub.flags & 16) {
dirty = true;
}
else if ((flags & (1 | 16)) === (1 | 16)) {
const subs = dep.subs;
if (update(dep)) {
if (subs.nextSub !== undefined) {
shallowPropagate(subs);
}
dirty = true;
}
}
else if ((flags & (1 | 32)) === (1 | 32)) {
stack = { value: link, prev: stack };
link = dep.deps;
sub = dep;
++checkDepth;
continue;
}
if (!dirty) {
const nextDep = link.nextDep;
if (nextDep !== undefined) {
link = nextDep;
continue;
}
}
while (checkDepth--) {
link = stack.value;
stack = stack.prev;
if (dirty) {
const subs = sub.subs;
if (update(sub)) {
if (subs.nextSub !== undefined) {
shallowPropagate(subs);
}
sub = link.sub;
continue;
}
dirty = false;
}
else {
sub.flags &= ~32;
}
sub = link.sub;
const nextDep = link.nextDep;
if (nextDep !== undefined) {
link = nextDep;
continue top;
}
}
return dirty && !!sub.flags;
} while (true);
}
function shallowPropagate(link) {
do {
const sub = link.sub;
const flags = sub.flags;
if ((flags & (32 | 16)) === 32) {
sub.flags = flags | 16;
if ((flags & (2 | 4)) === 2) {
notify(sub);
}
}
} while ((link = link.nextSub) !== undefined);
}
function isValidLink(checkLink, sub) {
let link = sub.depsTail;
while (link !== undefined) {
if (link === checkLink) {
return true;
}
link = link.prevDep;
}
return false;
}
}
+370
View File
@@ -0,0 +1,370 @@
import { createReactiveSystem } from './system.mjs';
const HasChildEffect = 64;
let cycle = 0;
let runDepth = 0;
let batchDepth = 0;
let notifyIndex = 0;
let queuedLength = 0;
let activeSub;
const queued = [];
const { link, unlink, propagate, checkDirty, shallowPropagate, } = createReactiveSystem({
update(node) {
if ('getter' in node) {
return updateComputed(node);
}
if ('currentValue' in node) {
return updateSignal(node);
}
node.flags = 1;
return true;
},
notify(effect) {
let insertIndex = queuedLength;
let firstInsertedIndex = insertIndex;
do {
queued[insertIndex++] = effect;
effect.flags &= ~2;
effect = effect.subs?.sub;
if (effect === undefined || !(effect.flags & 2)) {
break;
}
} while (true);
queuedLength = insertIndex;
while (firstInsertedIndex < --insertIndex) {
const left = queued[firstInsertedIndex];
queued[firstInsertedIndex++] = queued[insertIndex];
queued[insertIndex] = left;
}
},
unwatched(node) {
if ('getter' in node) {
if (node.depsTail !== undefined) {
node.flags = 1 | 16;
disposeAllDepsInReverse(node);
}
}
else if ('currentValue' in node) {
}
else if ('fn' in node) {
effectOper.call(node);
}
else {
effectScopeOper.call(node);
}
},
});
export function getActiveSub() {
return activeSub;
}
export function setActiveSub(sub) {
const prevSub = activeSub;
activeSub = sub;
return prevSub;
}
export function getBatchDepth() {
return batchDepth;
}
export function startBatch() {
++batchDepth;
}
export function endBatch() {
if (!--batchDepth) {
flush();
}
}
export function isSignal(fn) {
return fn.name === 'bound ' + signalOper.name;
}
export function isComputed(fn) {
return fn.name === 'bound ' + computedOper.name;
}
export function isEffect(fn) {
return fn.name === 'bound ' + effectOper.name;
}
export function isEffectScope(fn) {
return fn.name === 'bound ' + effectScopeOper.name;
}
export function signal(initialValue) {
return signalOper.bind({
currentValue: initialValue,
pendingValue: initialValue,
subs: undefined,
subsTail: undefined,
flags: 1,
});
}
export function computed(getter) {
return computedOper.bind({
value: undefined,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: 0,
getter: getter,
});
}
export function effect(fn) {
const e = {
fn,
cleanup: undefined,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: 2 | 4,
};
const prevSub = setActiveSub(e);
if (prevSub !== undefined) {
link(e, prevSub, 0);
prevSub.flags |= HasChildEffect;
}
try {
++runDepth;
e.cleanup = e.fn();
}
finally {
--runDepth;
activeSub = prevSub;
e.flags &= ~4;
}
return effectOper.bind(e);
}
export function effectScope(fn) {
const e = {
deps: undefined,
depsTail: undefined,
subs: undefined,
subsTail: undefined,
flags: 1,
};
const prevSub = setActiveSub(e);
if (prevSub !== undefined) {
link(e, prevSub, 0);
prevSub.flags |= HasChildEffect;
}
try {
fn();
}
finally {
activeSub = prevSub;
}
return effectScopeOper.bind(e);
}
export function trigger(fn) {
const sub = {
deps: undefined,
depsTail: undefined,
flags: 2,
};
const prevSub = setActiveSub(sub);
try {
fn();
}
finally {
activeSub = prevSub;
sub.flags = 0;
let link = sub.deps;
while (link !== undefined) {
const dep = link.dep;
link = unlink(link, sub);
const subs = dep.subs;
if (subs !== undefined) {
propagate(subs, !!runDepth);
shallowPropagate(subs);
}
}
if (!batchDepth) {
flush();
}
}
}
function updateComputed(c) {
if (c.flags & HasChildEffect) {
let link = c.depsTail;
while (link !== undefined) {
const prev = link.prevDep;
const dep = link.dep;
if (!('getter' in dep) && !('currentValue' in dep)) {
unlink(link, c);
}
link = prev;
}
}
c.depsTail = undefined;
c.flags = 1 | 4;
const prevSub = setActiveSub(c);
try {
++cycle;
const oldValue = c.value;
return oldValue !== (c.value = c.getter(oldValue));
}
finally {
activeSub = prevSub;
c.flags &= ~4;
purgeDeps(c);
}
}
function updateSignal(s) {
s.flags = 1;
return s.currentValue !== (s.currentValue = s.pendingValue);
}
function run(e) {
const flags = e.flags;
if (flags & 16
|| (flags & 32
&& checkDirty(e.deps, e))) {
if (flags & HasChildEffect) {
let link = e.depsTail;
while (link !== undefined) {
const prev = link.prevDep;
const dep = link.dep;
if (!('getter' in dep) && !('currentValue' in dep)) {
unlink(link, e);
}
link = prev;
}
}
if (e.cleanup) {
runCleanup(e);
if (!e.flags) {
return;
}
}
e.depsTail = undefined;
e.flags = 2 | 4;
const prevSub = setActiveSub(e);
try {
++cycle;
++runDepth;
e.cleanup = e.fn();
}
finally {
--runDepth;
activeSub = prevSub;
e.flags &= ~4;
purgeDeps(e);
}
}
else if (e.deps !== undefined) {
e.flags = 2 | (flags & HasChildEffect);
}
}
function flush() {
try {
while (notifyIndex < queuedLength) {
const effect = queued[notifyIndex];
queued[notifyIndex++] = undefined;
run(effect);
}
}
finally {
while (notifyIndex < queuedLength) {
const effect = queued[notifyIndex];
queued[notifyIndex++] = undefined;
effect.flags |= 2 | 8;
}
notifyIndex = 0;
queuedLength = 0;
}
}
function computedOper() {
const flags = this.flags;
if (flags & 16
|| (flags & 32
&& (checkDirty(this.deps, this)
|| (this.flags = flags & ~32, false)))) {
if (updateComputed(this)) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
}
else if (!flags) {
this.flags = 1 | 4;
const prevSub = setActiveSub(this);
try {
this.value = this.getter();
}
finally {
activeSub = prevSub;
this.flags &= ~4;
}
}
const sub = activeSub;
if (sub !== undefined) {
link(this, sub, cycle);
}
return this.value;
}
function signalOper(...value) {
if (value.length) {
if (this.pendingValue !== (this.pendingValue = value[0])) {
this.flags = 1 | 16;
const subs = this.subs;
if (subs !== undefined) {
propagate(subs, !!runDepth);
if (!batchDepth) {
flush();
}
}
}
}
else {
if (this.flags & 16) {
if (updateSignal(this)) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
}
const sub = activeSub;
if (sub !== undefined) {
link(this, sub, cycle);
}
return this.currentValue;
}
}
function runCleanup(e) {
const cleanup = e.cleanup;
e.cleanup = undefined;
const prevSub = activeSub;
activeSub = undefined;
try {
cleanup();
}
finally {
activeSub = prevSub;
}
}
function effectOper() {
effectScopeOper.call(this);
if (this.cleanup) {
runCleanup(this);
}
}
function effectScopeOper() {
this.flags = 0;
disposeAllDepsInReverse(this);
const sub = this.subs;
if (sub !== undefined) {
unlink(sub);
}
}
function disposeAllDepsInReverse(sub) {
let link = sub.depsTail;
while (link !== undefined) {
const prev = link.prevDep;
unlink(link, sub);
link = prev;
}
}
function purgeDeps(sub) {
const depsTail = sub.depsTail;
let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps;
while (dep !== undefined) {
dep = unlink(dep, sub);
}
}
+224
View File
@@ -0,0 +1,224 @@
export const ReactiveFlags = {
None: 0,
Mutable: 1,
Watching: 2,
RecursedCheck: 4,
Recursed: 8,
Dirty: 16,
Pending: 32,
};
export function createReactiveSystem({ update, notify, unwatched, }) {
return {
link,
unlink,
propagate,
checkDirty,
shallowPropagate,
};
function link(dep, sub, version) {
const prevDep = sub.depsTail;
if (prevDep !== undefined && prevDep.dep === dep) {
return;
}
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps;
if (nextDep !== undefined && nextDep.dep === dep) {
nextDep.version = version;
sub.depsTail = nextDep;
return;
}
const prevSub = dep.subsTail;
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub) {
return;
}
const newLink = sub.depsTail
= dep.subsTail
= {
version,
dep,
sub,
prevDep,
nextDep,
prevSub,
nextSub: undefined,
};
if (nextDep !== undefined) {
nextDep.prevDep = newLink;
}
if (prevDep !== undefined) {
prevDep.nextDep = newLink;
}
else {
sub.deps = newLink;
}
if (prevSub !== undefined) {
prevSub.nextSub = newLink;
}
else {
dep.subs = newLink;
}
}
function unlink(link, sub = link.sub) {
const { dep, prevDep, nextDep, nextSub, prevSub } = link;
if (nextDep !== undefined) {
nextDep.prevDep = prevDep;
}
else {
sub.depsTail = prevDep;
}
if (prevDep !== undefined) {
prevDep.nextDep = nextDep;
}
else {
sub.deps = nextDep;
}
if (nextSub !== undefined) {
nextSub.prevSub = prevSub;
}
else {
dep.subsTail = prevSub;
}
if (prevSub !== undefined) {
prevSub.nextSub = nextSub;
}
else if ((dep.subs = nextSub) === undefined) {
unwatched(dep);
}
return nextDep;
}
function propagate(link, innerWrite) {
let next = link.nextSub;
let stack;
top: do {
const sub = link.sub;
let flags = sub.flags;
if (!(flags & (4 | 8 | 16 | 32))) {
sub.flags = flags | 32;
if (innerWrite) {
sub.flags |= 8;
}
}
else if (!(flags & (4 | 8))) {
flags = 0;
}
else if (!(flags & 4)) {
sub.flags = (flags & ~8) | 32;
}
else if (!(flags & (16 | 32)) && isValidLink(link, sub)) {
sub.flags = flags | (8 | 32);
flags &= 1;
}
else {
flags = 0;
}
if (flags & 2) {
notify(sub);
}
if (flags & 1) {
const subSubs = sub.subs;
if (subSubs !== undefined) {
const nextSub = (link = subSubs).nextSub;
if (nextSub !== undefined) {
stack = { value: next, prev: stack };
next = nextSub;
}
continue;
}
}
if ((link = next) !== undefined) {
next = link.nextSub;
continue;
}
while (stack !== undefined) {
link = stack.value;
stack = stack.prev;
if (link !== undefined) {
next = link.nextSub;
continue top;
}
}
break;
} while (true);
}
function checkDirty(link, sub) {
let stack;
let checkDepth = 0;
let dirty = false;
top: do {
const dep = link.dep;
const flags = dep.flags;
if (sub.flags & 16) {
dirty = true;
}
else if ((flags & (1 | 16)) === (1 | 16)) {
const subs = dep.subs;
if (update(dep)) {
if (subs.nextSub !== undefined) {
shallowPropagate(subs);
}
dirty = true;
}
}
else if ((flags & (1 | 32)) === (1 | 32)) {
stack = { value: link, prev: stack };
link = dep.deps;
sub = dep;
++checkDepth;
continue;
}
if (!dirty) {
const nextDep = link.nextDep;
if (nextDep !== undefined) {
link = nextDep;
continue;
}
}
while (checkDepth--) {
link = stack.value;
stack = stack.prev;
if (dirty) {
const subs = sub.subs;
if (update(sub)) {
if (subs.nextSub !== undefined) {
shallowPropagate(subs);
}
sub = link.sub;
continue;
}
dirty = false;
}
else {
sub.flags &= ~32;
}
sub = link.sub;
const nextDep = link.nextDep;
if (nextDep !== undefined) {
link = nextDep;
continue top;
}
}
return dirty && !!sub.flags;
} while (true);
}
function shallowPropagate(link) {
do {
const sub = link.sub;
const flags = sub.flags;
if ((flags & (32 | 16)) === 32) {
sub.flags = flags | 16;
if ((flags & (2 | 4)) === 2) {
notify(sub);
}
}
} while ((link = link.nextSub) !== undefined);
}
function isValidLink(checkLink, sub) {
let link = sub.depsTail;
while (link !== undefined) {
if (link === checkLink) {
return true;
}
link = link.prevDep;
}
return false;
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"name": "alien-signals",
"version": "3.2.1",
"license": "MIT",
"description": "The lightest signal library.",
"packageManager": "pnpm@9.12.0",
"types": "./types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./esm/index.mjs",
"require": "./cjs/index.cjs"
},
"./cjs": {
"types": "./types/index.d.ts",
"import": "./cjs/index.cjs",
"require": "./cjs/index.cjs"
},
"./esm": {
"types": "./types/index.d.ts",
"import": "./esm/index.mjs",
"require": "./esm/index.mjs"
},
"./system": {
"types": "./types/system.d.ts",
"import": "./esm/system.mjs",
"require": "./cjs/system.cjs"
},
"./cjs/system": {
"types": "./types/system.d.ts",
"import": "./cjs/system.cjs",
"require": "./cjs/system.cjs"
},
"./esm/system": {
"types": "./types/system.d.ts",
"import": "./esm/system.mjs",
"require": "./esm/system.mjs"
}
},
"files": [
"cjs/*.cjs",
"esm/*.mjs",
"types/*.d.ts"
],
"repository": {
"type": "git",
"url": "git+https://github.com/stackblitz/alien-signals.git"
},
"scripts": {
"prepublishOnly": "npm run check && npm run test",
"check": "tsslint --project tsconfig.json",
"size": "node ./size.js",
"build": "node ./build.js",
"test": "npm run build && vitest run",
"bench": "npm run build && node --jitless --expose-gc benchs/propagate.mjs",
"memory": "npm run build && node --expose-gc benchs/memoryUsage.mjs"
},
"devDependencies": {
"@tsslint/cli": "latest",
"@tsslint/config": "latest",
"mitata": "latest",
"reactive-framework-test-suite": "0.0.2",
"rolldown": "latest",
"typescript": "latest",
"vitest": "latest"
}
}
+22
View File
@@ -0,0 +1,22 @@
import { type ReactiveNode } from './system.js';
export declare function getActiveSub(): ReactiveNode | undefined;
export declare function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined;
export declare function getBatchDepth(): number;
export declare function startBatch(): void;
export declare function endBatch(): void;
export declare function isSignal(fn: () => void): boolean;
export declare function isComputed(fn: () => void): boolean;
export declare function isEffect(fn: () => void): boolean;
export declare function isEffectScope(fn: () => void): boolean;
export declare function signal<T>(): {
(): T | undefined;
(value: T | undefined): void;
};
export declare function signal<T>(initialValue: T): {
(): T;
(value: T): void;
};
export declare function computed<T>(getter: (previousValue?: T) => T): () => T;
export declare function effect(fn: () => void | (() => void)): () => void;
export declare function effectScope(fn: () => void): () => void;
export declare function trigger(fn: () => void): void;
+36
View File
@@ -0,0 +1,36 @@
export interface ReactiveNode {
deps?: Link;
depsTail?: Link;
subs?: Link;
subsTail?: Link;
flags: ReactiveFlags;
}
export interface Link {
version: number;
dep: ReactiveNode;
sub: ReactiveNode;
prevSub: Link | undefined;
nextSub: Link | undefined;
prevDep: Link | undefined;
nextDep: Link | undefined;
}
export declare const enum ReactiveFlags {
None = 0,
Mutable = 1,
Watching = 2,
RecursedCheck = 4,
Recursed = 8,
Dirty = 16,
Pending = 32
}
export declare function createReactiveSystem({ update, notify, unwatched, }: {
update(sub: ReactiveNode): boolean;
notify(sub: ReactiveNode): void;
unwatched(sub: ReactiveNode): void;
}): {
link: (dep: ReactiveNode, sub: ReactiveNode, version: number) => void;
unlink: (link: Link, sub?: ReactiveNode) => Link | undefined;
propagate: (link: Link, innerWrite: boolean) => void;
checkDirty: (link: Link, sub: ReactiveNode) => boolean;
shallowPropagate: (link: Link) => void;
};