initial reactivity code
This commit is contained in:
parent
884f773575
commit
1c361de610
3 changed files with 607 additions and 0 deletions
|
|
@ -4,6 +4,7 @@
|
|||
"cobweb/": "./src/",
|
||||
"cobweb/routing": "./src/router.ts",
|
||||
"cobweb/helpers": "./src/helpers.ts",
|
||||
"cobweb/signals": "./src/signals.ts",
|
||||
"cobweb/jsx-runtime": "./src/jsx.ts"
|
||||
},
|
||||
"fmt": {
|
||||
|
|
|
|||
14
example.tsx
14
example.tsx
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { createRouter } from "cobweb/routing";
|
||||
import { Defer } from "cobweb/jsx-runtime";
|
||||
import { computed, effect, signal } from "cobweb/signals.ts";
|
||||
|
||||
interface Todo {
|
||||
id: string;
|
||||
|
|
@ -61,3 +62,16 @@ app.get("/meow/:test?", async (ctx) => {
|
|||
});
|
||||
|
||||
Deno.serve({ port: 8000 }, app.fetch);
|
||||
|
||||
const count = signal(1);
|
||||
const doubleCount = computed(() => count() * 2);
|
||||
|
||||
effect(() => {
|
||||
console.log(`Count is: ${count()}`);
|
||||
});
|
||||
|
||||
console.log(doubleCount());
|
||||
|
||||
count(2);
|
||||
|
||||
console.log("meow", doubleCount());
|
||||
|
|
|
|||
592
src/signals.ts
Normal file
592
src/signals.ts
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
/**
|
||||
* Copyright (c) 2025 favewa
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
// https://github.com/stackblitz/alien-signals/blob/master/src/{index,system}.ts
|
||||
|
||||
export interface ReactiveNode {
|
||||
firstSource?: Subscriber;
|
||||
lastSource?: Subscriber;
|
||||
firstObserver?: Subscriber;
|
||||
lastObserver?: Subscriber;
|
||||
flags: ReactiveFlags;
|
||||
}
|
||||
|
||||
export interface Subscriber {
|
||||
version: number;
|
||||
source: ReactiveNode;
|
||||
observer: ReactiveNode;
|
||||
previousObserver?: Subscriber;
|
||||
nextObserver?: Subscriber;
|
||||
previousSource?: Subscriber;
|
||||
nextSource?: Subscriber;
|
||||
}
|
||||
|
||||
interface StackNode<T> {
|
||||
value: T;
|
||||
previous?: StackNode<T>;
|
||||
}
|
||||
|
||||
export const enum ReactiveFlags {
|
||||
None = 0,
|
||||
Mutable = 1 << 0,
|
||||
Watching = 1 << 1,
|
||||
RecursionCheck = 1 << 2,
|
||||
Recursed = 1 << 3,
|
||||
Dirty = 1 << 4,
|
||||
Pending = 1 << 5,
|
||||
}
|
||||
|
||||
interface EffectNode extends ReactiveNode {
|
||||
execute(): void;
|
||||
}
|
||||
|
||||
interface ComputedNode<T = any> extends ReactiveNode {
|
||||
cachedValue?: T;
|
||||
compute: (previousValue?: T) => T;
|
||||
}
|
||||
|
||||
interface SignalNode<T = any> extends ReactiveNode {
|
||||
currentValue: T;
|
||||
pendingValue: T;
|
||||
}
|
||||
|
||||
let versionCounter = 0;
|
||||
let notifyIndex = 0;
|
||||
let queuedLength = 0;
|
||||
let activeObserver: ReactiveNode | undefined;
|
||||
|
||||
const effectQueue: (EffectNode | undefined)[] = [];
|
||||
|
||||
function subscribe(
|
||||
source: ReactiveNode,
|
||||
observer: ReactiveNode,
|
||||
version: number,
|
||||
): void {
|
||||
const lastSource = observer.lastSource;
|
||||
|
||||
if (lastSource?.source === source) return;
|
||||
|
||||
const nextSource = lastSource?.nextSource ?? observer.firstSource;
|
||||
if (nextSource?.source === source) {
|
||||
nextSource.version = version;
|
||||
observer.lastSource = nextSource;
|
||||
return;
|
||||
}
|
||||
|
||||
const lastObserver = source.lastObserver;
|
||||
if (lastObserver?.version === version && lastObserver.observer === observer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriber: Subscriber = {
|
||||
version,
|
||||
source,
|
||||
observer,
|
||||
previousSource: lastSource,
|
||||
nextSource,
|
||||
previousObserver: lastObserver,
|
||||
nextObserver: undefined,
|
||||
};
|
||||
|
||||
observer.lastSource = source.lastObserver = subscriber;
|
||||
|
||||
if (nextSource) nextSource.previousSource = subscriber;
|
||||
if (lastSource) lastSource.nextSource = subscriber;
|
||||
else observer.firstSource = subscriber;
|
||||
|
||||
if (lastObserver) lastObserver.nextObserver = subscriber;
|
||||
else source.firstObserver = subscriber;
|
||||
}
|
||||
|
||||
function unsubscribe(
|
||||
subscriber: Subscriber,
|
||||
observer = subscriber.observer,
|
||||
): Subscriber | undefined {
|
||||
const {
|
||||
source,
|
||||
previousSource: prevSource,
|
||||
nextSource,
|
||||
previousObserver: prevObserver,
|
||||
nextObserver,
|
||||
} = subscriber;
|
||||
|
||||
if (nextSource) nextSource.previousSource = prevSource;
|
||||
else observer.lastSource = prevSource;
|
||||
|
||||
if (prevSource) prevSource.nextSource = nextSource;
|
||||
else observer.firstSource = nextSource;
|
||||
|
||||
if (nextObserver) nextObserver.previousObserver = prevObserver;
|
||||
else source.lastObserver = prevObserver;
|
||||
|
||||
if (prevObserver) prevObserver.nextObserver = nextObserver;
|
||||
else if (!(source.firstObserver = nextObserver)) {
|
||||
handleUnwatched(source);
|
||||
}
|
||||
|
||||
return nextSource;
|
||||
}
|
||||
|
||||
function propagate(subscriber: Subscriber): void {
|
||||
let current = subscriber;
|
||||
let next = subscriber.nextObserver;
|
||||
let stack: StackNode<Subscriber | undefined> | undefined;
|
||||
|
||||
top: do {
|
||||
const observer = current.observer;
|
||||
let flags = observer.flags;
|
||||
const isClean = !(flags &
|
||||
(ReactiveFlags.RecursionCheck | ReactiveFlags.Recursed |
|
||||
ReactiveFlags.Dirty | ReactiveFlags.Pending));
|
||||
const noRecursionFlags =
|
||||
!(flags & (ReactiveFlags.RecursionCheck | ReactiveFlags.Recursed));
|
||||
const needsRecursionCheck = !(flags & ReactiveFlags.RecursionCheck);
|
||||
|
||||
if (isClean) {
|
||||
observer.flags = flags | ReactiveFlags.Pending;
|
||||
} else if (noRecursionFlags) {
|
||||
flags = ReactiveFlags.None;
|
||||
} else if (needsRecursionCheck) {
|
||||
observer.flags = (flags & ~ReactiveFlags.Recursed) |
|
||||
ReactiveFlags.Pending;
|
||||
} else if (
|
||||
!(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) &&
|
||||
isValidSubscriber(current, observer)
|
||||
) {
|
||||
observer.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending;
|
||||
flags &= ReactiveFlags.Mutable;
|
||||
} else {
|
||||
flags = ReactiveFlags.None;
|
||||
}
|
||||
|
||||
if (flags & ReactiveFlags.Watching) notify(observer as EffectNode);
|
||||
|
||||
if (flags & ReactiveFlags.Mutable) {
|
||||
const observerSubs = observer.firstObserver;
|
||||
if (observerSubs) {
|
||||
const nextSub = (current = observerSubs).nextObserver;
|
||||
if (nextSub) {
|
||||
stack = { value: next, previous: stack };
|
||||
next = nextSub;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ((current = next!)) {
|
||||
next = current.nextObserver;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (stack) {
|
||||
current = stack.value!;
|
||||
stack = stack.previous;
|
||||
if (current) {
|
||||
next = current.nextObserver;
|
||||
continue top;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
} while (true);
|
||||
}
|
||||
|
||||
function checkDirty(subscriber: Subscriber, observer: ReactiveNode): boolean {
|
||||
let current = subscriber;
|
||||
let currentObserver = observer;
|
||||
let stack: StackNode<Subscriber> | undefined;
|
||||
let checkDepth = 0;
|
||||
let isDirty = false;
|
||||
|
||||
top: do {
|
||||
const source = current.source;
|
||||
const flags = source.flags;
|
||||
|
||||
if (currentObserver.flags & ReactiveFlags.Dirty) {
|
||||
isDirty = true;
|
||||
} else if (
|
||||
(flags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) ===
|
||||
(ReactiveFlags.Mutable | ReactiveFlags.Dirty)
|
||||
) {
|
||||
if (update(source as SignalNode)) {
|
||||
const subs = source.firstObserver!;
|
||||
if (subs.nextObserver) shallowPropagate(subs);
|
||||
isDirty = true;
|
||||
}
|
||||
} else if (
|
||||
(flags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) ===
|
||||
(ReactiveFlags.Mutable | ReactiveFlags.Pending)
|
||||
) {
|
||||
if (current.nextObserver || current.previousObserver) {
|
||||
stack = { value: current, previous: stack };
|
||||
}
|
||||
current = source.firstSource!;
|
||||
currentObserver = source;
|
||||
++checkDepth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDirty) {
|
||||
const nextSource = current.nextSource;
|
||||
if (nextSource) {
|
||||
current = nextSource;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
while (checkDepth--) {
|
||||
const firstSub = currentObserver.firstObserver!;
|
||||
const hasMultipleObservers = !!firstSub.nextObserver;
|
||||
|
||||
current = hasMultipleObservers ? stack!.value : firstSub;
|
||||
if (hasMultipleObservers) stack = stack!.previous;
|
||||
|
||||
if (isDirty) {
|
||||
if (update(currentObserver as SignalNode)) {
|
||||
if (hasMultipleObservers) shallowPropagate(firstSub);
|
||||
currentObserver = current.observer;
|
||||
continue;
|
||||
}
|
||||
isDirty = false;
|
||||
} else {
|
||||
currentObserver.flags &= ~ReactiveFlags.Pending;
|
||||
}
|
||||
|
||||
currentObserver = current.observer;
|
||||
const nextSource = current.nextSource;
|
||||
if (nextSource) {
|
||||
current = nextSource;
|
||||
continue top;
|
||||
}
|
||||
}
|
||||
|
||||
return isDirty;
|
||||
} while (true);
|
||||
}
|
||||
|
||||
function shallowPropagate(subscriber: Subscriber): void {
|
||||
let current: Subscriber | undefined = subscriber;
|
||||
|
||||
do {
|
||||
const observer = current.observer;
|
||||
const flags = observer.flags;
|
||||
|
||||
if (
|
||||
(flags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) ===
|
||||
ReactiveFlags.Pending
|
||||
) {
|
||||
observer.flags = flags | ReactiveFlags.Dirty;
|
||||
if (
|
||||
(flags & (ReactiveFlags.Watching | ReactiveFlags.RecursionCheck)) ===
|
||||
ReactiveFlags.Watching
|
||||
) {
|
||||
notify(observer as EffectNode);
|
||||
}
|
||||
}
|
||||
} while ((current = current.nextObserver));
|
||||
}
|
||||
|
||||
function isValidSubscriber(
|
||||
checkSubscriber: Subscriber,
|
||||
observer: ReactiveNode,
|
||||
): boolean {
|
||||
let subscriber = observer.lastSource;
|
||||
while (subscriber) {
|
||||
if (subscriber === checkSubscriber) return true;
|
||||
subscriber = subscriber.previousSource;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function update(node: SignalNode | ComputedNode): boolean {
|
||||
return node.firstSource
|
||||
? updateComputed(node as ComputedNode)
|
||||
: updateSignal(node as SignalNode);
|
||||
}
|
||||
|
||||
function notify(effect: EffectNode) {
|
||||
let insertIndex = queuedLength;
|
||||
const firstInsertedIndex = insertIndex;
|
||||
let currentEffect: EffectNode | undefined = effect;
|
||||
|
||||
do {
|
||||
currentEffect.flags &= ~ReactiveFlags.Watching;
|
||||
effectQueue[insertIndex++] = currentEffect;
|
||||
currentEffect = currentEffect.firstObserver?.observer as EffectNode;
|
||||
} while (currentEffect?.flags & ReactiveFlags.Watching);
|
||||
|
||||
queuedLength = insertIndex;
|
||||
|
||||
for (let i = firstInsertedIndex, j = insertIndex - 1; i < j; i++, j--) {
|
||||
[effectQueue[i], effectQueue[j]] = [effectQueue[j], effectQueue[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnwatched(node: ReactiveNode) {
|
||||
if (!(node.flags & ReactiveFlags.Mutable)) {
|
||||
disposeEffectScope.call(node);
|
||||
} else if (node.firstSource) {
|
||||
node.lastSource = undefined;
|
||||
node.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty;
|
||||
clearSources(node);
|
||||
}
|
||||
}
|
||||
|
||||
export function setActiveObserver(observer?: ReactiveNode) {
|
||||
const previous = activeObserver;
|
||||
activeObserver = observer;
|
||||
return previous;
|
||||
}
|
||||
|
||||
export function signal<T>(): {
|
||||
(): T | undefined;
|
||||
(value: T | undefined): void;
|
||||
};
|
||||
export function signal<T>(initialValue: T): {
|
||||
(): T;
|
||||
(value: T): void;
|
||||
};
|
||||
export function signal<T>(initialValue?: T) {
|
||||
return signalOperation.bind({
|
||||
currentValue: initialValue,
|
||||
pendingValue: initialValue,
|
||||
firstObserver: undefined,
|
||||
lastObserver: undefined,
|
||||
flags: ReactiveFlags.Mutable,
|
||||
}) as () => T | undefined;
|
||||
}
|
||||
|
||||
export function computed<T>(compute: (previousValue?: T) => T): () => T {
|
||||
return computedOperation.bind({
|
||||
cachedValue: undefined,
|
||||
firstObserver: undefined,
|
||||
lastObserver: undefined,
|
||||
firstSource: undefined,
|
||||
lastSource: undefined,
|
||||
flags: ReactiveFlags.None,
|
||||
compute: compute as (previousValue?: unknown) => unknown,
|
||||
}) as () => T;
|
||||
}
|
||||
|
||||
export function effect(fn: () => void): () => void {
|
||||
const effectNode: EffectNode = {
|
||||
execute: fn,
|
||||
firstObserver: undefined,
|
||||
lastObserver: undefined,
|
||||
firstSource: undefined,
|
||||
lastSource: undefined,
|
||||
flags: ReactiveFlags.Watching | ReactiveFlags.RecursionCheck,
|
||||
};
|
||||
|
||||
const prevObserver = setActiveObserver(effectNode);
|
||||
if (prevObserver) subscribe(effectNode, prevObserver, 0);
|
||||
|
||||
try {
|
||||
effectNode.execute();
|
||||
} finally {
|
||||
activeObserver = prevObserver;
|
||||
effectNode.flags &= ~ReactiveFlags.RecursionCheck;
|
||||
}
|
||||
|
||||
return effectOperation.bind(effectNode);
|
||||
}
|
||||
|
||||
export function effectScope(fn: () => void): () => void {
|
||||
const scopeNode: ReactiveNode = {
|
||||
firstSource: undefined,
|
||||
lastSource: undefined,
|
||||
firstObserver: undefined,
|
||||
lastObserver: undefined,
|
||||
flags: ReactiveFlags.None,
|
||||
};
|
||||
|
||||
const prevObserver = setActiveObserver(scopeNode);
|
||||
if (prevObserver) subscribe(scopeNode, prevObserver, 0);
|
||||
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
activeObserver = prevObserver;
|
||||
}
|
||||
|
||||
return disposeEffectScope.bind(scopeNode);
|
||||
}
|
||||
|
||||
export function trigger(fn: () => void) {
|
||||
const triggerNode: ReactiveNode = {
|
||||
firstSource: undefined,
|
||||
lastSource: undefined,
|
||||
flags: ReactiveFlags.Watching,
|
||||
};
|
||||
|
||||
const prevObserver = setActiveObserver(triggerNode);
|
||||
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
activeObserver = prevObserver;
|
||||
|
||||
while (triggerNode.firstSource) {
|
||||
const subscriber = triggerNode.firstSource;
|
||||
const source = subscriber.source;
|
||||
unsubscribe(subscriber, triggerNode);
|
||||
|
||||
if (source.firstObserver) {
|
||||
propagate(source.firstObserver);
|
||||
shallowPropagate(source.firstObserver);
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
function updateComputed(node: ComputedNode): boolean {
|
||||
++versionCounter;
|
||||
node.lastSource = undefined;
|
||||
node.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursionCheck;
|
||||
|
||||
const prevObserver = setActiveObserver(node);
|
||||
|
||||
try {
|
||||
const oldValue = node.cachedValue;
|
||||
const newValue = node.compute(oldValue);
|
||||
node.cachedValue = newValue;
|
||||
return oldValue !== newValue;
|
||||
} finally {
|
||||
activeObserver = prevObserver;
|
||||
node.flags &= ~ReactiveFlags.RecursionCheck;
|
||||
clearSources(node);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSignal(signal: SignalNode): boolean {
|
||||
signal.flags = ReactiveFlags.Mutable;
|
||||
const hasChanged = signal.currentValue !== signal.pendingValue;
|
||||
signal.currentValue = signal.pendingValue;
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
function runEffect(effect: EffectNode): void {
|
||||
const flags = effect.flags;
|
||||
const needsRun = (flags & ReactiveFlags.Dirty) ||
|
||||
(flags & ReactiveFlags.Pending && checkDirty(effect.firstSource!, effect));
|
||||
|
||||
if (needsRun) {
|
||||
++versionCounter;
|
||||
effect.lastSource = undefined;
|
||||
effect.flags = ReactiveFlags.Watching | ReactiveFlags.RecursionCheck;
|
||||
|
||||
const prevObserver = setActiveObserver(effect);
|
||||
|
||||
try {
|
||||
effect.execute();
|
||||
} finally {
|
||||
activeObserver = prevObserver;
|
||||
effect.flags &= ~ReactiveFlags.RecursionCheck;
|
||||
clearSources(effect);
|
||||
}
|
||||
} else {
|
||||
effect.flags = ReactiveFlags.Watching;
|
||||
}
|
||||
}
|
||||
|
||||
function flush(): void {
|
||||
while (notifyIndex < queuedLength) {
|
||||
const effect = effectQueue[notifyIndex]!;
|
||||
effectQueue[notifyIndex++] = undefined;
|
||||
runEffect(effect);
|
||||
}
|
||||
notifyIndex = queuedLength = 0;
|
||||
}
|
||||
|
||||
function computedOperation<T>(this: ComputedNode<T>): T {
|
||||
const flags = this.flags;
|
||||
const isDirty = flags & ReactiveFlags.Dirty;
|
||||
const isPending = flags & ReactiveFlags.Pending;
|
||||
|
||||
if (
|
||||
isDirty ||
|
||||
(isPending &&
|
||||
(checkDirty(this.firstSource!, this) ||
|
||||
(this.flags = flags & ~ReactiveFlags.Pending, false)))
|
||||
) {
|
||||
if (updateComputed(this)) {
|
||||
const subs = this.firstObserver;
|
||||
if (subs) shallowPropagate(subs);
|
||||
}
|
||||
} else if (!flags) {
|
||||
this.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursionCheck;
|
||||
const prevObserver = setActiveObserver(this);
|
||||
|
||||
try {
|
||||
this.cachedValue = this.compute();
|
||||
} finally {
|
||||
activeObserver = prevObserver;
|
||||
this.flags &= ~ReactiveFlags.RecursionCheck;
|
||||
}
|
||||
}
|
||||
|
||||
const observer = activeObserver;
|
||||
if (observer) subscribe(this, observer, versionCounter);
|
||||
|
||||
return this.cachedValue!;
|
||||
}
|
||||
|
||||
function signalOperation<T>(this: SignalNode<T>, ...args: [T]): T | void {
|
||||
if (args.length) {
|
||||
const newValue = args[0];
|
||||
if (this.pendingValue !== newValue) {
|
||||
this.pendingValue = newValue;
|
||||
this.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty;
|
||||
|
||||
const subs = this.firstObserver;
|
||||
if (subs) {
|
||||
propagate(subs);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.flags & ReactiveFlags.Dirty) {
|
||||
if (updateSignal(this)) {
|
||||
const subs = this.firstObserver;
|
||||
if (subs) shallowPropagate(subs);
|
||||
}
|
||||
}
|
||||
|
||||
let observer = activeObserver;
|
||||
while (observer) {
|
||||
if (observer.flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
|
||||
subscribe(this, observer, versionCounter);
|
||||
break;
|
||||
}
|
||||
observer = observer.firstObserver?.observer;
|
||||
}
|
||||
|
||||
return this.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
function effectOperation(this: EffectNode): void {
|
||||
disposeEffectScope.call(this);
|
||||
}
|
||||
|
||||
function disposeEffectScope(this: ReactiveNode): void {
|
||||
this.lastSource = undefined;
|
||||
this.flags = ReactiveFlags.None;
|
||||
clearSources(this);
|
||||
|
||||
const sub = this.firstObserver;
|
||||
if (sub) unsubscribe(sub);
|
||||
}
|
||||
|
||||
function clearSources(observer: ReactiveNode) {
|
||||
const sourcesTail = observer.lastSource;
|
||||
let current = sourcesTail?.nextSource ?? observer.firstSource;
|
||||
|
||||
while (current) {
|
||||
current = unsubscribe(current, observer);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue