@biglogic/rgs 3.1.0 → 3.2.0
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/README.md +14 -0
- package/advanced.js +1 -1
- package/index.js +1 -1
- package/package.json +8 -3
- package/rgs-extension.vsix +0 -0
- package/core/advanced.js +0 -4
- package/core/async.js +0 -40
- package/core/hooks.js +0 -52
- package/core/security.js +0 -124
- package/core/store.js +0 -595
- package/core/types.js +0 -5
- package/core/utils.js +0 -72
- package/examples/README.md +0 -41
- package/examples/async-data-fetch/UserLoader.d.ts +0 -12
- package/examples/async-data-fetch/UserLoader.js +0 -10
- package/examples/async-data-fetch/UserLoader.ts +0 -30
- package/examples/basic-counter/CounterComponent.d.ts +0 -2
- package/examples/basic-counter/CounterComponent.js +0 -7
- package/examples/basic-counter/CounterComponent.tsx +0 -22
- package/examples/basic-counter/CounterStore.d.ts +0 -7
- package/examples/basic-counter/CounterStore.js +0 -13
- package/examples/basic-counter/CounterStore.ts +0 -25
- package/examples/big-data-indexeddb/BigDataStore.d.ts +0 -10
- package/examples/big-data-indexeddb/BigDataStore.js +0 -32
- package/examples/big-data-indexeddb/BigDataStore.ts +0 -60
- package/examples/global-theme/ThemeManager.d.ts +0 -7
- package/examples/global-theme/ThemeManager.js +0 -13
- package/examples/global-theme/ThemeManager.ts +0 -32
- package/examples/hybrid-cloud-sync/HybridStore.d.ts +0 -19
- package/examples/hybrid-cloud-sync/HybridStore.js +0 -44
- package/examples/hybrid-cloud-sync/HybridStore.ts +0 -78
- package/examples/persistent-cart/CartStore.d.ts +0 -13
- package/examples/persistent-cart/CartStore.js +0 -23
- package/examples/persistent-cart/CartStore.ts +0 -41
- package/examples/rbac-dashboard/DashboardStore.d.ts +0 -47
- package/examples/rbac-dashboard/DashboardStore.js +0 -31
- package/examples/rbac-dashboard/DashboardStore.ts +0 -46
- package/examples/secure-auth/AuthStore.d.ts +0 -14
- package/examples/secure-auth/AuthStore.js +0 -20
- package/examples/secure-auth/AuthStore.ts +0 -36
- package/examples/security-best-practices/SecurityStore.d.ts +0 -22
- package/examples/security-best-practices/SecurityStore.js +0 -30
- package/examples/security-best-practices/SecurityStore.ts +0 -75
- package/examples/stress-tests/StressStore.d.ts +0 -41
- package/examples/stress-tests/StressStore.js +0 -41
- package/examples/stress-tests/StressStore.ts +0 -61
- package/examples/super-easy/EasyStore.d.ts +0 -44
- package/examples/super-easy/EasyStore.js +0 -24
- package/examples/super-easy/EasyStore.ts +0 -61
- package/examples/undo-redo-editor/EditorStore.d.ts +0 -9
- package/examples/undo-redo-editor/EditorStore.js +0 -13
- package/examples/undo-redo-editor/EditorStore.ts +0 -28
- package/markdown/SUMMARY.md +0 -59
- package/markdown/api.md +0 -381
- package/markdown/chapters/01-philosophy.md +0 -54
- package/markdown/chapters/02-getting-started.md +0 -68
- package/markdown/chapters/03-the-magnetar-way.md +0 -69
- package/markdown/chapters/04-persistence-and-safety.md +0 -125
- package/markdown/chapters/05-plugin-sdk.md +0 -290
- package/markdown/chapters/05-plugins-and-extensibility.md +0 -190
- package/markdown/chapters/06-case-studies.md +0 -69
- package/markdown/chapters/07-faq.md +0 -53
- package/markdown/chapters/08-migration-guide.md +0 -253
- package/markdown/chapters/09-security-architecture.md +0 -40
- package/plugins/index.js +0 -34
- package/plugins/official/analytics.plugin.js +0 -19
- package/plugins/official/cloud-sync.plugin.js +0 -117
- package/plugins/official/debug.plugin.js +0 -62
- package/plugins/official/devtools.plugin.js +0 -28
- package/plugins/official/guard.plugin.js +0 -15
- package/plugins/official/immer.plugin.js +0 -10
- package/plugins/official/indexeddb.plugin.js +0 -102
- package/plugins/official/schema.plugin.js +0 -16
- package/plugins/official/snapshot.plugin.js +0 -27
- package/plugins/official/sync.plugin.js +0 -37
- package/plugins/official/undo-redo.plugin.js +0 -61
- package/tsconfig.tsbuildinfo +0 -1
package/core/store.js
DELETED
|
@@ -1,595 +0,0 @@
|
|
|
1
|
-
import { produce as _immerProduce, freeze as _immerFreeze } from 'immer';
|
|
2
|
-
import * as Security from "./security";
|
|
3
|
-
export const StorageAdapters = {
|
|
4
|
-
local: () => (typeof window !== "undefined" ? window.localStorage : null),
|
|
5
|
-
session: () => (typeof window !== "undefined" ? window.sessionStorage : null),
|
|
6
|
-
memory: () => {
|
|
7
|
-
const _m = new Map();
|
|
8
|
-
return {
|
|
9
|
-
getItem: (k) => _m.get(k) || null,
|
|
10
|
-
setItem: (k, v) => _m.set(k, v),
|
|
11
|
-
removeItem: (k) => _m.delete(k),
|
|
12
|
-
key: (i) => Array.from(_m.keys())[i] || null,
|
|
13
|
-
get length() { return _m.size; }
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
import { deepClone, isEqual } from './utils';
|
|
18
|
-
export const createStore = (config) => {
|
|
19
|
-
const _store = new Map(), _versions = new Map(), _sizes = new Map(), _listeners = new Set(), _keyListeners = new Map(), _middlewares = new Set(), _watchers = new Map(), _computed = new Map(), _computedDeps = new Map(), _plugins = new Map(), _diskQueue = new Map(), _regexCache = new Map(), _accessRules = new Map(), _consents = new Map(), _namespace = config?.namespace || "gstate", _silent = config?.silent ?? false, _debounceTime = config?.debounceTime ?? 150, _currentVersion = config?.version ?? 0, _storage = config?.storage || StorageAdapters.local(), _onError = config?.onError, _maxObjectSize = config?.maxObjectSize ?? (5 * 1024 * 1024), _maxTotalSize = config?.maxTotalSize ?? (50 * 1024 * 1024), _encryptionKey = config?.encryptionKey ?? null, _validateInput = config?.validateInput ?? true, _auditEnabled = config?.auditEnabled ?? true, _userId = config?.userId, _immer = config?.immer ?? true, _persistByDefault = config?.persistByDefault ?? config?.persistence ?? config?.persist ?? false;
|
|
20
|
-
if (config?.accessRules) {
|
|
21
|
-
config.accessRules.forEach(rule => Security.addAccessRule(_accessRules, rule.pattern, rule.permissions));
|
|
22
|
-
}
|
|
23
|
-
let _isTransaction = false, _pendingEmit = false, _isReady = false, _totalSize = 0, _diskTimer = null;
|
|
24
|
-
let _readyResolver;
|
|
25
|
-
const _readyPromise = new Promise(resolve => { _readyResolver = resolve; });
|
|
26
|
-
const _calculateSize = (val) => {
|
|
27
|
-
if (val === null || val === undefined)
|
|
28
|
-
return 0;
|
|
29
|
-
const type = typeof val;
|
|
30
|
-
if (type === 'boolean')
|
|
31
|
-
return 4;
|
|
32
|
-
if (type === 'number')
|
|
33
|
-
return 8;
|
|
34
|
-
if (type === 'string')
|
|
35
|
-
return val.length * 2;
|
|
36
|
-
if (type !== 'object')
|
|
37
|
-
return 0;
|
|
38
|
-
let bytes = 0;
|
|
39
|
-
const stack = [val];
|
|
40
|
-
const seen = new WeakSet();
|
|
41
|
-
while (stack.length > 0) {
|
|
42
|
-
const value = stack.pop();
|
|
43
|
-
if (typeof value === 'boolean') {
|
|
44
|
-
bytes += 4;
|
|
45
|
-
}
|
|
46
|
-
else if (typeof value === 'number') {
|
|
47
|
-
bytes += 8;
|
|
48
|
-
}
|
|
49
|
-
else if (typeof value === 'string') {
|
|
50
|
-
bytes += value.length * 2;
|
|
51
|
-
}
|
|
52
|
-
else if (typeof value === 'object' && value !== null) {
|
|
53
|
-
const obj = value;
|
|
54
|
-
if (seen.has(obj))
|
|
55
|
-
continue;
|
|
56
|
-
seen.add(obj);
|
|
57
|
-
if (Array.isArray(obj)) {
|
|
58
|
-
for (let i = 0; i < obj.length; i++)
|
|
59
|
-
stack.push(obj[i]);
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
for (const key of Object.keys(obj)) {
|
|
63
|
-
bytes += key.length * 2;
|
|
64
|
-
stack.push(obj[key]);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return bytes;
|
|
70
|
-
};
|
|
71
|
-
const _getPrefix = () => `${_namespace}_`;
|
|
72
|
-
const _runHook = (name, context) => {
|
|
73
|
-
if (_plugins.size === 0)
|
|
74
|
-
return;
|
|
75
|
-
for (const p of _plugins.values()) {
|
|
76
|
-
const hook = p.hooks?.[name];
|
|
77
|
-
if (hook) {
|
|
78
|
-
try {
|
|
79
|
-
hook(context);
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
83
|
-
if (_onError)
|
|
84
|
-
_onError(error, { operation: `plugin:${p.name}:${name}`, key: context.key });
|
|
85
|
-
else if (!_silent)
|
|
86
|
-
console.error(`[gState] Plugin "${p.name}" error:`, e);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
const _audit = (action, key, success, error) => {
|
|
92
|
-
if (_auditEnabled && Security.isAuditActive() && Security.logAudit) {
|
|
93
|
-
Security.logAudit({ timestamp: Date.now(), action, key, userId: _userId, success, error });
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
const _emit = (changedKey) => {
|
|
97
|
-
if (changedKey) {
|
|
98
|
-
const dependents = _computedDeps.get(changedKey);
|
|
99
|
-
if (dependents) {
|
|
100
|
-
for (const compKey of dependents)
|
|
101
|
-
_updateComputed(compKey);
|
|
102
|
-
}
|
|
103
|
-
const watchers = _watchers.get(changedKey);
|
|
104
|
-
if (watchers) {
|
|
105
|
-
const val = instance.get(changedKey);
|
|
106
|
-
for (const w of watchers) {
|
|
107
|
-
try {
|
|
108
|
-
w(val);
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
112
|
-
if (_onError)
|
|
113
|
-
_onError(error, { operation: 'watcher', key: changedKey });
|
|
114
|
-
else if (!_silent)
|
|
115
|
-
console.error(`[gState] Watcher error for "${changedKey}":`, e);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const keyListeners = _keyListeners.get(changedKey);
|
|
120
|
-
if (keyListeners) {
|
|
121
|
-
for (const l of keyListeners) {
|
|
122
|
-
try {
|
|
123
|
-
l();
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
127
|
-
if (_onError)
|
|
128
|
-
_onError(error, { operation: 'keyListener', key: changedKey });
|
|
129
|
-
else if (!_silent)
|
|
130
|
-
console.error(`[gState] Listener error for "${changedKey}":`, e);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (_isTransaction) {
|
|
136
|
-
_pendingEmit = true;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
for (const l of _listeners) {
|
|
140
|
-
try {
|
|
141
|
-
l();
|
|
142
|
-
}
|
|
143
|
-
catch (e) {
|
|
144
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
145
|
-
if (_onError)
|
|
146
|
-
_onError(error, { operation: 'listener' });
|
|
147
|
-
else if (!_silent)
|
|
148
|
-
console.error(`[gState] Global listener error: `, e);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
const _updateComputed = (key) => {
|
|
153
|
-
const comp = _computed.get(key), depsFound = new Set();
|
|
154
|
-
if (!comp)
|
|
155
|
-
return;
|
|
156
|
-
const getter = (k) => {
|
|
157
|
-
depsFound.add(k);
|
|
158
|
-
if (_computed.has(k))
|
|
159
|
-
return _computed.get(k).lastValue;
|
|
160
|
-
return instance.get(k);
|
|
161
|
-
};
|
|
162
|
-
const newValue = comp.selector(getter);
|
|
163
|
-
comp.deps.forEach(d => {
|
|
164
|
-
if (!depsFound.has(d)) {
|
|
165
|
-
const dependents = _computedDeps.get(d);
|
|
166
|
-
if (dependents) {
|
|
167
|
-
dependents.delete(key);
|
|
168
|
-
if (dependents.size === 0)
|
|
169
|
-
_computedDeps.delete(d);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
depsFound.forEach(d => {
|
|
174
|
-
if (!comp.deps.has(d)) {
|
|
175
|
-
if (!_computedDeps.has(d))
|
|
176
|
-
_computedDeps.set(d, new Set());
|
|
177
|
-
_computedDeps.get(d).add(key);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
comp.deps = depsFound;
|
|
181
|
-
if (!isEqual(comp.lastValue, newValue)) {
|
|
182
|
-
comp.lastValue = (_immer && newValue !== null && typeof newValue === 'object') ? _immerFreeze(deepClone(newValue), true) : newValue;
|
|
183
|
-
_versions.set(key, (_versions.get(key) || 0) + 1);
|
|
184
|
-
_emit(key);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
const _flushDisk = async () => {
|
|
188
|
-
if (!_storage)
|
|
189
|
-
return;
|
|
190
|
-
try {
|
|
191
|
-
const stateObj = {};
|
|
192
|
-
_store.forEach((v, k) => { stateObj[k] = v; });
|
|
193
|
-
let dataValue;
|
|
194
|
-
const isEncoded = config?.encoded;
|
|
195
|
-
if (isEncoded) {
|
|
196
|
-
dataValue = btoa(JSON.stringify(stateObj));
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
dataValue = JSON.stringify(stateObj);
|
|
200
|
-
}
|
|
201
|
-
_storage.setItem(_getPrefix().replace('_', ''), JSON.stringify({
|
|
202
|
-
v: 1, t: Date.now(), e: null,
|
|
203
|
-
d: dataValue, _sys_v: _currentVersion, _b64: isEncoded ? true : undefined
|
|
204
|
-
}));
|
|
205
|
-
_audit('set', 'FULL_STATE', true);
|
|
206
|
-
}
|
|
207
|
-
catch (e) {
|
|
208
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
209
|
-
if (_onError)
|
|
210
|
-
_onError(error, { operation: 'persist', key: 'FULL_STATE' });
|
|
211
|
-
else if (!_silent)
|
|
212
|
-
console.error(`[gState] Persist failed: `, error);
|
|
213
|
-
}
|
|
214
|
-
const queue = Array.from(_diskQueue.entries());
|
|
215
|
-
_diskQueue.clear();
|
|
216
|
-
for (const [key, data] of queue) {
|
|
217
|
-
try {
|
|
218
|
-
let dataValue = data.value;
|
|
219
|
-
const isEncoded = data.options.encoded || data.options.secure;
|
|
220
|
-
if (data.options.encrypted) {
|
|
221
|
-
if (!_encryptionKey)
|
|
222
|
-
throw new Error(`Encryption key missing for "${key}"`);
|
|
223
|
-
dataValue = await Security.encrypt(data.value, _encryptionKey);
|
|
224
|
-
}
|
|
225
|
-
else if (isEncoded) {
|
|
226
|
-
dataValue = btoa(JSON.stringify(data.value));
|
|
227
|
-
}
|
|
228
|
-
else if (typeof data.value === 'object' && data.value !== null) {
|
|
229
|
-
dataValue = JSON.stringify(data.value);
|
|
230
|
-
}
|
|
231
|
-
_storage.setItem(`${_getPrefix()}${key}`, JSON.stringify({
|
|
232
|
-
v: (_versions.get(key) || 1), t: Date.now(), e: data.options.ttl ? Date.now() + data.options.ttl : null,
|
|
233
|
-
d: dataValue, _sys_v: _currentVersion, _enc: data.options.encrypted ? true : undefined, _b64: isEncoded ? true : undefined
|
|
234
|
-
}));
|
|
235
|
-
_audit('set', key, true);
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
239
|
-
if (_onError)
|
|
240
|
-
_onError(error, { operation: 'persist', key });
|
|
241
|
-
else if (!_silent)
|
|
242
|
-
console.error(`[gState] Persist failed: `, error);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const _methodNamespace = {};
|
|
247
|
-
const instance = {
|
|
248
|
-
_setSilently: (key, value) => {
|
|
249
|
-
const oldSize = _sizes.get(key) || 0, frozen = (_immer && value !== null && typeof value === 'object') ? _immerFreeze(deepClone(value), true) : value;
|
|
250
|
-
const newSize = _calculateSize(frozen);
|
|
251
|
-
_totalSize = _totalSize - oldSize + newSize;
|
|
252
|
-
_sizes.set(key, newSize);
|
|
253
|
-
_store.set(key, frozen);
|
|
254
|
-
_versions.set(key, (_versions.get(key) || 0) + 1);
|
|
255
|
-
},
|
|
256
|
-
_registerMethod: (pluginNameOrName, methodNameOrFn, fn) => {
|
|
257
|
-
if (fn !== undefined) {
|
|
258
|
-
const pluginName = pluginNameOrName;
|
|
259
|
-
const methodName = methodNameOrFn;
|
|
260
|
-
if (!_methodNamespace[pluginName])
|
|
261
|
-
_methodNamespace[pluginName] = {};
|
|
262
|
-
_methodNamespace[pluginName][methodName] = fn;
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
console.warn('[gState] _registerMethod(name, fn) is deprecated. Use _registerMethod(pluginName, methodName, fn) instead.');
|
|
266
|
-
const name = pluginNameOrName;
|
|
267
|
-
const methodFn = methodNameOrFn;
|
|
268
|
-
if (!_methodNamespace['core'])
|
|
269
|
-
_methodNamespace['core'] = {};
|
|
270
|
-
_methodNamespace['core'][name] = methodFn;
|
|
271
|
-
},
|
|
272
|
-
set: (key, valOrUp, options = {}) => {
|
|
273
|
-
const oldVal = _store.get(key), newVal = _immer && typeof valOrUp === 'function' ? _immerProduce(oldVal, valOrUp) : valOrUp;
|
|
274
|
-
if (_validateInput && !Security.validateKey(key)) {
|
|
275
|
-
if (!_silent)
|
|
276
|
-
console.warn(`[gState] Invalid key: ${key}`);
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
if (!Security.hasPermission(_accessRules, key, 'write', _userId)) {
|
|
280
|
-
_audit('set', key, false, 'RBAC Denied');
|
|
281
|
-
if (!_silent)
|
|
282
|
-
console.error(`[gState] RBAC Denied for "${key}"`);
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
const sani = _validateInput ? Security.sanitizeValue(newVal) : newVal;
|
|
286
|
-
const oldSize = _sizes.get(key) || 0;
|
|
287
|
-
_runHook('onBeforeSet', { key, value: sani, store: instance, version: _versions.get(key) || 0 });
|
|
288
|
-
const frozen = (_immer && sani !== null && typeof sani === 'object') ? _immerFreeze(deepClone(sani), true) : sani;
|
|
289
|
-
if (!isEqual(oldVal, frozen)) {
|
|
290
|
-
const hasLimits = _maxObjectSize > 0 || _maxTotalSize > 0;
|
|
291
|
-
const finalSize = hasLimits ? _calculateSize(frozen) : 0;
|
|
292
|
-
if (_maxObjectSize > 0 && finalSize > _maxObjectSize) {
|
|
293
|
-
const error = new Error(`Object size (${finalSize} bytes) exceeds maxObjectSize (${_maxObjectSize} bytes)`);
|
|
294
|
-
if (_onError)
|
|
295
|
-
_onError(error, { operation: 'set', key });
|
|
296
|
-
else if (!_silent)
|
|
297
|
-
console.warn(`[gState] ${error.message} for "${key}"`);
|
|
298
|
-
}
|
|
299
|
-
if (_maxTotalSize > 0) {
|
|
300
|
-
const est = _totalSize - oldSize + finalSize;
|
|
301
|
-
if (est > _maxTotalSize) {
|
|
302
|
-
const error = new Error(`Total store size (${est} bytes) exceeds limit (${_maxTotalSize} bytes)`);
|
|
303
|
-
if (_onError)
|
|
304
|
-
_onError(error, { operation: 'set' });
|
|
305
|
-
else if (!_silent)
|
|
306
|
-
console.warn(`[gState] ${error.message}`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
_totalSize = _totalSize - oldSize + finalSize;
|
|
310
|
-
_sizes.set(key, finalSize);
|
|
311
|
-
_store.set(key, frozen);
|
|
312
|
-
_versions.set(key, (_versions.get(key) || 0) + 1);
|
|
313
|
-
const shouldPersist = options.persist ?? _persistByDefault;
|
|
314
|
-
if (shouldPersist) {
|
|
315
|
-
_diskQueue.set(key, { value: frozen, options: { ...options, persist: shouldPersist, encoded: options.encoded || config?.encoded } });
|
|
316
|
-
if (_diskTimer)
|
|
317
|
-
clearTimeout(_diskTimer);
|
|
318
|
-
_diskTimer = setTimeout(_flushDisk, _debounceTime);
|
|
319
|
-
}
|
|
320
|
-
_runHook('onSet', { key, value: frozen, store: instance, version: _versions.get(key) });
|
|
321
|
-
_audit('set', key, true);
|
|
322
|
-
_emit(key);
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
return false;
|
|
326
|
-
},
|
|
327
|
-
get: (key) => {
|
|
328
|
-
if (!Security.hasPermission(_accessRules, key, 'read', _userId)) {
|
|
329
|
-
_audit('get', key, false, 'RBAC Denied');
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
const val = _store.get(key);
|
|
333
|
-
_runHook('onGet', { store: instance, key, value: val });
|
|
334
|
-
_audit('get', key, true);
|
|
335
|
-
return val;
|
|
336
|
-
},
|
|
337
|
-
compute: (key, selector) => {
|
|
338
|
-
try {
|
|
339
|
-
if (!_computed.has(key)) {
|
|
340
|
-
_computed.set(key, { selector: selector, lastValue: null, deps: new Set() });
|
|
341
|
-
_updateComputed(key);
|
|
342
|
-
}
|
|
343
|
-
return _computed.get(key).lastValue;
|
|
344
|
-
}
|
|
345
|
-
catch (e) {
|
|
346
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
347
|
-
if (_onError)
|
|
348
|
-
_onError(error, { operation: 'compute', key });
|
|
349
|
-
else if (!_silent)
|
|
350
|
-
console.error(`[gState] Compute error for "${key}": `, e);
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
},
|
|
354
|
-
watch: (key, callback) => {
|
|
355
|
-
if (!_watchers.has(key))
|
|
356
|
-
_watchers.set(key, new Set());
|
|
357
|
-
const set = _watchers.get(key);
|
|
358
|
-
set.add(callback);
|
|
359
|
-
return () => { set.delete(callback); if (set.size === 0)
|
|
360
|
-
_watchers.delete(key); };
|
|
361
|
-
},
|
|
362
|
-
remove: (key) => {
|
|
363
|
-
if (!Security.hasPermission(_accessRules, key, 'delete', _userId)) {
|
|
364
|
-
_audit('delete', key, false, 'RBAC Denied');
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
const old = _store.get(key), deleted = _store.delete(key);
|
|
368
|
-
if (deleted) {
|
|
369
|
-
_totalSize -= (_sizes.get(key) || 0);
|
|
370
|
-
_sizes.delete(key);
|
|
371
|
-
_runHook('onRemove', { store: instance, key, value: old });
|
|
372
|
-
}
|
|
373
|
-
_versions.set(key, (_versions.get(key) || 0) + 1);
|
|
374
|
-
if (_storage)
|
|
375
|
-
_storage.removeItem(`${_getPrefix()}${key} `);
|
|
376
|
-
_audit('delete', key, true);
|
|
377
|
-
_emit(key);
|
|
378
|
-
return deleted;
|
|
379
|
-
},
|
|
380
|
-
delete: (key) => instance.remove(key),
|
|
381
|
-
deleteAll: () => {
|
|
382
|
-
Array.from(_store.keys()).forEach(k => instance.remove(k));
|
|
383
|
-
if (_storage) {
|
|
384
|
-
const prefix = _getPrefix();
|
|
385
|
-
for (let i = 0; i < (_storage.length || 0); i++) {
|
|
386
|
-
const k = _storage.key(i);
|
|
387
|
-
if (k?.startsWith(prefix)) {
|
|
388
|
-
_storage.removeItem(k);
|
|
389
|
-
i--;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
_totalSize = 0;
|
|
394
|
-
_sizes.clear();
|
|
395
|
-
return true;
|
|
396
|
-
},
|
|
397
|
-
list: () => Object.fromEntries(_store.entries()),
|
|
398
|
-
use: (m) => { _middlewares.add(m); },
|
|
399
|
-
transaction: (fn) => {
|
|
400
|
-
_isTransaction = true;
|
|
401
|
-
_runHook('onTransaction', { store: instance, key: 'START' });
|
|
402
|
-
try {
|
|
403
|
-
fn();
|
|
404
|
-
}
|
|
405
|
-
finally {
|
|
406
|
-
_isTransaction = false;
|
|
407
|
-
_runHook('onTransaction', { store: instance, key: 'END' });
|
|
408
|
-
if (_pendingEmit) {
|
|
409
|
-
_pendingEmit = false;
|
|
410
|
-
_emit();
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
},
|
|
414
|
-
destroy: () => {
|
|
415
|
-
if (_diskTimer) {
|
|
416
|
-
clearTimeout(_diskTimer);
|
|
417
|
-
_diskTimer = null;
|
|
418
|
-
}
|
|
419
|
-
_diskQueue.clear();
|
|
420
|
-
if (typeof window !== 'undefined')
|
|
421
|
-
window.removeEventListener('beforeunload', _unloadHandler);
|
|
422
|
-
_runHook('onDestroy', { store: instance });
|
|
423
|
-
_listeners.clear();
|
|
424
|
-
_keyListeners.clear();
|
|
425
|
-
_watchers.clear();
|
|
426
|
-
_computed.clear();
|
|
427
|
-
_computedDeps.clear();
|
|
428
|
-
_plugins.clear();
|
|
429
|
-
_store.clear();
|
|
430
|
-
_sizes.clear();
|
|
431
|
-
_totalSize = 0;
|
|
432
|
-
_accessRules.clear();
|
|
433
|
-
_consents.clear();
|
|
434
|
-
_versions.clear();
|
|
435
|
-
_regexCache.clear();
|
|
436
|
-
_middlewares.clear();
|
|
437
|
-
},
|
|
438
|
-
_addPlugin: (p) => {
|
|
439
|
-
try {
|
|
440
|
-
_plugins.set(p.name, p);
|
|
441
|
-
p.hooks?.onInstall?.({ store: instance });
|
|
442
|
-
}
|
|
443
|
-
catch (e) {
|
|
444
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
445
|
-
if (_onError)
|
|
446
|
-
_onError(error, { operation: 'plugin:install', key: p.name });
|
|
447
|
-
else if (!_silent)
|
|
448
|
-
console.error(`[gState] Failed to install plugin "${p.name}": `, e);
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
_removePlugin: (name) => { _plugins.delete(name); },
|
|
452
|
-
_subscribe: (cb, key) => {
|
|
453
|
-
if (key) {
|
|
454
|
-
if (!_keyListeners.has(key))
|
|
455
|
-
_keyListeners.set(key, new Set());
|
|
456
|
-
const set = _keyListeners.get(key);
|
|
457
|
-
set.add(cb);
|
|
458
|
-
return () => { set.delete(cb); if (set.size === 0)
|
|
459
|
-
_keyListeners.delete(key); };
|
|
460
|
-
}
|
|
461
|
-
_listeners.add(cb);
|
|
462
|
-
return () => _listeners.delete(cb);
|
|
463
|
-
},
|
|
464
|
-
_getVersion: (key) => _versions.get(key) ?? 0,
|
|
465
|
-
addAccessRule: (pattern, permissions) => Security.addAccessRule(_accessRules, pattern, permissions),
|
|
466
|
-
hasPermission: (key, action, userId) => {
|
|
467
|
-
if (_accessRules.size === 0)
|
|
468
|
-
return true;
|
|
469
|
-
for (const [pattern, perms] of _accessRules) {
|
|
470
|
-
let matches;
|
|
471
|
-
if (typeof pattern === 'function') {
|
|
472
|
-
matches = pattern(key, userId);
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
try {
|
|
476
|
-
let re = _regexCache.get(pattern);
|
|
477
|
-
if (!re) {
|
|
478
|
-
re = new RegExp(pattern);
|
|
479
|
-
_regexCache.set(pattern, re);
|
|
480
|
-
}
|
|
481
|
-
matches = re.test(key);
|
|
482
|
-
}
|
|
483
|
-
catch {
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (matches)
|
|
488
|
-
return perms.includes(action) || perms.includes('admin');
|
|
489
|
-
}
|
|
490
|
-
return false;
|
|
491
|
-
},
|
|
492
|
-
recordConsent: (userId, purpose, granted) => Security.recordConsent(_consents, userId, purpose, granted),
|
|
493
|
-
hasConsent: (userId, purpose) => Security.hasConsent(_consents, userId, purpose),
|
|
494
|
-
getConsents: (userId) => Security.getConsents(_consents, userId),
|
|
495
|
-
revokeConsent: (userId, purpose) => Security.revokeConsent(_consents, userId, purpose),
|
|
496
|
-
exportUserData: (userId) => Security.exportUserData(_consents, userId),
|
|
497
|
-
deleteUserData: (userId) => Security.deleteUserData(_consents, userId),
|
|
498
|
-
get plugins() { return _methodNamespace; },
|
|
499
|
-
get isReady() { return _isReady; },
|
|
500
|
-
get namespace() { return _namespace; },
|
|
501
|
-
get userId() { return _userId; },
|
|
502
|
-
whenReady: () => _readyPromise
|
|
503
|
-
};
|
|
504
|
-
const secMethods = ['addAccessRule', 'recordConsent', 'hasConsent', 'getConsents', 'revokeConsent', 'exportUserData', 'deleteUserData'];
|
|
505
|
-
secMethods.forEach(m => {
|
|
506
|
-
const fn = instance[m];
|
|
507
|
-
if (fn)
|
|
508
|
-
instance._registerMethod('security', m, fn);
|
|
509
|
-
});
|
|
510
|
-
const _unloadHandler = () => { if (_diskQueue.size > 0)
|
|
511
|
-
_flushDisk(); };
|
|
512
|
-
if (typeof window !== 'undefined')
|
|
513
|
-
window.addEventListener('beforeunload', _unloadHandler);
|
|
514
|
-
if (_storage) {
|
|
515
|
-
const hydrate = async () => {
|
|
516
|
-
try {
|
|
517
|
-
const persisted = {}, prefix = _getPrefix();
|
|
518
|
-
let savedV = 0;
|
|
519
|
-
for (let i = 0; i < (_storage.length || 0); i++) {
|
|
520
|
-
const k = _storage.key(i);
|
|
521
|
-
if (!k || !k.startsWith(prefix))
|
|
522
|
-
continue;
|
|
523
|
-
const raw = _storage.getItem(k);
|
|
524
|
-
if (!raw)
|
|
525
|
-
continue;
|
|
526
|
-
try {
|
|
527
|
-
const meta = JSON.parse(raw), key = k.substring(prefix.length);
|
|
528
|
-
savedV = Math.max(savedV, meta._sys_v !== undefined ? meta._sys_v : (meta.v || 0));
|
|
529
|
-
if (meta.e && Date.now() > meta.e) {
|
|
530
|
-
_storage.removeItem(k);
|
|
531
|
-
i--;
|
|
532
|
-
continue;
|
|
533
|
-
}
|
|
534
|
-
let d = meta.d;
|
|
535
|
-
if (meta._enc && _encryptionKey) {
|
|
536
|
-
d = await Security.decrypt(d, _encryptionKey);
|
|
537
|
-
}
|
|
538
|
-
else if (typeof d === "string") {
|
|
539
|
-
if (meta._b64) {
|
|
540
|
-
try {
|
|
541
|
-
d = JSON.parse(atob(d));
|
|
542
|
-
}
|
|
543
|
-
catch (_e) { }
|
|
544
|
-
}
|
|
545
|
-
else if (d.startsWith("{") || d.startsWith("[")) {
|
|
546
|
-
try {
|
|
547
|
-
d = JSON.parse(d);
|
|
548
|
-
}
|
|
549
|
-
catch (_e) { }
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
persisted[key] = d;
|
|
553
|
-
_audit('hydrate', key, true);
|
|
554
|
-
}
|
|
555
|
-
catch (err) {
|
|
556
|
-
_audit('hydrate', k, false, String(err));
|
|
557
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
558
|
-
if (_onError)
|
|
559
|
-
_onError(error, { operation: 'hydration', key: k });
|
|
560
|
-
else if (!_silent)
|
|
561
|
-
console.error(`[gState] Hydration failed for "${k}": `, err);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
const final = (savedV < _currentVersion && config?.migrate) ? config.migrate(persisted, savedV) : persisted;
|
|
565
|
-
Object.entries(final).forEach(([k, v]) => {
|
|
566
|
-
const frozen = (_immer && v !== null && typeof v === 'object') ? _immerFreeze(deepClone(v), true) : v;
|
|
567
|
-
const size = _calculateSize(frozen);
|
|
568
|
-
const oldSize = _sizes.get(k) || 0;
|
|
569
|
-
_totalSize = _totalSize - oldSize + size;
|
|
570
|
-
_sizes.set(k, size);
|
|
571
|
-
_store.set(k, frozen);
|
|
572
|
-
_versions.set(k, 1);
|
|
573
|
-
});
|
|
574
|
-
_isReady = true;
|
|
575
|
-
_readyResolver();
|
|
576
|
-
_emit();
|
|
577
|
-
}
|
|
578
|
-
catch (e) {
|
|
579
|
-
_isReady = true;
|
|
580
|
-
_readyResolver();
|
|
581
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
582
|
-
if (_onError)
|
|
583
|
-
_onError(error, { operation: 'hydration' });
|
|
584
|
-
else if (!_silent)
|
|
585
|
-
console.error(`[gState] Hydration failed: `, error);
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
hydrate();
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
_isReady = true;
|
|
592
|
-
_readyResolver();
|
|
593
|
-
}
|
|
594
|
-
return instance;
|
|
595
|
-
};
|
package/core/types.js
DELETED
package/core/utils.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export const deepClone = (obj) => {
|
|
2
|
-
if (obj === null || typeof obj !== 'object')
|
|
3
|
-
return obj;
|
|
4
|
-
if (typeof structuredClone === 'function') {
|
|
5
|
-
try {
|
|
6
|
-
return structuredClone(obj);
|
|
7
|
-
}
|
|
8
|
-
catch (_e) {
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
const seen = new WeakMap();
|
|
12
|
-
const clone = (value) => {
|
|
13
|
-
if (value === null || typeof value !== 'object')
|
|
14
|
-
return value;
|
|
15
|
-
if (typeof value === 'function')
|
|
16
|
-
return value;
|
|
17
|
-
if (seen.has(value))
|
|
18
|
-
return seen.get(value);
|
|
19
|
-
if (value instanceof Date)
|
|
20
|
-
return new Date(value.getTime());
|
|
21
|
-
if (value instanceof RegExp)
|
|
22
|
-
return new RegExp(value.source, value.flags);
|
|
23
|
-
if (value instanceof Map) {
|
|
24
|
-
const result = new Map();
|
|
25
|
-
seen.set(value, result);
|
|
26
|
-
value.forEach((v, k) => result.set(clone(k), clone(v)));
|
|
27
|
-
return result;
|
|
28
|
-
}
|
|
29
|
-
if (value instanceof Set) {
|
|
30
|
-
const result = new Set();
|
|
31
|
-
seen.set(value, result);
|
|
32
|
-
value.forEach((v) => result.add(clone(v)));
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
const result = (Array.isArray(value)
|
|
36
|
-
? []
|
|
37
|
-
: Object.create(Object.getPrototypeOf(value)));
|
|
38
|
-
seen.set(value, result);
|
|
39
|
-
const keys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)];
|
|
40
|
-
for (const key of keys) {
|
|
41
|
-
result[key] = clone(value[key]);
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
};
|
|
45
|
-
return clone(obj);
|
|
46
|
-
};
|
|
47
|
-
export const isEqual = (a, b) => {
|
|
48
|
-
if (a === b)
|
|
49
|
-
return true;
|
|
50
|
-
if (a === null || b === null)
|
|
51
|
-
return a === b;
|
|
52
|
-
if (typeof a !== 'object' || typeof b !== 'object')
|
|
53
|
-
return a === b;
|
|
54
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
55
|
-
if (a.length !== b.length)
|
|
56
|
-
return false;
|
|
57
|
-
for (let i = 0; i < a.length; i++)
|
|
58
|
-
if (!isEqual(a[i], b[i]))
|
|
59
|
-
return false;
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
const keysA = Object.keys(a);
|
|
63
|
-
const keysB = Object.keys(b);
|
|
64
|
-
if (keysA.length !== keysB.length)
|
|
65
|
-
return false;
|
|
66
|
-
for (let i = 0; i < keysA.length; i++) {
|
|
67
|
-
const key = keysA[i];
|
|
68
|
-
if (!(key in b) || !isEqual(a[key], b[key]))
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
};
|