react-smart-state
Version:
Next generation local and global state management
537 lines (536 loc) โข 23.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
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;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrimitiveObject = exports.PrimitiveValue = void 0;
const methods_1 = require("./methods");
const objects_1 = require("./objects");
__exportStar(require("./methods"), exports);
__exportStar(require("./types"), exports);
__exportStar(require("./objects"), exports);
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) {
(0, methods_1.SmartStateError)(e, { code: "batch", details: func });
}
finally {
enable();
}
});
}
resetState() {
var _a, _b;
if (this.getEvent().stateType == "Global")
throw (0, methods_1.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 = (0, methods_1.refCondition)(methods_1.newId).value;
let mappedKeys = (0, methods_1.refCondition)(() => (0, methods_1.toObject)(...keys)).value;
let [state, setState] = (0, methods_1.reactState)({});
let hookSettings = (0, methods_1.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
});
(0, methods_1.reactEffect)(() => {
return () => this.getEvent().remove(id);
}, []);
return {
on: (fn) => {
hookSettings.on = fn;
return this;
}
};
}
catch (e) {
throw (0, methods_1.SmartStateError)(e, { code: "hook", details: keys });
}
}
useComputed(fn, ...keys) {
try {
let id = (0, methods_1.refCondition)(methods_1.newId).value;
let mappedKeys = (0, methods_1.refCondition)(() => (0, methods_1.toObject)(...keys)).value;
let [state, setState] = (0, methods_1.reactState)(fn(this, undefined));
this.getEvent().add(id, {
func: () => {
let newValue = fn(this, state);
if (newValue !== state)
setState(newValue);
},
keys: mappedKeys
});
(0, methods_1.reactEffect)(() => {
return () => this.getEvent().remove(id);
}, []);
return state;
}
catch (e) {
throw (0, methods_1.SmartStateError)(e, { code: "useComputed", details: keys });
}
}
useEffect(fn, ...keys) {
try {
let id = (0, methods_1.refCondition)(methods_1.newId).value;
let mappedKeys = (0, methods_1.refCondition)(() => (0, methods_1.toObject)(...keys)).value;
let state = (0, methods_1.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
});
(0, methods_1.reactEffect)(() => {
return () => this.getEvent().remove(id);
}, []);
}
catch (e) {
throw (0, methods_1.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 && !(0, methods_1.isArray)(item) && (0, methods_1.valid)(item)) {
let v = item[key];
delete item[key];
item[key] = v;
}
}
catch (e) {
throw (0, methods_1.SmartStateError)(e, { code: "unbind", details: path });
}
return this;
}
localBind(path) {
try {
const [state, setState] = (0, methods_1.reactState)();
const id = (0, methods_1.refCondition)(methods_1.newId).value;
const hookSettings = (0, methods_1.refCondition)(() => ({ on: undefined })).value;
const mappedKeys = (0, methods_1.refCondition)(() => (0, methods_1.toObject)(path)).value;
if (!this.getEvent().localBindedEvents.has(path)) {
this.getEvent().localBindedEvents.set(path, new objects_1.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"
});
(0, methods_1.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 (0, methods_1.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;
}
if (item && !(0, methods_1.isArray)(item) && (0, methods_1.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);
}
}
});
}
}
if (autoUnbind) {
(0, methods_1.reactEffect)(() => {
() => this.unbind(path);
}, []);
}
}
catch (e) {
throw (0, methods_1.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 objects_1.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] && (0, methods_1.valid)(value, arrayParser)) {
if (cache.has(value)) {
// Cycle detected, return existing instance
return cache.get(value);
}
value = (0, methods_1.clone)(value);
if ((0, methods_1.isArray)(value)) {
// Parse each array item recursively
let arr = new objects_1.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 (0, methods_1.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 ((0, methods_1.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) && (0, methods_1.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 (0, methods_1.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 = (0, methods_1.refCondition)(() => this);
const update = (0, methods_1.updater)();
const $this = stateItem.value;
// Initialize only once
if ($this.initilized === undefined) {
$this.initilized = new Create({
item: (0, methods_1.getItem)($this.item),
ignoreKeys: (0, methods_1.toObject)(...$this.ignoreKeys),
hardIgnoreKeys: (0, methods_1.toObject)(...$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 (0, methods_1.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";
}
(0, methods_1.reactEffect)(() => {
if ($this.onStateInit)
$this.onStateInit($this.initilized);
}, [update.value]);
(0, methods_1.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: (0, methods_1.getItem)(this.item),
ignoreKeys: (0, methods_1.toObject)(...this.ignoreKeys),
hardIgnoreKeys: (0, methods_1.toObject)(...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);
};
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];
};
exports.PrimitiveValue = PrimitiveValue;
const PrimitiveObject = (initialValue) => {
const state = new StateBuilder(() => ({ value: initialValue })).build();
return state;
};
exports.PrimitiveObject = PrimitiveObject;
exports.default = StateManagment;