Implement ReactiveRoot.dispose() and real dispose on ReactiveNode

Adds ReactiveRoot.dispose() which calls render effect disposer, iterates
all tracked subscriber disposers, and clears internal state. subscribe()
now tracks effect disposers in a Set and returns idempotent unsubscribe.
render() now disposes previous render effect before overwriting. Both
reactiveComponent and reactiveElement return real dispose functions that
sever the computed signal reference on disposal.
This commit is contained in:
2026-05-18 17:23:51 +00:00
parent 95995f4602
commit f14916c1bb
2 changed files with 200 additions and 5 deletions

View File

@@ -1,6 +1,9 @@
import { signal, computed, effect, batch, type Signal, type ReadonlySignal } from "@preact/signals-core";
import type { UNode, UElement, UniversalProps, UComponent } from "./schema.js";
const _disposedSignal = signal<UNode>(undefined as unknown as UNode);
const disposedReadonlySignal: ReadonlySignal<UNode> = _disposedSignal;
export interface ReactiveNode {
readonly type: string;
readonly signal: ReadonlySignal<UNode>;
@@ -15,13 +18,19 @@ export function reactiveComponent<P extends UniversalProps>(
const props = propsSignal.value;
return component(props);
});
let disposed = false;
return {
get type() {
return component.displayName ?? "anonymous";
},
signal: nodeSignal,
dispose: () => {},
get signal() {
return disposed ? disposedReadonlySignal : nodeSignal;
},
dispose: () => {
if (disposed) return;
disposed = true;
},
};
}
@@ -38,17 +47,25 @@ export function reactiveElement(
children,
} as UElement;
});
let disposed = false;
return {
type,
signal: nodeSignal,
dispose: () => {},
get signal() {
return disposed ? disposedReadonlySignal : nodeSignal;
},
dispose: () => {
if (disposed) return;
disposed = true;
},
};
}
export class ReactiveRoot {
private root: Signal<UNode>;
private renderDisposer: (() => void) | null = null;
private subscriberDisposers: Set<() => void> = new Set();
private disposed = false;
constructor(initial: UNode) {
this.root = signal(initial);
@@ -65,12 +82,24 @@ export class ReactiveRoot {
}
subscribe(listener: (node: UNode) => void): () => void {
return effect(() => {
const disposer = effect(() => {
listener(this.root.value);
});
this.subscriberDisposers.add(disposer);
let unsubscribed = false;
return () => {
if (unsubscribed) return;
unsubscribed = true;
disposer();
this.subscriberDisposers.delete(disposer);
};
}
render(emit: (event: { type: string; id: string; payload: unknown }) => void): () => void {
if (this.renderDisposer) {
this.renderDisposer();
this.renderDisposer = null;
}
this.renderDisposer = effect(() => {
const node = this.root.value;
emit({
@@ -86,6 +115,19 @@ export class ReactiveRoot {
}
};
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
if (this.renderDisposer) {
this.renderDisposer();
this.renderDisposer = null;
}
for (const disposer of this.subscriberDisposers) {
disposer();
}
this.subscriberDisposers.clear();
}
}
export { signal, computed, effect, batch };