UNPKG

react-smart-state

Version:

Next generation local and global state management

526 lines (525 loc) โ€ข 22.2 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var _Create_events; import { clone, getItem, isArray, isSame, keys, newId, reactEffect, reactRef, reactState, refCondition, SmartStateError, toObject, updater, valid } from "./methods"; import { EventTrigger, FastList, ObservableArray } from "./objects"; export * from "./methods"; export * from "./types"; export * from "./objects"; class Create { batch(func) { return __awaiter(this, void 0, void 0, function* () { const batching = this.getEvent().batching; const index = batching.size; const enable = () => { batching.delete(index); this.getEvent().triggerSavedChanges(); }; batching.set(index, func); try { yield func(); } catch (e) { SmartStateError(e, { code: "batch", details: func }); } finally { enable(); } }); } resetState() { var _a, _b; if (this.getEvent().stateType == "Global") throw SmartStateError("resetState is not implemented in globalState, only on local state", { code: "resetState" }); return (_b = (_a = this.getEvent()).resetState) === null || _b === void 0 ? void 0 : _b.call(_a); } hook(...keys) { try { let id = refCondition(newId).value; let mappedKeys = refCondition(() => toObject(true, ...keys)).value; let [state, setState] = reactState({}); let hookSettings = refCondition(() => ({ on: undefined })).value; this.getEvent().add(id, { func: items => { let newState = this.getEvent().hasChange(items, state); const update = newState.hasChanges && (!hookSettings.on || hookSettings.on(this)); if (update) setState(Object.assign({}, newState.parentState)); }, keys: mappedKeys }); reactEffect(() => { return () => this.getEvent().remove(id); }, []); return { on: (fn) => { hookSettings.on = fn; return this; } }; } catch (e) { throw SmartStateError(e, { code: "hook", details: keys }); } } useComputed(fn, ...keys) { try { let id = refCondition(newId).value; let mappedKeys = refCondition(() => toObject(true, ...keys)).value; let [state, setState] = reactState(fn(this, undefined)); this.getEvent().add(id, { func: () => { let newValue = fn(this, state); if (newValue !== state) setState(newValue); }, keys: mappedKeys }); reactEffect(() => { return () => this.getEvent().remove(id); }, []); return state; } catch (e) { throw SmartStateError(e, { code: "useComputed", details: keys }); } } useEffect(fn, ...keys) { try { let id = refCondition(newId).value; let mappedKeys = refCondition(() => toObject(true, ...keys)).value; let state = reactRef({}); this.getEvent().add(id, { func: (items) => { let newState = this.getEvent().hasChange(items, state.current); if (newState.hasChanges) fn(this); state.current = newState.parentState; }, keys: mappedKeys }); reactEffect(() => { return () => this.getEvent().remove(id); }, []); } catch (e) { throw SmartStateError(e, { code: "useEffect", details: keys }); } } unbind(path) { try { if (!this.getEvent().addedPaths.has(path)) return; // not bound, do nothing this.getEvent().addedPaths.delete(path); let item = this; let key = path.split(".").reverse()[0]; for (let p of path.split(".")) { if (item[p] !== null && typeof item[p] === "object") { item = item[p]; } else break; } if (item && !isArray(item) && valid(item)) { let v = item[key]; delete item[key]; item[key] = v; } } catch (e) { throw SmartStateError(e, { code: "unbind", details: path }); } return this; } localBind(path) { try { const [state, setState] = reactState(); const id = refCondition(newId).value; const hookSettings = refCondition(() => ({ on: undefined })).value; const mappedKeys = refCondition(() => toObject(true, path)).value; if (!this.getEvent().localBindedEvents.has(path)) { this.getEvent().localBindedEvents.set(path, new FastList()); this.bind(path); } this.getEvent().localBindedEvents.get(path).set(id, true); this.getEvent().add(id, { func: (items) => { let newState = this.getEvent().hasChange(items, state); const update = newState.hasChanges && (!hookSettings.on || hookSettings.on(this)); if (update) setState(Object.assign({}, newState.parentState)); }, keys: mappedKeys, type: "Path" }); reactEffect(() => { return () => { var _a, _b; this.getEvent().remove(id); (_a = this.getEvent().localBindedEvents.get(path)) === null || _a === void 0 ? void 0 : _a.delete(id); if (!((_b = this.getEvent().localBindedEvents.get(path)) === null || _b === void 0 ? void 0 : _b.hasValue)) { this.getEvent().localBindedEvents.delete(path); this.unbind(path); } }; }, []); return { on: (fn) => { hookSettings.on = fn; return this; } }; } catch (e) { throw SmartStateError(e, { code: "localBind", details: path }); } } bind(path, autoUnbind, rebind) { try { if (!this.getEvent().addedPaths.has(path) || rebind) { this.getEvent().addedPaths.set(path, path); let item = this; let key = path.split(".").reverse()[0]; for (let p of path.split(".")) { if (typeof item[p] === "object") { item = item[p]; } else break; } let { ignoreKeys } = this.getEvent(); let oKeys = toObject(true, ...ignoreKeys._keys); let fromBind = false; for (let k of path.split(".")) if (oKeys[k]) { fromBind = true; break; } if (item && !isArray(item) && valid(item)) { let v = item[key]; Object.defineProperty(item, key, { enumerable: true, configurable: true, get: () => v, set: (value) => { let newValue = { oldValue: v, newValue: value }; if (value !== v) { v = value; this.getEvent().onChange(path, newValue, fromBind); } } }); } } if (autoUnbind) { reactEffect(() => { () => this.unbind(path); }, []); } } catch (e) { throw SmartStateError(e, { code: "bind", details: path }); } return this; } constructor(item) { _Create_events.set(this, void 0); if (item) this.initializeStateItem(item); } getEvent() { return __classPrivateFieldGet(this, _Create_events, "f"); } getInstanceType() { return "react-smart-state-item"; } initializeStateItem(data) { try { if (data.parentItem === undefined) { data.parentItem = this; __classPrivateFieldSet(this, _Create_events, new EventTrigger(data.ignoreKeys, data.hardIgnoreKeys), "f"); } let { item, parent, parentItem, arrayParser } = data; let { ignoreKeys, hardIgnoreKeys } = parentItem.getEvent(); let parentKeys = (key) => { if (parent && parent.length > 0) return `${parent}.${key}`; return key; }; const parse = (val, parentKey) => { try { const cache = parentItem.getEvent().seen; let value = val; if (!ignoreKeys[parentKey] && valid(value, arrayParser)) { if (cache.has(value)) { // Cycle detected, return existing instance return cache.get(value); } value = clone(value); if (isArray(value)) { // Parse each array item recursively let arr = new ObservableArray(parentKey, (item, index) => parse(item, parentKey), (a, b, changes) => parentItem.getEvent().onChange(changes.key, changes)); arr.push(...value); arr.hasInit = true; return arr; } else { // Create a placeholder instance and set it immediately const newInstance = new Create(); cache.set(val, newInstance); // Now call the constructor logic on the placeholder instance newInstance.initializeStateItem({ item: value, parent: parentKey, parentItem, arrayParser }); // Create a new Create instance and store in 'seen' map return newInstance; } } } catch (e) { console.error(e); } return val; }; for (let itemKey of keys(item, Create.prototype)) { let parentKey = parentKeys(itemKey); let itemValue = parse(item[itemKey], parentKey); parentItem.getEvent().seen.delete(item[itemKey]); if (itemValue !== item[itemKey]) item[itemKey] = itemValue; Object.defineProperty(this, itemKey, { enumerable: true, configurable: true, get: () => item[itemKey], set: (value) => { if (isSame(value, item[itemKey])) { return; // do nothing as the objects are the same } const newValue = { oldValue: item[itemKey], newValue: parse(value, parentKey) }; item[itemKey] = newValue.newValue; parentItem.getEvent().seen.delete(value); if ((parentKey.includes(".") || itemKey === parentKey) && valid(value, arrayParser)) { let parts = parentKey.split("."); // Traverse up from most specific to least specific (excluding the root level) if (parentItem.getEvent().addedPaths.hasValue || parentItem.getEvent().localBindedEvents.hasValue) while (parts.length > 1 || (parts.length > 0 && itemKey == parentKey)) { parts = parts.slice(0, -1); const pKey = itemKey == parentKey ? itemKey : parts.join("."); if (parentItem.getEvent().addedPaths.hasValue) parentItem.getEvent().addedPaths.keys.forEach((addedKey) => { if (addedKey.startsWith(pKey + ".") || addedKey === pKey) { parentItem.bind(addedKey, false, true); } }); } } parentItem.getEvent().onChange(parentKey, newValue); } }); } } catch (e) { throw SmartStateError(e, { code: "initializeStateItem", details: data }); } } } _Create_events = new WeakMap(); /** * StateBuilder is a fluent API to build local or global reactive state * using the Create proxy system with support for recursive structures, * selective key ignoring, and optional binding with event dispatch control. */ class StateBuilder { /** * @param item - The object to wrap in a reactive state, or a function returning it */ constructor(item) { this.ignoreKeys = []; this.hardIgnoreKeys = []; this.bindKeys = []; this.localBindKeys = []; this.timeoutSpeed = -1; this.item = item; } /** * create proxies for arrays items. * that are not included in ignore objects. * this is disabled by default for better performance. */ parseArray() { this.arrayParser = true; return this; } /** * Sets the debounce speed (in milliseconds) for event dispatch. * Set to `undefined` to disable throttling, or leave `-1` for default behavior. * @param speed - Optional timeout in milliseconds. */ timeout(speed) { this.timeoutSpeed = speed; return this; } /** * Registers an asynchronous initialization function that will be called * when the state is first created or initialized. * * Useful for performing setup logic such as loading data, setting defaults, or * triggering side effects after the state is ready. * * @param func - An async function that receives the initial state and performs setup. * @returns The current instance (for chaining). */ onInit(func) { this.onStateInit = func; return this; } /** * Prevents state updates for specific nested keys. * Any state update attempt on the specified keys will be ignored. * still keys added to .hook will override this settings * * @param keys - Array of nested key paths (e.g. 'user.name', 'settings.theme') * @returns The current instance (for chaining) */ ignoreUpdatesFor(...keys) { this.hardIgnoreKeys = keys; return this; } /** * Ignores specified nested keys from being proxied/reactive. * Useful for skipping large, static, or recursive subtrees to improve performance. * @param ignoreKeys - Keys to ignore from proxying */ ignore(...ignoreKeys) { this.ignoreKeys = ignoreKeys; return this; } /** * Rebinds specific nested keys that were previously ignored. * Enables listening to changes on those keys manually. * @param bindKeys - Keys within ignored objects to still bind to */ bind(...bindKeys) { this.bindKeys = bindKeys; return this; } /** * Like `bind()`, but events are scoped locally (do not trigger global listeners). * Useful for isolating state changes in local components. * @param bindKeys - Keys to bind with local-only change listeners */ localBind(...bindKeys) { this.localBindKeys = bindKeys; return this; } /** * Builds and returns a local reactive state object that can be used in components. * Initializes the state only once, applies bindings, and sets hook context. */ build() { var _a; const stateItem = refCondition(() => this); const update = updater(); const $this = stateItem.value; // Initialize only once if ($this.initilized === undefined) { $this.initilized = new Create({ item: getItem($this.item), ignoreKeys: toObject(false, ...$this.ignoreKeys), hardIgnoreKeys: toObject(false, ...$this.hardIgnoreKeys), arrayParser: (_a = this.arrayParser) !== null && _a !== void 0 ? _a : false }); // Apply timeout settings $this.initilized.getEvent().speed = $this.timeoutSpeed === -1 ? undefined : $this.timeoutSpeed; Object.defineProperty($this.initilized, "isMounted", { get() { var _a; if ($this.initilized.getEvent().stateType === "Global") { throw SmartStateError("isMounted is not implemented in globalState, only on local state", { code: "isMounted" }); } return (_a = $this.initilized.getEvent().isMounted) !== null && _a !== void 0 ? _a : false; }, enumerable: false, // ๐Ÿ”’ hides it from JSON.stringify and for..in }); $this.initilized.getEvent().stateType = "Local"; } reactEffect(() => { if ($this.onStateInit) $this.onStateInit($this.initilized); }, [update.value]); reactEffect(() => { $this.initilized.getEvent().isMounted = true; }, []); $this.initilized.getEvent().resetState = () => { stateItem.setValue(undefined); update.refresh(); }; // Hook into React render cycle $this.initilized.hook(); // Rebind specified keys for (let key of $this.bindKeys) { $this.initilized.bind(key, true); } // Rebind keys locally for (let key of $this.localBindKeys) { $this.initilized.localBind(key); } return $this.initilized; } /** * Builds and returns a global reactive state object. * This does not hook into React, so itโ€™s suitable for shared/global stores. */ globalBuild() { var _a; if (this.initilized === undefined) { this.initilized = new Create({ item: getItem(this.item), ignoreKeys: toObject(false, ...this.ignoreKeys), hardIgnoreKeys: toObject(false, ...this.hardIgnoreKeys), arrayParser: (_a = this.arrayParser) !== null && _a !== void 0 ? _a : false }); // Use default timeout unless explicitly set this.initilized.getEvent().speed = this.timeoutSpeed === -1 ? 2 : this.timeoutSpeed; this.initilized.getEvent().stateType = "Global"; if (this.onStateInit) this.onStateInit(this.initilized); } return this.initilized; } } const StateManagment = (item) => { return new StateBuilder(item); }; export const PrimitiveValue = (initialValue) => { const state = new StateBuilder(() => ({ value: initialValue })).build(); state.setValue = (newValue) => { state.value = (typeof newValue === "function" ? newValue(state.value) : newValue); }; return [state.value, state.setValue]; }; export const PrimitiveObject = (initialValue) => { const state = new StateBuilder(() => ({ value: initialValue })).build(); return state; }; export default StateManagment;