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.
@@ -1001,6 +1001,97 @@ var NativeDocument = (function (exports) {
1001
1001
  return cloned;
1002
1002
  };
1003
1003
 
1004
+ const $parseDateParts = (value, locale) => {
1005
+ const d = new Date(value);
1006
+ return {
1007
+ d,
1008
+ parts: new Intl.DateTimeFormat(locale, {
1009
+ year: 'numeric',
1010
+ month: 'long',
1011
+ day: '2-digit',
1012
+ hour: '2-digit',
1013
+ minute: '2-digit',
1014
+ second: '2-digit',
1015
+ }).formatToParts(d).reduce((acc, { type, value }) => {
1016
+ acc[type] = value;
1017
+ return acc;
1018
+ }, {})
1019
+ };
1020
+ };
1021
+
1022
+ const $applyDatePattern = (pattern, d, parts) => {
1023
+ const pad = n => String(n).padStart(2, '0');
1024
+ return pattern
1025
+ .replace('YYYY', parts.year)
1026
+ .replace('YY', parts.year.slice(-2))
1027
+ .replace('MMMM', parts.month)
1028
+ .replace('MMM', parts.month.slice(0, 3))
1029
+ .replace('MM', pad(d.getMonth() + 1))
1030
+ .replace('DD', pad(d.getDate()))
1031
+ .replace('D', d.getDate())
1032
+ .replace('HH', parts.hour)
1033
+ .replace('mm', parts.minute)
1034
+ .replace('ss', parts.second);
1035
+ };
1036
+
1037
+ const Formatters = {
1038
+ currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1039
+ new Intl.NumberFormat(locale, {
1040
+ style: 'currency',
1041
+ currency,
1042
+ notation,
1043
+ minimumFractionDigits,
1044
+ maximumFractionDigits
1045
+ }).format(value),
1046
+
1047
+ number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1048
+ new Intl.NumberFormat(locale, {
1049
+ notation,
1050
+ minimumFractionDigits,
1051
+ maximumFractionDigits
1052
+ }).format(value),
1053
+
1054
+ percent: (value, locale, { decimals = 1 } = {}) =>
1055
+ new Intl.NumberFormat(locale, {
1056
+ style: 'percent',
1057
+ maximumFractionDigits: decimals
1058
+ }).format(value),
1059
+
1060
+ date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1061
+ if (format) {
1062
+ const { d, parts } = $parseDateParts(value, locale);
1063
+ return $applyDatePattern(format, d, parts);
1064
+ }
1065
+ return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1066
+ },
1067
+
1068
+ time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1069
+ if (format) {
1070
+ const { d, parts } = $parseDateParts(value, locale);
1071
+ return $applyDatePattern(format, d, parts);
1072
+ }
1073
+ return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1074
+ },
1075
+
1076
+ datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1077
+ if (format) {
1078
+ const { d, parts } = $parseDateParts(value, locale);
1079
+ return $applyDatePattern(format, d, parts);
1080
+ }
1081
+ return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1082
+ },
1083
+
1084
+ relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1085
+ const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1086
+ return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1087
+ },
1088
+
1089
+ plural: (value, locale, { singular, plural } = {}) => {
1090
+ const rule = new Intl.PluralRules(locale).select(value);
1091
+ return `${value} ${rule === 'one' ? singular : plural}`;
1092
+ },
1093
+ };
1094
+
1004
1095
  const LocalStorage = {
1005
1096
  getJson(key) {
1006
1097
  let value = localStorage.getItem(key);
@@ -1057,688 +1148,197 @@ var NativeDocument = (function (exports) {
1057
1148
  }
1058
1149
  };
1059
1150
 
1060
- const StoreFactory = function() {
1151
+ /**
1152
+ *
1153
+ * @param {*} value
1154
+ * @param {{ propagation: boolean, reset: boolean} | null} configs
1155
+ * @class ObservableItem
1156
+ */
1157
+ function ObservableItem(value, configs = null) {
1158
+ value = Validator.isObservable(value) ? value.val() : value;
1061
1159
 
1062
- const $stores = new Map();
1063
- const $followersCache = new Map();
1160
+ this.$previousValue = null;
1161
+ this.$currentValue = value;
1162
+ {
1163
+ this.$isCleanedUp = false;
1164
+ }
1064
1165
 
1065
- /**
1066
- * Internal helper — retrieves a store entry or throws if not found.
1067
- */
1068
- const $getStoreOrThrow = (method, name) => {
1069
- const item = $stores.get(name);
1070
- if (!item) {
1071
- DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
1072
- throw new NativeDocumentError(
1073
- `Store.${method}('${name}') : store not found.`
1074
- );
1075
- }
1076
- return item;
1077
- };
1166
+ this.$firstListener = null;
1167
+ this.$listeners = null;
1168
+ this.$watchers = null;
1078
1169
 
1079
- /**
1080
- * Internal helper — blocks write operations on a read-only observer.
1081
- */
1082
- const $applyReadOnly = (observer, name, context) => {
1083
- const readOnlyError = (method) => () => {
1084
- DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
1085
- throw new NativeDocumentError(
1086
- `Store.${context}('${name}') is read-only.`
1087
- );
1088
- };
1089
- observer.set = readOnlyError('set');
1090
- observer.toggle = readOnlyError('toggle');
1091
- observer.reset = readOnlyError('reset');
1092
- };
1170
+ this.$memoryId = null;
1093
1171
 
1094
- const $createObservable = (value, options = {}) => {
1095
- if(Array.isArray(value)) {
1096
- return Observable.array(value, options);
1097
- }
1098
- if(typeof value === 'object') {
1099
- return Observable.object(value, options);
1172
+ if(configs) {
1173
+ this.configs = configs;
1174
+ if(configs.reset) {
1175
+ this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1100
1176
  }
1101
- return Observable(value, options);
1102
- };
1103
-
1104
- const $api = {
1105
- /**
1106
- * Create a new state and return the observer.
1107
- * Throws if a store with the same name already exists.
1108
- *
1109
- * @param {string} name
1110
- * @param {*} value
1111
- * @returns {ObservableItem}
1112
- */
1113
- create(name, value) {
1114
- if ($stores.has(name)) {
1115
- DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
1116
- throw new NativeDocumentError(
1117
- `Store.create('${name}') : a store with this name already exists.`
1118
- );
1119
- }
1120
- const observer = $createObservable(value);
1121
- $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
1122
- return observer;
1123
- },
1177
+ }
1178
+ {
1179
+ PluginsManager.emit('CreateObservable', this);
1180
+ }
1181
+ }
1124
1182
 
1125
- /**
1126
- * Create a new resettable state and return the observer.
1127
- * The store can be reset to its initial value via Store.reset(name).
1128
- * Throws if a store with the same name already exists.
1129
- *
1130
- * @param {string} name
1131
- * @param {*} value
1132
- * @returns {ObservableItem}
1133
- */
1134
- createResettable(name, value) {
1135
- if ($stores.has(name)) {
1136
- DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
1137
- throw new NativeDocumentError(
1138
- `Store.createResettable('${name}') : a store with this name already exists.`
1139
- );
1140
- }
1141
- const observer = $createObservable(value, { reset: true });
1142
- $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
1143
- return observer;
1144
- },
1183
+ Object.defineProperty(ObservableItem.prototype, '$value', {
1184
+ get() {
1185
+ return this.$currentValue;
1186
+ },
1187
+ set(value) {
1188
+ this.set(value);
1189
+ },
1190
+ configurable: true,
1191
+ });
1145
1192
 
1146
- /**
1147
- * Create a computed store derived from other stores.
1148
- * The value is automatically recalculated when any dependency changes.
1149
- * This store is read-only — Store.use() and Store.set() will throw.
1150
- * Throws if a store with the same name already exists.
1151
- *
1152
- * @param {string} name
1153
- * @param {() => *} computation - Function that returns the computed value
1154
- * @param {string[]} dependencies - Names of the stores to watch
1155
- * @returns {ObservableItem}
1156
- *
1157
- * @example
1158
- * Store.create('products', [{ id: 1, price: 10 }]);
1159
- * Store.create('cart', [{ productId: 1, quantity: 2 }]);
1160
- *
1161
- * Store.createComposed('total', () => {
1162
- * const products = Store.get('products').val();
1163
- * const cart = Store.get('cart').val();
1164
- * return cart.reduce((sum, item) => {
1165
- * const product = products.find(p => p.id === item.productId);
1166
- * return sum + (product.price * item.quantity);
1167
- * }, 0);
1168
- * }, ['products', 'cart']);
1169
- */
1170
- createComposed(name, computation, dependencies) {
1171
- if ($stores.has(name)) {
1172
- DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
1173
- throw new NativeDocumentError(
1174
- `Store.createComposed('${name}') : a store with this name already exists.`
1175
- );
1176
- }
1177
- if (typeof computation !== 'function') {
1178
- throw new NativeDocumentError(
1179
- `Store.createComposed('${name}') : computation must be a function.`
1180
- );
1181
- }
1182
- if (!Array.isArray(dependencies) || dependencies.length === 0) {
1183
- throw new NativeDocumentError(
1184
- `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
1185
- );
1186
- }
1193
+ ObservableItem.prototype.__$isObservable = true;
1194
+ const noneTrigger = function() {};
1187
1195
 
1188
- // Resolve dependency observers
1189
- const depObservers = dependencies.map(depName => {
1190
- if(typeof depName !== 'string') {
1191
- return depName;
1192
- }
1193
- const depItem = $stores.get(depName);
1194
- if (!depItem) {
1195
- DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
1196
- throw new NativeDocumentError(
1197
- `Store.createComposed('${name}') : dependency store '${depName}' not found.`
1198
- );
1199
- }
1200
- return depItem.observer;
1201
- });
1202
-
1203
- // Create computed observable from dependency observers
1204
- const observer = Observable.computed(computation, depObservers);
1196
+ /**
1197
+ * Intercepts and transforms values before they are set on the observable.
1198
+ * The interceptor can modify the value or return undefined to use the original value.
1199
+ *
1200
+ * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1201
+ * @returns {ObservableItem} The observable instance for chaining
1202
+ * @example
1203
+ * const count = Observable(0);
1204
+ * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1205
+ */
1206
+ ObservableItem.prototype.intercept = function(callback) {
1207
+ this.$interceptor = callback;
1208
+ this.set = this.$setWithInterceptor;
1209
+ return this;
1210
+ };
1205
1211
 
1206
- $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
1207
- return observer;
1208
- },
1212
+ ObservableItem.prototype.triggerFirstListener = function(operations) {
1213
+ this.$firstListener(this.$currentValue, this.$previousValue, operations);
1214
+ };
1209
1215
 
1210
- /**
1211
- * Returns true if a store with the given name exists.
1212
- *
1213
- * @param {string} name
1214
- * @returns {boolean}
1215
- */
1216
- has(name) {
1217
- return $stores.has(name);
1218
- },
1216
+ ObservableItem.prototype.triggerListeners = function(operations) {
1217
+ const $listeners = this.$listeners;
1218
+ const $previousValue = this.$previousValue;
1219
+ const $currentValue = this.$currentValue;
1219
1220
 
1220
- /**
1221
- * Resets a resettable store to its initial value and notifies all subscribers.
1222
- * Throws if the store was not created with createResettable().
1223
- *
1224
- * @param {string} name
1225
- */
1226
- reset(name) {
1227
- const item = $getStoreOrThrow('reset', name);
1228
- if (item.composed) {
1229
- DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
1230
- throw new NativeDocumentError(
1231
- `Store.reset('${name}') : composed stores cannot be reset.`
1232
- );
1233
- }
1234
- if (!item.resettable) {
1235
- DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
1236
- throw new NativeDocumentError(
1237
- `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
1238
- );
1239
- }
1240
- item.observer.reset();
1241
- },
1221
+ for(let i = 0, length = $listeners.length; i < length; i++) {
1222
+ $listeners[i]($currentValue, $previousValue, operations);
1223
+ }
1224
+ };
1242
1225
 
1243
- /**
1244
- * Returns a two-way synchronized follower of the store.
1245
- * Writing to the follower propagates the value back to the store and all its subscribers.
1246
- * Throws if called on a composed store — use Store.follow() instead.
1247
- * Call follower.destroy() or follower.dispose() to unsubscribe.
1248
- *
1249
- * @param {string} name
1250
- * @returns {ObservableItem}
1251
- */
1252
- use(name) {
1253
- const item = $getStoreOrThrow('use', name);
1226
+ ObservableItem.prototype.triggerWatchers = function(operations) {
1227
+ const $watchers = this.$watchers;
1228
+ const $previousValue = this.$previousValue;
1229
+ const $currentValue = this.$currentValue;
1254
1230
 
1255
- if (item.composed) {
1256
- DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
1257
- throw new NativeDocumentError(
1258
- `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
1259
- );
1260
- }
1231
+ const $currentValueCallbacks = $watchers.get($currentValue);
1232
+ const $previousValueCallbacks = $watchers.get($previousValue);
1233
+ if($currentValueCallbacks) {
1234
+ $currentValueCallbacks(true, $previousValue, operations);
1235
+ }
1236
+ if($previousValueCallbacks) {
1237
+ $previousValueCallbacks(false, $currentValue, operations);
1238
+ }
1239
+ };
1261
1240
 
1262
- const { observer: originalObserver, subscribers } = item;
1263
- const observerFollower = $createObservable(originalObserver.val());
1241
+ ObservableItem.prototype.triggerAll = function(operations) {
1242
+ this.triggerWatchers(operations);
1243
+ this.triggerListeners(operations);
1244
+ };
1264
1245
 
1265
- const onStoreChange = value => observerFollower.set(value);
1266
- const onFollowerChange = value => originalObserver.set(value);
1246
+ ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1247
+ this.triggerWatchers(operations);
1248
+ this.triggerFirstListener(operations);
1249
+ };
1267
1250
 
1268
- originalObserver.subscribe(onStoreChange);
1269
- observerFollower.subscribe(onFollowerChange);
1251
+ ObservableItem.prototype.assocTrigger = function() {
1252
+ this.$firstListener = null;
1253
+ if(this.$watchers?.size && this.$listeners?.length) {
1254
+ this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1255
+ return;
1256
+ }
1257
+ if(this.$listeners?.length) {
1258
+ if(this.$listeners.length === 1) {
1259
+ this.$firstListener = this.$listeners[0];
1260
+ this.trigger = this.$firstListener.length === 0 ? this.$firstListener : this.triggerFirstListener;
1261
+ }
1262
+ else {
1263
+ this.trigger = this.triggerListeners;
1264
+ }
1265
+ return;
1266
+ }
1267
+ if(this.$watchers?.size) {
1268
+ this.trigger = this.triggerWatchers;
1269
+ return;
1270
+ }
1271
+ this.trigger = noneTrigger;
1272
+ };
1273
+ ObservableItem.prototype.trigger = noneTrigger;
1270
1274
 
1271
- observerFollower.destroy = () => {
1272
- originalObserver.unsubscribe(onStoreChange);
1273
- observerFollower.unsubscribe(onFollowerChange);
1274
- subscribers.delete(observerFollower);
1275
- observerFollower.cleanup();
1276
- };
1277
- observerFollower.dispose = observerFollower.destroy;
1275
+ ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1276
+ newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1277
+ if(this.$currentValue === newValue) {
1278
+ return;
1279
+ }
1280
+ this.$previousValue = this.$currentValue;
1281
+ this.$currentValue = newValue;
1282
+ {
1283
+ PluginsManager.emit('ObservableBeforeChange', this);
1284
+ }
1285
+ this.trigger();
1286
+ this.$previousValue = null;
1287
+ {
1288
+ PluginsManager.emit('ObservableAfterChange', this);
1289
+ }
1290
+ };
1278
1291
 
1279
- subscribers.add(observerFollower);
1280
- return observerFollower;
1281
- },
1292
+ /**
1293
+ * @param {*} data
1294
+ */
1295
+ ObservableItem.prototype.$setWithInterceptor = function(data) {
1296
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1297
+ const result = this.$interceptor(newValue, this.$currentValue);
1282
1298
 
1283
- /**
1284
- * Returns a read-only follower of the store.
1285
- * The follower reflects store changes but cannot write back to the store.
1286
- * Any attempt to call .set(), .toggle() or .reset() will throw.
1287
- * Call follower.destroy() or follower.dispose() to unsubscribe.
1288
- *
1289
- * @param {string} name
1290
- * @returns {ObservableItem}
1291
- */
1292
- follow(name) {
1293
- const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
1294
- const observerFollower = $createObservable(originalObserver.val());
1299
+ if (result !== undefined) {
1300
+ newValue = result;
1301
+ }
1295
1302
 
1296
- const onStoreChange = value => observerFollower.set(value);
1297
- originalObserver.subscribe(onStoreChange);
1303
+ this.$updateWithNewValue(newValue);
1304
+ };
1298
1305
 
1299
- $applyReadOnly(observerFollower, name, 'follow');
1306
+ /**
1307
+ * @param {*} data
1308
+ */
1309
+ ObservableItem.prototype.$basicSet = function(data) {
1310
+ let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1311
+ this.$updateWithNewValue(newValue);
1312
+ };
1300
1313
 
1301
- observerFollower.destroy = () => {
1302
- originalObserver.unsubscribe(onStoreChange);
1303
- subscribers.delete(observerFollower);
1304
- observerFollower.cleanup();
1305
- };
1306
- observerFollower.dispose = observerFollower.destroy;
1314
+ ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1307
1315
 
1308
- subscribers.add(observerFollower);
1309
- return observerFollower;
1310
- },
1316
+ ObservableItem.prototype.val = function() {
1317
+ return this.$currentValue;
1318
+ };
1311
1319
 
1312
- /**
1313
- * Returns the raw store observer directly (no follower, no cleanup contract).
1314
- * Use this for direct read access when you don't need to unsubscribe.
1315
- * WARNING : mutations on this observer impact all subscribers immediately.
1316
- *
1317
- * @param {string} name
1318
- * @returns {ObservableItem|null}
1319
- */
1320
- get(name) {
1321
- const item = $stores.get(name);
1322
- if (!item) {
1323
- DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
1324
- return null;
1325
- }
1326
- return item.observer;
1327
- },
1320
+ ObservableItem.prototype.disconnectAll = function() {
1321
+ this.$previousValue = null;
1322
+ this.$currentValue = null;
1323
+ this.$listeners = null;
1324
+ this.$watchers = null;
1325
+ this.trigger = noneTrigger;
1326
+ };
1328
1327
 
1329
- /**
1330
- * @param {string} name
1331
- * @returns {{ observer: ObservableItem, subscribers: Set } | null}
1332
- */
1333
- getWithSubscribers(name) {
1334
- return $stores.get(name) ?? null;
1335
- },
1336
-
1337
- /**
1338
- * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
1339
- *
1340
- * @param {string} name
1341
- */
1342
- delete(name) {
1343
- const item = $stores.get(name);
1344
- if (!item) {
1345
- DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
1346
- return;
1347
- }
1348
- item.subscribers.forEach(follower => follower.destroy());
1349
- item.subscribers.clear();
1350
- item.observer.cleanup();
1351
- $stores.delete(name);
1352
- },
1353
- /**
1354
- * Creates an isolated store group with its own state namespace.
1355
- * Each group is a fully independent StoreFactory instance —
1356
- * no key conflicts, no shared state with the parent store.
1357
- *
1358
- * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
1359
- * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
1360
- * @returns {ReturnType<typeof StoreFactory>}
1361
- *
1362
- * @example
1363
- * // With name (recommended)
1364
- * const EventStore = Store.group('events', (group) => {
1365
- * group.create('catalog', []);
1366
- * group.create('filters', { category: null, date: null });
1367
- * group.createResettable('selected', null);
1368
- * group.createComposed('filtered', () => {
1369
- * const catalog = EventStore.get('catalog').val();
1370
- * const filters = EventStore.get('filters').val();
1371
- * return catalog.filter(event => {
1372
- * if (filters.category && event.category !== filters.category) return false;
1373
- * return true;
1374
- * });
1375
- * }, ['catalog', 'filters']);
1376
- * });
1377
- *
1378
- * // Without name
1379
- * const CartStore = Store.group((group) => {
1380
- * group.create('items', []);
1381
- * });
1382
- *
1383
- * // Usage
1384
- * EventStore.use('catalog'); // two-way follower
1385
- * EventStore.follow('filtered'); // read-only follower
1386
- * EventStore.get('filters'); // raw observable
1387
- *
1388
- * // Cross-group composed
1389
- * const OrderStore = Store.group('orders', (group) => {
1390
- * group.createComposed('summary', () => {
1391
- * const items = CartStore.get('items').val();
1392
- * const events = EventStore.get('catalog').val();
1393
- * return { items, events };
1394
- * }, [CartStore.get('items'), EventStore.get('catalog')]);
1395
- * });
1396
- */
1397
- group(name, callback) {
1398
- if (typeof name === 'function') {
1399
- callback = name;
1400
- name = 'anonymous';
1401
- }
1402
- const store = StoreFactory();
1403
- callback && callback(store);
1404
- return store;
1405
- },
1406
- createPersistent(name, value, localstorage_key) {
1407
- localstorage_key = localstorage_key || name;
1408
- const observer = this.create(name, $getFromStorage(localstorage_key, value));
1409
- const saver = $saveToStorage(value);
1410
-
1411
- observer.subscribe((val) => saver(localstorage_key, val));
1412
- return observer;
1413
- },
1414
- createPersistentResettable(name, value, localstorage_key) {
1415
- localstorage_key = localstorage_key || name;
1416
- const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
1417
- const saver = $saveToStorage(value);
1418
- observer.subscribe((val) => saver(localstorage_key, val));
1419
-
1420
- const originalReset = observer.reset.bind(observer);
1421
- observer.reset = () => {
1422
- LocalStorage.remove(localstorage_key);
1423
- originalReset();
1424
- };
1425
-
1426
- return observer;
1427
- }
1428
- };
1429
-
1430
-
1431
- return new Proxy($api, {
1432
- get(target, prop) {
1433
- if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
1434
- return target[prop];
1435
- }
1436
- if (target.has(prop)) {
1437
- if ($followersCache.has(prop)) {
1438
- return $followersCache.get(prop);
1439
- }
1440
- const follower = target.follow(prop);
1441
- $followersCache.set(prop, follower);
1442
- return follower;
1443
- }
1444
- return undefined;
1445
- },
1446
- set(target, prop, value) {
1447
- DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
1448
- throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
1449
- },
1450
- deleteProperty(target, prop) {
1451
- throw new NativeDocumentError(`Store keys cannot be deleted.`);
1452
- }
1453
- });
1454
- };
1455
-
1456
- const Store = StoreFactory();
1457
-
1458
- Store.create('locale', navigator.language.split('-')[0] || 'en');
1459
-
1460
- const $parseDateParts = (value, locale) => {
1461
- const d = new Date(value);
1462
- return {
1463
- d,
1464
- parts: new Intl.DateTimeFormat(locale, {
1465
- year: 'numeric',
1466
- month: 'long',
1467
- day: '2-digit',
1468
- hour: '2-digit',
1469
- minute: '2-digit',
1470
- second: '2-digit',
1471
- }).formatToParts(d).reduce((acc, { type, value }) => {
1472
- acc[type] = value;
1473
- return acc;
1474
- }, {})
1475
- };
1476
- };
1477
-
1478
- const $applyDatePattern = (pattern, d, parts) => {
1479
- const pad = n => String(n).padStart(2, '0');
1480
- return pattern
1481
- .replace('YYYY', parts.year)
1482
- .replace('YY', parts.year.slice(-2))
1483
- .replace('MMMM', parts.month)
1484
- .replace('MMM', parts.month.slice(0, 3))
1485
- .replace('MM', pad(d.getMonth() + 1))
1486
- .replace('DD', pad(d.getDate()))
1487
- .replace('D', d.getDate())
1488
- .replace('HH', parts.hour)
1489
- .replace('mm', parts.minute)
1490
- .replace('ss', parts.second);
1491
- };
1492
-
1493
- const Formatters = {
1494
- currency: (value, locale, { currency = 'XOF', notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1495
- new Intl.NumberFormat(locale, {
1496
- style: 'currency',
1497
- currency,
1498
- notation,
1499
- minimumFractionDigits,
1500
- maximumFractionDigits
1501
- }).format(value),
1502
-
1503
- number: (value, locale, { notation, minimumFractionDigits, maximumFractionDigits } = {}) =>
1504
- new Intl.NumberFormat(locale, {
1505
- notation,
1506
- minimumFractionDigits,
1507
- maximumFractionDigits
1508
- }).format(value),
1509
-
1510
- percent: (value, locale, { decimals = 1 } = {}) =>
1511
- new Intl.NumberFormat(locale, {
1512
- style: 'percent',
1513
- maximumFractionDigits: decimals
1514
- }).format(value),
1515
-
1516
- date: (value, locale, { format, dateStyle = 'long' } = {}) => {
1517
- if (format) {
1518
- const { d, parts } = $parseDateParts(value, locale);
1519
- return $applyDatePattern(format, d, parts);
1520
- }
1521
- return new Intl.DateTimeFormat(locale, { dateStyle }).format(new Date(value));
1522
- },
1523
-
1524
- time: (value, locale, { format, hour = '2-digit', minute = '2-digit', second } = {}) => {
1525
- if (format) {
1526
- const { d, parts } = $parseDateParts(value, locale);
1527
- return $applyDatePattern(format, d, parts);
1528
- }
1529
- return new Intl.DateTimeFormat(locale, { hour, minute, second }).format(new Date(value));
1530
- },
1531
-
1532
- datetime: (value, locale, { format, dateStyle = 'long', hour = '2-digit', minute = '2-digit', second } = {}) => {
1533
- if (format) {
1534
- const { d, parts } = $parseDateParts(value, locale);
1535
- return $applyDatePattern(format, d, parts);
1536
- }
1537
- return new Intl.DateTimeFormat(locale, { dateStyle, hour, minute, second }).format(new Date(value));
1538
- },
1539
-
1540
- relative: (value, locale, { unit = 'day', numeric = 'auto' } = {}) => {
1541
- const diff = Math.round((value - Date.now()) / (1000 * 60 * 60 * 24));
1542
- return new Intl.RelativeTimeFormat(locale, { numeric }).format(diff, unit);
1543
- },
1544
-
1545
- plural: (value, locale, { singular, plural } = {}) => {
1546
- const rule = new Intl.PluralRules(locale).select(value);
1547
- return `${value} ${rule === 'one' ? singular : plural}`;
1548
- },
1549
- };
1550
-
1551
- /**
1552
- *
1553
- * @param {*} value
1554
- * @param {{ propagation: boolean, reset: boolean} | null} configs
1555
- * @class ObservableItem
1556
- */
1557
- function ObservableItem(value, configs = null) {
1558
- value = Validator.isObservable(value) ? value.val() : value;
1559
-
1560
- this.$previousValue = null;
1561
- this.$currentValue = value;
1562
- {
1563
- this.$isCleanedUp = false;
1564
- }
1565
-
1566
- this.$firstListener = null;
1567
- this.$listeners = null;
1568
- this.$watchers = null;
1569
-
1570
- this.$memoryId = null;
1571
-
1572
- if(configs) {
1573
- this.configs = configs;
1574
- if(configs.reset) {
1575
- this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
1576
- }
1577
- }
1578
- {
1579
- PluginsManager.emit('CreateObservable', this);
1580
- }
1581
- }
1582
-
1583
- Object.defineProperty(ObservableItem.prototype, '$value', {
1584
- get() {
1585
- return this.$currentValue;
1586
- },
1587
- set(value) {
1588
- this.set(value);
1589
- },
1590
- configurable: true,
1591
- });
1592
-
1593
- ObservableItem.prototype.__$isObservable = true;
1594
- const noneTrigger = function() {};
1595
-
1596
- /**
1597
- * Intercepts and transforms values before they are set on the observable.
1598
- * The interceptor can modify the value or return undefined to use the original value.
1599
- *
1600
- * @param {(value) => any} callback - Interceptor function that receives (newValue, currentValue) and returns the transformed value or undefined
1601
- * @returns {ObservableItem} The observable instance for chaining
1602
- * @example
1603
- * const count = Observable(0);
1604
- * count.intercept((newVal, oldVal) => Math.max(0, newVal)); // Prevent negative values
1605
- */
1606
- ObservableItem.prototype.intercept = function(callback) {
1607
- this.$interceptor = callback;
1608
- this.set = this.$setWithInterceptor;
1609
- return this;
1610
- };
1611
-
1612
- ObservableItem.prototype.triggerFirstListener = function(operations) {
1613
- this.$firstListener(this.$currentValue, this.$previousValue, operations);
1614
- };
1615
-
1616
- ObservableItem.prototype.triggerListeners = function(operations) {
1617
- const $listeners = this.$listeners;
1618
- const $previousValue = this.$previousValue;
1619
- const $currentValue = this.$currentValue;
1620
-
1621
- for(let i = 0, length = $listeners.length; i < length; i++) {
1622
- $listeners[i]($currentValue, $previousValue, operations);
1623
- }
1624
- };
1625
-
1626
- ObservableItem.prototype.triggerWatchers = function(operations) {
1627
- const $watchers = this.$watchers;
1628
- const $previousValue = this.$previousValue;
1629
- const $currentValue = this.$currentValue;
1630
-
1631
- const $currentValueCallbacks = $watchers.get($currentValue);
1632
- const $previousValueCallbacks = $watchers.get($previousValue);
1633
- if($currentValueCallbacks) {
1634
- $currentValueCallbacks(true, $previousValue, operations);
1635
- }
1636
- if($previousValueCallbacks) {
1637
- $previousValueCallbacks(false, $currentValue, operations);
1638
- }
1639
- };
1640
-
1641
- ObservableItem.prototype.triggerAll = function(operations) {
1642
- this.triggerWatchers(operations);
1643
- this.triggerListeners(operations);
1644
- };
1645
-
1646
- ObservableItem.prototype.triggerWatchersAndFirstListener = function(operations) {
1647
- this.triggerWatchers(operations);
1648
- this.triggerFirstListener(operations);
1649
- };
1650
-
1651
- ObservableItem.prototype.assocTrigger = function() {
1652
- this.$firstListener = null;
1653
- if(this.$watchers?.size && this.$listeners?.length) {
1654
- this.trigger = (this.$listeners.length === 1) ? this.triggerWatchersAndFirstListener : this.triggerAll;
1655
- return;
1656
- }
1657
- if(this.$listeners?.length) {
1658
- if(this.$listeners.length === 1) {
1659
- this.$firstListener = this.$listeners[0];
1660
- this.trigger = this.triggerFirstListener;
1661
- }
1662
- else {
1663
- this.trigger = this.triggerListeners;
1664
- }
1665
- return;
1666
- }
1667
- if(this.$watchers?.size) {
1668
- this.trigger = this.triggerWatchers;
1669
- return;
1670
- }
1671
- this.trigger = noneTrigger;
1672
- };
1673
- ObservableItem.prototype.trigger = noneTrigger;
1674
-
1675
- ObservableItem.prototype.$updateWithNewValue = function(newValue) {
1676
- newValue = newValue?.__$isObservable ? newValue.val() : newValue;
1677
- if(this.$currentValue === newValue) {
1678
- return;
1679
- }
1680
- this.$previousValue = this.$currentValue;
1681
- this.$currentValue = newValue;
1682
- {
1683
- PluginsManager.emit('ObservableBeforeChange', this);
1684
- }
1685
- this.trigger();
1686
- this.$previousValue = null;
1687
- {
1688
- PluginsManager.emit('ObservableAfterChange', this);
1689
- }
1690
- };
1691
-
1692
- /**
1693
- * @param {*} data
1694
- */
1695
- ObservableItem.prototype.$setWithInterceptor = function(data) {
1696
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1697
- const result = this.$interceptor(newValue, this.$currentValue);
1698
-
1699
- if (result !== undefined) {
1700
- newValue = result;
1701
- }
1702
-
1703
- this.$updateWithNewValue(newValue);
1704
- };
1705
-
1706
- /**
1707
- * @param {*} data
1708
- */
1709
- ObservableItem.prototype.$basicSet = function(data) {
1710
- let newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
1711
- this.$updateWithNewValue(newValue);
1712
- };
1713
-
1714
- ObservableItem.prototype.set = ObservableItem.prototype.$basicSet;
1715
-
1716
- ObservableItem.prototype.val = function() {
1717
- return this.$currentValue;
1718
- };
1719
-
1720
- ObservableItem.prototype.disconnectAll = function() {
1721
- this.$previousValue = null;
1722
- this.$currentValue = null;
1723
- this.$listeners = null;
1724
- this.$watchers = null;
1725
- this.trigger = noneTrigger;
1726
- };
1727
-
1728
- /**
1729
- * Registers a cleanup callback that will be executed when the observable is cleaned up.
1730
- * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1731
- *
1732
- * @param {Function} callback - Cleanup function to execute on observable disposal
1733
- * @example
1734
- * const obs = Observable(0);
1735
- * obs.onCleanup(() => console.log('Cleaned up!'));
1736
- * obs.cleanup(); // Logs: "Cleaned up!"
1737
- */
1738
- ObservableItem.prototype.onCleanup = function(callback) {
1739
- this.$cleanupListeners = this.$cleanupListeners ?? [];
1740
- this.$cleanupListeners.push(callback);
1741
- };
1328
+ /**
1329
+ * Registers a cleanup callback that will be executed when the observable is cleaned up.
1330
+ * Useful for disposing resources, removing event listeners, or other cleanup tasks.
1331
+ *
1332
+ * @param {Function} callback - Cleanup function to execute on observable disposal
1333
+ * @example
1334
+ * const obs = Observable(0);
1335
+ * obs.onCleanup(() => console.log('Cleaned up!'));
1336
+ * obs.cleanup(); // Logs: "Cleaned up!"
1337
+ */
1338
+ ObservableItem.prototype.onCleanup = function(callback) {
1339
+ this.$cleanupListeners = this.$cleanupListeners ?? [];
1340
+ this.$cleanupListeners.push(callback);
1341
+ };
1742
1342
 
1743
1343
  ObservableItem.prototype.cleanup = function() {
1744
1344
  if (this.$cleanupListeners) {
@@ -2203,7 +1803,6 @@ var NativeDocument = (function (exports) {
2203
1803
  }
2204
1804
  element.classes.toggle(className, value);
2205
1805
  }
2206
- data = null;
2207
1806
  };
2208
1807
 
2209
1808
  /**
@@ -3267,29 +2866,71 @@ var NativeDocument = (function (exports) {
3267
2866
  }
3268
2867
  const steps = [];
3269
2868
  if(this.$ndMethods) {
3270
- steps.push((clonedNode, data) => {
3271
- for(const methodName in this.$ndMethods) {
2869
+ const methods = Object.keys(this.$ndMethods);
2870
+ if(methods.length === 1) {
2871
+ const methodName = methods[0];
2872
+ steps.push((clonedNode, data) => {
3272
2873
  clonedNode.nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
3273
- }
3274
- });
2874
+ });
2875
+ } else {
2876
+ steps.push((clonedNode, data) => {
2877
+ const nd = clonedNode.nd;
2878
+ for(const methodName in this.$ndMethods) {
2879
+ nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
2880
+ }
2881
+ });
2882
+ }
3275
2883
  }
3276
2884
  if(this.$classes) {
3277
2885
  const cache = {};
3278
- steps.push((clonedNode, data) => {
3279
- ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
3280
- });
2886
+ const keys = Object.keys(this.$classes);
2887
+
2888
+ if(keys.length === 1) {
2889
+ const key = keys[0];
2890
+ const callback = this.$classes[key];
2891
+ steps.push((clonedNode, data) => {
2892
+ cache[key] = callback.apply(null, data);
2893
+ ElementCreator.processClassAttribute(clonedNode, cache);
2894
+ });
2895
+ } else {
2896
+ steps.push((clonedNode, data) => {
2897
+ ElementCreator.processClassAttribute(clonedNode, buildProperties(cache, this.$classes, data));
2898
+ });
2899
+ }
3281
2900
  }
3282
2901
  if(this.$styles) {
3283
2902
  const cache = {};
3284
- steps.push((clonedNode, data) => {
3285
- ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
3286
- });
2903
+ const keys = Object.keys(this.$styles);
2904
+
2905
+ if(keys.length === 1) {
2906
+ const key = keys[0];
2907
+ const callback = this.$styles[key];
2908
+ steps.push((clonedNode, data) => {
2909
+ cache[key] = callback.apply(null, data);
2910
+ ElementCreator.processStyleAttribute(clonedNode, cache);
2911
+ });
2912
+ } else {
2913
+ steps.push((clonedNode, data) => {
2914
+ ElementCreator.processStyleAttribute(clonedNode, buildProperties(cache, this.$styles, data));
2915
+ });
2916
+ }
3287
2917
  }
3288
2918
  if(this.$attrs) {
3289
2919
  const cache = {};
3290
- steps.push((clonedNode, data) => {
3291
- ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
3292
- });
2920
+ const keys = Object.keys(this.$attrs);
2921
+
2922
+ if(keys.length === 1) {
2923
+ const key = keys[0];
2924
+ const callback = this.$attrs[key];
2925
+ steps.push((clonedNode, data) => {
2926
+ cache[key] = callback.apply(null, data);
2927
+ ElementCreator.processAttributes(clonedNode, cache);
2928
+ });
2929
+ } else {
2930
+ steps.push((clonedNode, data) => {
2931
+ ElementCreator.processAttributes(clonedNode, buildProperties(cache, this.$attrs, data));
2932
+ });
2933
+ }
3293
2934
  }
3294
2935
 
3295
2936
  const stepsCount = steps.length;
@@ -3379,8 +3020,7 @@ var NativeDocument = (function (exports) {
3379
3020
  $node.dynamicCloneNode = (data) => {
3380
3021
  const clonedNode = $node.nodeCloner.cloneNode(data);
3381
3022
  for(let i = 0; i < childNodesLength; i++) {
3382
- const child = childNodes[i].dynamicCloneNode(data);
3383
- clonedNode.appendChild(child);
3023
+ clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
3384
3024
  }
3385
3025
  return clonedNode;
3386
3026
  };
@@ -3388,8 +3028,7 @@ var NativeDocument = (function (exports) {
3388
3028
  $node.dynamicCloneNode = (data) => {
3389
3029
  const clonedNode = $node.cloneNode();
3390
3030
  for(let i = 0; i < childNodesLength; i++) {
3391
- const child = childNodes[i].dynamicCloneNode(data);
3392
- clonedNode.appendChild(child);
3031
+ clonedNode.appendChild(childNodes[i].dynamicCloneNode(data));
3393
3032
  }
3394
3033
  return clonedNode;
3395
3034
  };
@@ -4221,491 +3860,891 @@ var NativeDocument = (function (exports) {
4221
3860
  };
4222
3861
 
4223
3862
  /**
4224
- * Triggers a populate operation with the current array, iteration count, and callback.
4225
- * Used internally for rendering optimizations.
3863
+ * Triggers a populate operation with the current array, iteration count, and callback.
3864
+ * Used internally for rendering optimizations.
3865
+ *
3866
+ * @param {number} iteration - Iteration count for rendering
3867
+ * @param {Function} callback - Callback function for rendering items
3868
+ */
3869
+ ObservableArray.prototype.populateAndRender = function(iteration, callback) {
3870
+ this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
3871
+ };
3872
+
3873
+
3874
+ /**
3875
+ * Creates a filtered view of the array based on predicates.
3876
+ * The filtered array updates automatically when source data or predicates change.
3877
+ *
3878
+ * @param {Object} predicates - Object mapping property names to filter conditions or functions
3879
+ * @returns {ObservableArray} A new observable array containing filtered items
3880
+ * @example
3881
+ * const users = Observable.array([
3882
+ * { name: 'John', age: 25 },
3883
+ * { name: 'Jane', age: 30 }
3884
+ * ]);
3885
+ * const adults = users.where({ age: (val) => val >= 18 });
3886
+ */
3887
+ ObservableArray.prototype.where = function(predicates) {
3888
+ const sourceArray = this;
3889
+ const observableDependencies = [sourceArray];
3890
+ const filterCallbacks = {};
3891
+
3892
+ for (const [key, rawPredicate] of Object.entries(predicates)) {
3893
+ const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
3894
+ if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
3895
+ filterCallbacks[key] = predicate.callback;
3896
+
3897
+ if (predicate.dependencies) {
3898
+ const deps = Array.isArray(predicate.dependencies)
3899
+ ? predicate.dependencies
3900
+ : [predicate.dependencies];
3901
+ observableDependencies.push.apply(observableDependencies, deps);
3902
+ }
3903
+ } else if(typeof predicate === 'function') {
3904
+ filterCallbacks[key] = predicate;
3905
+ } else {
3906
+ filterCallbacks[key] = (value) => value === predicate;
3907
+ }
3908
+ }
3909
+
3910
+ const viewArray = Observable.array();
3911
+
3912
+ const filters = Object.entries(filterCallbacks);
3913
+ const updateView = () => {
3914
+ const filtered = sourceArray.val().filter(item => {
3915
+ for (const [key, callback] of filters) {
3916
+ if(key === '_') {
3917
+ if (!callback(item)) return false;
3918
+ } else {
3919
+ if (!callback(item[key])) return false;
3920
+ }
3921
+ }
3922
+ return true;
3923
+ });
3924
+
3925
+ viewArray.set(filtered);
3926
+ };
3927
+
3928
+ observableDependencies.forEach(dep => dep.subscribe(updateView));
3929
+
3930
+ updateView();
3931
+
3932
+ return viewArray;
3933
+ };
3934
+
3935
+ /**
3936
+ * Creates a filtered view where at least one of the specified fields matches the filter.
3937
+ *
3938
+ * @param {Array<string>} fields - Array of field names to check
3939
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
3940
+ * @returns {ObservableArray} A new observable array containing filtered items
3941
+ * @example
3942
+ * const products = Observable.array([
3943
+ * { name: 'Apple', category: 'Fruit' },
3944
+ * { name: 'Carrot', category: 'Vegetable' }
3945
+ * ]);
3946
+ * const searchTerm = Observable('App');
3947
+ * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
3948
+ */
3949
+ ObservableArray.prototype.whereSome = function(fields, filter) {
3950
+ return this.where({
3951
+ _: {
3952
+ dependencies: filter.dependencies,
3953
+ callback: (item) => fields.some(field => filter.callback(item[field]))
3954
+ }
3955
+ });
3956
+ };
3957
+
3958
+ /**
3959
+ * Creates a filtered view where all specified fields match the filter.
3960
+ *
3961
+ * @param {Array<string>} fields - Array of field names to check
3962
+ * @param {FilterResult} filter - Filter condition with callback and dependencies
3963
+ * @returns {ObservableArray} A new observable array containing filtered items
3964
+ * @example
3965
+ * const items = Observable.array([
3966
+ * { status: 'active', verified: true },
3967
+ * { status: 'active', verified: false }
3968
+ * ]);
3969
+ * const activeFilter = equals('active');
3970
+ * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
3971
+ */
3972
+ ObservableArray.prototype.whereEvery = function(fields, filter) {
3973
+ return this.where({
3974
+ _: {
3975
+ dependencies: filter.dependencies,
3976
+ callback: (item) => fields.every(field => filter.callback(item[field]))
3977
+ }
3978
+ });
3979
+ };
3980
+
3981
+ ObservableArray.prototype.deepSubscribe = function(callback) {
3982
+ const updatedValue = nextTick(() => callback(this.val()));
3983
+ const $listeners = new WeakMap();
3984
+
3985
+ const bindItem = (item) => {
3986
+ if ($listeners.has(item)) {
3987
+ return;
3988
+ }
3989
+ if (item?.__$isObservableArray) {
3990
+ $listeners.set(item, item.deepSubscribe(updatedValue));
3991
+ return;
3992
+ }
3993
+ if (item?.__$isObservable) {
3994
+ item.subscribe(updatedValue);
3995
+ $listeners.set(item, () => item.unsubscribe(updatedValue));
3996
+ }
3997
+ };
3998
+
3999
+ const unbindItem = (item) => {
4000
+ const unsub = $listeners.get(item);
4001
+ if (unsub) {
4002
+ unsub();
4003
+ $listeners.delete(item);
4004
+ }
4005
+ };
4006
+
4007
+ this.$currentValue.forEach(bindItem);
4008
+ this.subscribe(updatedValue);
4009
+
4010
+ this.subscribe((items, _, operations) => {
4011
+ switch (operations?.action) {
4012
+ case 'push':
4013
+ case 'unshift':
4014
+ operations.args.forEach(bindItem);
4015
+ break;
4016
+
4017
+ case 'splice': {
4018
+ const [start, deleteCount, ...newItems] = operations.args;
4019
+ operations.result?.forEach(unbindItem);
4020
+ newItems.forEach(bindItem);
4021
+ break;
4022
+ }
4023
+
4024
+ case 'remove':
4025
+ unbindItem(operations.result);
4026
+ break;
4027
+
4028
+ case 'merge':
4029
+ operations.args.forEach(bindItem);
4030
+ break;
4031
+
4032
+ case 'clear':
4033
+ this.$currentValue.forEach(unbindItem);
4034
+ break;
4035
+ }
4036
+ });
4037
+
4038
+ return () => {
4039
+ this.$currentValue.forEach(unbindItem);
4040
+ };
4041
+ };
4042
+
4043
+ /**
4044
+ * Creates an observable array with reactive array methods.
4045
+ * All mutations trigger updates automatically.
4046
+ *
4047
+ * @param {Array} [target=[]] - Initial array value
4048
+ * @param {Object|null} [configs=null] - Configuration options
4049
+ * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
4050
+ * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
4051
+ * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
4052
+ * @returns {ObservableArray} An observable array with reactive methods
4053
+ * @example
4054
+ * const items = Observable.array([1, 2, 3]);
4055
+ * items.push(4); // Triggers update
4056
+ * items.subscribe((arr) => console.log(arr));
4057
+ */
4058
+ Observable.array = function(target = [], configs = null) {
4059
+ return new ObservableArray(target, configs);
4060
+ };
4061
+
4062
+ /**
4226
4063
  *
4227
- * @param {number} iteration - Iteration count for rendering
4228
- * @param {Function} callback - Callback function for rendering items
4064
+ * @param {Function} callback
4065
+ * @returns {Function}
4229
4066
  */
4230
- ObservableArray.prototype.populateAndRender = function(iteration, callback) {
4231
- this.trigger({ action: 'populate', args: [this.$currentValue, iteration, callback] });
4067
+ Observable.batch = function(callback) {
4068
+ const $observer = Observable(0);
4069
+ const batch = function() {
4070
+ if(Validator.isAsyncFunction(callback)) {
4071
+ return (callback(...arguments)).then(() => {
4072
+ $observer.trigger();
4073
+ }).catch(error => { throw error; });
4074
+ }
4075
+ callback(...arguments);
4076
+ $observer.trigger();
4077
+ };
4078
+ batch.$observer = $observer;
4079
+ return batch;
4232
4080
  };
4233
4081
 
4082
+ const ObservableObject = function(target, configs) {
4083
+ ObservableItem.call(this, target);
4084
+ this.$observables = {};
4085
+ this.configs = configs;
4234
4086
 
4235
- /**
4236
- * Creates a filtered view of the array based on predicates.
4237
- * The filtered array updates automatically when source data or predicates change.
4238
- *
4239
- * @param {Object} predicates - Object mapping property names to filter conditions or functions
4240
- * @returns {ObservableArray} A new observable array containing filtered items
4241
- * @example
4242
- * const users = Observable.array([
4243
- * { name: 'John', age: 25 },
4244
- * { name: 'Jane', age: 30 }
4245
- * ]);
4246
- * const adults = users.where({ age: (val) => val >= 18 });
4247
- */
4248
- ObservableArray.prototype.where = function(predicates) {
4249
- const sourceArray = this;
4250
- const observableDependencies = [sourceArray];
4251
- const filterCallbacks = {};
4087
+ this.$load(target);
4252
4088
 
4253
- for (const [key, rawPredicate] of Object.entries(predicates)) {
4254
- const predicate = Validator.isObservable(rawPredicate) ? match(rawPredicate, false) : rawPredicate;
4255
- if (predicate && typeof predicate === 'object' && 'callback' in predicate) {
4256
- filterCallbacks[key] = predicate.callback;
4089
+ for(const name in target) {
4090
+ if(!Object.hasOwn(this, name)) {
4091
+ Object.defineProperty(this, name, {
4092
+ get: () => this.$observables[name],
4093
+ set: (value) => this.$observables[name].set(value)
4094
+ });
4095
+ }
4096
+ }
4257
4097
 
4258
- if (predicate.dependencies) {
4259
- const deps = Array.isArray(predicate.dependencies)
4260
- ? predicate.dependencies
4261
- : [predicate.dependencies];
4262
- observableDependencies.push.apply(observableDependencies, deps);
4098
+ };
4099
+
4100
+ ObservableObject.prototype = Object.create(ObservableItem.prototype);
4101
+
4102
+ Object.defineProperty(ObservableObject, '$value', {
4103
+ get() {
4104
+ return this.val();
4105
+ },
4106
+ set(value) {
4107
+ this.set(value);
4108
+ }
4109
+ });
4110
+
4111
+ ObservableObject.prototype.__$isObservableObject = true;
4112
+ ObservableObject.prototype.__isProxy__ = true;
4113
+
4114
+ ObservableObject.prototype.$load = function(initialValue) {
4115
+ const configs = this.configs;
4116
+ for(const key in initialValue) {
4117
+ const itemValue = initialValue[key];
4118
+ if(Array.isArray(itemValue)) {
4119
+ if(configs?.deep !== false) {
4120
+ const mappedItemValue = itemValue.map(item => {
4121
+ if(Validator.isJson(item)) {
4122
+ return Observable.json(item, configs);
4123
+ }
4124
+ if(Validator.isArray(item)) {
4125
+ return Observable.array(item, configs);
4126
+ }
4127
+ return Observable(item, configs);
4128
+ });
4129
+ this.$observables[key] = Observable.array(mappedItemValue, configs);
4130
+ continue;
4263
4131
  }
4264
- } else if(typeof predicate === 'function') {
4265
- filterCallbacks[key] = predicate;
4132
+ this.$observables[key] = Observable.array(itemValue, configs);
4133
+ continue;
4134
+ }
4135
+ if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4136
+ this.$observables[key] = itemValue;
4137
+ continue;
4138
+ }
4139
+ this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4140
+ }
4141
+ };
4142
+
4143
+ ObservableObject.prototype.val = function() {
4144
+ const result = {};
4145
+ for(const key in this.$observables) {
4146
+ const dataItem = this.$observables[key];
4147
+ if(Validator.isObservable(dataItem)) {
4148
+ let value = dataItem.val();
4149
+ if(Array.isArray(value)) {
4150
+ value = value.map(item => {
4151
+ if(Validator.isObservable(item)) {
4152
+ return item.val();
4153
+ }
4154
+ if(Validator.isProxy(item)) {
4155
+ return item.$value;
4156
+ }
4157
+ return item;
4158
+ });
4159
+ }
4160
+ result[key] = value;
4161
+ } else if(Validator.isProxy(dataItem)) {
4162
+ result[key] = dataItem.$value;
4266
4163
  } else {
4267
- filterCallbacks[key] = (value) => value === predicate;
4164
+ result[key] = dataItem;
4268
4165
  }
4269
4166
  }
4167
+ return result;
4168
+ };
4169
+ ObservableObject.prototype.$val = ObservableObject.prototype.val;
4270
4170
 
4271
- const viewArray = Observable.array();
4171
+ ObservableObject.prototype.get = function(property) {
4172
+ const item = this.$observables[property];
4173
+ if(Validator.isObservable(item)) {
4174
+ return item.val();
4175
+ }
4176
+ if(Validator.isProxy(item)) {
4177
+ return item.$value;
4178
+ }
4179
+ return item;
4180
+ };
4181
+ ObservableObject.prototype.$get = ObservableObject.prototype.get;
4272
4182
 
4273
- const filters = Object.entries(filterCallbacks);
4274
- const updateView = () => {
4275
- const filtered = sourceArray.val().filter(item => {
4276
- for (const [key, callback] of filters) {
4277
- if(key === '_') {
4278
- if (!callback(item)) return false;
4279
- } else {
4280
- if (!callback(item[key])) return false;
4281
- }
4183
+ ObservableObject.prototype.set = function(newData) {
4184
+ const data = Validator.isProxy(newData) ? newData.$value : newData;
4185
+ const configs = this.configs;
4186
+
4187
+ for(const key in data) {
4188
+ const targetItem = this.$observables[key];
4189
+ const newValueOrigin = newData[key];
4190
+ const newValue = data[key];
4191
+
4192
+ if(Validator.isObservable(targetItem)) {
4193
+ if(!Validator.isArray(newValue)) {
4194
+ targetItem.set(newValue);
4195
+ continue;
4282
4196
  }
4283
- return true;
4284
- });
4197
+ const firstElementFromOriginalValue = newValueOrigin.at(0);
4198
+ if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4199
+ const newValues = newValue.map(item => {
4200
+ if(Validator.isProxy(firstElementFromOriginalValue)) {
4201
+ return Observable.init(item, configs);
4202
+ }
4203
+ return Observable(item, configs);
4204
+ });
4205
+ targetItem.set(newValues);
4206
+ continue;
4207
+ }
4208
+ targetItem.set([...newValue]);
4209
+ continue;
4210
+ }
4211
+ if(Validator.isProxy(targetItem)) {
4212
+ targetItem.update(newValue);
4213
+ continue;
4214
+ }
4215
+ this[key] = newValue;
4216
+ }
4217
+ };
4218
+ ObservableObject.prototype.$set = ObservableObject.prototype.set;
4219
+ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
4285
4220
 
4286
- viewArray.set(filtered);
4287
- };
4221
+ ObservableObject.prototype.observables = function() {
4222
+ return Object.values(this.$observables);
4223
+ };
4224
+ ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
4288
4225
 
4289
- observableDependencies.forEach(dep => dep.subscribe(updateView));
4226
+ ObservableObject.prototype.keys = function() {
4227
+ return Object.keys(this.$observables);
4228
+ };
4229
+ ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4230
+ ObservableObject.prototype.clone = function() {
4231
+ return Observable.init(this.val(), this.configs);
4232
+ };
4233
+ ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4234
+ ObservableObject.prototype.reset = function() {
4235
+ for(const key in this.$observables) {
4236
+ this.$observables[key].reset();
4237
+ }
4238
+ };
4239
+ ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4240
+ ObservableObject.prototype.subscribe = function(callback) {
4241
+ const observables = this.observables();
4242
+ const updatedValue = nextTick(() => this.trigger());
4290
4243
 
4291
- updateView();
4244
+ this.originalSubscribe(callback);
4292
4245
 
4293
- return viewArray;
4246
+ for (let i = 0, length = observables.length; i < length; i++) {
4247
+ const observable = observables[i];
4248
+ if (observable.__$isObservableArray) {
4249
+ observable.deepSubscribe(updatedValue);
4250
+ continue
4251
+ }
4252
+ observable.subscribe(updatedValue);
4253
+ }
4254
+ };
4255
+ ObservableObject.prototype.configs = function() {
4256
+ return this.configs;
4257
+ };
4258
+
4259
+ ObservableObject.prototype.update = ObservableObject.prototype.set;
4260
+
4261
+ Observable.init = function(initialValue, configs = null) {
4262
+ return new ObservableObject(initialValue, configs)
4294
4263
  };
4295
4264
 
4296
4265
  /**
4297
- * Creates a filtered view where at least one of the specified fields matches the filter.
4298
- *
4299
- * @param {Array<string>} fields - Array of field names to check
4300
- * @param {FilterResult} filter - Filter condition with callback and dependencies
4301
- * @returns {ObservableArray} A new observable array containing filtered items
4302
- * @example
4303
- * const products = Observable.array([
4304
- * { name: 'Apple', category: 'Fruit' },
4305
- * { name: 'Carrot', category: 'Vegetable' }
4306
- * ]);
4307
- * const searchTerm = Observable('App');
4308
- * const filtered = products.whereSome(['name', 'category'], match(searchTerm));
4309
- */
4310
- ObservableArray.prototype.whereSome = function(fields, filter) {
4311
- return this.where({
4312
- _: {
4313
- dependencies: filter.dependencies,
4314
- callback: (item) => fields.some(field => filter.callback(item[field]))
4315
- }
4316
- });
4266
+ *
4267
+ * @param {any[]} data
4268
+ * @return Proxy[]
4269
+ */
4270
+ Observable.arrayOfObject = function(data) {
4271
+ return data.map(item => Observable.object(item));
4317
4272
  };
4318
4273
 
4319
4274
  /**
4320
- * Creates a filtered view where all specified fields match the filter.
4321
- *
4322
- * @param {Array<string>} fields - Array of field names to check
4323
- * @param {FilterResult} filter - Filter condition with callback and dependencies
4324
- * @returns {ObservableArray} A new observable array containing filtered items
4325
- * @example
4326
- * const items = Observable.array([
4327
- * { status: 'active', verified: true },
4328
- * { status: 'active', verified: false }
4329
- * ]);
4330
- * const activeFilter = equals('active');
4331
- * const filtered = items.whereEvery(['status', 'verified'], activeFilter);
4275
+ * Get the value of an observable or an object of observables.
4276
+ * @param {ObservableItem|Object<ObservableItem>} data
4277
+ * @returns {{}|*|null}
4332
4278
  */
4333
- ObservableArray.prototype.whereEvery = function(fields, filter) {
4334
- return this.where({
4335
- _: {
4336
- dependencies: filter.dependencies,
4337
- callback: (item) => fields.every(field => filter.callback(item[field]))
4279
+ Observable.value = function(data) {
4280
+ if(Validator.isObservable(data)) {
4281
+ return data.val();
4282
+ }
4283
+ if(Validator.isProxy(data)) {
4284
+ return data.$value;
4285
+ }
4286
+ if(Validator.isArray(data)) {
4287
+ const result = [];
4288
+ for(let i = 0, length = data.length; i < length; i++) {
4289
+ const item = data[i];
4290
+ result.push(Observable.value(item));
4338
4291
  }
4339
- });
4292
+ return result;
4293
+ }
4294
+ return data;
4340
4295
  };
4341
4296
 
4342
- ObservableArray.prototype.deepSubscribe = function(callback) {
4343
- const updatedValue = nextTick(() => callback(this.val()));
4344
- const $listeners = new WeakMap();
4297
+ Observable.object = Observable.init;
4298
+ Observable.json = Observable.init;
4345
4299
 
4346
- const bindItem = (item) => {
4347
- if ($listeners.has(item)) {
4348
- return;
4349
- }
4350
- if (item?.__$isObservableArray) {
4351
- $listeners.set(item, item.deepSubscribe(updatedValue));
4352
- return;
4353
- }
4354
- if (item?.__$isObservable) {
4355
- item.subscribe(updatedValue);
4356
- $listeners.set(item, () => item.unsubscribe(updatedValue));
4357
- }
4358
- };
4300
+ /**
4301
+ * Creates a computed observable that automatically updates when its dependencies change.
4302
+ * The callback is re-executed whenever any dependency observable changes.
4303
+ *
4304
+ * @param {Function} callback - Function that returns the computed value
4305
+ * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4306
+ * @returns {ObservableItem} A new observable that updates automatically
4307
+ * @example
4308
+ * const firstName = Observable('John');
4309
+ * const lastName = Observable('Doe');
4310
+ * const fullName = Observable.computed(
4311
+ * () => `${firstName.val()} ${lastName.val()}`,
4312
+ * [firstName, lastName]
4313
+ * );
4314
+ *
4315
+ * // With batch function
4316
+ * const batch = Observable.batch(() => { ... });
4317
+ * const computed = Observable.computed(() => { ... }, batch);
4318
+ */
4319
+ Observable.computed = function(callback, dependencies = []) {
4320
+ const initialValue = callback();
4321
+ const observable = new ObservableItem(initialValue);
4322
+ const updatedValue = nextTick(() => observable.set(callback()));
4323
+ {
4324
+ PluginsManager.emit('CreateObservableComputed', observable, dependencies);
4325
+ }
4359
4326
 
4360
- const unbindItem = (item) => {
4361
- const unsub = $listeners.get(item);
4362
- if (unsub) {
4363
- unsub();
4364
- $listeners.delete(item);
4327
+ if(Validator.isFunction(dependencies)) {
4328
+ if(!Validator.isObservable(dependencies.$observer)) {
4329
+ throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4365
4330
  }
4366
- };
4367
-
4368
- this.$currentValue.forEach(bindItem);
4369
- this.subscribe(updatedValue);
4331
+ dependencies.$observer.subscribe(updatedValue);
4332
+ return observable;
4333
+ }
4370
4334
 
4371
- this.subscribe((items, _, operations) => {
4372
- switch (operations?.action) {
4373
- case 'push':
4374
- case 'unshift':
4375
- operations.args.forEach(bindItem);
4376
- break;
4335
+ dependencies.forEach(dependency => {
4336
+ if(Validator.isProxy(dependency)) {
4337
+ dependency.$observables.forEach((observable) => {
4338
+ observable.subscribe(updatedValue);
4339
+ });
4340
+ return;
4341
+ }
4342
+ dependency.subscribe(updatedValue);
4343
+ });
4377
4344
 
4378
- case 'splice': {
4379
- const [start, deleteCount, ...newItems] = operations.args;
4380
- operations.result?.forEach(unbindItem);
4381
- newItems.forEach(bindItem);
4382
- break;
4383
- }
4345
+ return observable;
4346
+ };
4384
4347
 
4385
- case 'remove':
4386
- unbindItem(operations.result);
4387
- break;
4348
+ const StoreFactory = function() {
4388
4349
 
4389
- case 'merge':
4390
- operations.args.forEach(bindItem);
4391
- break;
4350
+ const $stores = new Map();
4351
+ const $followersCache = new Map();
4392
4352
 
4393
- case 'clear':
4394
- this.$currentValue.forEach(unbindItem);
4395
- break;
4353
+ /**
4354
+ * Internal helper — retrieves a store entry or throws if not found.
4355
+ */
4356
+ const $getStoreOrThrow = (method, name) => {
4357
+ const item = $stores.get(name);
4358
+ if (!item) {
4359
+ DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
4360
+ throw new NativeDocumentError(
4361
+ `Store.${method}('${name}') : store not found.`
4362
+ );
4396
4363
  }
4397
- });
4398
-
4399
- return () => {
4400
- this.$currentValue.forEach(unbindItem);
4364
+ return item;
4401
4365
  };
4402
- };
4403
4366
 
4404
- /**
4405
- * Creates an observable array with reactive array methods.
4406
- * All mutations trigger updates automatically.
4407
- *
4408
- * @param {Array} [target=[]] - Initial array value
4409
- * @param {Object|null} [configs=null] - Configuration options
4410
- * // @param {boolean} [configs.propagation=true] - Whether to propagate changes to parent observables
4411
- * // @param {boolean} [configs.deep=false] - Whether to make nested objects observable
4412
- * @param {boolean} [configs.reset=false] - Whether to store initial value for reset()
4413
- * @returns {ObservableArray} An observable array with reactive methods
4414
- * @example
4415
- * const items = Observable.array([1, 2, 3]);
4416
- * items.push(4); // Triggers update
4417
- * items.subscribe((arr) => console.log(arr));
4418
- */
4419
- Observable.array = function(target = [], configs = null) {
4420
- return new ObservableArray(target, configs);
4421
- };
4367
+ /**
4368
+ * Internal helper blocks write operations on a read-only observer.
4369
+ */
4370
+ const $applyReadOnly = (observer, name, context) => {
4371
+ const readOnlyError = (method) => () => {
4372
+ DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
4373
+ throw new NativeDocumentError(
4374
+ `Store.${context}('${name}') is read-only.`
4375
+ );
4376
+ };
4377
+ observer.set = readOnlyError('set');
4378
+ observer.toggle = readOnlyError('toggle');
4379
+ observer.reset = readOnlyError('reset');
4380
+ };
4422
4381
 
4423
- /**
4424
- *
4425
- * @param {Function} callback
4426
- * @returns {Function}
4427
- */
4428
- Observable.batch = function(callback) {
4429
- const $observer = Observable(0);
4430
- const batch = function() {
4431
- if(Validator.isAsyncFunction(callback)) {
4432
- return (callback(...arguments)).then(() => {
4433
- $observer.trigger();
4434
- }).catch(error => { throw error; });
4382
+ const $createObservable = (value, options = {}) => {
4383
+ if(Array.isArray(value)) {
4384
+ return Observable.array(value, options);
4435
4385
  }
4436
- callback(...arguments);
4437
- $observer.trigger();
4386
+ if(typeof value === 'object') {
4387
+ return Observable.object(value, options);
4388
+ }
4389
+ return Observable(value, options);
4438
4390
  };
4439
- batch.$observer = $observer;
4440
- return batch;
4441
- };
4442
4391
 
4443
- const ObservableObject = function(target, configs) {
4444
- ObservableItem.call(this, target);
4445
- this.$observables = {};
4446
- this.configs = configs;
4392
+ const $api = {
4393
+ /**
4394
+ * Create a new state and return the observer.
4395
+ * Throws if a store with the same name already exists.
4396
+ *
4397
+ * @param {string} name
4398
+ * @param {*} value
4399
+ * @returns {ObservableItem}
4400
+ */
4401
+ create(name, value) {
4402
+ if ($stores.has(name)) {
4403
+ DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
4404
+ throw new NativeDocumentError(
4405
+ `Store.create('${name}') : a store with this name already exists.`
4406
+ );
4407
+ }
4408
+ const observer = $createObservable(value);
4409
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
4410
+ return observer;
4411
+ },
4447
4412
 
4448
- this.$load(target);
4413
+ /**
4414
+ * Create a new resettable state and return the observer.
4415
+ * The store can be reset to its initial value via Store.reset(name).
4416
+ * Throws if a store with the same name already exists.
4417
+ *
4418
+ * @param {string} name
4419
+ * @param {*} value
4420
+ * @returns {ObservableItem}
4421
+ */
4422
+ createResettable(name, value) {
4423
+ if ($stores.has(name)) {
4424
+ DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
4425
+ throw new NativeDocumentError(
4426
+ `Store.createResettable('${name}') : a store with this name already exists.`
4427
+ );
4428
+ }
4429
+ const observer = $createObservable(value, { reset: true });
4430
+ $stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
4431
+ return observer;
4432
+ },
4433
+
4434
+ /**
4435
+ * Create a computed store derived from other stores.
4436
+ * The value is automatically recalculated when any dependency changes.
4437
+ * This store is read-only — Store.use() and Store.set() will throw.
4438
+ * Throws if a store with the same name already exists.
4439
+ *
4440
+ * @param {string} name
4441
+ * @param {() => *} computation - Function that returns the computed value
4442
+ * @param {string[]} dependencies - Names of the stores to watch
4443
+ * @returns {ObservableItem}
4444
+ *
4445
+ * @example
4446
+ * Store.create('products', [{ id: 1, price: 10 }]);
4447
+ * Store.create('cart', [{ productId: 1, quantity: 2 }]);
4448
+ *
4449
+ * Store.createComposed('total', () => {
4450
+ * const products = Store.get('products').val();
4451
+ * const cart = Store.get('cart').val();
4452
+ * return cart.reduce((sum, item) => {
4453
+ * const product = products.find(p => p.id === item.productId);
4454
+ * return sum + (product.price * item.quantity);
4455
+ * }, 0);
4456
+ * }, ['products', 'cart']);
4457
+ */
4458
+ createComposed(name, computation, dependencies) {
4459
+ if ($stores.has(name)) {
4460
+ DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
4461
+ throw new NativeDocumentError(
4462
+ `Store.createComposed('${name}') : a store with this name already exists.`
4463
+ );
4464
+ }
4465
+ if (typeof computation !== 'function') {
4466
+ throw new NativeDocumentError(
4467
+ `Store.createComposed('${name}') : computation must be a function.`
4468
+ );
4469
+ }
4470
+ if (!Array.isArray(dependencies) || dependencies.length === 0) {
4471
+ throw new NativeDocumentError(
4472
+ `Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
4473
+ );
4474
+ }
4449
4475
 
4450
- for(const name in target) {
4451
- if(!Object.hasOwn(this, name)) {
4452
- Object.defineProperty(this, name, {
4453
- get: () => this.$observables[name],
4454
- set: (value) => this.$observables[name].set(value)
4476
+ // Resolve dependency observers
4477
+ const depObservers = dependencies.map(depName => {
4478
+ if(typeof depName !== 'string') {
4479
+ return depName;
4480
+ }
4481
+ const depItem = $stores.get(depName);
4482
+ if (!depItem) {
4483
+ DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
4484
+ throw new NativeDocumentError(
4485
+ `Store.createComposed('${name}') : dependency store '${depName}' not found.`
4486
+ );
4487
+ }
4488
+ return depItem.observer;
4455
4489
  });
4456
- }
4457
- }
4458
-
4459
- };
4460
4490
 
4461
- ObservableObject.prototype = Object.create(ObservableItem.prototype);
4491
+ // Create computed observable from dependency observers
4492
+ const observer = Observable.computed(computation, depObservers);
4462
4493
 
4463
- Object.defineProperty(ObservableObject, '$value', {
4464
- get() {
4465
- return this.val();
4466
- },
4467
- set(value) {
4468
- this.set(value);
4469
- }
4470
- });
4494
+ $stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
4495
+ return observer;
4496
+ },
4471
4497
 
4472
- ObservableObject.prototype.__$isObservableObject = true;
4473
- ObservableObject.prototype.__isProxy__ = true;
4498
+ /**
4499
+ * Returns true if a store with the given name exists.
4500
+ *
4501
+ * @param {string} name
4502
+ * @returns {boolean}
4503
+ */
4504
+ has(name) {
4505
+ return $stores.has(name);
4506
+ },
4474
4507
 
4475
- ObservableObject.prototype.$load = function(initialValue) {
4476
- const configs = this.configs;
4477
- for(const key in initialValue) {
4478
- const itemValue = initialValue[key];
4479
- if(Array.isArray(itemValue)) {
4480
- if(configs?.deep !== false) {
4481
- const mappedItemValue = itemValue.map(item => {
4482
- if(Validator.isJson(item)) {
4483
- return Observable.json(item, configs);
4484
- }
4485
- if(Validator.isArray(item)) {
4486
- return Observable.array(item, configs);
4487
- }
4488
- return Observable(item, configs);
4489
- });
4490
- this.$observables[key] = Observable.array(mappedItemValue, configs);
4491
- continue;
4508
+ /**
4509
+ * Resets a resettable store to its initial value and notifies all subscribers.
4510
+ * Throws if the store was not created with createResettable().
4511
+ *
4512
+ * @param {string} name
4513
+ */
4514
+ reset(name) {
4515
+ const item = $getStoreOrThrow('reset', name);
4516
+ if (item.composed) {
4517
+ DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
4518
+ throw new NativeDocumentError(
4519
+ `Store.reset('${name}') : composed stores cannot be reset.`
4520
+ );
4492
4521
  }
4493
- this.$observables[key] = Observable.array(itemValue, configs);
4494
- continue;
4495
- }
4496
- if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
4497
- this.$observables[key] = itemValue;
4498
- continue;
4499
- }
4500
- this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
4501
- }
4502
- };
4522
+ if (!item.resettable) {
4523
+ DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
4524
+ throw new NativeDocumentError(
4525
+ `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
4526
+ );
4527
+ }
4528
+ item.observer.reset();
4529
+ },
4503
4530
 
4504
- ObservableObject.prototype.val = function() {
4505
- const result = {};
4506
- for(const key in this.$observables) {
4507
- const dataItem = this.$observables[key];
4508
- if(Validator.isObservable(dataItem)) {
4509
- let value = dataItem.val();
4510
- if(Array.isArray(value)) {
4511
- value = value.map(item => {
4512
- if(Validator.isObservable(item)) {
4513
- return item.val();
4514
- }
4515
- if(Validator.isProxy(item)) {
4516
- return item.$value;
4517
- }
4518
- return item;
4519
- });
4531
+ /**
4532
+ * Returns a two-way synchronized follower of the store.
4533
+ * Writing to the follower propagates the value back to the store and all its subscribers.
4534
+ * Throws if called on a composed store — use Store.follow() instead.
4535
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4536
+ *
4537
+ * @param {string} name
4538
+ * @returns {ObservableItem}
4539
+ */
4540
+ use(name) {
4541
+ const item = $getStoreOrThrow('use', name);
4542
+
4543
+ if (item.composed) {
4544
+ DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
4545
+ throw new NativeDocumentError(
4546
+ `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
4547
+ );
4520
4548
  }
4521
- result[key] = value;
4522
- } else if(Validator.isProxy(dataItem)) {
4523
- result[key] = dataItem.$value;
4524
- } else {
4525
- result[key] = dataItem;
4526
- }
4527
- }
4528
- return result;
4529
- };
4530
- ObservableObject.prototype.$val = ObservableObject.prototype.val;
4531
4549
 
4532
- ObservableObject.prototype.get = function(property) {
4533
- const item = this.$observables[property];
4534
- if(Validator.isObservable(item)) {
4535
- return item.val();
4536
- }
4537
- if(Validator.isProxy(item)) {
4538
- return item.$value;
4539
- }
4540
- return item;
4541
- };
4542
- ObservableObject.prototype.$get = ObservableObject.prototype.get;
4550
+ const { observer: originalObserver, subscribers } = item;
4551
+ const observerFollower = $createObservable(originalObserver.val());
4543
4552
 
4544
- ObservableObject.prototype.set = function(newData) {
4545
- const data = Validator.isProxy(newData) ? newData.$value : newData;
4546
- const configs = this.configs;
4553
+ const onStoreChange = value => observerFollower.set(value);
4554
+ const onFollowerChange = value => originalObserver.set(value);
4547
4555
 
4548
- for(const key in data) {
4549
- const targetItem = this.$observables[key];
4550
- const newValueOrigin = newData[key];
4551
- const newValue = data[key];
4556
+ originalObserver.subscribe(onStoreChange);
4557
+ observerFollower.subscribe(onFollowerChange);
4552
4558
 
4553
- if(Validator.isObservable(targetItem)) {
4554
- if(!Validator.isArray(newValue)) {
4555
- targetItem.set(newValue);
4556
- continue;
4557
- }
4558
- const firstElementFromOriginalValue = newValueOrigin.at(0);
4559
- if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
4560
- const newValues = newValue.map(item => {
4561
- if(Validator.isProxy(firstElementFromOriginalValue)) {
4562
- return Observable.init(item, configs);
4563
- }
4564
- return Observable(item, configs);
4565
- });
4566
- targetItem.set(newValues);
4567
- continue;
4568
- }
4569
- targetItem.set([...newValue]);
4570
- continue;
4571
- }
4572
- if(Validator.isProxy(targetItem)) {
4573
- targetItem.update(newValue);
4574
- continue;
4575
- }
4576
- this[key] = newValue;
4577
- }
4578
- };
4579
- ObservableObject.prototype.$set = ObservableObject.prototype.set;
4580
- ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
4559
+ observerFollower.destroy = () => {
4560
+ originalObserver.unsubscribe(onStoreChange);
4561
+ observerFollower.unsubscribe(onFollowerChange);
4562
+ subscribers.delete(observerFollower);
4563
+ observerFollower.cleanup();
4564
+ };
4565
+ observerFollower.dispose = observerFollower.destroy;
4581
4566
 
4582
- ObservableObject.prototype.observables = function() {
4583
- return Object.values(this.$observables);
4584
- };
4585
- ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
4567
+ subscribers.add(observerFollower);
4568
+ return observerFollower;
4569
+ },
4570
+
4571
+ /**
4572
+ * Returns a read-only follower of the store.
4573
+ * The follower reflects store changes but cannot write back to the store.
4574
+ * Any attempt to call .set(), .toggle() or .reset() will throw.
4575
+ * Call follower.destroy() or follower.dispose() to unsubscribe.
4576
+ *
4577
+ * @param {string} name
4578
+ * @returns {ObservableItem}
4579
+ */
4580
+ follow(name) {
4581
+ const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
4582
+ const observerFollower = $createObservable(originalObserver.val());
4586
4583
 
4587
- ObservableObject.prototype.keys = function() {
4588
- return Object.keys(this.$observables);
4589
- };
4590
- ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
4591
- ObservableObject.prototype.clone = function() {
4592
- return Observable.init(this.val(), this.configs);
4593
- };
4594
- ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
4595
- ObservableObject.prototype.reset = function() {
4596
- for(const key in this.$observables) {
4597
- this.$observables[key].reset();
4598
- }
4599
- };
4600
- ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
4601
- ObservableObject.prototype.subscribe = function(callback) {
4602
- const observables = this.observables();
4603
- const updatedValue = nextTick(() => this.trigger());
4584
+ const onStoreChange = value => observerFollower.set(value);
4585
+ originalObserver.subscribe(onStoreChange);
4604
4586
 
4605
- this.originalSubscribe(callback);
4587
+ $applyReadOnly(observerFollower, name, 'follow');
4606
4588
 
4607
- for (let i = 0, length = observables.length; i < length; i++) {
4608
- const observable = observables[i];
4609
- if (observable.__$isObservableArray) {
4610
- observable.deepSubscribe(updatedValue);
4611
- continue
4612
- }
4613
- observable.subscribe(updatedValue);
4614
- }
4615
- };
4616
- ObservableObject.prototype.configs = function() {
4617
- return this.configs;
4618
- };
4589
+ observerFollower.destroy = () => {
4590
+ originalObserver.unsubscribe(onStoreChange);
4591
+ subscribers.delete(observerFollower);
4592
+ observerFollower.cleanup();
4593
+ };
4594
+ observerFollower.dispose = observerFollower.destroy;
4619
4595
 
4620
- ObservableObject.prototype.update = ObservableObject.prototype.set;
4596
+ subscribers.add(observerFollower);
4597
+ return observerFollower;
4598
+ },
4621
4599
 
4622
- Observable.init = function(initialValue, configs = null) {
4623
- return new ObservableObject(initialValue, configs)
4624
- };
4600
+ /**
4601
+ * Returns the raw store observer directly (no follower, no cleanup contract).
4602
+ * Use this for direct read access when you don't need to unsubscribe.
4603
+ * WARNING : mutations on this observer impact all subscribers immediately.
4604
+ *
4605
+ * @param {string} name
4606
+ * @returns {ObservableItem|null}
4607
+ */
4608
+ get(name) {
4609
+ const item = $stores.get(name);
4610
+ if (!item) {
4611
+ DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
4612
+ return null;
4613
+ }
4614
+ return item.observer;
4615
+ },
4625
4616
 
4626
- /**
4627
- *
4628
- * @param {any[]} data
4629
- * @return Proxy[]
4630
- */
4631
- Observable.arrayOfObject = function(data) {
4632
- return data.map(item => Observable.object(item));
4633
- };
4617
+ /**
4618
+ * @param {string} name
4619
+ * @returns {{ observer: ObservableItem, subscribers: Set } | null}
4620
+ */
4621
+ getWithSubscribers(name) {
4622
+ return $stores.get(name) ?? null;
4623
+ },
4634
4624
 
4635
- /**
4636
- * Get the value of an observable or an object of observables.
4637
- * @param {ObservableItem|Object<ObservableItem>} data
4638
- * @returns {{}|*|null}
4639
- */
4640
- Observable.value = function(data) {
4641
- if(Validator.isObservable(data)) {
4642
- return data.val();
4643
- }
4644
- if(Validator.isProxy(data)) {
4645
- return data.$value;
4646
- }
4647
- if(Validator.isArray(data)) {
4648
- const result = [];
4649
- for(let i = 0, length = data.length; i < length; i++) {
4650
- const item = data[i];
4651
- result.push(Observable.value(item));
4652
- }
4653
- return result;
4654
- }
4655
- return data;
4656
- };
4625
+ /**
4626
+ * Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
4627
+ *
4628
+ * @param {string} name
4629
+ */
4630
+ delete(name) {
4631
+ const item = $stores.get(name);
4632
+ if (!item) {
4633
+ DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
4634
+ return;
4635
+ }
4636
+ item.subscribers.forEach(follower => follower.destroy());
4637
+ item.subscribers.clear();
4638
+ item.observer.cleanup();
4639
+ $stores.delete(name);
4640
+ },
4641
+ /**
4642
+ * Creates an isolated store group with its own state namespace.
4643
+ * Each group is a fully independent StoreFactory instance —
4644
+ * no key conflicts, no shared state with the parent store.
4645
+ *
4646
+ * @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
4647
+ * @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
4648
+ * @returns {ReturnType<typeof StoreFactory>}
4649
+ *
4650
+ * @example
4651
+ * // With name (recommended)
4652
+ * const EventStore = Store.group('events', (group) => {
4653
+ * group.create('catalog', []);
4654
+ * group.create('filters', { category: null, date: null });
4655
+ * group.createResettable('selected', null);
4656
+ * group.createComposed('filtered', () => {
4657
+ * const catalog = EventStore.get('catalog').val();
4658
+ * const filters = EventStore.get('filters').val();
4659
+ * return catalog.filter(event => {
4660
+ * if (filters.category && event.category !== filters.category) return false;
4661
+ * return true;
4662
+ * });
4663
+ * }, ['catalog', 'filters']);
4664
+ * });
4665
+ *
4666
+ * // Without name
4667
+ * const CartStore = Store.group((group) => {
4668
+ * group.create('items', []);
4669
+ * });
4670
+ *
4671
+ * // Usage
4672
+ * EventStore.use('catalog'); // two-way follower
4673
+ * EventStore.follow('filtered'); // read-only follower
4674
+ * EventStore.get('filters'); // raw observable
4675
+ *
4676
+ * // Cross-group composed
4677
+ * const OrderStore = Store.group('orders', (group) => {
4678
+ * group.createComposed('summary', () => {
4679
+ * const items = CartStore.get('items').val();
4680
+ * const events = EventStore.get('catalog').val();
4681
+ * return { items, events };
4682
+ * }, [CartStore.get('items'), EventStore.get('catalog')]);
4683
+ * });
4684
+ */
4685
+ group(name, callback) {
4686
+ if (typeof name === 'function') {
4687
+ callback = name;
4688
+ name = 'anonymous';
4689
+ }
4690
+ const store = StoreFactory();
4691
+ callback && callback(store);
4692
+ return store;
4693
+ },
4694
+ createPersistent(name, value, localstorage_key) {
4695
+ localstorage_key = localstorage_key || name;
4696
+ const observer = this.create(name, $getFromStorage(localstorage_key, value));
4697
+ const saver = $saveToStorage(value);
4657
4698
 
4658
- Observable.object = Observable.init;
4659
- Observable.json = Observable.init;
4699
+ observer.subscribe((val) => saver(localstorage_key, val));
4700
+ return observer;
4701
+ },
4702
+ createPersistentResettable(name, value, localstorage_key) {
4703
+ localstorage_key = localstorage_key || name;
4704
+ const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
4705
+ const saver = $saveToStorage(value);
4706
+ observer.subscribe((val) => saver(localstorage_key, val));
4660
4707
 
4661
- /**
4662
- * Creates a computed observable that automatically updates when its dependencies change.
4663
- * The callback is re-executed whenever any dependency observable changes.
4664
- *
4665
- * @param {Function} callback - Function that returns the computed value
4666
- * @param {Array<ObservableItem|ObservableChecker|ObservableProxy>|Function} [dependencies=[]] - Array of observables to watch, or batch function
4667
- * @returns {ObservableItem} A new observable that updates automatically
4668
- * @example
4669
- * const firstName = Observable('John');
4670
- * const lastName = Observable('Doe');
4671
- * const fullName = Observable.computed(
4672
- * () => `${firstName.val()} ${lastName.val()}`,
4673
- * [firstName, lastName]
4674
- * );
4675
- *
4676
- * // With batch function
4677
- * const batch = Observable.batch(() => { ... });
4678
- * const computed = Observable.computed(() => { ... }, batch);
4679
- */
4680
- Observable.computed = function(callback, dependencies = []) {
4681
- const initialValue = callback();
4682
- const observable = new ObservableItem(initialValue);
4683
- const updatedValue = nextTick(() => observable.set(callback()));
4684
- {
4685
- PluginsManager.emit('CreateObservableComputed', observable, dependencies);
4686
- }
4708
+ const originalReset = observer.reset.bind(observer);
4709
+ observer.reset = () => {
4710
+ LocalStorage.remove(localstorage_key);
4711
+ originalReset();
4712
+ };
4687
4713
 
4688
- if(Validator.isFunction(dependencies)) {
4689
- if(!Validator.isObservable(dependencies.$observer)) {
4690
- throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
4714
+ return observer;
4691
4715
  }
4692
- dependencies.$observer.subscribe(updatedValue);
4693
- return observable;
4694
- }
4716
+ };
4695
4717
 
4696
- dependencies.forEach(dependency => {
4697
- if(Validator.isProxy(dependency)) {
4698
- dependency.$observables.forEach((observable) => {
4699
- observable.subscribe(updatedValue);
4700
- });
4701
- return;
4718
+
4719
+ return new Proxy($api, {
4720
+ get(target, prop) {
4721
+ if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
4722
+ return target[prop];
4723
+ }
4724
+ if (target.has(prop)) {
4725
+ if ($followersCache.has(prop)) {
4726
+ return $followersCache.get(prop);
4727
+ }
4728
+ const follower = target.follow(prop);
4729
+ $followersCache.set(prop, follower);
4730
+ return follower;
4731
+ }
4732
+ return undefined;
4733
+ },
4734
+ set(target, prop, value) {
4735
+ DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
4736
+ throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
4737
+ },
4738
+ deleteProperty(target, prop) {
4739
+ throw new NativeDocumentError(`Store keys cannot be deleted.`);
4702
4740
  }
4703
- dependency.subscribe(updatedValue);
4704
4741
  });
4705
-
4706
- return observable;
4707
4742
  };
4708
4743
 
4744
+ const Store = StoreFactory();
4745
+
4746
+ Store.create('locale', navigator.language.split('-')[0] || 'en');
4747
+
4709
4748
  /**
4710
4749
  * Renders a list of items from an observable array or object, automatically updating when data changes.
4711
4750
  * Efficiently manages DOM updates by tracking items with keys.