track signal dependencies

This commit is contained in:
Yura Dupyn 2026-02-16 20:35:24 +01:00
parent c0198d419f
commit b9332ad565
2 changed files with 32 additions and 16 deletions

View file

@ -65,12 +65,13 @@ export function eval_signal_expression(program: Program, env: Env, e: SignalExpr
// Setup a subscription to each dependency, when it changes, poll every signal and re-evaluate
// TODO: This is extremely inefficient.
for (const { signal } of signalBindings) {
signal.subscribe(() => {
const cancelSignal = signal.subscribe(() => {
letSignal.set(() => {
const value = eval_expr_in_signal_bindings(program, env, signalBindings, e.body);
return value;
});
});
letSignal.dependencies.push(cancelSignal);
}
return letSignal;
@ -117,16 +118,20 @@ export interface Signal<T> {
observers: Observer<T>[],
set(transform: (state: T) => T): void,
read(): T,
subscribe(observer: Observer<T>): void,
subscribe(observer: Observer<T>): UnsubscribeCapability,
map<S>(transform: (state: T) => S): Signal<S>,
dependencies: UnsubscribeCapability[],
dropDependencies(): void,
};
export type Observer<T> = (state: T) => void;
export type UnsubscribeCapability = () => void;
export function signal<T>(initState: T): Signal<T> {
return {
state: initState,
observers: [],
dependencies: [],
set(transform: (state: T) => T) {
const state = transform(this.state);
this.state = state;
@ -138,27 +143,39 @@ export function signal<T>(initState: T): Signal<T> {
return this.state;
},
subscribe(observer: Observer<T>) {
// TODO: This needs to return `cancellation`
this.observers.push(observer);
const that = this;
return () => {
that.observers = that.observers.filter(sub => sub !== observer);
};
},
dropDependencies() {
for (const cancel of this.dependencies) {
cancel()
}
this.dependencies = [];
},
map<S>(transform: (state: T) => S): Signal<S> {
const Y = signal(transform(this.state));
this.subscribe((state: T) => {
const cancelY = this.subscribe((state: T) => {
Y.set(() => transform(state));
});
Y.dependencies.push(cancelY);
return Y;
}
},
};
}
function pair<A, B>(X: Signal<A>, Y: Signal<B>): Signal<[A, B]> {
const Z = signal([X.read(), Y.read()] as [A, B]);
X.subscribe(x => {
const cancelX = X.subscribe(x => {
Z.set(() => [x, Y.read()]);
});
Y.subscribe(y => {
Z.dependencies.push(cancelX);
const cancelY = Y.subscribe(y => {
Z.set(() => [X.read(), y]);
});
Z.dependencies.push(cancelY);
return Z;
}
@ -167,9 +184,10 @@ function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
// TODO: This is just preliminary. Has the glitch bug. Also has insane quadratic behaviour.
Xs.forEach((X, i) => {
X.subscribe(_ => {
const cancelX = X.subscribe(_ => {
Z.set(() => f(Xs.map(X => X.read())));
});
Z.dependencies.push(cancelX);
});
return Z;
@ -183,7 +201,7 @@ function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
// console.log(count.read())
// count.subscribe(x => {
// const _ = count.subscribe(x => {
// console.log("count is now", x);
// });
@ -194,7 +212,7 @@ function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
// console.log("DOUBLE EXISTS");
// double.subscribe(x => {
// const _ = double.subscribe(x => {
// console.log("double is now", x);
// });
@ -204,7 +222,7 @@ function tupleThen<A, B>(Xs: Signal<A>[], f: (values: A[]) => B): Signal<B> {
// const WTF = pair(count, double)
// console.log("-> WTF EXISTS");
// WTF.subscribe(([x, y]) => {
// const _ = WTF.subscribe(([x, y]) => {
// console.log("WTF is now ", [x, y]);
// });

View file

@ -51,13 +51,11 @@ export function SignalDigith(props: { signal: Digith.Signal }) {
// TODO: Not sure about this one. Setting a signal in `setValue` is discouraged.
setTimeout(() => setValue(signal.read()), 0);
// TODO: Handle cancelation...
signal.subscribe((newValue) => {
const cancel = signal.subscribe((newValue) => {
setValue(newValue);
});
onCleanup(() => {
// TODO:
});
onCleanup(cancel);
} catch (e) {
// TODO: setErrors... but how? Shouldn't this be independent of `errors`?
console.log("Failed to link Signal: ", e);