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,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useState Hook Implementation
|
|
3
|
+
* ElementDrawing Framework - State management hook with batched updates,
|
|
4
|
+
* functional updates, lazy initialization, state comparison, subscriptions,
|
|
5
|
+
* priority-based updates, transition support, and debug tracing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ─── Internal State ───────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let currentlyRenderingComponent = null;
|
|
13
|
+
let hookIndex = 0;
|
|
14
|
+
let isBatchingUpdates = false;
|
|
15
|
+
const pendingStateUpdates = new Map();
|
|
16
|
+
const stateSubscribers = new Map();
|
|
17
|
+
const statePersistenceKeys = new WeakMap();
|
|
18
|
+
let scheduleUpdateCallback = null;
|
|
19
|
+
|
|
20
|
+
// ─── Update Priority Levels ──────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const PRIORITY = {
|
|
23
|
+
IMMEDIATE: 0, // Synchronous, user-initiated (click, keypress)
|
|
24
|
+
NORMAL: 1, // Standard state update
|
|
25
|
+
TRANSITION: 2, // Non-urgent, can be interrupted (useTransition)
|
|
26
|
+
IDLE: 3, // Lowest priority, runs when idle
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let currentUpdatePriority = PRIORITY.NORMAL;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current update priority level.
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
function getCurrentPriority() {
|
|
36
|
+
return currentUpdatePriority;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set the current update priority level.
|
|
41
|
+
* @param {number} priority
|
|
42
|
+
*/
|
|
43
|
+
function setCurrentPriority(priority) {
|
|
44
|
+
if (!Object.values(PRIORITY).includes(priority)) {
|
|
45
|
+
console.warn('[useState] Invalid priority level:', priority);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
currentUpdatePriority = priority;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Update Queue ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Represents a pending state update in the queue.
|
|
55
|
+
* @param {*} updateFnOrValue - New value or updater function
|
|
56
|
+
* @param {number} priority - Update priority
|
|
57
|
+
* @param {number} lane - Lane for concurrent rendering
|
|
58
|
+
*/
|
|
59
|
+
function createStateUpdate(updateFnOrValue, priority, lane) {
|
|
60
|
+
return {
|
|
61
|
+
updateFnOrValue,
|
|
62
|
+
priority: priority !== undefined ? priority : currentUpdatePriority,
|
|
63
|
+
lane: lane || 0,
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
id: ++updateIdCounter,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let updateIdCounter = 0;
|
|
70
|
+
|
|
71
|
+
// ─── Batching System ─────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Start batching state updates. All setState calls within the batch
|
|
75
|
+
* will be deferred until endBatchUpdates is called.
|
|
76
|
+
*/
|
|
77
|
+
function startBatchUpdates() {
|
|
78
|
+
isBatchingUpdates = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Flush all pending batched updates and re-render affected components.
|
|
83
|
+
*/
|
|
84
|
+
function endBatchUpdates() {
|
|
85
|
+
isBatchingUpdates = false;
|
|
86
|
+
flushPendingUpdates();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Flush all pending state updates, merging multiple updates for the same hook.
|
|
91
|
+
* Processes updates by priority order (IMMEDIATE first).
|
|
92
|
+
*/
|
|
93
|
+
function flushPendingUpdates() {
|
|
94
|
+
if (pendingStateUpdates.size === 0) return;
|
|
95
|
+
|
|
96
|
+
const updates = new Map(pendingStateUpdates);
|
|
97
|
+
pendingStateUpdates.clear();
|
|
98
|
+
|
|
99
|
+
updates.forEach((stateQueue, component) => {
|
|
100
|
+
if (stateQueue.length === 0) return;
|
|
101
|
+
|
|
102
|
+
// Sort updates by priority (lower number = higher priority)
|
|
103
|
+
stateQueue.sort((a, b) => a.priority - b.priority);
|
|
104
|
+
|
|
105
|
+
const hooks = component._hooks;
|
|
106
|
+
stateQueue.forEach(({ hookIdx, update }) => {
|
|
107
|
+
const hook = hooks[hookIdx];
|
|
108
|
+
if (!hook) return;
|
|
109
|
+
|
|
110
|
+
let newValue;
|
|
111
|
+
if (typeof update.updateFnOrValue === 'function') {
|
|
112
|
+
newValue = update.updateFnOrValue(hook.state);
|
|
113
|
+
} else {
|
|
114
|
+
newValue = update.updateFnOrValue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// State comparison - skip update if value is the same
|
|
118
|
+
const areEqual = hook._areEqual || Object.is;
|
|
119
|
+
if (areEqual(hook.state, newValue)) return;
|
|
120
|
+
|
|
121
|
+
const prevState = hook.state;
|
|
122
|
+
hook.state = newValue;
|
|
123
|
+
|
|
124
|
+
// Track update in debug history
|
|
125
|
+
if (hook._debug) {
|
|
126
|
+
hook._updateHistory.push({
|
|
127
|
+
prev: prevState,
|
|
128
|
+
next: newValue,
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
priority: update.priority,
|
|
131
|
+
});
|
|
132
|
+
// Keep only last 50 entries
|
|
133
|
+
if (hook._updateHistory.length > 50) {
|
|
134
|
+
hook._updateHistory.shift();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Notify subscribers of state change
|
|
139
|
+
notifySubscribers(component, hookIdx, newValue, prevState);
|
|
140
|
+
|
|
141
|
+
// Persist state if persistence is configured
|
|
142
|
+
persistState(component, hookIdx, newValue);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Schedule re-render for the component
|
|
146
|
+
if (scheduleUpdateCallback) {
|
|
147
|
+
scheduleUpdateCallback(component);
|
|
148
|
+
} else if (component._scheduleReRender) {
|
|
149
|
+
component._scheduleReRender();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Execute a function with batched updates enabled.
|
|
156
|
+
* @param {Function} fn - Function to execute within the batch
|
|
157
|
+
* @returns {*} Return value of the executed function
|
|
158
|
+
*/
|
|
159
|
+
function batchedUpdates(fn) {
|
|
160
|
+
startBatchUpdates();
|
|
161
|
+
try {
|
|
162
|
+
return fn();
|
|
163
|
+
} finally {
|
|
164
|
+
endBatchUpdates();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Execute a function at a specific priority level.
|
|
170
|
+
* @param {number} priority - PRIORITY.IMMEDIATE | NORMAL | TRANSITION | IDLE
|
|
171
|
+
* @param {Function} fn - Function to execute
|
|
172
|
+
* @returns {*} Return value of the executed function
|
|
173
|
+
*/
|
|
174
|
+
function runWithPriority(priority, fn) {
|
|
175
|
+
const prevPriority = currentUpdatePriority;
|
|
176
|
+
setCurrentPriority(priority);
|
|
177
|
+
try {
|
|
178
|
+
return fn();
|
|
179
|
+
} finally {
|
|
180
|
+
setCurrentPriority(prevPriority);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Transition Support ───────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
let isTransitionActive = false;
|
|
187
|
+
const pendingTransitions = [];
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Start a transition - marks updates as non-urgent.
|
|
191
|
+
* @param {Function} fn - Function containing transition updates
|
|
192
|
+
*/
|
|
193
|
+
function startTransition(fn) {
|
|
194
|
+
isTransitionActive = true;
|
|
195
|
+
const prevPriority = currentUpdatePriority;
|
|
196
|
+
currentUpdatePriority = PRIORITY.TRANSITION;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
fn();
|
|
200
|
+
} finally {
|
|
201
|
+
isTransitionActive = false;
|
|
202
|
+
currentUpdatePriority = prevPriority;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if a transition is currently active.
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
function isTransitionInProgress() {
|
|
211
|
+
return isTransitionActive;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Subscription System ──────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Subscribe to state changes for a specific component's hook.
|
|
218
|
+
* @param {Object} component - The component instance
|
|
219
|
+
* @param {number} hookIdx - The hook index
|
|
220
|
+
* @param {Function} callback - Called with (newValue, oldValue)
|
|
221
|
+
* @returns {Function} Unsubscribe function
|
|
222
|
+
*/
|
|
223
|
+
function subscribeToState(component, hookIdx, callback) {
|
|
224
|
+
if (!stateSubscribers.has(component)) {
|
|
225
|
+
stateSubscribers.set(component, new Map());
|
|
226
|
+
}
|
|
227
|
+
const componentSubs = stateSubscribers.get(component);
|
|
228
|
+
|
|
229
|
+
if (!componentSubs.has(hookIdx)) {
|
|
230
|
+
componentSubs.set(hookIdx, new Set());
|
|
231
|
+
}
|
|
232
|
+
const subs = componentSubs.get(hookIdx);
|
|
233
|
+
subs.add(callback);
|
|
234
|
+
|
|
235
|
+
return function unsubscribe() {
|
|
236
|
+
subs.delete(callback);
|
|
237
|
+
if (subs.size === 0) {
|
|
238
|
+
componentSubs.delete(hookIdx);
|
|
239
|
+
}
|
|
240
|
+
if (componentSubs.size === 0) {
|
|
241
|
+
stateSubscribers.delete(component);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Notify all subscribers of a state change.
|
|
248
|
+
*/
|
|
249
|
+
function notifySubscribers(component, hookIdx, newValue, oldValue) {
|
|
250
|
+
const componentSubs = stateSubscribers.get(component);
|
|
251
|
+
if (!componentSubs) return;
|
|
252
|
+
|
|
253
|
+
const subs = componentSubs.get(hookIdx);
|
|
254
|
+
if (!subs) return;
|
|
255
|
+
|
|
256
|
+
subs.forEach((callback) => {
|
|
257
|
+
try {
|
|
258
|
+
callback(newValue, oldValue);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('[useState] Subscriber error:', error);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── State Persistence ────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Configure state persistence for a hook.
|
|
269
|
+
* @param {string} key - The localStorage/sessionStorage key
|
|
270
|
+
* @param {Object} options - Persistence options
|
|
271
|
+
* @param {string} options.storage - 'localStorage' | 'sessionStorage'
|
|
272
|
+
* @param {Function} [options.serialize] - Custom serialize function
|
|
273
|
+
* @param {Function} [options.deserialize] - Custom deserialize function
|
|
274
|
+
* @param {number} [options.debounceMs] - Debounce persistence writes
|
|
275
|
+
*/
|
|
276
|
+
function configurePersistence(key, options = {}) {
|
|
277
|
+
const config = {
|
|
278
|
+
key,
|
|
279
|
+
storage: options.storage || 'localStorage',
|
|
280
|
+
serialize: options.serialize || JSON.stringify,
|
|
281
|
+
deserialize: options.deserialize || JSON.parse,
|
|
282
|
+
debounceMs: options.debounceMs || 0,
|
|
283
|
+
};
|
|
284
|
+
return config;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Persist state to storage.
|
|
289
|
+
*/
|
|
290
|
+
let persistenceTimers = new Map();
|
|
291
|
+
|
|
292
|
+
function persistState(component, hookIdx, value) {
|
|
293
|
+
const persistenceConfig = component._hooks[hookIdx]?._persistence;
|
|
294
|
+
if (!persistenceConfig) return;
|
|
295
|
+
|
|
296
|
+
const writeFn = () => {
|
|
297
|
+
try {
|
|
298
|
+
const storage = window[persistenceConfig.storage];
|
|
299
|
+
const serialized = persistenceConfig.serialize(value);
|
|
300
|
+
storage.setItem(persistenceConfig.key, serialized);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('[useState] Persistence error:', error);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Debounced persistence
|
|
307
|
+
if (persistenceConfig.debounceMs > 0) {
|
|
308
|
+
const timerKey = component._hooks[hookIdx]._persistenceTimer;
|
|
309
|
+
if (timerKey) clearTimeout(timerKey);
|
|
310
|
+
component._hooks[hookIdx]._persistenceTimer = setTimeout(writeFn, persistenceConfig.debounceMs);
|
|
311
|
+
} else {
|
|
312
|
+
writeFn();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Load persisted state from storage.
|
|
318
|
+
*/
|
|
319
|
+
function loadPersistedState(persistenceConfig) {
|
|
320
|
+
if (!persistenceConfig) return undefined;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const storage = window[persistenceConfig.storage];
|
|
324
|
+
const serialized = storage.getItem(persistenceConfig.key);
|
|
325
|
+
if (serialized === null) return undefined;
|
|
326
|
+
return persistenceConfig.deserialize(serialized);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error('[useState] Load persistence error:', error);
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── State Comparison Utilities ───────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Deep comparison function for state values.
|
|
337
|
+
* @param {*} a - First value
|
|
338
|
+
* @param {*} b - Second value
|
|
339
|
+
* @returns {boolean} True if deeply equal
|
|
340
|
+
*/
|
|
341
|
+
function deepEqual(a, b) {
|
|
342
|
+
if (Object.is(a, b)) return true;
|
|
343
|
+
|
|
344
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
349
|
+
|
|
350
|
+
const keysA = Object.keys(a);
|
|
351
|
+
const keysB = Object.keys(b);
|
|
352
|
+
|
|
353
|
+
if (keysA.length !== keysB.length) return false;
|
|
354
|
+
|
|
355
|
+
for (const key of keysA) {
|
|
356
|
+
if (!keysB.includes(key)) return false;
|
|
357
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Shallow comparison function for state values.
|
|
365
|
+
* @param {*} a - First value
|
|
366
|
+
* @param {*} b - Second value
|
|
367
|
+
* @returns {boolean}
|
|
368
|
+
*/
|
|
369
|
+
function shallowEqual(a, b) {
|
|
370
|
+
if (Object.is(a, b)) return true;
|
|
371
|
+
|
|
372
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const keysA = Object.keys(a);
|
|
377
|
+
const keysB = Object.keys(b);
|
|
378
|
+
|
|
379
|
+
if (keysA.length !== keysB.length) return false;
|
|
380
|
+
|
|
381
|
+
for (const key of keysA) {
|
|
382
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Hook Context Management ──────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Set the currently rendering component (called by the framework before render).
|
|
392
|
+
* @param {Object} component - The component being rendered
|
|
393
|
+
*/
|
|
394
|
+
function setCurrentComponent(component) {
|
|
395
|
+
currentlyRenderingComponent = component;
|
|
396
|
+
hookIndex = 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get the currently rendering component.
|
|
401
|
+
* @returns {Object|null}
|
|
402
|
+
*/
|
|
403
|
+
function getCurrentComponent() {
|
|
404
|
+
return currentlyRenderingComponent;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Reset the hook index after rendering is complete.
|
|
409
|
+
*/
|
|
410
|
+
function resetHookIndex() {
|
|
411
|
+
hookIndex = 0;
|
|
412
|
+
currentlyRenderingComponent = null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Set the framework's update scheduling callback.
|
|
417
|
+
* @param {Function} callback - Called with (component) to schedule re-render
|
|
418
|
+
*/
|
|
419
|
+
function setScheduleUpdateCallback(callback) {
|
|
420
|
+
scheduleUpdateCallback = callback;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── useState Hook ────────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* useState - State hook with initial value, lazy initializer, batched updates,
|
|
427
|
+
* functional updates, state comparison, subscriptions, persistence, and
|
|
428
|
+
* priority-based updates.
|
|
429
|
+
*
|
|
430
|
+
* @param {*} initialState - Initial state value or lazy initializer function
|
|
431
|
+
* @param {Object} [options] - Additional options
|
|
432
|
+
* @param {Object} [options.persistence] - Persistence configuration
|
|
433
|
+
* @param {Function} [options.areEqual] - Custom equality comparison function
|
|
434
|
+
* @param {boolean} [options.debug] - Enable debug tracing for this state
|
|
435
|
+
* @returns {[*, Function]} Tuple of [currentState, setState]
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* // Simple state
|
|
439
|
+
* const [count, setCount] = useState(0);
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* // Lazy initializer (expensive computation runs once)
|
|
443
|
+
* const [data, setData] = useState(() => computeExpensiveValue());
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* // Functional update
|
|
447
|
+
* setCount(prev => prev + 1);
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* // With persistence
|
|
451
|
+
* const [theme, setTheme] = useState('light', {
|
|
452
|
+
* persistence: configurePersistence('app-theme', { storage: 'localStorage' })
|
|
453
|
+
* });
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* // With deep comparison
|
|
457
|
+
* const [form, setForm] = useState({}, { areEqual: deepEqual });
|
|
458
|
+
*/
|
|
459
|
+
function useState(initialState, options = {}) {
|
|
460
|
+
const component = currentlyRenderingComponent;
|
|
461
|
+
if (!component) {
|
|
462
|
+
throw new Error('[useState] Must be called within a component render phase');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Initialize hooks array on first render
|
|
466
|
+
if (!component._hooks) {
|
|
467
|
+
component._hooks = [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Get or create the hook state
|
|
471
|
+
const currentHookIndex = hookIndex;
|
|
472
|
+
let hook;
|
|
473
|
+
|
|
474
|
+
if (component._hooks[currentHookIndex]) {
|
|
475
|
+
// Re-render: use existing hook state
|
|
476
|
+
hook = component._hooks[currentHookIndex];
|
|
477
|
+
} else {
|
|
478
|
+
// First render: initialize hook state
|
|
479
|
+
let resolvedInitialState;
|
|
480
|
+
|
|
481
|
+
// Check for persisted state first
|
|
482
|
+
if (options.persistence) {
|
|
483
|
+
const persistedValue = loadPersistedState(options.persistence);
|
|
484
|
+
if (persistedValue !== undefined) {
|
|
485
|
+
resolvedInitialState = persistedValue;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// If no persisted value, resolve initial state
|
|
490
|
+
if (resolvedInitialState === undefined) {
|
|
491
|
+
if (typeof initialState === 'function') {
|
|
492
|
+
// Lazy initializer - only called once on mount
|
|
493
|
+
try {
|
|
494
|
+
resolvedInitialState = initialState();
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error('[useState] Lazy initializer error:', error);
|
|
497
|
+
resolvedInitialState = undefined;
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
resolvedInitialState = initialState;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
hook = {
|
|
505
|
+
state: resolvedInitialState,
|
|
506
|
+
_persistence: options.persistence || null,
|
|
507
|
+
_areEqual: options.areEqual || Object.is,
|
|
508
|
+
_isMounted: false,
|
|
509
|
+
_pendingUpdates: [],
|
|
510
|
+
_queue: [],
|
|
511
|
+
_debug: options.debug || false,
|
|
512
|
+
_updateHistory: options.debug ? [] : null,
|
|
513
|
+
_persistenceTimer: null,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
component._hooks[currentHookIndex] = hook;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Process any pending functional updates that were queued
|
|
520
|
+
if (hook._pendingUpdates.length > 0) {
|
|
521
|
+
const updates = hook._pendingUpdates.splice(0);
|
|
522
|
+
updates.forEach((update) => {
|
|
523
|
+
if (typeof update === 'function') {
|
|
524
|
+
hook.state = update(hook.state);
|
|
525
|
+
} else {
|
|
526
|
+
hook.state = update;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Increment hook index for the next hook call
|
|
532
|
+
hookIndex++;
|
|
533
|
+
|
|
534
|
+
// ─── setState Implementation ────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Update state with a new value or updater function.
|
|
538
|
+
* Supports batching, functional updates, priority, and state comparison.
|
|
539
|
+
*
|
|
540
|
+
* @param {*} updateFnOrValue - New state value or updater function
|
|
541
|
+
*/
|
|
542
|
+
const setState = function setState(updateFnOrValue) {
|
|
543
|
+
if (!component._hooks) {
|
|
544
|
+
console.warn('[useState] setState called on unmounted component');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const targetHook = component._hooks[currentHookIndex];
|
|
549
|
+
if (!targetHook) {
|
|
550
|
+
console.warn('[useState] Hook index out of bounds');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const update = createStateUpdate(updateFnOrValue, currentUpdatePriority);
|
|
555
|
+
|
|
556
|
+
if (isBatchingUpdates) {
|
|
557
|
+
// Queue the update for batch processing
|
|
558
|
+
if (!pendingStateUpdates.has(component)) {
|
|
559
|
+
pendingStateUpdates.set(component, []);
|
|
560
|
+
}
|
|
561
|
+
pendingStateUpdates.get(component).push({
|
|
562
|
+
hookIdx: currentHookIndex,
|
|
563
|
+
update,
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
// Immediate update
|
|
567
|
+
let newValue;
|
|
568
|
+
if (typeof updateFnOrValue === 'function') {
|
|
569
|
+
newValue = updateFnOrValue(targetHook.state);
|
|
570
|
+
} else {
|
|
571
|
+
newValue = updateFnOrValue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Use custom equality check or Object.is
|
|
575
|
+
const areEqual = targetHook._areEqual;
|
|
576
|
+
if (areEqual(targetHook.state, newValue)) {
|
|
577
|
+
return; // Skip update - state is the same
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const prevState = targetHook.state;
|
|
581
|
+
targetHook.state = newValue;
|
|
582
|
+
|
|
583
|
+
// Track update in debug history
|
|
584
|
+
if (targetHook._debug && targetHook._updateHistory) {
|
|
585
|
+
targetHook._updateHistory.push({
|
|
586
|
+
prev: prevState,
|
|
587
|
+
next: newValue,
|
|
588
|
+
timestamp: Date.now(),
|
|
589
|
+
priority: update.priority,
|
|
590
|
+
});
|
|
591
|
+
if (targetHook._updateHistory.length > 50) {
|
|
592
|
+
targetHook._updateHistory.shift();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Notify subscribers
|
|
597
|
+
notifySubscribers(component, currentHookIndex, newValue, prevState);
|
|
598
|
+
|
|
599
|
+
// Persist state if configured
|
|
600
|
+
persistState(component, currentHookIndex, newValue);
|
|
601
|
+
|
|
602
|
+
// Schedule re-render
|
|
603
|
+
if (scheduleUpdateCallback) {
|
|
604
|
+
scheduleUpdateCallback(component);
|
|
605
|
+
} else if (component._scheduleReRender) {
|
|
606
|
+
component._scheduleReRender();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// ─── Extended setState Methods ──────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Reset state to initial value.
|
|
615
|
+
*/
|
|
616
|
+
setState.reset = function reset() {
|
|
617
|
+
let resetValue;
|
|
618
|
+
if (typeof initialState === 'function') {
|
|
619
|
+
resetValue = initialState();
|
|
620
|
+
} else {
|
|
621
|
+
resetValue = initialState;
|
|
622
|
+
}
|
|
623
|
+
setState(resetValue);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Subscribe to state changes.
|
|
628
|
+
* @param {Function} callback - Called with (newValue, oldValue)
|
|
629
|
+
* @returns {Function} Unsubscribe function
|
|
630
|
+
*/
|
|
631
|
+
setState.subscribe = function subscribe(callback) {
|
|
632
|
+
return subscribeToState(component, currentHookIndex, callback);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get current state without triggering a re-render.
|
|
637
|
+
* @returns {*} Current state value
|
|
638
|
+
*/
|
|
639
|
+
setState.getCurrent = function getCurrent() {
|
|
640
|
+
const targetHook = component._hooks?.[currentHookIndex];
|
|
641
|
+
return targetHook ? targetHook.state : undefined;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Check if the component is mounted.
|
|
646
|
+
* @returns {boolean}
|
|
647
|
+
*/
|
|
648
|
+
setState.isMounted = function isMounted() {
|
|
649
|
+
const targetHook = component._hooks?.[currentHookIndex];
|
|
650
|
+
return targetHook ? targetHook._isMounted : false;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Set state with a specific priority level.
|
|
655
|
+
* @param {*} updateFnOrValue - New state value or updater function
|
|
656
|
+
* @param {number} priority - PRIORITY level
|
|
657
|
+
*/
|
|
658
|
+
setState.withPriority = function withPriority(updateFnOrValue, priority) {
|
|
659
|
+
const prevPriority = currentUpdatePriority;
|
|
660
|
+
setCurrentPriority(priority);
|
|
661
|
+
try {
|
|
662
|
+
setState(updateFnOrValue);
|
|
663
|
+
} finally {
|
|
664
|
+
setCurrentPriority(prevPriority);
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Set state as a transition (non-urgent update).
|
|
670
|
+
* @param {*} updateFnOrValue - New state value or updater function
|
|
671
|
+
*/
|
|
672
|
+
setState.asTransition = function asTransition(updateFnOrValue) {
|
|
673
|
+
startTransition(() => {
|
|
674
|
+
setState(updateFnOrValue);
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Get the update history for debugging (only if debug mode is enabled).
|
|
680
|
+
* @returns {Array|null} Update history array or null
|
|
681
|
+
*/
|
|
682
|
+
setState.getHistory = function getHistory() {
|
|
683
|
+
const targetHook = component._hooks?.[currentHookIndex];
|
|
684
|
+
return targetHook?._updateHistory || null;
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
return [hook.state, setState];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ─── useTransition Hook ───────────────────────────────────────────────────────
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* useTransition - Hook for marking updates as transitions (non-urgent).
|
|
694
|
+
* Returns a pending flag and a startTransition function.
|
|
695
|
+
*
|
|
696
|
+
* @returns {[boolean, Function]} Tuple of [isPending, startTransition]
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* const [isPending, startTransition] = useTransition();
|
|
700
|
+
* startTransition(() => {
|
|
701
|
+
* setSearchResults(filteredData);
|
|
702
|
+
* });
|
|
703
|
+
*/
|
|
704
|
+
function useTransition() {
|
|
705
|
+
const component = currentlyRenderingComponent;
|
|
706
|
+
if (!component) {
|
|
707
|
+
throw new Error('[useTransition] Must be called within a component render phase');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!component._hooks) {
|
|
711
|
+
component._hooks = [];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const currentHookIndex = hookIndex;
|
|
715
|
+
let hook;
|
|
716
|
+
|
|
717
|
+
if (component._hooks[currentHookIndex]) {
|
|
718
|
+
hook = component._hooks[currentHookIndex];
|
|
719
|
+
} else {
|
|
720
|
+
hook = {
|
|
721
|
+
type: 'transition',
|
|
722
|
+
isPending: false,
|
|
723
|
+
_pendingCount: 0,
|
|
724
|
+
};
|
|
725
|
+
component._hooks[currentHookIndex] = hook;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
hookIndex++;
|
|
729
|
+
|
|
730
|
+
const startTransitionFn = function startTransitionFn(callback) {
|
|
731
|
+
hook._pendingCount++;
|
|
732
|
+
hook.isPending = true;
|
|
733
|
+
|
|
734
|
+
startTransition(() => {
|
|
735
|
+
try {
|
|
736
|
+
callback();
|
|
737
|
+
} finally {
|
|
738
|
+
hook._pendingCount--;
|
|
739
|
+
if (hook._pendingCount <= 0) {
|
|
740
|
+
hook.isPending = false;
|
|
741
|
+
hook._pendingCount = 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
return [hook.isPending, startTransitionFn];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ─── useDeferredValue Hook ────────────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* useDeferredValue - Returns a deferred version of the value that may lag
|
|
754
|
+
* behind the current value. Useful for optimizing renders of expensive lists.
|
|
755
|
+
*
|
|
756
|
+
* @param {*} value - The value to defer
|
|
757
|
+
* @param {number} [timeoutMs] - Maximum time to defer (optional)
|
|
758
|
+
* @returns {*} Deferred value
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* const deferredQuery = useDeferredValue(query);
|
|
762
|
+
* const filteredItems = useMemo(() => items.filter(i => i.name.includes(deferredQuery)), [deferredQuery, items]);
|
|
763
|
+
*/
|
|
764
|
+
function useDeferredValue(value, timeoutMs) {
|
|
765
|
+
const component = currentlyRenderingComponent;
|
|
766
|
+
if (!component) {
|
|
767
|
+
throw new Error('[useDeferredValue] Must be called within a component render phase');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (!component._hooks) {
|
|
771
|
+
component._hooks = [];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const currentHookIndex = hookIndex;
|
|
775
|
+
let hook;
|
|
776
|
+
|
|
777
|
+
if (component._hooks[currentHookIndex]) {
|
|
778
|
+
hook = component._hooks[currentHookIndex];
|
|
779
|
+
|
|
780
|
+
// Update the current value immediately
|
|
781
|
+
hook.currentValue = value;
|
|
782
|
+
|
|
783
|
+
// If no timeout or no deferred update pending, use current value
|
|
784
|
+
if (!hook._deferredUpdatePending) {
|
|
785
|
+
hook.deferredValue = value;
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
hook = {
|
|
789
|
+
type: 'deferred-value',
|
|
790
|
+
currentValue: value,
|
|
791
|
+
deferredValue: value,
|
|
792
|
+
_deferredUpdatePending: false,
|
|
793
|
+
_timeoutId: null,
|
|
794
|
+
};
|
|
795
|
+
component._hooks[currentHookIndex] = hook;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
hookIndex++;
|
|
799
|
+
return hook.deferredValue;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────────
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Clean up hook state when a component unmounts.
|
|
806
|
+
* @param {Object} component - The unmounting component
|
|
807
|
+
*/
|
|
808
|
+
function cleanupComponentState(component) {
|
|
809
|
+
// Remove all subscribers for this component
|
|
810
|
+
stateSubscribers.delete(component);
|
|
811
|
+
|
|
812
|
+
// Remove any pending updates
|
|
813
|
+
pendingStateUpdates.delete(component);
|
|
814
|
+
|
|
815
|
+
// Clean up persistence timers and references
|
|
816
|
+
if (component._hooks) {
|
|
817
|
+
component._hooks.forEach((hook) => {
|
|
818
|
+
hook._pendingUpdates = [];
|
|
819
|
+
hook._queue = [];
|
|
820
|
+
if (hook._persistenceTimer) {
|
|
821
|
+
clearTimeout(hook._persistenceTimer);
|
|
822
|
+
}
|
|
823
|
+
if (hook._updateHistory) {
|
|
824
|
+
hook._updateHistory = null;
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
module.exports = {
|
|
833
|
+
useState,
|
|
834
|
+
useTransition,
|
|
835
|
+
useDeferredValue,
|
|
836
|
+
setCurrentComponent,
|
|
837
|
+
getCurrentComponent,
|
|
838
|
+
resetHookIndex,
|
|
839
|
+
setScheduleUpdateCallback,
|
|
840
|
+
startBatchUpdates,
|
|
841
|
+
endBatchUpdates,
|
|
842
|
+
batchedUpdates,
|
|
843
|
+
configurePersistence,
|
|
844
|
+
subscribeToState,
|
|
845
|
+
cleanupComponentState,
|
|
846
|
+
deepEqual,
|
|
847
|
+
shallowEqual,
|
|
848
|
+
runWithPriority,
|
|
849
|
+
startTransition,
|
|
850
|
+
isTransitionInProgress,
|
|
851
|
+
getCurrentPriority,
|
|
852
|
+
setCurrentPriority,
|
|
853
|
+
PRIORITY,
|
|
854
|
+
};
|