scope-state 0.1.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 +372 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.esm.js +2023 -0
- package/dist/index.js +2073 -0
- package/dist/src/config/index.d.ts +84 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/core/listeners.d.ts +24 -0
- package/dist/src/core/listeners.d.ts.map +1 -0
- package/dist/src/core/monitoring.d.ts +167 -0
- package/dist/src/core/monitoring.d.ts.map +1 -0
- package/dist/src/core/proxy.d.ts +50 -0
- package/dist/src/core/proxy.d.ts.map +1 -0
- package/dist/src/core/store.d.ts +39 -0
- package/dist/src/core/store.d.ts.map +1 -0
- package/dist/src/core/tracking.d.ts +36 -0
- package/dist/src/core/tracking.d.ts.map +1 -0
- package/dist/src/hooks/useLocal.d.ts +32 -0
- package/dist/src/hooks/useLocal.d.ts.map +1 -0
- package/dist/src/hooks/useScope.d.ts +29 -0
- package/dist/src/hooks/useScope.d.ts.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/persistence/advanced.d.ts +47 -0
- package/dist/src/persistence/advanced.d.ts.map +1 -0
- package/dist/src/persistence/storage.d.ts +14 -0
- package/dist/src/persistence/storage.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +175 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/package.json +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2073 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var localforage = require('localforage');
|
|
5
|
+
var memoryDriver = require('localforage-driver-memory');
|
|
6
|
+
|
|
7
|
+
function _interopNamespaceDefault(e) {
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var memoryDriver__namespace = /*#__PURE__*/_interopNamespaceDefault(memoryDriver);
|
|
25
|
+
|
|
26
|
+
// Default configurations
|
|
27
|
+
const defaultProxyConfig = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
maxDepth: 5,
|
|
30
|
+
selectiveProxying: false,
|
|
31
|
+
trackPathUsage: true,
|
|
32
|
+
lazyProxyDeepObjects: false,
|
|
33
|
+
preProxyPaths: [],
|
|
34
|
+
maxPathLength: 20,
|
|
35
|
+
smartArrayTracking: true,
|
|
36
|
+
nonBlockingProxyCreation: true,
|
|
37
|
+
batchSize: 1,
|
|
38
|
+
prioritizeUIObjects: true,
|
|
39
|
+
maxQueueSize: 1000,
|
|
40
|
+
memoryLimit: false,
|
|
41
|
+
memoryThreshold: 1,
|
|
42
|
+
targetMemoryUsage: 3000,
|
|
43
|
+
proxyEvictionStrategy: 'lru',
|
|
44
|
+
disableProxyingUnderPressure: false,
|
|
45
|
+
maxProxyCacheSize: 5000,
|
|
46
|
+
ultraSelectiveProxying: false,
|
|
47
|
+
proxySelectorPaths: true,
|
|
48
|
+
forceMemoryCheck: true,
|
|
49
|
+
aggressiveMemoryManagement: false,
|
|
50
|
+
priorityPaths: [],
|
|
51
|
+
};
|
|
52
|
+
const defaultMonitoringConfig = {
|
|
53
|
+
enabled: true,
|
|
54
|
+
verboseLogging: false,
|
|
55
|
+
logSubscriptions: false,
|
|
56
|
+
logProxyCreation: false,
|
|
57
|
+
logStateChanges: false,
|
|
58
|
+
logPersistence: false,
|
|
59
|
+
logTimings: false,
|
|
60
|
+
autoLeakDetection: false,
|
|
61
|
+
leakDetectionInterval: 30000,
|
|
62
|
+
leakAlertThreshold: 0,
|
|
63
|
+
leakScanMinimumRenderCycles: 3,
|
|
64
|
+
};
|
|
65
|
+
const defaultPersistenceConfig = {
|
|
66
|
+
enabled: true,
|
|
67
|
+
paths: [],
|
|
68
|
+
blacklist: [],
|
|
69
|
+
batchDelay: 300,
|
|
70
|
+
};
|
|
71
|
+
// Current active configurations (will be modified by configure())
|
|
72
|
+
let proxyConfig = { ...defaultProxyConfig };
|
|
73
|
+
let monitoringConfig = { ...defaultMonitoringConfig };
|
|
74
|
+
let persistenceConfig = { ...defaultPersistenceConfig };
|
|
75
|
+
// Note: The main configure function is now in src/index.ts to avoid circular dependencies
|
|
76
|
+
// These configuration objects are still exported for internal use
|
|
77
|
+
/**
|
|
78
|
+
* Get current configuration
|
|
79
|
+
*/
|
|
80
|
+
function getConfig() {
|
|
81
|
+
return {
|
|
82
|
+
proxy: { ...proxyConfig },
|
|
83
|
+
monitoring: { ...monitoringConfig },
|
|
84
|
+
persistence: { ...persistenceConfig },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reset all configuration to defaults
|
|
89
|
+
*/
|
|
90
|
+
function resetConfig() {
|
|
91
|
+
proxyConfig = { ...defaultProxyConfig };
|
|
92
|
+
monitoringConfig = { ...defaultMonitoringConfig };
|
|
93
|
+
persistenceConfig = { ...defaultPersistenceConfig };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Quick presets for common use cases
|
|
97
|
+
*/
|
|
98
|
+
const presets = {
|
|
99
|
+
/**
|
|
100
|
+
* Development preset: Enhanced debugging and monitoring
|
|
101
|
+
*/
|
|
102
|
+
development: () => ({
|
|
103
|
+
monitoring: {
|
|
104
|
+
enabled: true,
|
|
105
|
+
verboseLogging: true,
|
|
106
|
+
logSubscriptions: true,
|
|
107
|
+
logStateChanges: true,
|
|
108
|
+
autoLeakDetection: true,
|
|
109
|
+
},
|
|
110
|
+
proxy: {
|
|
111
|
+
maxDepth: 3,
|
|
112
|
+
smartArrayTracking: true,
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
/**
|
|
116
|
+
* Production preset: Optimized for performance
|
|
117
|
+
*/
|
|
118
|
+
production: () => ({
|
|
119
|
+
monitoring: {
|
|
120
|
+
enabled: false,
|
|
121
|
+
verboseLogging: false,
|
|
122
|
+
logSubscriptions: false,
|
|
123
|
+
logStateChanges: false,
|
|
124
|
+
autoLeakDetection: false,
|
|
125
|
+
},
|
|
126
|
+
proxy: {
|
|
127
|
+
maxDepth: 6,
|
|
128
|
+
ultraSelectiveProxying: false,
|
|
129
|
+
aggressiveMemoryManagement: false,
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
/**
|
|
133
|
+
* Memory-constrained preset: Minimal memory usage
|
|
134
|
+
*/
|
|
135
|
+
minimal: () => ({
|
|
136
|
+
monitoring: {
|
|
137
|
+
enabled: false,
|
|
138
|
+
},
|
|
139
|
+
proxy: {
|
|
140
|
+
maxDepth: 1,
|
|
141
|
+
ultraSelectiveProxying: true,
|
|
142
|
+
aggressiveMemoryManagement: true,
|
|
143
|
+
maxProxyCacheSize: 1000,
|
|
144
|
+
maxQueueSize: 100,
|
|
145
|
+
},
|
|
146
|
+
persistence: {
|
|
147
|
+
enabled: false,
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
/**
|
|
151
|
+
* Full-featured preset: All features enabled
|
|
152
|
+
*/
|
|
153
|
+
full: () => ({
|
|
154
|
+
monitoring: {
|
|
155
|
+
enabled: true,
|
|
156
|
+
verboseLogging: true,
|
|
157
|
+
autoLeakDetection: true,
|
|
158
|
+
},
|
|
159
|
+
proxy: {
|
|
160
|
+
maxDepth: 5,
|
|
161
|
+
selectiveProxying: false,
|
|
162
|
+
},
|
|
163
|
+
persistence: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Path-specific listeners
|
|
170
|
+
const pathListeners = new Map();
|
|
171
|
+
// Statistics for monitoring
|
|
172
|
+
let monitoringStats = {
|
|
173
|
+
proxyCount: 0,
|
|
174
|
+
totalSubscriptionsCreated: 0,
|
|
175
|
+
totalSubscriptionsRemoved: 0,
|
|
176
|
+
activeSubscriptions: 0,
|
|
177
|
+
pathSubscriptionCounts: {},
|
|
178
|
+
timings: {
|
|
179
|
+
lastNotifyTime: 0,
|
|
180
|
+
lastPersistTime: 0,
|
|
181
|
+
averageNotifyTime: 0,
|
|
182
|
+
averagePersistTime: 0,
|
|
183
|
+
notifyTimeTotal: 0,
|
|
184
|
+
persistTimeTotal: 0,
|
|
185
|
+
notifyCount: 0,
|
|
186
|
+
persistCount: 0,
|
|
187
|
+
},
|
|
188
|
+
leakDetection: {
|
|
189
|
+
lastCheckTime: 0,
|
|
190
|
+
totalChecks: 0,
|
|
191
|
+
leaksDetected: 0,
|
|
192
|
+
renderCyclesSinceCheck: 0,
|
|
193
|
+
isLeakDetectionRunning: false,
|
|
194
|
+
leakDetectionTimer: null,
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Subscribe to changes on a specific path
|
|
199
|
+
*/
|
|
200
|
+
function subscribe(path, listener) {
|
|
201
|
+
var _a;
|
|
202
|
+
if (!pathListeners.has(path)) {
|
|
203
|
+
pathListeners.set(path, new Set());
|
|
204
|
+
}
|
|
205
|
+
(_a = pathListeners.get(path)) === null || _a === void 0 ? void 0 : _a.add(listener);
|
|
206
|
+
logSubscriptionAdded(path);
|
|
207
|
+
// Return unsubscribe function
|
|
208
|
+
return () => {
|
|
209
|
+
var _a, _b;
|
|
210
|
+
(_a = pathListeners.get(path)) === null || _a === void 0 ? void 0 : _a.delete(listener);
|
|
211
|
+
logSubscriptionRemoved(path);
|
|
212
|
+
// Clean up empty listener sets
|
|
213
|
+
if (((_b = pathListeners.get(path)) === null || _b === void 0 ? void 0 : _b.size) === 0) {
|
|
214
|
+
pathListeners.delete(path);
|
|
215
|
+
if (monitoringConfig.enabled && monitoringConfig.logSubscriptions) {
|
|
216
|
+
console.log(`🧹 Removed empty listener set for ${path}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Notify all listeners for a given path and its parents/children
|
|
223
|
+
*/
|
|
224
|
+
function notifyListeners(path) {
|
|
225
|
+
if (typeof window === 'undefined')
|
|
226
|
+
return;
|
|
227
|
+
const pathKey = path.join('.');
|
|
228
|
+
let startTime = 0;
|
|
229
|
+
if (monitoringConfig.enabled && monitoringConfig.logStateChanges) {
|
|
230
|
+
startTime = logTimestamp$1(`⚡️ Notifying path: ${pathKey}`);
|
|
231
|
+
}
|
|
232
|
+
// Track render cycle for leak detection
|
|
233
|
+
if (monitoringConfig.enabled && monitoringConfig.autoLeakDetection) {
|
|
234
|
+
monitoringStats.leakDetection.renderCyclesSinceCheck++;
|
|
235
|
+
}
|
|
236
|
+
// Notify exact path listeners
|
|
237
|
+
if (pathListeners.has(pathKey)) {
|
|
238
|
+
const listeners = pathListeners.get(pathKey);
|
|
239
|
+
if (monitoringConfig.enabled && monitoringConfig.verboseLogging) {
|
|
240
|
+
console.log(`🔔 Notifying ${listeners === null || listeners === void 0 ? void 0 : listeners.size} exact listeners for ${pathKey}`);
|
|
241
|
+
}
|
|
242
|
+
listeners === null || listeners === void 0 ? void 0 : listeners.forEach(listener => listener());
|
|
243
|
+
}
|
|
244
|
+
// Special handling for array index changes - notify the array itself too
|
|
245
|
+
const lastSegment = path.length > 0 ? path[path.length - 1] : '';
|
|
246
|
+
if (lastSegment && !isNaN(Number(lastSegment))) {
|
|
247
|
+
// This is an array index update, also notify about the array
|
|
248
|
+
const arrayPath = path.slice(0, -1).join('.');
|
|
249
|
+
if (pathListeners.has(arrayPath)) {
|
|
250
|
+
const listeners = pathListeners.get(arrayPath);
|
|
251
|
+
if (monitoringConfig.enabled && monitoringConfig.verboseLogging) {
|
|
252
|
+
console.log(`🔔 Notifying ${listeners === null || listeners === void 0 ? void 0 : listeners.size} array listeners for ${arrayPath} (index change)`);
|
|
253
|
+
}
|
|
254
|
+
listeners === null || listeners === void 0 ? void 0 : listeners.forEach(listener => listener());
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Also notify parent paths (more granular update notifications)
|
|
258
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
259
|
+
const parentPath = path.slice(0, i).join('.');
|
|
260
|
+
if (pathListeners.has(parentPath)) {
|
|
261
|
+
const listeners = pathListeners.get(parentPath);
|
|
262
|
+
if (monitoringConfig.enabled && monitoringConfig.verboseLogging) {
|
|
263
|
+
console.log(`🔔 Notifying ${listeners === null || listeners === void 0 ? void 0 : listeners.size} parent listeners for ${parentPath}`);
|
|
264
|
+
}
|
|
265
|
+
listeners === null || listeners === void 0 ? void 0 : listeners.forEach(listener => listener());
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Notify child paths (if an object was completely replaced)
|
|
269
|
+
const prefix = pathKey + '.';
|
|
270
|
+
let childListenerCount = 0;
|
|
271
|
+
pathListeners.forEach((listeners, key) => {
|
|
272
|
+
if (key.startsWith(prefix)) {
|
|
273
|
+
childListenerCount += listeners.size;
|
|
274
|
+
listeners.forEach(listener => listener());
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
if (monitoringConfig.enabled && monitoringConfig.verboseLogging && childListenerCount > 0) {
|
|
278
|
+
console.log(`🔔 Notified ${childListenerCount} child listeners for paths starting with ${prefix}`);
|
|
279
|
+
}
|
|
280
|
+
if (monitoringConfig.enabled && monitoringConfig.logStateChanges && startTime > 0) {
|
|
281
|
+
const duration = logTimingEnd$1('Notification cycle', startTime);
|
|
282
|
+
updateTimingStat('notify', duration);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Logging helper functions
|
|
286
|
+
function logTimestamp$1(action) {
|
|
287
|
+
if (!monitoringConfig.enabled || !monitoringConfig.logTimings)
|
|
288
|
+
return 0;
|
|
289
|
+
const now = performance.now();
|
|
290
|
+
console.log(`⏱️ [${now.toFixed(2)}ms] ${action}`);
|
|
291
|
+
return now;
|
|
292
|
+
}
|
|
293
|
+
function logTimingEnd$1(action, startTime) {
|
|
294
|
+
if (!monitoringConfig.enabled || !monitoringConfig.logTimings)
|
|
295
|
+
return 0;
|
|
296
|
+
const now = performance.now();
|
|
297
|
+
const duration = now - startTime;
|
|
298
|
+
console.log(`⏱️ [${now.toFixed(2)}ms] ${action} completed in ${duration.toFixed(2)}ms`);
|
|
299
|
+
return duration;
|
|
300
|
+
}
|
|
301
|
+
function updateTimingStat(type, duration) {
|
|
302
|
+
if (!monitoringConfig.enabled)
|
|
303
|
+
return;
|
|
304
|
+
if (type === 'notify') {
|
|
305
|
+
monitoringStats.timings.lastNotifyTime = duration;
|
|
306
|
+
monitoringStats.timings.notifyTimeTotal += duration;
|
|
307
|
+
monitoringStats.timings.notifyCount++;
|
|
308
|
+
monitoringStats.timings.averageNotifyTime =
|
|
309
|
+
monitoringStats.timings.notifyTimeTotal / monitoringStats.timings.notifyCount;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
monitoringStats.timings.lastPersistTime = duration;
|
|
313
|
+
monitoringStats.timings.persistTimeTotal += duration;
|
|
314
|
+
monitoringStats.timings.persistCount++;
|
|
315
|
+
monitoringStats.timings.averagePersistTime =
|
|
316
|
+
monitoringStats.timings.persistTimeTotal / monitoringStats.timings.persistCount;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function logSubscriptionAdded(path) {
|
|
320
|
+
if (!monitoringConfig.enabled || !monitoringConfig.logSubscriptions)
|
|
321
|
+
return;
|
|
322
|
+
monitoringStats.totalSubscriptionsCreated++;
|
|
323
|
+
monitoringStats.activeSubscriptions++;
|
|
324
|
+
monitoringStats.pathSubscriptionCounts[path] = (monitoringStats.pathSubscriptionCounts[path] || 0) + 1;
|
|
325
|
+
console.log(`📈 Subscription ADDED to ${path}. Total: ${monitoringStats.activeSubscriptions}, Path count: ${monitoringStats.pathSubscriptionCounts[path]}`);
|
|
326
|
+
}
|
|
327
|
+
function logSubscriptionRemoved(path) {
|
|
328
|
+
var _a;
|
|
329
|
+
if (!monitoringConfig.enabled || !monitoringConfig.logSubscriptions)
|
|
330
|
+
return;
|
|
331
|
+
monitoringStats.totalSubscriptionsRemoved++;
|
|
332
|
+
monitoringStats.activeSubscriptions--;
|
|
333
|
+
monitoringStats.pathSubscriptionCounts[path] = (monitoringStats.pathSubscriptionCounts[path] || 0) - 1;
|
|
334
|
+
console.log(`📉 Subscription REMOVED from ${path}. Total: ${monitoringStats.activeSubscriptions}, Path count: ${monitoringStats.pathSubscriptionCounts[path]}`);
|
|
335
|
+
// Alert if a path has no subscribers but still exists in the map
|
|
336
|
+
if (monitoringStats.pathSubscriptionCounts[path] <= 0) {
|
|
337
|
+
const actualListeners = ((_a = pathListeners.get(path)) === null || _a === void 0 ? void 0 : _a.size) || 0;
|
|
338
|
+
if (actualListeners > 0) {
|
|
339
|
+
console.warn(`⚠️ Path ${path} shows ${actualListeners} listeners but tracking shows ${monitoringStats.pathSubscriptionCounts[path]}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Track proxy to path mapping for type-safe activation
|
|
345
|
+
const proxyPathMap = new WeakMap();
|
|
346
|
+
// Store a deep clone of the initial store state for use with $reset
|
|
347
|
+
let initialStoreState$1 = {};
|
|
348
|
+
// Path usage tracking
|
|
349
|
+
const pathUsageStats = {
|
|
350
|
+
accessedPaths: new Set(),
|
|
351
|
+
modifiedPaths: new Set(),
|
|
352
|
+
subscribedPaths: new Set(),
|
|
353
|
+
};
|
|
354
|
+
// Selector paths for ultra-selective proxying
|
|
355
|
+
const selectorPaths = new Set();
|
|
356
|
+
// Proxy cache using WeakMap for garbage collection
|
|
357
|
+
let proxyCache = new WeakMap();
|
|
358
|
+
// Proxy cache statistics
|
|
359
|
+
const proxyCacheStats = {
|
|
360
|
+
cacheHits: 0,
|
|
361
|
+
cacheMisses: 0,
|
|
362
|
+
totalProxiesCreated: 0,
|
|
363
|
+
activeCachedProxies: 0
|
|
364
|
+
};
|
|
365
|
+
// LRU tracking for proxy cache
|
|
366
|
+
const proxyCacheLRU = {
|
|
367
|
+
keys: [],
|
|
368
|
+
timestamp: [],
|
|
369
|
+
maxSize: proxyConfig.maxProxyCacheSize,
|
|
370
|
+
add(key, proxy) {
|
|
371
|
+
if (key && typeof key === 'object') {
|
|
372
|
+
const index = this.keys.findIndex(k => k === key);
|
|
373
|
+
if (index !== -1) {
|
|
374
|
+
this.timestamp[index] = Date.now();
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
this.keys.push(key);
|
|
378
|
+
this.timestamp.push(Date.now());
|
|
379
|
+
if (this.keys.length > this.maxSize) {
|
|
380
|
+
this.evictOldest();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
touch(key) {
|
|
386
|
+
const index = this.keys.findIndex(k => k === key);
|
|
387
|
+
if (index !== -1) {
|
|
388
|
+
this.timestamp[index] = Date.now();
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
evictOldest() {
|
|
392
|
+
if (this.keys.length === 0)
|
|
393
|
+
return;
|
|
394
|
+
let oldestIndex = 0;
|
|
395
|
+
let oldestTime = this.timestamp[0];
|
|
396
|
+
for (let i = 1; i < this.timestamp.length; i++) {
|
|
397
|
+
if (this.timestamp[i] < oldestTime) {
|
|
398
|
+
oldestTime = this.timestamp[i];
|
|
399
|
+
oldestIndex = i;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const oldestKey = this.keys[oldestIndex];
|
|
403
|
+
if (oldestKey && proxyCache.has(oldestKey)) {
|
|
404
|
+
proxyCache.delete(oldestKey);
|
|
405
|
+
proxyCacheStats.activeCachedProxies--;
|
|
406
|
+
}
|
|
407
|
+
this.keys.splice(oldestIndex, 1);
|
|
408
|
+
this.timestamp.splice(oldestIndex, 1);
|
|
409
|
+
},
|
|
410
|
+
evictPercentage(percent) {
|
|
411
|
+
const count = Math.ceil(this.keys.length * percent);
|
|
412
|
+
if (count <= 0)
|
|
413
|
+
return 0;
|
|
414
|
+
const indices = Array.from({ length: this.keys.length }, (_, i) => i)
|
|
415
|
+
.sort((a, b) => this.timestamp[a] - this.timestamp[b]);
|
|
416
|
+
let evicted = 0;
|
|
417
|
+
for (let i = 0; i < count && i < indices.length; i++) {
|
|
418
|
+
const key = this.keys[indices[i]];
|
|
419
|
+
if (key && proxyCache.has(key)) {
|
|
420
|
+
proxyCache.delete(key);
|
|
421
|
+
evicted++;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const newKeys = [];
|
|
425
|
+
const newTimestamps = [];
|
|
426
|
+
for (let i = count; i < indices.length; i++) {
|
|
427
|
+
const idx = indices[i];
|
|
428
|
+
newKeys.push(this.keys[idx]);
|
|
429
|
+
newTimestamps.push(this.timestamp[idx]);
|
|
430
|
+
}
|
|
431
|
+
this.keys = newKeys;
|
|
432
|
+
this.timestamp = newTimestamps;
|
|
433
|
+
proxyCacheStats.activeCachedProxies -= evicted;
|
|
434
|
+
return evicted;
|
|
435
|
+
},
|
|
436
|
+
size() {
|
|
437
|
+
return this.keys.length;
|
|
438
|
+
},
|
|
439
|
+
clear() {
|
|
440
|
+
this.keys = [];
|
|
441
|
+
this.timestamp = [];
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
// Memory pressure detection
|
|
445
|
+
const memoryPressure = {
|
|
446
|
+
isUnderPressure: false,
|
|
447
|
+
lastCheckTime: 0,
|
|
448
|
+
checkInterval: 5000,
|
|
449
|
+
highUsageCount: 0,
|
|
450
|
+
check() {
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
if (!proxyConfig.forceMemoryCheck && now - this.lastCheckTime < this.checkInterval) {
|
|
453
|
+
return this.isUnderPressure;
|
|
454
|
+
}
|
|
455
|
+
this.lastCheckTime = now;
|
|
456
|
+
try {
|
|
457
|
+
const estimatedUsageMB = this.estimateMemoryUsage();
|
|
458
|
+
const thresholdMB = proxyConfig.targetMemoryUsage * proxyConfig.memoryThreshold;
|
|
459
|
+
const wasUnderPressure = this.isUnderPressure;
|
|
460
|
+
this.isUnderPressure = estimatedUsageMB > thresholdMB;
|
|
461
|
+
if (proxyConfig.aggressiveMemoryManagement && estimatedUsageMB > thresholdMB * 0.8) {
|
|
462
|
+
this.highUsageCount++;
|
|
463
|
+
if (this.highUsageCount > 2) {
|
|
464
|
+
this.isUnderPressure = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
this.highUsageCount = 0;
|
|
469
|
+
}
|
|
470
|
+
if (this.isUnderPressure !== wasUnderPressure && monitoringConfig.enabled) {
|
|
471
|
+
if (this.isUnderPressure) {
|
|
472
|
+
console.warn(`⚠️ High memory pressure detected: ${estimatedUsageMB.toFixed(1)}MB > ${thresholdMB.toFixed(1)}MB threshold`);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.log(`✅ Memory pressure relieved: ${estimatedUsageMB.toFixed(1)}MB < ${thresholdMB.toFixed(1)}MB threshold`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
console.error('Error checking memory pressure:', e);
|
|
481
|
+
}
|
|
482
|
+
return this.isUnderPressure;
|
|
483
|
+
},
|
|
484
|
+
estimateMemoryUsage() {
|
|
485
|
+
const proxyCacheSize = proxyCacheLRU.size() * 2;
|
|
486
|
+
const pathStatsSize = (pathUsageStats.accessedPaths.size +
|
|
487
|
+
pathUsageStats.modifiedPaths.size +
|
|
488
|
+
pathUsageStats.subscribedPaths.size) * 0.2;
|
|
489
|
+
const baseMB = 400;
|
|
490
|
+
return baseMB + proxyCacheSize + pathStatsSize;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Set the initial store state for reset functionality
|
|
495
|
+
*/
|
|
496
|
+
function setInitialStoreState(state) {
|
|
497
|
+
initialStoreState$1 = JSON.parse(JSON.stringify(state));
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Check if a path is high priority
|
|
501
|
+
*/
|
|
502
|
+
function isHighPriorityPath(path) {
|
|
503
|
+
if (!proxyConfig.prioritizeUIObjects)
|
|
504
|
+
return false;
|
|
505
|
+
const pathStr = path.join('.');
|
|
506
|
+
return proxyConfig.priorityPaths.some(p => pathStr === p || pathStr.startsWith(`${p}.`));
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Create an advanced proxy with all the features from the original code
|
|
510
|
+
*/
|
|
511
|
+
function createAdvancedProxy(target, path = [], depth = 0) {
|
|
512
|
+
if (target === null || typeof target !== 'object') {
|
|
513
|
+
return target;
|
|
514
|
+
}
|
|
515
|
+
if (!proxyConfig.enabled) {
|
|
516
|
+
return target;
|
|
517
|
+
}
|
|
518
|
+
const pathKey = path.join('.');
|
|
519
|
+
// Check memory pressure
|
|
520
|
+
const isUnderPressure = memoryPressure.check();
|
|
521
|
+
if (isUnderPressure && proxyConfig.disableProxyingUnderPressure && depth > 0 && !isHighPriorityPath(path)) {
|
|
522
|
+
return target;
|
|
523
|
+
}
|
|
524
|
+
// Ultra-selective proxying
|
|
525
|
+
if (proxyConfig.ultraSelectiveProxying && depth > 0) {
|
|
526
|
+
if (!selectorPaths.has(pathKey) &&
|
|
527
|
+
!isHighPriorityPath(path) &&
|
|
528
|
+
!proxyConfig.preProxyPaths.some(p => pathKey === p || pathKey.startsWith(`${p}.`))) {
|
|
529
|
+
return target;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Check for cached proxy
|
|
533
|
+
if (proxyCache.has(target)) {
|
|
534
|
+
proxyCacheStats.cacheHits++;
|
|
535
|
+
proxyCacheLRU.touch(target);
|
|
536
|
+
return proxyCache.get(target);
|
|
537
|
+
}
|
|
538
|
+
// Check depth limit
|
|
539
|
+
if (proxyConfig.selectiveProxying && depth > proxyConfig.maxDepth) {
|
|
540
|
+
return target;
|
|
541
|
+
}
|
|
542
|
+
// Create new proxy
|
|
543
|
+
proxyCacheStats.cacheMisses++;
|
|
544
|
+
proxyCacheStats.totalProxiesCreated++;
|
|
545
|
+
proxyCacheStats.activeCachedProxies++;
|
|
546
|
+
// If target is not extensible, clone it
|
|
547
|
+
if (!Object.isExtensible(target)) {
|
|
548
|
+
target = Array.isArray(target) ? [...target] : { ...target };
|
|
549
|
+
}
|
|
550
|
+
// Add custom methods for objects
|
|
551
|
+
if (typeof target === 'object' && !Array.isArray(target)) {
|
|
552
|
+
addObjectMethods(target, path);
|
|
553
|
+
}
|
|
554
|
+
// Add custom methods for arrays
|
|
555
|
+
if (Array.isArray(target)) {
|
|
556
|
+
addArrayMethods(target, path);
|
|
557
|
+
}
|
|
558
|
+
const proxy = new Proxy(target, {
|
|
559
|
+
get(obj, prop, receiver) {
|
|
560
|
+
if (typeof prop === 'symbol') {
|
|
561
|
+
return Reflect.get(obj, prop, receiver);
|
|
562
|
+
}
|
|
563
|
+
const currentPropPath = [...path, prop.toString()];
|
|
564
|
+
const propPathKey = currentPropPath.join('.');
|
|
565
|
+
// Track path access during dependency tracking
|
|
566
|
+
if (proxyConfig.trackPathUsage && propPathKey.split('.').length <= proxyConfig.maxPathLength) {
|
|
567
|
+
pathUsageStats.accessedPaths.add(propPathKey);
|
|
568
|
+
// Also add to selector paths for ultra-selective proxying
|
|
569
|
+
selectorPaths.add(propPathKey);
|
|
570
|
+
}
|
|
571
|
+
// Track path access for dependency tracking (inline to avoid circular dependency)
|
|
572
|
+
if (typeof require !== 'undefined') {
|
|
573
|
+
try {
|
|
574
|
+
const { trackPathAccess } = require('./tracking');
|
|
575
|
+
trackPathAccess(currentPropPath);
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
// Skip tracking if module not available
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const value = obj[prop];
|
|
582
|
+
// For objects, create proxies for nested values
|
|
583
|
+
if (value && typeof value === 'object' && path.length < proxyConfig.maxDepth) {
|
|
584
|
+
const shouldProxy = !proxyConfig.lazyProxyDeepObjects ||
|
|
585
|
+
pathUsageStats.accessedPaths.has(propPathKey) ||
|
|
586
|
+
pathUsageStats.modifiedPaths.has(propPathKey) ||
|
|
587
|
+
pathUsageStats.subscribedPaths.has(propPathKey) ||
|
|
588
|
+
proxyConfig.preProxyPaths.some(p => propPathKey === p || propPathKey.startsWith(`${p}.`));
|
|
589
|
+
if (shouldProxy) {
|
|
590
|
+
return createAdvancedProxy(value, currentPropPath, depth + 1);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return value;
|
|
594
|
+
},
|
|
595
|
+
set(obj, prop, value, receiver) {
|
|
596
|
+
if (typeof prop === 'symbol') {
|
|
597
|
+
return Reflect.set(obj, prop, value, receiver);
|
|
598
|
+
}
|
|
599
|
+
const propPath = [...path, prop.toString()];
|
|
600
|
+
const propPathKey = propPath.join('.');
|
|
601
|
+
// Track modification
|
|
602
|
+
if (proxyConfig.trackPathUsage) {
|
|
603
|
+
pathUsageStats.modifiedPaths.add(propPathKey);
|
|
604
|
+
}
|
|
605
|
+
// Handle object assignment with proxying
|
|
606
|
+
if (value && typeof value === 'object' && !proxyCache.has(value)) {
|
|
607
|
+
const newPath = [...path, prop.toString()];
|
|
608
|
+
const proxiedValue = createAdvancedProxy(value, newPath, 0);
|
|
609
|
+
const result = Reflect.set(obj, prop, proxiedValue, receiver);
|
|
610
|
+
// Notify the specific property path and all parent/child paths
|
|
611
|
+
notifyListeners(propPath);
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
const result = Reflect.set(obj, prop, value, receiver);
|
|
615
|
+
// Notify the specific property path (this will also notify parent/child paths)
|
|
616
|
+
notifyListeners(propPath);
|
|
617
|
+
return result;
|
|
618
|
+
},
|
|
619
|
+
deleteProperty(obj, prop) {
|
|
620
|
+
if (typeof prop === 'string') {
|
|
621
|
+
const propPath = [...path, prop];
|
|
622
|
+
const propPathKey = propPath.join('.');
|
|
623
|
+
if (proxyConfig.trackPathUsage) {
|
|
624
|
+
pathUsageStats.modifiedPaths.add(propPathKey);
|
|
625
|
+
}
|
|
626
|
+
const result = Reflect.deleteProperty(obj, prop);
|
|
627
|
+
if (result) {
|
|
628
|
+
// Notify the deleted property path (this will also notify parent/child paths)
|
|
629
|
+
notifyListeners(propPath);
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
return Reflect.deleteProperty(obj, prop);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
// Cache the proxy
|
|
637
|
+
proxyCache.set(target, proxy);
|
|
638
|
+
proxyCacheLRU.add(target, proxy);
|
|
639
|
+
// Track the path for this proxy
|
|
640
|
+
proxyPathMap.set(proxy, [...path]);
|
|
641
|
+
return proxy;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Add custom methods to object targets
|
|
645
|
+
*/
|
|
646
|
+
function addObjectMethods(target, path) {
|
|
647
|
+
const methodsToDefine = {};
|
|
648
|
+
if (!('$merge' in target)) {
|
|
649
|
+
methodsToDefine.$merge = {
|
|
650
|
+
value: function (newProps) {
|
|
651
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
652
|
+
Object.keys(newProps).forEach(key => {
|
|
653
|
+
if (proxyConfig.trackPathUsage) {
|
|
654
|
+
const propPath = [...currentPath, key].join('.');
|
|
655
|
+
pathUsageStats.modifiedPaths.add(propPath);
|
|
656
|
+
}
|
|
657
|
+
const newValue = newProps[key];
|
|
658
|
+
if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
|
|
659
|
+
const newPath = [...currentPath, key];
|
|
660
|
+
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
661
|
+
Reflect.set(this, key, createAdvancedProxy(newValue, newPath, 0), this);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
665
|
+
Reflect.set(this, key, newValue, this);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
return this;
|
|
669
|
+
},
|
|
670
|
+
enumerable: false
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (!('$set' in target)) {
|
|
674
|
+
methodsToDefine.$set = {
|
|
675
|
+
value: function (newProps) {
|
|
676
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
677
|
+
// Clear existing properties
|
|
678
|
+
Object.keys(this).forEach(key => {
|
|
679
|
+
if (typeof this[key] !== 'function') {
|
|
680
|
+
Reflect.deleteProperty(this, key);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
// Set new properties
|
|
684
|
+
Object.keys(newProps || {}).forEach(key => {
|
|
685
|
+
if (proxyConfig.trackPathUsage) {
|
|
686
|
+
const propPath = [...currentPath, key].join('.');
|
|
687
|
+
pathUsageStats.modifiedPaths.add(propPath);
|
|
688
|
+
}
|
|
689
|
+
const newValue = newProps[key];
|
|
690
|
+
if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
|
|
691
|
+
const newPath = [...currentPath, key];
|
|
692
|
+
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
693
|
+
Reflect.set(this, key, createAdvancedProxy(newValue, newPath, 0), this);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
697
|
+
Reflect.set(this, key, newValue, this);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
return this;
|
|
701
|
+
},
|
|
702
|
+
enumerable: false
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
if (!('$delete' in target)) {
|
|
706
|
+
methodsToDefine.$delete = {
|
|
707
|
+
value: function (keys) {
|
|
708
|
+
proxyPathMap.get(this) || path;
|
|
709
|
+
const keysToDelete = Array.isArray(keys) ? keys : [keys];
|
|
710
|
+
keysToDelete.forEach(key => {
|
|
711
|
+
// Use Reflect.deleteProperty to trigger the deleteProperty handler
|
|
712
|
+
Reflect.deleteProperty(this, key);
|
|
713
|
+
});
|
|
714
|
+
return this;
|
|
715
|
+
},
|
|
716
|
+
enumerable: false
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (!('$update' in target)) {
|
|
720
|
+
methodsToDefine.$update = {
|
|
721
|
+
value: function (key, updater) {
|
|
722
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
723
|
+
if (proxyConfig.trackPathUsage && currentPath.length > 0) {
|
|
724
|
+
const propPath = [...currentPath, key].join('.');
|
|
725
|
+
pathUsageStats.modifiedPaths.add(propPath);
|
|
726
|
+
}
|
|
727
|
+
const currentValue = this[key];
|
|
728
|
+
let newValue = updater(currentValue);
|
|
729
|
+
if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
|
|
730
|
+
const newPath = [...currentPath, key];
|
|
731
|
+
newValue = createAdvancedProxy(newValue, newPath, 0);
|
|
732
|
+
}
|
|
733
|
+
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
734
|
+
Reflect.set(this, key, newValue, this);
|
|
735
|
+
return this;
|
|
736
|
+
},
|
|
737
|
+
enumerable: false
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (!('$reset' in target)) {
|
|
741
|
+
methodsToDefine.$reset = {
|
|
742
|
+
value: function () {
|
|
743
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
744
|
+
let initialValue = initialStoreState$1;
|
|
745
|
+
for (const segment of currentPath) {
|
|
746
|
+
if (initialValue && typeof initialValue === 'object' && segment in initialValue) {
|
|
747
|
+
initialValue = initialValue[segment];
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
initialValue = undefined;
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (initialValue !== undefined) {
|
|
755
|
+
// Clear existing properties
|
|
756
|
+
Object.keys(this).forEach((key) => {
|
|
757
|
+
if (typeof this[key] !== 'function') {
|
|
758
|
+
Reflect.deleteProperty(this, key);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
// Set new properties
|
|
762
|
+
if (initialValue && typeof initialValue === 'object') {
|
|
763
|
+
Object.entries(initialValue).forEach(([key, value]) => {
|
|
764
|
+
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
765
|
+
Reflect.set(this, key, JSON.parse(JSON.stringify(value)), this);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return this;
|
|
770
|
+
},
|
|
771
|
+
enumerable: false
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (!('raw' in target)) {
|
|
775
|
+
methodsToDefine.raw = {
|
|
776
|
+
value: function () {
|
|
777
|
+
return JSON.parse(JSON.stringify(this));
|
|
778
|
+
},
|
|
779
|
+
enumerable: false,
|
|
780
|
+
configurable: true
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (Object.keys(methodsToDefine).length > 0) {
|
|
784
|
+
Object.defineProperties(target, methodsToDefine);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Add custom methods to array targets
|
|
789
|
+
*/
|
|
790
|
+
function addArrayMethods(target, path) {
|
|
791
|
+
const originalPush = target.push;
|
|
792
|
+
const originalSplice = target.splice;
|
|
793
|
+
// Override push
|
|
794
|
+
Object.defineProperty(target, 'push', {
|
|
795
|
+
value: function (...items) {
|
|
796
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
797
|
+
const processedItems = items.map(item => {
|
|
798
|
+
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
799
|
+
const itemPath = [...currentPath, '_item'];
|
|
800
|
+
return createAdvancedProxy(item, itemPath, 0);
|
|
801
|
+
}
|
|
802
|
+
return item;
|
|
803
|
+
});
|
|
804
|
+
const result = originalPush.apply(this, processedItems);
|
|
805
|
+
if (currentPath.length > 0) {
|
|
806
|
+
if (proxyConfig.trackPathUsage) {
|
|
807
|
+
pathUsageStats.modifiedPaths.add(currentPath.join('.'));
|
|
808
|
+
}
|
|
809
|
+
notifyListeners(currentPath);
|
|
810
|
+
}
|
|
811
|
+
return result;
|
|
812
|
+
},
|
|
813
|
+
writable: true,
|
|
814
|
+
configurable: true
|
|
815
|
+
});
|
|
816
|
+
// Override splice
|
|
817
|
+
Object.defineProperty(target, 'splice', {
|
|
818
|
+
value: function (start, deleteCount, ...items) {
|
|
819
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
820
|
+
const arrayLength = this.length;
|
|
821
|
+
const processedItems = items.map((item, index) => {
|
|
822
|
+
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
823
|
+
const itemPath = [...currentPath, (start + index).toString()];
|
|
824
|
+
return createAdvancedProxy(item, itemPath, 0);
|
|
825
|
+
}
|
|
826
|
+
return item;
|
|
827
|
+
});
|
|
828
|
+
const actualDeleteCount = deleteCount === undefined ? (arrayLength - start) : deleteCount;
|
|
829
|
+
const result = originalSplice.apply(this, [start, actualDeleteCount, ...processedItems]);
|
|
830
|
+
if (currentPath.length > 0) {
|
|
831
|
+
if (proxyConfig.trackPathUsage) {
|
|
832
|
+
pathUsageStats.modifiedPaths.add(currentPath.join('.'));
|
|
833
|
+
}
|
|
834
|
+
// Notify the array itself
|
|
835
|
+
notifyListeners(currentPath);
|
|
836
|
+
// Also notify about each index that was affected
|
|
837
|
+
for (let i = start; i < arrayLength; i++) {
|
|
838
|
+
const indexPath = [...currentPath, i.toString()];
|
|
839
|
+
notifyListeners(indexPath);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return result;
|
|
843
|
+
},
|
|
844
|
+
writable: true,
|
|
845
|
+
configurable: true
|
|
846
|
+
});
|
|
847
|
+
// Add $set method for arrays
|
|
848
|
+
if (!('$set' in target)) {
|
|
849
|
+
Object.defineProperty(target, '$set', {
|
|
850
|
+
value: function (newArray) {
|
|
851
|
+
if (!Array.isArray(newArray)) {
|
|
852
|
+
console.error('$set on array must be called with an array');
|
|
853
|
+
return this;
|
|
854
|
+
}
|
|
855
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
856
|
+
this.length = 0;
|
|
857
|
+
const processedItems = newArray.map((item, index) => {
|
|
858
|
+
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
859
|
+
const itemPath = [...currentPath, index.toString()];
|
|
860
|
+
return createAdvancedProxy(item, itemPath, 0);
|
|
861
|
+
}
|
|
862
|
+
return item;
|
|
863
|
+
});
|
|
864
|
+
originalPush.apply(this, processedItems);
|
|
865
|
+
if (currentPath.length > 0) {
|
|
866
|
+
if (proxyConfig.trackPathUsage) {
|
|
867
|
+
pathUsageStats.modifiedPaths.add(currentPath.join('.'));
|
|
868
|
+
}
|
|
869
|
+
notifyListeners(currentPath);
|
|
870
|
+
}
|
|
871
|
+
return this;
|
|
872
|
+
},
|
|
873
|
+
enumerable: false,
|
|
874
|
+
configurable: true
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
// Add $reset method for arrays
|
|
878
|
+
if (!('$reset' in target)) {
|
|
879
|
+
Object.defineProperty(target, '$reset', {
|
|
880
|
+
value: function () {
|
|
881
|
+
const currentPath = proxyPathMap.get(this) || path;
|
|
882
|
+
let initialValue = initialStoreState$1;
|
|
883
|
+
for (const segment of currentPath) {
|
|
884
|
+
if (initialValue && typeof initialValue === 'object' && segment in initialValue) {
|
|
885
|
+
initialValue = initialValue[segment];
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
initialValue = [];
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (!Array.isArray(initialValue)) {
|
|
893
|
+
initialValue = [];
|
|
894
|
+
}
|
|
895
|
+
this.length = 0;
|
|
896
|
+
const processedItems = initialValue.map((item, index) => {
|
|
897
|
+
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
898
|
+
const itemPath = [...currentPath, index.toString()];
|
|
899
|
+
return createAdvancedProxy(item, itemPath, 0);
|
|
900
|
+
}
|
|
901
|
+
return item;
|
|
902
|
+
});
|
|
903
|
+
if (processedItems.length > 0) {
|
|
904
|
+
originalPush.apply(this, processedItems);
|
|
905
|
+
}
|
|
906
|
+
if (currentPath.length > 0) {
|
|
907
|
+
notifyListeners(currentPath);
|
|
908
|
+
}
|
|
909
|
+
return this;
|
|
910
|
+
},
|
|
911
|
+
enumerable: false,
|
|
912
|
+
configurable: true
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
// Add raw method
|
|
916
|
+
if (!('raw' in target)) {
|
|
917
|
+
Object.defineProperty(target, 'raw', {
|
|
918
|
+
value: function () {
|
|
919
|
+
return JSON.parse(JSON.stringify(this));
|
|
920
|
+
},
|
|
921
|
+
enumerable: false,
|
|
922
|
+
configurable: true
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Clear proxy cache
|
|
928
|
+
*/
|
|
929
|
+
function clearProxyCache() {
|
|
930
|
+
proxyCache = new WeakMap();
|
|
931
|
+
proxyCacheLRU.clear();
|
|
932
|
+
proxyCacheStats.cacheHits = 0;
|
|
933
|
+
proxyCacheStats.cacheMisses = 0;
|
|
934
|
+
proxyCacheStats.activeCachedProxies = 0;
|
|
935
|
+
if (monitoringConfig.enabled) {
|
|
936
|
+
console.log('🧹 Proxy cache cleared');
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get proxy cache statistics
|
|
941
|
+
*/
|
|
942
|
+
function getProxyCacheStats() {
|
|
943
|
+
return {
|
|
944
|
+
...proxyCacheStats,
|
|
945
|
+
hitRatio: proxyCacheStats.cacheHits / (proxyCacheStats.cacheHits + proxyCacheStats.cacheMisses || 1),
|
|
946
|
+
estimatedCacheSize: proxyCacheLRU.size()
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Optimize memory usage
|
|
951
|
+
*/
|
|
952
|
+
function optimizeMemoryUsage(aggressive = false) {
|
|
953
|
+
pathUsageStats.accessedPaths.clear();
|
|
954
|
+
pathUsageStats.modifiedPaths.clear();
|
|
955
|
+
const evictedCount = aggressive ?
|
|
956
|
+
proxyCacheLRU.evictPercentage(0.8) :
|
|
957
|
+
proxyCacheLRU.evictPercentage(0.4);
|
|
958
|
+
if (aggressive && typeof global.gc === 'function') {
|
|
959
|
+
try {
|
|
960
|
+
global.gc();
|
|
961
|
+
if (monitoringConfig.enabled) {
|
|
962
|
+
console.log('🧹 Forced garbage collection');
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch (e) {
|
|
966
|
+
console.error('Failed to force garbage collection:', e);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (monitoringConfig.enabled) {
|
|
970
|
+
console.log(`🧹 Memory optimization complete. Evicted ${evictedCount} cached proxies`);
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
proxiesEvicted: evictedCount,
|
|
974
|
+
currentMemoryEstimate: memoryPressure.estimateMemoryUsage(),
|
|
975
|
+
selectorPathsRemaining: selectorPaths.size
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Track the current path we're accessing during a selector function
|
|
980
|
+
let currentPath = [];
|
|
981
|
+
/**
|
|
982
|
+
* Track dependencies during selector execution - tracks individual path segments
|
|
983
|
+
*/
|
|
984
|
+
function trackDependencies(selector) {
|
|
985
|
+
currentPath = [];
|
|
986
|
+
// Execute selector to track dependencies
|
|
987
|
+
const value = selector();
|
|
988
|
+
// Clean up and return individual path segments (not full paths)
|
|
989
|
+
const cleanedPaths = [...currentPath].filter(segment => {
|
|
990
|
+
// Filter out segments that would create overly long paths
|
|
991
|
+
return segment && segment.length < 100; // Basic length check for individual segments
|
|
992
|
+
});
|
|
993
|
+
currentPath = [];
|
|
994
|
+
return { value, paths: cleanedPaths };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Hook to subscribe to the global store and re-render when specific data changes.
|
|
999
|
+
*
|
|
1000
|
+
* The hook tracks which store paths your selector function accesses and only
|
|
1001
|
+
* re-renders the component when those specific paths change. This provides
|
|
1002
|
+
* fine-grained reactivity without unnecessary renders.
|
|
1003
|
+
*
|
|
1004
|
+
* For objects, the returned value includes custom methods ($merge, $set, $delete, $update)
|
|
1005
|
+
* that allow you to modify the data directly and trigger reactive updates.
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* // Subscribe to user data
|
|
1009
|
+
* const user = useScope(() => $.user);
|
|
1010
|
+
*
|
|
1011
|
+
* // Update user data directly (triggers re-render only for components using $.user)
|
|
1012
|
+
* user.$merge({ name: 'New Name' });
|
|
1013
|
+
*
|
|
1014
|
+
* // Subscribe to a specific property
|
|
1015
|
+
* const userName = useScope(() => $.user.name);
|
|
1016
|
+
*
|
|
1017
|
+
* // Subscribe to a computed value
|
|
1018
|
+
* const isAdmin = useScope(() => $.user.role === 'admin');
|
|
1019
|
+
*
|
|
1020
|
+
* @param selector - Function that returns the data you want to subscribe to
|
|
1021
|
+
* @returns The selected data, with custom methods attached if it's an object
|
|
1022
|
+
*/
|
|
1023
|
+
function useScope(selector) {
|
|
1024
|
+
// Use ref to store the latest selector to avoid stale closures
|
|
1025
|
+
const selectorRef = react.useRef(selector);
|
|
1026
|
+
selectorRef.current = selector;
|
|
1027
|
+
// Track dependencies and get initial value with advanced tracking
|
|
1028
|
+
const { value: initialValue, paths: trackedPaths } = trackDependencies(selector);
|
|
1029
|
+
// Add tracked paths to selector paths for ultra-selective proxying
|
|
1030
|
+
trackedPaths.forEach(path => {
|
|
1031
|
+
selectorPaths.add(path);
|
|
1032
|
+
pathUsageStats.subscribedPaths.add(path);
|
|
1033
|
+
});
|
|
1034
|
+
// Use a counter to force re-renders instead of storing the value
|
|
1035
|
+
// This way we always return the fresh proxy object from the selector
|
|
1036
|
+
const [, forceUpdate] = react.useState(0);
|
|
1037
|
+
// Create stable update handler that forces re-render
|
|
1038
|
+
const handleChange = react.useCallback(() => {
|
|
1039
|
+
try {
|
|
1040
|
+
forceUpdate(count => count + 1);
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
console.error('Error in useScope update:', error);
|
|
1044
|
+
}
|
|
1045
|
+
}, []);
|
|
1046
|
+
react.useEffect(() => {
|
|
1047
|
+
// Create path keys for subscription using the original approach
|
|
1048
|
+
// If trackedPaths is ['user', 'name'], create subscriptions for ['user', 'user.name']
|
|
1049
|
+
const pathKeys = trackedPaths.length > 0
|
|
1050
|
+
? trackedPaths.map((_, index, array) => array.slice(0, index + 1).join('.'))
|
|
1051
|
+
: [''];
|
|
1052
|
+
// Subscribe to all relevant paths
|
|
1053
|
+
const unsubscribeFunctions = pathKeys.map(pathKey => {
|
|
1054
|
+
// Mark this path as subscribed for usage tracking
|
|
1055
|
+
pathUsageStats.subscribedPaths.add(pathKey);
|
|
1056
|
+
return subscribe(pathKey, handleChange);
|
|
1057
|
+
});
|
|
1058
|
+
// Clean up subscriptions on unmount or dependency change
|
|
1059
|
+
return () => {
|
|
1060
|
+
unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
|
1061
|
+
};
|
|
1062
|
+
}, [trackedPaths.join(','), handleChange]); // Stable dependencies
|
|
1063
|
+
// Always return the fresh result from the selector (preserves proxy and methods)
|
|
1064
|
+
return selectorRef.current();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Hook to create reactive local state that persists across component re-renders.
|
|
1069
|
+
*
|
|
1070
|
+
* This creates a reactive proxy object with custom methods ($merge, $set, $update, etc.)
|
|
1071
|
+
* that can be used to update local component state. Unlike the global store,
|
|
1072
|
+
* this state is isolated to the component instance.
|
|
1073
|
+
*
|
|
1074
|
+
* The state persists across re-renders but is reset when the component unmounts.
|
|
1075
|
+
*
|
|
1076
|
+
* @example
|
|
1077
|
+
* function MyComponent() {
|
|
1078
|
+
* const localState = useLocal({ count: 0, name: 'John' });
|
|
1079
|
+
*
|
|
1080
|
+
* return (
|
|
1081
|
+
* <div>
|
|
1082
|
+
* <p>Count: {localState.count}</p>
|
|
1083
|
+
* <button onClick={() => localState.$merge({ count: localState.count + 1 })}>
|
|
1084
|
+
* Increment
|
|
1085
|
+
* </button>
|
|
1086
|
+
* <button onClick={() => localState.$update('name', name => name.toUpperCase())}>
|
|
1087
|
+
* Uppercase Name
|
|
1088
|
+
* </button>
|
|
1089
|
+
* </div>
|
|
1090
|
+
* );
|
|
1091
|
+
* }
|
|
1092
|
+
*
|
|
1093
|
+
* @param initialState - The initial state object
|
|
1094
|
+
* @returns A reactive proxy object with custom methods for state updates
|
|
1095
|
+
*/
|
|
1096
|
+
function useLocal(initialState) {
|
|
1097
|
+
// Create the reactive state only once using lazy initialization
|
|
1098
|
+
const [localState] = react.useState(() => createAdvancedProxy(initialState));
|
|
1099
|
+
return localState;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Default store structure - can be overridden via configuration
|
|
1103
|
+
const defaultStore = {
|
|
1104
|
+
user: {
|
|
1105
|
+
name: 'John Doe',
|
|
1106
|
+
age: 30,
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
// The actual store that will be used - start with default but allow override
|
|
1110
|
+
let store = defaultStore;
|
|
1111
|
+
// Store a deep clone of the initial store state for use with $reset
|
|
1112
|
+
let initialStoreState = JSON.parse(JSON.stringify(store));
|
|
1113
|
+
/**
|
|
1114
|
+
* Initialize the store with custom initial state
|
|
1115
|
+
* This function updates both the store and its type
|
|
1116
|
+
*/
|
|
1117
|
+
function initializeStore(config = {}) {
|
|
1118
|
+
if (config.initialState) {
|
|
1119
|
+
// Update the actual store with the new state
|
|
1120
|
+
store = { ...config.initialState };
|
|
1121
|
+
initialStoreState = JSON.parse(JSON.stringify(store));
|
|
1122
|
+
if (typeof window !== 'undefined') {
|
|
1123
|
+
console.log('🏪 Store initialized with custom state');
|
|
1124
|
+
}
|
|
1125
|
+
return store;
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
// Use default store
|
|
1129
|
+
store = { ...defaultStore };
|
|
1130
|
+
initialStoreState = JSON.parse(JSON.stringify(store));
|
|
1131
|
+
return store;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Get the current store (for debugging)
|
|
1136
|
+
*/
|
|
1137
|
+
function getStore() {
|
|
1138
|
+
return store;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Reset the entire store to its initial state
|
|
1142
|
+
*/
|
|
1143
|
+
function resetStore() {
|
|
1144
|
+
// Clear current store
|
|
1145
|
+
Object.keys(store).forEach(key => {
|
|
1146
|
+
delete store[key];
|
|
1147
|
+
});
|
|
1148
|
+
// Restore initial state
|
|
1149
|
+
Object.assign(store, JSON.parse(JSON.stringify(initialStoreState)));
|
|
1150
|
+
if (typeof window !== 'undefined') {
|
|
1151
|
+
console.log('🔄 Store reset to initial state');
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Perform automatic leak check
|
|
1157
|
+
*/
|
|
1158
|
+
function performAutoLeakCheck() {
|
|
1159
|
+
if (typeof window === 'undefined')
|
|
1160
|
+
return;
|
|
1161
|
+
if (!monitoringConfig.enabled || !monitoringConfig.autoLeakDetection ||
|
|
1162
|
+
monitoringStats.leakDetection.isLeakDetectionRunning) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
monitoringStats.leakDetection.isLeakDetectionRunning = true;
|
|
1166
|
+
try {
|
|
1167
|
+
console.log(`Running automatic leak detection (check #${monitoringStats.leakDetection.totalChecks + 1})`);
|
|
1168
|
+
const leakReport = monitorAPI.checkForLeaks(false);
|
|
1169
|
+
if (!leakReport)
|
|
1170
|
+
return;
|
|
1171
|
+
// Only show detailed report if threshold is exceeded
|
|
1172
|
+
if (leakReport.orphanedListeners >= monitoringConfig.leakAlertThreshold) {
|
|
1173
|
+
console.warn(`MEMORY LEAK ALERT: ${leakReport.orphanedListeners} orphaned listeners detected!`);
|
|
1174
|
+
console.log('Paths with potential leaks:');
|
|
1175
|
+
leakReport.mismatchedCounts.forEach(msg => console.log(`- ${msg}`));
|
|
1176
|
+
monitoringStats.leakDetection.leaksDetected++;
|
|
1177
|
+
// Auto-optimize if many leaks found
|
|
1178
|
+
if (leakReport.orphanedListeners > monitoringConfig.leakAlertThreshold * 2) {
|
|
1179
|
+
console.log('Auto-optimizing memory due to high leak count...');
|
|
1180
|
+
optimizeMemoryUsage(false);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
monitoringStats.leakDetection.lastCheckTime = Date.now();
|
|
1184
|
+
monitoringStats.leakDetection.totalChecks++;
|
|
1185
|
+
monitoringStats.leakDetection.renderCyclesSinceCheck = 0;
|
|
1186
|
+
}
|
|
1187
|
+
catch (e) {
|
|
1188
|
+
console.error('Error during automatic leak check:', e);
|
|
1189
|
+
}
|
|
1190
|
+
finally {
|
|
1191
|
+
monitoringStats.leakDetection.isLeakDetectionRunning = false;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Monitor API for debugging and memory leak detection
|
|
1196
|
+
*/
|
|
1197
|
+
const monitorAPI = {
|
|
1198
|
+
// Enable or disable monitoring
|
|
1199
|
+
setEnabled: (enabled) => {
|
|
1200
|
+
if (typeof window === 'undefined')
|
|
1201
|
+
return;
|
|
1202
|
+
monitoringConfig.enabled = enabled;
|
|
1203
|
+
},
|
|
1204
|
+
// Set verbose logging
|
|
1205
|
+
setVerboseLogging: (enabled) => {
|
|
1206
|
+
if (typeof window === 'undefined')
|
|
1207
|
+
return;
|
|
1208
|
+
monitoringConfig.verboseLogging = enabled;
|
|
1209
|
+
},
|
|
1210
|
+
// Configure specific monitoring features
|
|
1211
|
+
configure: (config) => {
|
|
1212
|
+
Object.assign(monitoringConfig, config);
|
|
1213
|
+
},
|
|
1214
|
+
// Get current stats
|
|
1215
|
+
getStats: () => ({ ...monitoringStats }),
|
|
1216
|
+
// Get detailed listener info
|
|
1217
|
+
getListenerInfo: () => {
|
|
1218
|
+
const info = {};
|
|
1219
|
+
pathListeners.forEach((listeners, path) => {
|
|
1220
|
+
info[path] = listeners.size;
|
|
1221
|
+
});
|
|
1222
|
+
return info;
|
|
1223
|
+
},
|
|
1224
|
+
// Check for potential memory leaks
|
|
1225
|
+
checkForLeaks: (logReport = true) => {
|
|
1226
|
+
if (typeof window === 'undefined')
|
|
1227
|
+
return null;
|
|
1228
|
+
const leakReport = {
|
|
1229
|
+
orphanedListeners: 0,
|
|
1230
|
+
mismatchedCounts: [],
|
|
1231
|
+
emptyPaths: [],
|
|
1232
|
+
totalListeners: 0,
|
|
1233
|
+
summary: ''
|
|
1234
|
+
};
|
|
1235
|
+
// Check for paths with listeners but no subscribers in our tracking
|
|
1236
|
+
pathListeners.forEach((listeners, path) => {
|
|
1237
|
+
leakReport.totalListeners += listeners.size;
|
|
1238
|
+
const trackedCount = monitoringStats.pathSubscriptionCounts[path] || 0;
|
|
1239
|
+
if (listeners.size > 0 && trackedCount <= 0) {
|
|
1240
|
+
leakReport.orphanedListeners += listeners.size;
|
|
1241
|
+
leakReport.mismatchedCounts.push(`Path ${path}: actual=${listeners.size}, tracked=${trackedCount}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (listeners.size === 0) {
|
|
1244
|
+
leakReport.emptyPaths.push(path);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
leakReport.summary = `Found ${leakReport.orphanedListeners} potential leaked listeners across ${leakReport.mismatchedCounts.length} paths. Total listeners: ${leakReport.totalListeners}`;
|
|
1248
|
+
if (logReport) {
|
|
1249
|
+
console.log('LEAK CHECK REPORT:');
|
|
1250
|
+
console.log(leakReport.summary);
|
|
1251
|
+
if (leakReport.mismatchedCounts.length > 0) {
|
|
1252
|
+
console.log('Paths with potential leaks:');
|
|
1253
|
+
leakReport.mismatchedCounts.forEach(msg => console.log(`- ${msg}`));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return leakReport;
|
|
1257
|
+
},
|
|
1258
|
+
// Start automatic leak detection
|
|
1259
|
+
startAutoLeakDetection: (interval = 30000) => {
|
|
1260
|
+
if (typeof window === 'undefined')
|
|
1261
|
+
return false;
|
|
1262
|
+
monitoringConfig.autoLeakDetection = true;
|
|
1263
|
+
monitoringConfig.leakDetectionInterval = interval;
|
|
1264
|
+
// Clear any existing timer
|
|
1265
|
+
if (monitoringStats.leakDetection.leakDetectionTimer) {
|
|
1266
|
+
clearInterval(monitoringStats.leakDetection.leakDetectionTimer);
|
|
1267
|
+
}
|
|
1268
|
+
// Set up periodic leak detection
|
|
1269
|
+
monitoringStats.leakDetection.leakDetectionTimer = setInterval(() => {
|
|
1270
|
+
if (monitoringStats.leakDetection.renderCyclesSinceCheck >= monitoringConfig.leakScanMinimumRenderCycles) {
|
|
1271
|
+
performAutoLeakCheck();
|
|
1272
|
+
}
|
|
1273
|
+
}, interval);
|
|
1274
|
+
console.log(`Automatic leak detection started (interval: ${interval}ms)`);
|
|
1275
|
+
return true;
|
|
1276
|
+
},
|
|
1277
|
+
// Stop automatic leak detection
|
|
1278
|
+
stopAutoLeakDetection: () => {
|
|
1279
|
+
if (typeof window === 'undefined')
|
|
1280
|
+
return false;
|
|
1281
|
+
monitoringConfig.autoLeakDetection = false;
|
|
1282
|
+
if (monitoringStats.leakDetection.leakDetectionTimer) {
|
|
1283
|
+
clearInterval(monitoringStats.leakDetection.leakDetectionTimer);
|
|
1284
|
+
monitoringStats.leakDetection.leakDetectionTimer = null;
|
|
1285
|
+
}
|
|
1286
|
+
console.log('Automatic leak detection stopped');
|
|
1287
|
+
return true;
|
|
1288
|
+
},
|
|
1289
|
+
// Configure leak detection
|
|
1290
|
+
configureLeakDetection: (config) => {
|
|
1291
|
+
if (typeof window === 'undefined')
|
|
1292
|
+
return;
|
|
1293
|
+
if (config.enabled !== undefined) {
|
|
1294
|
+
monitoringConfig.autoLeakDetection = config.enabled;
|
|
1295
|
+
}
|
|
1296
|
+
if (config.interval !== undefined) {
|
|
1297
|
+
monitoringConfig.leakDetectionInterval = config.interval;
|
|
1298
|
+
}
|
|
1299
|
+
if (config.alertThreshold !== undefined) {
|
|
1300
|
+
monitoringConfig.leakAlertThreshold = config.alertThreshold;
|
|
1301
|
+
}
|
|
1302
|
+
if (config.minimumRenderCycles !== undefined) {
|
|
1303
|
+
monitoringConfig.leakScanMinimumRenderCycles = config.minimumRenderCycles;
|
|
1304
|
+
}
|
|
1305
|
+
// Restart if enabled
|
|
1306
|
+
if (monitoringConfig.autoLeakDetection) {
|
|
1307
|
+
monitorAPI.startAutoLeakDetection(monitoringConfig.leakDetectionInterval);
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
monitorAPI.stopAutoLeakDetection();
|
|
1311
|
+
}
|
|
1312
|
+
console.log('Leak detection configuration updated:', {
|
|
1313
|
+
enabled: monitoringConfig.autoLeakDetection,
|
|
1314
|
+
interval: monitoringConfig.leakDetectionInterval,
|
|
1315
|
+
alertThreshold: monitoringConfig.leakAlertThreshold,
|
|
1316
|
+
minimumRenderCycles: monitoringConfig.leakScanMinimumRenderCycles
|
|
1317
|
+
});
|
|
1318
|
+
},
|
|
1319
|
+
// Get leak detection stats
|
|
1320
|
+
getLeakDetectionStats: () => ({
|
|
1321
|
+
isEnabled: monitoringConfig.autoLeakDetection,
|
|
1322
|
+
interval: monitoringConfig.leakDetectionInterval,
|
|
1323
|
+
checksPerformed: monitoringStats.leakDetection.totalChecks,
|
|
1324
|
+
leaksDetected: monitoringStats.leakDetection.leaksDetected,
|
|
1325
|
+
alertThreshold: monitoringConfig.leakAlertThreshold
|
|
1326
|
+
}),
|
|
1327
|
+
// Reset statistics
|
|
1328
|
+
resetStats: () => {
|
|
1329
|
+
monitoringStats.proxyCount = 0;
|
|
1330
|
+
monitoringStats.totalSubscriptionsCreated = 0;
|
|
1331
|
+
monitoringStats.totalSubscriptionsRemoved = 0;
|
|
1332
|
+
monitoringStats.activeSubscriptions = 0;
|
|
1333
|
+
monitoringStats.pathSubscriptionCounts = {};
|
|
1334
|
+
monitoringStats.timings = {
|
|
1335
|
+
lastNotifyTime: 0,
|
|
1336
|
+
lastPersistTime: 0,
|
|
1337
|
+
averageNotifyTime: 0,
|
|
1338
|
+
averagePersistTime: 0,
|
|
1339
|
+
notifyTimeTotal: 0,
|
|
1340
|
+
persistTimeTotal: 0,
|
|
1341
|
+
notifyCount: 0,
|
|
1342
|
+
persistCount: 0,
|
|
1343
|
+
};
|
|
1344
|
+
console.log('Monitoring statistics reset');
|
|
1345
|
+
},
|
|
1346
|
+
// Force cleanup of empty listener sets
|
|
1347
|
+
cleanupEmptyListeners: () => {
|
|
1348
|
+
let cleanedCount = 0;
|
|
1349
|
+
pathListeners.forEach((listeners, path) => {
|
|
1350
|
+
if (listeners.size === 0) {
|
|
1351
|
+
pathListeners.delete(path);
|
|
1352
|
+
cleanedCount++;
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
console.log(`Cleaned up ${cleanedCount} empty listener sets`);
|
|
1356
|
+
return cleanedCount;
|
|
1357
|
+
},
|
|
1358
|
+
// Get proxy cache stats (delegated to proxy module)
|
|
1359
|
+
getProxyCacheStats,
|
|
1360
|
+
// Get proxy usage stats
|
|
1361
|
+
getProxyUsageStats: () => ({
|
|
1362
|
+
accessedPaths: Array.from(pathUsageStats.accessedPaths),
|
|
1363
|
+
modifiedPaths: Array.from(pathUsageStats.modifiedPaths),
|
|
1364
|
+
subscribedPaths: Array.from(pathUsageStats.subscribedPaths),
|
|
1365
|
+
totalAccessedPaths: pathUsageStats.accessedPaths.size,
|
|
1366
|
+
totalModifiedPaths: pathUsageStats.modifiedPaths.size,
|
|
1367
|
+
totalSubscribedPaths: pathUsageStats.subscribedPaths.size,
|
|
1368
|
+
}),
|
|
1369
|
+
// Reset usage stats
|
|
1370
|
+
resetProxyUsageStats: () => {
|
|
1371
|
+
pathUsageStats.accessedPaths.clear();
|
|
1372
|
+
pathUsageStats.modifiedPaths.clear();
|
|
1373
|
+
// Don't clear subscribedPaths as they're used for optimization
|
|
1374
|
+
console.log('Proxy usage statistics reset (except subscriptions)');
|
|
1375
|
+
},
|
|
1376
|
+
// Generate report of most accessed/modified paths for optimization
|
|
1377
|
+
generatePathUsageReport: () => {
|
|
1378
|
+
const report = {
|
|
1379
|
+
mostAccessedPaths: Array.from(pathUsageStats.accessedPaths).slice(0, 20),
|
|
1380
|
+
mostModifiedPaths: Array.from(pathUsageStats.modifiedPaths).slice(0, 20),
|
|
1381
|
+
subscribedPaths: Array.from(pathUsageStats.subscribedPaths),
|
|
1382
|
+
recommendations: []
|
|
1383
|
+
};
|
|
1384
|
+
// Generate recommendations
|
|
1385
|
+
if (pathUsageStats.accessedPaths.size > 100) {
|
|
1386
|
+
report.recommendations.push(`Consider adding frequently accessed paths to preProxyPaths: ${report.mostAccessedPaths.slice(0, 5).join(', ')}`);
|
|
1387
|
+
}
|
|
1388
|
+
return report;
|
|
1389
|
+
},
|
|
1390
|
+
// Memory-optimized leak detection
|
|
1391
|
+
optimizeMemoryUsage,
|
|
1392
|
+
// Get selector path statistics
|
|
1393
|
+
getSelectorPathStats: () => {
|
|
1394
|
+
return {
|
|
1395
|
+
totalSelectorPaths: selectorPaths.size,
|
|
1396
|
+
paths: Array.from(selectorPaths),
|
|
1397
|
+
mostAccessedSelectorPaths: Array.from(selectorPaths).slice(0, 20),
|
|
1398
|
+
};
|
|
1399
|
+
},
|
|
1400
|
+
// Clear selector paths (force rebuilding)
|
|
1401
|
+
clearSelectorPaths: () => {
|
|
1402
|
+
const count = selectorPaths.size;
|
|
1403
|
+
selectorPaths.clear();
|
|
1404
|
+
console.log(`Cleared ${count} selector paths. They will be rebuilt as components render.`);
|
|
1405
|
+
return count;
|
|
1406
|
+
},
|
|
1407
|
+
// Get comprehensive system status
|
|
1408
|
+
getSystemStatus: () => {
|
|
1409
|
+
const proxyCacheStats = getProxyCacheStats();
|
|
1410
|
+
const leakStats = monitorAPI.getLeakDetectionStats();
|
|
1411
|
+
return {
|
|
1412
|
+
monitoring: {
|
|
1413
|
+
enabled: monitoringConfig.enabled,
|
|
1414
|
+
verboseLogging: monitoringConfig.verboseLogging,
|
|
1415
|
+
autoLeakDetection: monitoringConfig.autoLeakDetection,
|
|
1416
|
+
},
|
|
1417
|
+
listeners: {
|
|
1418
|
+
total: monitoringStats.activeSubscriptions,
|
|
1419
|
+
pathCount: pathListeners.size,
|
|
1420
|
+
},
|
|
1421
|
+
proxies: {
|
|
1422
|
+
cacheSize: proxyCacheStats.estimatedCacheSize,
|
|
1423
|
+
hitRatio: proxyCacheStats.hitRatio,
|
|
1424
|
+
totalCreated: proxyCacheStats.totalProxiesCreated,
|
|
1425
|
+
},
|
|
1426
|
+
paths: {
|
|
1427
|
+
accessed: pathUsageStats.accessedPaths.size,
|
|
1428
|
+
modified: pathUsageStats.modifiedPaths.size,
|
|
1429
|
+
subscribed: pathUsageStats.subscribedPaths.size,
|
|
1430
|
+
selectors: selectorPaths.size,
|
|
1431
|
+
},
|
|
1432
|
+
leakDetection: leakStats,
|
|
1433
|
+
performance: {
|
|
1434
|
+
averageNotifyTime: monitoringStats.timings.averageNotifyTime,
|
|
1435
|
+
averagePersistTime: monitoringStats.timings.averagePersistTime,
|
|
1436
|
+
totalNotifications: monitoringStats.timings.notifyCount,
|
|
1437
|
+
totalPersistOperations: monitoringStats.timings.persistCount,
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
},
|
|
1441
|
+
// Force a comprehensive health check
|
|
1442
|
+
performHealthCheck: () => {
|
|
1443
|
+
console.log('Performing comprehensive health check...');
|
|
1444
|
+
const leakReport = monitorAPI.checkForLeaks(true);
|
|
1445
|
+
const systemStatus = monitorAPI.getSystemStatus();
|
|
1446
|
+
const cleanedListeners = monitorAPI.cleanupEmptyListeners();
|
|
1447
|
+
const recommendations = [];
|
|
1448
|
+
if (leakReport && leakReport.orphanedListeners > 5) {
|
|
1449
|
+
recommendations.push('🚨 High number of orphaned listeners detected - consider running optimizeMemoryUsage()');
|
|
1450
|
+
}
|
|
1451
|
+
if (systemStatus.proxies.hitRatio < 0.8) {
|
|
1452
|
+
recommendations.push('📈 Low proxy cache hit ratio - consider reviewing proxy configuration');
|
|
1453
|
+
}
|
|
1454
|
+
if (systemStatus.paths.accessed > 1000) {
|
|
1455
|
+
recommendations.push('📊 High number of accessed paths - consider enabling ultraSelectiveProxying');
|
|
1456
|
+
}
|
|
1457
|
+
const healthScore = Math.max(0, 100 -
|
|
1458
|
+
(leakReport ? leakReport.orphanedListeners * 5 : 0) -
|
|
1459
|
+
(systemStatus.proxies.hitRatio < 0.8 ? 20 : 0) -
|
|
1460
|
+
(systemStatus.paths.accessed > 1000 ? 15 : 0));
|
|
1461
|
+
console.log(`Health Check Complete - Score: ${healthScore}/100`);
|
|
1462
|
+
if (recommendations.length > 0) {
|
|
1463
|
+
console.log('Recommendations:');
|
|
1464
|
+
recommendations.forEach(rec => console.log(` ${rec}`));
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
healthScore,
|
|
1468
|
+
recommendations,
|
|
1469
|
+
systemStatus,
|
|
1470
|
+
leakReport,
|
|
1471
|
+
cleanedListeners
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
// Initialize auto leak detection if configured
|
|
1476
|
+
if (typeof window !== 'undefined' && monitoringConfig.enabled && monitoringConfig.autoLeakDetection) {
|
|
1477
|
+
setTimeout(() => monitorAPI.startAutoLeakDetection(), 5000); // Start after 5 seconds
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
let storage = null;
|
|
1481
|
+
/**
|
|
1482
|
+
* Initialize storage for persistence
|
|
1483
|
+
*/
|
|
1484
|
+
function initializeStorage() {
|
|
1485
|
+
try {
|
|
1486
|
+
if (typeof window !== 'undefined') {
|
|
1487
|
+
storage = localforage.createInstance({
|
|
1488
|
+
name: 'SCOPE_STATE',
|
|
1489
|
+
description: 'Scope state management storage'
|
|
1490
|
+
});
|
|
1491
|
+
// Add memory driver as fallback
|
|
1492
|
+
localforage.defineDriver(memoryDriver__namespace);
|
|
1493
|
+
localforage.setDriver([
|
|
1494
|
+
localforage.INDEXEDDB,
|
|
1495
|
+
localforage.LOCALSTORAGE,
|
|
1496
|
+
localforage.WEBSQL,
|
|
1497
|
+
memoryDriver__namespace._driver
|
|
1498
|
+
]);
|
|
1499
|
+
console.log('💾 Storage initialized successfully');
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
console.error('❌ Error creating storage instance:', error);
|
|
1504
|
+
storage = null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Get the current storage instance
|
|
1509
|
+
*/
|
|
1510
|
+
function getStorage() {
|
|
1511
|
+
return storage;
|
|
1512
|
+
}
|
|
1513
|
+
// Initialize storage by default
|
|
1514
|
+
if (typeof window !== 'undefined') {
|
|
1515
|
+
initializeStorage();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Storage constants
|
|
1519
|
+
const PERSISTED_STATE_KEY = 'persisted_state';
|
|
1520
|
+
const PERSISTENCE_CONFIG_KEY = 'persistence_config';
|
|
1521
|
+
// Batch persistence state
|
|
1522
|
+
const persistenceBatch = {
|
|
1523
|
+
paths: new Set(),
|
|
1524
|
+
timeoutId: null,
|
|
1525
|
+
isPersisting: false,
|
|
1526
|
+
};
|
|
1527
|
+
// Performance timing helpers
|
|
1528
|
+
function logTimestamp(action) {
|
|
1529
|
+
if (!monitoringConfig.enabled || !monitoringConfig.logTimings)
|
|
1530
|
+
return 0;
|
|
1531
|
+
const now = performance.now();
|
|
1532
|
+
console.log(`⏱️ [${now.toFixed(2)}ms] ${action}`);
|
|
1533
|
+
return now;
|
|
1534
|
+
}
|
|
1535
|
+
function logTimingEnd(action, startTime) {
|
|
1536
|
+
if (!monitoringConfig.enabled || !monitoringConfig.logTimings)
|
|
1537
|
+
return 0;
|
|
1538
|
+
const now = performance.now();
|
|
1539
|
+
const duration = now - startTime;
|
|
1540
|
+
console.log(`⏱️ [${now.toFixed(2)}ms] ${action} completed in ${duration.toFixed(2)}ms`);
|
|
1541
|
+
return duration;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Process all paths in the batch
|
|
1545
|
+
*/
|
|
1546
|
+
function processPersistenceBatch() {
|
|
1547
|
+
if (typeof window === 'undefined')
|
|
1548
|
+
return;
|
|
1549
|
+
if (persistenceBatch.isPersisting || persistenceBatch.paths.size === 0) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
persistenceBatch.isPersisting = true;
|
|
1553
|
+
let startTime = 0;
|
|
1554
|
+
if (monitoringConfig.enabled && monitoringConfig.logPersistence) {
|
|
1555
|
+
startTime = logTimestamp(`💾 Batch persisting ${persistenceBatch.paths.size} paths`);
|
|
1556
|
+
}
|
|
1557
|
+
try {
|
|
1558
|
+
// Convert set to array of path arrays
|
|
1559
|
+
const pathArrays = Array.from(persistenceBatch.paths).map(path => path.split('.'));
|
|
1560
|
+
// Persist entire state if we're persisting many paths
|
|
1561
|
+
if (pathArrays.length > 10) {
|
|
1562
|
+
persistState();
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
// Otherwise persist individual paths
|
|
1566
|
+
pathArrays.forEach(pathArray => {
|
|
1567
|
+
persistState(pathArray);
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
// Clear the batch
|
|
1571
|
+
persistenceBatch.paths.clear();
|
|
1572
|
+
}
|
|
1573
|
+
catch (e) {
|
|
1574
|
+
console.error('Error during batch persistence:', e);
|
|
1575
|
+
}
|
|
1576
|
+
finally {
|
|
1577
|
+
persistenceBatch.isPersisting = false;
|
|
1578
|
+
// If new paths were added during processing, schedule another batch
|
|
1579
|
+
if (persistenceBatch.paths.size > 0) {
|
|
1580
|
+
schedulePersistenceBatch();
|
|
1581
|
+
}
|
|
1582
|
+
if (monitoringConfig.enabled && monitoringConfig.logPersistence && startTime > 0) {
|
|
1583
|
+
logTimingEnd('Batch persistence', startTime);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Schedule batch persistence with debounce
|
|
1589
|
+
*/
|
|
1590
|
+
function schedulePersistenceBatch() {
|
|
1591
|
+
if (persistenceBatch.timeoutId) {
|
|
1592
|
+
clearTimeout(persistenceBatch.timeoutId);
|
|
1593
|
+
}
|
|
1594
|
+
persistenceBatch.timeoutId = setTimeout(() => {
|
|
1595
|
+
persistenceBatch.timeoutId = null;
|
|
1596
|
+
processPersistenceBatch();
|
|
1597
|
+
}, persistenceConfig.batchDelay);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Add a path to the persistence batch
|
|
1601
|
+
*/
|
|
1602
|
+
function addToPersistenceBatch(path) {
|
|
1603
|
+
if (!persistenceConfig.enabled)
|
|
1604
|
+
return;
|
|
1605
|
+
const pathKey = path.join('.');
|
|
1606
|
+
if (shouldPersistPath(pathKey)) {
|
|
1607
|
+
persistenceBatch.paths.add(pathKey);
|
|
1608
|
+
schedulePersistenceBatch();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Function to determine if a path should be persisted
|
|
1613
|
+
*/
|
|
1614
|
+
function shouldPersistPath(path) {
|
|
1615
|
+
if (!persistenceConfig.enabled)
|
|
1616
|
+
return false;
|
|
1617
|
+
// Check if path is blacklisted
|
|
1618
|
+
if (persistenceConfig.blacklist.some(blacklistedPath => path === blacklistedPath || path.startsWith(`${blacklistedPath}.`))) {
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
// If paths array is empty, persist everything not blacklisted
|
|
1622
|
+
if (!persistenceConfig.paths || persistenceConfig.paths.length === 0)
|
|
1623
|
+
return true;
|
|
1624
|
+
// Check if path is in the persistence paths list
|
|
1625
|
+
return persistenceConfig.paths.some(persistedPath => path === persistedPath || path.startsWith(`${persistedPath}.`));
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Save state to storage
|
|
1629
|
+
*/
|
|
1630
|
+
function persistState(path = []) {
|
|
1631
|
+
if (typeof window === 'undefined')
|
|
1632
|
+
return;
|
|
1633
|
+
if (!persistenceConfig.enabled)
|
|
1634
|
+
return;
|
|
1635
|
+
const currentStorage = getStorage();
|
|
1636
|
+
if (!currentStorage)
|
|
1637
|
+
return;
|
|
1638
|
+
const pathKey = path.join('.');
|
|
1639
|
+
if (path.length === 0) {
|
|
1640
|
+
// We need access to the store to persist entire state
|
|
1641
|
+
// For now, we'll skip this and only support path-specific persistence
|
|
1642
|
+
console.warn('Cannot persist entire state without store reference');
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (shouldPersistPath(pathKey)) {
|
|
1646
|
+
// For individual path persistence, we'd need access to the store
|
|
1647
|
+
// This will be implemented when integrating with the main store
|
|
1648
|
+
try {
|
|
1649
|
+
currentStorage.setItem(`${PERSISTED_STATE_KEY}_${pathKey}`, JSON.stringify({}));
|
|
1650
|
+
}
|
|
1651
|
+
catch (e) {
|
|
1652
|
+
console.error(`Error persisting path ${pathKey}:`, e);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Hydrate state from storage
|
|
1658
|
+
*/
|
|
1659
|
+
async function hydrateState(store) {
|
|
1660
|
+
if (typeof window === 'undefined')
|
|
1661
|
+
return false;
|
|
1662
|
+
if (!store) {
|
|
1663
|
+
console.warn('Cannot hydrate state without store reference');
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
const currentStorage = getStorage();
|
|
1667
|
+
if (!currentStorage)
|
|
1668
|
+
return false;
|
|
1669
|
+
try {
|
|
1670
|
+
// First try to load entire state
|
|
1671
|
+
const savedState = await currentStorage.getItem(PERSISTED_STATE_KEY);
|
|
1672
|
+
if (savedState) {
|
|
1673
|
+
const parsedState = JSON.parse(savedState);
|
|
1674
|
+
// Direct replacement instead of merging
|
|
1675
|
+
Object.keys(parsedState).forEach(key => {
|
|
1676
|
+
if (key in store) {
|
|
1677
|
+
store[key] = parsedState[key];
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
if (monitoringConfig.enabled) {
|
|
1681
|
+
console.log('🔄 State hydrated from storage');
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
// Then try to load individual persisted paths
|
|
1685
|
+
const keys = await currentStorage.keys();
|
|
1686
|
+
if (!keys)
|
|
1687
|
+
return true;
|
|
1688
|
+
for (const key of keys) {
|
|
1689
|
+
if (key.startsWith(`${PERSISTED_STATE_KEY}_`)) {
|
|
1690
|
+
const path = key.replace(`${PERSISTED_STATE_KEY}_`, '').split('.');
|
|
1691
|
+
const value = await currentStorage.getItem(key);
|
|
1692
|
+
if (value) {
|
|
1693
|
+
try {
|
|
1694
|
+
const parsedValue = JSON.parse(value);
|
|
1695
|
+
// Set the value in the store
|
|
1696
|
+
let current = store;
|
|
1697
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1698
|
+
if (!(path[i] in current)) {
|
|
1699
|
+
current[path[i]] = {};
|
|
1700
|
+
}
|
|
1701
|
+
current = current[path[i]];
|
|
1702
|
+
}
|
|
1703
|
+
const lastKey = path[path.length - 1];
|
|
1704
|
+
current[lastKey] = parsedValue;
|
|
1705
|
+
}
|
|
1706
|
+
catch (e) {
|
|
1707
|
+
console.error(`Error hydrating path ${path.join('.')}:`, e);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (monitoringConfig.enabled) {
|
|
1713
|
+
console.log(`🔄 Hydrated ${keys.length} individual paths`);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
catch (e) {
|
|
1717
|
+
console.error('Error hydrating state:', e);
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
return true;
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Load persistence configuration
|
|
1724
|
+
*/
|
|
1725
|
+
async function loadPersistenceConfig() {
|
|
1726
|
+
if (typeof window === 'undefined')
|
|
1727
|
+
return;
|
|
1728
|
+
const currentStorage = getStorage();
|
|
1729
|
+
if (!currentStorage)
|
|
1730
|
+
return;
|
|
1731
|
+
try {
|
|
1732
|
+
const config = await currentStorage.getItem(PERSISTENCE_CONFIG_KEY);
|
|
1733
|
+
if (config) {
|
|
1734
|
+
const parsedConfig = JSON.parse(config);
|
|
1735
|
+
Object.assign(persistenceConfig, parsedConfig);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
catch (e) {
|
|
1739
|
+
console.error('Error loading persistence configuration:', e);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Save persistence configuration
|
|
1744
|
+
*/
|
|
1745
|
+
function savePersistenceConfig() {
|
|
1746
|
+
if (typeof window === 'undefined')
|
|
1747
|
+
return;
|
|
1748
|
+
const currentStorage = getStorage();
|
|
1749
|
+
if (!currentStorage)
|
|
1750
|
+
return;
|
|
1751
|
+
try {
|
|
1752
|
+
currentStorage.setItem(PERSISTENCE_CONFIG_KEY, JSON.stringify(persistenceConfig));
|
|
1753
|
+
}
|
|
1754
|
+
catch (e) {
|
|
1755
|
+
console.error('Error saving persistence configuration:', e);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Persistence API - matches the original implementation
|
|
1760
|
+
*/
|
|
1761
|
+
const persistenceAPI = {
|
|
1762
|
+
// Enable or disable persistence
|
|
1763
|
+
setEnabled: (enabled) => {
|
|
1764
|
+
persistenceConfig.enabled = enabled;
|
|
1765
|
+
savePersistenceConfig();
|
|
1766
|
+
},
|
|
1767
|
+
// Add paths to be persisted
|
|
1768
|
+
persistPaths: (paths) => {
|
|
1769
|
+
if (!persistenceConfig.paths) {
|
|
1770
|
+
persistenceConfig.paths = [];
|
|
1771
|
+
}
|
|
1772
|
+
persistenceConfig.paths = Array.from(new Set([...persistenceConfig.paths, ...paths]));
|
|
1773
|
+
savePersistenceConfig();
|
|
1774
|
+
// Force persist current state for these paths
|
|
1775
|
+
paths.forEach(path => addToPersistenceBatch(path.split('.')));
|
|
1776
|
+
},
|
|
1777
|
+
// Remove paths from persistence
|
|
1778
|
+
unpersistPaths: (paths) => {
|
|
1779
|
+
if (persistenceConfig.paths) {
|
|
1780
|
+
persistenceConfig.paths = persistenceConfig.paths.filter(p => !paths.includes(p));
|
|
1781
|
+
savePersistenceConfig();
|
|
1782
|
+
}
|
|
1783
|
+
// Remove from storage
|
|
1784
|
+
const currentStorage = getStorage();
|
|
1785
|
+
if (currentStorage) {
|
|
1786
|
+
paths.forEach(path => {
|
|
1787
|
+
currentStorage.removeItem(`${PERSISTED_STATE_KEY}_${path}`);
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
// Add paths to blacklist
|
|
1792
|
+
blacklistPaths: (paths) => {
|
|
1793
|
+
persistenceConfig.blacklist = Array.from(new Set([...persistenceConfig.blacklist, ...paths]));
|
|
1794
|
+
savePersistenceConfig();
|
|
1795
|
+
// Remove from storage
|
|
1796
|
+
const currentStorage = getStorage();
|
|
1797
|
+
if (currentStorage) {
|
|
1798
|
+
paths.forEach(path => {
|
|
1799
|
+
currentStorage.removeItem(`${PERSISTED_STATE_KEY}_${path}`);
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
// Remove paths from blacklist
|
|
1804
|
+
unblacklistPaths: (paths) => {
|
|
1805
|
+
persistenceConfig.blacklist = persistenceConfig.blacklist.filter(p => !paths.includes(p));
|
|
1806
|
+
savePersistenceConfig();
|
|
1807
|
+
},
|
|
1808
|
+
// Get current persistence configuration
|
|
1809
|
+
getConfig: () => ({ ...persistenceConfig }),
|
|
1810
|
+
// Set the batch delay (in ms)
|
|
1811
|
+
setBatchDelay: (delay) => {
|
|
1812
|
+
persistenceConfig.batchDelay = delay;
|
|
1813
|
+
savePersistenceConfig();
|
|
1814
|
+
},
|
|
1815
|
+
// Reset persistence
|
|
1816
|
+
reset: async () => {
|
|
1817
|
+
const currentStorage = getStorage();
|
|
1818
|
+
if (!currentStorage)
|
|
1819
|
+
return;
|
|
1820
|
+
// Clear all persisted state
|
|
1821
|
+
const keys = await currentStorage.keys();
|
|
1822
|
+
if (keys) {
|
|
1823
|
+
for (const key of keys) {
|
|
1824
|
+
if (key.startsWith(PERSISTED_STATE_KEY)) {
|
|
1825
|
+
await currentStorage.removeItem(key);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Reset configuration to defaults
|
|
1830
|
+
Object.assign(persistenceConfig, {
|
|
1831
|
+
enabled: true,
|
|
1832
|
+
paths: [],
|
|
1833
|
+
blacklist: [],
|
|
1834
|
+
batchDelay: 300,
|
|
1835
|
+
});
|
|
1836
|
+
savePersistenceConfig();
|
|
1837
|
+
},
|
|
1838
|
+
// Force persist current state
|
|
1839
|
+
persist: () => persistState(),
|
|
1840
|
+
// Force persist current batch immediately
|
|
1841
|
+
flushBatch: () => {
|
|
1842
|
+
if (persistenceBatch.timeoutId) {
|
|
1843
|
+
clearTimeout(persistenceBatch.timeoutId);
|
|
1844
|
+
persistenceBatch.timeoutId = null;
|
|
1845
|
+
}
|
|
1846
|
+
processPersistenceBatch();
|
|
1847
|
+
},
|
|
1848
|
+
// Force rehydrate state
|
|
1849
|
+
rehydrate: (store) => hydrateState(store),
|
|
1850
|
+
// Get batch status
|
|
1851
|
+
getBatchStatus: () => ({
|
|
1852
|
+
pendingPaths: Array.from(persistenceBatch.paths),
|
|
1853
|
+
isPersisting: persistenceBatch.isPersisting,
|
|
1854
|
+
batchSize: persistenceBatch.paths.size,
|
|
1855
|
+
}),
|
|
1856
|
+
};
|
|
1857
|
+
/**
|
|
1858
|
+
* Initialize persistence system
|
|
1859
|
+
*/
|
|
1860
|
+
function initializePersistence() {
|
|
1861
|
+
if (typeof window !== 'undefined') {
|
|
1862
|
+
loadPersistenceConfig();
|
|
1863
|
+
console.log('💾 Advanced persistence system initialized');
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
// Initialize automatically
|
|
1867
|
+
initializePersistence();
|
|
1868
|
+
|
|
1869
|
+
// Main exports for the library
|
|
1870
|
+
// Global state
|
|
1871
|
+
let globalStore = {
|
|
1872
|
+
user: {
|
|
1873
|
+
name: 'John Doe',
|
|
1874
|
+
age: 30,
|
|
1875
|
+
},
|
|
1876
|
+
};
|
|
1877
|
+
let globalStoreProxy = null;
|
|
1878
|
+
/**
|
|
1879
|
+
* Configure Scope with custom settings and return a properly typed store
|
|
1880
|
+
* This is the main way to set up Scope with TypeScript support
|
|
1881
|
+
*/
|
|
1882
|
+
function configure(config) {
|
|
1883
|
+
// Update configurations
|
|
1884
|
+
if (config.proxy) {
|
|
1885
|
+
Object.assign(proxyConfig, config.proxy);
|
|
1886
|
+
}
|
|
1887
|
+
if (config.monitoring) {
|
|
1888
|
+
Object.assign(monitoringConfig, config.monitoring);
|
|
1889
|
+
}
|
|
1890
|
+
if (config.persistence) {
|
|
1891
|
+
Object.assign(persistenceConfig, config.persistence);
|
|
1892
|
+
}
|
|
1893
|
+
// Update the global store
|
|
1894
|
+
if (config.initialState) {
|
|
1895
|
+
globalStore = { ...config.initialState };
|
|
1896
|
+
// Store initial state for reset functionality
|
|
1897
|
+
setInitialStoreState(globalStore);
|
|
1898
|
+
if (typeof window !== 'undefined' && monitoringConfig.enabled) {
|
|
1899
|
+
console.log('🏪 Store configured with custom state');
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
// Create and cache the advanced proxy
|
|
1903
|
+
globalStoreProxy = createAdvancedProxy(globalStore);
|
|
1904
|
+
// Hydrate persisted state if persistence is enabled
|
|
1905
|
+
if (typeof window !== 'undefined' && persistenceConfig.enabled) {
|
|
1906
|
+
hydrateState(globalStore).then((success) => {
|
|
1907
|
+
if (success && monitoringConfig.enabled) {
|
|
1908
|
+
console.log('🔄 State hydrated from persistence');
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
if (typeof window !== 'undefined' && monitoringConfig.enabled) {
|
|
1913
|
+
console.log('🔧 Scope configured with advanced features');
|
|
1914
|
+
}
|
|
1915
|
+
return globalStoreProxy;
|
|
1916
|
+
}
|
|
1917
|
+
// Enhanced tracking that integrates with the advanced proxy system
|
|
1918
|
+
function trackDependenciesAdvanced(selector) {
|
|
1919
|
+
try {
|
|
1920
|
+
// Execute selector and track paths
|
|
1921
|
+
const result = trackDependencies(selector);
|
|
1922
|
+
// Add tracked paths to selector paths for ultra-selective proxying
|
|
1923
|
+
result.paths.forEach(path => {
|
|
1924
|
+
selectorPaths.add(path);
|
|
1925
|
+
pathUsageStats.subscribedPaths.add(path);
|
|
1926
|
+
});
|
|
1927
|
+
return result;
|
|
1928
|
+
}
|
|
1929
|
+
finally {
|
|
1930
|
+
// Cleanup any temporary tracking state
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
// Default store for when configure() isn't called
|
|
1934
|
+
if (!globalStoreProxy) {
|
|
1935
|
+
globalStoreProxy = createAdvancedProxy(globalStore);
|
|
1936
|
+
setInitialStoreState(globalStore);
|
|
1937
|
+
}
|
|
1938
|
+
// Export the main $ proxy - will be properly typed if configure() is called first
|
|
1939
|
+
const $ = globalStoreProxy;
|
|
1940
|
+
// Utility functions from the original code
|
|
1941
|
+
function createReactive(obj) {
|
|
1942
|
+
return createAdvancedProxy(obj);
|
|
1943
|
+
}
|
|
1944
|
+
// Legacy alias - use useLocal() hook instead for proper React integration
|
|
1945
|
+
const $local = createReactive;
|
|
1946
|
+
// Development helper to check if an object is reactive
|
|
1947
|
+
function isReactive(obj) {
|
|
1948
|
+
return obj && typeof obj === 'object' && typeof obj.$merge === 'function';
|
|
1949
|
+
}
|
|
1950
|
+
// Type-safe activation functions from the original
|
|
1951
|
+
function activate(obj) {
|
|
1952
|
+
if (obj === null || typeof obj !== 'object') {
|
|
1953
|
+
return obj;
|
|
1954
|
+
}
|
|
1955
|
+
const path = proxyPathMap.get(obj);
|
|
1956
|
+
if (!path) {
|
|
1957
|
+
console.warn('⚠️ Could not determine path for object. Make sure it\'s from the $ tree.');
|
|
1958
|
+
return obj;
|
|
1959
|
+
}
|
|
1960
|
+
if (monitoringConfig.enabled && monitoringConfig.verboseLogging) {
|
|
1961
|
+
console.log(`🔄 Type-safe activation of path: ${path.join('.')}`);
|
|
1962
|
+
}
|
|
1963
|
+
// Add to selector paths
|
|
1964
|
+
selectorPaths.add(path.join('.'));
|
|
1965
|
+
// Add parent paths
|
|
1966
|
+
for (let i = 1; i < path.length; i++) {
|
|
1967
|
+
const parentPath = path.slice(0, i).join('.');
|
|
1968
|
+
selectorPaths.add(parentPath);
|
|
1969
|
+
}
|
|
1970
|
+
return obj;
|
|
1971
|
+
}
|
|
1972
|
+
function $activate(obj) {
|
|
1973
|
+
return activate(obj);
|
|
1974
|
+
}
|
|
1975
|
+
function getProxy(path) {
|
|
1976
|
+
// Handle case where an object from $ is passed directly
|
|
1977
|
+
if (path !== null && typeof path === 'object') {
|
|
1978
|
+
return activate(path);
|
|
1979
|
+
}
|
|
1980
|
+
const pathArray = typeof path === 'string' ? path.split('.') : path;
|
|
1981
|
+
const pathKey = pathArray.join('.');
|
|
1982
|
+
// Add to selector paths
|
|
1983
|
+
selectorPaths.add(pathKey);
|
|
1984
|
+
if (monitoringConfig.enabled && monitoringConfig.verboseLogging) {
|
|
1985
|
+
console.log(`🔄 Explicitly activating proxy for path: ${pathKey}`);
|
|
1986
|
+
}
|
|
1987
|
+
// Navigate to the target
|
|
1988
|
+
let target = globalStore;
|
|
1989
|
+
for (let i = 0; i < pathArray.length; i++) {
|
|
1990
|
+
if (target === undefined || target === null)
|
|
1991
|
+
break;
|
|
1992
|
+
target = target[pathArray[i]];
|
|
1993
|
+
}
|
|
1994
|
+
if (target === undefined || target === null) {
|
|
1995
|
+
console.warn(`⚠️ Path not found: ${pathKey}`);
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
// Force create an immediate proxy for this specific path
|
|
1999
|
+
const immediateProxy = createAdvancedProxy(target, pathArray, 0);
|
|
2000
|
+
// Add parent paths
|
|
2001
|
+
for (let i = 1; i < pathArray.length; i++) {
|
|
2002
|
+
const parentPath = pathArray.slice(0, i).join('.');
|
|
2003
|
+
selectorPaths.add(parentPath);
|
|
2004
|
+
}
|
|
2005
|
+
return immediateProxy;
|
|
2006
|
+
}
|
|
2007
|
+
function $get(path) {
|
|
2008
|
+
if (path !== null && typeof path === 'object') {
|
|
2009
|
+
return activate(path);
|
|
2010
|
+
}
|
|
2011
|
+
return getProxy(path);
|
|
2012
|
+
}
|
|
2013
|
+
// For debugging - matches original API
|
|
2014
|
+
const debugInfo = {
|
|
2015
|
+
getListenerCount: () => {
|
|
2016
|
+
const { getListenerCount } = require('./core/listeners');
|
|
2017
|
+
return getListenerCount();
|
|
2018
|
+
},
|
|
2019
|
+
getPathCount: () => {
|
|
2020
|
+
const { pathListeners } = require('./core/listeners');
|
|
2021
|
+
return pathListeners.size;
|
|
2022
|
+
},
|
|
2023
|
+
getActivePaths: () => {
|
|
2024
|
+
const { getActivePaths } = require('./core/listeners');
|
|
2025
|
+
return getActivePaths();
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
// Expose raw store for debugging
|
|
2029
|
+
const rawStore = globalStore;
|
|
2030
|
+
// Initialize library with enhanced features
|
|
2031
|
+
if (typeof window !== 'undefined') {
|
|
2032
|
+
console.log('🎯 Scope State initialized with advanced features - ready for reactive state management');
|
|
2033
|
+
console.log('💡 Tip: Call configure() with your initialState for full TypeScript support');
|
|
2034
|
+
console.log('🔧 Advanced features: WeakMap caching, leak detection, batch persistence, memory management');
|
|
2035
|
+
// Initialize auto-hydration if persistence is enabled
|
|
2036
|
+
if (persistenceConfig.enabled) {
|
|
2037
|
+
setTimeout(() => {
|
|
2038
|
+
hydrateState(globalStore);
|
|
2039
|
+
}, 100);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
exports.$ = $;
|
|
2044
|
+
exports.$activate = $activate;
|
|
2045
|
+
exports.$get = $get;
|
|
2046
|
+
exports.$local = $local;
|
|
2047
|
+
exports.activate = activate;
|
|
2048
|
+
exports.clearProxyCache = clearProxyCache;
|
|
2049
|
+
exports.configure = configure;
|
|
2050
|
+
exports.createAdvancedProxy = createAdvancedProxy;
|
|
2051
|
+
exports.createReactive = createReactive;
|
|
2052
|
+
exports.debugInfo = debugInfo;
|
|
2053
|
+
exports.getConfig = getConfig;
|
|
2054
|
+
exports.getProxy = getProxy;
|
|
2055
|
+
exports.getProxyCacheStats = getProxyCacheStats;
|
|
2056
|
+
exports.getStore = getStore;
|
|
2057
|
+
exports.initializeStore = initializeStore;
|
|
2058
|
+
exports.isReactive = isReactive;
|
|
2059
|
+
exports.monitorAPI = monitorAPI;
|
|
2060
|
+
exports.optimizeMemoryUsage = optimizeMemoryUsage;
|
|
2061
|
+
exports.pathUsageStats = pathUsageStats;
|
|
2062
|
+
exports.persistenceAPI = persistenceAPI;
|
|
2063
|
+
exports.presets = presets;
|
|
2064
|
+
exports.proxyPathMap = proxyPathMap;
|
|
2065
|
+
exports.rawStore = rawStore;
|
|
2066
|
+
exports.resetConfig = resetConfig;
|
|
2067
|
+
exports.resetStore = resetStore;
|
|
2068
|
+
exports.selectorPaths = selectorPaths;
|
|
2069
|
+
exports.setInitialStoreState = setInitialStoreState;
|
|
2070
|
+
exports.trackDependenciesAdvanced = trackDependenciesAdvanced;
|
|
2071
|
+
exports.useLocal = useLocal;
|
|
2072
|
+
exports.useScope = useScope;
|
|
2073
|
+
//# sourceMappingURL=index.js.map
|