native-document 1.0.117 → 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,29 +2897,71 @@ 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;
@@ -3641,500 +3282,900 @@ var NativeComponents = (function (exports) {
3641
3282
  };
3642
3283
 
3643
3284
  /**
3644
- * 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
+ /**
3645
3497
  *
3646
- * @returns {boolean} True if array has no elements
3647
- * @example
3648
- * const items = Observable.array([]);
3649
- * items.isEmpty(); // true
3498
+ * @param {Function} callback
3499
+ * @returns {Function}
3650
3500
  */
3651
- ObservableArray.prototype.isEmpty = function() {
3652
- 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;
3653
3514
  };
3654
3515
 
3655
- /**
3656
- * Triggers a populate operation with the current array, iteration count, and callback.
3657
- * Used internally for rendering optimizations.
3658
- *
3659
- * @param {number} iteration - Iteration count for rendering
3660
- * @param {Function} callback - Callback function for rendering items
3661
- */
3662
- ObservableArray.prototype.populateAndRender = function(iteration, callback) {
3663
- 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
+
3664
3532
  };
3665
3533
 
3534
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
3666
3535
 
3667
- /**
3668
- * Creates a filtered view of the array based on predicates.
3669
- * The filtered array updates automatically when source data or predicates change.
3670
- *
3671
- * @param {Object} predicates - Object mapping property names to filter conditions or functions
3672
- * @returns {ObservableArray} A new observable array containing filtered items
3673
- * @example
3674
- * const users = Observable.array([
3675
- * { name: 'John', age: 25 },
3676
- * { name: 'Jane', age: 30 }
3677
- * ]);
3678
- * const adults = users.where({ age: (val) => val >= 18 });
3679
- */
3680
- ObservableArray.prototype.where = function(predicates) {
3681
- const sourceArray = this;
3682
- const observableDependencies = [sourceArray];
3683
- const filterCallbacks = {};
3536
+ Object.defineProperty(ObservableObject, '$value', {
3537
+ get() {
3538
+ return this.val();
3539
+ },
3540
+ set(value) {
3541
+ this.set(value);
3542
+ }
3543
+ });
3684
3544
 
3685
- for (const [key, rawPredicate] of Object.entries(predicates)) {
3686
- const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
3687
- if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
3688
- filterCallbacks[key] = predicate.callback;
3545
+ ObservableObject.prototype.__$isObservableObject = true;
3546
+ ObservableObject.prototype.__isProxy__ = true;
3689
3547
 
3690
- if (predicate.dependencies) {
3691
- const deps = Array.isArray(predicate.dependencies)
3692
- ? predicate.dependencies
3693
- : [predicate.dependencies];
3694
- 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;
3695
3565
  }
3696
- } else if(typeof predicate === 'function') {
3697
- 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;
3698
3597
  } else {
3699
- filterCallbacks[key] = (value) => value === predicate;
3598
+ result[key] = dataItem;
3700
3599
  }
3701
3600
  }
3601
+ return result;
3602
+ };
3603
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
3702
3604
 
3703
- 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;
3704
3616
 
3705
- const filters = Object.entries(filterCallbacks);
3706
- const updateView = () => {
3707
- const filtered = sourceArray.val().filter(item => {
3708
- for (const [key, callback] of filters) {
3709
- if(key === '_') {
3710
- if (!callback(item)) return false;
3711
- } else {
3712
- if (!callback(item[key])) return false;
3713
- }
3714
- }
3715
- return true;
3716
- });
3617
+ ObservableObject.prototype.set = function(newData) {
3618
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
3619
+ const configs = this.configs;
3717
3620
 
3718
- viewArray.set(filtered);
3719
- };
3621
+ for(const key in data) {
3622
+ const targetItem = this.$observables[key];
3623
+ const newValueOrigin = newData[key];
3624
+ const newValue = data[key];
3720
3625
 
3721
- 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;
3722
3654
 
3723
- updateView();
3655
+ ObservableObject.prototype.observables = function() {
3656
+ return Object.values(this.$observables);
3657
+ };
3658
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
3724
3659
 
3725
- 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
+ }
3726
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());
3727
3677
 
3728
- /**
3729
- * Creates a filtered view where at least one of the specified fields matches the filter.
3730
- *
3731
- * @param {Array<string>} fields - Array of field names to check
3732
- * @param {FilterResult} filter - Filter condition with callback and dependencies
3733
- * @returns {ObservableArray} A new observable array containing filtered items
3734
- * @example
3735
- * const products = Observable.array([
3736
- * { name: 'Apple', category: 'Fruit' },
3737
- * { name: 'Carrot', category: 'Vegetable' }
3738
- * ]);
3739
- * const searchTerm = Observable('App');
3740
- * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
3741
- */
3742
- ObservableArray.prototype.whereSome = function(fields, filter) {
3743
- return this.where({
3744
- _: {
3745
- dependencies: filter.dependencies,
3746
- 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
3747
3685
  }
3748
- });
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)
3749
3697
  };
3750
3698
 
3751
3699
  /**
3752
- * Creates a filtered view where all specified fields match the filter.
3753
3700
  *
3754
- * @param {Array<string>} fields - Array of field names to check
3755
- * @param {FilterResult} filter - Filter condition with callback and dependencies
3756
- * @returns {ObservableArray} A new observable array containing filtered items
3757
- * @example
3758
- * const items = Observable.array([
3759
- * { status: 'active', verified: true },
3760
- * { status: 'active', verified: false }
3761
- * ]);
3762
- * const activeFilter = equals('active');
3763
- * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
3701
+ * @param {any[]} data
3702
+ * @return Proxy[]
3764
3703
  */
3765
- ObservableArray.prototype.whereEvery = function(fields, filter) {
3766
- return this.where({
3767
- _: {
3768
- dependencies: filter.dependencies,
3769
- 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));
3770
3725
  }
3771
- });
3726
+ return result;
3727
+ }
3728
+ return data;
3772
3729
  };
3773
3730
 
3774
- ObservableArray.prototype.deepSubscribe = function(callback) {
3775
- const updatedValue = nextTick(() => callback(this.val()));
3776
- const $listeners = new WeakMap();
3731
+ Observable.object = Observable.init;
3732
+ Observable.json = Observable.init;
3777
3733
 
3778
- const bindItem = (item) => {
3779
- if ($listeners.has(item)) {
3780
- return;
3781
- }
3782
- if (item?.__$isObservableArray) {
3783
- $listeners.set(item, item.deepSubscribe(updatedValue));
3784
- return;
3785
- }
3786
- if (item?.__$isObservable) {
3787
- item.subscribe(updatedValue);
3788
- $listeners.set(item, () => item.unsubscribe(updatedValue));
3789
- }
3790
- };
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()));
3791
3757
 
3792
- const unbindItem = (item) => {
3793
- const unsub = $listeners.get(item);
3794
- if (unsub) {
3795
- unsub();
3796
- $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');
3797
3761
  }
3798
- };
3799
-
3800
- this.$currentValue.forEach(bindItem);
3801
- this.subscribe(updatedValue);
3762
+ dependencies.$observer.subscribe(updatedValue);
3763
+ return observable;
3764
+ }
3802
3765
 
3803
- this.subscribe((items, _, operations) => {
3804
- switch (operations?.action) {
3805
- case 'push':
3806
- case 'unshift':
3807
- operations.args.forEach(bindItem);
3808
- 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
+ });
3809
3775
 
3810
- case 'splice': {
3811
- const [start, deleteCount, ...newItems] = operations.args;
3812
- operations.result?.forEach(unbindItem);
3813
- newItems.forEach(bindItem);
3814
- break;
3815
- }
3776
+ return observable;
3777
+ };
3816
3778
 
3817
- case 'remove':
3818
- unbindItem(operations.result);
3819
- break;
3779
+ const StoreFactory = function() {
3820
3780
 
3821
- case 'merge':
3822
- operations.args.forEach(bindItem);
3823
- break;
3781
+ const $stores = new Map();
3782
+ const $followersCache = new Map();
3824
3783
 
3825
- case 'clear':
3826
- this.$currentValue.forEach(unbindItem);
3827
- 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
+ );
3828
3794
  }
3829
- });
3830
-
3831
- return () => {
3832
- this.$currentValue.forEach(unbindItem);
3795
+ return item;
3833
3796
  };
3834
- };
3835
3797
 
3836
- /**
3837
- * Creates an observable array with reactive array methods.
3838
- * All mutations trigger updates automatically.
3839
- *
3840
- * @param {Array} [target=[]] - Initial array value
3841
- * @param {Object|null} [configs=null] - Configuration options
3842
- * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
3843
- * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
3844
- * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
3845
- * @returns {ObservableArray} An observable array with reactive methods
3846
- * @example
3847
- * const items = Observable.array([1, 2, 3]);
3848
- * items.push(4); // Triggers update
3849
- * items.subscribe((arr) => console.log(arr));
3850
- */
3851
- Observable.array = function(target = [], configs = null) {
3852
- return new ObservableArray(target, configs);
3853
- };
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
+ };
3854
3812
 
3855
- /**
3856
- *
3857
- * @param {Function} callback
3858
- * @returns {Function}
3859
- */
3860
- Observable.batch = function(callback) {
3861
- const $observer = Observable(0);
3862
- const batch = function() {
3863
- if(Validator.isAsyncFunction(callback)) {
3864
- return (callback(...arguments)).then(() => {
3865
- $observer.trigger();
3866
- }).catch(error => { throw error; });
3813
+ const $createObservable = (value, options = {}) => {
3814
+ if(Array.isArray(value)) {
3815
+ return Observable.array(value, options);
3867
3816
  }
3868
- callback(...arguments);
3869
- $observer.trigger();
3817
+ if(typeof value === 'object') {
3818
+ return Observable.object(value, options);
3819
+ }
3820
+ return Observable(value, options);
3870
3821
  };
3871
- batch.$observer = $observer;
3872
- return batch;
3873
- };
3874
3822
 
3875
- const ObservableObject = function(target, configs) {
3876
- ObservableItem.call(this, target);
3877
- this.$observables = {};
3878
- 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
+ },
3879
3864
 
3880
- 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
+ }
3881
3906
 
3882
- for(const name in target) {
3883
- if(!Object.hasOwn(this, name)) {
3884
- Object.defineProperty(this, name, {
3885
- get: () => this.$observables[name],
3886
- 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;
3887
3920
  });
3888
- }
3889
- }
3890
-
3891
- };
3892
3921
 
3893
- ObservableObject.prototype = Object.create(ObservableItem.prototype);
3922
+ // Create computed observable from dependency observers
3923
+ const observer = Observable.computed(computation, depObservers);
3894
3924
 
3895
- Object.defineProperty(ObservableObject, '$value', {
3896
- get() {
3897
- return this.val();
3898
- },
3899
- set(value) {
3900
- this.set(value);
3901
- }
3902
- });
3925
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
3926
+ return observer;
3927
+ },
3903
3928
 
3904
- ObservableObject.prototype.__$isObservableObject = true;
3905
- 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
+ },
3906
3938
 
3907
- ObservableObject.prototype.$load = function(initialValue) {
3908
- const configs = this.configs;
3909
- for(const key in initialValue) {
3910
- const itemValue = initialValue[key];
3911
- if(Array.isArray(itemValue)) {
3912
- if(configs?.deep !== false) {
3913
- const mappedItemValue = itemValue.map(item => {
3914
- if(Validator.isJson(item)) {
3915
- return Observable.json(item, configs);
3916
- }
3917
- if(Validator.isArray(item)) {
3918
- return Observable.array(item, configs);
3919
- }
3920
- return Observable(item, configs);
3921
- });
3922
- this.$observables[key] = Observable.array(mappedItemValue, configs);
3923
- 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
+ );
3924
3952
  }
3925
- this.$observables[key] = Observable.array(itemValue, configs);
3926
- continue;
3927
- }
3928
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
3929
- this.$observables[key] = itemValue;
3930
- continue;
3931
- }
3932
- this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
3933
- }
3934
- };
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
+ },
3935
3961
 
3936
- ObservableObject.prototype.val = function() {
3937
- const result = {};
3938
- for(const key in this.$observables) {
3939
- const dataItem = this.$observables[key];
3940
- if(Validator.isObservable(dataItem)) {
3941
- let value = dataItem.val();
3942
- if(Array.isArray(value)) {
3943
- value = value.map(item => {
3944
- if(Validator.isObservable(item)) {
3945
- return item.val();
3946
- }
3947
- if(Validator.isProxy(item)) {
3948
- return item.$value;
3949
- }
3950
- return item;
3951
- });
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
+ );
3952
3979
  }
3953
- result[key] = value;
3954
- } else if(Validator.isProxy(dataItem)) {
3955
- result[key] = dataItem.$value;
3956
- } else {
3957
- result[key] = dataItem;
3958
- }
3959
- }
3960
- return result;
3961
- };
3962
- ObservableObject.prototype.$val = ObservableObject.prototype.val;
3963
3980
 
3964
- ObservableObject.prototype.get = function(property) {
3965
- const item = this.$observables[property];
3966
- if(Validator.isObservable(item)) {
3967
- return item.val();
3968
- }
3969
- if(Validator.isProxy(item)) {
3970
- return item.$value;
3971
- }
3972
- return item;
3973
- };
3974
- ObservableObject.prototype.$get = ObservableObject.prototype.get;
3981
+ const { observer: originalObserver, subscribers } = item;
3982
+ const observerFollower = $createObservable(originalObserver.val());
3975
3983
 
3976
- ObservableObject.prototype.set = function(newData) {
3977
- const data = Validator.isProxy(newData) ? newData.$value : newData;
3978
- const configs = this.configs;
3984
+ const onStoreChange = value => observerFollower.set(value);
3985
+ const onFollowerChange = value => originalObserver.set(value);
3979
3986
 
3980
- for(const key in data) {
3981
- const targetItem = this.$observables[key];
3982
- const newValueOrigin = newData[key];
3983
- const newValue = data[key];
3987
+ originalObserver.subscribe(onStoreChange);
3988
+ observerFollower.subscribe(onFollowerChange);
3984
3989
 
3985
- if(Validator.isObservable(targetItem)) {
3986
- if(!Validator.isArray(newValue)) {
3987
- targetItem.set(newValue);
3988
- continue;
3989
- }
3990
- const firstElementFromOriginalValue = newValueOrigin.at(0);
3991
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
3992
- const newValues = newValue.map(item => {
3993
- if(Validator.isProxy(firstElementFromOriginalValue)) {
3994
- return Observable.init(item, configs);
3995
- }
3996
- return Observable(item, configs);
3997
- });
3998
- targetItem.set(newValues);
3999
- continue;
4000
- }
4001
- targetItem.set([...newValue]);
4002
- continue;
4003
- }
4004
- if(Validator.isProxy(targetItem)) {
4005
- targetItem.update(newValue);
4006
- continue;
4007
- }
4008
- this[key] = newValue;
4009
- }
4010
- };
4011
- ObservableObject.prototype.$set = ObservableObject.prototype.set;
4012
- 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;
4013
3997
 
4014
- ObservableObject.prototype.observables = function() {
4015
- return Object.values(this.$observables);
4016
- };
4017
- 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());
4018
4014
 
4019
- ObservableObject.prototype.keys = function() {
4020
- return Object.keys(this.$observables);
4021
- };
4022
- ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4023
- ObservableObject.prototype.clone = function() {
4024
- return Observable.init(this.val(), this.configs);
4025
- };
4026
- ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4027
- ObservableObject.prototype.reset = function() {
4028
- for(const key in this.$observables) {
4029
- this.$observables[key].reset();
4030
- }
4031
- };
4032
- ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4033
- ObservableObject.prototype.subscribe = function(callback) {
4034
- const observables = this.observables();
4035
- const updatedValue = nextTick(() => this.trigger());
4015
+ const onStoreChange = value => observerFollower.set(value);
4016
+ originalObserver.subscribe(onStoreChange);
4036
4017
 
4037
- this.originalSubscribe(callback);
4018
+ $applyReadOnly(observerFollower, name, 'follow');
4038
4019
 
4039
- for (let i = 0, length = observables.length; i < length; i++) {
4040
- const observable = observables[i];
4041
- if (observable.__$isObservableArray) {
4042
- observable.deepSubscribe(updatedValue);
4043
- continue
4044
- }
4045
- observable.subscribe(updatedValue);
4046
- }
4047
- };
4048
- ObservableObject.prototype.configs = function() {
4049
- return this.configs;
4050
- };
4020
+ observerFollower.destroy = () => {
4021
+ originalObserver.unsubscribe(onStoreChange);
4022
+ subscribers.delete(observerFollower);
4023
+ observerFollower.cleanup();
4024
+ };
4025
+ observerFollower.dispose = observerFollower.destroy;
4051
4026
 
4052
- ObservableObject.prototype.update = ObservableObject.prototype.set;
4027
+ subscribers.add(observerFollower);
4028
+ return observerFollower;
4029
+ },
4053
4030
 
4054
- Observable.init = function(initialValue, configs = null) {
4055
- return new ObservableObject(initialValue, configs)
4056
- };
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
+ },
4057
4047
 
4058
- /**
4059
- *
4060
- * @param {any[]} data
4061
- * @return Proxy[]
4062
- */
4063
- Observable.arrayOfObject = function(data) {
4064
- return data.map(item => Observable.object(item));
4065
- };
4048
+ /**
4049
+ * @param {string} name
4050
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
4051
+ */
4052
+ getWithSubscribers(name) {
4053
+ return $stores.get(name) ?? null;
4054
+ },
4066
4055
 
4067
- /**
4068
- * Get the value of an observable or an object of observables.
4069
- * @param {ObservableItem|Object<ObservableItem>} data
4070
- * @returns {{}|*|null}
4071
- */
4072
- Observable.value = function(data) {
4073
- if(Validator.isObservable(data)) {
4074
- return data.val();
4075
- }
4076
- if(Validator.isProxy(data)) {
4077
- return data.$value;
4078
- }
4079
- if(Validator.isArray(data)) {
4080
- const result = [];
4081
- for(let i = 0, length = data.length; i < length; i++) {
4082
- const item = data[i];
4083
- result.push(Observable.value(item));
4084
- }
4085
- return result;
4086
- }
4087
- return data;
4088
- };
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);
4089
4129
 
4090
- Observable.object = Observable.init;
4091
- 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));
4092
4138
 
4093
- /**
4094
- * Creates a computed observable that automatically updates when its dependencies change.
4095
- * The callback is re-executed whenever any dependency observable changes.
4096
- *
4097
- * @param {Function} callback - Function that returns the computed value
4098
- * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4099
- * @returns {ObservableItem} A new observable that updates automatically
4100
- * @example
4101
- * const firstName = Observable('John');
4102
- * const lastName = Observable('Doe');
4103
- * const fullName = Observable.computed(
4104
- * () => `${firstName.val()} ${lastName.val()}`,
4105
- * [firstName, lastName]
4106
- * );
4107
- *
4108
- * // With batch function
4109
- * const batch = Observable.batch(() => { ... });
4110
- * const computed = Observable.computed(() => { ... }, batch);
4111
- */
4112
- Observable.computed = function(callback, dependencies = []) {
4113
- const initialValue = callback();
4114
- const observable = new ObservableItem(initialValue);
4115
- const updatedValue = nextTick(() => observable.set(callback()));
4139
+ const originalReset = observer.reset.bind(observer);
4140
+ observer.reset = () => {
4141
+ LocalStorage.remove(localstorage_key);
4142
+ originalReset();
4143
+ };
4116
4144
 
4117
- if(Validator.isFunction(dependencies)) {
4118
- if(!Validator.isObservable(dependencies.$observer)) {
4119
- throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4145
+ return observer;
4120
4146
  }
4121
- dependencies.$observer.subscribe(updatedValue);
4122
- return observable;
4123
- }
4147
+ };
4124
4148
 
4125
- dependencies.forEach(dependency => {
4126
- if(Validator.isProxy(dependency)) {
4127
- dependency.$observables.forEach((observable) => {
4128
- observable.subscribe(updatedValue);
4129
- });
4130
- 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.`);
4131
4171
  }
4132
- dependency.subscribe(updatedValue);
4133
4172
  });
4134
-
4135
- return observable;
4136
4173
  };
4137
4174
 
4175
+ const Store = StoreFactory();
4176
+
4177
+ Store.create('locale', navigator.language.split('-')[0] || 'en');
4178
+
4138
4179
  /**
4139
4180
  * Creates a `<button>` element.
4140
4181
  * @type {function(ButtonAttributes=, NdChild|NdChild[]=): HTMLButtonElement}