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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user