@jay-js/system
Version:
A powerful and flexible TypeScript library for UI, state management, lazy loading, routing and managing draggable elements in modern web applications.
344 lines • 13 kB
JavaScript
import { generateFunctionHash, SETCHILD_MARKER, SETVALUE_MARKER } from "../utils/helpers.js";
import { subscriberManager } from "./subscriber.js";
import { subscriptionRegistry } from "./subscription-registry.js";
const buildPath = (segments) => segments.length ? segments.map(String).join(".") : "<root>";
const isObjectLike = (value) => typeof value === "object" && value !== null;
const VALID_SUBSCRIPTION_ID = /^[a-zA-Z0-9_:<>.()-]+$/;
/**
* Creates a reactive state container that can be subscribed to for changes
*
* @template T Type of the state data
* @param data Initial value of the state
* @returns A state object with methods to manage the state
*/
export const state = (data) => {
let _data = data;
const _effects = new Map();
const _effects_ids = new Set();
const _effects_by_target = new Map();
const _effects_global = new Set();
const _proxy_cache = new WeakMap();
function isStructuralMutation(target, prop, hadKey) {
if (Array.isArray(target)) {
if (prop === "length") {
return true;
}
if (typeof prop === "string") {
// Numeric index: setting an existing slot is not structural.
// Adding/removing (new index) is structural.
if (/^(0|[1-9]\d*)$/.test(prop)) {
return !hadKey;
}
}
return !hadKey;
}
// Para objetos: adicionar/remover propriedade NÃO é estrutural
// (apenas notifica quem acessa essa propriedade específica)
return false;
}
function subscribeEffect(path) {
const currentSubscriber = subscriberManager.getSubscriber();
if (!currentSubscriber) {
return;
}
const hash = generateFunctionHash(currentSubscriber, path);
// Register in subscription registry if element is available
const element = currentSubscriber._element;
if (element && element instanceof HTMLElement) {
const cleanupFn = () => state.unsub(hash);
subscriptionRegistry.registerSubscription(element, hash, state, cleanupFn);
}
state.sub(hash, Object.assign(currentSubscriber, { _target: path }));
_effects_ids.add(hash);
}
function getProxyForPath(value, pathSegments) {
if (!isObjectLike(value)) {
return value;
}
const targetObj = value;
const path = buildPath(pathSegments);
let byPath = _proxy_cache.get(targetObj);
if (!byPath) {
byPath = new Map();
_proxy_cache.set(targetObj, byPath);
}
const existing = byPath.get(path);
if (existing) {
return existing;
}
const proxy = new Proxy(value, {
get(target, prop, receiver) {
// Always allow common symbol-based introspection without tracking noise.
if (prop === Symbol.toStringTag || prop === Symbol.toPrimitive || prop === Symbol.iterator) {
return Reflect.get(target, prop, receiver);
}
const nextPathSegments = pathSegments.concat(prop);
const res = Reflect.get(target, prop, receiver);
if (isObjectLike(res)) {
return getProxyForPath(res, nextPathSegments);
}
subscribeEffect(buildPath(nextPathSegments));
return res;
},
set(target, prop, newValue, receiver) {
if (prop === "__proto__" || prop === "constructor" || prop === "prototype") {
return false;
}
const hadKey = Reflect.has(target, prop);
const prev = Reflect.get(target, prop, receiver);
if (Object.is(prev, newValue)) {
return true;
}
const ok = Reflect.set(target, prop, newValue, receiver);
if (!ok) {
return false;
}
const nextPathSegments = pathSegments.concat(prop);
const changedPath = buildPath(nextPathSegments);
const structural = isStructuralMutation(target, prop, hadKey);
// Prefer targeted invalidation; for structural mutations be conservative.
if (structural) {
runEffects(null, _effects_ids);
}
else {
runEffects(changedPath);
}
return true;
},
deleteProperty(target, prop) {
if (prop === "__proto__" || prop === "constructor" || prop === "prototype") {
return false;
}
const hadKey = Reflect.has(target, prop);
const ok = Reflect.deleteProperty(target, prop);
if (!ok) {
return false;
}
if (!hadKey) {
return true;
}
const nextPathSegments = pathSegments.concat(prop);
const changedPath = buildPath(nextPathSegments);
const structural = isStructuralMutation(target, prop, hadKey);
if (structural) {
runEffects(null, _effects_ids);
}
else {
runEffects(changedPath);
}
return true;
},
});
byPath.set(path, proxy);
subscribeEffect(path);
return proxy;
}
function runEffects(targetKey = null, targets, includeGlobal = true) {
if (_effects.size === 0) {
return;
}
const effectsToRun = new Set();
if (targetKey) {
const targetedEffects = _effects_by_target.get(targetKey);
if (targetedEffects) {
for (const id of targetedEffects) {
effectsToRun.add(id);
}
}
}
if (includeGlobal) {
for (const id of _effects_global) {
effectsToRun.add(id);
}
}
if (targets) {
if (targets instanceof Set) {
for (const id of targets) {
effectsToRun.add(id);
}
}
else {
const targetArray = Array.isArray(targets) ? targets : [targets];
for (const target of targetArray) {
effectsToRun.add(target);
}
}
}
if (effectsToRun.size === 0 && !targetKey && !targets) {
for (const [id] of _effects) {
effectsToRun.add(id);
}
}
const executedFunctions = new Set();
for (const id of effectsToRun) {
const effect = _effects.get(id);
if (effect && !executedFunctions.has(effect)) {
executedFunctions.add(effect);
effect(_data);
}
}
}
const state = {
/**
* Sets a new value for the state and notifies subscribers
*
* @param newData New data value or function that receives the current state and returns new state
* @param options Configuration options for the update operation
*/
set: (newData, options) => {
let newValue;
if (typeof newData === "function") {
newValue = newData(_data);
}
else {
newValue = newData;
}
if (Object.is(newValue, _data)) {
return;
}
// Detecta se o objeto/array raiz foi completamente trocado
// Isso inclui: object->object, object->null, null->object, array->array
const wasObjectLike = isObjectLike(_data);
const isObjectLike_new = isObjectLike(newValue);
const isRootObjectReplaced = (wasObjectLike || isObjectLike_new) && _data !== newValue;
_data = newValue;
if (options === null || options === void 0 ? void 0 : options.silent) {
return;
}
if (options === null || options === void 0 ? void 0 : options.target) {
runEffects(null, options.target, false);
return;
}
// Se o objeto/array raiz foi trocado, todas as propriedades mudaram
// Dispara TODOS os effects (globais + targeted)
if (isRootObjectReplaced) {
runEffects(null, _effects_ids, true);
}
else {
runEffects();
}
},
/**
* Gets the current value of the state
*
* @param callback Optional callback function that receives the current state value
* @returns The current state value
*/
get: (callback) => {
if (callback) {
callback(_data);
}
return _data;
},
/**
* Subscribes to state changes with a specific ID
*
* @param id Unique identifier for this subscription
* @param effect Callback function to be called when state changes
* @param run Whether to immediately run the effect with current state
* @returns Result of the effect if run is true
*/
sub: (id, effect, run = false) => {
var _a;
if (!id || typeof id !== "string") {
throw new TypeError("Subscription ID must be a non-empty string");
}
if (!VALID_SUBSCRIPTION_ID.test(id)) {
throw new Error(`Invalid subscription ID: "${id}". Only alphanumeric characters, underscore, and hyphen are allowed.`);
}
if (typeof effect !== "function") {
throw new TypeError("Effect must be a function");
}
_effects.set(id, effect);
_effects_ids.add(id);
const target = effect._target;
if (target) {
if (!_effects_by_target.has(target)) {
_effects_by_target.set(target, new Set());
}
(_a = _effects_by_target.get(target)) === null || _a === void 0 ? void 0 : _a.add(id);
}
else if (!effect[SETVALUE_MARKER] && !effect[SETCHILD_MARKER]) {
_effects_global.add(id);
}
if (run) {
return effect(_data);
}
},
/**
* Unsubscribes from state changes by ID
*
* @param id ID of the subscription to remove
*/
unsub: (id) => {
const effect = _effects.get(id);
if (!effect)
return;
_effects.delete(id);
_effects_ids.delete(id);
_effects_global.delete(id);
const target = effect._target;
if (target) {
const targetSet = _effects_by_target.get(target);
if (targetSet) {
targetSet.delete(id);
if (targetSet.size === 0) {
_effects_by_target.delete(target);
}
}
}
},
/**
* Manually triggers notifications to subscribers
*
* @param ids Specific subscriber IDs to trigger, if none provided all subscribers will be notified
*/
trigger: (...ids) => {
if (ids.length === 0) {
runEffects(null, _effects_ids);
return;
}
runEffects(null, ids, false);
},
/**
* Clears all subscriptions and optionally sets a new value
*
* @param newData Optional new value for the state
*/
clear: (newData) => {
if (typeof newData === "function") {
_data = newData(_data);
}
else if (newData !== undefined) {
_data = newData;
}
else {
_data = undefined;
}
_effects.clear();
_effects_ids.clear();
_effects_by_target.clear();
_effects_global.clear();
},
/**
* Getter for state value that automatically registers the current subscriber
*/
get value() {
// Para objetos/arrays, retorna um Proxy que registra a propriedade acessada
// e dispara invalidação por caminho (keyed-tracking).
if (isObjectLike(_data)) {
return getProxyForPath(_data, []);
}
subscribeEffect();
return _data;
},
/**
* Setter for state value
*/
set value(newData) {
this.set(newData);
},
};
return state;
};
//# sourceMappingURL=state.js.map