Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@nerdalytics/beacon

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nerdalytics/beacon - npm Package Compare versions

Comparing version
1000.3.3
to
2000.0.0
+2
dist/src/hooks/compose.d.ts
import type { HookFunction, SingleOrArray } from '../types.ts';
export declare function composeHook<Args extends unknown[]>(hook: SingleOrArray<HookFunction<Args>> | undefined): HookFunction<Args> | undefined;
export function composeHook(hook) {
if (hook == null)
return undefined;
if (typeof hook === 'function')
return hook;
if (hook.length === 0)
return undefined;
if (hook.length === 1)
return hook[0];
const fns = hook;
return (...args) => invokeHookArray(fns, args);
}
function invokeHookArray(fns, args) {
for (let i = 0; i < fns.length; i++) {
try {
fns[i]?.(...args);
}
catch { }
}
}
//# sourceMappingURL=compose.js.map
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../src/hooks/compose.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAC1B,IAAmD;IAEnD,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,SAAS,CAAA;IAClC,IAAI,OAAO,IAAI,KAAK,UAAU;QAAE,OAAO,IAAI,CAAA;IAC3C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA;IACrC,MAAM,GAAG,GAAG,IAAI,CAAA;IAChB,OAAO,CAAC,GAAG,IAAU,EAAQ,EAAE,CAAC,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AAC3D,CAAC;AAED,SAAS,eAAe,CAAyB,GAAyB,EAAE,IAAU;IACrF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,CAAC;YACJ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAA;QAClB,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACX,CAAC;AACF,CAAC"}
export type { BatchHooks, DeriveHooks, EffectHooks, HookFunction, SingleOrArray, StateHooks, } from '../types.ts';
export { composeHook } from './compose.ts';
export { composeHook } from "./compose.js";
//# sourceMappingURL=index.js.map
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA"}
export type HookFunction<Args extends unknown[]> = (...args: Args) => void;
export type SingleOrArray<T> = T | T[];
export interface BatchHooks {
onBatchEnd?: SingleOrArray<HookFunction<[
depth: number
]>>;
onBatchError?: SingleOrArray<HookFunction<[
error: Error,
depth: number
]>>;
onBatchStart?: SingleOrArray<HookFunction<[
depth: number
]>>;
}
export interface DeriveHooks<T = unknown> {
onCacheHit?: SingleOrArray<HookFunction<[
value: T,
cacheHit: boolean
]>>;
onCompute?: SingleOrArray<HookFunction<[
previousValue: T | undefined
]>>;
onDependencyChange?: SingleOrArray<HookFunction<[
target: object,
prop: PropertyKey
]>>;
onDispose?: SingleOrArray<HookFunction<[]>>;
onError?: SingleOrArray<HookFunction<[
error: Error
]>>;
}
export interface EffectHooks {
onDependencyAdd?: SingleOrArray<HookFunction<[
target: object,
prop: PropertyKey,
effectName?: string
]>>;
onDependencyChange?: SingleOrArray<HookFunction<[
target: object,
prop: PropertyKey
]>>;
onDispose?: SingleOrArray<HookFunction<[
effectName?: string
]>>;
onError?: SingleOrArray<HookFunction<[
error: Error,
effectName?: string
]>>;
onRun?: SingleOrArray<HookFunction<[
effectName?: string
]>>;
onSchedule?: SingleOrArray<HookFunction<[
effectName?: string
]>>;
}
export interface StateHooks<T = unknown> {
onDelete?: SingleOrArray<HookFunction<[
prop: PropertyKey,
hadProperty: boolean,
target: T
]>>;
onHas?: SingleOrArray<HookFunction<[
prop: PropertyKey,
exists: boolean,
target: T
]>>;
onOwnKeys?: SingleOrArray<HookFunction<[
keys: PropertyKey[],
target: T
]>>;
onRead?: SingleOrArray<HookFunction<[
prop: PropertyKey,
value: unknown,
target: T
]>>;
onWrite?: SingleOrArray<HookFunction<[
prop: PropertyKey,
oldValue: unknown,
newValue: unknown,
target: T
]>>;
}
# src/hooks/ — Beacon Hooks Module
Public API for zero-cost instrumentation. Re-exports types from `../types.ts` and the composition utility.
## Files
| File | Purpose |
|------|---------|
| `index.ts` | Public entry point — re-exports types and `composeHook` |
| `compose.ts` | `composeHook()` utility for composing hook arrays into single functions |
## Exports
**Types:** `BatchHooks`, `DeriveHooks`, `EffectHooks`, `StateHooks`, `HookFunction`, `SingleOrArray`
**Functions:** `composeHook`
## Hook Interfaces
All hooks are optional. Each accepts `SingleOrArray<HookFunction<Args>>` — a single function or array of functions.
| Interface | Hooks | Fired during |
|-----------|-------|-------------|
| `StateHooks<T>` | `onDelete`, `onHas`, `onOwnKeys`, `onRead`, `onWrite` | Proxy trap execution |
| `EffectHooks` | `onDependencyAdd`, `onDependencyChange`, `onDispose`, `onError`, `onRun`, `onSchedule` | Effect lifecycle |
| `DeriveHooks<T>` | `onCacheHit`, `onCompute`, `onDependencyChange`, `onDispose`, `onError` | Derived value computation |
| `BatchHooks` | `onBatchEnd`, `onBatchError`, `onBatchStart` | Batch boundary lifecycle |
## `composeHook<Args>(hook)`
Normalizes `SingleOrArray<HookFunction<Args>> | undefined` into a single function or `undefined`:
- `undefined`/`null` → `undefined`
- Single function → returned as-is
- Empty array → `undefined`
- Single-element array → that element
- Multiple functions → composed function that calls all in order
**Error isolation**: Each hook in a composed array runs inside try-catch. One failing hook does not prevent others from executing. Errors are silently swallowed — hook failures must never break core functionality.
## Integration with Core
Hooks are injected via optional last parameter on each API function:
- `state(initial, hooks?: StateHooks<T>)`
- `effect(fn, name?, hooks?: EffectHooks)`
- `derive(computeFn, hooks?: DeriveHooks<T>)`
- `batch(fn, hooks?: BatchHooks)`
The core `index.ts` imports `composeHook` from this module.
## Design Constraints
- All hook invocations wrapped in try-catch — never propagate to core
- State hooks propagate to nested objects via `HOOKS` symbol
- Frozen/sealed objects store hooks in `frozenHooksCache` WeakMap
- All hooks fire synchronously during their respective operations
- Types defined in `../types.ts`, not in this module
<!--— BEACON-START —>[Hooks Module Index]
|root: ./src/hooks
|IMPORTANT: Hook types live in ../types.ts. This module only re-exports and composes.
|.:{index.ts,compose.ts}
<!--— BEACON-END —>
import type { HookFunction, SingleOrArray } from '../types.ts'
export function composeHook<Args extends unknown[]>(
hook: SingleOrArray<HookFunction<Args>> | undefined
): HookFunction<Args> | undefined {
if (hook == null) return undefined
if (typeof hook === 'function') return hook
if (hook.length === 0) return undefined
if (hook.length === 1) return hook[0]
const fns = hook
return (...args: Args): void => invokeHookArray(fns, args)
}
function invokeHookArray<Args extends unknown[]>(fns: HookFunction<Args>[], args: Args): void {
for (let i = 0; i < fns.length; i++) {
try {
fns[i]?.(...args)
} catch {}
}
}
export type {
BatchHooks,
DeriveHooks,
EffectHooks,
HookFunction,
SingleOrArray,
StateHooks,
} from '../types.ts'
export { composeHook } from './compose.ts'
export type HookFunction<Args extends unknown[]> = (...args: Args) => void
export type SingleOrArray<T> = T | T[]
export interface BatchHooks {
onBatchEnd?: SingleOrArray<
HookFunction<
[
depth: number,
]
>
>
onBatchError?: SingleOrArray<
HookFunction<
[
error: Error,
depth: number,
]
>
>
onBatchStart?: SingleOrArray<
HookFunction<
[
depth: number,
]
>
>
}
export interface DeriveHooks<T = unknown> {
onCacheHit?: SingleOrArray<
HookFunction<
[
value: T,
cacheHit: boolean,
]
>
>
onCompute?: SingleOrArray<
HookFunction<
[
previousValue: T | undefined,
]
>
>
onDependencyChange?: SingleOrArray<
HookFunction<
[
target: object,
prop: PropertyKey,
]
>
>
onDispose?: SingleOrArray<HookFunction<[]>>
onError?: SingleOrArray<
HookFunction<
[
error: Error,
]
>
>
}
export interface EffectHooks {
onDependencyAdd?: SingleOrArray<
HookFunction<
[
target: object,
prop: PropertyKey,
effectName?: string,
]
>
>
onDependencyChange?: SingleOrArray<
HookFunction<
[
target: object,
prop: PropertyKey,
]
>
>
onDispose?: SingleOrArray<
HookFunction<
[
effectName?: string,
]
>
>
onError?: SingleOrArray<
HookFunction<
[
error: Error,
effectName?: string,
]
>
>
onRun?: SingleOrArray<
HookFunction<
[
effectName?: string,
]
>
>
onSchedule?: SingleOrArray<
HookFunction<
[
effectName?: string,
]
>
>
}
export interface StateHooks<T = unknown> {
onDelete?: SingleOrArray<
HookFunction<
[
prop: PropertyKey,
hadProperty: boolean,
target: T,
]
>
>
onHas?: SingleOrArray<
HookFunction<
[
prop: PropertyKey,
exists: boolean,
target: T,
]
>
>
onOwnKeys?: SingleOrArray<
HookFunction<
[
keys: PropertyKey[],
target: T,
]
>
>
onRead?: SingleOrArray<
HookFunction<
[
prop: PropertyKey,
value: unknown,
target: T,
]
>
>
onWrite?: SingleOrArray<
HookFunction<
[
prop: PropertyKey,
oldValue: unknown,
newValue: unknown,
target: T,
]
>
>
}
+12
-17

@@ -1,17 +0,12 @@

type Unsubscribe = () => void;
type ReadOnlyState<T> = () => T;
interface WriteableState<T> {
set(value: T): void;
update(fn: (value: T) => T): void;
}
type State<T> = ReadOnlyState<T> & WriteableState<T>;
declare const createState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>;
declare const createEffect: (fn: () => void) => Unsubscribe;
declare const executeBatch: <T>(fn: () => T) => T;
declare const createDerive: <T>(computeFn: () => T) => ReadOnlyState<T>;
declare const createSelect: <T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean) => ReadOnlyState<R>;
declare const createLens: <T, K>(source: State<T>, accessor: (state: T) => K) => State<K>;
declare const createReadonlyState: <T>(source: State<T>) => ReadOnlyState<T>;
declare const createProtectedState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => [ReadOnlyState<T>, WriteableState<T>];
export type { ReadOnlyState, State, Unsubscribe, WriteableState };
export { createDerive as derive, createEffect as effect, createLens as lens, createProtectedState as protectedState, createReadonlyState as readonlyState, createSelect as select, createState as state, executeBatch as batch, };
import type { BatchHooks, DeriveHooks, EffectHooks, StateHooks } from './types.ts';
export type Unsubscribe = () => void;
export type EffectCallback = () => void;
export type EffectName = string;
export type ComputedValue<T> = {
readonly value: T | undefined | null;
reactive: boolean;
};
export declare function state<T extends object>(initial: T, hooks?: StateHooks<T>): T;
export declare function effect(fn: EffectCallback, name?: EffectName, hooks?: EffectHooks): Unsubscribe;
export declare function batch<T>(fn: () => T, hooks?: BatchHooks): T;
export declare function derive<T>(computeFn: () => T, hooks?: DeriveHooks<T>): ComputedValue<T>;

@@ -1,1 +0,2 @@

let o=null,r=new Set,a=new Set,s=a,u=!1,c=0,n=[],f=new Set,i=new WeakMap,v=new WeakMap,d=new WeakMap,l=new WeakMap,w=new Set(["__proto__","constructor","prototype"]),p=(e,t,r)=>{let a=e.get(t);return a||(a=r(),e.set(t,a)),a},y=()=>{if(!u){u=!0;try{for(;0<s.size;){var e,t=s;s=t===a?r:a;for(e of t)e();t.clear()}}finally{u=!1}}},S=e=>{s.delete(e);var t=v.get(e);if(t){for(var r of t)r.delete(e);t.clear(),v.delete(e)}},g=e=>{S(e),f.delete(e),i.delete(e);var t=d.get(e),t=(t&&(t=l.get(t))&&t.delete(e),d.delete(e),l.get(e));if(t){for(var r of t)g(r);t.clear(),l.delete(e)}},h=(e,a=Object.is)=>{let n=e,f=new Set,l=Symbol(),t=()=>{var e=o;return e&&(f.add(e),p(v,e,()=>new Set).add(f),p(i,e,()=>new Set).add(l)),n};return t.set=e=>{if(!a(n,e)){var t=o;if(t)if(i.get(t)?.has(l)&&!d.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(n=e,0!==f.size){for(var r of f)s.add(r);0!==c||u||y()}}},t.update=e=>{t.set(e(n))},t},b=r=>{let a=()=>{if(!f.has(a)){f.add(a);var e=o;try{S(a),o=a;var t=i.get(a);t?t.clear():i.set(a,new Set),e&&(d.set(a,e),p(l,e,()=>new Set).add(a)),r()}finally{o=e,f.delete(a)}}};var e;return 0===c?a():(o&&(e=o,d.set(a,e),p(l,e,()=>new Set).add(a)),n.push(a)),()=>{g(a)}};var e=e=>{c++;try{return e()}catch(e){throw 1===c&&(s.clear(),n.length=0),e}finally{if(0===--c){if(0<n.length){var t,e=n;n=[];for(t of e)t()}0<s.size&&!u&&y()}}},t=r=>{let a=void 0,n=!1,f=new Set;return b(function(){var e=r();if(!n||!Object.is(a,e)){a=e;for(var t of f)s.add(t);0!==c||u||y()}n=!0}),function(){var e=o;return e&&(f.add(e),p(v,e,()=>new Set).add(f)),n||(a=r(),n=!0),a}},j=(r,a,n=Object.is)=>{let f=!1,l,i,d=new Set;return b(function(){var e=r();if(!f||!Object.is(i,e)){i=e;e=a(e);if(!f||void 0===l||!n(l,e)){l=e,f=!0;for(var t of d)s.add(t);0!==c||u||y()}}}),function(){var e=o;return e&&(d.add(e),p(v,e,()=>new Set).add(d)),f||(i=r(),l=a(i),f=!0),l}};let N=(e,t)=>null!=e?e:void 0===t||Number.isNaN(Number(t))?{}:[],O=(t,e,r,a)=>{if(r>=e.length)return a;if(null==t)return O({},e,r,a);var n=e[r];if(void 0===n)return t;var f=Array.isArray(t),n=f?Number(n):n;if(r===e.length-1){if(f)return(l=[...t])[n]=a,l;let e={...t};return e[n]=a,e}var l=r+1,r=e[l],i=t[n],i=N(i,r);if(f)return(r=[...t])[n]=O(i,e,l,a),r;let d={...t};return d[n]=O(i,e,l,a),d};var k=(e,t)=>{let r=!1;let a=(()=>{let r=[],a=!1,n=new Proxy({},{get:(e,t)=>(a||"string"!=typeof t||(w.has(String(t))?a=!0:r.push(t)),n)});try{t(n)}catch{}return a?[]:r})(),n=h(t(e())),f=n.set;return b(function(){if(!r){r=!0;try{n.set(t(e()))}finally{r=!1}}}),n.set=function(t){if(!r&&0!==a.length){r=!0;try{f(t),e.update(e=>O(e,a,0,t))}finally{r=!1}}},n.update=function(e){n.set(e(n()))},n};let m=e=>()=>e();var M=(e,t=Object.is)=>{let r=h(e,t);return[m(r),{set:e=>r.set(e),update:e=>r.update(e)}]};export{t as derive,b as effect,k as lens,M as protectedState,m as readonlyState,j as select,h as state,e as batch};
import{composeHook as f}from"./hooks/compose.js";const F=Symbol("[[ownKeysRead]]"),H=Symbol("[[beacon_subscribers]]"),K=Symbol("[[beacon_proxy]]"),Y=Symbol("[[beacon_hooks]]");let o=null,a=0,v=!1,A=!1;const h=new Set,L=[],D=[];let p=null,y=null,_=null,S=0,m=!1,O=null;const M=new Map,N=new WeakMap,B=new WeakMap,me=new Set(["push","pop","shift","unshift","splice","sort","reverse"]),G=Symbol("[[cachedMethods]]"),k=new WeakMap,Ce=new WeakMap;function U(e,n,t){let r=k.get(e);r||(r=new Map,k.set(e,r));let i=r.get(n);return i||(i=(...c)=>{const s=t.apply(e,c);return C(e),s},r.set(n,i)),i}function Oe(e,n){try{Object.isExtensible(e)?Object.defineProperty(e,H,{configurable:!0,enumerable:!1,value:n,writable:!1}):B.set(e,n)}catch{B.set(e,n)}}function $(e){const n=e[H];if(n)return n;const t=B.get(e);if(t)return t;const r=new Set;return Oe(e,r),r}function Q(e,n){p=new Set,y=new Map;for(let t=0;t<n;t+=2){const r=e[t],i=e[t+1];p.add(r);let c=y.get(r);c||(c=new Set,y.set(r,c)),c.add(i)}}function Me(e,n,t){return S<e.length&&e[S]===n&&e[S+1]===t}function xe(e,n){if(p&&y){p.add(e);let t=y.get(e);t||(t=new Set,y.set(e,t)),t.add(n),O&&O.push(e,n)}}function X(e,n,t,r){if(r){if(m&&_){if(Me(_,n,t)){S+=2;return}m=!1,Q(_,S)}xe(n,t);return}let i=e.__deps;i||(i=new Set,e.__deps=i),i.add(n);let c=e.__reads;c||(c=new Map,e.__reads=c);let s=c.get(n);s||(s=new Set,c.set(n,s));const u=!s.has(t);s.add(t),u&&l(e.__hooks?.onDependencyAdd,n,t,e.effectName)}function He(e,n,t){if(y)return y.get(n)?.has(t)??!1;if(m&&_){for(let c=0;c<S;c+=2)if(_[c]===n&&_[c+1]===t)return!0;return!1}const r=e.__reads;return r?r.get(n)?.has(t)??!1:!1}function l(e,...n){if(e)try{e(...n)}catch{}}function V(e){return e[H]??B.get(e)}function q(e){const n=h.size;h.add(e),h.size!==n&&l(e.__hooks?.onSchedule,e.effectName)}function Pe(e,n,t){const r=e.__hooks;if(h.has(e)&&!r?.onDependencyChange)return;const i=e.__reads?.get(n);!i?.has(t)&&!i?.has(F)||(q(e),r?.onDependencyChange&&l(r.onDependencyChange,n,t))}function Re(e,n,t){t===void 0?q(e):Pe(e,n,t)}function C(e,n){const t=V(e);if(t?.size){for(const r of t)Re(r,e,n);a===0&&!v&&Z()}}function J(e){if(e.__deps!==void 0)try{e()}catch(n){throw h.clear(),n}}function De(){if(h.size===1){const e=h.values().next().value;h.clear(),J(e);return}for(const e of h)L.push(e);h.clear();for(let e=0;e<L.length;e++){const n=L[e];n&&J(n)}L.length=0}function Z(){if(h.size!==0&&!v){v=!0;try{for(;h.size>0;)De()}finally{v=!1}}}function T(e){h.delete(e),e.__deps=void 0,e.__readList=void 0,e.__reads=void 0,e.__prevDeps=void 0,e.__prevReads=void 0}function ee(e){w(e,e.__deps),T(e)}function je(e,n){ee(e);const t=e.__children;if(t){for(const r of t)n.push(r);t.clear(),e.__children=void 0}e.__parent=void 0,e.__active=!1}function ne(e){ee(e);const n=[],t=e.__children;if(t){for(const i of t)n.push(i);t.clear(),e.__children=void 0}for(;n.length>0;)je(n.pop(),n);const r=e.__parent;r&&r.__children?.delete(e),e.__parent=void 0,e.__active=!1}function ze(e,n){if(e===n)return!0;for(const t of n)if(!e.has(t))return!1;return!0}function ve(e,n,t){const r=e.get(t),i=n.get(t);return!r||!i?!1:r===i?!0:r.size!==i.size?!1:ze(r,i)}function Ae(e,n,t,r){for(const i of n)if(!e.has(i)||!ve(t,r,i))return!1;return!0}function te(e,n,t,r){return e.size!==n.size?!1:e===n&&t===r?!0:Ae(e,n,t,r)}function re(e){return(n,t)=>{const r=Object.hasOwn(n,t),i=delete n[t];return l(e,t,r,n),r&&i&&C(n,t),i}}function Le(e,n){return e===null||typeof e!="object"?e:ce(e,n)}function ie(e){return e===H||e===K||e===Y}function ce(e,n){return N.get(e)??be(e,n)}function W(e,n){o&&(A?X(o,e,n,!0):($(e).add(o),X(o,e,n,!1)))}function Ne(e){const t=e[G];if(t)return t;const r=Object.create(null);try{Object.defineProperty(e,G,{configurable:!0,enumerable:!1,value:r,writable:!1})}catch{return null}return r}function se(e,n,t){if(!Array.isArray(e)||typeof n!="string"||!me.has(n)||typeof t!="function")return;if(!Object.isExtensible(e))return U(e,n,t);const r=Ne(e);return r?Be(e,r,n,t):U(e,n,t)}function Be(e,n,t,r){return n[t]||(n[t]=(...i)=>{const c=r.apply(e,i);return C(e),c}),n[t]}function ue(e,n){return!e&&!n?(t,r)=>{if(ie(r))return t[r];o&&W(t,r);const i=t[r];if(i===null||typeof i!="object")return i;const c=se(t,r,i);return c||ce(i,void 0)}:(t,r)=>{if(ie(r))return t[r];o&&W(t,r);const i=t[r];l(e,r,i,t);const c=se(t,r,i);return c||Le(i,n)}}function oe(e){return(n,t)=>{W(n,t);const r=t in n;return l(e,t,r,n),r}}function fe(e){return n=>{W(n,F);const t=Reflect.ownKeys(n);return l(e,t,n),t}}function le(e,n){n!==void 0&&e.length!==n&&C(e,"length")}function de(e,n){if(!Array.isArray(e)||typeof n!="string")return;const t=Number(n);if(!(Number.isNaN(t)||t<0))return e.length}function We(e,n,t){const r=e[n];if(Object.is(r,t)||(e[n]=t,!e[H]?.size))return!0;let i=M.get(e);return i||(i=new Set,M.set(e,i)),i.add(n),!0}function ge(e,n){if(!o||!He(o,e,n)||o.__parent)return;const r=o.effectName,i=r?`Infinite loop detected: effect "${r}" cannot update property "${String(n)}" it depends on`:"Infinite loop detected: effect cannot update a state it depends on";throw new Error(i)}function ae(e){return e?(n,t,r)=>he(n,t,r,e):(n,t,r)=>{if(a>0&&!o)return We(n,t,r);if(!o){const i=n[t];if(Object.is(i,r))return!0;if(!n[H]?.size)return n[t]=r,!0;const s=de(n,t);return n[t]=r,C(n,t),le(n,s),!0}return he(n,t,r,void 0)}}function he(e,n,t,r){ge(e,n);const i=e[n];if(Object.is(i,t))return!0;const c=de(e,n);return e[n]=t,l(r,n,i,t,e),C(e,n),le(e,c),!0}function _e(e,n){n&&Ce.set(e,n)}function Ke(e,n,t){Object.defineProperty(e,K,{configurable:!0,enumerable:!1,value:n,writable:!1}),t&&Object.defineProperty(e,Y,{configurable:!0,enumerable:!1,value:t,writable:!1})}function ye(e,n,t){try{Object.isExtensible(e)?Ke(e,n,t):_e(e,t)}catch{_e(e,t)}}const Ve={deleteProperty:re(void 0),get:ue(void 0,void 0),has:oe(void 0),ownKeys:fe(void 0),set:ae(void 0)};function be(e,n){if(e==null||typeof e!="object")return e;const r=e?.[K];if(r)return r;const i=e,c=N.get(i);if(c)return c;if(!n){const E=new Proxy(i,Ve);return N.set(i,E),ye(i,E,void 0),E}const s=f(n.onDelete),u=f(n.onHas),b=f(n.onOwnKeys),d=f(n.onRead),x=f(n.onWrite),j={deleteProperty:re(s),get:ue(d,n),has:oe(u),ownKeys:fe(b),set:ae(x)},P=new Proxy(i,j);return N.set(i,P),ye(i,P,n),P}function we(e){const n=e.__children;if(n?.size){e.__children=void 0;for(const t of n)ne(t);n.clear()}}function Ie(e,n,t){for(const r of n)t.has(r)||V(r)?.delete(e)}function Fe(e,n,t){for(const r of t)n.has(r)||$(r).add(e)}function Ye(e,n,t,r){return!e||!n?!1:te(t,e,r,n)}function Ge(e,n,t,r,i){return!n||!t||!r||!i||!Ye(r,i,n,t)?!1:(e.__reads=t,e.__deps=n,!0)}function ke(e,n,t){const r=e.__deps,i=e.__reads;Ge(e,n,t,r,i)||!r||!i||(n&&(Ie(e,n,r),Fe(e,n,r)),e.__prevDeps=r,e.__prevReads=i)}function w(e,n){if(n)for(const t of n)V(t)?.delete(e)}function Ue(e){w(e,e.__prevDeps),w(e,e.__deps),T(e)}function $e(e,n,t,r){!n&&!r&&!t||(e.__hooks=Qe(n,t,r))}function Qe(e,n,t){const r=Object.create(null);return e&&(r.onDependencyAdd=e),n&&(r.onDependencyChange=n),t&&(r.onSchedule=t),r}function Xe(e,n,t){e.__deps=n,e.__reads=t}function g(){_=null,S=0,m=!1,p=null,y=null,O=null}function qe(e){_&&(m&&(m=!1,Q(_,S)),e.__readList=void 0,_=null,S=0)}function Je(e,n,t){if(m&&_&&S===_.length)return g(),!0;if(qe(e),p&&y){if(te(n,p,t,y))return O&&(e.__readList=O),g(),!0;Xe(e,p,y),e.__readList=void 0}return g(),!1}function Ze(e,n,t,r,i,c){const s=!t;if(s||(A=!0),h.delete(e),we(e),o=e,s)e.__reads=new Map,e.__deps=new Set;else{const u=e.__readList;u?(_=u,S=0,m=!0,O=null):(p=new Set,y=new Map,O=[],_=null,m=!1)}l(i,c),n(),!(!s&&t&&r&&Je(e,t,r))&&ke(e,t,r)}function Te(e,n,t,r,i){const c=o,s=A;try{Ze(e,n,e.__prevDeps,e.__prevReads,t,i)}catch(u){throw g(),Ue(e),l(r,u,i),u}finally{o=c,A=s,e.__active=!1}}function en(e){if(!o)return;e.__parent=o;let n=o.__children;n||(n=new Set,o.__children=n),n.add(e)}function nn(e,n,t){const r=f(t?.onRun),i=f(t?.onDispose),c=f(t?.onError),s=f(t?.onDependencyAdd),u=f(t?.onDependencyChange),b=f(t?.onSchedule),d=()=>{d.__active||(d.__active=!0,Te(d,e,r,c,n))};return $e(d,s,u,b),en(d),n&&(d.effectName=n),a===0?d():D.push(d),()=>{l(i,n),ne(d)}}function Se(){for(const[e,n]of M)if(Array.isArray(e))C(e);else for(const t of n)C(e,t);M.clear()}function I(){h.clear(),D.length=0,M.clear()}function tn(){const e=D.length;if(e>0){for(let n=0;n<e;n++){const t=D[n];t&&t()}D.length=0}}function Ee(){try{tn(),h.size>0&&Z()}catch(e){throw I(),e}}function rn(e,n,t){a--,l(n,e,t),a===0&&I()}function ln(e,n){if(!n){a++;let u;try{u=e()}catch(b){throw a--,a===0&&I(),b}return a===1&&M.size>0&&Se(),a--,a===0&&Ee(),u}const t=f(n.onBatchStart),r=f(n.onBatchEnd),i=f(n.onBatchError);a++;const c=a;l(t,c);let s;try{s=e()}catch(u){throw rn(u,i,c),u}return a===1&&M.size>0&&Se(),a--,a===0&&Ee(),l(r,c),s}function cn(e,n,t,r){l(r,n.lastValue);const i=e();Object.is(i,n.lastValue)||(n.lastValue=i,t&&(t.value=i))}function sn(e,n){return n&&o?n.value:e.value}function un(e,n,t,r,i){e&&!n&&!t?r():!e&&n&&t&&i()}function on(e,n,t,r,i,c){try{cn(e,n,t,r)}catch(s){throw l(i,s),s}finally{c()}}function dn(e,n){const t=f(n?.onCompute),r=f(n?.onCacheHit),i=f(n?.onDispose),c=f(n?.onError),s=f(n?.onDependencyChange),u={lastValue:void 0,reactive:!0,value:void 0};let b=null,d=!1,x=null;const j=()=>{if(b)return;x=be(u),b=nn(()=>{!u.reactive||d||(d=!0,on(e,u,x,t,c,()=>{d=!1}))},void 0,s?{onDependencyChange:s}:void 0)},P=()=>{b&&(l(i),b(),b=null,x=null)};return u.reactive&&j(),new Proxy(u,{get(E,z){if(z==="value"){const R=sn(E,x);return l(r,R,!d),R}if(z==="reactive")return E.reactive},set(E,z,R){if(z==="reactive"){const pe=E.reactive;return E.reactive=R,un(R,pe,!!b,j,P),!0}return!1}})}export{ln as batch,dn as derive,nn as effect,be as state};
//# sourceMappingURL=index.min.js.map

@@ -7,12 +7,13 @@ {

"@types/node": "25.5.2",
"esbuild": "0.27.4",
"fast-check": "4.5.3",
"jsr": "0.14.3",
"npm-check-updates": "20.0.0",
"typescript": "6.0.2",
"uglify-js": "3.19.3"
"typescript": "6.0.2"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.0.0"
},
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"typescript": "./src/index.ts",

@@ -26,9 +27,20 @@ "import": {

}
},
"./hooks": {
"typescript": "./src/hooks/index.ts",
"import": {
"types": "./dist/src/hooks/index.d.ts",
"default": "./dist/src/hooks/index.js"
}
}
},
"files": [
"dist/src/hooks/",
"dist/src/index.d.ts",
"dist/src/index.min.js",
"dist/src/index.d.ts",
"dist/src/types.d.ts",
"LICENSE",
"src/hooks/",
"src/index.ts",
"LICENSE"
"src/types.ts"
],

@@ -55,7 +67,6 @@ "keywords": [

"benchmark": "node --expose-gc scripts/benchmark.ts -R 3",
"build": "npm run build:lts",
"build:lts": "tsc -p tsconfig.lts.json",
"check": "npx @biomejs/biome check",
"check:fix": "npx @biomejs/biome format --fix",
"check": "npx @biomejs/biome check --enforce-assist=true",
"check:fix": "npx @biomejs/biome check --enforce-assist=true --write",
"format": "npx @biomejs/biome format --write",

@@ -65,9 +76,8 @@ "lint": "npx @biomejs/biome lint",

"lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe",
"postbuild:lts": "npx uglify-js --compress --mangle --module --toplevel --v8 --warn --source-map \"content='dist/src/index.js.map'\" --output dist/src/index.min.js dist/src/index.js",
"postbuild:lts": "npx esbuild dist/src/index.js --minify --sourcemap --sources-content=false --outfile=dist/src/index.min.js --format=esm --platform=node",
"prebuild:lts": "rm -rf dist/",
"prepublishOnly": "npm run build:lts",
"pretest:lts": "node scripts/run-lts-tests.js",
"pretest:lts:22": "node scripts/run-lts-tests.js",
"test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts",
"test:coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts",
"test:lts:20": "node --test dist/tests/**.js",
"test:lts:22": "node --test --test-skip-pattern=\"COMPONENT NAME\" dist/tests/**/*.js",

@@ -79,3 +89,3 @@ "update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange"

"types": "dist/src/index.d.ts",
"version": "1000.3.3"
"version": "2000.0.0"
}
+32
-10

@@ -5,14 +5,32 @@ # Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo-v2.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/>

[![license:mit](https://flat.badgen.net/static/license/MIT/blue)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
[![pm:yarn](https://img.shields.io/badge/yarn-2C8EBB?style=flat-square&logo=yarn&logoColor=white)](https://yarnpkg.com/)
[![pm:pnpm](https://img.shields.io/badge/pnpm-F69220?style=flat-square&logo=pnpm&logoColor=white)](https://pnpm.io/)
[![pm:jsr](https://img.shields.io/badge/jsr-F7DF1E?style=flat-square&logo=jsr&logoColor=black)](https://jsr.io/@nerdalytics/beacon)
[![pm:vlt](https://img.shields.io/badge/vlt-1A1A2E?style=flat-square&logoColor=white)](https://vlt.sh/)
[![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.3.3)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.3)
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/2000.0.0)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/2000.0.0)
[![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![tech:nodejs](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
[![linter:biome](https://img.shields.io/badge/biome-60a5fa?style=for-the-badge&logo=biome&logoColor=white)](https://biomejs.dev/)
[![linter:biome](https://img.shields.io/badge/Biome-60a5fa?style=for-the-badge&logo=biome&logoColor=white)](https://biomejs.dev/)
[![license:mit](https://img.shields.io/badge/MIT-blue?style=for-the-badge)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
Tracks which properties each effect reads and re-runs only when those properties change. Zero dependencies, TypeScript-first.
## Installation
```
```bash
npm install @nerdalytics/beacon --save-exact
# or
yarn add @nerdalytics/beacon
# or
pnpm add @nerdalytics/beacon
# or
bun add @nerdalytics/beacon
# or
deno install npm:@nerdalytics/beacon
# or
npx jsr add @nerdalytics/beacon
# or
vlt install @nerdalytics/beacon
```

@@ -25,11 +43,15 @@

const count = state(0);
const doubled = derive(() => count() * 2);
const signal = state({ count: 0 });
const doubled = derive(() => signal.count * 2);
effect(() => {
console.log(`Count: ${count()}, Doubled: ${doubled()}`);
const dispose = effect(() => {
console.log(`Count: ${signal.count}, Doubled: ${doubled.value}`);
});
// => "Count: 0, Doubled: 0"
count.set(5);
signal.count = 5;
// => "Count: 5, Doubled: 10"
dispose();
doubled.reactive = false;
```

@@ -36,0 +58,0 @@

+1271
-345

@@ -1,482 +0,1408 @@

// Core types for reactive primitives
type Subscriber = () => void
type Unsubscribe = () => void
type ReadOnlyState<T> = () => T
interface WriteableState<T> {
set(value: T): void
update(fn: (value: T) => T): void
// Beacon - reactive state management system
import { composeHook } from './hooks/compose.ts'
import type { BatchHooks, DeriveHooks, EffectHooks, HookFunction, StateHooks } from './types.ts'
// Type definitions
export type Unsubscribe = () => void
export type EffectCallback = () => void
export type EffectName = string
export type ComputedValue<T> = {
readonly value: T | undefined | null
reactive: boolean
}
type State<T> = ReadOnlyState<T> & WriteableState<T>
type ProxyTarget = Record<PropertyKey, unknown>
// Module-level reactive state
let currentSubscriber: Subscriber | null = null
const flushing: Set<Subscriber> = new Set<Subscriber>()
const queued: Set<Subscriber> = new Set<Subscriber>()
let pendingSubscribers: Set<Subscriber> = queued
// Symbol definitions for internal tracking
const OWN_KEYS_SYMBOL: unique symbol = Symbol('[[ownKeysRead]]')
const SUBSCRIBERS: unique symbol = Symbol('[[beacon_subscribers]]')
const PROXY: unique symbol = Symbol('[[beacon_proxy]]')
const HOOKS: unique symbol = Symbol('[[beacon_hooks]]')
// Effect tracking state
let currentEffect: EffectFunction | null = null
let batchDepth = 0
let isNotifying = false
let batchDepth = 0
let deferredEffectCreations: Subscriber[] = []
const activeSubscribers: Set<Subscriber> = new Set<Subscriber>()
const stateTracking: WeakMap<Subscriber, Set<symbol>> = new WeakMap<Subscriber, Set<symbol>>()
const subscriberDependencies: WeakMap<Subscriber, Set<Set<Subscriber>>> = new WeakMap<
Subscriber,
Set<Set<Subscriber>>
>()
const parentSubscriber: WeakMap<Subscriber, Subscriber> = new WeakMap<Subscriber, Subscriber>()
const childSubscribers: WeakMap<Subscriber, Set<Subscriber>> = new WeakMap<Subscriber, Set<Subscriber>>()
const DANGEROUS_KEYS: ReadonlySet<string> = new Set([
'__proto__',
'constructor',
'prototype',
let isTrackingOnly = false
const pendingEffects: Set<EffectFunction> = new Set<EffectFunction>()
const effectQueue: EffectFunction[] = []
const deferredEffectCreations: EffectFunction[] = []
let rerunTempDeps: Set<object> | null = null
let rerunTempReads: Map<object, Set<PropertyKey>> | null = null
let rerunReadList: (object | PropertyKey)[] | null = null
let rerunReadIndex = 0
let rerunStable = false
let buildingReadList: (object | PropertyKey)[] | null = null
const dirtyTargets: Map<object, Set<PropertyKey>> = new Map<object, Set<PropertyKey>>()
// Proxy caching
const proxyCache: WeakMap<object, object> = new WeakMap<object, object>()
const proxyCacheSubs: WeakMap<object, Set<EffectFunction>> = new WeakMap<object, Set<EffectFunction>>()
const MUTATING_ARRAY_METHODS: Set<string> = new Set([
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
])
const getOrCreate = <K extends object, V>(map: WeakMap<K, V>, key: K, factory: () => V): V => {
let value = map.get(key)
if (!value) {
value = factory()
map.set(key, value)
// Symbol for cached methods
const CACHED_METHODS: unique symbol = Symbol('[[cachedMethods]]')
// Function type for cached methods
type CachedMethod = (...args: unknown[]) => unknown
const frozenMethodCache: WeakMap<object, Map<PropertyKey, CachedMethod>> = new WeakMap()
const frozenHooksCache: WeakMap<object, StateHooks> = new WeakMap()
// Effect function type
type EffectFunction = {
(): void
__active?: boolean
__children?: Set<EffectFunction> | undefined
__deps?: Set<object> | undefined
__hooks?: {
onDependencyAdd?: HookFunction<
[
object,
PropertyKey,
string | undefined,
]
>
onDependencyChange?: HookFunction<
[
object,
PropertyKey,
]
>
onSchedule?: HookFunction<
[
string | undefined,
]
>
}
return value
__parent?: EffectFunction | undefined
__prevDeps?: Set<object> | undefined
__prevReads?: Map<object, Set<PropertyKey>> | undefined
__readList?: (object | PropertyKey)[] | undefined
__reads?: Map<object, Set<PropertyKey>> | undefined
effectName?: string
}
const notifySubscribers = (): void => {
if (isNotifying) {
return
// Helper types for objects with internal symbols
type SubscribersObject = ProxyTarget & {
[SUBSCRIBERS]?: Set<EffectFunction>
}
type ProxyObject = ProxyTarget & {
[PROXY]?: object
}
type CachedMethodsObject = ProxyTarget & {
[CACHED_METHODS]?: Record<PropertyKey, CachedMethod>
}
function getCachedMethodFromWeakMap(target: object, prop: PropertyKey, originalMethod: CachedMethod): CachedMethod {
let cache = frozenMethodCache.get(target)
if (!cache) {
cache = new Map<PropertyKey, CachedMethod>()
frozenMethodCache.set(target, cache)
}
isNotifying = true
let wrapped = cache.get(prop)
if (!wrapped) {
wrapped = (...args: unknown[]): unknown => {
const result = originalMethod.apply(target, args)
scheduleSubscribersForTarget(target)
return result
}
cache.set(prop, wrapped)
}
return wrapped
}
function storeSubscriberSet(target: object, subscriberSet: Set<EffectFunction>): void {
try {
while (pendingSubscribers.size > 0) {
const subscribers = pendingSubscribers
pendingSubscribers = subscribers === queued ? flushing : queued
for (const effect of subscribers) {
effect()
}
subscribers.clear()
if (Object.isExtensible(target)) {
Object.defineProperty(target, SUBSCRIBERS, {
configurable: true,
enumerable: false,
value: subscriberSet,
writable: false,
})
} else {
proxyCacheSubs.set(target, subscriberSet)
}
} finally {
isNotifying = false
} catch {
proxyCacheSubs.set(target, subscriberSet)
}
}
const cleanupEffect = (effect: Subscriber): void => {
pendingSubscribers.delete(effect)
function getSubscribers(target: object): Set<EffectFunction> {
const subs = (target as SubscribersObject)[SUBSCRIBERS]
if (subs) return subs
const deps = subscriberDependencies.get(effect)
if (deps) {
for (const subscribers of deps) {
subscribers.delete(effect)
const fallbackSubs = proxyCacheSubs.get(target)
if (fallbackSubs) return fallbackSubs
const subscriberSet = new Set<EffectFunction>()
storeSubscriberSet(target, subscriberSet)
return subscriberSet
}
function replayReadListToTempCollections(list: (object | PropertyKey)[], endIndex: number): void {
rerunTempDeps = new Set<object>()
rerunTempReads = new Map<object, Set<PropertyKey>>()
for (let j = 0; j < endIndex; j += 2) {
const t = list[j] as object
const p = list[j + 1] as PropertyKey
rerunTempDeps.add(t)
let s = rerunTempReads.get(t)
if (!s) {
s = new Set<PropertyKey>()
rerunTempReads.set(t, s)
}
deps.clear()
subscriberDependencies.delete(effect)
s.add(p)
}
}
const disposeEffect = (effect: Subscriber): void => {
cleanupEffect(effect)
activeSubscribers.delete(effect)
stateTracking.delete(effect)
function readListMatchesRead(list: (object | PropertyKey)[], target: object, prop: PropertyKey): boolean {
return rerunReadIndex < list.length && list[rerunReadIndex] === target && list[rerunReadIndex + 1] === prop
}
const parent = parentSubscriber.get(effect)
if (parent) {
const siblings = childSubscribers.get(parent)
if (siblings) {
siblings.delete(effect)
function recordSilentRead(target: object, prop: PropertyKey): void {
if (rerunTempDeps && rerunTempReads) {
rerunTempDeps.add(target)
let set = rerunTempReads.get(target)
if (!set) {
set = new Set<PropertyKey>()
rerunTempReads.set(target, set)
}
set.add(prop)
if (buildingReadList) buildingReadList.push(target, prop)
}
parentSubscriber.delete(effect)
}
const children = childSubscribers.get(effect)
if (children) {
for (const child of children) {
disposeEffect(child)
function recordEffectRead(eff: EffectFunction, target: object, prop: PropertyKey, silent: boolean): void {
if (silent) {
if (rerunStable && rerunReadList) {
if (readListMatchesRead(rerunReadList, target, prop)) {
rerunReadIndex += 2
return
}
rerunStable = false
replayReadListToTempCollections(rerunReadList, rerunReadIndex)
}
children.clear()
childSubscribers.delete(effect)
recordSilentRead(target, prop)
return
}
}
const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
let value = initialValue
const subscribers = new Set<Subscriber>()
const stateId = Symbol()
let deps = eff.__deps
if (!deps) {
deps = new Set<object>()
eff.__deps = deps
}
deps.add(target)
const get = (): T => {
const currentEffect = currentSubscriber
if (currentEffect) {
subscribers.add(currentEffect)
let map = eff.__reads
if (!map) {
map = new Map<object, Set<PropertyKey>>()
eff.__reads = map
}
let set = map.get(target)
if (!set) {
set = new Set<PropertyKey>()
map.set(target, set)
}
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
const isNew = !set.has(prop)
set.add(prop)
if (isNew) {
callHookSafe(eff.__hooks?.onDependencyAdd, target, prop, eff.effectName)
}
}
getOrCreate(stateTracking, currentEffect, () => new Set()).add(stateId)
function didEffectReadProp(effect: EffectFunction, target: object, prop: PropertyKey): boolean {
if (rerunTempReads) {
const set = rerunTempReads.get(target)
return set?.has(prop) ?? false
}
if (rerunStable && rerunReadList) {
for (let j = 0; j < rerunReadIndex; j += 2) {
if (rerunReadList[j] === target && rerunReadList[j + 1] === prop) return true
}
return value
return false
}
const map = effect.__reads
if (!map) return false
const set = map.get(target)
return set?.has(prop) ?? false
}
get.set = (newValue: T): void => {
if (equalityFn(value, newValue)) {
return
}
function callHookSafe<Args extends unknown[]>(hook: HookFunction<Args> | undefined, ...args: Args): void {
if (!hook) return
try {
hook(...args)
} catch {}
}
const effect = currentSubscriber
if (effect) {
const states = stateTracking.get(effect)
if (states?.has(stateId) && !parentSubscriber.get(effect)) {
throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
}
}
function findSubscribers(target: object): Set<EffectFunction> | undefined {
const t = target as SubscribersObject
return t[SUBSCRIBERS] ?? proxyCacheSubs.get(target)
}
value = newValue
function addPendingEffect(subscriber: EffectFunction): void {
const prevSize = pendingEffects.size
pendingEffects.add(subscriber)
if (pendingEffects.size === prevSize) return
callHookSafe(subscriber.__hooks?.onSchedule, subscriber.effectName)
}
if (subscribers.size === 0) {
return
}
function scheduleSubscriberWithProp(subscriber: EffectFunction, target: object, prop: PropertyKey): void {
const hooks = subscriber.__hooks
if (pendingEffects.has(subscriber) && !hooks?.onDependencyChange) return
const set = subscriber.__reads?.get(target)
if (!set?.has(prop) && !set?.has(OWN_KEYS_SYMBOL)) return
addPendingEffect(subscriber)
if (hooks?.onDependencyChange) callHookSafe(hooks.onDependencyChange, target, prop)
}
for (const sub of subscribers) {
pendingSubscribers.add(sub)
}
function scheduleSubscriber(subscriber: EffectFunction, target: object, prop: PropertyKey | undefined): void {
if (prop === undefined) {
addPendingEffect(subscriber)
} else {
scheduleSubscriberWithProp(subscriber, target, prop)
}
}
if (batchDepth === 0 && !isNotifying) {
notifySubscribers()
function scheduleSubscribersForTarget(target: object, prop?: PropertyKey): void {
const subs = findSubscribers(target)
if (!subs?.size) return
for (const subscriber of subs) {
scheduleSubscriber(subscriber, target, prop)
}
if (batchDepth === 0 && !isNotifying) flushEffects()
}
// Flush all pending effects
function runEffectIfActive(effect: EffectFunction): void {
if (effect.__deps !== undefined) {
try {
effect()
} catch (err) {
pendingEffects.clear()
throw err
}
}
}
get.update = (fn: (currentValue: T) => T): void => {
get.set(fn(value))
function runPendingEffectBatch(): void {
if (pendingEffects.size === 1) {
const eff = pendingEffects.values().next().value as EffectFunction
pendingEffects.clear()
runEffectIfActive(eff)
return
}
for (const eff of pendingEffects) effectQueue.push(eff)
pendingEffects.clear()
return get as State<T>
for (let i = 0; i < effectQueue.length; i++) {
const eff = effectQueue[i]
if (eff) runEffectIfActive(eff)
}
effectQueue.length = 0
}
const createEffect = (fn: () => void): Unsubscribe => {
const runEffect = (): void => {
if (activeSubscribers.has(runEffect)) {
return
function flushEffects(): void {
if (pendingEffects.size === 0) return
if (isNotifying) return
isNotifying = true
try {
while (pendingEffects.size > 0) {
runPendingEffectBatch()
}
} finally {
isNotifying = false
}
}
activeSubscribers.add(runEffect)
const parentEffect = currentSubscriber
function resetEffectTracking(eff: EffectFunction): void {
pendingEffects.delete(eff)
eff.__deps = undefined
eff.__readList = undefined
eff.__reads = undefined
eff.__prevDeps = undefined
eff.__prevReads = undefined
}
try {
cleanupEffect(runEffect)
function cleanupEffect(effect: EffectFunction): void {
removeEffectFromSubscribers(effect, effect.__deps)
resetEffectTracking(effect)
}
currentSubscriber = runEffect
const existingStates = stateTracking.get(runEffect)
if (existingStates) {
existingStates.clear()
} else {
stateTracking.set(runEffect, new Set())
}
function cleanupChildEffect(child: EffectFunction, toCleanup: EffectFunction[]): void {
cleanupEffect(child)
if (parentEffect) {
parentSubscriber.set(runEffect, parentEffect)
getOrCreate(childSubscribers, parentEffect, () => new Set()).add(runEffect)
}
const grandchildren = child.__children
if (grandchildren) {
for (const gc of grandchildren) toCleanup.push(gc)
grandchildren.clear()
child.__children = undefined
}
fn()
} finally {
currentSubscriber = parentEffect
activeSubscribers.delete(runEffect)
}
child.__parent = undefined
child.__active = false
}
function cleanupEffectCompletely(effect: EffectFunction): void {
cleanupEffect(effect)
const toCleanup: EffectFunction[] = []
const children = effect.__children
if (children) {
for (const c of children) toCleanup.push(c)
children.clear()
effect.__children = undefined
}
if (batchDepth === 0) {
runEffect()
} else {
if (currentSubscriber) {
const parent = currentSubscriber
parentSubscriber.set(runEffect, parent)
getOrCreate(childSubscribers, parent, () => new Set()).add(runEffect)
}
while (toCleanup.length > 0) {
cleanupChildEffect(toCleanup.pop() as EffectFunction, toCleanup)
}
deferredEffectCreations.push(runEffect)
// Clean up parent relationship
const parent = effect.__parent
if (parent) {
const pchildren = parent.__children
pchildren?.delete(effect)
}
return (): void => {
disposeEffect(runEffect)
// Final cleanup
effect.__parent = undefined
effect.__active = false
}
function setContainsAll(superset: Set<PropertyKey>, subset: Set<PropertyKey>): boolean {
if (superset === subset) return true
for (const prop of subset) {
if (!superset.has(prop)) return false
}
return true
}
const executeBatch = <T>(fn: () => T): T => {
batchDepth++
function propsMatch(
prevReads: Map<object, Set<PropertyKey>>,
newReads: Map<object, Set<PropertyKey>>,
target: object
): boolean {
const prevProps = prevReads.get(target)
const newProps = newReads.get(target)
if (!prevProps || !newProps) return false
if (prevProps === newProps) return true
if (prevProps.size !== newProps.size) return false
return setContainsAll(prevProps, newProps)
}
function allDepsPropsMatch(
prevDeps: Set<object>,
newDeps: Set<object>,
prevReads: Map<object, Set<PropertyKey>>,
newReads: Map<object, Set<PropertyKey>>
): boolean {
for (const target of newDeps) {
if (!prevDeps.has(target) || !propsMatch(prevReads, newReads, target)) return false
}
return true
}
function depsMatch(
prevDeps: Set<object>,
newDeps: Set<object>,
prevReads: Map<object, Set<PropertyKey>>,
newReads: Map<object, Set<PropertyKey>>
): boolean {
if (prevDeps.size !== newDeps.size) return false
if (prevDeps === newDeps && prevReads === newReads) return true
return allDepsPropsMatch(prevDeps, newDeps, prevReads, newReads)
}
// Proxy handler functions
function createDeleteHandler<T>(
onDelete:
| HookFunction<
[
PropertyKey,
boolean,
T,
]
>
| undefined
): NonNullable<ProxyHandler<ProxyTarget>['deleteProperty']> {
return (rawTarget: ProxyTarget, prop: PropertyKey): boolean => {
const had = Object.hasOwn(rawTarget, prop)
const ok = delete rawTarget[prop]
callHookSafe(onDelete, prop, had, rawTarget as T)
if (had && ok) scheduleSubscribersForTarget(rawTarget, prop)
return ok
}
}
function resolveValue(value: unknown, hooks: StateHooks<object> | undefined): unknown {
if (value === null || typeof value !== 'object') return value
return wrapNestedObject(value as object, hooks)
}
function isInternalSymbol(prop: PropertyKey): boolean {
return prop === SUBSCRIBERS || prop === PROXY || prop === HOOKS
}
function wrapNestedObject(value: object, hooks: StateHooks<object> | undefined): object {
return proxyCache.get(value) ?? state(value, hooks)
}
function trackDependency(rawTarget: ProxyTarget, prop: PropertyKey): void {
if (!currentEffect) return
if (isTrackingOnly) {
recordEffectRead(currentEffect, rawTarget, prop, true)
} else {
const subs = getSubscribers(rawTarget)
subs.add(currentEffect)
recordEffectRead(currentEffect, rawTarget, prop, false)
}
}
function getOrCreateMethodCache(rawTarget: ProxyTarget): Record<PropertyKey, CachedMethod> | null {
const rawTargetWithCache = rawTarget as CachedMethodsObject
const existing = rawTargetWithCache[CACHED_METHODS]
if (existing) return existing
const cache = Object.create(null) as Record<PropertyKey, CachedMethod>
try {
return fn()
} catch (error: unknown) {
if (batchDepth === 1) {
pendingSubscribers.clear()
deferredEffectCreations.length = 0
}
throw error
} finally {
batchDepth--
Object.defineProperty(rawTarget, CACHED_METHODS, {
configurable: true,
enumerable: false,
value: cache,
writable: false,
})
} catch {
return null
}
return cache
}
if (batchDepth === 0) {
if (deferredEffectCreations.length > 0) {
const effectsToRun = deferredEffectCreations
deferredEffectCreations = []
for (const effect of effectsToRun) {
effect()
}
}
function getWrappedArrayMethod(rawTarget: ProxyTarget, prop: PropertyKey, value: unknown): CachedMethod | undefined {
if (
!Array.isArray(rawTarget) ||
typeof prop !== 'string' ||
!MUTATING_ARRAY_METHODS.has(prop) ||
typeof value !== 'function'
)
return undefined
if (pendingSubscribers.size > 0 && !isNotifying) {
notifySubscribers()
}
if (!Object.isExtensible(rawTarget)) {
return getCachedMethodFromWeakMap(rawTarget, prop, value as CachedMethod)
}
const cache = getOrCreateMethodCache(rawTarget)
if (!cache) return getCachedMethodFromWeakMap(rawTarget, prop, value as CachedMethod)
return createCachedArrayMethod(rawTarget, cache, prop, value as CachedMethod)
}
function createCachedArrayMethod(
rawTarget: ProxyTarget,
cache: Record<PropertyKey, CachedMethod>,
prop: PropertyKey,
value: CachedMethod
): CachedMethod {
if (!cache[prop]) {
cache[prop] = (...args: unknown[]): unknown => {
const result = value.apply(rawTarget, args)
scheduleSubscribersForTarget(rawTarget)
return result
}
}
return cache[prop] as CachedMethod
}
const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
let cachedValue: T = undefined as unknown as T
let initialized = false
const subscribers = new Set<Subscriber>()
function createGetHandler<T>(
onRead:
| HookFunction<
[
PropertyKey,
unknown,
T,
]
>
| undefined,
hooks: StateHooks<T> | undefined
): NonNullable<ProxyHandler<ProxyTarget>['get']> {
if (!onRead && !hooks) {
return (rawTarget: ProxyTarget, prop: PropertyKey): unknown => {
if (isInternalSymbol(prop)) return rawTarget[prop]
if (currentEffect) trackDependency(rawTarget, prop)
const value = rawTarget[prop]
if (value === null || typeof value !== 'object') return value
const wrapped = getWrappedArrayMethod(rawTarget, prop, value)
if (wrapped) return wrapped
return wrapNestedObject(value as object, undefined)
}
}
createEffect(function deriveEffect(): void {
const newValue = computeFn()
return (rawTarget: ProxyTarget, prop: PropertyKey): unknown => {
if (isInternalSymbol(prop)) return rawTarget[prop]
if (currentEffect) trackDependency(rawTarget, prop)
const value = rawTarget[prop]
if (!(initialized && Object.is(cachedValue, newValue))) {
cachedValue = newValue
callHookSafe(onRead, prop, value, rawTarget as T)
for (const sub of subscribers) {
pendingSubscribers.add(sub)
const wrapped = getWrappedArrayMethod(rawTarget, prop, value)
if (wrapped) return wrapped
return resolveValue(value, hooks as StateHooks<object> | undefined)
}
}
function createHasHandler<T>(
onHas:
| HookFunction<
[
PropertyKey,
boolean,
T,
]
>
| undefined
): NonNullable<ProxyHandler<ProxyTarget>['has']> {
return (rawTarget: ProxyTarget, prop: PropertyKey): boolean => {
trackDependency(rawTarget, prop)
const exists = prop in rawTarget
callHookSafe(onHas, prop, exists, rawTarget as T)
return exists
}
}
function createOwnKeysHandler<T>(
onOwnKeys:
| HookFunction<
[
PropertyKey[],
T,
]
>
| undefined
): NonNullable<ProxyHandler<ProxyTarget>['ownKeys']> {
return (rawTarget: ProxyTarget): (string | symbol)[] => {
trackDependency(rawTarget, OWN_KEYS_SYMBOL)
const keys = Reflect.ownKeys(rawTarget) as (string | symbol)[]
callHookSafe(onOwnKeys, keys, rawTarget as T)
return keys
}
}
function notifyLengthChangeIfNeeded(rawTarget: ProxyTarget, oldLength: number | undefined): void {
if (oldLength !== undefined && (rawTarget as unknown as unknown[]).length !== oldLength) {
scheduleSubscribersForTarget(rawTarget, 'length')
}
}
function getArrayLengthBeforeMutation(rawTarget: ProxyTarget, prop: PropertyKey): number | undefined {
if (!Array.isArray(rawTarget) || typeof prop !== 'string') return undefined
const index = Number(prop)
if (Number.isNaN(index) || index < 0) return undefined
return (rawTarget as unknown as unknown[]).length
}
function handleBatchFastPath(rawTarget: ProxyTarget, prop: PropertyKey, value: unknown): boolean {
const oldValue = rawTarget[prop]
if (Object.is(oldValue, value)) return true
rawTarget[prop] = value
if (!(rawTarget as SubscribersObject)[SUBSCRIBERS]?.size) return true
let props = dirtyTargets.get(rawTarget)
if (!props) {
props = new Set<PropertyKey>()
dirtyTargets.set(rawTarget, props)
}
props.add(prop)
return true
}
function checkInfiniteLoop(rawTarget: ProxyTarget, prop: PropertyKey): void {
if (!currentEffect || !didEffectReadProp(currentEffect, rawTarget, prop)) return
const parent = currentEffect.__parent
if (parent) return
const effectName = currentEffect.effectName
const errorMsg = effectName
? `Infinite loop detected: effect "${effectName}" cannot update property "${String(prop)}" it depends on`
: 'Infinite loop detected: effect cannot update a state it depends on'
throw new Error(errorMsg)
}
function createSetHandler<T>(
onWrite:
| HookFunction<
[
PropertyKey,
unknown,
unknown,
T,
]
>
| undefined
): NonNullable<ProxyHandler<ProxyTarget>['set']> {
if (!onWrite) {
return (rawTarget: ProxyTarget, prop: PropertyKey, value: unknown): boolean => {
if (batchDepth > 0 && !currentEffect) {
return handleBatchFastPath(rawTarget, prop, value)
}
if (batchDepth === 0 && !isNotifying) {
notifySubscribers()
if (!currentEffect) {
const oldValue = rawTarget[prop]
if (Object.is(oldValue, value)) return true
const subs = (rawTarget as SubscribersObject)[SUBSCRIBERS]
if (!subs?.size) {
rawTarget[prop] = value
return true
}
const oldLength = getArrayLengthBeforeMutation(rawTarget, prop)
rawTarget[prop] = value
scheduleSubscribersForTarget(rawTarget, prop)
notifyLengthChangeIfNeeded(rawTarget, oldLength)
return true
}
return performWrite(rawTarget, prop, value, undefined)
}
}
initialized = true
return (rawTarget: ProxyTarget, prop: PropertyKey, value: unknown): boolean => {
return performWrite(rawTarget, prop, value, onWrite)
}
}
function performWrite<T>(
rawTarget: ProxyTarget,
prop: PropertyKey,
value: unknown,
onWrite:
| HookFunction<
[
PropertyKey,
unknown,
unknown,
T,
]
>
| undefined
): boolean {
checkInfiniteLoop(rawTarget, prop)
const oldValue = rawTarget[prop]
if (Object.is(oldValue, value)) return true
const oldLength = getArrayLengthBeforeMutation(rawTarget, prop)
rawTarget[prop] = value
callHookSafe(onWrite, prop, oldValue, value, rawTarget as T)
scheduleSubscribersForTarget(rawTarget, prop)
notifyLengthChangeIfNeeded(rawTarget, oldLength)
return true
}
function storeHooksForFrozenTarget(target: ProxyTarget, hooks: StateHooks | undefined): void {
if (hooks) {
frozenHooksCache.set(target, hooks)
}
}
function definePropertyOnTarget(target: ProxyTarget, proxy: unknown, hooks: StateHooks | undefined): void {
Object.defineProperty(target, PROXY, {
configurable: true,
enumerable: false,
value: proxy,
writable: false,
})
if (hooks) {
Object.defineProperty(target, HOOKS, {
configurable: true,
enumerable: false,
value: hooks,
writable: false,
})
}
}
return function deriveGetter(): T {
const currentEffect = currentSubscriber
if (currentEffect) {
subscribers.add(currentEffect)
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
function defineProxyProperties(target: ProxyTarget, proxy: unknown, hooks: StateHooks | undefined): void {
try {
if (Object.isExtensible(target)) {
definePropertyOnTarget(target, proxy, hooks)
} else {
storeHooksForFrozenTarget(target, hooks)
}
if (!initialized) {
cachedValue = computeFn()
initialized = true
}
return cachedValue
} catch {
storeHooksForFrozenTarget(target, hooks)
}
}
const createSelect = <T, R>(
source: ReadOnlyState<T>,
selectorFn: (state: T) => R,
equalityFn: (a: R, b: R) => boolean = Object.is
): ReadOnlyState<R> => {
let initialized = false
let lastSelectedValue: R | undefined
let lastSourceValue: T | undefined
const subscribers = new Set<Subscriber>()
const HOOKLESS_HANDLER: ProxyHandler<ProxyTarget> = {
deleteProperty: createDeleteHandler(undefined),
get: createGetHandler(undefined, undefined),
has: createHasHandler(undefined),
ownKeys: createOwnKeysHandler(undefined),
set: createSetHandler(undefined),
}
createEffect(function selectEffect(): void {
const sourceValue = source()
export function state<T extends object>(initial: T, hooks?: StateHooks<T>): T {
if (initial === null || initial === undefined || typeof initial !== 'object') return initial
if (initialized && Object.is(lastSourceValue, sourceValue)) {
return
}
const initialWithProxy = initial as ProxyObject
const existingProxy = initialWithProxy?.[PROXY]
if (existingProxy) return existingProxy as T
lastSourceValue = sourceValue
const newSelectedValue = selectorFn(sourceValue)
const target = initial as ProxyTarget
const cached = proxyCache.get(target)
if (cached) return cached as T
if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
return
}
if (!hooks) {
const proxy = new Proxy(target, HOOKLESS_HANDLER) as T
proxyCache.set(target, proxy)
defineProxyProperties(target, proxy, undefined)
return proxy
}
lastSelectedValue = newSelectedValue
initialized = true
const onDelete = composeHook(hooks.onDelete)
const onHas = composeHook(hooks.onHas)
const onOwnKeys = composeHook(hooks.onOwnKeys)
const onRead = composeHook(hooks.onRead)
const onWrite = composeHook(hooks.onWrite)
for (const sub of subscribers) {
pendingSubscribers.add(sub)
}
if (batchDepth === 0 && !isNotifying) {
notifySubscribers()
}
})
const handler: ProxyHandler<ProxyTarget> = {
deleteProperty: createDeleteHandler(onDelete),
get: createGetHandler(onRead, hooks),
has: createHasHandler(onHas),
ownKeys: createOwnKeysHandler(onOwnKeys),
set: createSetHandler(onWrite),
} as ProxyHandler<ProxyTarget>
return function selectGetter(): R {
const currentEffect = currentSubscriber
if (currentEffect) {
subscribers.add(currentEffect)
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
const proxy = new Proxy(target, handler) as T
proxyCache.set(target, proxy)
defineProxyProperties(target, proxy, hooks as StateHooks | undefined)
return proxy
}
function disposeChildEffects(eff: EffectFunction): void {
const existing = eff.__children
if (!existing?.size) return
eff.__children = undefined
for (const c of existing) {
cleanupEffectCompletely(c)
}
existing.clear()
}
function removeStaleSubscribers(eff: EffectFunction, prevDeps: Set<object>, newDeps: Set<object>): void {
for (const dep of prevDeps) {
if (!newDeps.has(dep)) {
findSubscribers(dep)?.delete(eff)
}
}
}
if (!initialized) {
lastSourceValue = source()
lastSelectedValue = selectorFn(lastSourceValue)
initialized = true
function registerNewSubscribers(eff: EffectFunction, prevDeps: Set<object>, newDeps: Set<object>): void {
for (const dep of newDeps) {
if (!prevDeps.has(dep)) {
const subs = getSubscribers(dep)
subs.add(eff)
}
return lastSelectedValue as R
}
}
// Returns the value if non-nullish, otherwise creates the appropriate container type
const ensureContainer = (value: unknown, nextKey: string | undefined): unknown => {
if (value != null) return value
if (nextKey !== undefined && !Number.isNaN(Number(nextKey))) return []
return {}
function areDepsStable(
newDeps: Set<object> | undefined,
newReads: Map<object, Set<PropertyKey>> | undefined,
prevDeps: Set<object>,
prevReads: Map<object, Set<PropertyKey>>
): boolean {
if (!newDeps || !newReads) return false
return depsMatch(prevDeps, newDeps, prevReads, newReads)
}
const setValueAtPath = <V, O>(obj: O, pathSegments: string[], depth: number, value: V): O => {
if (depth >= pathSegments.length) {
return value as unknown as O
function tryRestoreStableDeps(
eff: EffectFunction,
prevDeps: Set<object> | undefined,
prevReads: Map<object, Set<PropertyKey>> | undefined,
newDeps: Set<object> | undefined,
newReads: Map<object, Set<PropertyKey>> | undefined
): boolean {
if (!prevDeps || !prevReads || !newDeps || !newReads) return false
if (!areDepsStable(newDeps, newReads, prevDeps, prevReads)) return false
eff.__reads = prevReads
eff.__deps = prevDeps
return true
}
function updateEffectSubscriptions(
eff: EffectFunction,
prevDeps: Set<object> | undefined,
prevReads: Map<object, Set<PropertyKey>> | undefined
): void {
const newDeps = eff.__deps
const newReads = eff.__reads
if (tryRestoreStableDeps(eff, prevDeps, prevReads, newDeps, newReads)) return
if (!newDeps || !newReads) return
if (prevDeps) {
removeStaleSubscribers(eff, prevDeps, newDeps)
registerNewSubscribers(eff, prevDeps, newDeps)
}
eff.__prevDeps = newDeps
eff.__prevReads = newReads
}
if (obj == null) {
return setValueAtPath({} as O, pathSegments, depth, value)
function removeEffectFromSubscribers(eff: EffectFunction, deps: Set<object> | undefined): void {
if (!deps) return
for (const dep of deps) {
findSubscribers(dep)?.delete(eff)
}
}
const currentKey = pathSegments[depth]
if (currentKey === undefined) {
return obj
function cleanupEffectOnError(eff: EffectFunction): void {
removeEffectFromSubscribers(eff, eff.__prevDeps)
removeEffectFromSubscribers(eff, eff.__deps)
resetEffectTracking(eff)
}
function attachEffectHooks(
eff: EffectFunction,
onDependencyAdd:
| HookFunction<
[
object,
PropertyKey,
string | undefined,
]
>
| undefined,
onDependencyChange:
| HookFunction<
[
object,
PropertyKey,
]
>
| undefined,
onSchedule:
| HookFunction<
[
string | undefined,
]
>
| undefined
): void {
if (!onDependencyAdd && !onSchedule && !onDependencyChange) return
eff.__hooks = buildEffectHooksMap(onDependencyAdd, onDependencyChange, onSchedule)
}
function buildEffectHooksMap(
onDependencyAdd:
| HookFunction<
[
object,
PropertyKey,
string | undefined,
]
>
| undefined,
onDependencyChange:
| HookFunction<
[
object,
PropertyKey,
]
>
| undefined,
onSchedule:
| HookFunction<
[
string | undefined,
]
>
| undefined
): NonNullable<EffectFunction['__hooks']> {
const map: NonNullable<EffectFunction['__hooks']> = Object.create(null)
if (onDependencyAdd) map.onDependencyAdd = onDependencyAdd
if (onDependencyChange) map.onDependencyChange = onDependencyChange
if (onSchedule) map.onSchedule = onSchedule
return map
}
function promoteTempToGlobal(
eff: EffectFunction,
tempDeps: Set<object>,
tempReads: Map<object, Set<PropertyKey>>
): void {
eff.__deps = tempDeps
eff.__reads = tempReads
}
function clearRerunState(): void {
rerunReadList = null
rerunReadIndex = 0
rerunStable = false
rerunTempDeps = null
rerunTempReads = null
buildingReadList = null
}
function handleReadListMismatch(eff: EffectFunction): void {
if (rerunReadList) {
if (rerunStable) {
rerunStable = false
replayReadListToTempCollections(rerunReadList, rerunReadIndex)
}
eff.__readList = undefined
rerunReadList = null
rerunReadIndex = 0
}
}
const isArray = Array.isArray(obj)
const key = isArray ? Number(currentKey) : currentKey
function handleRerunResult(
eff: EffectFunction,
prevDeps: Set<object>,
prevReads: Map<object, Set<PropertyKey>>
): boolean {
if (rerunStable && rerunReadList && rerunReadIndex === rerunReadList.length) {
clearRerunState()
return true
}
if (depth === pathSegments.length - 1) {
if (isArray) {
const copy = [
...(obj as unknown[]),
]
copy[key as number] = value
return copy as unknown as O
handleReadListMismatch(eff)
if (rerunTempDeps && rerunTempReads) {
if (depsMatch(prevDeps, rerunTempDeps, prevReads, rerunTempReads)) {
if (buildingReadList) {
eff.__readList = buildingReadList
}
clearRerunState()
return true
}
const result = {
...(obj as Record<string, unknown>),
promoteTempToGlobal(eff, rerunTempDeps, rerunTempReads)
eff.__readList = undefined
}
clearRerunState()
return false
}
function executeEffectBody(
eff: EffectFunction,
fn: EffectCallback,
prevDeps: Set<object> | undefined,
prevReads: Map<object, Set<PropertyKey>> | undefined,
onRun:
| HookFunction<
[
EffectName | undefined,
]
>
| undefined,
name: EffectName | undefined
): void {
const isFirstRun = !prevDeps
if (!isFirstRun) isTrackingOnly = true
pendingEffects.delete(eff)
disposeChildEffects(eff)
currentEffect = eff
if (isFirstRun) {
eff.__reads = new Map<object, Set<PropertyKey>>()
eff.__deps = new Set<object>()
} else {
const existingReadList = eff.__readList
if (existingReadList) {
rerunReadList = existingReadList
rerunReadIndex = 0
rerunStable = true
buildingReadList = null
} else {
rerunTempDeps = new Set<object>()
rerunTempReads = new Map<object, Set<PropertyKey>>()
buildingReadList = []
rerunReadList = null
rerunStable = false
}
result[key] = value
return result as unknown as O
}
const nextDepth = depth + 1
const nextKey = pathSegments[nextDepth]
const source = isArray ? (obj as unknown[])[key as number] : (obj as Record<string | number, unknown>)[key]
callHookSafe(onRun, name)
const nextValue = ensureContainer(source, nextKey)
fn()
if (isArray) {
const copy = [
...(obj as unknown[]),
]
copy[key as number] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
return copy as unknown as O
if (!isFirstRun && prevDeps && prevReads) {
if (handleRerunResult(eff, prevDeps, prevReads)) return
}
const result = {
...(obj as Record<string | number, unknown>),
updateEffectSubscriptions(eff, prevDeps, prevReads)
}
function runEffectSafely(
eff: EffectFunction,
fn: EffectCallback,
onRun:
| HookFunction<
[
EffectName | undefined,
]
>
| undefined,
onError:
| HookFunction<
[
Error,
EffectName | undefined,
]
>
| undefined,
name: EffectName | undefined
): void {
const prev = currentEffect
const prevTrackingOnly = isTrackingOnly
try {
executeEffectBody(eff, fn, eff.__prevDeps, eff.__prevReads, onRun, name)
} catch (err) {
clearRerunState()
cleanupEffectOnError(eff)
callHookSafe(onError, err as Error, name)
throw err
} finally {
currentEffect = prev
isTrackingOnly = prevTrackingOnly
eff.__active = false
}
result[key] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
return result as unknown as O
}
const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
let isUpdating = false
function registerChildEffect(eff: EffectFunction): void {
if (!currentEffect) return
eff.__parent = currentEffect
let children = currentEffect.__children
if (!children) {
children = new Set<EffectFunction>()
currentEffect.__children = children
}
children.add(eff)
}
const extractPath = (): string[] => {
const pathCollector: string[] = []
let tainted = false
const proxy = new Proxy(
{},
{
get: (_: object, prop: string | symbol): unknown => {
if (!tainted && typeof prop === 'string') {
if (DANGEROUS_KEYS.has(String(prop))) {
tainted = true
} else {
pathCollector.push(prop)
}
}
return proxy
},
}
)
export function effect(fn: EffectCallback, name?: EffectName, hooks?: EffectHooks): Unsubscribe {
const onRun = composeHook(hooks?.onRun)
const onDispose = composeHook(hooks?.onDispose)
const onError = composeHook(hooks?.onError)
const onDependencyAdd = composeHook(hooks?.onDependencyAdd)
const onDependencyChange = composeHook(hooks?.onDependencyChange)
const onSchedule = composeHook(hooks?.onSchedule)
try {
accessor(proxy as unknown as T)
} catch {
// Ignore errors, we're just collecting the path
}
const runEffect: EffectFunction = () => {
if (runEffect.__active) return
runEffect.__active = true
runEffectSafely(runEffect, fn, onRun, onError, name)
}
return tainted ? [] : pathCollector
attachEffectHooks(runEffect, onDependencyAdd, onDependencyChange, onSchedule)
registerChildEffect(runEffect)
if (name) {
runEffect.effectName = name
}
const path = extractPath()
const lensState = createState<K>(accessor(source()))
const originalSet = lensState.set
if (batchDepth === 0) runEffect()
else deferredEffectCreations.push(runEffect)
createEffect(function lensEffect(): void {
if (isUpdating) {
return
}
return (): void => {
callHookSafe(onDispose, name)
cleanupEffectCompletely(runEffect)
}
}
isUpdating = true
try {
lensState.set(accessor(source()))
} finally {
isUpdating = false
function flushDirtyTargets(): void {
for (const [target, props] of dirtyTargets) {
if (Array.isArray(target)) {
scheduleSubscribersForTarget(target)
} else {
for (const prop of props) {
scheduleSubscribersForTarget(target, prop)
}
}
})
}
dirtyTargets.clear()
}
lensState.set = function lensSet(value: K): void {
if (isUpdating || path.length === 0) {
return
function clearBatchState(): void {
pendingEffects.clear()
deferredEffectCreations.length = 0
dirtyTargets.clear()
}
function runDeferredEffects(): void {
const len = deferredEffectCreations.length
if (len > 0) {
for (let i = 0; i < len; i++) {
const eff = deferredEffectCreations[i]
if (eff) eff()
}
deferredEffectCreations.length = 0
}
}
isUpdating = true
function flushBatchEffects(): void {
try {
runDeferredEffects()
if (pendingEffects.size > 0) flushEffects()
} catch (err) {
clearBatchState()
throw err
}
}
function handleBatchError(
err: unknown,
onBatchError:
| HookFunction<
[
Error,
number,
]
>
| undefined,
entryDepth: number
): void {
batchDepth--
callHookSafe(onBatchError, err as Error, entryDepth)
if (batchDepth === 0) {
clearBatchState()
}
}
export function batch<T>(fn: () => T, hooks?: BatchHooks): T {
if (!hooks) {
batchDepth++
let result: T
try {
originalSet(value)
source.update((current: T): T => setValueAtPath(current, path, 0, value))
} finally {
isUpdating = false
result = fn()
} catch (err) {
batchDepth--
if (batchDepth === 0) clearBatchState()
throw err
}
if (batchDepth === 1 && dirtyTargets.size > 0) {
flushDirtyTargets()
}
batchDepth--
if (batchDepth === 0) {
flushBatchEffects()
}
return result
}
lensState.update = function lensUpdate(fn: (value: K) => K): void {
lensState.set(fn(lensState()))
const onBatchStart = composeHook(hooks.onBatchStart)
const onBatchEnd = composeHook(hooks.onBatchEnd)
const onBatchError = composeHook(hooks.onBatchError)
batchDepth++
const entryDepth = batchDepth
callHookSafe(onBatchStart, entryDepth)
let result: T
try {
result = fn()
} catch (err) {
handleBatchError(err, onBatchError, entryDepth)
throw err
}
if (batchDepth === 1 && dirtyTargets.size > 0) {
flushDirtyTargets()
}
return lensState
batchDepth--
if (batchDepth === 0) {
flushBatchEffects()
}
callHookSafe(onBatchEnd, entryDepth)
return result
}
const createReadonlyState =
<T>(source: State<T>): ReadOnlyState<T> =>
(): T =>
source()
function runDeriveComputation<T>(
computeFn: () => T,
internalState: {
lastValue: T | undefined | null
},
reactiveInternal: {
value: T | undefined | null
} | null,
onCompute:
| HookFunction<
[
T | undefined,
]
>
| undefined
): void {
callHookSafe(onCompute, internalState.lastValue as T | undefined)
const newValue = computeFn()
if (!Object.is(newValue, internalState.lastValue)) {
internalState.lastValue = newValue
if (reactiveInternal) {
reactiveInternal.value = newValue
}
}
}
const createProtectedState = <T>(
initialValue: T,
equalityFn: (a: T, b: T) => boolean = Object.is
): [
ReadOnlyState<T>,
WriteableState<T>,
] => {
const fullState = createState(initialValue, equalityFn)
const reader = createReadonlyState(fullState)
return [
reader,
{
set: (value: T): void => fullState.set(value),
update: (fn: (value: T) => T): void => fullState.update(fn),
},
]
function resolveDeriveValue<T>(
target: {
value: T | undefined | null
},
reactiveInternal: {
value: T | undefined | null
} | null
): T | undefined | null {
return reactiveInternal && currentEffect ? reactiveInternal.value : target.value
}
export type { ReadOnlyState, State, Unsubscribe, WriteableState }
export {
createDerive as derive,
createEffect as effect,
createLens as lens,
createProtectedState as protectedState,
createReadonlyState as readonlyState,
createSelect as select,
createState as state,
executeBatch as batch,
function toggleDeriveReactivity(
value: boolean,
wasReactive: boolean,
hasDispose: boolean,
createEffect: () => void,
disposeEffect: () => void
): void {
if (value && !wasReactive && !hasDispose) {
createEffect()
} else if (!value && wasReactive && hasDispose) {
disposeEffect()
}
}
function runDeriveWithErrorHandling<T>(
computeFn: () => T,
internalState: {
lastValue: T | undefined | null
},
reactiveInternal: {
value: T | undefined | null
} | null,
onCompute:
| HookFunction<
[
T | undefined,
]
>
| undefined,
onError:
| HookFunction<
[
Error,
]
>
| undefined,
cleanup: () => void
): void {
try {
runDeriveComputation(computeFn, internalState, reactiveInternal, onCompute)
} catch (err) {
callHookSafe(onError, err as Error)
throw err
} finally {
cleanup()
}
}
export function derive<T>(computeFn: () => T, hooks?: DeriveHooks<T>): ComputedValue<T> {
const onCompute = composeHook(hooks?.onCompute)
const onCacheHit = composeHook(hooks?.onCacheHit)
const onDeriveDispose = composeHook(hooks?.onDispose)
const onError = composeHook(hooks?.onError)
const onDependencyChange = composeHook(hooks?.onDependencyChange)
const internalState = {
lastValue: undefined as T | undefined | null,
reactive: true,
value: undefined as T | undefined | null,
}
let dispose: Unsubscribe | null = null
let isComputing = false
let reactiveInternal: typeof internalState | null = null
const createEffect = (): void => {
if (dispose) return
reactiveInternal = state(internalState)
const internalEffectHooks: EffectHooks | undefined = onDependencyChange
? {
onDependencyChange,
}
: undefined
dispose = effect(
(): void => {
if (!internalState.reactive || isComputing) return
isComputing = true
runDeriveWithErrorHandling(computeFn, internalState, reactiveInternal, onCompute, onError, () => {
isComputing = false
})
},
undefined,
internalEffectHooks
)
}
const disposeEffect = (): void => {
if (dispose) {
callHookSafe(onDeriveDispose)
dispose()
dispose = null
reactiveInternal = null
}
}
if (internalState.reactive) {
createEffect()
}
return new Proxy(internalState, {
get(target: typeof internalState, prop: PropertyKey): unknown {
if (prop === 'value') {
const value = resolveDeriveValue(target, reactiveInternal)
callHookSafe(onCacheHit, value as T, !isComputing)
return value
}
if (prop === 'reactive') {
return target.reactive
}
return undefined
},
set(target: typeof internalState, prop: PropertyKey, value: unknown): boolean {
if (prop === 'reactive') {
const wasReactive = target.reactive
target.reactive = value as boolean
toggleDeriveReactivity(value as boolean, wasReactive, !!dispose, createEffect, disposeEffect)
return true
}
return false
},
}) as ComputedValue<T>
}