elementdrawing 1.0.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/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMemo Hook Implementation
|
|
3
|
+
* ElementDrawing Framework - Value memoization with dependency comparison,
|
|
4
|
+
* cache invalidation, LRU caching, deep comparison, TTL, and profiling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// ─── Internal State ───────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
let currentlyRenderingComponent = null;
|
|
12
|
+
let hookIndex = 0;
|
|
13
|
+
const memoCache = new WeakMap();
|
|
14
|
+
|
|
15
|
+
// ─── Memo Profiling ───────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const memoProfiles = new WeakMap();
|
|
18
|
+
let memoProfilingEnabled = false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enable or disable memo profiling.
|
|
22
|
+
* @param {boolean} enabled
|
|
23
|
+
*/
|
|
24
|
+
function setMemoProfiling(enabled) {
|
|
25
|
+
memoProfilingEnabled = enabled;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get profiling data for a component's memo hooks.
|
|
30
|
+
* @param {Object} component
|
|
31
|
+
* @returns {Object|null}
|
|
32
|
+
*/
|
|
33
|
+
function getMemoProfile(component) {
|
|
34
|
+
return memoProfiles.get(component) || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── LRU Cache ────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Simple LRU cache for memo values.
|
|
41
|
+
* @param {number} capacity - Maximum cache entries
|
|
42
|
+
*/
|
|
43
|
+
class LRUMemoCache {
|
|
44
|
+
constructor(capacity) {
|
|
45
|
+
this.capacity = capacity || 10;
|
|
46
|
+
this.cache = new Map();
|
|
47
|
+
this.hits = 0;
|
|
48
|
+
this.misses = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get(key) {
|
|
52
|
+
if (!this.cache.has(key)) {
|
|
53
|
+
this.misses++;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// Move to end (most recently used)
|
|
57
|
+
const value = this.cache.get(key);
|
|
58
|
+
this.cache.delete(key);
|
|
59
|
+
this.cache.set(key, value);
|
|
60
|
+
this.hits++;
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
set(key, value) {
|
|
65
|
+
if (this.cache.has(key)) {
|
|
66
|
+
this.cache.delete(key);
|
|
67
|
+
} else if (this.cache.size >= this.capacity) {
|
|
68
|
+
// Delete least recently used (first entry)
|
|
69
|
+
const firstKey = this.cache.keys().next().value;
|
|
70
|
+
this.cache.delete(firstKey);
|
|
71
|
+
}
|
|
72
|
+
this.cache.set(key, value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
has(key) {
|
|
76
|
+
return this.cache.has(key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear() {
|
|
80
|
+
this.cache.clear();
|
|
81
|
+
this.hits = 0;
|
|
82
|
+
this.misses = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getStats() {
|
|
86
|
+
return {
|
|
87
|
+
size: this.cache.size,
|
|
88
|
+
capacity: this.capacity,
|
|
89
|
+
hits: this.hits,
|
|
90
|
+
misses: this.misses,
|
|
91
|
+
hitRate: this.hits + this.misses > 0 ? this.hits / (this.hits + this.misses) : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Dependency Comparison ────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Compare two dependency arrays for shallow equality.
|
|
100
|
+
* @param {Array|null} prevDeps
|
|
101
|
+
* @param {Array|null} nextDeps
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function areDepsEqual(prevDeps, nextDeps) {
|
|
105
|
+
if (prevDeps === null || nextDeps === null) return false;
|
|
106
|
+
if (prevDeps.length !== nextDeps.length) return false;
|
|
107
|
+
for (let i = 0; i < prevDeps.length; i++) {
|
|
108
|
+
if (Object.is(prevDeps[i], nextDeps[i])) continue;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deep dependency comparison.
|
|
116
|
+
*/
|
|
117
|
+
function areDepsDeepEqual(prevDeps, nextDeps) {
|
|
118
|
+
if (prevDeps === null || nextDeps === null) return false;
|
|
119
|
+
if (prevDeps.length !== nextDeps.length) return false;
|
|
120
|
+
for (let i = 0; i < prevDeps.length; i++) {
|
|
121
|
+
if (deepEqualVal(prevDeps[i], nextDeps[i])) continue;
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function deepEqualVal(a, b) {
|
|
128
|
+
if (Object.is(a, b)) return true;
|
|
129
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
|
|
130
|
+
const keysA = Object.keys(a);
|
|
131
|
+
const keysB = Object.keys(b);
|
|
132
|
+
if (keysA.length !== keysB.length) return false;
|
|
133
|
+
for (const key of keysA) {
|
|
134
|
+
if (!deepEqualVal(a[key], b[key])) return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validate dependency array.
|
|
141
|
+
* @param {*} deps
|
|
142
|
+
*/
|
|
143
|
+
function validateDeps(deps) {
|
|
144
|
+
if (deps !== undefined && !Array.isArray(deps)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'[useMemo] Dependencies must be an array or undefined. ' +
|
|
147
|
+
`Received: ${typeof deps}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Hook Context Management ──────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
function setCurrentComponent(component) {
|
|
155
|
+
currentlyRenderingComponent = component;
|
|
156
|
+
hookIndex = 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resetHookIndex() {
|
|
160
|
+
hookIndex = 0;
|
|
161
|
+
currentlyRenderingComponent = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── useMemo Hook ──────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* useMemo - Memoize a computed value based on dependencies.
|
|
168
|
+
* Only recomputes when dependencies change.
|
|
169
|
+
*
|
|
170
|
+
* @param {Function} factory - Function that returns the memoized value
|
|
171
|
+
* @param {Array} deps - Dependency array; value recomputes when deps change
|
|
172
|
+
* @returns {*} Memoized value
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const expensiveValue = useMemo(() => computeExpensive(a, b), [a, b]);
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* const sortedList = useMemo(() => {
|
|
179
|
+
* return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
180
|
+
* }, [items]);
|
|
181
|
+
*/
|
|
182
|
+
function useMemo(factory, deps) {
|
|
183
|
+
const component = currentlyRenderingComponent;
|
|
184
|
+
if (!component) {
|
|
185
|
+
throw new Error('[useMemo] Must be called within a component render phase');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof factory !== 'function') {
|
|
189
|
+
throw new Error(
|
|
190
|
+
'[useMemo] First argument must be a function. ' +
|
|
191
|
+
`Received: ${typeof factory}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
validateDeps(deps);
|
|
196
|
+
|
|
197
|
+
// Initialize hooks array
|
|
198
|
+
if (!component._hooks) {
|
|
199
|
+
component._hooks = [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const currentHookIndex = hookIndex;
|
|
203
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
204
|
+
let hook;
|
|
205
|
+
|
|
206
|
+
if (component._hooks[currentHookIndex]) {
|
|
207
|
+
// Re-render: check if dependencies changed
|
|
208
|
+
hook = component._hooks[currentHookIndex];
|
|
209
|
+
|
|
210
|
+
const prevDeps = hook.deps;
|
|
211
|
+
const depsChanged = !areDepsEqual(prevDeps, resolvedDeps);
|
|
212
|
+
|
|
213
|
+
if (depsChanged) {
|
|
214
|
+
// Dependencies changed - recompute the value
|
|
215
|
+
hook.deps = resolvedDeps;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const prevValue = hook.value;
|
|
219
|
+
hook.value = factory();
|
|
220
|
+
hook._computeCount++;
|
|
221
|
+
|
|
222
|
+
// Track value change for debugging
|
|
223
|
+
if (!Object.is(prevValue, hook.value)) {
|
|
224
|
+
hook._valueChangedCount++;
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[useMemo] Factory function error:', error);
|
|
228
|
+
if (component._handleError) {
|
|
229
|
+
component._handleError(error);
|
|
230
|
+
}
|
|
231
|
+
// Keep previous value on error
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// If deps unchanged, keep the existing memoized value
|
|
235
|
+
} else {
|
|
236
|
+
// First render: compute initial value
|
|
237
|
+
let initialValue;
|
|
238
|
+
try {
|
|
239
|
+
initialValue = factory();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('[useMemo] Factory function error on mount:', error);
|
|
242
|
+
if (component._handleError) {
|
|
243
|
+
component._handleError(error);
|
|
244
|
+
}
|
|
245
|
+
initialValue = undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
hook = {
|
|
249
|
+
type: 'memo',
|
|
250
|
+
value: initialValue,
|
|
251
|
+
deps: resolvedDeps,
|
|
252
|
+
_computeCount: 1,
|
|
253
|
+
_valueChangedCount: 1,
|
|
254
|
+
_createdAt: Date.now(),
|
|
255
|
+
_lastComputedAt: Date.now(),
|
|
256
|
+
_onInvalidate: null,
|
|
257
|
+
_lruCache: null,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
component._hooks[currentHookIndex] = hook;
|
|
261
|
+
|
|
262
|
+
// Register in the global memo cache for invalidation support
|
|
263
|
+
if (!memoCache.has(component)) {
|
|
264
|
+
memoCache.set(component, new Set());
|
|
265
|
+
}
|
|
266
|
+
memoCache.get(component).add(currentHookIndex);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update profiling data
|
|
270
|
+
if (memoProfilingEnabled) {
|
|
271
|
+
if (!memoProfiles.has(component)) {
|
|
272
|
+
memoProfiles.set(component, {});
|
|
273
|
+
}
|
|
274
|
+
const profile = memoProfiles.get(component);
|
|
275
|
+
profile[currentHookIndex] = {
|
|
276
|
+
computeCount: hook._computeCount,
|
|
277
|
+
valueChangedCount: hook._valueChangedCount,
|
|
278
|
+
lastComputedAt: hook._lastComputedAt,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
hookIndex++;
|
|
283
|
+
return hook.value;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Cache Invalidation ───────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Invalidate a specific memoized value, forcing recomputation on next render.
|
|
290
|
+
*
|
|
291
|
+
* @param {Object} component - The component owning the memo
|
|
292
|
+
* @param {number} hookIdx - The hook index to invalidate
|
|
293
|
+
*/
|
|
294
|
+
function invalidateMemo(component, hookIdx) {
|
|
295
|
+
if (!component._hooks) return;
|
|
296
|
+
const hook = component._hooks[hookIdx];
|
|
297
|
+
if (!hook || hook.type !== 'memo') return;
|
|
298
|
+
|
|
299
|
+
// Clear deps to force recomputation
|
|
300
|
+
hook.deps = null;
|
|
301
|
+
|
|
302
|
+
// Call invalidation callback if registered
|
|
303
|
+
if (typeof hook._onInvalidate === 'function') {
|
|
304
|
+
try {
|
|
305
|
+
hook._onInvalidate(hook.value);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('[useMemo] Invalidation callback error:', error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Invalidate all memoized values for a component.
|
|
314
|
+
* @param {Object} component
|
|
315
|
+
*/
|
|
316
|
+
function invalidateAllMemos(component) {
|
|
317
|
+
if (!component._hooks) return;
|
|
318
|
+
|
|
319
|
+
component._hooks.forEach((hook, idx) => {
|
|
320
|
+
if (hook && hook.type === 'memo') {
|
|
321
|
+
invalidateMemo(component, idx);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Register a callback to be called when a memoized value is invalidated.
|
|
328
|
+
* Must be called during the same render as the useMemo.
|
|
329
|
+
*
|
|
330
|
+
* @param {Function} callback - Called with (oldValue) when invalidated
|
|
331
|
+
*/
|
|
332
|
+
function useMemoOnInvalidate(callback) {
|
|
333
|
+
const component = currentlyRenderingComponent;
|
|
334
|
+
if (!component) return;
|
|
335
|
+
|
|
336
|
+
// Attach to the previous memo hook
|
|
337
|
+
const prevHookIdx = hookIndex - 1;
|
|
338
|
+
const hook = component._hooks?.[prevHookIdx];
|
|
339
|
+
if (hook && hook.type === 'memo') {
|
|
340
|
+
hook._onInvalidate = callback;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Extended useMemo Features ────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Memoize with a custom equality function instead of Object.is.
|
|
348
|
+
*
|
|
349
|
+
* @param {Function} factory - Factory function
|
|
350
|
+
* @param {Array} deps - Dependency array
|
|
351
|
+
* @param {Function} areEqual - Custom equality: (prev, next) => boolean
|
|
352
|
+
* @returns {*} Memoized value
|
|
353
|
+
*/
|
|
354
|
+
function useMemoWith(factory, deps, areEqual) {
|
|
355
|
+
const component = currentlyRenderingComponent;
|
|
356
|
+
if (!component) {
|
|
357
|
+
throw new Error('[useMemoWith] Must be called within a component render phase');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!component._hooks) {
|
|
361
|
+
component._hooks = [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const currentHookIndex = hookIndex;
|
|
365
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
366
|
+
let hook;
|
|
367
|
+
|
|
368
|
+
if (component._hooks[currentHookIndex]) {
|
|
369
|
+
hook = component._hooks[currentHookIndex];
|
|
370
|
+
|
|
371
|
+
const prevDeps = hook.deps;
|
|
372
|
+
let depsChanged = !areDepsEqual(prevDeps, resolvedDeps);
|
|
373
|
+
|
|
374
|
+
// Even if deps changed by reference, use custom equality to compare values
|
|
375
|
+
if (depsChanged && typeof areEqual === 'function' && hook.value !== undefined) {
|
|
376
|
+
try {
|
|
377
|
+
const newValue = factory();
|
|
378
|
+
if (areEqual(hook.value, newValue)) {
|
|
379
|
+
// Values are equal despite dep reference changes
|
|
380
|
+
hook.deps = resolvedDeps;
|
|
381
|
+
hook.value = newValue;
|
|
382
|
+
} else {
|
|
383
|
+
hook.deps = resolvedDeps;
|
|
384
|
+
hook.value = newValue;
|
|
385
|
+
hook._computeCount++;
|
|
386
|
+
hook._valueChangedCount++;
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error('[useMemoWith] Factory error:', error);
|
|
390
|
+
}
|
|
391
|
+
} else if (depsChanged) {
|
|
392
|
+
try {
|
|
393
|
+
hook.deps = resolvedDeps;
|
|
394
|
+
hook.value = factory();
|
|
395
|
+
hook._computeCount++;
|
|
396
|
+
hook._valueChangedCount++;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('[useMemoWith] Factory error:', error);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
let initialValue;
|
|
403
|
+
try {
|
|
404
|
+
initialValue = factory();
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error('[useMemoWith] Factory error on mount:', error);
|
|
407
|
+
initialValue = undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
hook = {
|
|
411
|
+
type: 'memo',
|
|
412
|
+
value: initialValue,
|
|
413
|
+
deps: resolvedDeps,
|
|
414
|
+
_computeCount: 1,
|
|
415
|
+
_valueChangedCount: 1,
|
|
416
|
+
_createdAt: Date.now(),
|
|
417
|
+
_lastComputedAt: Date.now(),
|
|
418
|
+
_onInvalidate: null,
|
|
419
|
+
_lruCache: null,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
component._hooks[currentHookIndex] = hook;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
hookIndex++;
|
|
426
|
+
return hook.value;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Memoize with a time-to-live (TTL). The cached value expires after
|
|
431
|
+
* the specified duration and will be recomputed.
|
|
432
|
+
*
|
|
433
|
+
* @param {Function} factory - Factory function
|
|
434
|
+
* @param {Array} deps - Dependency array
|
|
435
|
+
* @param {number} ttlMs - Time-to-live in milliseconds
|
|
436
|
+
* @returns {*} Memoized value
|
|
437
|
+
*/
|
|
438
|
+
function useMemoTTL(factory, deps, ttlMs) {
|
|
439
|
+
const component = currentlyRenderingComponent;
|
|
440
|
+
if (!component) {
|
|
441
|
+
throw new Error('[useMemoTTL] Must be called within a component render phase');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!component._hooks) {
|
|
445
|
+
component._hooks = [];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const currentHookIndex = hookIndex;
|
|
449
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
450
|
+
let hook;
|
|
451
|
+
|
|
452
|
+
if (component._hooks[currentHookIndex]) {
|
|
453
|
+
hook = component._hooks[currentHookIndex];
|
|
454
|
+
|
|
455
|
+
const now = Date.now();
|
|
456
|
+
const expired = (now - hook._lastComputedAt) > ttlMs;
|
|
457
|
+
const prevDeps = hook.deps;
|
|
458
|
+
const depsChanged = !areDepsEqual(prevDeps, resolvedDeps);
|
|
459
|
+
|
|
460
|
+
if (expired || depsChanged) {
|
|
461
|
+
try {
|
|
462
|
+
hook.deps = resolvedDeps;
|
|
463
|
+
hook.value = factory();
|
|
464
|
+
hook._computeCount++;
|
|
465
|
+
hook._valueChangedCount++;
|
|
466
|
+
hook._lastComputedAt = now;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.error('[useMemoTTL] Factory error:', error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
let initialValue;
|
|
473
|
+
try {
|
|
474
|
+
initialValue = factory();
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error('[useMemoTTL] Factory error on mount:', error);
|
|
477
|
+
initialValue = undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
hook = {
|
|
481
|
+
type: 'memo',
|
|
482
|
+
value: initialValue,
|
|
483
|
+
deps: resolvedDeps,
|
|
484
|
+
_computeCount: 1,
|
|
485
|
+
_valueChangedCount: 1,
|
|
486
|
+
_createdAt: Date.now(),
|
|
487
|
+
_lastComputedAt: Date.now(),
|
|
488
|
+
_onInvalidate: null,
|
|
489
|
+
_lruCache: null,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
component._hooks[currentHookIndex] = hook;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
hookIndex++;
|
|
496
|
+
return hook.value;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Memoize with deep comparison of dependencies.
|
|
501
|
+
*
|
|
502
|
+
* @param {Function} factory - Factory function
|
|
503
|
+
* @param {Array} deps - Dependency array (compared deeply)
|
|
504
|
+
* @returns {*} Memoized value
|
|
505
|
+
*/
|
|
506
|
+
function useDeepCompareMemo(factory, deps) {
|
|
507
|
+
const component = currentlyRenderingComponent;
|
|
508
|
+
if (!component) {
|
|
509
|
+
throw new Error('[useDeepCompareMemo] Must be called within a component render phase');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!component._hooks) {
|
|
513
|
+
component._hooks = [];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const currentHookIndex = hookIndex;
|
|
517
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
518
|
+
let hook;
|
|
519
|
+
|
|
520
|
+
if (component._hooks[currentHookIndex]) {
|
|
521
|
+
hook = component._hooks[currentHookIndex];
|
|
522
|
+
|
|
523
|
+
const prevDeps = hook.deps;
|
|
524
|
+
const depsChanged = !areDepsDeepEqual(prevDeps, resolvedDeps);
|
|
525
|
+
|
|
526
|
+
if (depsChanged) {
|
|
527
|
+
hook.deps = resolvedDeps;
|
|
528
|
+
try {
|
|
529
|
+
hook.value = factory();
|
|
530
|
+
hook._computeCount++;
|
|
531
|
+
hook._valueChangedCount++;
|
|
532
|
+
hook._lastComputedAt = Date.now();
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error('[useDeepCompareMemo] Factory error:', error);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
let initialValue;
|
|
539
|
+
try {
|
|
540
|
+
initialValue = factory();
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error('[useDeepCompareMemo] Factory error on mount:', error);
|
|
543
|
+
initialValue = undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
hook = {
|
|
547
|
+
type: 'memo',
|
|
548
|
+
value: initialValue,
|
|
549
|
+
deps: resolvedDeps,
|
|
550
|
+
_computeCount: 1,
|
|
551
|
+
_valueChangedCount: 1,
|
|
552
|
+
_createdAt: Date.now(),
|
|
553
|
+
_lastComputedAt: Date.now(),
|
|
554
|
+
_onInvalidate: null,
|
|
555
|
+
_lruCache: null,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
component._hooks[currentHookIndex] = hook;
|
|
559
|
+
if (!memoCache.has(component)) {
|
|
560
|
+
memoCache.set(component, new Set());
|
|
561
|
+
}
|
|
562
|
+
memoCache.get(component).add(currentHookIndex);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
hookIndex++;
|
|
566
|
+
return hook.value;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Memoize with LRU cache - keeps multiple cached values for different dep sets.
|
|
571
|
+
*
|
|
572
|
+
* @param {Function} factory - Factory function
|
|
573
|
+
* @param {Array} deps - Dependency array
|
|
574
|
+
* @param {number} capacity - LRU cache capacity (default 10)
|
|
575
|
+
* @returns {*} Memoized value
|
|
576
|
+
*/
|
|
577
|
+
function useMemoLRU(factory, deps, capacity) {
|
|
578
|
+
const component = currentlyRenderingComponent;
|
|
579
|
+
if (!component) {
|
|
580
|
+
throw new Error('[useMemoLRU] Must be called within a component render phase');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!component._hooks) {
|
|
584
|
+
component._hooks = [];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const currentHookIndex = hookIndex;
|
|
588
|
+
const resolvedDeps = deps !== undefined ? deps : null;
|
|
589
|
+
const depKey = resolvedDeps ? JSON.stringify(resolvedDeps) : '__no_deps__';
|
|
590
|
+
let hook;
|
|
591
|
+
|
|
592
|
+
if (component._hooks[currentHookIndex]) {
|
|
593
|
+
hook = component._hooks[currentHookIndex];
|
|
594
|
+
|
|
595
|
+
// Initialize LRU cache if not present
|
|
596
|
+
if (!hook._lruCache) {
|
|
597
|
+
hook._lruCache = new LRUMemoCache(capacity || 10);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check LRU cache
|
|
601
|
+
const cached = hook._lruCache.get(depKey);
|
|
602
|
+
if (cached !== undefined) {
|
|
603
|
+
hook.value = cached;
|
|
604
|
+
} else {
|
|
605
|
+
try {
|
|
606
|
+
const newValue = factory();
|
|
607
|
+
hook._lruCache.set(depKey, newValue);
|
|
608
|
+
hook.value = newValue;
|
|
609
|
+
hook._computeCount++;
|
|
610
|
+
hook._valueChangedCount++;
|
|
611
|
+
hook._lastComputedAt = Date.now();
|
|
612
|
+
} catch (error) {
|
|
613
|
+
console.error('[useMemoLRU] Factory error:', error);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
let initialValue;
|
|
618
|
+
try {
|
|
619
|
+
initialValue = factory();
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error('[useMemoLRU] Factory error on mount:', error);
|
|
622
|
+
initialValue = undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const lru = new LRUMemoCache(capacity || 10);
|
|
626
|
+
lru.set(depKey, initialValue);
|
|
627
|
+
|
|
628
|
+
hook = {
|
|
629
|
+
type: 'memo',
|
|
630
|
+
value: initialValue,
|
|
631
|
+
deps: resolvedDeps,
|
|
632
|
+
_computeCount: 1,
|
|
633
|
+
_valueChangedCount: 1,
|
|
634
|
+
_createdAt: Date.now(),
|
|
635
|
+
_lastComputedAt: Date.now(),
|
|
636
|
+
_onInvalidate: null,
|
|
637
|
+
_lruCache: lru,
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
component._hooks[currentHookIndex] = hook;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
hookIndex++;
|
|
644
|
+
return hook.value;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Clean up memo cache when a component unmounts.
|
|
651
|
+
* @param {Object} component
|
|
652
|
+
*/
|
|
653
|
+
function cleanupMemoCache(component) {
|
|
654
|
+
const indices = memoCache.get(component);
|
|
655
|
+
if (indices) {
|
|
656
|
+
indices.forEach((idx) => {
|
|
657
|
+
const hook = component._hooks?.[idx];
|
|
658
|
+
if (hook) {
|
|
659
|
+
hook._onInvalidate = null;
|
|
660
|
+
if (hook._lruCache) {
|
|
661
|
+
hook._lruCache.clear();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
memoCache.delete(component);
|
|
666
|
+
}
|
|
667
|
+
memoProfiles.delete(component);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
module.exports = {
|
|
673
|
+
useMemo,
|
|
674
|
+
useMemoWith,
|
|
675
|
+
useMemoTTL,
|
|
676
|
+
useDeepCompareMemo,
|
|
677
|
+
useMemoLRU,
|
|
678
|
+
useMemoOnInvalidate,
|
|
679
|
+
invalidateMemo,
|
|
680
|
+
invalidateAllMemos,
|
|
681
|
+
setCurrentComponent,
|
|
682
|
+
resetHookIndex,
|
|
683
|
+
areDepsEqual,
|
|
684
|
+
areDepsDeepEqual,
|
|
685
|
+
cleanupMemoCache,
|
|
686
|
+
setMemoProfiling,
|
|
687
|
+
getMemoProfile,
|
|
688
|
+
LRUMemoCache,
|
|
689
|
+
};
|