native-document 1.0.116 → 1.0.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1221,6 +1221,97 @@ var NativeComponents = (function (exports) {
1221
1221
  return cloned;
1222
1222
  };
1223
1223
 
1224
+ const $parseDateParts = (value, locale) => {
1225
+ const d = new Date(value);
1226
+ return {
1227
+ d,
1228
+ parts: new Intl.DateTimeFormat(locale, {
1229
+ year: 'numeric',
1230
+ month: 'long',
1231
+ day: '2-digit',
1232
+ hour: '2-digit',
1233
+ minute: '2-digit',
1234
+ second: '2-digit',
1235
+ }).formatToParts(d).reduce((acc, { type, value }) => {
1236
+ acc[type] = value;
1237
+ return acc;
1238
+ }, {})
1239
+ };
1240
+ };
1241
+
1242
+ const $applyDatePattern = (pattern, d, parts) => {
1243
+ const pad = n => String(n).padStart(2, '0');
1244
+ return pattern
1245
+ .replace('YYYY', parts.year)
1246
+ .replace('YY', parts.year.slice(-2))
1247
+ .replace('MMMM', parts.month)
1248
+ .replace('MMM', parts.month.slice(0, 3))
1249
+ .replace('MM', pad(d.getMonth() + 1))
1250
+ .replace('DD', pad(d.getDate()))
1251
+ .replace('D', d.getDate())
1252
+ .replace('HH', parts.hour)
1253
+ .replace('mm', parts.minute)
1254
+ .replace('ss', parts.second);
1255
+ };
1256
+
1257
+ const Formatters = {
1258
+ currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1259
+ new Intl.NumberFormat(locale, {
1260
+ style: 'currency',
1261
+ currency,
1262
+ notation,
1263
+ minimumFractionDigits,
1264
+ maximumFractionDigits
1265
+ }).format(value),
1266
+
1267
+ number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1268
+ new Intl.NumberFormat(locale, {
1269
+ notation,
1270
+ minimumFractionDigits,
1271
+ maximumFractionDigits
1272
+ }).format(value),
1273
+
1274
+ percent: (value, locale, { decimals = 1 } = {}) =>
1275
+ new Intl.NumberFormat(locale, {
1276
+ style: 'percent',
1277
+ maximumFractionDigits: decimals
1278
+ }).format(value),
1279
+
1280
+ date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1281
+ if (format) {
1282
+ const { d, parts } = $parseDateParts(value, locale);
1283
+ return $applyDatePattern(format, d, parts);
1284
+ }
1285
+ return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1286
+ },
1287
+
1288
+ time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1289
+ if (format) {
1290
+ const { d, parts } = $parseDateParts(value, locale);
1291
+ return $applyDatePattern(format, d, parts);
1292
+ }
1293
+ return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1294
+ },
1295
+
1296
+ datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1297
+ if (format) {
1298
+ const { d, parts } = $parseDateParts(value, locale);
1299
+ return $applyDatePattern(format, d, parts);
1300
+ }
1301
+ return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1302
+ },
1303
+
1304
+ relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1305
+ const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1306
+ return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1307
+ },
1308
+
1309
+ plural: (value, locale, { singular, plural } = {}) => {
1310
+ const rule = new Intl.PluralRules(locale).select(value);
1311
+ return `${value} ${rule === 'one' ? singular : plural}`;
1312
+ },
1313
+ };
1314
+
1224
1315
  const LocalStorage = {
1225
1316
  getJson(key) {
1226
1317
  let value = localStorage.getItem(key);
@@ -1277,696 +1368,205 @@ var NativeComponents = (function (exports) {
1277
1368
  }
1278
1369
  };
1279
1370
 
1280
- const StoreFactory = function() {
1371
+ /**
1372
+ *
1373
+ * @param {*} value
1374
+ * @param {{ propagation: boolean, reset: boolean} | null} configs
1375
+ * @class ObservableItem
1376
+ */
1377
+ function ObservableItem(value, configs = null) {
1378
+ value = Validator.isObservable(value) ? value.val() : value;
1281
1379
 
1282
- const $stores = new Map();
1283
- const $followersCache = new Map();
1380
+ this.$previousValue = null;
1381
+ this.$currentValue = value;
1284
1382
 
1285
- /**
1286
- * Internal helper — retrieves a store entry or throws if not found.
1287
- */
1288
- const $getStoreOrThrow = (method, name) => {
1289
- const item = $stores.get(name);
1290
- if (!item) {
1291
- DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
1292
- throw new NativeDocumentError(
1293
- `Store.${method}('${name}') : store not found.`
1294
- );
1295
- }
1296
- return item;
1297
- };
1383
+ this.$firstListener = null;
1384
+ this.$listeners = null;
1385
+ this.$watchers = null;
1298
1386
 
1299
- /**
1300
- * Internal helper — blocks write operations on a read-only observer.
1301
- */
1302
- const $applyReadOnly = (observer, name, context) => {
1303
- const readOnlyError = (method) => () => {
1304
- DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
1305
- throw new NativeDocumentError(
1306
- `Store.${context}('${name}') is read-only.`
1307
- );
1308
- };
1309
- observer.set = readOnlyError('set');
1310
- observer.toggle = readOnlyError('toggle');
1311
- observer.reset = readOnlyError('reset');
1312
- };
1387
+ this.$memoryId = null;
1313
1388
 
1314
- const $createObservable = (value, options = {}) => {
1315
- if(Array.isArray(value)) {
1316
- return Observable.array(value, options);
1317
- }
1318
- if(typeof value === 'object') {
1319
- return Observable.object(value, options);
1389
+ if(configs) {
1390
+ this.configs = configs;
1391
+ if(configs.reset) {
1392
+ this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1320
1393
  }
1321
- return Observable(value, options);
1322
- };
1394
+ }
1395
+ }
1323
1396
 
1324
- const $api = {
1325
- /**
1326
- * Create a new state and return the observer.
1327
- * Throws if a store with the same name already exists.
1328
- *
1329
- * @param {string} name
1330
- * @param {*} value
1331
- * @returns {ObservableItem}
1332
- */
1333
- create(name, value) {
1334
- if ($stores.has(name)) {
1335
- DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
1336
- throw new NativeDocumentError(
1337
- `Store.create('${name}') : a store with this name already exists.`
1338
- );
1339
- }
1340
- const observer = $createObservable(value);
1341
- $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
1342
- return observer;
1343
- },
1397
+ Object.defineProperty(ObservableItem.prototype, '$value', {
1398
+ get() {
1399
+ return this.$currentValue;
1400
+ },
1401
+ set(value) {
1402
+ this.set(value);
1403
+ },
1404
+ configurable: true,
1405
+ });
1344
1406
 
1345
- /**
1346
- * Create a new resettable state and return the observer.
1347
- * The store can be reset to its initial value via Store.reset(name).
1348
- * Throws if a store with the same name already exists.
1349
- *
1350
- * @param {string} name
1351
- * @param {*} value
1352
- * @returns {ObservableItem}
1353
- */
1354
- createResettable(name, value) {
1355
- if ($stores.has(name)) {
1356
- DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
1357
- throw new NativeDocumentError(
1358
- `Store.createResettable('${name}') : a store with this name already exists.`
1359
- );
1360
- }
1361
- const observer = $createObservable(value, { reset: true });
1362
- $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
1363
- return observer;
1364
- },
1407
+ ObservableItem.prototype.__$isObservable = true;
1408
+ const noneTrigger = function() {};
1365
1409
 
1366
- /**
1367
- * Create a computed store derived from other stores.
1368
- * The value is automatically recalculated when any dependency changes.
1369
- * This store is read-only — Store.use() and Store.set() will throw.
1370
- * Throws if a store with the same name already exists.
1371
- *
1372
- * @param {string} name
1373
- * @param {() => *} computation - Function that returns the computed value
1374
- * @param {string[]} dependencies - Names of the stores to watch
1375
- * @returns {ObservableItem}
1376
- *
1377
- * @example
1378
- * Store.create('products', [{ id: 1, price: 10 }]);
1379
- * Store.create('cart', [{ productId: 1, quantity: 2 }]);
1380
- *
1381
- * Store.createComposed('total', () => {
1382
- * const products = Store.get('products').val();
1383
- * const cart = Store.get('cart').val();
1384
- * return cart.reduce((sum, item) => {
1385
- * const product = products.find(p => p.id === item.productId);
1386
- * return sum + (product.price * item.quantity);
1387
- * }, 0);
1388
- * }, ['products', 'cart']);
1389
- */
1390
- createComposed(name, computation, dependencies) {
1391
- if ($stores.has(name)) {
1392
- DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
1393
- throw new NativeDocumentError(
1394
- `Store.createComposed('${name}') : a store with this name already exists.`
1395
- );
1396
- }
1397
- if (typeof computation !== 'function') {
1398
- throw new NativeDocumentError(
1399
- `Store.createComposed('${name}') : computation must be a function.`
1400
- );
1401
- }
1402
- if (!Array.isArray(dependencies) || dependencies.length === 0) {
1403
- throw new NativeDocumentError(
1404
- `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
1405
- );
1406
- }
1410
+ /**
1411
+ * Intercepts and transforms values before they are set on the observable.
1412
+ * The interceptor can modify the value or return undefined to use the original value.
1413
+ *
1414
+ * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1415
+ * @returns {ObservableItem} The observable instance for chaining
1416
+ * @example
1417
+ * const count = Observable(0);
1418
+ * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1419
+ */
1420
+ ObservableItem.prototype.intercept = function(callback) {
1421
+ this.$interceptor = callback;
1422
+ this.set = this.$setWithInterceptor;
1423
+ return this;
1424
+ };
1407
1425
 
1408
- // Resolve dependency observers
1409
- const depObservers = dependencies.map(depName => {
1410
- if(typeof depName !== 'string') {
1411
- return depName;
1412
- }
1413
- const depItem = $stores.get(depName);
1414
- if (!depItem) {
1415
- DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
1416
- throw new NativeDocumentError(
1417
- `Store.createComposed('${name}') : dependency store '${depName}' not found.`
1418
- );
1419
- }
1420
- return depItem.observer;
1421
- });
1426
+ ObservableItem.prototype.triggerFirstListener = function(operations) {
1427
+ this.$firstListener(this.$currentValue, this.$previousValue, operations);
1428
+ };
1422
1429
 
1423
- // Create computed observable from dependency observers
1424
- const observer = Observable.computed(computation, depObservers);
1430
+ ObservableItem.prototype.triggerListeners = function(operations) {
1431
+ const $listeners = this.$listeners;
1432
+ const $previousValue = this.$previousValue;
1433
+ const $currentValue = this.$currentValue;
1425
1434
 
1426
- $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
1427
- return observer;
1428
- },
1435
+ for(let i = 0, length = $listeners.length; i < length; i++) {
1436
+ $listeners[i]($currentValue, $previousValue, operations);
1437
+ }
1438
+ };
1429
1439
 
1430
- /**
1431
- * Returns true if a store with the given name exists.
1432
- *
1433
- * @param {string} name
1434
- * @returns {boolean}
1435
- */
1436
- has(name) {
1437
- return $stores.has(name);
1438
- },
1440
+ ObservableItem.prototype.triggerWatchers = function(operations) {
1441
+ const $watchers = this.$watchers;
1442
+ const $previousValue = this.$previousValue;
1443
+ const $currentValue = this.$currentValue;
1439
1444
 
1440
- /**
1441
- * Resets a resettable store to its initial value and notifies all subscribers.
1442
- * Throws if the store was not created with createResettable().
1443
- *
1444
- * @param {string} name
1445
- */
1446
- reset(name) {
1447
- const item = $getStoreOrThrow('reset', name);
1448
- if (item.composed) {
1449
- DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
1450
- throw new NativeDocumentError(
1451
- `Store.reset('${name}') : composed stores cannot be reset.`
1452
- );
1453
- }
1454
- if (!item.resettable) {
1455
- DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
1456
- throw new NativeDocumentError(
1457
- `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
1458
- );
1459
- }
1460
- item.observer.reset();
1461
- },
1445
+ const $currentValueCallbacks = $watchers.get($currentValue);
1446
+ const $previousValueCallbacks = $watchers.get($previousValue);
1447
+ if($currentValueCallbacks) {
1448
+ $currentValueCallbacks(true, $previousValue, operations);
1449
+ }
1450
+ if($previousValueCallbacks) {
1451
+ $previousValueCallbacks(false, $currentValue, operations);
1452
+ }
1453
+ };
1462
1454
 
1463
- /**
1464
- * Returns a two-way synchronized follower of the store.
1465
- * Writing to the follower propagates the value back to the store and all its subscribers.
1466
- * Throws if called on a composed store — use Store.follow() instead.
1467
- * Call follower.destroy() or follower.dispose() to unsubscribe.
1468
- *
1469
- * @param {string} name
1470
- * @returns {ObservableItem}
1471
- */
1472
- use(name) {
1473
- const item = $getStoreOrThrow('use', name);
1455
+ ObservableItem.prototype.triggerAll = function(operations) {
1456
+ this.triggerWatchers(operations);
1457
+ this.triggerListeners(operations);
1458
+ };
1474
1459
 
1475
- if (item.composed) {
1476
- DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
1477
- throw new NativeDocumentError(
1478
- `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
1479
- );
1480
- }
1460
+ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1461
+ this.triggerWatchers(operations);
1462
+ this.triggerFirstListener(operations);
1463
+ };
1481
1464
 
1482
- const { observer: originalObserver, subscribers } = item;
1483
- const observerFollower = $createObservable(originalObserver.val());
1465
+ ObservableItem.prototype.assocTrigger = function() {
1466
+ this.$firstListener = null;
1467
+ if(this.$watchers?.size && this.$listeners?.length) {
1468
+ this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1469
+ return;
1470
+ }
1471
+ if(this.$listeners?.length) {
1472
+ if(this.$listeners.length === 1) {
1473
+ this.$firstListener = this.$listeners[0];
1474
+ this.trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
1475
+ }
1476
+ else {
1477
+ this.trigger = this.triggerListeners;
1478
+ }
1479
+ return;
1480
+ }
1481
+ if(this.$watchers?.size) {
1482
+ this.trigger = this.triggerWatchers;
1483
+ return;
1484
+ }
1485
+ this.trigger = noneTrigger;
1486
+ };
1487
+ ObservableItem.prototype.trigger = noneTrigger;
1484
1488
 
1485
- const onStoreChange = value => observerFollower.set(value);
1486
- const onFollowerChange = value => originalObserver.set(value);
1489
+ ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1490
+ newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1491
+ if(this.$currentValue === newValue) {
1492
+ return;
1493
+ }
1494
+ this.$previousValue = this.$currentValue;
1495
+ this.$currentValue = newValue;
1496
+ this.trigger();
1497
+ this.$previousValue = null;
1498
+ };
1487
1499
 
1488
- originalObserver.subscribe(onStoreChange);
1489
- observerFollower.subscribe(onFollowerChange);
1500
+ /**
1501
+ * @param {*} data
1502
+ */
1503
+ ObservableItem.prototype.$setWithInterceptor = function(data) {
1504
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1505
+ const result = this.$interceptor(newValue, this.$currentValue);
1490
1506
 
1491
- observerFollower.destroy = () => {
1492
- originalObserver.unsubscribe(onStoreChange);
1493
- observerFollower.unsubscribe(onFollowerChange);
1494
- subscribers.delete(observerFollower);
1495
- observerFollower.cleanup();
1496
- };
1497
- observerFollower.dispose = observerFollower.destroy;
1507
+ if (result !== undefined) {
1508
+ newValue = result;
1509
+ }
1498
1510
 
1499
- subscribers.add(observerFollower);
1500
- return observerFollower;
1501
- },
1511
+ this.$updateWithNewValue(newValue);
1512
+ };
1502
1513
 
1503
- /**
1504
- * Returns a read-only follower of the store.
1505
- * The follower reflects store changes but cannot write back to the store.
1506
- * Any attempt to call .set(), .toggle() or .reset() will throw.
1507
- * Call follower.destroy() or follower.dispose() to unsubscribe.
1508
- *
1509
- * @param {string} name
1510
- * @returns {ObservableItem}
1511
- */
1512
- follow(name) {
1513
- const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
1514
- const observerFollower = $createObservable(originalObserver.val());
1514
+ /**
1515
+ * @param {*} data
1516
+ */
1517
+ ObservableItem.prototype.$basicSet = function(data) {
1518
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1519
+ this.$updateWithNewValue(newValue);
1520
+ };
1515
1521
 
1516
- const onStoreChange = value => observerFollower.set(value);
1517
- originalObserver.subscribe(onStoreChange);
1522
+ ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1518
1523
 
1519
- $applyReadOnly(observerFollower, name, 'follow');
1524
+ ObservableItem.prototype.val = function() {
1525
+ return this.$currentValue;
1526
+ };
1520
1527
 
1521
- observerFollower.destroy = () => {
1522
- originalObserver.unsubscribe(onStoreChange);
1523
- subscribers.delete(observerFollower);
1524
- observerFollower.cleanup();
1525
- };
1526
- observerFollower.dispose = observerFollower.destroy;
1528
+ ObservableItem.prototype.disconnectAll = function() {
1529
+ this.$previousValue = null;
1530
+ this.$currentValue = null;
1531
+ this.$listeners = null;
1532
+ this.$watchers = null;
1533
+ this.trigger = noneTrigger;
1534
+ };
1527
1535
 
1528
- subscribers.add(observerFollower);
1529
- return observerFollower;
1530
- },
1536
+ /**
1537
+ * Registers a cleanup callback that will be executed when the observable is cleaned up.
1538
+ * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1539
+ *
1540
+ * @param {Function} callback - Cleanup function to execute on observable disposal
1541
+ * @example
1542
+ * const obs = Observable(0);
1543
+ * obs.onCleanup(() => console.log('Cleaned up!'));
1544
+ * obs.cleanup(); // Logs: "Cleaned up!"
1545
+ */
1546
+ ObservableItem.prototype.onCleanup = function(callback) {
1547
+ this.$cleanupListeners = this.$cleanupListeners ?? [];
1548
+ this.$cleanupListeners.push(callback);
1549
+ };
1531
1550
 
1532
- /**
1533
- * Returns the raw store observer directly (no follower, no cleanup contract).
1534
- * Use this for direct read access when you don't need to unsubscribe.
1535
- * WARNING : mutations on this observer impact all subscribers immediately.
1536
- *
1537
- * @param {string} name
1538
- * @returns {ObservableItem|null}
1539
- */
1540
- get(name) {
1541
- const item = $stores.get(name);
1542
- if (!item) {
1543
- DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
1544
- return null;
1545
- }
1546
- return item.observer;
1547
- },
1551
+ ObservableItem.prototype.cleanup = function() {
1552
+ if (this.$cleanupListeners) {
1553
+ for (let i = 0; i < this.$cleanupListeners.length; i++) {
1554
+ this.$cleanupListeners[i]();
1555
+ }
1556
+ this.$cleanupListeners = null;
1557
+ }
1558
+ MemoryManager.unregister(this.$memoryId);
1559
+ this.disconnectAll();
1560
+ delete this.$value;
1561
+ };
1548
1562
 
1549
- /**
1550
- * @param {string} name
1551
- * @returns {{ observer: ObservableItem, subscribers: Set } | null}
1552
- */
1553
- getWithSubscribers(name) {
1554
- return $stores.get(name) ?? null;
1555
- },
1556
-
1557
- /**
1558
- * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
1559
- *
1560
- * @param {string} name
1561
- */
1562
- delete(name) {
1563
- const item = $stores.get(name);
1564
- if (!item) {
1565
- DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
1566
- return;
1567
- }
1568
- item.subscribers.forEach(follower => follower.destroy());
1569
- item.subscribers.clear();
1570
- item.observer.cleanup();
1571
- $stores.delete(name);
1572
- },
1573
- /**
1574
- * Creates an isolated store group with its own state namespace.
1575
- * Each group is a fully independent StoreFactory instance —
1576
- * no key conflicts, no shared state with the parent store.
1577
- *
1578
- * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
1579
- * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
1580
- * @returns {ReturnType<typeof StoreFactory>}
1581
- *
1582
- * @example
1583
- * // With name (recommended)
1584
- * const EventStore = Store.group('events', (group) => {
1585
- * group.create('catalog', []);
1586
- * group.create('filters', { category: null, date: null });
1587
- * group.createResettable('selected', null);
1588
- * group.createComposed('filtered', () => {
1589
- * const catalog = EventStore.get('catalog').val();
1590
- * const filters = EventStore.get('filters').val();
1591
- * return catalog.filter(event => {
1592
- * if (filters.category && event.category !== filters.category) return false;
1593
- * return true;
1594
- * });
1595
- * }, ['catalog', 'filters']);
1596
- * });
1597
- *
1598
- * // Without name
1599
- * const CartStore = Store.group((group) => {
1600
- * group.create('items', []);
1601
- * });
1602
- *
1603
- * // Usage
1604
- * EventStore.use('catalog'); // two-way follower
1605
- * EventStore.follow('filtered'); // read-only follower
1606
- * EventStore.get('filters'); // raw observable
1607
- *
1608
- * // Cross-group composed
1609
- * const OrderStore = Store.group('orders', (group) => {
1610
- * group.createComposed('summary', () => {
1611
- * const items = CartStore.get('items').val();
1612
- * const events = EventStore.get('catalog').val();
1613
- * return { items, events };
1614
- * }, [CartStore.get('items'), EventStore.get('catalog')]);
1615
- * });
1616
- */
1617
- group(name, callback) {
1618
- if (typeof name === 'function') {
1619
- callback = name;
1620
- name = 'anonymous';
1621
- }
1622
- const store = StoreFactory();
1623
- callback && callback(store);
1624
- return store;
1625
- },
1626
- createPersistent(name, value, localstorage_key) {
1627
- localstorage_key = localstorage_key || name;
1628
- const observer = this.create(name, $getFromStorage(localstorage_key, value));
1629
- const saver = $saveToStorage(value);
1630
-
1631
- observer.subscribe((val) => saver(localstorage_key, val));
1632
- return observer;
1633
- },
1634
- createPersistentResettable(name, value, localstorage_key) {
1635
- localstorage_key = localstorage_key || name;
1636
- const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
1637
- const saver = $saveToStorage(value);
1638
- observer.subscribe((val) => saver(localstorage_key, val));
1639
-
1640
- const originalReset = observer.reset.bind(observer);
1641
- observer.reset = () => {
1642
- LocalStorage.remove(localstorage_key);
1643
- originalReset();
1644
- };
1645
-
1646
- return observer;
1647
- }
1648
- };
1649
-
1650
-
1651
- return new Proxy($api, {
1652
- get(target, prop) {
1653
- if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
1654
- return target[prop];
1655
- }
1656
- if (target.has(prop)) {
1657
- if ($followersCache.has(prop)) {
1658
- return $followersCache.get(prop);
1659
- }
1660
- const follower = target.follow(prop);
1661
- $followersCache.set(prop, follower);
1662
- return follower;
1663
- }
1664
- return undefined;
1665
- },
1666
- set(target, prop, value) {
1667
- DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
1668
- throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
1669
- },
1670
- deleteProperty(target, prop) {
1671
- throw new NativeDocumentError(`Store keys cannot be deleted.`);
1672
- }
1673
- });
1674
- };
1675
-
1676
- const Store = StoreFactory();
1677
-
1678
- Store.create('locale', navigator.language.split('-')[0] || 'en');
1679
-
1680
- const $parseDateParts = (value, locale) => {
1681
- const d = new Date(value);
1682
- return {
1683
- d,
1684
- parts: new Intl.DateTimeFormat(locale, {
1685
- year: 'numeric',
1686
- month: 'long',
1687
- day: '2-digit',
1688
- hour: '2-digit',
1689
- minute: '2-digit',
1690
- second: '2-digit',
1691
- }).formatToParts(d).reduce((acc, { type, value }) => {
1692
- acc[type] = value;
1693
- return acc;
1694
- }, {})
1695
- };
1696
- };
1697
-
1698
- const $applyDatePattern = (pattern, d, parts) => {
1699
- const pad = n => String(n).padStart(2, '0');
1700
- return pattern
1701
- .replace('YYYY', parts.year)
1702
- .replace('YY', parts.year.slice(-2))
1703
- .replace('MMMM', parts.month)
1704
- .replace('MMM', parts.month.slice(0, 3))
1705
- .replace('MM', pad(d.getMonth() + 1))
1706
- .replace('DD', pad(d.getDate()))
1707
- .replace('D', d.getDate())
1708
- .replace('HH', parts.hour)
1709
- .replace('mm', parts.minute)
1710
- .replace('ss', parts.second);
1711
- };
1712
-
1713
- const Formatters = {
1714
- currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1715
- new Intl.NumberFormat(locale, {
1716
- style: 'currency',
1717
- currency,
1718
- notation,
1719
- minimumFractionDigits,
1720
- maximumFractionDigits
1721
- }).format(value),
1722
-
1723
- number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1724
- new Intl.NumberFormat(locale, {
1725
- notation,
1726
- minimumFractionDigits,
1727
- maximumFractionDigits
1728
- }).format(value),
1729
-
1730
- percent: (value, locale, { decimals = 1 } = {}) =>
1731
- new Intl.NumberFormat(locale, {
1732
- style: 'percent',
1733
- maximumFractionDigits: decimals
1734
- }).format(value),
1735
-
1736
- date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1737
- if (format) {
1738
- const { d, parts } = $parseDateParts(value, locale);
1739
- return $applyDatePattern(format, d, parts);
1740
- }
1741
- return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1742
- },
1743
-
1744
- time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1745
- if (format) {
1746
- const { d, parts } = $parseDateParts(value, locale);
1747
- return $applyDatePattern(format, d, parts);
1748
- }
1749
- return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1750
- },
1751
-
1752
- datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1753
- if (format) {
1754
- const { d, parts } = $parseDateParts(value, locale);
1755
- return $applyDatePattern(format, d, parts);
1756
- }
1757
- return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1758
- },
1759
-
1760
- relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1761
- const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1762
- return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1763
- },
1764
-
1765
- plural: (value, locale, { singular, plural } = {}) => {
1766
- const rule = new Intl.PluralRules(locale).select(value);
1767
- return `${value} ${rule === 'one' ? singular : plural}`;
1768
- },
1769
- };
1770
-
1771
- /**
1772
- *
1773
- * @param {*} value
1774
- * @param {{ propagation: boolean, reset: boolean} | null} configs
1775
- * @class ObservableItem
1776
- */
1777
- function ObservableItem(value, configs = null) {
1778
- value = Validator.isObservable(value) ? value.val() : value;
1779
-
1780
- this.$previousValue = null;
1781
- this.$currentValue = value;
1782
-
1783
- this.$firstListener = null;
1784
- this.$listeners = null;
1785
- this.$watchers = null;
1786
-
1787
- this.$memoryId = null;
1788
-
1789
- if(configs) {
1790
- this.configs = configs;
1791
- if(configs.reset) {
1792
- this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1793
- }
1794
- }
1795
- }
1796
-
1797
- Object.defineProperty(ObservableItem.prototype, '$value', {
1798
- get() {
1799
- return this.$currentValue;
1800
- },
1801
- set(value) {
1802
- this.set(value);
1803
- },
1804
- configurable: true,
1805
- });
1806
-
1807
- ObservableItem.prototype.__$isObservable = true;
1808
- const noneTrigger = function() {};
1809
-
1810
- /**
1811
- * Intercepts and transforms values before they are set on the observable.
1812
- * The interceptor can modify the value or return undefined to use the original value.
1813
- *
1814
- * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1815
- * @returns {ObservableItem} The observable instance for chaining
1816
- * @example
1817
- * const count = Observable(0);
1818
- * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1819
- */
1820
- ObservableItem.prototype.intercept = function(callback) {
1821
- this.$interceptor = callback;
1822
- this.set = this.$setWithInterceptor;
1823
- return this;
1824
- };
1825
-
1826
- ObservableItem.prototype.triggerFirstListener = function(operations) {
1827
- this.$firstListener(this.$currentValue, this.$previousValue, operations);
1828
- };
1829
-
1830
- ObservableItem.prototype.triggerListeners = function(operations) {
1831
- const $listeners = this.$listeners;
1832
- const $previousValue = this.$previousValue;
1833
- const $currentValue = this.$currentValue;
1834
-
1835
- for(let i = 0, length = $listeners.length; i < length; i++) {
1836
- $listeners[i]($currentValue, $previousValue, operations);
1837
- }
1838
- };
1839
-
1840
- ObservableItem.prototype.triggerWatchers = function(operations) {
1841
- const $watchers = this.$watchers;
1842
- const $previousValue = this.$previousValue;
1843
- const $currentValue = this.$currentValue;
1844
-
1845
- const $currentValueCallbacks = $watchers.get($currentValue);
1846
- const $previousValueCallbacks = $watchers.get($previousValue);
1847
- if($currentValueCallbacks) {
1848
- $currentValueCallbacks(true, $previousValue, operations);
1849
- }
1850
- if($previousValueCallbacks) {
1851
- $previousValueCallbacks(false, $currentValue, operations);
1852
- }
1853
- };
1854
-
1855
- ObservableItem.prototype.triggerAll = function(operations) {
1856
- this.triggerWatchers(operations);
1857
- this.triggerListeners(operations);
1858
- };
1859
-
1860
- ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1861
- this.triggerWatchers(operations);
1862
- this.triggerFirstListener(operations);
1863
- };
1864
-
1865
- ObservableItem.prototype.assocTrigger = function() {
1866
- this.$firstListener = null;
1867
- if(this.$watchers?.size && this.$listeners?.length) {
1868
- this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1869
- return;
1870
- }
1871
- if(this.$listeners?.length) {
1872
- if(this.$listeners.length === 1) {
1873
- this.$firstListener = this.$listeners[0];
1874
- this.trigger = this.triggerFirstListener;
1875
- }
1876
- else {
1877
- this.trigger = this.triggerListeners;
1878
- }
1879
- return;
1880
- }
1881
- if(this.$watchers?.size) {
1882
- this.trigger = this.triggerWatchers;
1883
- return;
1884
- }
1885
- this.trigger = noneTrigger;
1886
- };
1887
- ObservableItem.prototype.trigger = noneTrigger;
1888
-
1889
- ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1890
- newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1891
- if(this.$currentValue === newValue) {
1892
- return;
1893
- }
1894
- this.$previousValue = this.$currentValue;
1895
- this.$currentValue = newValue;
1896
- this.trigger();
1897
- this.$previousValue = null;
1898
- };
1899
-
1900
- /**
1901
- * @param {*} data
1902
- */
1903
- ObservableItem.prototype.$setWithInterceptor = function(data) {
1904
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1905
- const result = this.$interceptor(newValue, this.$currentValue);
1906
-
1907
- if (result !== undefined) {
1908
- newValue = result;
1909
- }
1910
-
1911
- this.$updateWithNewValue(newValue);
1912
- };
1913
-
1914
- /**
1915
- * @param {*} data
1916
- */
1917
- ObservableItem.prototype.$basicSet = function(data) {
1918
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1919
- this.$updateWithNewValue(newValue);
1920
- };
1921
-
1922
- ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1923
-
1924
- ObservableItem.prototype.val = function() {
1925
- return this.$currentValue;
1926
- };
1927
-
1928
- ObservableItem.prototype.disconnectAll = function() {
1929
- this.$previousValue = null;
1930
- this.$currentValue = null;
1931
- this.$listeners = null;
1932
- this.$watchers = null;
1933
- this.trigger = noneTrigger;
1934
- };
1935
-
1936
- /**
1937
- * Registers a cleanup callback that will be executed when the observable is cleaned up.
1938
- * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1939
- *
1940
- * @param {Function} callback - Cleanup function to execute on observable disposal
1941
- * @example
1942
- * const obs = Observable(0);
1943
- * obs.onCleanup(() => console.log('Cleaned up!'));
1944
- * obs.cleanup(); // Logs: "Cleaned up!"
1945
- */
1946
- ObservableItem.prototype.onCleanup = function(callback) {
1947
- this.$cleanupListeners = this.$cleanupListeners ?? [];
1948
- this.$cleanupListeners.push(callback);
1949
- };
1950
-
1951
- ObservableItem.prototype.cleanup = function() {
1952
- if (this.$cleanupListeners) {
1953
- for (let i = 0; i < this.$cleanupListeners.length; i++) {
1954
- this.$cleanupListeners[i]();
1955
- }
1956
- this.$cleanupListeners = null;
1957
- }
1958
- MemoryManager.unregister(this.$memoryId);
1959
- this.disconnectAll();
1960
- delete this.$value;
1961
- };
1962
-
1963
- /**
1964
- *
1965
- * @param {Function} callback
1966
- * @returns {(function(): void)}
1967
- */
1968
- ObservableItem.prototype.subscribe = function(callback) {
1969
- this.$listeners = this.$listeners ?? [];
1563
+ /**
1564
+ *
1565
+ * @param {Function} callback
1566
+ * @returns {(function(): void)}
1567
+ */
1568
+ ObservableItem.prototype.subscribe = function(callback) {
1569
+ this.$listeners = this.$listeners ?? [];
1970
1570
 
1971
1571
  this.$listeners.push(callback);
1972
1572
  this.assocTrigger();
@@ -2384,7 +1984,6 @@ var NativeComponents = (function (exports) {
2384
1984
  }
2385
1985
  element.classes.toggle(className, value);
2386
1986
  }
2387
- data = null;
2388
1987
  };
2389
1988
 
2390
1989
  /**
@@ -3298,35 +2897,78 @@ var NativeComponents = (function (exports) {
3298
2897
  }
3299
2898
  const steps = [];
3300
2899
  if(this.$ndMethods) {
3301
- steps.push((clonedNode, data) => {
3302
- for(const methodName in this.$ndMethods) {
2900
+ const methods = Object.keys(this.$ndMethods);
2901
+ if(methods.length === 1) {
2902
+ const methodName = methods[0];
2903
+ steps.push((clonedNode, data) => {
3303
2904
  clonedNode.nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
3304
- }
3305
- });
2905
+ });
2906
+ } else {
2907
+ steps.push((clonedNode, data) => {
2908
+ const nd = clonedNode.nd;
2909
+ for(const methodName in this.$ndMethods) {
2910
+ nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
2911
+ }
2912
+ });
2913
+ }
3306
2914
  }
3307
2915
  if(this.$classes) {
3308
2916
  const cache = {};
3309
- steps.push((clonedNode, data) => {
3310
- ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
3311
- });
2917
+ const keys = Object.keys(this.$classes);
2918
+
2919
+ if(keys.length === 1) {
2920
+ const key = keys[0];
2921
+ const callback = this.$classes[key];
2922
+ steps.push((clonedNode, data) => {
2923
+ cache[key] = callback.apply(null, data);
2924
+ ElementCreator.processClassAttribute(clonedNode, cache);
2925
+ });
2926
+ } else {
2927
+ steps.push((clonedNode, data) => {
2928
+ ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
2929
+ });
2930
+ }
3312
2931
  }
3313
2932
  if(this.$styles) {
3314
2933
  const cache = {};
3315
- steps.push((clonedNode, data) => {
3316
- ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
3317
- });
2934
+ const keys = Object.keys(this.$styles);
2935
+
2936
+ if(keys.length === 1) {
2937
+ const key = keys[0];
2938
+ const callback = this.$styles[key];
2939
+ steps.push((clonedNode, data) => {
2940
+ cache[key] = callback.apply(null, data);
2941
+ ElementCreator.processStyleAttribute(clonedNode, cache);
2942
+ });
2943
+ } else {
2944
+ steps.push((clonedNode, data) => {
2945
+ ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
2946
+ });
2947
+ }
3318
2948
  }
3319
2949
  if(this.$attrs) {
3320
2950
  const cache = {};
3321
- steps.push((clonedNode, data) => {
3322
- ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
3323
- });
2951
+ const keys = Object.keys(this.$attrs);
2952
+
2953
+ if(keys.length === 1) {
2954
+ const key = keys[0];
2955
+ const callback = this.$attrs[key];
2956
+ steps.push((clonedNode, data) => {
2957
+ cache[key] = callback.apply(null, data);
2958
+ ElementCreator.processAttributes(clonedNode, cache);
2959
+ });
2960
+ } else {
2961
+ steps.push((clonedNode, data) => {
2962
+ ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
2963
+ });
2964
+ }
3324
2965
  }
3325
2966
 
3326
2967
  const stepsCount = steps.length;
2968
+ const $element = this.$element;
3327
2969
 
3328
- this.cloneNode = function(data) {
3329
- const clonedNode = this.$element.cloneNode(false);
2970
+ this.cloneNode = (data) => {
2971
+ const clonedNode = $element.cloneNode(false);
3330
2972
  for(let i = 0; i < stepsCount; i++) {
3331
2973
  steps[i](clonedNode, data);
3332
2974
  }
@@ -3338,14 +2980,6 @@ var NativeComponents = (function (exports) {
3338
2980
  return this.$element.cloneNode(false);
3339
2981
  };
3340
2982
 
3341
- NodeCloner.prototype.cloneTextNodeByProperty = function(data) {
3342
- return createTextNode(data[0][this.$content]);
3343
- };
3344
-
3345
- NodeCloner.prototype.cloneTextNodeByCallback = function(data) {
3346
- return createTextNode(this.$content.apply(null, data));
3347
- };
3348
-
3349
2983
  NodeCloner.prototype.attach = function(methodName, callback) {
3350
2984
  this.$ndMethods = this.$ndMethods || {};
3351
2985
  this.$ndMethods[methodName] = callback;
@@ -3355,10 +2989,10 @@ var NativeComponents = (function (exports) {
3355
2989
  NodeCloner.prototype.text = function(value) {
3356
2990
  this.$content = value;
3357
2991
  if(typeof value === 'function') {
3358
- this.cloneNode = NodeCloner.prototype.cloneTextNodeByCallback;
2992
+ this.cloneNode = (data) => createTextNode(value.apply(null, data));
3359
2993
  return this;
3360
2994
  }
3361
- this.cloneNode = NodeCloner.prototype.cloneTextNodeByProperty;
2995
+ this.cloneNode = (data) => createTextNode(data[0][value]);
3362
2996
  return this;
3363
2997
  };
3364
2998
 
@@ -3648,500 +3282,900 @@ var NativeComponents = (function (exports) {
3648
3282
  };
3649
3283
 
3650
3284
  /**
3651
- * Checks if the array is empty.
3285
+ * Checks if the array is empty.
3286
+ *
3287
+ * @returns {boolean} True if array has no elements
3288
+ * @example
3289
+ * const items = Observable.array([]);
3290
+ * items.isEmpty(); // true
3291
+ */
3292
+ ObservableArray.prototype.isEmpty = function() {
3293
+ return this.$currentValue.length === 0;
3294
+ };
3295
+
3296
+ /**
3297
+ * Triggers a populate operation with the current array, iteration count, and callback.
3298
+ * Used internally for rendering optimizations.
3299
+ *
3300
+ * @param {number} iteration - Iteration count for rendering
3301
+ * @param {Function} callback - Callback function for rendering items
3302
+ */
3303
+ ObservableArray.prototype.populateAndRender = function(iteration, callback) {
3304
+ this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
3305
+ };
3306
+
3307
+
3308
+ /**
3309
+ * Creates a filtered view of the array based on predicates.
3310
+ * The filtered array updates automatically when source data or predicates change.
3311
+ *
3312
+ * @param {Object} predicates - Object mapping property names to filter conditions or functions
3313
+ * @returns {ObservableArray} A new observable array containing filtered items
3314
+ * @example
3315
+ * const users = Observable.array([
3316
+ * { name: 'John', age: 25 },
3317
+ * { name: 'Jane', age: 30 }
3318
+ * ]);
3319
+ * const adults = users.where({ age: (val) => val >= 18 });
3320
+ */
3321
+ ObservableArray.prototype.where = function(predicates) {
3322
+ const sourceArray = this;
3323
+ const observableDependencies = [sourceArray];
3324
+ const filterCallbacks = {};
3325
+
3326
+ for (const [key, rawPredicate] of Object.entries(predicates)) {
3327
+ const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
3328
+ if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
3329
+ filterCallbacks[key] = predicate.callback;
3330
+
3331
+ if (predicate.dependencies) {
3332
+ const deps = Array.isArray(predicate.dependencies)
3333
+ ? predicate.dependencies
3334
+ : [predicate.dependencies];
3335
+ observableDependencies.push.apply(observableDependencies, deps);
3336
+ }
3337
+ } else if(typeof predicate === 'function') {
3338
+ filterCallbacks[key] = predicate;
3339
+ } else {
3340
+ filterCallbacks[key] = (value) => value === predicate;
3341
+ }
3342
+ }
3343
+
3344
+ const viewArray = Observable.array();
3345
+
3346
+ const filters = Object.entries(filterCallbacks);
3347
+ const updateView = () => {
3348
+ const filtered = sourceArray.val().filter(item => {
3349
+ for (const [key, callback] of filters) {
3350
+ if(key === '_') {
3351
+ if (!callback(item)) return false;
3352
+ } else {
3353
+ if (!callback(item[key])) return false;
3354
+ }
3355
+ }
3356
+ return true;
3357
+ });
3358
+
3359
+ viewArray.set(filtered);
3360
+ };
3361
+
3362
+ observableDependencies.forEach(dep => dep.subscribe(updateView));
3363
+
3364
+ updateView();
3365
+
3366
+ return viewArray;
3367
+ };
3368
+
3369
+ /**
3370
+ * Creates a filtered view where at least one of the specified fields matches the filter.
3371
+ *
3372
+ * @param {Array<string>} fields - Array of field names to check
3373
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
3374
+ * @returns {ObservableArray} A new observable array containing filtered items
3375
+ * @example
3376
+ * const products = Observable.array([
3377
+ * { name: 'Apple', category: 'Fruit' },
3378
+ * { name: 'Carrot', category: 'Vegetable' }
3379
+ * ]);
3380
+ * const searchTerm = Observable('App');
3381
+ * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
3382
+ */
3383
+ ObservableArray.prototype.whereSome = function(fields, filter) {
3384
+ return this.where({
3385
+ _: {
3386
+ dependencies: filter.dependencies,
3387
+ callback: (item) => fields.some(field => filter.callback(item[field]))
3388
+ }
3389
+ });
3390
+ };
3391
+
3392
+ /**
3393
+ * Creates a filtered view where all specified fields match the filter.
3394
+ *
3395
+ * @param {Array<string>} fields - Array of field names to check
3396
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
3397
+ * @returns {ObservableArray} A new observable array containing filtered items
3398
+ * @example
3399
+ * const items = Observable.array([
3400
+ * { status: 'active', verified: true },
3401
+ * { status: 'active', verified: false }
3402
+ * ]);
3403
+ * const activeFilter = equals('active');
3404
+ * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
3405
+ */
3406
+ ObservableArray.prototype.whereEvery = function(fields, filter) {
3407
+ return this.where({
3408
+ _: {
3409
+ dependencies: filter.dependencies,
3410
+ callback: (item) => fields.every(field => filter.callback(item[field]))
3411
+ }
3412
+ });
3413
+ };
3414
+
3415
+ ObservableArray.prototype.deepSubscribe = function(callback) {
3416
+ const updatedValue = nextTick(() => callback(this.val()));
3417
+ const $listeners = new WeakMap();
3418
+
3419
+ const bindItem = (item) => {
3420
+ if ($listeners.has(item)) {
3421
+ return;
3422
+ }
3423
+ if (item?.__$isObservableArray) {
3424
+ $listeners.set(item, item.deepSubscribe(updatedValue));
3425
+ return;
3426
+ }
3427
+ if (item?.__$isObservable) {
3428
+ item.subscribe(updatedValue);
3429
+ $listeners.set(item, () => item.unsubscribe(updatedValue));
3430
+ }
3431
+ };
3432
+
3433
+ const unbindItem = (item) => {
3434
+ const unsub = $listeners.get(item);
3435
+ if (unsub) {
3436
+ unsub();
3437
+ $listeners.delete(item);
3438
+ }
3439
+ };
3440
+
3441
+ this.$currentValue.forEach(bindItem);
3442
+ this.subscribe(updatedValue);
3443
+
3444
+ this.subscribe((items, _, operations) => {
3445
+ switch (operations?.action) {
3446
+ case 'push':
3447
+ case 'unshift':
3448
+ operations.args.forEach(bindItem);
3449
+ break;
3450
+
3451
+ case 'splice': {
3452
+ const [start, deleteCount, ...newItems] = operations.args;
3453
+ operations.result?.forEach(unbindItem);
3454
+ newItems.forEach(bindItem);
3455
+ break;
3456
+ }
3457
+
3458
+ case 'remove':
3459
+ unbindItem(operations.result);
3460
+ break;
3461
+
3462
+ case 'merge':
3463
+ operations.args.forEach(bindItem);
3464
+ break;
3465
+
3466
+ case 'clear':
3467
+ this.$currentValue.forEach(unbindItem);
3468
+ break;
3469
+ }
3470
+ });
3471
+
3472
+ return () => {
3473
+ this.$currentValue.forEach(unbindItem);
3474
+ };
3475
+ };
3476
+
3477
+ /**
3478
+ * Creates an observable array with reactive array methods.
3479
+ * All mutations trigger updates automatically.
3480
+ *
3481
+ * @param {Array} [target=[]] - Initial array value
3482
+ * @param {Object|null} [configs=null] - Configuration options
3483
+ * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
3484
+ * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
3485
+ * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
3486
+ * @returns {ObservableArray} An observable array with reactive methods
3487
+ * @example
3488
+ * const items = Observable.array([1, 2, 3]);
3489
+ * items.push(4); // Triggers update
3490
+ * items.subscribe((arr) => console.log(arr));
3491
+ */
3492
+ Observable.array = function(target = [], configs = null) {
3493
+ return new ObservableArray(target, configs);
3494
+ };
3495
+
3496
+ /**
3652
3497
  *
3653
- * @returns {boolean} True if array has no elements
3654
- * @example
3655
- * const items = Observable.array([]);
3656
- * items.isEmpty(); // true
3498
+ * @param {Function} callback
3499
+ * @returns {Function}
3657
3500
  */
3658
- ObservableArray.prototype.isEmpty = function() {
3659
- return this.$currentValue.length === 0;
3501
+ Observable.batch = function(callback) {
3502
+ const $observer = Observable(0);
3503
+ const batch = function() {
3504
+ if(Validator.isAsyncFunction(callback)) {
3505
+ return (callback(...arguments)).then(() => {
3506
+ $observer.trigger();
3507
+ }).catch(error => { throw error; });
3508
+ }
3509
+ callback(...arguments);
3510
+ $observer.trigger();
3511
+ };
3512
+ batch.$observer = $observer;
3513
+ return batch;
3660
3514
  };
3661
3515
 
3662
- /**
3663
- * Triggers a populate operation with the current array, iteration count, and callback.
3664
- * Used internally for rendering optimizations.
3665
- *
3666
- * @param {number} iteration - Iteration count for rendering
3667
- * @param {Function} callback - Callback function for rendering items
3668
- */
3669
- ObservableArray.prototype.populateAndRender = function(iteration, callback) {
3670
- this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
3516
+ const ObservableObject = function(target, configs) {
3517
+ ObservableItem.call(this, target);
3518
+ this.$observables = {};
3519
+ this.configs = configs;
3520
+
3521
+ this.$load(target);
3522
+
3523
+ for(const name in target) {
3524
+ if(!Object.hasOwn(this, name)) {
3525
+ Object.defineProperty(this, name, {
3526
+ get: () => this.$observables[name],
3527
+ set: (value) => this.$observables[name].set(value)
3528
+ });
3529
+ }
3530
+ }
3531
+
3671
3532
  };
3672
3533
 
3534
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
3673
3535
 
3674
- /**
3675
- * Creates a filtered view of the array based on predicates.
3676
- * The filtered array updates automatically when source data or predicates change.
3677
- *
3678
- * @param {Object} predicates - Object mapping property names to filter conditions or functions
3679
- * @returns {ObservableArray} A new observable array containing filtered items
3680
- * @example
3681
- * const users = Observable.array([
3682
- * { name: 'John', age: 25 },
3683
- * { name: 'Jane', age: 30 }
3684
- * ]);
3685
- * const adults = users.where({ age: (val) => val >= 18 });
3686
- */
3687
- ObservableArray.prototype.where = function(predicates) {
3688
- const sourceArray = this;
3689
- const observableDependencies = [sourceArray];
3690
- const filterCallbacks = {};
3536
+ Object.defineProperty(ObservableObject, '$value', {
3537
+ get() {
3538
+ return this.val();
3539
+ },
3540
+ set(value) {
3541
+ this.set(value);
3542
+ }
3543
+ });
3691
3544
 
3692
- for (const [key, rawPredicate] of Object.entries(predicates)) {
3693
- const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
3694
- if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
3695
- filterCallbacks[key] = predicate.callback;
3545
+ ObservableObject.prototype.__$isObservableObject = true;
3546
+ ObservableObject.prototype.__isProxy__ = true;
3696
3547
 
3697
- if (predicate.dependencies) {
3698
- const deps = Array.isArray(predicate.dependencies)
3699
- ? predicate.dependencies
3700
- : [predicate.dependencies];
3701
- observableDependencies.push.apply(observableDependencies, deps);
3548
+ ObservableObject.prototype.$load = function(initialValue) {
3549
+ const configs = this.configs;
3550
+ for(const key in initialValue) {
3551
+ const itemValue = initialValue[key];
3552
+ if(Array.isArray(itemValue)) {
3553
+ if(configs?.deep !== false) {
3554
+ const mappedItemValue = itemValue.map(item => {
3555
+ if(Validator.isJson(item)) {
3556
+ return Observable.json(item, configs);
3557
+ }
3558
+ if(Validator.isArray(item)) {
3559
+ return Observable.array(item, configs);
3560
+ }
3561
+ return Observable(item, configs);
3562
+ });
3563
+ this.$observables[key] = Observable.array(mappedItemValue, configs);
3564
+ continue;
3702
3565
  }
3703
- } else if(typeof predicate === 'function') {
3704
- filterCallbacks[key] = predicate;
3566
+ this.$observables[key] = Observable.array(itemValue, configs);
3567
+ continue;
3568
+ }
3569
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3570
+ this.$observables[key] = itemValue;
3571
+ continue;
3572
+ }
3573
+ this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
3574
+ }
3575
+ };
3576
+
3577
+ ObservableObject.prototype.val = function() {
3578
+ const result = {};
3579
+ for(const key in this.$observables) {
3580
+ const dataItem = this.$observables[key];
3581
+ if(Validator.isObservable(dataItem)) {
3582
+ let value = dataItem.val();
3583
+ if(Array.isArray(value)) {
3584
+ value = value.map(item => {
3585
+ if(Validator.isObservable(item)) {
3586
+ return item.val();
3587
+ }
3588
+ if(Validator.isProxy(item)) {
3589
+ return item.$value;
3590
+ }
3591
+ return item;
3592
+ });
3593
+ }
3594
+ result[key] = value;
3595
+ } else if(Validator.isProxy(dataItem)) {
3596
+ result[key] = dataItem.$value;
3705
3597
  } else {
3706
- filterCallbacks[key] = (value) => value === predicate;
3598
+ result[key] = dataItem;
3707
3599
  }
3708
3600
  }
3601
+ return result;
3602
+ };
3603
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
3709
3604
 
3710
- const viewArray = Observable.array();
3605
+ ObservableObject.prototype.get = function(property) {
3606
+ const item = this.$observables[property];
3607
+ if(Validator.isObservable(item)) {
3608
+ return item.val();
3609
+ }
3610
+ if(Validator.isProxy(item)) {
3611
+ return item.$value;
3612
+ }
3613
+ return item;
3614
+ };
3615
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
3711
3616
 
3712
- const filters = Object.entries(filterCallbacks);
3713
- const updateView = () => {
3714
- const filtered = sourceArray.val().filter(item => {
3715
- for (const [key, callback] of filters) {
3716
- if(key === '_') {
3717
- if (!callback(item)) return false;
3718
- } else {
3719
- if (!callback(item[key])) return false;
3720
- }
3721
- }
3722
- return true;
3723
- });
3617
+ ObservableObject.prototype.set = function(newData) {
3618
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
3619
+ const configs = this.configs;
3724
3620
 
3725
- viewArray.set(filtered);
3726
- };
3621
+ for(const key in data) {
3622
+ const targetItem = this.$observables[key];
3623
+ const newValueOrigin = newData[key];
3624
+ const newValue = data[key];
3727
3625
 
3728
- observableDependencies.forEach(dep => dep.subscribe(updateView));
3626
+ if(Validator.isObservable(targetItem)) {
3627
+ if(!Validator.isArray(newValue)) {
3628
+ targetItem.set(newValue);
3629
+ continue;
3630
+ }
3631
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
3632
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3633
+ const newValues = newValue.map(item => {
3634
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
3635
+ return Observable.init(item, configs);
3636
+ }
3637
+ return Observable(item, configs);
3638
+ });
3639
+ targetItem.set(newValues);
3640
+ continue;
3641
+ }
3642
+ targetItem.set([...newValue]);
3643
+ continue;
3644
+ }
3645
+ if(Validator.isProxy(targetItem)) {
3646
+ targetItem.update(newValue);
3647
+ continue;
3648
+ }
3649
+ this[key] = newValue;
3650
+ }
3651
+ };
3652
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
3653
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
3729
3654
 
3730
- updateView();
3655
+ ObservableObject.prototype.observables = function() {
3656
+ return Object.values(this.$observables);
3657
+ };
3658
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
3731
3659
 
3732
- return viewArray;
3660
+ ObservableObject.prototype.keys = function() {
3661
+ return Object.keys(this.$observables);
3662
+ };
3663
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
3664
+ ObservableObject.prototype.clone = function() {
3665
+ return Observable.init(this.val(), this.configs);
3666
+ };
3667
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
3668
+ ObservableObject.prototype.reset = function() {
3669
+ for(const key in this.$observables) {
3670
+ this.$observables[key].reset();
3671
+ }
3733
3672
  };
3673
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
3674
+ ObservableObject.prototype.subscribe = function(callback) {
3675
+ const observables = this.observables();
3676
+ const updatedValue = nextTick(() => this.trigger());
3734
3677
 
3735
- /**
3736
- * Creates a filtered view where at least one of the specified fields matches the filter.
3737
- *
3738
- * @param {Array<string>} fields - Array of field names to check
3739
- * @param {FilterResult} filter - Filter condition with callback and dependencies
3740
- * @returns {ObservableArray} A new observable array containing filtered items
3741
- * @example
3742
- * const products = Observable.array([
3743
- * { name: 'Apple', category: 'Fruit' },
3744
- * { name: 'Carrot', category: 'Vegetable' }
3745
- * ]);
3746
- * const searchTerm = Observable('App');
3747
- * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
3748
- */
3749
- ObservableArray.prototype.whereSome = function(fields, filter) {
3750
- return this.where({
3751
- _: {
3752
- dependencies: filter.dependencies,
3753
- callback: (item) => fields.some(field => filter.callback(item[field]))
3678
+ this.originalSubscribe(callback);
3679
+
3680
+ for (let i = 0, length = observables.length; i < length; i++) {
3681
+ const observable = observables[i];
3682
+ if (observable.__$isObservableArray) {
3683
+ observable.deepSubscribe(updatedValue);
3684
+ continue
3754
3685
  }
3755
- });
3686
+ observable.subscribe(updatedValue);
3687
+ }
3688
+ };
3689
+ ObservableObject.prototype.configs = function() {
3690
+ return this.configs;
3691
+ };
3692
+
3693
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
3694
+
3695
+ Observable.init = function(initialValue, configs = null) {
3696
+ return new ObservableObject(initialValue, configs)
3756
3697
  };
3757
3698
 
3758
3699
  /**
3759
- * Creates a filtered view where all specified fields match the filter.
3760
3700
  *
3761
- * @param {Array<string>} fields - Array of field names to check
3762
- * @param {FilterResult} filter - Filter condition with callback and dependencies
3763
- * @returns {ObservableArray} A new observable array containing filtered items
3764
- * @example
3765
- * const items = Observable.array([
3766
- * { status: 'active', verified: true },
3767
- * { status: 'active', verified: false }
3768
- * ]);
3769
- * const activeFilter = equals('active');
3770
- * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
3701
+ * @param {any[]} data
3702
+ * @return Proxy[]
3771
3703
  */
3772
- ObservableArray.prototype.whereEvery = function(fields, filter) {
3773
- return this.where({
3774
- _: {
3775
- dependencies: filter.dependencies,
3776
- callback: (item) => fields.every(field => filter.callback(item[field]))
3704
+ Observable.arrayOfObject = function(data) {
3705
+ return data.map(item => Observable.object(item));
3706
+ };
3707
+
3708
+ /**
3709
+ * Get the value of an observable or an object of observables.
3710
+ * @param {ObservableItem|Object<ObservableItem>} data
3711
+ * @returns {{}|*|null}
3712
+ */
3713
+ Observable.value = function(data) {
3714
+ if(Validator.isObservable(data)) {
3715
+ return data.val();
3716
+ }
3717
+ if(Validator.isProxy(data)) {
3718
+ return data.$value;
3719
+ }
3720
+ if(Validator.isArray(data)) {
3721
+ const result = [];
3722
+ for(let i = 0, length = data.length; i < length; i++) {
3723
+ const item = data[i];
3724
+ result.push(Observable.value(item));
3777
3725
  }
3778
- });
3726
+ return result;
3727
+ }
3728
+ return data;
3779
3729
  };
3780
3730
 
3781
- ObservableArray.prototype.deepSubscribe = function(callback) {
3782
- const updatedValue = nextTick(() => callback(this.val()));
3783
- const $listeners = new WeakMap();
3731
+ Observable.object = Observable.init;
3732
+ Observable.json = Observable.init;
3784
3733
 
3785
- const bindItem = (item) => {
3786
- if ($listeners.has(item)) {
3787
- return;
3788
- }
3789
- if (item?.__$isObservableArray) {
3790
- $listeners.set(item, item.deepSubscribe(updatedValue));
3791
- return;
3792
- }
3793
- if (item?.__$isObservable) {
3794
- item.subscribe(updatedValue);
3795
- $listeners.set(item, () => item.unsubscribe(updatedValue));
3796
- }
3797
- };
3734
+ /**
3735
+ * Creates a computed observable that automatically updates when its dependencies change.
3736
+ * The callback is re-executed whenever any dependency observable changes.
3737
+ *
3738
+ * @param {Function} callback - Function that returns the computed value
3739
+ * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
3740
+ * @returns {ObservableItem} A new observable that updates automatically
3741
+ * @example
3742
+ * const firstName = Observable('John');
3743
+ * const lastName = Observable('Doe');
3744
+ * const fullName = Observable.computed(
3745
+ * () => `${firstName.val()} ${lastName.val()}`,
3746
+ * [firstName, lastName]
3747
+ * );
3748
+ *
3749
+ * // With batch function
3750
+ * const batch = Observable.batch(() => { ... });
3751
+ * const computed = Observable.computed(() => { ... }, batch);
3752
+ */
3753
+ Observable.computed = function(callback, dependencies = []) {
3754
+ const initialValue = callback();
3755
+ const observable = new ObservableItem(initialValue);
3756
+ const updatedValue = nextTick(() => observable.set(callback()));
3798
3757
 
3799
- const unbindItem = (item) => {
3800
- const unsub = $listeners.get(item);
3801
- if (unsub) {
3802
- unsub();
3803
- $listeners.delete(item);
3758
+ if(Validator.isFunction(dependencies)) {
3759
+ if(!Validator.isObservable(dependencies.$observer)) {
3760
+ throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
3804
3761
  }
3805
- };
3806
-
3807
- this.$currentValue.forEach(bindItem);
3808
- this.subscribe(updatedValue);
3762
+ dependencies.$observer.subscribe(updatedValue);
3763
+ return observable;
3764
+ }
3809
3765
 
3810
- this.subscribe((items, _, operations) => {
3811
- switch (operations?.action) {
3812
- case 'push':
3813
- case 'unshift':
3814
- operations.args.forEach(bindItem);
3815
- break;
3766
+ dependencies.forEach(dependency => {
3767
+ if(Validator.isProxy(dependency)) {
3768
+ dependency.$observables.forEach((observable) => {
3769
+ observable.subscribe(updatedValue);
3770
+ });
3771
+ return;
3772
+ }
3773
+ dependency.subscribe(updatedValue);
3774
+ });
3816
3775
 
3817
- case 'splice': {
3818
- const [start, deleteCount, ...newItems] = operations.args;
3819
- operations.result?.forEach(unbindItem);
3820
- newItems.forEach(bindItem);
3821
- break;
3822
- }
3776
+ return observable;
3777
+ };
3823
3778
 
3824
- case 'remove':
3825
- unbindItem(operations.result);
3826
- break;
3779
+ const StoreFactory = function() {
3827
3780
 
3828
- case 'merge':
3829
- operations.args.forEach(bindItem);
3830
- break;
3781
+ const $stores = new Map();
3782
+ const $followersCache = new Map();
3831
3783
 
3832
- case 'clear':
3833
- this.$currentValue.forEach(unbindItem);
3834
- break;
3784
+ /**
3785
+ * Internal helper — retrieves a store entry or throws if not found.
3786
+ */
3787
+ const $getStoreOrThrow = (method, name) => {
3788
+ const item = $stores.get(name);
3789
+ if (!item) {
3790
+ DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
3791
+ throw new NativeDocumentError(
3792
+ `Store.${method}('${name}') : store not found.`
3793
+ );
3835
3794
  }
3836
- });
3837
-
3838
- return () => {
3839
- this.$currentValue.forEach(unbindItem);
3795
+ return item;
3840
3796
  };
3841
- };
3842
3797
 
3843
- /**
3844
- * Creates an observable array with reactive array methods.
3845
- * All mutations trigger updates automatically.
3846
- *
3847
- * @param {Array} [target=[]] - Initial array value
3848
- * @param {Object|null} [configs=null] - Configuration options
3849
- * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
3850
- * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
3851
- * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
3852
- * @returns {ObservableArray} An observable array with reactive methods
3853
- * @example
3854
- * const items = Observable.array([1, 2, 3]);
3855
- * items.push(4); // Triggers update
3856
- * items.subscribe((arr) => console.log(arr));
3857
- */
3858
- Observable.array = function(target = [], configs = null) {
3859
- return new ObservableArray(target, configs);
3860
- };
3798
+ /**
3799
+ * Internal helper blocks write operations on a read-only observer.
3800
+ */
3801
+ const $applyReadOnly = (observer, name, context) => {
3802
+ const readOnlyError = (method) => () => {
3803
+ DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
3804
+ throw new NativeDocumentError(
3805
+ `Store.${context}('${name}') is read-only.`
3806
+ );
3807
+ };
3808
+ observer.set = readOnlyError('set');
3809
+ observer.toggle = readOnlyError('toggle');
3810
+ observer.reset = readOnlyError('reset');
3811
+ };
3861
3812
 
3862
- /**
3863
- *
3864
- * @param {Function} callback
3865
- * @returns {Function}
3866
- */
3867
- Observable.batch = function(callback) {
3868
- const $observer = Observable(0);
3869
- const batch = function() {
3870
- if(Validator.isAsyncFunction(callback)) {
3871
- return (callback(...arguments)).then(() => {
3872
- $observer.trigger();
3873
- }).catch(error => { throw error; });
3813
+ const $createObservable = (value, options = {}) => {
3814
+ if(Array.isArray(value)) {
3815
+ return Observable.array(value, options);
3874
3816
  }
3875
- callback(...arguments);
3876
- $observer.trigger();
3817
+ if(typeof value === 'object') {
3818
+ return Observable.object(value, options);
3819
+ }
3820
+ return Observable(value, options);
3877
3821
  };
3878
- batch.$observer = $observer;
3879
- return batch;
3880
- };
3881
3822
 
3882
- const ObservableObject = function(target, configs) {
3883
- ObservableItem.call(this, target);
3884
- this.$observables = {};
3885
- this.configs = configs;
3823
+ const $api = {
3824
+ /**
3825
+ * Create a new state and return the observer.
3826
+ * Throws if a store with the same name already exists.
3827
+ *
3828
+ * @param {string} name
3829
+ * @param {*} value
3830
+ * @returns {ObservableItem}
3831
+ */
3832
+ create(name, value) {
3833
+ if ($stores.has(name)) {
3834
+ DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
3835
+ throw new NativeDocumentError(
3836
+ `Store.create('${name}') : a store with this name already exists.`
3837
+ );
3838
+ }
3839
+ const observer = $createObservable(value);
3840
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
3841
+ return observer;
3842
+ },
3843
+
3844
+ /**
3845
+ * Create a new resettable state and return the observer.
3846
+ * The store can be reset to its initial value via Store.reset(name).
3847
+ * Throws if a store with the same name already exists.
3848
+ *
3849
+ * @param {string} name
3850
+ * @param {*} value
3851
+ * @returns {ObservableItem}
3852
+ */
3853
+ createResettable(name, value) {
3854
+ if ($stores.has(name)) {
3855
+ DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
3856
+ throw new NativeDocumentError(
3857
+ `Store.createResettable('${name}') : a store with this name already exists.`
3858
+ );
3859
+ }
3860
+ const observer = $createObservable(value, { reset: true });
3861
+ $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
3862
+ return observer;
3863
+ },
3886
3864
 
3887
- this.$load(target);
3865
+ /**
3866
+ * Create a computed store derived from other stores.
3867
+ * The value is automatically recalculated when any dependency changes.
3868
+ * This store is read-only — Store.use() and Store.set() will throw.
3869
+ * Throws if a store with the same name already exists.
3870
+ *
3871
+ * @param {string} name
3872
+ * @param {() => *} computation - Function that returns the computed value
3873
+ * @param {string[]} dependencies - Names of the stores to watch
3874
+ * @returns {ObservableItem}
3875
+ *
3876
+ * @example
3877
+ * Store.create('products', [{ id: 1, price: 10 }]);
3878
+ * Store.create('cart', [{ productId: 1, quantity: 2 }]);
3879
+ *
3880
+ * Store.createComposed('total', () => {
3881
+ * const products = Store.get('products').val();
3882
+ * const cart = Store.get('cart').val();
3883
+ * return cart.reduce((sum, item) => {
3884
+ * const product = products.find(p => p.id === item.productId);
3885
+ * return sum + (product.price * item.quantity);
3886
+ * }, 0);
3887
+ * }, ['products', 'cart']);
3888
+ */
3889
+ createComposed(name, computation, dependencies) {
3890
+ if ($stores.has(name)) {
3891
+ DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
3892
+ throw new NativeDocumentError(
3893
+ `Store.createComposed('${name}') : a store with this name already exists.`
3894
+ );
3895
+ }
3896
+ if (typeof computation !== 'function') {
3897
+ throw new NativeDocumentError(
3898
+ `Store.createComposed('${name}') : computation must be a function.`
3899
+ );
3900
+ }
3901
+ if (!Array.isArray(dependencies) || dependencies.length === 0) {
3902
+ throw new NativeDocumentError(
3903
+ `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
3904
+ );
3905
+ }
3888
3906
 
3889
- for(const name in target) {
3890
- if(!Object.hasOwn(this, name)) {
3891
- Object.defineProperty(this, name, {
3892
- get: () => this.$observables[name],
3893
- set: (value) => this.$observables[name].set(value)
3907
+ // Resolve dependency observers
3908
+ const depObservers = dependencies.map(depName => {
3909
+ if(typeof depName !== 'string') {
3910
+ return depName;
3911
+ }
3912
+ const depItem = $stores.get(depName);
3913
+ if (!depItem) {
3914
+ DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
3915
+ throw new NativeDocumentError(
3916
+ `Store.createComposed('${name}') : dependency store '${depName}' not found.`
3917
+ );
3918
+ }
3919
+ return depItem.observer;
3894
3920
  });
3895
- }
3896
- }
3897
-
3898
- };
3899
3921
 
3900
- ObservableObject.prototype = Object.create(ObservableItem.prototype);
3922
+ // Create computed observable from dependency observers
3923
+ const observer = Observable.computed(computation, depObservers);
3901
3924
 
3902
- Object.defineProperty(ObservableObject, '$value', {
3903
- get() {
3904
- return this.val();
3905
- },
3906
- set(value) {
3907
- this.set(value);
3908
- }
3909
- });
3925
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
3926
+ return observer;
3927
+ },
3910
3928
 
3911
- ObservableObject.prototype.__$isObservableObject = true;
3912
- ObservableObject.prototype.__isProxy__ = true;
3929
+ /**
3930
+ * Returns true if a store with the given name exists.
3931
+ *
3932
+ * @param {string} name
3933
+ * @returns {boolean}
3934
+ */
3935
+ has(name) {
3936
+ return $stores.has(name);
3937
+ },
3913
3938
 
3914
- ObservableObject.prototype.$load = function(initialValue) {
3915
- const configs = this.configs;
3916
- for(const key in initialValue) {
3917
- const itemValue = initialValue[key];
3918
- if(Array.isArray(itemValue)) {
3919
- if(configs?.deep !== false) {
3920
- const mappedItemValue = itemValue.map(item => {
3921
- if(Validator.isJson(item)) {
3922
- return Observable.json(item, configs);
3923
- }
3924
- if(Validator.isArray(item)) {
3925
- return Observable.array(item, configs);
3926
- }
3927
- return Observable(item, configs);
3928
- });
3929
- this.$observables[key] = Observable.array(mappedItemValue, configs);
3930
- continue;
3939
+ /**
3940
+ * Resets a resettable store to its initial value and notifies all subscribers.
3941
+ * Throws if the store was not created with createResettable().
3942
+ *
3943
+ * @param {string} name
3944
+ */
3945
+ reset(name) {
3946
+ const item = $getStoreOrThrow('reset', name);
3947
+ if (item.composed) {
3948
+ DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
3949
+ throw new NativeDocumentError(
3950
+ `Store.reset('${name}') : composed stores cannot be reset.`
3951
+ );
3931
3952
  }
3932
- this.$observables[key] = Observable.array(itemValue, configs);
3933
- continue;
3934
- }
3935
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3936
- this.$observables[key] = itemValue;
3937
- continue;
3938
- }
3939
- this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
3940
- }
3941
- };
3953
+ if (!item.resettable) {
3954
+ DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
3955
+ throw new NativeDocumentError(
3956
+ `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
3957
+ );
3958
+ }
3959
+ item.observer.reset();
3960
+ },
3942
3961
 
3943
- ObservableObject.prototype.val = function() {
3944
- const result = {};
3945
- for(const key in this.$observables) {
3946
- const dataItem = this.$observables[key];
3947
- if(Validator.isObservable(dataItem)) {
3948
- let value = dataItem.val();
3949
- if(Array.isArray(value)) {
3950
- value = value.map(item => {
3951
- if(Validator.isObservable(item)) {
3952
- return item.val();
3953
- }
3954
- if(Validator.isProxy(item)) {
3955
- return item.$value;
3956
- }
3957
- return item;
3958
- });
3962
+ /**
3963
+ * Returns a two-way synchronized follower of the store.
3964
+ * Writing to the follower propagates the value back to the store and all its subscribers.
3965
+ * Throws if called on a composed store — use Store.follow() instead.
3966
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
3967
+ *
3968
+ * @param {string} name
3969
+ * @returns {ObservableItem}
3970
+ */
3971
+ use(name) {
3972
+ const item = $getStoreOrThrow('use', name);
3973
+
3974
+ if (item.composed) {
3975
+ DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
3976
+ throw new NativeDocumentError(
3977
+ `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
3978
+ );
3959
3979
  }
3960
- result[key] = value;
3961
- } else if(Validator.isProxy(dataItem)) {
3962
- result[key] = dataItem.$value;
3963
- } else {
3964
- result[key] = dataItem;
3965
- }
3966
- }
3967
- return result;
3968
- };
3969
- ObservableObject.prototype.$val = ObservableObject.prototype.val;
3970
3980
 
3971
- ObservableObject.prototype.get = function(property) {
3972
- const item = this.$observables[property];
3973
- if(Validator.isObservable(item)) {
3974
- return item.val();
3975
- }
3976
- if(Validator.isProxy(item)) {
3977
- return item.$value;
3978
- }
3979
- return item;
3980
- };
3981
- ObservableObject.prototype.$get = ObservableObject.prototype.get;
3981
+ const { observer: originalObserver, subscribers } = item;
3982
+ const observerFollower = $createObservable(originalObserver.val());
3982
3983
 
3983
- ObservableObject.prototype.set = function(newData) {
3984
- const data = Validator.isProxy(newData) ? newData.$value : newData;
3985
- const configs = this.configs;
3984
+ const onStoreChange = value => observerFollower.set(value);
3985
+ const onFollowerChange = value => originalObserver.set(value);
3986
3986
 
3987
- for(const key in data) {
3988
- const targetItem = this.$observables[key];
3989
- const newValueOrigin = newData[key];
3990
- const newValue = data[key];
3987
+ originalObserver.subscribe(onStoreChange);
3988
+ observerFollower.subscribe(onFollowerChange);
3991
3989
 
3992
- if(Validator.isObservable(targetItem)) {
3993
- if(!Validator.isArray(newValue)) {
3994
- targetItem.set(newValue);
3995
- continue;
3996
- }
3997
- const firstElementFromOriginalValue = newValueOrigin.at(0);
3998
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3999
- const newValues = newValue.map(item => {
4000
- if(Validator.isProxy(firstElementFromOriginalValue)) {
4001
- return Observable.init(item, configs);
4002
- }
4003
- return Observable(item, configs);
4004
- });
4005
- targetItem.set(newValues);
4006
- continue;
4007
- }
4008
- targetItem.set([...newValue]);
4009
- continue;
4010
- }
4011
- if(Validator.isProxy(targetItem)) {
4012
- targetItem.update(newValue);
4013
- continue;
4014
- }
4015
- this[key] = newValue;
4016
- }
4017
- };
4018
- ObservableObject.prototype.$set = ObservableObject.prototype.set;
4019
- ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
3990
+ observerFollower.destroy = () => {
3991
+ originalObserver.unsubscribe(onStoreChange);
3992
+ observerFollower.unsubscribe(onFollowerChange);
3993
+ subscribers.delete(observerFollower);
3994
+ observerFollower.cleanup();
3995
+ };
3996
+ observerFollower.dispose = observerFollower.destroy;
4020
3997
 
4021
- ObservableObject.prototype.observables = function() {
4022
- return Object.values(this.$observables);
4023
- };
4024
- ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
3998
+ subscribers.add(observerFollower);
3999
+ return observerFollower;
4000
+ },
4001
+
4002
+ /**
4003
+ * Returns a read-only follower of the store.
4004
+ * The follower reflects store changes but cannot write back to the store.
4005
+ * Any attempt to call .set(), .toggle() or .reset() will throw.
4006
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4007
+ *
4008
+ * @param {string} name
4009
+ * @returns {ObservableItem}
4010
+ */
4011
+ follow(name) {
4012
+ const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
4013
+ const observerFollower = $createObservable(originalObserver.val());
4025
4014
 
4026
- ObservableObject.prototype.keys = function() {
4027
- return Object.keys(this.$observables);
4028
- };
4029
- ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4030
- ObservableObject.prototype.clone = function() {
4031
- return Observable.init(this.val(), this.configs);
4032
- };
4033
- ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4034
- ObservableObject.prototype.reset = function() {
4035
- for(const key in this.$observables) {
4036
- this.$observables[key].reset();
4037
- }
4038
- };
4039
- ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4040
- ObservableObject.prototype.subscribe = function(callback) {
4041
- const observables = this.observables();
4042
- const updatedValue = nextTick(() => this.trigger());
4015
+ const onStoreChange = value => observerFollower.set(value);
4016
+ originalObserver.subscribe(onStoreChange);
4043
4017
 
4044
- this.originalSubscribe(callback);
4018
+ $applyReadOnly(observerFollower, name, 'follow');
4045
4019
 
4046
- for (let i = 0, length = observables.length; i < length; i++) {
4047
- const observable = observables[i];
4048
- if (observable.__$isObservableArray) {
4049
- observable.deepSubscribe(updatedValue);
4050
- continue
4051
- }
4052
- observable.subscribe(updatedValue);
4053
- }
4054
- };
4055
- ObservableObject.prototype.configs = function() {
4056
- return this.configs;
4057
- };
4020
+ observerFollower.destroy = () => {
4021
+ originalObserver.unsubscribe(onStoreChange);
4022
+ subscribers.delete(observerFollower);
4023
+ observerFollower.cleanup();
4024
+ };
4025
+ observerFollower.dispose = observerFollower.destroy;
4058
4026
 
4059
- ObservableObject.prototype.update = ObservableObject.prototype.set;
4027
+ subscribers.add(observerFollower);
4028
+ return observerFollower;
4029
+ },
4060
4030
 
4061
- Observable.init = function(initialValue, configs = null) {
4062
- return new ObservableObject(initialValue, configs)
4063
- };
4031
+ /**
4032
+ * Returns the raw store observer directly (no follower, no cleanup contract).
4033
+ * Use this for direct read access when you don't need to unsubscribe.
4034
+ * WARNING : mutations on this observer impact all subscribers immediately.
4035
+ *
4036
+ * @param {string} name
4037
+ * @returns {ObservableItem|null}
4038
+ */
4039
+ get(name) {
4040
+ const item = $stores.get(name);
4041
+ if (!item) {
4042
+ DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
4043
+ return null;
4044
+ }
4045
+ return item.observer;
4046
+ },
4064
4047
 
4065
- /**
4066
- *
4067
- * @param {any[]} data
4068
- * @return Proxy[]
4069
- */
4070
- Observable.arrayOfObject = function(data) {
4071
- return data.map(item => Observable.object(item));
4072
- };
4048
+ /**
4049
+ * @param {string} name
4050
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
4051
+ */
4052
+ getWithSubscribers(name) {
4053
+ return $stores.get(name) ?? null;
4054
+ },
4073
4055
 
4074
- /**
4075
- * Get the value of an observable or an object of observables.
4076
- * @param {ObservableItem|Object<ObservableItem>} data
4077
- * @returns {{}|*|null}
4078
- */
4079
- Observable.value = function(data) {
4080
- if(Validator.isObservable(data)) {
4081
- return data.val();
4082
- }
4083
- if(Validator.isProxy(data)) {
4084
- return data.$value;
4085
- }
4086
- if(Validator.isArray(data)) {
4087
- const result = [];
4088
- for(let i = 0, length = data.length; i < length; i++) {
4089
- const item = data[i];
4090
- result.push(Observable.value(item));
4091
- }
4092
- return result;
4093
- }
4094
- return data;
4095
- };
4056
+ /**
4057
+ * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
4058
+ *
4059
+ * @param {string} name
4060
+ */
4061
+ delete(name) {
4062
+ const item = $stores.get(name);
4063
+ if (!item) {
4064
+ DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
4065
+ return;
4066
+ }
4067
+ item.subscribers.forEach(follower => follower.destroy());
4068
+ item.subscribers.clear();
4069
+ item.observer.cleanup();
4070
+ $stores.delete(name);
4071
+ },
4072
+ /**
4073
+ * Creates an isolated store group with its own state namespace.
4074
+ * Each group is a fully independent StoreFactory instance —
4075
+ * no key conflicts, no shared state with the parent store.
4076
+ *
4077
+ * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
4078
+ * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
4079
+ * @returns {ReturnType<typeof StoreFactory>}
4080
+ *
4081
+ * @example
4082
+ * // With name (recommended)
4083
+ * const EventStore = Store.group('events', (group) => {
4084
+ * group.create('catalog', []);
4085
+ * group.create('filters', { category: null, date: null });
4086
+ * group.createResettable('selected', null);
4087
+ * group.createComposed('filtered', () => {
4088
+ * const catalog = EventStore.get('catalog').val();
4089
+ * const filters = EventStore.get('filters').val();
4090
+ * return catalog.filter(event => {
4091
+ * if (filters.category && event.category !== filters.category) return false;
4092
+ * return true;
4093
+ * });
4094
+ * }, ['catalog', 'filters']);
4095
+ * });
4096
+ *
4097
+ * // Without name
4098
+ * const CartStore = Store.group((group) => {
4099
+ * group.create('items', []);
4100
+ * });
4101
+ *
4102
+ * // Usage
4103
+ * EventStore.use('catalog'); // two-way follower
4104
+ * EventStore.follow('filtered'); // read-only follower
4105
+ * EventStore.get('filters'); // raw observable
4106
+ *
4107
+ * // Cross-group composed
4108
+ * const OrderStore = Store.group('orders', (group) => {
4109
+ * group.createComposed('summary', () => {
4110
+ * const items = CartStore.get('items').val();
4111
+ * const events = EventStore.get('catalog').val();
4112
+ * return { items, events };
4113
+ * }, [CartStore.get('items'), EventStore.get('catalog')]);
4114
+ * });
4115
+ */
4116
+ group(name, callback) {
4117
+ if (typeof name === 'function') {
4118
+ callback = name;
4119
+ name = 'anonymous';
4120
+ }
4121
+ const store = StoreFactory();
4122
+ callback && callback(store);
4123
+ return store;
4124
+ },
4125
+ createPersistent(name, value, localstorage_key) {
4126
+ localstorage_key = localstorage_key || name;
4127
+ const observer = this.create(name, $getFromStorage(localstorage_key, value));
4128
+ const saver = $saveToStorage(value);
4096
4129
 
4097
- Observable.object = Observable.init;
4098
- Observable.json = Observable.init;
4130
+ observer.subscribe((val) => saver(localstorage_key, val));
4131
+ return observer;
4132
+ },
4133
+ createPersistentResettable(name, value, localstorage_key) {
4134
+ localstorage_key = localstorage_key || name;
4135
+ const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
4136
+ const saver = $saveToStorage(value);
4137
+ observer.subscribe((val) => saver(localstorage_key, val));
4099
4138
 
4100
- /**
4101
- * Creates a computed observable that automatically updates when its dependencies change.
4102
- * The callback is re-executed whenever any dependency observable changes.
4103
- *
4104
- * @param {Function} callback - Function that returns the computed value
4105
- * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4106
- * @returns {ObservableItem} A new observable that updates automatically
4107
- * @example
4108
- * const firstName = Observable('John');
4109
- * const lastName = Observable('Doe');
4110
- * const fullName = Observable.computed(
4111
- * () => `${firstName.val()} ${lastName.val()}`,
4112
- * [firstName, lastName]
4113
- * );
4114
- *
4115
- * // With batch function
4116
- * const batch = Observable.batch(() => { ... });
4117
- * const computed = Observable.computed(() => { ... }, batch);
4118
- */
4119
- Observable.computed = function(callback, dependencies = []) {
4120
- const initialValue = callback();
4121
- const observable = new ObservableItem(initialValue);
4122
- const updatedValue = nextTick(() => observable.set(callback()));
4139
+ const originalReset = observer.reset.bind(observer);
4140
+ observer.reset = () => {
4141
+ LocalStorage.remove(localstorage_key);
4142
+ originalReset();
4143
+ };
4123
4144
 
4124
- if(Validator.isFunction(dependencies)) {
4125
- if(!Validator.isObservable(dependencies.$observer)) {
4126
- throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4145
+ return observer;
4127
4146
  }
4128
- dependencies.$observer.subscribe(updatedValue);
4129
- return observable;
4130
- }
4147
+ };
4131
4148
 
4132
- dependencies.forEach(dependency => {
4133
- if(Validator.isProxy(dependency)) {
4134
- dependency.$observables.forEach((observable) => {
4135
- observable.subscribe(updatedValue);
4136
- });
4137
- return;
4149
+
4150
+ return new Proxy($api, {
4151
+ get(target, prop) {
4152
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
4153
+ return target[prop];
4154
+ }
4155
+ if (target.has(prop)) {
4156
+ if ($followersCache.has(prop)) {
4157
+ return $followersCache.get(prop);
4158
+ }
4159
+ const follower = target.follow(prop);
4160
+ $followersCache.set(prop, follower);
4161
+ return follower;
4162
+ }
4163
+ return undefined;
4164
+ },
4165
+ set(target, prop, value) {
4166
+ DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
4167
+ throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
4168
+ },
4169
+ deleteProperty(target, prop) {
4170
+ throw new NativeDocumentError(`Store keys cannot be deleted.`);
4138
4171
  }
4139
- dependency.subscribe(updatedValue);
4140
4172
  });
4141
-
4142
- return observable;
4143
4173
  };
4144
4174
 
4175
+ const Store = StoreFactory();
4176
+
4177
+ Store.create('locale', navigator.language.split('-')[0] || 'en');
4178
+
4145
4179
  /**
4146
4180
  * Creates a `<button>` element.
4147
4181
  * @type {function(ButtonAttributes=, NdChild|NdChild[]=): HTMLButtonElement}