watch-selector
Version:
Runs a function when a selector is added to dom
595 lines (556 loc) • 17 kB
text/typescript
/**
* @fileoverview Synchronous state management operations for the generator submodule
*
* This module provides synchronous state management operations that return SyncWorkflow<T>
* directly, enabling efficient state management in sync generators without async overhead.
*
* @example Basic State Management with Sync Generators
* ```typescript
* import { watch } from 'watch-selector';
* import { getState, setState, updateState } from 'watch-selector/generator-sync';
*
* watch('.counter', function*() { // Note: sync function*, not async
* // Initialize state
* yield* setState('count', 0);
*
* // Get state with type safety
* const count = yield* getState<number>('count', 0);
*
* // Update state with a function
* yield* updateState('count', (c) => c + 1);
* });
* ```
*
* @module generator-sync/state
*/
import type { SyncWorkflow, WatchContext, Operation } from "../types";
// ============================================================================
// BASIC STATE OPERATIONS
// ============================================================================
/**
* Gets a state value for the current element using the sync generator API.
*
* @param key - The state key to retrieve
* @param defaultValue - Optional default value if the key doesn't exist
* @returns A SyncWorkflow that returns the state value
*
* @example Getting typed state in sync generator
* ```typescript
* import { watch } from 'watch-selector';
* import { getState, setState } from 'watch-selector/generator-sync';
*
* watch('.user-card', function*() {
* const userId = yield* getState<string>('userId', 'anonymous');
* const clickCount = yield* getState<number>('clicks', 0);
* });
* ```
*/
export function getState<T = any>(
key: string,
defaultValue?: T,
): SyncWorkflow<T> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return defaultValue as T;
}
return context.state.has(key)
? (context.state.get(key) as T)
: (defaultValue as T);
}) as Operation<T>;
return result as T;
})();
}
/**
* Generator version of getState for explicit yield* usage.
*/
getState.gen = function <T = any>(
key: string,
defaultValue?: T,
): SyncWorkflow<T> {
return getState<T>(key, defaultValue);
};
/**
* Sets a state value for the current element using the sync generator API.
*
* @param key - The state key to set
* @param value - The value to set
* @returns A SyncWorkflow<void> that sets the state
*
* @example Setting state in sync generator
* ```typescript
* import { watch } from 'watch-selector';
* import { setState, click } from 'watch-selector/generator-sync';
*
* watch('.toggle', function*() {
* yield* setState('isActive', false);
*
* yield* click(function*() {
* yield* setState('isActive', true);
* });
* });
* ```
*/
export function setState<T = any>(key: string, value: T): SyncWorkflow<void> {
return (function* () {
yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
context.state.set(key, value);
}) as Operation<void>;
})();
}
/**
* Generator version of setState for explicit yield* usage.
*/
setState.gen = function <T = any>(key: string, value: T): SyncWorkflow<void> {
return setState<T>(key, value);
};
/**
* Updates a state value using a function in the sync generator API.
*
* @param key - The state key to update
* @param updater - Function that receives current value and returns new value
* @returns A SyncWorkflow that returns the new value
*
* @example Updating state with a function
* ```typescript
* import { watch } from 'watch-selector';
* import { updateState, click } from 'watch-selector/generator-sync';
*
* watch('.counter', function*() {
* yield* click(function*() {
* const newCount = yield* updateState('count', (c = 0) => c + 1);
* console.log('New count:', newCount);
* });
* });
* ```
*/
export function updateState<T = any>(
key: string,
updater: (current: T | undefined) => T,
): SyncWorkflow<T> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = context.state.get(key) as T | undefined;
const newValue = updater(current);
context.state.set(key, newValue);
return newValue;
}) as Operation<T>;
return result as T;
})();
}
/**
* Generator version of updateState for explicit yield* usage.
*/
updateState.gen = function <T = any>(
key: string,
updater: (current: T | undefined) => T,
): SyncWorkflow<T> {
return updateState<T>(key, updater);
};
/**
* Checks if a state key exists using the sync generator API.
*
* @param key - The state key to check
* @returns A SyncWorkflow<boolean> that returns true if the key exists
*/
export function hasState(key: string): SyncWorkflow<boolean> {
return (function* () {
const result = yield ((context: WatchContext) => {
return context.state ? context.state.has(key) : false;
}) as Operation<boolean>;
return result as boolean;
})();
}
/**
* Generator version of hasState for explicit yield* usage.
*/
hasState.gen = function (key: string): SyncWorkflow<boolean> {
return hasState(key);
};
/**
* Deletes a state key using the sync generator API.
*
* @param key - The state key to delete
* @returns A SyncWorkflow<boolean> that returns true if the key was deleted
*/
export function deleteState(key: string): SyncWorkflow<boolean> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) return false;
return context.state.delete(key);
}) as Operation<boolean>;
return result as boolean;
})();
}
/**
* Generator version of deleteState for explicit yield* usage.
*/
deleteState.gen = function (key: string): SyncWorkflow<boolean> {
return deleteState(key);
};
// ============================================================================
// ADVANCED STATE OPERATIONS
// ============================================================================
/**
* Initializes state with a value only if it doesn't exist.
*
* @param key - The state key
* @param initialValue - The initial value to set if key doesn't exist
* @returns A SyncWorkflow that returns the existing or initialized value
*/
export function initState<T = any>(
key: string,
initialValue: T,
): SyncWorkflow<T> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
if (!context.state.has(key)) {
context.state.set(key, initialValue);
return initialValue;
}
return context.state.get(key) as T;
}) as Operation<T>;
return result as T;
})();
}
/**
* Increments a numeric state value.
*
* @param key - The state key
* @param amount - Amount to increment by (default: 1)
* @returns A SyncWorkflow that returns the new value
*/
export function incrementState(
key: string,
amount: number = 1,
): SyncWorkflow<number> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) || 0) as number;
const newValue = current + amount;
context.state.set(key, newValue);
return newValue;
}) as Operation<number>;
return result as number;
})();
}
/**
* Decrements a numeric state value.
*
* @param key - The state key
* @param amount - Amount to decrement by (default: 1)
* @returns A SyncWorkflow that returns the new value
*/
export function decrementState(
key: string,
amount: number = 1,
): SyncWorkflow<number> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) || 0) as number;
const newValue = current - amount;
context.state.set(key, newValue);
return newValue;
}) as Operation<number>;
return result as number;
})();
}
/**
* Toggles a boolean state value.
*
* @param key - The state key
* @returns A SyncWorkflow that returns the new boolean value
*/
export function toggleState(key: string): SyncWorkflow<boolean> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) as boolean) || false;
const newValue = !current;
context.state.set(key, newValue);
return newValue;
}) as Operation<boolean>;
return result as boolean;
})();
}
/**
* Appends a value to an array state.
*
* @param key - The state key
* @param value - Value to append
* @returns A SyncWorkflow that returns the updated array
*/
export function appendToState<T = any>(
key: string,
value: T,
): SyncWorkflow<T[]> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) || []) as T[];
const newArray = [...current, value];
context.state.set(key, newArray);
return newArray;
}) as Operation<T[]>;
return result as T[];
})();
}
/**
* Prepends a value to an array state.
*
* @param key - The state key
* @param value - Value to prepend
* @returns A SyncWorkflow that returns the updated array
*/
export function prependToState<T = any>(
key: string,
value: T,
): SyncWorkflow<T[]> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) || []) as T[];
const newArray = [value, ...current];
context.state.set(key, newArray);
return newArray;
}) as Operation<T[]>;
return result as T[];
})();
}
/**
* Removes a value from an array state.
*
* @param key - The state key
* @param value - Value to remove (uses indexOf for comparison)
* @returns A SyncWorkflow that returns the updated array
*/
export function removeFromState<T = any>(
key: string,
value: T,
): SyncWorkflow<T[]> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) || []) as T[];
const index = current.indexOf(value);
const newArray =
index >= 0
? [...current.slice(0, index), ...current.slice(index + 1)]
: current;
context.state.set(key, newArray);
return newArray;
}) as Operation<T[]>;
return result as T[];
})();
}
/**
* Merges an object into existing state.
*
* @param key - The state key
* @param updates - Object to merge into current state
* @returns A SyncWorkflow that returns the merged object
*/
export function mergeState<T extends object>(
key: string,
updates: Partial<T>,
): SyncWorkflow<T> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
const current = (context.state.get(key) || {}) as T;
const merged = { ...current, ...updates };
context.state.set(key, merged);
return merged;
}) as Operation<T>;
return result as T;
})();
}
// ============================================================================
// STATE WATCHING OPERATIONS
// ============================================================================
/**
* Watches for changes to a state key.
*
* @param key - The state key to watch
* @param callback - Function to call when state changes
* @returns A SyncWorkflow<void> that sets up the watcher
*/
export function watchState<T = any>(
key: string,
callback: (newValue: T, oldValue: T | undefined) => void,
): SyncWorkflow<void> {
return (function* () {
yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
// Store the current value as the initial value for comparison
// This is used in the callback when the value changes
void context.state.get(key) as T | undefined;
// Override the set method for this key (simple implementation)
// In a real implementation, you'd want a more sophisticated observer pattern
const originalSet = context.state.set.bind(context.state);
context.state.set = function (k: any, v: any) {
if (k === key) {
const old = this.get(k);
originalSet(k, v);
if (old !== v) {
callback(v, old);
}
} else {
originalSet(k, v);
}
return this;
};
}) as Operation<void>;
})();
}
// ============================================================================
// REACTIVE STATE OPERATIONS
// ============================================================================
/**
* Creates a computed state value that updates when dependencies change.
*
* @param key - The state key for the computed value
* @param deps - Array of dependency state keys
* @param compute - Function to compute the value from dependencies
* @returns A SyncWorkflow that returns the computed value
*/
export function computedState<T = any>(
key: string,
deps: string[],
compute: (...values: any[]) => T,
): SyncWorkflow<T> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
context.state = new Map();
}
// Get dependency values
const depValues = deps.map((dep) => context.state!.get(dep));
// Compute and store the value
const computed = compute(...depValues);
context.state.set(key, computed);
return computed;
}) as Operation<T>;
return result as T;
})();
}
// ============================================================================
// STATE DEBUGGING OPERATIONS
// ============================================================================
/**
* Logs all state for the current element.
*
* @param label - Optional label for the log
* @returns A SyncWorkflow<void> that logs the state
*/
export function logState(label?: string): SyncWorkflow<void> {
return (function* () {
yield ((context: WatchContext) => {
const stateObj = context.state
? Object.fromEntries(context.state.entries())
: {};
// console.log(label || "State:", stateObj);
void stateObj; // Prevent unused variable warning
void label; // Prevent unused variable warning
}) as Operation<void>;
})();
}
/**
* Logs a specific state key value.
*
* @param key - The state key to log
* @param label - Optional label for the log
* @returns A SyncWorkflow<void> that logs the value
*/
export function logStateKey(key: string, label?: string): SyncWorkflow<void> {
return (function* () {
yield ((context: WatchContext) => {
const value = context.state?.get(key);
// console.log(label || `State[${key}]:`, value);
void value; // Prevent unused variable warning
void label; // Prevent unused variable warning
}) as Operation<void>;
})();
}
/**
* Gets a snapshot of all state as a plain object.
*
* @returns A SyncWorkflow that returns the state snapshot
*/
export function getStateSnapshot(): SyncWorkflow<Record<string, any>> {
return (function* () {
const result = yield ((context: WatchContext) => {
return context.state ? Object.fromEntries(context.state.entries()) : {};
}) as Operation<Record<string, any>>;
return result as Record<string, any>;
})();
}
/**
* Clears all state for the current element.
*
* @returns A SyncWorkflow<void> that clears the state
*/
export function clearState(): SyncWorkflow<void> {
return (function* () {
yield ((context: WatchContext) => {
if (context.state) {
context.state.clear();
}
}) as Operation<void>;
})();
}
/**
* Generator version of clearState for explicit yield* usage.
*/
clearState.gen = function (): SyncWorkflow<void> {
return clearState();
};
/**
* Gets all state keys for the current element.
*
* @returns A SyncWorkflow<string[]> that returns an array of state keys
*/
export function getStateKeys(): SyncWorkflow<string[]> {
return (function* () {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return [];
}
return Array.from(context.state.keys());
}) as Operation<string[]>;
return result as string[];
})();
}
/**
* Generator version of getStateKeys for explicit yield* usage.
*/
getStateKeys.gen = function (): SyncWorkflow<string[]> {
return getStateKeys();
};