react-smart-state
Version:
Next generation local and global state management
526 lines (525 loc) โข 22.2 kB
JavaScript
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;