native-document 1.0.95 → 1.0.99
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.
- package/{src/devtools/hrm → devtools}/ComponentRegistry.js +2 -2
- package/devtools/index.js +8 -0
- package/{src/devtools/plugin.js → devtools/plugin/dev-tools-plugin.js} +2 -2
- package/{src/devtools/hrm/nd-vite-hot-reload.js → devtools/transformers/nd-vite-devtools.js} +16 -6
- package/devtools/transformers/src/transformComponentForHrm.js +74 -0
- package/devtools/transformers/src/transformJsFile.js +9 -0
- package/devtools/transformers/src/utils.js +79 -0
- package/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +8 -0
- package/devtools/widget/Widget.js +48 -0
- package/devtools/widget/widget.css +81 -0
- package/devtools/widget.js +23 -0
- package/dist/native-document.components.min.js +1953 -1245
- package/dist/native-document.dev.js +2022 -1375
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.devtools.min.js +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/cache.md +1 -1
- package/docs/core-concepts.md +1 -1
- package/docs/native-document-element.md +51 -15
- package/docs/observables.md +333 -315
- package/docs/state-management.md +198 -193
- package/package.json +1 -1
- package/readme.md +1 -1
- package/rollup.config.js +1 -1
- package/src/core/data/ObservableArray.js +67 -0
- package/src/core/data/ObservableChecker.js +2 -0
- package/src/core/data/ObservableItem.js +97 -0
- package/src/core/data/ObservableObject.js +183 -0
- package/src/core/data/Store.js +364 -34
- package/src/core/data/observable-helpers/object.js +2 -166
- package/src/core/utils/formatters.js +91 -0
- package/src/core/utils/localstorage.js +57 -0
- package/src/core/utils/validator.js +0 -2
- package/src/fetch/NativeFetch.js +5 -2
- package/types/observable.d.ts +73 -15
- package/types/plugins-manager.d.ts +1 -1
- package/types/store.d.ts +33 -6
- package/hrm.js +0 -7
- package/src/devtools/app/App.js +0 -66
- package/src/devtools/app/app.css +0 -0
- package/src/devtools/hrm/transformComponent.js +0 -129
- package/src/devtools/index.js +0 -18
- package/src/devtools/widget/DevToolsWidget.js +0 -26
- /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import ObservableItem from "./ObservableItem";
|
|
2
|
+
import Validator from "../utils/validator";
|
|
3
|
+
import {nextTick} from "../utils/helpers";
|
|
4
|
+
import {Observable} from "./Observable";
|
|
5
|
+
|
|
6
|
+
export const ObservableObject = function(target, configs) {
|
|
7
|
+
ObservableItem.call(this, target);
|
|
8
|
+
this.$observables = {};
|
|
9
|
+
this.configs = configs;
|
|
10
|
+
|
|
11
|
+
this.$load(target);
|
|
12
|
+
|
|
13
|
+
for(const name in target) {
|
|
14
|
+
if(!Object.hasOwn(this, name)) {
|
|
15
|
+
Object.defineProperty(this, name, {
|
|
16
|
+
get: () => this.$observables[name],
|
|
17
|
+
set: (value) => this.$observables[name].set(value)
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
ObservableObject.prototype = Object.create(ObservableItem.prototype);
|
|
25
|
+
|
|
26
|
+
Object.defineProperty(ObservableObject, '$value', {
|
|
27
|
+
get() {
|
|
28
|
+
return this.val();
|
|
29
|
+
},
|
|
30
|
+
set(value) {
|
|
31
|
+
this.set(value);
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
ObservableObject.prototype.__$isObservableObject = true;
|
|
36
|
+
ObservableObject.prototype.__isProxy__ = true;
|
|
37
|
+
|
|
38
|
+
ObservableObject.prototype.$load = function(initialValue) {
|
|
39
|
+
const configs = this.configs;
|
|
40
|
+
for(const key in initialValue) {
|
|
41
|
+
const itemValue = initialValue[key];
|
|
42
|
+
if(Array.isArray(itemValue)) {
|
|
43
|
+
if(configs?.deep !== false) {
|
|
44
|
+
const mappedItemValue = itemValue.map(item => {
|
|
45
|
+
if(Validator.isJson(item)) {
|
|
46
|
+
return Observable.json(item, configs);
|
|
47
|
+
}
|
|
48
|
+
if(Validator.isArray(item)) {
|
|
49
|
+
return Observable.array(item, configs);
|
|
50
|
+
}
|
|
51
|
+
return Observable(item, configs);
|
|
52
|
+
});
|
|
53
|
+
this.$observables[key] = Observable.array(mappedItemValue, configs);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
this.$observables[key] = Observable.array(itemValue, configs);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if(Validator.isObservable(itemValue) || Validator.isProxy(itemValue)) {
|
|
60
|
+
this.$observables[key] = itemValue;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
this.$observables[key] = (typeof itemValue === 'object') ? Observable.object(itemValue, configs) : Observable(itemValue, configs);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
ObservableObject.prototype.val = function() {
|
|
68
|
+
const result = {};
|
|
69
|
+
for(const key in this.$observables) {
|
|
70
|
+
const dataItem = this.$observables[key];
|
|
71
|
+
if(Validator.isObservable(dataItem)) {
|
|
72
|
+
let value = dataItem.val();
|
|
73
|
+
if(Array.isArray(value)) {
|
|
74
|
+
value = value.map(item => {
|
|
75
|
+
if(Validator.isObservable(item)) {
|
|
76
|
+
return item.val();
|
|
77
|
+
}
|
|
78
|
+
if(Validator.isProxy(item)) {
|
|
79
|
+
return item.$value;
|
|
80
|
+
}
|
|
81
|
+
return item;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
result[key] = value;
|
|
85
|
+
} else if(Validator.isProxy(dataItem)) {
|
|
86
|
+
result[key] = dataItem.$value;
|
|
87
|
+
} else {
|
|
88
|
+
result[key] = dataItem;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
ObservableObject.prototype.$val = ObservableObject.prototype.val;
|
|
94
|
+
|
|
95
|
+
ObservableObject.prototype.get = function(property) {
|
|
96
|
+
const item = this.$observables[property];
|
|
97
|
+
if(Validator.isObservable(item)) {
|
|
98
|
+
return item.val();
|
|
99
|
+
}
|
|
100
|
+
if(Validator.isProxy(item)) {
|
|
101
|
+
return item.$value;
|
|
102
|
+
}
|
|
103
|
+
return item;
|
|
104
|
+
};
|
|
105
|
+
ObservableObject.prototype.$get = ObservableObject.prototype.get;
|
|
106
|
+
|
|
107
|
+
ObservableObject.prototype.set = function(newData) {
|
|
108
|
+
const data = Validator.isProxy(newData) ? newData.$value : newData;
|
|
109
|
+
const configs = this.configs;
|
|
110
|
+
|
|
111
|
+
for(const key in data) {
|
|
112
|
+
const targetItem = this.$observables[key];
|
|
113
|
+
const newValueOrigin = newData[key];
|
|
114
|
+
const newValue = data[key];
|
|
115
|
+
|
|
116
|
+
if(Validator.isObservable(targetItem)) {
|
|
117
|
+
if(!Validator.isArray(newValue)) {
|
|
118
|
+
targetItem.set(newValue);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const firstElementFromOriginalValue = newValueOrigin.at(0);
|
|
122
|
+
if(Validator.isObservable(firstElementFromOriginalValue) || Validator.isProxy(firstElementFromOriginalValue)) {
|
|
123
|
+
const newValues = newValue.map(item => {
|
|
124
|
+
if(Validator.isProxy(firstElementFromOriginalValue)) {
|
|
125
|
+
return Observable.init(item, configs);
|
|
126
|
+
}
|
|
127
|
+
return Observable(item, configs);
|
|
128
|
+
});
|
|
129
|
+
targetItem.set(newValues);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
targetItem.set([...newValue]);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if(Validator.isProxy(targetItem)) {
|
|
136
|
+
targetItem.update(newValue);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
this[key] = newValue;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
ObservableObject.prototype.$set = ObservableObject.prototype.set;
|
|
143
|
+
ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
|
|
144
|
+
|
|
145
|
+
ObservableObject.prototype.observables = function() {
|
|
146
|
+
return Object.values(this.$observables);
|
|
147
|
+
};
|
|
148
|
+
ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
|
|
149
|
+
|
|
150
|
+
ObservableObject.prototype.keys = function() {
|
|
151
|
+
return Object.keys(this.$observables);
|
|
152
|
+
};
|
|
153
|
+
ObservableObject.prototype.$keys = ObservableObject.prototype.keys;
|
|
154
|
+
ObservableObject.prototype.clone = function() {
|
|
155
|
+
return Observable.init(this.val(), this.configs);
|
|
156
|
+
};
|
|
157
|
+
ObservableObject.prototype.$clone = ObservableObject.prototype.clone;
|
|
158
|
+
ObservableObject.prototype.reset = function() {
|
|
159
|
+
for(const key in this.$observables) {
|
|
160
|
+
this.$observables[key].reset();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
ObservableObject.prototype.originalSubscribe = ObservableObject.prototype.subscribe;
|
|
164
|
+
ObservableObject.prototype.subscribe = function(callback) {
|
|
165
|
+
const observables = this.observables();
|
|
166
|
+
const updatedValue = nextTick(() => this.trigger());
|
|
167
|
+
|
|
168
|
+
this.originalSubscribe(callback);
|
|
169
|
+
|
|
170
|
+
for (let i = 0, length = observables.length; i < length; i++) {
|
|
171
|
+
const observable = observables[i];
|
|
172
|
+
if (observable.__$isObservableArray) {
|
|
173
|
+
observable.deepSubscribe(updatedValue);
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
observable.subscribe(updatedValue);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
ObservableObject.prototype.configs = function() {
|
|
180
|
+
return this.configs;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
ObservableObject.prototype.update = ObservableObject.prototype.set;
|
package/src/core/data/Store.js
CHANGED
|
@@ -1,74 +1,404 @@
|
|
|
1
|
-
import {Observable} from "./Observable";
|
|
1
|
+
import { Observable } from "./Observable";
|
|
2
|
+
import NativeDocumentError from "../errors/NativeDocumentError";
|
|
3
|
+
import DebugManager from "../utils/debug-manager";
|
|
4
|
+
import {$getFromStorage, $saveToStorage, LocalStorage} from "../utils/localstorage";
|
|
2
5
|
|
|
3
|
-
export const
|
|
6
|
+
export const StoreFactory = function() {
|
|
4
7
|
|
|
5
8
|
const $stores = new Map();
|
|
9
|
+
const $followersCache = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal helper — retrieves a store entry or throws if not found.
|
|
13
|
+
*/
|
|
14
|
+
const $getStoreOrThrow = (method, name) => {
|
|
15
|
+
const item = $stores.get(name);
|
|
16
|
+
if (!item) {
|
|
17
|
+
DebugManager.error('Store', `Store.${method}('${name}') : store not found. Did you call Store.create('${name}') first?`);
|
|
18
|
+
throw new NativeDocumentError(
|
|
19
|
+
`Store.${method}('${name}') : store not found.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return item;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Internal helper — blocks write operations on a read-only observer.
|
|
27
|
+
*/
|
|
28
|
+
const $applyReadOnly = (observer, name, context) => {
|
|
29
|
+
const readOnlyError = (method) => () => {
|
|
30
|
+
DebugManager.error('Store', `Store.${context}('${name}') is read-only. '${method}()' is not allowed.`);
|
|
31
|
+
throw new NativeDocumentError(
|
|
32
|
+
`Store.${context}('${name}') is read-only.`
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
observer.set = readOnlyError('set');
|
|
36
|
+
observer.toggle = readOnlyError('toggle');
|
|
37
|
+
observer.reset = readOnlyError('reset');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const $createObservable = (value, options = {}) => {
|
|
41
|
+
if(Array.isArray(value)) {
|
|
42
|
+
return Observable.array(value, options);
|
|
43
|
+
}
|
|
44
|
+
if(typeof value === 'object') {
|
|
45
|
+
return Observable.object(value, options);
|
|
46
|
+
}
|
|
47
|
+
return Observable(value, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const $api = {
|
|
51
|
+
/**
|
|
52
|
+
* Create a new state and return the observer.
|
|
53
|
+
* Throws if a store with the same name already exists.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @param {*} value
|
|
57
|
+
* @returns {ObservableItem}
|
|
58
|
+
*/
|
|
59
|
+
create(name, value) {
|
|
60
|
+
if ($stores.has(name)) {
|
|
61
|
+
DebugManager.warn('Store', `Store.create('${name}') : a store with this name already exists. Use Store.get('${name}') to retrieve it.`);
|
|
62
|
+
throw new NativeDocumentError(
|
|
63
|
+
`Store.create('${name}') : a store with this name already exists.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const observer = $createObservable(value)
|
|
67
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: false });
|
|
68
|
+
return observer;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a new resettable state and return the observer.
|
|
73
|
+
* The store can be reset to its initial value via Store.reset(name).
|
|
74
|
+
* Throws if a store with the same name already exists.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name
|
|
77
|
+
* @param {*} value
|
|
78
|
+
* @returns {ObservableItem}
|
|
79
|
+
*/
|
|
80
|
+
createResettable(name, value) {
|
|
81
|
+
if ($stores.has(name)) {
|
|
82
|
+
DebugManager.warn('Store', `Store.createResettable('${name}') : a store with this name already exists.`);
|
|
83
|
+
throw new NativeDocumentError(
|
|
84
|
+
`Store.createResettable('${name}') : a store with this name already exists.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const observer = $createObservable(value, { reset: true });
|
|
88
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: true, composed: false });
|
|
89
|
+
return observer;
|
|
90
|
+
},
|
|
6
91
|
|
|
7
|
-
return {
|
|
8
92
|
/**
|
|
9
|
-
* Create a
|
|
93
|
+
* Create a computed store derived from other stores.
|
|
94
|
+
* The value is automatically recalculated when any dependency changes.
|
|
95
|
+
* This store is read-only — Store.use() and Store.set() will throw.
|
|
96
|
+
* Throws if a store with the same name already exists.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} name
|
|
99
|
+
* @param {() => *} computation - Function that returns the computed value
|
|
100
|
+
* @param {string[]} dependencies - Names of the stores to watch
|
|
101
|
+
* @returns {ObservableItem}
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* Store.create('products', [{ id: 1, price: 10 }]);
|
|
105
|
+
* Store.create('cart', [{ productId: 1, quantity: 2 }]);
|
|
106
|
+
*
|
|
107
|
+
* Store.createComposed('total', () => {
|
|
108
|
+
* const products = Store.get('products').val();
|
|
109
|
+
* const cart = Store.get('cart').val();
|
|
110
|
+
* return cart.reduce((sum, item) => {
|
|
111
|
+
* const product = products.find(p => p.id === item.productId);
|
|
112
|
+
* return sum + (product.price * item.quantity);
|
|
113
|
+
* }, 0);
|
|
114
|
+
* }, ['products', 'cart']);
|
|
115
|
+
*/
|
|
116
|
+
createComposed(name, computation, dependencies) {
|
|
117
|
+
if ($stores.has(name)) {
|
|
118
|
+
DebugManager.warn('Store', `Store.createComposed('${name}') : a store with this name already exists.`);
|
|
119
|
+
throw new NativeDocumentError(
|
|
120
|
+
`Store.createComposed('${name}') : a store with this name already exists.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (typeof computation !== 'function') {
|
|
124
|
+
throw new NativeDocumentError(
|
|
125
|
+
`Store.createComposed('${name}') : computation must be a function.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
|
129
|
+
throw new NativeDocumentError(
|
|
130
|
+
`Store.createComposed('${name}') : dependencies must be a non-empty array of store names.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Resolve dependency observers
|
|
135
|
+
const depObservers = dependencies.map(depName => {
|
|
136
|
+
if(typeof depName !== 'string') {
|
|
137
|
+
return depName;
|
|
138
|
+
}
|
|
139
|
+
const depItem = $stores.get(depName);
|
|
140
|
+
if (!depItem) {
|
|
141
|
+
DebugManager.error('Store', `Store.createComposed('${name}') : dependency '${depName}' not found. Create it first.`);
|
|
142
|
+
throw new NativeDocumentError(
|
|
143
|
+
`Store.createComposed('${name}') : dependency store '${depName}' not found.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return depItem.observer;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Create computed observable from dependency observers
|
|
150
|
+
const observer = Observable.computed(computation, depObservers);
|
|
151
|
+
|
|
152
|
+
$stores.set(name, { observer, subscribers: new Set(), resettable: false, composed: true });
|
|
153
|
+
return observer;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns true if a store with the given name exists.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} name
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
has(name) {
|
|
163
|
+
return $stores.has(name);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resets a resettable store to its initial value and notifies all subscribers.
|
|
168
|
+
* Throws if the store was not created with createResettable().
|
|
169
|
+
*
|
|
170
|
+
* @param {string} name
|
|
171
|
+
*/
|
|
172
|
+
reset(name) {
|
|
173
|
+
const item = $getStoreOrThrow('reset', name);
|
|
174
|
+
if (item.composed) {
|
|
175
|
+
DebugManager.error('Store', `Store.reset('${name}') : composed stores cannot be reset. Their value is derived from dependencies.`);
|
|
176
|
+
throw new NativeDocumentError(
|
|
177
|
+
`Store.reset('${name}') : composed stores cannot be reset.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (!item.resettable) {
|
|
181
|
+
DebugManager.error('Store', `Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`);
|
|
182
|
+
throw new NativeDocumentError(
|
|
183
|
+
`Store.reset('${name}') : this store is not resettable. Use Store.createResettable('${name}', value) instead of Store.create().`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
item.observer.reset();
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns a two-way synchronized follower of the store.
|
|
191
|
+
* Writing to the follower propagates the value back to the store and all its subscribers.
|
|
192
|
+
* Throws if called on a composed store — use Store.follow() instead.
|
|
193
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
194
|
+
*
|
|
10
195
|
* @param {string} name
|
|
11
196
|
* @returns {ObservableItem}
|
|
12
197
|
*/
|
|
13
198
|
use(name) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
199
|
+
const item = $getStoreOrThrow('use', name);
|
|
200
|
+
|
|
201
|
+
if (item.composed) {
|
|
202
|
+
DebugManager.error('Store', `Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`);
|
|
203
|
+
throw new NativeDocumentError(
|
|
204
|
+
`Store.use('${name}') : composed stores are read-only. Use Store.follow('${name}') instead.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { observer: originalObserver, subscribers } = item;
|
|
209
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
210
|
+
|
|
211
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
212
|
+
const onFollowerChange = value => originalObserver.set(value);
|
|
213
|
+
|
|
214
|
+
originalObserver.subscribe(onStoreChange);
|
|
215
|
+
observerFollower.subscribe(onFollowerChange);
|
|
216
|
+
|
|
18
217
|
observerFollower.destroy = () => {
|
|
19
|
-
|
|
20
|
-
|
|
218
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
219
|
+
observerFollower.unsubscribe(onFollowerChange);
|
|
220
|
+
subscribers.delete(observerFollower);
|
|
21
221
|
observerFollower.cleanup();
|
|
22
222
|
};
|
|
23
|
-
|
|
223
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
24
224
|
|
|
225
|
+
subscribers.add(observerFollower);
|
|
25
226
|
return observerFollower;
|
|
26
227
|
},
|
|
228
|
+
|
|
27
229
|
/**
|
|
230
|
+
* Returns a read-only follower of the store.
|
|
231
|
+
* The follower reflects store changes but cannot write back to the store.
|
|
232
|
+
* Any attempt to call .set(), .toggle() or .reset() will throw.
|
|
233
|
+
* Call follower.destroy() or follower.dispose() to unsubscribe.
|
|
234
|
+
*
|
|
28
235
|
* @param {string} name
|
|
29
236
|
* @returns {ObservableItem}
|
|
30
237
|
*/
|
|
31
238
|
follow(name) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
239
|
+
const { observer: originalObserver, subscribers } = $getStoreOrThrow('follow', name);
|
|
240
|
+
const observerFollower = $createObservable(originalObserver.val());
|
|
241
|
+
|
|
242
|
+
const onStoreChange = value => observerFollower.set(value);
|
|
243
|
+
originalObserver.subscribe(onStoreChange);
|
|
244
|
+
|
|
245
|
+
$applyReadOnly(observerFollower, name, 'follow');
|
|
246
|
+
|
|
247
|
+
observerFollower.destroy = () => {
|
|
248
|
+
originalObserver.unsubscribe(onStoreChange);
|
|
249
|
+
subscribers.delete(observerFollower);
|
|
250
|
+
observerFollower.cleanup();
|
|
251
|
+
};
|
|
252
|
+
observerFollower.dispose = observerFollower.destroy;
|
|
253
|
+
|
|
254
|
+
subscribers.add(observerFollower);
|
|
255
|
+
return observerFollower;
|
|
44
256
|
},
|
|
257
|
+
|
|
45
258
|
/**
|
|
46
|
-
*
|
|
259
|
+
* Returns the raw store observer directly (no follower, no cleanup contract).
|
|
260
|
+
* Use this for direct read access when you don't need to unsubscribe.
|
|
261
|
+
* WARNING : mutations on this observer impact all subscribers immediately.
|
|
262
|
+
*
|
|
47
263
|
* @param {string} name
|
|
48
|
-
* @returns {null
|
|
264
|
+
* @returns {ObservableItem|null}
|
|
49
265
|
*/
|
|
50
266
|
get(name) {
|
|
51
267
|
const item = $stores.get(name);
|
|
52
|
-
|
|
268
|
+
if (!item) {
|
|
269
|
+
DebugManager.warn('Store', `Store.get('${name}') : store not found.`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return item.observer;
|
|
53
273
|
},
|
|
274
|
+
|
|
54
275
|
/**
|
|
55
|
-
*
|
|
56
276
|
* @param {string} name
|
|
57
|
-
* @returns {{observer: ObservableItem, subscribers: Set}}
|
|
277
|
+
* @returns {{ observer: ObservableItem, subscribers: Set } | null}
|
|
58
278
|
*/
|
|
59
279
|
getWithSubscribers(name) {
|
|
60
|
-
return $stores.get(name);
|
|
280
|
+
return $stores.get(name) ?? null;
|
|
61
281
|
},
|
|
282
|
+
|
|
62
283
|
/**
|
|
63
|
-
*
|
|
284
|
+
* Destroys a store : cleans up the observer, destroys all followers, and removes the entry.
|
|
285
|
+
*
|
|
64
286
|
* @param {string} name
|
|
65
287
|
*/
|
|
66
288
|
delete(name) {
|
|
67
289
|
const item = $stores.get(name);
|
|
68
|
-
if(!item)
|
|
69
|
-
|
|
290
|
+
if (!item) {
|
|
291
|
+
DebugManager.warn('Store', `Store.delete('${name}') : store not found, nothing to delete.`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
70
294
|
item.subscribers.forEach(follower => follower.destroy());
|
|
71
|
-
item.
|
|
295
|
+
item.subscribers.clear();
|
|
296
|
+
item.observer.cleanup();
|
|
297
|
+
$stores.delete(name);
|
|
298
|
+
},
|
|
299
|
+
/**
|
|
300
|
+
* Creates an isolated store group with its own state namespace.
|
|
301
|
+
* Each group is a fully independent StoreFactory instance —
|
|
302
|
+
* no key conflicts, no shared state with the parent store.
|
|
303
|
+
*
|
|
304
|
+
* @param {string | ((group: ReturnType<typeof StoreFactory>) => void)} name - Group name for debugging, or setup callback if no name is provided
|
|
305
|
+
* @param {((group: ReturnType<typeof StoreFactory>) => void)} [callback] - Setup function receiving the isolated store instance
|
|
306
|
+
* @returns {ReturnType<typeof StoreFactory>}
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* // With name (recommended)
|
|
310
|
+
* const EventStore = Store.group('events', (group) => {
|
|
311
|
+
* group.create('catalog', []);
|
|
312
|
+
* group.create('filters', { category: null, date: null });
|
|
313
|
+
* group.createResettable('selected', null);
|
|
314
|
+
* group.createComposed('filtered', () => {
|
|
315
|
+
* const catalog = EventStore.get('catalog').val();
|
|
316
|
+
* const filters = EventStore.get('filters').val();
|
|
317
|
+
* return catalog.filter(event => {
|
|
318
|
+
* if (filters.category && event.category !== filters.category) return false;
|
|
319
|
+
* return true;
|
|
320
|
+
* });
|
|
321
|
+
* }, ['catalog', 'filters']);
|
|
322
|
+
* });
|
|
323
|
+
*
|
|
324
|
+
* // Without name
|
|
325
|
+
* const CartStore = Store.group((group) => {
|
|
326
|
+
* group.create('items', []);
|
|
327
|
+
* });
|
|
328
|
+
*
|
|
329
|
+
* // Usage
|
|
330
|
+
* EventStore.use('catalog'); // two-way follower
|
|
331
|
+
* EventStore.follow('filtered'); // read-only follower
|
|
332
|
+
* EventStore.get('filters'); // raw observable
|
|
333
|
+
*
|
|
334
|
+
* // Cross-group composed
|
|
335
|
+
* const OrderStore = Store.group('orders', (group) => {
|
|
336
|
+
* group.createComposed('summary', () => {
|
|
337
|
+
* const items = CartStore.get('items').val();
|
|
338
|
+
* const events = EventStore.get('catalog').val();
|
|
339
|
+
* return { items, events };
|
|
340
|
+
* }, [CartStore.get('items'), EventStore.get('catalog')]);
|
|
341
|
+
* });
|
|
342
|
+
*/
|
|
343
|
+
group(name, callback) {
|
|
344
|
+
if (typeof name === 'function') {
|
|
345
|
+
callback = name;
|
|
346
|
+
name = 'anonymous';
|
|
347
|
+
}
|
|
348
|
+
const store = StoreFactory();
|
|
349
|
+
callback && callback(store);
|
|
350
|
+
return store;
|
|
351
|
+
},
|
|
352
|
+
createPersistent(name, value, localstorage_key) {
|
|
353
|
+
localstorage_key = localstorage_key || name;
|
|
354
|
+
const observer = this.create(name, $getFromStorage(localstorage_key, value));
|
|
355
|
+
const saver = $saveToStorage(value)
|
|
356
|
+
|
|
357
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
358
|
+
return observer;
|
|
359
|
+
},
|
|
360
|
+
createPersistentResettable(name, value, localstorage_key) {
|
|
361
|
+
localstorage_key = localstorage_key || name;
|
|
362
|
+
const observer = this.createResettable(name, $getFromStorage(localstorage_key, value));
|
|
363
|
+
const saver = $saveToStorage(value)
|
|
364
|
+
observer.subscribe((val) => saver(localstorage_key, val));
|
|
365
|
+
|
|
366
|
+
const originalReset = observer.reset.bind(observer);
|
|
367
|
+
observer.reset = () => {
|
|
368
|
+
LocalStorage.remove(localstorage_key);
|
|
369
|
+
originalReset();
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
return observer;
|
|
72
373
|
}
|
|
73
374
|
};
|
|
74
|
-
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
return new Proxy($api, {
|
|
378
|
+
get(target, prop) {
|
|
379
|
+
if (typeof prop === 'symbol' || prop.startsWith('$') || prop in target) {
|
|
380
|
+
return target[prop];
|
|
381
|
+
}
|
|
382
|
+
if (target.has(prop)) {
|
|
383
|
+
if ($followersCache.has(prop)) {
|
|
384
|
+
return $followersCache.get(prop);
|
|
385
|
+
}
|
|
386
|
+
const follower = target.follow(prop);
|
|
387
|
+
$followersCache.set(prop, follower);
|
|
388
|
+
return follower;
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
},
|
|
392
|
+
set(target, prop, value) {
|
|
393
|
+
DebugManager.error('Store', `Forbidden: You cannot overwrite the store key '${String(prop)}'. Use .use('${String(prop)}').set(value) instead.`);
|
|
394
|
+
throw new NativeDocumentError(`Store structure is immutable. Use .set() on the observable.`);
|
|
395
|
+
},
|
|
396
|
+
deleteProperty(target, prop) {
|
|
397
|
+
throw new NativeDocumentError(`Store keys cannot be deleted.`);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export const Store = StoreFactory();
|
|
403
|
+
|
|
404
|
+
Store.create('locale', 'fr')
|