rvx
Version:
A signal based rendering library
264 lines • 6.45 kB
JavaScript
import { Context } from "./context.js";
import { NOOP } from "./internals/noop.js";
import { useStack } from "./internals/use-stack.js";
import { capture, nocapture, teardown } from "./lifecycle.js";
let BATCH;
const TRACKING_STACK = [true];
const ACCESS_STACK = [];
const notify = (fn) => fn();
const queueBatch = (fn) => BATCH.add(fn);
export class Signal {
#value;
#hooks = new Set();
#source;
#root;
constructor(value, source) {
this.#value = value;
this.#source = source;
this.#root = source ? source.#root : this;
}
get value() {
this.access();
return this.#value;
}
set value(value) {
if (!Object.is(this.#value, value)) {
this.#value = value;
this.notify();
}
}
get source() {
return this.#source;
}
get root() {
return this.#root;
}
update(fn) {
if (fn(this.#value) !== false) {
this.notify();
}
}
get active() {
return this.#hooks.size > 0;
}
access() {
if (TRACKING_STACK[TRACKING_STACK.length - 1]) {
const length = ACCESS_STACK.length;
if (length > 0) {
ACCESS_STACK[length - 1]?.(this.#hooks);
}
}
}
notify() {
const hooks = this.#hooks;
if (hooks.size === 0) {
return;
}
if (BATCH === undefined) {
const record = Array.from(hooks);
hooks.clear();
record.forEach(notify);
}
else {
hooks.forEach(queueBatch);
}
}
pipe(fn, ...args) {
return fn(this, ...args);
}
}
export function $(value, source) {
return new Signal(value, source);
}
const _unfold = (hook) => {
let depth = 0;
return () => {
if (depth < 2) {
depth++;
}
if (depth === 1) {
try {
while (depth > 0) {
hook();
depth--;
}
}
finally {
depth = 0;
}
}
};
};
const _observer = (hook) => {
const signals = [];
return {
c: () => {
for (let i = 0; i < signals.length; i++) {
signals[i].delete(hook);
}
signals.length = 0;
},
a: (hooks) => {
signals.push(hooks);
hooks.add(hook);
},
};
};
const _access = (frame, fn) => {
try {
ACCESS_STACK.push(frame);
TRACKING_STACK.push(true);
return fn();
}
finally {
ACCESS_STACK.pop();
TRACKING_STACK.pop();
}
};
export function watch(expr, fn) {
const isSignal = expr instanceof Signal;
if (isSignal || typeof expr === "function") {
let value;
let disposed = false;
let dispose = NOOP;
const runExpr = isSignal ? () => expr.value : expr;
const runFn = () => fn(value);
const entry = _unfold(Context.wrap(() => {
if (disposed) {
return;
}
clear();
value = _access(access, () => nocapture(runExpr));
dispose = _access(undefined, () => {
dispose();
return capture(runFn);
});
}));
const { c: clear, a: access } = _observer(entry);
teardown(() => {
disposed = true;
clear();
dispose();
});
entry();
}
else {
fn(expr);
}
}
export function watchUpdates(expr, fn) {
let first;
let update = false;
watch(expr, value => {
if (update) {
fn(value);
}
else {
first = value;
update = true;
}
});
return first;
}
export function effect(fn) {
let disposed = false;
let dispose = NOOP;
const runFn = Context.wrap(fn);
const entry = _unfold(() => {
if (disposed) {
return;
}
useStack(ACCESS_STACK, undefined, dispose);
clear();
dispose = _access(access, () => capture(runFn));
});
const { c: clear, a: access } = _observer(entry);
teardown(() => {
disposed = true;
clear();
dispose();
});
entry();
}
export function batch(fn) {
if (BATCH === undefined) {
const batch = new Set();
let value;
try {
BATCH = batch;
value = fn();
while (batch.size > 0) {
batch.forEach(notify => {
batch.delete(notify);
notify();
});
}
}
finally {
BATCH = undefined;
}
return value;
}
return fn();
}
export function memo(fn) {
const signal = $(undefined);
effect(() => signal.value = get(fn));
return () => signal.value;
}
export function untrack(fn) {
return useStack(TRACKING_STACK, false, fn);
}
export function track(fn) {
return useStack(TRACKING_STACK, true, fn);
}
export function isTracking() {
return TRACKING_STACK[TRACKING_STACK.length - 1] && ACCESS_STACK[ACCESS_STACK.length - 1] !== undefined;
}
export function trigger(fn) {
const hookFn = Context.wrap(() => {
clear();
_access(undefined, fn);
});
const { c: clear, a: access } = _observer(hookFn);
teardown(clear);
return (expr) => {
clear();
try {
const outerLength = ACCESS_STACK.length;
if (outerLength > 0) {
const outer = ACCESS_STACK[outerLength - 1];
ACCESS_STACK.push(hooks => {
access(hooks);
outer?.(hooks);
});
}
else {
ACCESS_STACK.push(access);
}
return get(expr);
}
finally {
ACCESS_STACK.pop();
}
};
}
export function get(expr) {
if (expr instanceof Signal) {
return expr.value;
}
if (typeof expr === "function") {
return expr();
}
return expr;
}
export function map(input, mapFn) {
if (input instanceof Signal) {
return () => mapFn(input.value);
}
if (typeof input === "function") {
return () => mapFn(input());
}
return mapFn(input);
}
//# sourceMappingURL=signals.js.map