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.2.2
to
1000.2.3
+673
src/index.ts
// Core types for reactive primitives
type Subscriber = () => void
export type Unsubscribe = () => void
export type ReadOnlyState<T> = () => T
export interface WriteableState<T> {
set(value: T): void
update(fn: (value: T) => T): void
}
// Special symbol used for internal tracking
const STATE_ID: unique symbol = Symbol('STATE_ID')
export type State<T> = ReadOnlyState<T> &
WriteableState<T> & {
[STATE_ID]?: symbol
}
/**
* Creates a reactive state container with the provided initial value.
*/
export const state = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> =>
StateImpl.createState(initialValue, equalityFn)
/**
* Registers a function to run whenever its reactive dependencies change.
*/
export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn)
/**
* Groups multiple state updates to trigger effects only once at the end.
*/
export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn)
/**
* Creates a read-only computed value that updates when its dependencies change.
*/
export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn)
/**
* Creates an efficient subscription to a subset of a state value.
*/
export const select = <T, R>(
source: ReadOnlyState<T>,
selectorFn: (state: T) => R,
equalityFn: (a: R, b: R) => boolean = Object.is
): ReadOnlyState<R> => StateImpl.createSelect(source, selectorFn, equalityFn)
/**
* Creates a read-only view of a state, hiding mutation methods.
*/
export const readonlyState =
<T>(state: State<T>): ReadOnlyState<T> =>
(): T =>
state()
/**
* Creates a state with access control, returning a tuple of reader and writer.
*/
export const protectedState = <T>(
initialValue: T,
equalityFn: (a: T, b: T) => boolean = Object.is
): [
ReadOnlyState<T>,
WriteableState<T>,
] => {
const fullState = state(initialValue, equalityFn)
return [
(): T => readonlyState(fullState)(),
{
set: (value: T): void => fullState.set(value),
update: (fn: (value: T) => T): void => fullState.update(fn),
},
]
}
/**
* Creates a lens for direct updates to nested properties of a state.
*/
export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> =>
StateImpl.createLens(source, accessor)
class StateImpl<T> {
// Static fields track global reactivity state - this centralized approach allows
// for coordinated updates while maintaining individual state isolation
private static currentSubscriber: Subscriber | null = null
private static pendingSubscribers = new Set<Subscriber>()
private static isNotifying = false
private static batchDepth = 0
private static deferredEffectCreations: Subscriber[] = []
private static activeSubscribers = new Set<Subscriber>()
// WeakMaps enable automatic garbage collection when subscribers are no
// longer referenced, preventing memory leaks in long-running applications
private static stateTracking = new WeakMap<Subscriber, Set<symbol>>()
private static subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>()
private static parentSubscriber = new WeakMap<Subscriber, Subscriber>()
private static childSubscribers = new WeakMap<Subscriber, Set<Subscriber>>()
// Instance state - each state has unique subscribers and ID
private value: T
private subscribers = new Set<Subscriber>()
private stateId = Symbol()
private equalityFn: (a: T, b: T) => boolean
constructor(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is) {
this.value = initialValue
this.equalityFn = equalityFn
}
/**
* Creates a reactive state container with the provided initial value.
* Implementation of the public 'state' function.
*/
static createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
const instance = new StateImpl<T>(initialValue, equalityFn)
const get = (): T => instance.get()
get.set = (value: T): void => instance.set(value)
get.update = (fn: (currentValue: T) => T): void => instance.update(fn)
get[STATE_ID] = instance.stateId
return get as State<T>
}
// Auto-tracks dependencies when called within effects, creating a fine-grained
// reactivity graph that only updates affected components
get = (): T => {
const currentEffect = StateImpl.currentSubscriber
if (currentEffect) {
// Add this effect to subscribers for future notification
this.subscribers.add(currentEffect)
// Maintain bidirectional dependency tracking to enable precise cleanup
// when effects are unsubscribed, preventing memory leaks
let dependencies = StateImpl.subscriberDependencies.get(currentEffect)
if (!dependencies) {
dependencies = new Set()
StateImpl.subscriberDependencies.set(currentEffect, dependencies)
}
dependencies.add(this.subscribers)
// Track read states to detect direct cyclical dependencies that
// could cause infinite loops
let readStates = StateImpl.stateTracking.get(currentEffect)
if (!readStates) {
readStates = new Set()
StateImpl.stateTracking.set(currentEffect, readStates)
}
readStates.add(this.stateId)
}
return this.value
}
// Handles value updates with built-in optimizations and safeguards
set = (newValue: T): void => {
// Skip updates for unchanged values to prevent redundant effect executions
if (this.equalityFn(this.value, newValue)) {
return
}
// Infinite loop detection prevents direct self-mutation within effects,
// while allowing nested effect patterns that would otherwise appear cyclical
const effect = StateImpl.currentSubscriber
if (effect) {
const states = StateImpl.stateTracking.get(effect)
if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
}
}
this.value = newValue
// Skip updates when there are no subscribers, avoiding unnecessary processing
if (this.subscribers.size === 0) {
return
}
// Queue notifications instead of executing immediately to support batch operations
// and prevent redundant effect runs
for (const sub of this.subscribers) {
StateImpl.pendingSubscribers.add(sub)
}
// Immediate execution outside of batches, deferred execution inside batches
if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
StateImpl.notifySubscribers()
}
}
update = (fn: (currentValue: T) => T): void => {
this.set(fn(this.value))
}
/**
* Registers a function to run whenever its reactive dependencies change.
* Implementation of the public 'effect' function.
*/
static createEffect = (fn: () => void): Unsubscribe => {
const runEffect = (): void => {
// Prevent re-entrance to avoid cascade updates during effect execution
if (StateImpl.activeSubscribers.has(runEffect)) {
return
}
StateImpl.activeSubscribers.add(runEffect)
const parentEffect = StateImpl.currentSubscriber
try {
// Clean existing subscriptions before running to ensure only
// currently accessed states are tracked as dependencies
StateImpl.cleanupEffect(runEffect)
// Set current context for automatic dependency tracking
StateImpl.currentSubscriber = runEffect
StateImpl.stateTracking.set(runEffect, new Set())
// Track parent-child relationships to handle nested effects correctly
// and enable hierarchical cleanup later
if (parentEffect) {
StateImpl.parentSubscriber.set(runEffect, parentEffect)
let children = StateImpl.childSubscribers.get(parentEffect)
if (!children) {
children = new Set()
StateImpl.childSubscribers.set(parentEffect, children)
}
children.add(runEffect)
}
// Execute the effect function, which will auto-track dependencies
fn()
} finally {
// Restore previous context when done
StateImpl.currentSubscriber = parentEffect
StateImpl.activeSubscribers.delete(runEffect)
}
}
// Run immediately unless we're in a batch operation
if (StateImpl.batchDepth === 0) {
runEffect()
} else {
// Still track parent-child relationship even when deferred,
// ensuring proper hierarchical cleanup later
if (StateImpl.currentSubscriber) {
const parent = StateImpl.currentSubscriber
StateImpl.parentSubscriber.set(runEffect, parent)
let children = StateImpl.childSubscribers.get(parent)
if (!children) {
children = new Set()
StateImpl.childSubscribers.set(parent, children)
}
children.add(runEffect)
}
// Queue for execution when batch completes
StateImpl.deferredEffectCreations.push(runEffect)
}
// Return cleanup function to properly disconnect from reactivity graph
return (): void => {
// Remove from dependency tracking to stop future notifications
StateImpl.cleanupEffect(runEffect)
StateImpl.pendingSubscribers.delete(runEffect)
StateImpl.activeSubscribers.delete(runEffect)
StateImpl.stateTracking.delete(runEffect)
// Clean up parent-child relationship bidirectionally
const parent = StateImpl.parentSubscriber.get(runEffect)
if (parent) {
const siblings = StateImpl.childSubscribers.get(parent)
if (siblings) {
siblings.delete(runEffect)
}
}
StateImpl.parentSubscriber.delete(runEffect)
// Recursively clean up child effects to prevent memory leaks in
// nested effect scenarios
const children = StateImpl.childSubscribers.get(runEffect)
if (children) {
for (const child of children) {
StateImpl.cleanupEffect(child)
}
children.clear()
StateImpl.childSubscribers.delete(runEffect)
}
}
}
/**
* Groups multiple state updates to trigger effects only once at the end.
* Implementation of the public 'batch' function.
*/
static executeBatch = <T>(fn: () => T): T => {
// Increment depth counter to handle nested batches correctly
StateImpl.batchDepth++
try {
return fn()
} catch (error: unknown) {
// Clean up on error to prevent stale subscribers from executing
// and potentially causing cascading errors
if (StateImpl.batchDepth === 1) {
StateImpl.pendingSubscribers.clear()
StateImpl.deferredEffectCreations.length = 0
}
throw error
} finally {
StateImpl.batchDepth--
// Only process effects when exiting the outermost batch,
// maintaining proper execution order while avoiding redundant runs
if (StateImpl.batchDepth === 0) {
// Process effects created during the batch
if (StateImpl.deferredEffectCreations.length > 0) {
const effectsToRun = [
...StateImpl.deferredEffectCreations,
]
StateImpl.deferredEffectCreations.length = 0
for (const effect of effectsToRun) {
effect()
}
}
// Process state updates that occurred during the batch
if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
StateImpl.notifySubscribers()
}
}
}
}
/**
* Creates a read-only computed value that updates when its dependencies change.
* Implementation of the public 'derive' function.
*/
static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
// Create a container to hold state and minimize closure captures
const container = {
cachedValue: undefined as unknown as T,
computeFn,
initialized: false,
valueState: StateImpl.createState<T | undefined>(undefined),
}
// Internal effect automatically tracks dependencies and updates the derived value
StateImpl.createEffect(function deriveEffect(): void {
const newValue = container.computeFn()
// Only update if the value actually changed to preserve referential equality
// and prevent unnecessary downstream updates
if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
container.cachedValue = newValue
container.valueState.set(newValue)
}
container.initialized = true
})
// Return function with lazy initialization - ensures value is available
// even when accessed before its dependencies have had a chance to update
return function deriveGetter(): T {
if (!container.initialized) {
container.cachedValue = container.computeFn()
container.initialized = true
container.valueState.set(container.cachedValue)
}
return container.valueState() as T
}
}
/**
* Creates an efficient subscription to a subset of a state value.
* Implementation of the public 'select' function.
*/
static createSelect = <T, R>(
source: ReadOnlyState<T>,
selectorFn: (state: T) => R,
equalityFn: (a: R, b: R) => boolean = Object.is
): ReadOnlyState<R> => {
// Create a container to hold state and minimize closure captures
const container = {
equalityFn,
initialized: false,
lastSelectedValue: undefined as R | undefined,
lastSourceValue: undefined as T | undefined,
selectorFn,
source,
valueState: StateImpl.createState<R | undefined>(undefined),
}
// Internal effect to track the source and update only when needed
StateImpl.createEffect(function selectEffect(): void {
const sourceValue = container.source()
// Skip computation if source reference hasn't changed
if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
return
}
container.lastSourceValue = sourceValue
const newSelectedValue = container.selectorFn(sourceValue)
// Use custom equality function to determine if value semantically changed,
// allowing for deep equality comparisons with complex objects
if (
container.initialized &&
container.lastSelectedValue !== undefined &&
container.equalityFn(container.lastSelectedValue, newSelectedValue)
) {
return
}
// Update cache and notify subscribers due the value has changed
container.lastSelectedValue = newSelectedValue
container.valueState.set(newSelectedValue)
container.initialized = true
})
// Return function with eager initialization capability
return function selectGetter(): R {
if (!container.initialized) {
container.lastSourceValue = container.source()
container.lastSelectedValue = container.selectorFn(container.lastSourceValue)
container.valueState.set(container.lastSelectedValue)
container.initialized = true
}
return container.valueState() as R
}
}
/**
* Creates a lens for direct updates to nested properties of a state.
* Implementation of the public 'lens' function.
*/
static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
// Create a container to hold lens state and minimize closure captures
const container = {
accessor,
isUpdating: false,
lensState: null as unknown as State<K>,
originalSet: null as unknown as (value: K) => void,
path: [] as (string | number)[],
source,
}
// Extract the property path once during lens creation
const extractPath = (): (string | number)[] => {
const pathCollector: (string | number)[] = []
const proxy = new Proxy(
{},
{
get: (_: object, prop: string | symbol): unknown => {
if (typeof prop === 'string' || typeof prop === 'number') {
pathCollector.push(prop)
}
return proxy
},
}
)
try {
container.accessor(proxy as unknown as T)
} catch {
// Ignore errors, we're just collecting the path
}
return pathCollector
}
// Capture the path once
container.path = extractPath()
// Create a state with the initial value from the source
container.lensState = StateImpl.createState<K>(container.accessor(container.source()))
container.originalSet = container.lensState.set
// Set up an effect to sync from source to lens
StateImpl.createEffect(function lensEffect(): void {
if (container.isUpdating) {
return
}
container.isUpdating = true
try {
container.lensState.set(container.accessor(container.source()))
} finally {
container.isUpdating = false
}
})
// Override the lens state's set method to update the source
container.lensState.set = function lensSet(value: K): void {
if (container.isUpdating) {
return
}
container.isUpdating = true
try {
// Update lens state
container.originalSet(value)
// Update source by modifying the value at path
container.source.update((current: T): T => setValueAtPath(current, container.path, value))
} finally {
container.isUpdating = false
}
}
// Add update method for completeness
container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
container.lensState.set(fn(container.lensState()))
}
return container.lensState
}
// Processes queued subscriber notifications in a controlled, non-reentrant way
private static notifySubscribers = (): void => {
// Prevent reentrance to avoid cascading notification loops when
// effects trigger further state changes
if (StateImpl.isNotifying) {
return
}
StateImpl.isNotifying = true
try {
// Process all pending effects in batches for better perf,
// ensuring topological execution order is maintained
while (StateImpl.pendingSubscribers.size > 0) {
// Process in snapshot batches to prevent infinite loops
// when effects trigger further state changes
const subscribers = Array.from(StateImpl.pendingSubscribers)
StateImpl.pendingSubscribers.clear()
for (const effect of subscribers) {
effect()
}
}
} finally {
StateImpl.isNotifying = false
}
}
// Removes effect from dependency tracking to prevent memory leaks
private static cleanupEffect = (effect: Subscriber): void => {
// Remove from execution queue to prevent stale updates
StateImpl.pendingSubscribers.delete(effect)
// Remove bidirectional dependency references to prevent memory leaks
const deps = StateImpl.subscriberDependencies.get(effect)
if (deps) {
for (const subscribers of deps) {
subscribers.delete(effect)
}
deps.clear()
StateImpl.subscriberDependencies.delete(effect)
}
}
}
// Helper for array updates
const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
const copy = [
...arr,
]
copy[index] = value
return copy
}
// Helper for single-level updates (optimization)
const updateShallowProperty = <V>(
obj: Record<string | number, unknown>,
key: string | number,
value: V
): Record<string | number, unknown> => {
const result = {
...obj,
}
result[key] = value
return result
}
// Helper to create the appropriate container type
const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => {
const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key))
return isArrayKey ? [] : {}
}
// Helper for handling array path updates
const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => {
const index = Number(pathSegments[0])
if (pathSegments.length === 1) {
// Simple array item update
return updateArrayItem(array, index, value)
}
// Nested path in array
const copy = [
...array,
]
const nextPathSegments = pathSegments.slice(1)
const nextKey = nextPathSegments[0]
// For null/undefined values in arrays, create appropriate containers
let nextValue = array[index]
if (nextValue === undefined || nextValue === null) {
// Use empty object as default if nextKey is undefined
nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
}
copy[index] = setValueAtPath(nextValue, nextPathSegments, value)
return copy
}
// Helper for handling object path updates
const updateObjectPath = <V>(
obj: Record<string | number, unknown>,
pathSegments: (string | number)[],
value: V
): Record<string | number, unknown> => {
// Ensure we have a valid key
const currentKey = pathSegments[0]
if (currentKey === undefined) {
// This shouldn't happen given our checks in the main function
return obj
}
if (pathSegments.length === 1) {
// Simple object property update
return updateShallowProperty(obj, currentKey, value)
}
// Nested path in object
const nextPathSegments = pathSegments.slice(1)
const nextKey = nextPathSegments[0]
// For null/undefined values, create appropriate containers
let currentValue = obj[currentKey]
if (currentValue === undefined || currentValue === null) {
// Use empty object as default if nextKey is undefined
currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
}
// Create new object with updated property
const result = {
...obj,
}
result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
return result
}
// Simplified function to update a nested value at a path
const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
// Handle base cases
if (pathSegments.length === 0) {
return value as unknown as O
}
if (obj === undefined || obj === null) {
return setValueAtPath({} as O, pathSegments, value)
}
const currentKey = pathSegments[0]
if (currentKey === undefined) {
return obj
}
// Delegate to specialized handlers based on data type
if (Array.isArray(obj)) {
return updateArrayPath(obj, pathSegments, value) as unknown as O
}
return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
}
+8
-6

@@ -5,4 +5,5 @@ {

"devDependencies": {
"@biomejs/biome": "2.2.6",
"@biomejs/biome": "2.2.7",
"@types/node": "24.9.1",
"npm-check-updates": "19.1.1",
"typescript": "5.9.3",

@@ -24,9 +25,9 @@ "uglify-js": "3.19.3"

"types": "./dist/src/index.d.ts",
"typescript": "./dist/src/index.ts"
"typescript": "./src/index.ts"
}
},
"files": [
"dist/src/index.js",
"dist/src/index.min.js",
"dist/src/index.d.ts",
"dist/src/index.ts",
"src/index.ts",
"LICENSE"

@@ -54,3 +55,3 @@ ],

"name": "@nerdalytics/beacon",
"packageManager": "npm@11.6.0",
"packageManager": "npm@11.6.2",
"repository": {

@@ -93,2 +94,3 @@ "type": "git",

"test:unit:state": "node --test tests/state.test.ts",
"update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange",
"update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"

@@ -98,3 +100,3 @@ },

"types": "dist/src/index.d.ts",
"version": "1000.2.2"
"version": "1000.2.3"
}

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

[![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/latest)](https://badge.socket.dev/npm/package/@nerdalytics/beacon/latest)

@@ -9,0 +10,0 @@ [![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)

const STATE_ID = Symbol('STATE_ID');
export const state = (initialValue, equalityFn = Object.is) => StateImpl.createState(initialValue, equalityFn);
export const effect = (fn) => StateImpl.createEffect(fn);
export const batch = (fn) => StateImpl.executeBatch(fn);
export const derive = (computeFn) => StateImpl.createDerive(computeFn);
export const select = (source, selectorFn, equalityFn = Object.is) => StateImpl.createSelect(source, selectorFn, equalityFn);
export const readonlyState = (state) => () => state();
export const protectedState = (initialValue, equalityFn = Object.is) => {
const fullState = state(initialValue, equalityFn);
return [
() => readonlyState(fullState)(),
{
set: (value) => fullState.set(value),
update: (fn) => fullState.update(fn),
},
];
};
export const lens = (source, accessor) => StateImpl.createLens(source, accessor);
class StateImpl {
static currentSubscriber = null;
static pendingSubscribers = new Set();
static isNotifying = false;
static batchDepth = 0;
static deferredEffectCreations = [];
static activeSubscribers = new Set();
static stateTracking = new WeakMap();
static subscriberDependencies = new WeakMap();
static parentSubscriber = new WeakMap();
static childSubscribers = new WeakMap();
value;
subscribers = new Set();
stateId = Symbol();
equalityFn;
constructor(initialValue, equalityFn = Object.is) {
this.value = initialValue;
this.equalityFn = equalityFn;
}
static createState = (initialValue, equalityFn = Object.is) => {
const instance = new StateImpl(initialValue, equalityFn);
const get = () => instance.get();
get.set = (value) => instance.set(value);
get.update = (fn) => instance.update(fn);
get[STATE_ID] = instance.stateId;
return get;
};
get = () => {
const currentEffect = StateImpl.currentSubscriber;
if (currentEffect) {
this.subscribers.add(currentEffect);
let dependencies = StateImpl.subscriberDependencies.get(currentEffect);
if (!dependencies) {
dependencies = new Set();
StateImpl.subscriberDependencies.set(currentEffect, dependencies);
}
dependencies.add(this.subscribers);
let readStates = StateImpl.stateTracking.get(currentEffect);
if (!readStates) {
readStates = new Set();
StateImpl.stateTracking.set(currentEffect, readStates);
}
readStates.add(this.stateId);
}
return this.value;
};
set = (newValue) => {
if (this.equalityFn(this.value, newValue)) {
return;
}
const effect = StateImpl.currentSubscriber;
if (effect) {
const states = StateImpl.stateTracking.get(effect);
if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!');
}
}
this.value = newValue;
if (this.subscribers.size === 0) {
return;
}
for (const sub of this.subscribers) {
StateImpl.pendingSubscribers.add(sub);
}
if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
StateImpl.notifySubscribers();
}
};
update = (fn) => {
this.set(fn(this.value));
};
static createEffect = (fn) => {
const runEffect = () => {
if (StateImpl.activeSubscribers.has(runEffect)) {
return;
}
StateImpl.activeSubscribers.add(runEffect);
const parentEffect = StateImpl.currentSubscriber;
try {
StateImpl.cleanupEffect(runEffect);
StateImpl.currentSubscriber = runEffect;
StateImpl.stateTracking.set(runEffect, new Set());
if (parentEffect) {
StateImpl.parentSubscriber.set(runEffect, parentEffect);
let children = StateImpl.childSubscribers.get(parentEffect);
if (!children) {
children = new Set();
StateImpl.childSubscribers.set(parentEffect, children);
}
children.add(runEffect);
}
fn();
}
finally {
StateImpl.currentSubscriber = parentEffect;
StateImpl.activeSubscribers.delete(runEffect);
}
};
if (StateImpl.batchDepth === 0) {
runEffect();
}
else {
if (StateImpl.currentSubscriber) {
const parent = StateImpl.currentSubscriber;
StateImpl.parentSubscriber.set(runEffect, parent);
let children = StateImpl.childSubscribers.get(parent);
if (!children) {
children = new Set();
StateImpl.childSubscribers.set(parent, children);
}
children.add(runEffect);
}
StateImpl.deferredEffectCreations.push(runEffect);
}
return () => {
StateImpl.cleanupEffect(runEffect);
StateImpl.pendingSubscribers.delete(runEffect);
StateImpl.activeSubscribers.delete(runEffect);
StateImpl.stateTracking.delete(runEffect);
const parent = StateImpl.parentSubscriber.get(runEffect);
if (parent) {
const siblings = StateImpl.childSubscribers.get(parent);
if (siblings) {
siblings.delete(runEffect);
}
}
StateImpl.parentSubscriber.delete(runEffect);
const children = StateImpl.childSubscribers.get(runEffect);
if (children) {
for (const child of children) {
StateImpl.cleanupEffect(child);
}
children.clear();
StateImpl.childSubscribers.delete(runEffect);
}
};
};
static executeBatch = (fn) => {
StateImpl.batchDepth++;
try {
return fn();
}
catch (error) {
if (StateImpl.batchDepth === 1) {
StateImpl.pendingSubscribers.clear();
StateImpl.deferredEffectCreations.length = 0;
}
throw error;
}
finally {
StateImpl.batchDepth--;
if (StateImpl.batchDepth === 0) {
if (StateImpl.deferredEffectCreations.length > 0) {
const effectsToRun = [
...StateImpl.deferredEffectCreations,
];
StateImpl.deferredEffectCreations.length = 0;
for (const effect of effectsToRun) {
effect();
}
}
if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
StateImpl.notifySubscribers();
}
}
}
};
static createDerive = (computeFn) => {
const container = {
cachedValue: undefined,
computeFn,
initialized: false,
valueState: StateImpl.createState(undefined),
};
StateImpl.createEffect(function deriveEffect() {
const newValue = container.computeFn();
if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
container.cachedValue = newValue;
container.valueState.set(newValue);
}
container.initialized = true;
});
return function deriveGetter() {
if (!container.initialized) {
container.cachedValue = container.computeFn();
container.initialized = true;
container.valueState.set(container.cachedValue);
}
return container.valueState();
};
};
static createSelect = (source, selectorFn, equalityFn = Object.is) => {
const container = {
equalityFn,
initialized: false,
lastSelectedValue: undefined,
lastSourceValue: undefined,
selectorFn,
source,
valueState: StateImpl.createState(undefined),
};
StateImpl.createEffect(function selectEffect() {
const sourceValue = container.source();
if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
return;
}
container.lastSourceValue = sourceValue;
const newSelectedValue = container.selectorFn(sourceValue);
if (container.initialized &&
container.lastSelectedValue !== undefined &&
container.equalityFn(container.lastSelectedValue, newSelectedValue)) {
return;
}
container.lastSelectedValue = newSelectedValue;
container.valueState.set(newSelectedValue);
container.initialized = true;
});
return function selectGetter() {
if (!container.initialized) {
container.lastSourceValue = container.source();
container.lastSelectedValue = container.selectorFn(container.lastSourceValue);
container.valueState.set(container.lastSelectedValue);
container.initialized = true;
}
return container.valueState();
};
};
static createLens = (source, accessor) => {
const container = {
accessor,
isUpdating: false,
lensState: null,
originalSet: null,
path: [],
source,
};
const extractPath = () => {
const pathCollector = [];
const proxy = new Proxy({}, {
get: (_, prop) => {
if (typeof prop === 'string' || typeof prop === 'number') {
pathCollector.push(prop);
}
return proxy;
},
});
try {
container.accessor(proxy);
}
catch {
}
return pathCollector;
};
container.path = extractPath();
container.lensState = StateImpl.createState(container.accessor(container.source()));
container.originalSet = container.lensState.set;
StateImpl.createEffect(function lensEffect() {
if (container.isUpdating) {
return;
}
container.isUpdating = true;
try {
container.lensState.set(container.accessor(container.source()));
}
finally {
container.isUpdating = false;
}
});
container.lensState.set = function lensSet(value) {
if (container.isUpdating) {
return;
}
container.isUpdating = true;
try {
container.originalSet(value);
container.source.update((current) => setValueAtPath(current, container.path, value));
}
finally {
container.isUpdating = false;
}
};
container.lensState.update = function lensUpdate(fn) {
container.lensState.set(fn(container.lensState()));
};
return container.lensState;
};
static notifySubscribers = () => {
if (StateImpl.isNotifying) {
return;
}
StateImpl.isNotifying = true;
try {
while (StateImpl.pendingSubscribers.size > 0) {
const subscribers = Array.from(StateImpl.pendingSubscribers);
StateImpl.pendingSubscribers.clear();
for (const effect of subscribers) {
effect();
}
}
}
finally {
StateImpl.isNotifying = false;
}
};
static cleanupEffect = (effect) => {
StateImpl.pendingSubscribers.delete(effect);
const deps = StateImpl.subscriberDependencies.get(effect);
if (deps) {
for (const subscribers of deps) {
subscribers.delete(effect);
}
deps.clear();
StateImpl.subscriberDependencies.delete(effect);
}
};
}
const updateArrayItem = (arr, index, value) => {
const copy = [
...arr,
];
copy[index] = value;
return copy;
};
const updateShallowProperty = (obj, key, value) => {
const result = {
...obj,
};
result[key] = value;
return result;
};
const createContainer = (key) => {
const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key));
return isArrayKey ? [] : {};
};
const updateArrayPath = (array, pathSegments, value) => {
const index = Number(pathSegments[0]);
if (pathSegments.length === 1) {
return updateArrayItem(array, index, value);
}
const copy = [
...array,
];
const nextPathSegments = pathSegments.slice(1);
const nextKey = nextPathSegments[0];
let nextValue = array[index];
if (nextValue === undefined || nextValue === null) {
nextValue = nextKey !== undefined ? createContainer(nextKey) : {};
}
copy[index] = setValueAtPath(nextValue, nextPathSegments, value);
return copy;
};
const updateObjectPath = (obj, pathSegments, value) => {
const currentKey = pathSegments[0];
if (currentKey === undefined) {
return obj;
}
if (pathSegments.length === 1) {
return updateShallowProperty(obj, currentKey, value);
}
const nextPathSegments = pathSegments.slice(1);
const nextKey = nextPathSegments[0];
let currentValue = obj[currentKey];
if (currentValue === undefined || currentValue === null) {
currentValue = nextKey !== undefined ? createContainer(nextKey) : {};
}
const result = {
...obj,
};
result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value);
return result;
};
const setValueAtPath = (obj, pathSegments, value) => {
if (pathSegments.length === 0) {
return value;
}
if (obj === undefined || obj === null) {
return setValueAtPath({}, pathSegments, value);
}
const currentKey = pathSegments[0];
if (currentKey === undefined) {
return obj;
}
if (Array.isArray(obj)) {
return updateArrayPath(obj, pathSegments, value);
}
return updateObjectPath(obj, pathSegments, value);
};
//# sourceMappingURL=index.js.map