@nerdalytics/beacon
Advanced tools
| 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' |
+158
| 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 |
+23
-13
@@ -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"/> | ||
| [](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE) | ||
| [](https://yarnpkg.com/) | ||
| [](https://pnpm.io/) | ||
| [](https://jsr.io/@nerdalytics/beacon) | ||
| [](https://vlt.sh/) | ||
| [](https://www.npmjs.com/package/@nerdalytics/beacon) | ||
| [](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.3) | ||
| [](https://socket.dev/npm/package/@nerdalytics/beacon/overview/2000.0.0) | ||
| [](https://nodejs.org/) | ||
| [](https://nodejs.org/) | ||
| [](https://typescriptlang.org/) | ||
| [](https://biomejs.dev/) | ||
| [](https://biomejs.dev/) | ||
| [](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> | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
62368
192.53%17
183.33%1583
274.23%75
41.51%7
40%1
Infinity%