rvx
Version:
A signal based rendering library
152 lines (139 loc) • 4.59 kB
text/typescript
import { NOOP } from "./internals/noop.js";
import { TEARDOWN_STACK, TeardownFrame } from "./internals/teardown-stack.js";
import { useStack } from "./internals/use-stack.js";
/**
* A function that is called to dispose something.
*/
export type TeardownHook = () => void;
/**
* A function that is called after something has been synchronously created. E.g. after rendering a tree of elements.
*/
export type CreatedHook = () => void;
/**
* Internal utility to dispose the specified hooks in reverse order.
*/
function dispose(hooks: TeardownHook[]) {
for (let i = hooks.length - 1; i >= 0; i--) {
hooks[i]();
}
}
export type LeakHook = (hook: TeardownHook) => void;
/**
* Register a hook to be called when any teardown hooks are registered outside of any capture calls.
*
* Errors thrown from the leak hook will be thrown by the **teardown** calls.
*/
export function onLeak(hook: LeakHook): void {
if (TEARDOWN_STACK.length > 0) {
// onLeak must only be called once and outside of any capture calls:
throw new Error("G4");
}
TEARDOWN_STACK.push({ push: hook });
}
/**
* Run a function while capturing teardown hooks.
*
* + If an error is thrown by the specified function, teardown hooks are called in reverse registration order and the error is re-thrown.
* + If an error is thrown by a teardown hook, remaining ones are not called and the error is re-thrown.
*
* @param fn The function to run.
* @returns A function to run all captured teardown hooks in reverse registration order.
*/
export function capture(fn: () => void): TeardownHook {
const hooks: TeardownHook[] = [];
try {
useStack(TEARDOWN_STACK, hooks, fn);
} catch (error) {
dispose(hooks);
throw error;
}
const length = hooks.length;
return length === 1
? hooks[0]
: (length === 0 ? NOOP : () => dispose(hooks));
}
/**
* Run a function while capturing teardown hooks.
*
* + When disposed before the specified function finishes, teardown hooks are called in reverse registration order immediately after the function finishes.
* + If an error is thrown by the specified function, teardown hooks are called in reverse registration order and the error is re-thrown.
* + If an error is thrown by a teardown hook, remaining ones are not called and the error is re-thrown.
*
* @param fn The function to run.
* @returns The function's return value.
*/
export function captureSelf<T>(fn: (dispose: TeardownHook) => T): T {
let disposed = false;
let dispose: TeardownHook = NOOP;
let value: T;
dispose = capture(() => {
value = fn(() => {
disposed = true;
dispose();
});
});
if (disposed) {
dispose();
}
return value!;
}
/**
* Run a function without capturing any teardown hooks.
*
* This is the opposite of {@link capture}.
*
* @param fn The function to run.
* @returns The function's return value.
*/
export function uncapture<T>(fn: () => T): T {
return useStack(TEARDOWN_STACK, undefined, fn);
}
const NOCAPTURE: TeardownFrame = {
push() {
// Teardown hooks are explicitly not supported in this context and registering them is very likely a mistake:
throw new Error("G0");
},
};
/**
* Run a function and explicitly un-support teardown hooks.
*
* Teardown hooks are still supported when using {@link capture}, {@link captureSelf} or {@link uncapture} inside of the function.
*
* This should be used in places where lifecycle side are never expected.
*
* @param fn The function to run.
* @returns The function's return value.
*/
export function nocapture<T>(fn: () => T): T {
return useStack(TEARDOWN_STACK, NOCAPTURE, fn);
}
/**
* Run a function and immediately call teardown hooks if it throws an error.
*
* + If an error is thrown, teardown hooks are immediately called in reverse registration order and the error is re-thrown.
* + If no error is thrown, teardown hooks are registered in the outer context.
*
* @param fn The function to run.
* @returns The function's return value.
*/
export function teardownOnError<T>(fn: () => T): T {
let value!: T;
teardown(capture(() => {
value = fn();
}));
return value;
}
/**
* Register a teardown hook to be called when the current lifecycle is disposed.
*
* This has no effect if teardown hooks are not captured in the current context.
*
* @param hook The hook to register. This may be called multiple times.
* @throws An error if teardown hooks are {@link nocapture explicitly un-supported}.
*/
export function teardown(hook: TeardownHook): void {
const length = TEARDOWN_STACK.length;
if (length > 0) {
TEARDOWN_STACK[length - 1]?.push(hook);
}
}