rvx
Version:
A signal based rendering library
367 lines • 8.46 kB
JavaScript
import { Context } from "./context.js";
import { NOOP } from "./internals/noop.js";
const THROW_ON_LEAK = {
push(_hook) {
throw new Error("G5");
},
};
const LEAK = {
push() { },
};
let TEARDOWN_FRAME = THROW_ON_LEAK;
let ACCESS_FRAME;
function dispose(hooks) {
for (let i = hooks.length - 1; i >= 0; i--) {
hooks[i]();
}
}
export function capture(fn) {
const parent = TEARDOWN_FRAME;
const hooks = [];
try {
TEARDOWN_FRAME = hooks;
fn();
}
catch (error) {
isolate(dispose, hooks);
throw error;
}
finally {
TEARDOWN_FRAME = parent;
}
return hooks.length === 0 ? NOOP : () => isolate(dispose, hooks);
}
export function captureSelf(fn) {
let disposed = false;
let dispose = NOOP;
let value;
dispose = capture(() => {
value = fn(() => {
disposed = true;
dispose();
});
});
if (disposed) {
dispose();
}
return value;
}
export function leak(fn) {
const parent = TEARDOWN_FRAME;
try {
TEARDOWN_FRAME = LEAK;
return fn();
}
finally {
TEARDOWN_FRAME = parent;
}
}
export function teardownOnError(fn) {
let value;
teardown(capture(() => {
value = fn();
}));
return value;
}
export function teardown(hook) {
return TEARDOWN_FRAME.push(hook);
}
export function isolate(fn, ...args) {
const parentTeardownFrame = TEARDOWN_FRAME;
const parentAccessFrame = ACCESS_FRAME;
try {
TEARDOWN_FRAME = THROW_ON_LEAK;
ACCESS_FRAME = undefined;
return fn(...args);
}
finally {
TEARDOWN_FRAME = parentTeardownFrame;
ACCESS_FRAME = parentAccessFrame;
}
}
export function isIsolated() {
return TEARDOWN_FRAME === THROW_ON_LEAK && ACCESS_FRAME === undefined;
}
let BATCH;
const _notify = (fn) => {
try {
fn();
}
catch (error) {
Promise.reject(error);
}
};
const _queueBatch = (fn) => BATCH.add(fn);
export class Signal {
inert;
#core = {
c: 0,
h: new Set(),
};
#source;
#root;
constructor(value, source) {
this.inert = value;
this.#source = source;
this.#root = source ? source.#root : this;
}
get value() {
this.access();
return this.inert;
}
set value(value) {
if (!Object.is(this.inert, value)) {
this.inert = value;
this.notify();
}
}
get source() {
return this.#source;
}
get root() {
return this.#root;
}
get active() {
return this.#core.h.size > 0;
}
access() {
ACCESS_FRAME?.(this.#core);
}
notify() {
const core = this.#core;
core.c++;
if (core.h.size === 0) {
return;
}
if (BATCH === undefined) {
const record = Array.from(core.h);
core.h.clear();
record.forEach(_notify);
}
else {
core.h.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].h.delete(hook);
}
signals.length = 0;
},
a: (hooks) => {
signals.push(hooks);
hooks.h.add(hook);
},
};
};
export function watch(expr, effect) {
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 entry = _unfold(Context.bind(() => {
if (disposed) {
return;
}
clear();
isolate(dispose);
dispose = capture(() => {
const parent = ACCESS_FRAME;
try {
ACCESS_FRAME = access;
value = runExpr();
if (effect) {
ACCESS_FRAME = undefined;
effect(value);
}
}
finally {
ACCESS_FRAME = parent;
}
});
}));
const { c: clear, a: access } = _observer(entry);
teardown(() => {
disposed = true;
clear();
dispose();
});
entry();
}
else {
effect(expr);
}
}
export function watchUpdates(expr, effect) {
let first;
let update = false;
watch(expr, value => {
if (update) {
effect(value);
}
else {
first = value;
update = true;
}
});
return first;
}
function _isStale(dep) {
return dep.c !== dep.s.c;
}
export function lazy(expr) {
let stale = true;
let value;
const deps = [];
const access = signal => {
deps.push({
s: signal,
c: signal.c,
});
};
return Context.bind(() => {
const observer = ACCESS_FRAME;
if (stale || (stale = deps.some(_isStale))) {
const parentTeardownFrame = TEARDOWN_FRAME;
try {
deps.length = 0;
ACCESS_FRAME = access;
TEARDOWN_FRAME = THROW_ON_LEAK;
value = expr();
stale = false;
}
finally {
ACCESS_FRAME = observer;
TEARDOWN_FRAME = parentTeardownFrame;
if (observer) {
deps.forEach(dep => observer(dep.s));
}
}
}
else {
if (observer) {
deps.forEach(dep => observer(dep.s));
}
}
return value;
});
}
function _dispatch(batch) {
while (batch.size > 0) {
batch.forEach(notify => {
batch.delete(notify);
_notify(notify);
});
}
}
export function batch(fn) {
if (BATCH === undefined) {
const batch = new Set();
let value;
try {
BATCH = batch;
value = fn();
_dispatch(batch);
}
finally {
BATCH = undefined;
}
return value;
}
return fn();
}
export function memo(expr) {
const signal = $(undefined);
watch(() => signal.value = get(expr));
return () => signal.value;
}
export function untrack(expr) {
const parent = ACCESS_FRAME;
try {
ACCESS_FRAME = undefined;
return get(expr);
}
finally {
ACCESS_FRAME = parent;
}
}
export function isTracking() {
return ACCESS_FRAME !== undefined;
}
export function trigger(callback) {
const hookFn = Context.bind(() => {
clear();
isolate(_notify, callback);
});
const { c: clear, a: access } = _observer(hookFn);
teardown(clear);
return (expr) => {
clear();
const parent = ACCESS_FRAME;
try {
if (parent === undefined) {
ACCESS_FRAME = access;
}
else {
ACCESS_FRAME = hooks => {
access(hooks);
parent?.(hooks);
};
}
return get(expr);
}
finally {
ACCESS_FRAME = parent;
}
};
}
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