fynixui 1.0.11 → 1.0.13

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/dist/runtime.js CHANGED
@@ -22,10 +22,62 @@ import { nixRef } from "./hooks/nixRef";
22
22
  import { nixState } from "./hooks/nixState";
23
23
  import { nixStore } from "./hooks/nixStore";
24
24
  import createFynix from "./router/router";
25
+ function shallowEqual(a, b) {
26
+ if (a === b)
27
+ return true;
28
+ if (!a || !b || typeof a !== "object" || typeof b !== "object")
29
+ return false;
30
+ const keysA = Object.keys(a);
31
+ const keysB = Object.keys(b);
32
+ if (keysA.length !== keysB.length)
33
+ return false;
34
+ return keysA.every((k) => a[k] === b[k]);
35
+ }
36
+ let batchingStorage = null;
37
+ try {
38
+ if (typeof globalThis.AsyncLocalStorage !== "undefined") {
39
+ batchingStorage = new globalThis.AsyncLocalStorage();
40
+ }
41
+ }
42
+ catch {
43
+ }
44
+ export function batchUpdates(fn) {
45
+ const store = batchingStorage?.getStore();
46
+ const isBatching = store?.isBatching ?? false;
47
+ if (isBatching) {
48
+ fn();
49
+ return;
50
+ }
51
+ const newStore = { isBatching: true, callbacks: [] };
52
+ const run = () => {
53
+ try {
54
+ fn();
55
+ }
56
+ finally {
57
+ const callbacks = newStore.callbacks;
58
+ newStore.isBatching = false;
59
+ if (callbacks.length > 0)
60
+ scheduler.executeBatchedCallbacks(callbacks);
61
+ }
62
+ };
63
+ if (batchingStorage) {
64
+ batchingStorage.run(newStore, run);
65
+ }
66
+ else {
67
+ run();
68
+ }
69
+ }
70
+ export function isCurrentlyBatching() {
71
+ const store = batchingStorage?.getStore();
72
+ return store?.isBatching ?? false;
73
+ }
74
+ function getCurrentBatchStore() {
75
+ return batchingStorage?.getStore() ?? null;
76
+ }
25
77
  class SimplePriorityQueue {
26
78
  constructor() {
27
- this.items = [];
28
- this.priorityOrder = {
79
+ this.heap = [];
80
+ this.order = {
29
81
  immediate: 0,
30
82
  high: 1,
31
83
  normal: 2,
@@ -33,21 +85,76 @@ class SimplePriorityQueue {
33
85
  idle: 4,
34
86
  };
35
87
  }
88
+ cmp(a, b) {
89
+ return this.order[a.priority] - this.order[b.priority];
90
+ }
91
+ siftUp(i) {
92
+ if (i < 0 || i >= this.heap.length)
93
+ return;
94
+ while (i > 0) {
95
+ const p = (i - 1) >> 1;
96
+ if (p < 0 || p >= this.heap.length)
97
+ break;
98
+ if (this.cmp(this.heap[i], this.heap[p]) < 0) {
99
+ [this.heap[i], this.heap[p]] = [this.heap[p], this.heap[i]];
100
+ i = p;
101
+ }
102
+ else
103
+ break;
104
+ }
105
+ }
106
+ siftDown(i) {
107
+ const n = this.heap.length;
108
+ if (i < 0 || i >= n)
109
+ return;
110
+ while (true) {
111
+ let s = i;
112
+ const l = 2 * i + 1, r = 2 * i + 2;
113
+ if (l < n && this.cmp(this.heap[l], this.heap[s]) < 0)
114
+ s = l;
115
+ if (r < n && this.cmp(this.heap[r], this.heap[s]) < 0)
116
+ s = r;
117
+ if (s === i)
118
+ break;
119
+ if (s < 0 || s >= n)
120
+ break;
121
+ [this.heap[i], this.heap[s]] = [this.heap[s], this.heap[i]];
122
+ i = s;
123
+ }
124
+ }
36
125
  push(item, priority) {
37
- this.items.push({ item, priority });
38
- this.items.sort((a, b) => this.priorityOrder[a.priority] - this.priorityOrder[b.priority]);
126
+ if (!item) {
127
+ console.warn("[SimplePriorityQueue] Attempted to push null/undefined item");
128
+ return;
129
+ }
130
+ this.heap.push({ item, priority });
131
+ this.siftUp(this.heap.length - 1);
39
132
  }
40
133
  pop() {
41
- return this.items.shift()?.item;
134
+ if (!this.heap.length)
135
+ return undefined;
136
+ const top = this.heap[0];
137
+ if (!top)
138
+ return undefined;
139
+ const last = this.heap.pop();
140
+ if (!last)
141
+ return top.item;
142
+ if (this.heap.length > 0) {
143
+ this.heap[0] = last;
144
+ this.siftDown(0);
145
+ }
146
+ return top.item;
42
147
  }
43
148
  peek() {
44
- return this.items[0]?.item;
149
+ if (!this.heap.length)
150
+ return undefined;
151
+ return this.heap[0]?.item;
45
152
  }
46
153
  size() {
47
- return this.items.length;
154
+ return this.heap.length;
48
155
  }
49
156
  isEmpty() {
50
- return this.items.length === 0;
157
+ return !this.heap.length;
51
158
  }
52
159
  }
53
160
  class FynixScheduler {
@@ -57,39 +164,36 @@ class FynixScheduler {
57
164
  this.isScheduled = false;
58
165
  this.isWorking = false;
59
166
  this.currentPriority = "normal";
60
- this.updateIdCounter = 0;
167
+ this.idCounter = 0;
61
168
  }
62
169
  schedule(update, priority = "normal") {
63
- update.id = `update_${this.updateIdCounter++}`;
170
+ update.id = `u_${this.idCounter++}`;
64
171
  update.priority = priority;
65
172
  update.timestamp = performance.now();
66
173
  if (priority === "immediate") {
67
- this.flushUpdate(update);
174
+ this.flushOne(update);
68
175
  }
69
176
  else {
70
177
  this.updateQueue.push(update, priority);
71
- this.scheduleWork();
178
+ this.kick();
72
179
  }
73
180
  }
74
181
  batchUpdates(updates) {
75
- updates.forEach((update) => this.batchedUpdates.add(update));
76
- this.scheduleWork();
182
+ updates.forEach((u) => this.batchedUpdates.add(u));
183
+ this.kick();
77
184
  }
78
185
  timeSlice(deadline) {
79
- const startTime = performance.now();
80
- const previousPriority = this.currentPriority;
81
- while (!this.updateQueue.isEmpty() &&
82
- performance.now() - startTime < deadline) {
83
- const update = this.updateQueue.pop();
84
- if (update) {
85
- if (this.shouldYield() && update.priority !== "immediate") {
86
- this.updateQueue.push(update, update.priority);
87
- break;
88
- }
89
- this.flushUpdate(update);
186
+ const t0 = performance.now();
187
+ const prev = this.currentPriority;
188
+ while (!this.updateQueue.isEmpty() && performance.now() - t0 < deadline) {
189
+ const u = this.updateQueue.pop();
190
+ if (this.shouldYield() && u.priority !== "immediate") {
191
+ this.updateQueue.push(u, u.priority);
192
+ break;
90
193
  }
194
+ this.flushOne(u);
91
195
  }
92
- this.currentPriority = previousPriority;
196
+ this.currentPriority = prev;
93
197
  return this.updateQueue.isEmpty();
94
198
  }
95
199
  flush() {
@@ -97,63 +201,57 @@ class FynixScheduler {
97
201
  return;
98
202
  this.isWorking = true;
99
203
  try {
100
- while (!this.updateQueue.isEmpty()) {
101
- const update = this.updateQueue.peek();
102
- if (update && update.priority === "immediate") {
103
- this.flushUpdate(this.updateQueue.pop());
104
- }
105
- else {
106
- break;
107
- }
204
+ let passes = 0;
205
+ while ((!this.updateQueue.isEmpty() || this.batchedUpdates.size > 0) &&
206
+ passes++ < 10) {
207
+ while (!this.updateQueue.isEmpty())
208
+ this.flushOne(this.updateQueue.pop());
209
+ this.batchedUpdates.forEach((u) => this.flushOne(u));
210
+ this.batchedUpdates.clear();
108
211
  }
109
- this.batchedUpdates.forEach((update) => this.flushUpdate(update));
110
- this.batchedUpdates.clear();
111
212
  }
112
213
  finally {
113
214
  this.isWorking = false;
114
215
  this.isScheduled = false;
115
216
  }
116
217
  }
117
- flushUpdate(update) {
118
- const previousPriority = this.currentPriority;
119
- this.currentPriority = update.priority;
218
+ flushOne(u) {
219
+ const prev = this.currentPriority;
220
+ this.currentPriority = u.priority;
120
221
  try {
121
- update.callback();
222
+ u.callback();
122
223
  }
123
- catch (error) {
124
- console.error("[FynixScheduler] Update error:", error);
125
- showErrorOverlay(error);
224
+ catch (e) {
225
+ console.error("[FynixScheduler]", e);
226
+ showErrorOverlay(e);
126
227
  }
127
228
  finally {
128
- this.currentPriority = previousPriority;
229
+ this.currentPriority = prev;
129
230
  }
130
231
  }
131
- scheduleWork() {
232
+ kick() {
132
233
  if (this.isScheduled)
133
234
  return;
134
235
  this.isScheduled = true;
135
- const nextUpdate = this.updateQueue.peek();
136
- if (nextUpdate) {
137
- if (nextUpdate.priority === "high") {
138
- requestAnimationFrame(() => this.workLoop(16.67));
139
- }
140
- else {
141
- if ("requestIdleCallback" in window) {
142
- requestIdleCallback((deadline) => {
143
- this.workLoop(deadline.timeRemaining());
144
- });
145
- }
146
- else {
147
- setTimeout(() => this.workLoop(5), 0);
148
- }
149
- }
236
+ const next = this.updateQueue.peek();
237
+ if (!next) {
238
+ this.isScheduled = false;
239
+ return;
240
+ }
241
+ if (next.priority === "high" || next.priority === "immediate") {
242
+ requestAnimationFrame(() => this.loop(16.67));
243
+ }
244
+ else if ("requestIdleCallback" in window) {
245
+ requestIdleCallback((d) => this.loop(d.timeRemaining()));
246
+ }
247
+ else {
248
+ setTimeout(() => this.loop(5), 0);
150
249
  }
151
250
  }
152
- workLoop(deadline) {
153
- const hasMoreWork = !this.timeSlice(deadline);
154
- if (hasMoreWork) {
251
+ loop(deadline) {
252
+ if (!this.timeSlice(deadline)) {
155
253
  this.isScheduled = false;
156
- this.scheduleWork();
254
+ this.kick();
157
255
  }
158
256
  else {
159
257
  this.flush();
@@ -163,249 +261,125 @@ class FynixScheduler {
163
261
  return this.currentPriority;
164
262
  }
165
263
  shouldYield() {
166
- const nextUpdate = this.updateQueue.peek();
167
- if (!nextUpdate)
264
+ const next = this.updateQueue.peek();
265
+ if (!next)
168
266
  return false;
169
- const currentPriorityLevel = this.getPriorityLevel(this.currentPriority);
170
- const nextPriorityLevel = this.getPriorityLevel(nextUpdate.priority);
171
- return nextPriorityLevel < currentPriorityLevel;
172
- }
173
- getPriorityLevel(priority) {
174
- const levels = { immediate: 0, high: 1, normal: 2, low: 3, idle: 4 };
175
- return levels[priority];
176
- }
177
- }
178
- const scheduler = new FynixScheduler();
179
- class FiberRenderer {
180
- constructor() {
181
- this.workInProgressRoot = null;
182
- this.nextUnitOfWork = null;
183
- this.currentRoot = null;
184
- this.deletions = [];
267
+ return this.level(next.priority) < this.level(this.currentPriority);
185
268
  }
186
- scheduleWork(fiber) {
187
- this.workInProgressRoot = {
188
- ...fiber,
189
- alternate: this.currentRoot,
190
- };
191
- this.nextUnitOfWork = this.workInProgressRoot;
192
- this.deletions = [];
193
- scheduler.schedule({
194
- id: "",
195
- type: "layout",
196
- priority: "high",
197
- callback: () => this.workLoop(5),
198
- timestamp: performance.now(),
199
- }, "high");
269
+ level(p) {
270
+ return { immediate: 0, high: 1, normal: 2, low: 3, idle: 4 }[p];
200
271
  }
201
- workLoop(deadline) {
202
- const startTime = performance.now();
203
- while (this.nextUnitOfWork && performance.now() - startTime < deadline) {
204
- this.nextUnitOfWork = this.performUnitOfWork(this.nextUnitOfWork);
272
+ executeBatchedCallbacks(cbs) {
273
+ const store = getCurrentBatchStore();
274
+ if (store) {
275
+ store.callbacks.push(...cbs);
205
276
  }
206
- if (!this.nextUnitOfWork && this.workInProgressRoot) {
207
- this.commitRoot();
208
- }
209
- else if (this.nextUnitOfWork) {
210
- scheduler.schedule({
211
- id: "",
212
- type: "layout",
213
- priority: "normal",
214
- callback: () => this.workLoop(5),
277
+ else {
278
+ const updates = cbs.map((cb) => ({
279
+ id: `b_${this.idCounter++}`,
280
+ type: "state",
281
+ priority: "high",
282
+ callback: cb,
215
283
  timestamp: performance.now(),
216
- }, "normal");
217
- }
218
- }
219
- performUnitOfWork(fiber) {
220
- this.reconcileChildren(fiber, fiber.props?.children || []);
221
- if (fiber.child) {
222
- return fiber.child;
223
- }
224
- let nextFiber = fiber;
225
- while (nextFiber) {
226
- if (nextFiber.sibling) {
227
- return nextFiber.sibling;
228
- }
229
- nextFiber = nextFiber.parent;
230
- }
231
- return null;
232
- }
233
- reconcileChildren(wipFiber, elements) {
234
- let index = 0;
235
- let oldFiber = wipFiber.alternate?.child;
236
- let prevSibling = null;
237
- while (index < elements.length || oldFiber != null) {
238
- const element = elements[index];
239
- let newFiber = null;
240
- const sameType = oldFiber && element && element.type === oldFiber.type;
241
- if (sameType && oldFiber) {
242
- newFiber = {
243
- type: oldFiber.type,
244
- props: element.props,
245
- key: element.key,
246
- _domNode: oldFiber._domNode,
247
- parent: wipFiber,
248
- alternate: oldFiber,
249
- effectTag: "UPDATE",
250
- updatePriority: "normal",
251
- child: null,
252
- sibling: null,
253
- _rendered: null,
254
- };
255
- }
256
- if (element && !sameType) {
257
- newFiber = {
258
- type: element.type,
259
- props: element.props,
260
- key: element.key,
261
- _domNode: null,
262
- parent: wipFiber,
263
- alternate: null,
264
- effectTag: "PLACEMENT",
265
- updatePriority: "normal",
266
- child: null,
267
- sibling: null,
268
- _rendered: null,
269
- };
270
- }
271
- if (oldFiber && !sameType) {
272
- oldFiber.effectTag = "DELETION";
273
- this.deletions.push(oldFiber);
274
- }
275
- if (oldFiber) {
276
- oldFiber = oldFiber.sibling;
277
- }
278
- if (index === 0) {
279
- wipFiber.child = newFiber;
280
- }
281
- else if (newFiber && prevSibling) {
282
- prevSibling.sibling = newFiber;
283
- }
284
- prevSibling = newFiber;
285
- index++;
286
- }
287
- }
288
- commitRoot() {
289
- this.deletions.forEach((fiber) => this.commitWork(fiber));
290
- if (this.workInProgressRoot?.child) {
291
- this.commitWork(this.workInProgressRoot.child);
284
+ }));
285
+ this.batchUpdates(updates);
286
+ this.flush();
292
287
  }
293
- this.currentRoot = this.workInProgressRoot;
294
- this.workInProgressRoot = null;
295
288
  }
296
- commitWork(fiber) {
297
- if (!fiber)
298
- return;
299
- let domParentFiber = fiber.parent;
300
- while (!domParentFiber?._domNode) {
301
- domParentFiber = domParentFiber?.parent || null;
302
- }
303
- const domParent = domParentFiber?._domNode;
304
- if (fiber.effectTag === "PLACEMENT" && fiber._domNode && domParent) {
305
- domParent.appendChild(fiber._domNode);
306
- }
307
- else if (fiber.effectTag === "UPDATE" && fiber._domNode) {
308
- this.updateDom(fiber._domNode, fiber.alternate?.props || {}, fiber.props);
309
- }
310
- else if (fiber.effectTag === "DELETION" && domParent) {
311
- this.commitDeletion(fiber, domParent);
312
- }
313
- this.commitWork(fiber.child);
314
- this.commitWork(fiber.sibling);
289
+ getState() {
290
+ return {
291
+ isScheduled: this.isScheduled,
292
+ isWorking: this.isWorking,
293
+ currentPriority: this.currentPriority,
294
+ queueSize: this.updateQueue.size(),
295
+ batchedUpdatesSize: this.batchedUpdates.size,
296
+ idCounter: this.idCounter,
297
+ };
315
298
  }
316
- commitDeletion(fiber, domParent) {
317
- if (fiber._domNode) {
318
- domParent.removeChild(fiber._domNode);
319
- }
320
- else if (fiber.child) {
321
- this.commitDeletion(fiber.child, domParent);
322
- }
299
+ getQueueMetrics() {
300
+ return {
301
+ pending: this.updateQueue.size(),
302
+ batched: this.batchedUpdates.size,
303
+ isActive: this.isWorking || this.isScheduled,
304
+ currentPriority: this.currentPriority,
305
+ };
323
306
  }
324
- updateDom(dom, prevProps, nextProps) {
325
- Object.keys(prevProps)
326
- .filter((key) => key !== "children" && !(key in nextProps))
327
- .forEach((name) => {
328
- if (name.startsWith("on")) {
329
- const eventType = name.toLowerCase().substring(2);
330
- dom.removeEventListener(eventType, prevProps[name]);
331
- }
332
- else {
333
- dom[name] = "";
334
- }
335
- });
336
- Object.keys(nextProps)
337
- .filter((key) => key !== "children")
338
- .forEach((name) => {
339
- if (prevProps[name] !== nextProps[name]) {
340
- if (name.startsWith("on")) {
341
- const eventType = name.toLowerCase().substring(2);
342
- dom.addEventListener(eventType, nextProps[name]);
343
- }
344
- else {
345
- dom[name] = nextProps[name];
346
- }
347
- }
348
- });
307
+ clearQueue() {
308
+ this.updateQueue = new SimplePriorityQueue();
309
+ this.batchedUpdates.clear();
349
310
  }
350
311
  }
351
- const fiberRenderer = new FiberRenderer();
352
- export function useFiberRenderer() {
353
- return fiberRenderer;
312
+ const scheduler = new FynixScheduler();
313
+ let errorConfig = {
314
+ logToConsole: true,
315
+ showOverlay: true,
316
+ };
317
+ export function configureErrorHandling(config) {
318
+ errorConfig = { ...errorConfig, ...config };
354
319
  }
355
- class HierarchicalStore {
356
- constructor() {
357
- this.root = new Map();
358
- this.selectorCache = new Map();
359
- this.stateSnapshot = {};
320
+ export function getErrorConfig() {
321
+ return { ...errorConfig };
322
+ }
323
+ let perfConfig = {
324
+ enabled: false,
325
+ logMeasurements: false,
326
+ slowRenderThreshold: 16.67,
327
+ };
328
+ export function enablePerformanceProfiling(config) {
329
+ perfConfig = { ...perfConfig, ...config };
330
+ }
331
+ export function getPerfConfig() {
332
+ return { ...perfConfig };
333
+ }
334
+ function perfMark(name) {
335
+ if (!perfConfig.enabled || typeof performance === "undefined")
336
+ return;
337
+ try {
338
+ performance.mark(`fynix:${name}`);
360
339
  }
361
- select(selector) {
362
- const selectorKey = selector.toString();
363
- if (this.selectorCache.has(selectorKey)) {
364
- return this.selectorCache.get(selectorKey);
365
- }
366
- const result = selector(this.getState());
367
- this.selectorCache.set(selectorKey, result);
368
- return result;
340
+ catch {
369
341
  }
370
- optimisticUpdate(path, update, rollback) {
371
- const original = this.get(path);
372
- this.set(path, update);
373
- return {
374
- commit: () => this.clearRollback(path),
375
- rollback: () => {
376
- this.set(path, original);
377
- rollback?.();
378
- },
379
- };
342
+ }
343
+ function perfMeasure(name, startMark, endMark) {
344
+ if (!perfConfig.enabled || typeof performance === "undefined")
345
+ return 0;
346
+ try {
347
+ performance.measure(`fynix:${name}`, `fynix:${startMark}`, `fynix:${endMark}`);
348
+ const measures = performance.getEntriesByName(`fynix:${name}`);
349
+ const duration = measures[measures.length - 1]?.duration ?? 0;
350
+ if (perfConfig.logMeasurements) {
351
+ console.debug(`[Fynix Performance] ${name}: ${duration.toFixed(2)}ms`);
352
+ }
353
+ return duration;
380
354
  }
381
- getState() {
382
- return this.stateSnapshot;
355
+ catch {
356
+ return 0;
383
357
  }
384
- get(path) {
385
- return this.root.get(path)?.value;
358
+ }
359
+ const asyncErrorHandlers = [];
360
+ function publishAsyncError(error) {
361
+ if (errorConfig.logToConsole) {
362
+ console.error("[Fynix] Async Error:", error);
363
+ }
364
+ if (errorConfig.onAsyncError) {
365
+ const handled = errorConfig.onAsyncError(error);
366
+ if (handled)
367
+ return;
386
368
  }
387
- set(path, value) {
388
- const node = this.root.get(path);
389
- if (node) {
390
- node.value = value;
391
- this.stateSnapshot = { ...this.stateSnapshot, [path]: value };
392
- this.invalidateSelectors();
369
+ if (asyncErrorHandlers.length > 0) {
370
+ const handler = asyncErrorHandlers[asyncErrorHandlers.length - 1];
371
+ if (handler) {
372
+ handler(error);
373
+ return;
393
374
  }
394
375
  }
395
- clearRollback(path) {
396
- console.log(`[HierarchicalStore] Optimistic update committed for path: ${path}`);
376
+ if (errorConfig.showOverlay) {
377
+ showErrorOverlay(error);
397
378
  }
398
- invalidateSelectors() {
399
- this.selectorCache.clear();
400
- }
401
- }
402
- const hierarchicalStore = new HierarchicalStore();
403
- export function useHierarchicalStore() {
404
- return hierarchicalStore;
405
379
  }
406
380
  export const TEXT = Symbol("text");
407
381
  export const Fragment = Symbol("Fragment");
408
- const BOOLEAN_ATTRS = new Set([
382
+ export const BOOLEAN_ATTRS = new Set([
409
383
  "checked",
410
384
  "selected",
411
385
  "disabled",
@@ -424,7 +398,7 @@ const BOOLEAN_ATTRS = new Set([
424
398
  "novalidate",
425
399
  "formnovalidate",
426
400
  ]);
427
- const DOM_PROPERTIES = new Set([
401
+ export const DOM_PROPERTIES = new Set([
428
402
  "value",
429
403
  "checked",
430
404
  "selected",
@@ -433,20 +407,20 @@ const DOM_PROPERTIES = new Set([
433
407
  "textContent",
434
408
  "innerText",
435
409
  ]);
436
- const DANGEROUS_HTML_PROPS = new Set([
410
+ export const DANGEROUS_HTML_PROPS = new Set([
437
411
  "innerHTML",
438
412
  "outerHTML",
439
413
  "insertAdjacentHTML",
440
414
  "srcdoc",
441
415
  ]);
442
- const DANGEROUS_PROTOCOLS = new Set([
416
+ export const DANGEROUS_PROTOCOLS = new Set([
443
417
  "javascript:",
444
418
  "data:",
445
419
  "vbscript:",
446
420
  "file:",
447
421
  "about:",
448
422
  ]);
449
- const SAFE_PROTOCOLS = new Set([
423
+ export const SAFE_PROTOCOLS = new Set([
450
424
  "http:",
451
425
  "https:",
452
426
  "ftp:",
@@ -458,10 +432,173 @@ const SAFE_PROTOCOLS = new Set([
458
432
  "./",
459
433
  "../",
460
434
  ]);
435
+ function sanitizeText(text) {
436
+ if (typeof text !== "string")
437
+ return String(text);
438
+ return text
439
+ .replace(/[<>"'&]/g, (c) => ({
440
+ "<": "&lt;",
441
+ ">": "&gt;",
442
+ '"': "&quot;",
443
+ "'": "&#x27;",
444
+ "&": "&amp;",
445
+ })[c] || c)
446
+ .replace(/javascript:/gi, "blocked:")
447
+ .replace(/data:.*?base64/gi, "blocked:");
448
+ }
449
+ function sanitizeAttributeValue(value) {
450
+ if (typeof value !== "string")
451
+ return String(value);
452
+ return value
453
+ .replace(/["'<>]/g, (c) => ({ '"': "&quot;", "'": "&#x27;", "<": "&lt;", ">": "&gt;" })[c] || c)
454
+ .replace(/javascript:/gi, "blocked:")
455
+ .replace(/on\w+=/gi, "blocked=");
456
+ }
457
+ function sanitizeErrorMessage(error) {
458
+ if (!error)
459
+ return "Unknown error";
460
+ return sanitizeText(String(error.message || error.toString() || "Unknown error")).slice(0, 200);
461
+ }
462
+ function applyAttributeValue(el, key, value) {
463
+ const k = key.toLowerCase();
464
+ if (BOOLEAN_ATTRS.has(k)) {
465
+ if (value) {
466
+ el.setAttribute(k, "");
467
+ el[k] = true;
468
+ }
469
+ else {
470
+ el.removeAttribute(k);
471
+ el[k] = false;
472
+ }
473
+ return;
474
+ }
475
+ if (DOM_PROPERTIES.has(key) && !DANGEROUS_HTML_PROPS.has(key)) {
476
+ el[key] =
477
+ key === "textContent" || key === "innerText"
478
+ ? sanitizeText(value ?? "")
479
+ : (value ?? "");
480
+ return;
481
+ }
482
+ if (key.startsWith("data-") || key.startsWith("aria-")) {
483
+ if (value != null && value !== false)
484
+ el.setAttribute(key, sanitizeAttributeValue(String(value)));
485
+ else
486
+ el.removeAttribute(key);
487
+ return;
488
+ }
489
+ if (value != null && value !== false)
490
+ el.setAttribute(key, value);
491
+ }
492
+ function setProperty(el, key, value) {
493
+ if (value &&
494
+ typeof value === "object" &&
495
+ (value._isNixState || value._isRestState) &&
496
+ key !== "r-class" &&
497
+ key !== "rc") {
498
+ const anyEl = el;
499
+ if (!anyEl._fynixCleanups)
500
+ anyEl._fynixCleanups = [];
501
+ applyAttributeValue(el, key, value.value);
502
+ const unsub = value.subscribe(() => applyAttributeValue(el, key, value.value));
503
+ anyEl._fynixCleanups.push(unsub);
504
+ return;
505
+ }
506
+ if (key === "r-class" || key === "rc") {
507
+ if (typeof value === "string") {
508
+ el.setAttribute("class", value);
509
+ return;
510
+ }
511
+ if (value && (value._isNixState || value._isRestState)) {
512
+ el.setAttribute("class", value.value);
513
+ const anyEl = el;
514
+ if (!anyEl._fynixCleanups)
515
+ anyEl._fynixCleanups = [];
516
+ anyEl._fynixCleanups.push(value.subscribe(() => el.setAttribute("class", value.value)));
517
+ }
518
+ return;
519
+ }
520
+ if (key.startsWith("r-")) {
521
+ registerDelegatedHandler(el, key.slice(2).toLowerCase(), value);
522
+ return;
523
+ }
524
+ if (key === "style" && typeof value === "object") {
525
+ Object.assign(el.style, value);
526
+ return;
527
+ }
528
+ if (DANGEROUS_HTML_PROPS.has(key)) {
529
+ console.error(`[Fynix] Security: ${key} blocked. Use textContent or children.`);
530
+ return;
531
+ }
532
+ if (["href", "src", "action", "formaction"].includes(key) &&
533
+ typeof value === "string") {
534
+ const n = value.trim().toLowerCase();
535
+ for (const p of DANGEROUS_PROTOCOLS) {
536
+ if (n.startsWith(p)) {
537
+ console.error(`[Fynix] Security: ${p} blocked in ${key}`);
538
+ return;
539
+ }
540
+ }
541
+ if (n.includes(":")) {
542
+ const proto = n.split(":")[0] + ":";
543
+ if (!SAFE_PROTOCOLS.has(proto) && !SAFE_PROTOCOLS.has(n.charAt(0))) {
544
+ console.error(`[Fynix] Security: protocol '${proto}' not safe in ${key}`);
545
+ return;
546
+ }
547
+ }
548
+ if (n.startsWith("data:") &&
549
+ (n.includes("javascript") || n.includes("<script"))) {
550
+ console.error(`[Fynix] Security: suspicious data: URL blocked in ${key}`);
551
+ return;
552
+ }
553
+ }
554
+ if (key.toLowerCase().startsWith("on") && key !== "open") {
555
+ console.error(`[Fynix] Security: inline handler '${key}' blocked. Use r-${key.slice(2)}.`);
556
+ return;
557
+ }
558
+ applyAttributeValue(el, key, value);
559
+ }
560
+ const delegatedEvents = new Map();
561
+ let eventIdCounter = 1;
562
+ function ensureDelegated(type) {
563
+ if (delegatedEvents.has(type))
564
+ return;
565
+ delegatedEvents.set(type, new Map());
566
+ document.addEventListener(type, (e) => {
567
+ let cur = e.target;
568
+ while (cur && cur !== document) {
569
+ if (cur.nodeType !== 1)
570
+ break;
571
+ const eid = cur._rest_eid;
572
+ const handler = eid != null ? delegatedEvents.get(type)?.get(eid) : undefined;
573
+ if (handler) {
574
+ handler(e);
575
+ return;
576
+ }
577
+ cur = cur.parentElement;
578
+ }
579
+ });
580
+ }
581
+ function registerDelegatedHandler(el, name, fn) {
582
+ if (typeof fn !== "function" || el.nodeType !== 1)
583
+ return;
584
+ const anyEl = el;
585
+ const eid = anyEl._rest_eid ?? (anyEl._rest_eid = ++eventIdCounter);
586
+ ensureDelegated(name);
587
+ delegatedEvents.get(name).set(eid, (e) => {
588
+ try {
589
+ isCurrentlyBatching()
590
+ ? fn.call(el, e)
591
+ : batchUpdates(() => fn.call(el, e));
592
+ }
593
+ catch (err) {
594
+ console.error("[Fynix] Event handler error:", err);
595
+ showErrorOverlay(err);
596
+ }
597
+ });
598
+ }
461
599
  export function createTextVNode(text) {
462
- if (text == null || text === false) {
600
+ if (text == null || text === false)
463
601
  return { type: TEXT, props: { nodeValue: "" }, key: null };
464
- }
465
602
  if (text && typeof text === "object" && text._isNixState) {
466
603
  const vnode = {
467
604
  type: TEXT,
@@ -471,9 +608,8 @@ export function createTextVNode(text) {
471
608
  _cleanup: null,
472
609
  };
473
610
  vnode._cleanup = text.subscribe(() => {
474
- if (vnode._domNode) {
611
+ if (vnode._domNode)
475
612
  vnode._domNode.nodeValue = String(text.value);
476
- }
477
613
  });
478
614
  return vnode;
479
615
  }
@@ -482,7 +618,7 @@ export function createTextVNode(text) {
482
618
  export function h(type, props = null, ...children) {
483
619
  const normalizedProps = props === null || typeof props !== "object" || Array.isArray(props)
484
620
  ? {}
485
- : props;
621
+ : { ...props };
486
622
  const flatChildren = [];
487
623
  for (const c of children.flat(Infinity)) {
488
624
  if (c == null || c === false)
@@ -495,8 +631,7 @@ export function h(type, props = null, ...children) {
495
631
  }
496
632
  else if (c && typeof c === "object" && "type" in c) {
497
633
  if (c.type === Fragment) {
498
- const fragmentChildren = (c.props.children || []).filter((x) => x != null && x !== false);
499
- flatChildren.push(...fragmentChildren);
634
+ flatChildren.push(...(c.props.children || []).filter((x) => x != null && x !== false));
500
635
  }
501
636
  else {
502
637
  flatChildren.push(c);
@@ -510,41 +645,101 @@ export function h(type, props = null, ...children) {
510
645
  }
511
646
  }
512
647
  const key = normalizedProps.key ?? null;
513
- if (key !== undefined)
514
- delete normalizedProps.key;
515
- if (type === Fragment) {
648
+ delete normalizedProps.key;
649
+ if (type === Fragment)
516
650
  return { type: Fragment, props: { children: flatChildren }, key };
517
- }
518
- return {
519
- type,
520
- props: { ...normalizedProps, children: flatChildren },
521
- key,
522
- };
651
+ return { type, props: { ...normalizedProps, children: flatChildren }, key };
523
652
  }
524
653
  h.Fragment = ({ children }) => children || [];
525
654
  export const Fynix = h;
526
655
  Fynix.Fragment = h.Fragment;
527
- const componentInstances = new WeakMap();
528
- let rootRenderFn = null;
656
+ class ContextTracker {
657
+ constructor() {
658
+ this.contextRefs = new Map();
659
+ this.cleanup = null;
660
+ if (globalThis.FinalizationRegistry) {
661
+ this.cleanup = new globalThis.FinalizationRegistry((vnode) => {
662
+ this.contextRefs.delete(vnode);
663
+ });
664
+ }
665
+ }
666
+ set(vnode, ctx) {
667
+ if (globalThis.WeakRef) {
668
+ const ref = new globalThis.WeakRef(ctx);
669
+ this.contextRefs.set(vnode, ref);
670
+ if (this.cleanup) {
671
+ this.cleanup.register(ctx, vnode);
672
+ }
673
+ }
674
+ else {
675
+ this.contextRefs.set(vnode, ctx);
676
+ }
677
+ }
678
+ get(vnode) {
679
+ const ref = this.contextRefs.get(vnode);
680
+ if (!ref)
681
+ return undefined;
682
+ const ctx = ref.deref ? ref.deref() : ref;
683
+ if (!ctx && ref.deref) {
684
+ this.contextRefs.delete(vnode);
685
+ }
686
+ return ctx;
687
+ }
688
+ has(vnode) {
689
+ const ref = this.contextRefs.get(vnode);
690
+ if (!ref)
691
+ return false;
692
+ const ctx = ref.deref ? ref.deref() : ref;
693
+ if (!ctx && ref.deref) {
694
+ this.contextRefs.delete(vnode);
695
+ return false;
696
+ }
697
+ return true;
698
+ }
699
+ delete(vnode) {
700
+ this.contextRefs.delete(vnode);
701
+ }
702
+ clear() {
703
+ this.contextRefs.clear();
704
+ }
705
+ }
706
+ let componentInstances;
707
+ try {
708
+ if (typeof globalThis.WeakRef !== "undefined" &&
709
+ typeof globalThis.FinalizationRegistry !== "undefined") {
710
+ componentInstances = new ContextTracker();
711
+ }
712
+ else {
713
+ componentInstances = new WeakMap();
714
+ }
715
+ }
716
+ catch {
717
+ componentInstances = new WeakMap();
718
+ }
529
719
  const pendingRerenders = new WeakSet();
720
+ function makeContext(vnode, Component) {
721
+ return {
722
+ hooks: [],
723
+ hookIndex: 0,
724
+ effects: [],
725
+ cleanups: [],
726
+ _vnode: vnode,
727
+ _fiber: null,
728
+ _accessedStates: new Set(),
729
+ _subscriptions: new Set(),
730
+ _subscriptionCleanups: [],
731
+ version: 0,
732
+ rerender: null,
733
+ Component,
734
+ _isMounted: false,
735
+ _isRerendering: false,
736
+ _rerenderTimeout: null,
737
+ };
738
+ }
530
739
  function beginComponent(vnode) {
531
740
  let ctx = componentInstances.get(vnode);
532
741
  if (!ctx) {
533
- ctx = {
534
- hooks: [],
535
- hookIndex: 0,
536
- effects: [],
537
- cleanups: [],
538
- _vnode: vnode,
539
- _accessedStates: new Set(),
540
- _subscriptions: new Set(),
541
- _subscriptionCleanups: [],
542
- version: 0,
543
- rerender: null,
544
- Component: vnode.type,
545
- _isMounted: false,
546
- _isRerendering: false,
547
- };
742
+ ctx = makeContext(vnode, vnode.type);
548
743
  componentInstances.set(vnode, ctx);
549
744
  }
550
745
  ctx.hookIndex = 0;
@@ -559,66 +754,11 @@ function endComponent() {
559
754
  return;
560
755
  ctx._accessedStates.forEach((state) => {
561
756
  if (!ctx._subscriptions.has(state)) {
562
- if (!ctx.rerender) {
563
- let rerenderTimeout = null;
564
- ctx.rerender = function rerender() {
565
- if (ctx._isRerendering || pendingRerenders.has(ctx)) {
566
- return;
567
- }
568
- if (rerenderTimeout) {
569
- clearTimeout(rerenderTimeout);
570
- }
571
- rerenderTimeout = setTimeout(async () => {
572
- if (ctx._isRerendering || !ctx._isMounted)
573
- return;
574
- ctx._isRerendering = true;
575
- pendingRerenders.add(ctx);
576
- try {
577
- removeErrorOverlay();
578
- const vnode = ctx._vnode;
579
- const oldRendered = vnode._rendered;
580
- beginComponent(vnode);
581
- const result = ctx.Component(vnode.props);
582
- const newRendered = result instanceof Promise ? await result : result;
583
- endComponent();
584
- vnode._rendered = newRendered;
585
- const domNode = vnode._domNode;
586
- if (domNode && domNode.parentNode) {
587
- await patch(domNode.parentNode, newRendered, oldRendered);
588
- if (newRendered && typeof newRendered === "object") {
589
- vnode._domNode = newRendered._domNode;
590
- }
591
- ctx._isRerendering = false;
592
- pendingRerenders.delete(ctx);
593
- }
594
- else if (rootRenderFn) {
595
- await rootRenderFn();
596
- ctx._isRerendering = false;
597
- pendingRerenders.delete(ctx);
598
- }
599
- else {
600
- ctx._isRerendering = false;
601
- pendingRerenders.delete(ctx);
602
- }
603
- }
604
- catch (err) {
605
- console.error("[Fynix] Component rerender error:", err);
606
- showErrorOverlay(err);
607
- ctx._isRerendering = false;
608
- pendingRerenders.delete(ctx);
609
- }
610
- rerenderTimeout = null;
611
- }, 0);
612
- };
613
- }
757
+ if (!ctx.rerender)
758
+ ctx.rerender = createRerender(ctx);
614
759
  const unsub = state.subscribe(() => {
615
760
  if (ctx.rerender && ctx._isMounted) {
616
- if (typeof queueMicrotask === "function") {
617
- queueMicrotask(() => ctx.rerender());
618
- }
619
- else {
620
- setTimeout(ctx.rerender, 0);
621
- }
761
+ ctx.rerender();
622
762
  }
623
763
  });
624
764
  ctx._subscriptions.add(state);
@@ -627,735 +767,830 @@ function endComponent() {
627
767
  });
628
768
  setActiveContext(null);
629
769
  }
630
- export function renderComponent(Component, props = {}) {
631
- const vnode = { type: Component, props, key: null };
632
- const ctx = beginComponent(vnode);
633
- ctx.Component = Component;
634
- if (!ctx.rerender) {
635
- let rerenderTimeout = null;
636
- ctx.rerender = () => {
637
- if (ctx._isRerendering || pendingRerenders.has(ctx))
638
- return;
639
- if (rerenderTimeout) {
640
- clearTimeout(rerenderTimeout);
641
- }
642
- rerenderTimeout = setTimeout(async () => {
643
- if (ctx._isRerendering || !ctx._isMounted)
644
- return;
645
- ctx._isRerendering = true;
646
- pendingRerenders.add(ctx);
647
- try {
648
- removeErrorOverlay();
649
- const vnode = ctx._vnode;
650
- const oldRendered = vnode._rendered;
651
- beginComponent(vnode);
652
- const result = ctx.Component(vnode.props);
653
- const newRendered = result instanceof Promise ? await result : result;
654
- endComponent();
655
- vnode._rendered = newRendered;
656
- const domNode = vnode._domNode;
657
- if (domNode && domNode.parentNode) {
658
- await patch(domNode.parentNode, newRendered, oldRendered);
659
- if (newRendered && typeof newRendered === "object") {
660
- vnode._domNode = newRendered._domNode;
661
- }
662
- ctx._isRerendering = false;
663
- pendingRerenders.delete(ctx);
664
- }
665
- else if (rootRenderFn) {
666
- await rootRenderFn();
667
- ctx._isRerendering = false;
668
- pendingRerenders.delete(ctx);
669
- }
670
- else {
671
- ctx._isRerendering = false;
672
- pendingRerenders.delete(ctx);
673
- }
674
- }
675
- catch (err) {
676
- console.error("[Fynix] Component rerender error:", err);
677
- showErrorOverlay(err);
678
- ctx._isRerendering = false;
679
- pendingRerenders.delete(ctx);
680
- }
681
- rerenderTimeout = null;
682
- }, 0);
683
- };
684
- }
685
- try {
686
- removeErrorOverlay();
687
- const result = Component(props);
688
- if (result instanceof Promise) {
689
- const placeholderVNode = h("div", null, "Loading...");
690
- ctx._vnode = vnode;
691
- vnode._rendered = placeholderVNode;
692
- ctx._isMounted = true;
693
- result
694
- .then((resolvedVNode) => {
695
- vnode._rendered = resolvedVNode;
696
- if (ctx.rerender) {
697
- ctx.rerender();
698
- }
699
- })
700
- .catch((err) => {
701
- console.error("[Fynix] Async component error:", err);
702
- showErrorOverlay(err);
703
- });
704
- return placeholderVNode;
770
+ function createRerender(ctx) {
771
+ return function rerender() {
772
+ if (ctx._isRerendering || pendingRerenders.has(ctx))
773
+ return;
774
+ if (ctx._rerenderTimeout !== null) {
775
+ clearTimeout(ctx._rerenderTimeout);
776
+ ctx._rerenderTimeout = null;
705
777
  }
706
- ctx._vnode = vnode;
707
- vnode._rendered = result;
708
- ctx._isMounted = true;
709
- return result;
710
- }
711
- catch (err) {
712
- console.error("[Fynix] Component render error:", err);
713
- showErrorOverlay(err);
714
- return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
715
- }
716
- finally {
717
- endComponent();
718
- }
719
- }
720
- const delegatedEvents = new Map();
721
- let eventIdCounter = 1;
722
- function ensureDelegated(eventType) {
723
- if (delegatedEvents.has(eventType))
724
- return;
725
- delegatedEvents.set(eventType, new Map());
726
- document.addEventListener(eventType, (e) => {
727
- let cur = e.target;
728
- while (cur && cur !== document) {
729
- if (cur.nodeType !== 1)
730
- break;
731
- const el = cur;
732
- const eid = el._rest_eid;
733
- const map = delegatedEvents.get(eventType);
734
- if (eid != null && map?.has(eid)) {
735
- map.get(eid)(e);
778
+ ctx._rerenderTimeout = setTimeout(() => {
779
+ ctx._rerenderTimeout = null;
780
+ if (ctx._isRerendering || !ctx._isMounted)
736
781
  return;
782
+ if (ctx._fiber) {
783
+ fiberReconciler.scheduleUpdate(ctx._fiber, "high");
737
784
  }
738
- cur = cur.parentElement;
739
- }
740
- });
741
- }
742
- function registerDelegatedHandler(el, eventName, fn) {
743
- if (!fn || el.nodeType !== 1)
744
- return;
745
- const anyEl = el;
746
- const eid = anyEl._rest_eid ?? (anyEl._rest_eid = ++eventIdCounter);
747
- ensureDelegated(eventName);
748
- delegatedEvents.get(eventName).set(eid, (e) => {
749
- try {
750
- fn.call(el, e);
751
- }
752
- catch (err) {
753
- console.error("[Fynix] Event handler error:", err);
754
- showErrorOverlay(err);
755
- }
756
- });
757
- }
758
- function sanitizeText(text) {
759
- if (typeof text !== "string")
760
- return String(text);
761
- return text
762
- .replace(/[<>"'&]/g, (match) => {
763
- const entityMap = {
764
- "<": "&lt;",
765
- ">": "&gt;",
766
- '"': "&quot;",
767
- "'": "&#x27;",
768
- "&": "&amp;",
769
- };
770
- return entityMap[match] || match;
771
- })
772
- .replace(/javascript:/gi, "blocked:")
773
- .replace(/data:.*?base64/gi, "blocked:");
774
- }
775
- function sanitizeAttributeValue(value) {
776
- if (typeof value !== "string")
777
- return String(value);
778
- return value
779
- .replace(/["'<>]/g, (match) => {
780
- const entityMap = {
781
- '"': "&quot;",
782
- "'": "&#x27;",
783
- "<": "&lt;",
784
- ">": "&gt;",
785
- };
786
- return entityMap[match] || match;
787
- })
788
- .replace(/javascript:/gi, "blocked:")
789
- .replace(/on\w+=/gi, "blocked=");
790
- }
791
- function sanitizeErrorMessage(error) {
792
- if (!error)
793
- return "Unknown error";
794
- const message = error.message || error.toString() || "Unknown error";
795
- return sanitizeText(String(message)).slice(0, 200);
785
+ else if (ctx._vnode) {
786
+ console.warn("[Fynix] Rerender triggered before fiber assigned — skipping.");
787
+ }
788
+ }, 0);
789
+ };
796
790
  }
797
- function setProperty(el, key, value) {
798
- const k = key.toLowerCase();
799
- if (key === "r-class" || key === "rc") {
800
- if (typeof value === "string") {
801
- el.setAttribute("class", value);
802
- }
803
- else if (value && (value._isNixState || value._isRestState)) {
804
- el.setAttribute("class", value.value);
805
- const anyEl = el;
806
- if (!anyEl._fynixCleanups)
807
- anyEl._fynixCleanups = [];
808
- const unsub = value.subscribe(() => el.setAttribute("class", value.value));
809
- anyEl._fynixCleanups.push(unsub);
810
- }
811
- return;
812
- }
813
- if (key.startsWith("r-")) {
814
- registerDelegatedHandler(el, key.slice(2).toLowerCase(), value);
815
- return;
816
- }
817
- if (key === "style" && typeof value === "object") {
818
- Object.assign(el.style, value);
819
- return;
820
- }
821
- if (DANGEROUS_HTML_PROPS.has(key)) {
822
- console.error(`[Fynix] Security: ${key} is blocked for security reasons. Use textContent or children instead.`);
791
+ function updateProps(el, newProps = {}, oldProps = {}) {
792
+ if (!el || el.nodeType !== 1)
823
793
  return;
824
- }
825
- if ((key === "href" ||
826
- key === "src" ||
827
- key === "action" ||
828
- key === "formaction") &&
829
- typeof value === "string") {
830
- const normalizedValue = value.trim().toLowerCase();
831
- for (const protocol of DANGEROUS_PROTOCOLS) {
832
- if (normalizedValue.startsWith(protocol)) {
833
- console.error(`[Fynix] Security: ${protocol} protocol blocked in ${key}`);
834
- return;
794
+ for (const k of Object.keys(oldProps)) {
795
+ if (k === "children")
796
+ continue;
797
+ if (!(k in newProps)) {
798
+ if (k.startsWith("r-")) {
799
+ const eid = el._rest_eid;
800
+ if (eid)
801
+ delegatedEvents.get(k.slice(2).toLowerCase())?.delete(eid);
835
802
  }
836
- }
837
- if (normalizedValue.includes(":")) {
838
- const protocol = normalizedValue.split(":")[0] + ":";
839
- if (!SAFE_PROTOCOLS.has(protocol) &&
840
- !SAFE_PROTOCOLS.has(normalizedValue.charAt(0))) {
841
- console.error(`[Fynix] Security: Protocol '${protocol}' not in safe list for ${key}`);
842
- return;
803
+ else if (BOOLEAN_ATTRS.has(k.toLowerCase())) {
804
+ el.removeAttribute(k);
805
+ el[k] = false;
843
806
  }
844
- }
845
- if (normalizedValue.startsWith("data:")) {
846
- if (normalizedValue.includes("javascript") ||
847
- normalizedValue.includes("<script")) {
848
- console.error(`[Fynix] Security: Suspicious data: URL blocked in ${key}`);
849
- return;
807
+ else if (DOM_PROPERTIES.has(k)) {
808
+ el[k] = "";
809
+ }
810
+ else {
811
+ el.removeAttribute(k);
850
812
  }
851
813
  }
852
814
  }
853
- if (key.toLowerCase().startsWith("on") && key !== "open") {
854
- console.error(`[Fynix] Security: Inline event handler '${key}' blocked. Use r-${key.slice(2)} instead.`);
855
- return;
815
+ for (const [k, v] of Object.entries(newProps)) {
816
+ if (k === "children")
817
+ continue;
818
+ if (oldProps[k] !== v)
819
+ setProperty(el, k, v);
856
820
  }
857
- if (BOOLEAN_ATTRS.has(k)) {
858
- if (value) {
859
- el.setAttribute(k, "");
860
- el[k] = true;
861
- }
862
- else {
863
- el.removeAttribute(k);
864
- el[k] = false;
865
- }
866
- return;
821
+ }
822
+ function createDomElement(type, props) {
823
+ const el = document.createElement(type);
824
+ for (const [k, v] of Object.entries(props)) {
825
+ if (k !== "children")
826
+ setProperty(el, k, v);
867
827
  }
868
- if (DOM_PROPERTIES.has(key) && !DANGEROUS_HTML_PROPS.has(key)) {
869
- if (key === "textContent" || key === "innerText") {
870
- el[key] = sanitizeText(value ?? "");
871
- }
872
- else {
873
- el[key] = value ?? "";
874
- }
875
- return;
828
+ return el;
829
+ }
830
+ function unmountCtx(ctx) {
831
+ ctx._isMounted = false;
832
+ if (ctx._rerenderTimeout !== null) {
833
+ clearTimeout(ctx._rerenderTimeout);
834
+ ctx._rerenderTimeout = null;
876
835
  }
877
- if (key.startsWith("data-") || key.startsWith("aria-")) {
878
- if (value != null && value !== false) {
879
- el.setAttribute(key, sanitizeAttributeValue(String(value)));
836
+ ctx._subscriptionCleanups.forEach((u) => {
837
+ try {
838
+ u();
880
839
  }
881
- else {
882
- el.removeAttribute(key);
840
+ catch { }
841
+ });
842
+ ctx.cleanups.forEach((c) => {
843
+ try {
844
+ c?.();
883
845
  }
884
- return;
885
- }
886
- if (value != null && value !== false) {
887
- el.setAttribute(key, value);
888
- }
846
+ catch { }
847
+ });
848
+ ctx._subscriptions.clear();
849
+ ctx._accessedStates.clear();
850
+ ctx._subscriptionCleanups = [];
851
+ ctx.cleanups = [];
852
+ ctx.hooks = [];
853
+ ctx.effects = [];
854
+ ctx.rerender = null;
855
+ ctx._vnode = null;
856
+ ctx._fiber = null;
889
857
  }
890
- async function createDom(vnode, existing = null) {
891
- if (vnode == null) {
892
- return document.createTextNode("");
893
- }
894
- if (typeof vnode === "string" || typeof vnode === "number") {
895
- return document.createTextNode(String(vnode));
896
- }
897
- if (vnode instanceof Promise) {
898
- const placeholder = document.createTextNode("Loading...");
899
- vnode
900
- .then(async (resolved) => {
858
+ function removeDomCleanups(node) {
859
+ const any = node;
860
+ const eid = any._rest_eid;
861
+ if (eid)
862
+ delegatedEvents.forEach((m) => m.delete(eid));
863
+ if (any._fynixCleanups) {
864
+ any._fynixCleanups.forEach((fn) => {
901
865
  try {
902
- const dom = await createDom(resolved);
903
- if (placeholder.parentNode) {
904
- placeholder.replaceWith(dom);
905
- }
906
- }
907
- catch (err) {
908
- console.error("[Fynix] Async component error:", err);
909
- if (placeholder.parentNode) {
910
- placeholder.textContent = "Error loading component";
911
- }
912
- }
913
- })
914
- .catch((err) => {
915
- console.error("[Fynix] Async component promise error:", err);
916
- if (placeholder.parentNode) {
917
- placeholder.textContent = "Error loading async component";
866
+ fn();
918
867
  }
868
+ catch { }
919
869
  });
920
- return placeholder;
921
- }
922
- const vnodeObj = vnode;
923
- if (vnodeObj.type === TEXT) {
924
- const textNode = existing || document.createTextNode(vnodeObj.props.nodeValue ?? "");
925
- vnodeObj._domNode = textNode;
926
- return textNode;
927
- }
928
- if (vnodeObj.type === Fragment) {
929
- const frag = document.createDocumentFragment();
930
- for (const child of vnodeObj.props?.children || []) {
931
- frag.appendChild(await createDom(child));
932
- }
933
- vnodeObj._domNode = frag;
934
- return frag;
935
- }
936
- if (typeof vnodeObj.type === "function") {
937
- const rendered = await renderMaybeAsyncComponent(vnodeObj.type, vnodeObj.props, vnodeObj);
938
- vnodeObj._rendered = rendered;
939
- const dom = await createDom(rendered);
940
- vnodeObj._domNode = dom;
941
- return dom;
942
- }
943
- const el = existing || document.createElement(vnodeObj.type);
944
- for (const [k, v] of Object.entries(vnodeObj.props || {})) {
945
- if (k !== "children") {
946
- setProperty(el, k, v);
947
- }
870
+ any._fynixCleanups = null;
948
871
  }
949
- for (const child of vnodeObj.props?.children || []) {
950
- el.appendChild(await createDom(child));
951
- }
952
- vnodeObj._domNode = el;
953
- return el;
954
872
  }
955
- async function renderMaybeAsyncComponent(Component, props, vnode) {
956
- const ctx = beginComponent(vnode);
957
- removeErrorOverlay();
958
- try {
959
- const result = await Component(props);
960
- ctx._vnode = vnode;
961
- vnode._rendered = result;
962
- ctx._isMounted = true;
963
- endComponent();
964
- return result ?? null;
873
+ class FiberReconciler {
874
+ constructor() {
875
+ this.wipRoot = null;
876
+ this.wipEntry = null;
877
+ this.nextWork = null;
878
+ this.deletions = [];
965
879
  }
966
- catch (err) {
967
- console.error("[Fynix] async render error:", err);
968
- showErrorOverlay(err);
969
- ctx._isMounted = false;
970
- endComponent();
971
- return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
880
+ mountRoot(vnode, container) {
881
+ const rootFiber = this.vnodeToFiber(vnode, null, null);
882
+ rootFiber._domNode = container;
883
+ this.wipRoot = rootFiber;
884
+ this.wipEntry = null;
885
+ this.nextWork = rootFiber;
886
+ this.deletions = [];
887
+ this.scheduleRender("high");
888
+ }
889
+ scheduleUpdate(fiber, priority = "normal") {
890
+ const wip = this.cloneFiber(fiber);
891
+ wip.alternate = fiber;
892
+ let root = wip;
893
+ while (root.parent)
894
+ root = root.parent;
895
+ this.wipRoot = root;
896
+ this.wipEntry = wip;
897
+ this.nextWork = wip;
898
+ this.deletions = [];
899
+ this.scheduleRender(priority);
972
900
  }
973
- }
974
- export async function patch(parent, newVNode, oldVNode) {
975
- if (!(parent instanceof Node)) {
976
- console.error("[Fynix] patch() expects a DOM Node, got:", typeof parent, parent);
977
- return;
901
+ scheduleRender(priority) {
902
+ scheduler.schedule({
903
+ id: "",
904
+ type: "layout",
905
+ priority,
906
+ callback: () => this.workLoop(priority === "high" ? 16 : 5),
907
+ timestamp: performance.now(),
908
+ }, priority);
978
909
  }
979
- if (!newVNode && !oldVNode)
980
- return;
981
- if (!newVNode && oldVNode) {
982
- const domNode = oldVNode._domNode;
983
- if (domNode?.parentNode) {
984
- domNode.parentNode.removeChild(domNode);
910
+ workLoop(deadline) {
911
+ perfMark("workloop-start");
912
+ const t0 = performance.now();
913
+ while (this.nextWork && performance.now() - t0 < deadline) {
914
+ this.nextWork = this.performWork(this.nextWork);
985
915
  }
986
- unmountVNode(oldVNode);
987
- return;
988
- }
989
- if (newVNode && !oldVNode) {
990
- const newDom = await createDom(newVNode);
991
- if (newDom instanceof Node) {
992
- parent.appendChild(newDom);
916
+ if (!this.nextWork && this.wipRoot) {
917
+ perfMark("render-complete");
918
+ this.commitRoot();
919
+ perfMark("commit-complete");
920
+ if (perfConfig.enabled) {
921
+ const renderTime = perfMeasure("render", "workloop-start", "render-complete");
922
+ const commitTime = perfMeasure("commit", "render-complete", "commit-complete");
923
+ if (perfConfig.slowRenderThreshold &&
924
+ renderTime + commitTime > perfConfig.slowRenderThreshold) {
925
+ console.warn(`[Fynix] Slow render: ${(renderTime + commitTime).toFixed(2)}ms`);
926
+ }
927
+ if (perfConfig.onMetrics) {
928
+ perfConfig.onMetrics({
929
+ renderTime,
930
+ commitTime,
931
+ totalTime: renderTime + commitTime,
932
+ updateCount: 0,
933
+ fiberCount: 0,
934
+ timestamp: performance.now(),
935
+ });
936
+ }
937
+ }
938
+ }
939
+ else if (this.nextWork) {
940
+ this.scheduleRender("normal");
993
941
  }
994
- return;
995
942
  }
996
- const newIsPrimitive = typeof newVNode === "string" || typeof newVNode === "number";
997
- const oldIsPrimitive = typeof oldVNode === "string" || typeof oldVNode === "number";
998
- if (newIsPrimitive || oldIsPrimitive) {
999
- if (newIsPrimitive &&
1000
- oldIsPrimitive &&
1001
- String(newVNode) === String(oldVNode))
1002
- return;
1003
- const newDom = await createDom(newVNode);
1004
- const oldDom = oldVNode?._domNode || parent.firstChild;
1005
- if (oldDom?.parentNode && newDom instanceof Node) {
1006
- oldDom.parentNode.replaceChild(newDom, oldDom);
943
+ performWork(fiber) {
944
+ if (typeof fiber.type === "function") {
945
+ this.updateComponentFiber(fiber);
1007
946
  }
1008
- if (oldVNode && typeof oldVNode === "object") {
1009
- unmountVNode(oldVNode);
947
+ else {
948
+ this.updateHostFiber(fiber);
1010
949
  }
1011
- return;
1012
- }
1013
- const newVN = newVNode;
1014
- const oldVN = oldVNode;
1015
- const newType = newVN.type;
1016
- const oldType = oldVN.type;
1017
- if (newType !== oldType) {
1018
- const newDom = await createDom(newVN);
1019
- const oldDom = oldVN._domNode;
1020
- if (oldDom?.parentNode && newDom instanceof Node) {
1021
- oldDom.parentNode.replaceChild(newDom, oldDom);
1022
- }
1023
- unmountVNode(oldVN);
1024
- return;
1025
- }
1026
- if (newType === TEXT) {
1027
- const oldDom = oldVN._domNode;
1028
- const newText = newVN.props.nodeValue ?? "";
1029
- const oldText = oldVN.props.nodeValue ?? "";
1030
- if (newText !== oldText && oldDom) {
1031
- oldDom.nodeValue = newText;
950
+ if (fiber.child)
951
+ return fiber.child;
952
+ let f = fiber;
953
+ while (f) {
954
+ if (f.sibling)
955
+ return f.sibling;
956
+ f = f.parent;
1032
957
  }
1033
- newVN._domNode = oldDom;
1034
- return;
1035
- }
1036
- if (newType === Fragment) {
1037
- const newChildren = newVN.props?.children || [];
1038
- const oldChildren = oldVN.props?.children || [];
1039
- await patchChildren(parent, newChildren, oldChildren);
1040
- newVN._domNode = oldVN._domNode;
1041
- return;
958
+ return null;
1042
959
  }
1043
- if (typeof newType === "function") {
1044
- const oldCtx = componentInstances.get(oldVN);
1045
- if (oldCtx && newType === oldType) {
1046
- componentInstances.delete(oldVN);
1047
- componentInstances.set(newVN, oldCtx);
1048
- oldCtx._vnode = newVN;
1049
- beginComponent(newVN);
1050
- const rendered = await oldCtx.Component(newVN.props);
1051
- endComponent();
1052
- newVN._rendered = rendered;
1053
- const oldRendered = oldVN._rendered;
1054
- const oldDom = oldVN._domNode;
1055
- if (oldDom?.parentNode instanceof Node) {
1056
- await patch(oldDom.parentNode, rendered, oldRendered);
1057
- newVN._domNode = rendered?._domNode || oldDom;
960
+ updateComponentFiber(fiber) {
961
+ const vnode = fiber._vnode;
962
+ let ctx = componentInstances.get(vnode);
963
+ if (!ctx && fiber.alternate?._vnode) {
964
+ ctx = componentInstances.get(fiber.alternate._vnode);
965
+ if (ctx) {
966
+ componentInstances.delete(fiber.alternate._vnode);
967
+ componentInstances.set(vnode, ctx);
968
+ ctx._vnode = vnode;
1058
969
  }
1059
970
  }
1060
- else {
1061
- const rendered = await renderMaybeAsyncComponent(newType, newVN.props, newVN);
1062
- newVN._rendered = rendered;
1063
- const oldRendered = oldVN._rendered;
1064
- const oldDom = oldVN._domNode;
1065
- if (oldDom?.parentNode instanceof Node) {
1066
- await patch(oldDom.parentNode, rendered, oldRendered);
1067
- newVN._domNode = rendered?._domNode || oldDom;
971
+ if (!ctx) {
972
+ ctx = makeContext(vnode, fiber.type);
973
+ componentInstances.set(vnode, ctx);
974
+ }
975
+ ctx._fiber = fiber;
976
+ fiber.ctx = ctx;
977
+ ctx.hookIndex = 0;
978
+ ctx._accessedStates.clear();
979
+ setActiveContext(ctx);
980
+ ctx.version++;
981
+ let rendered = null;
982
+ try {
983
+ removeErrorOverlay();
984
+ const result = fiber.type(fiber.props);
985
+ if (result instanceof Promise) {
986
+ rendered = h("div", null, "Loading...");
987
+ result
988
+ .then((resolved) => {
989
+ vnode._rendered = resolved;
990
+ if (ctx.rerender)
991
+ ctx.rerender();
992
+ })
993
+ .catch((err) => publishAsyncError(err instanceof Error ? err : new Error(String(err))));
1068
994
  }
1069
995
  else {
1070
- const newDom = await createDom(rendered);
1071
- if (parent && newDom instanceof Node) {
1072
- parent.appendChild(newDom);
1073
- }
1074
- newVN._domNode = newDom;
1075
- }
1076
- if (oldCtx && newType !== oldType) {
1077
- unmountVNode(oldVN);
996
+ rendered = result;
1078
997
  }
1079
998
  }
1080
- return;
1081
- }
1082
- const el = oldVN._domNode;
1083
- if (!el || el.nodeType !== 1) {
1084
- const newDom = await createDom(newVN);
1085
- if (parent && newDom instanceof Node) {
1086
- parent.appendChild(newDom);
999
+ catch (err) {
1000
+ const error = err instanceof Error ? err : new Error(String(err));
1001
+ if (errorConfig.logToConsole) {
1002
+ console.error("[Fynix] Component render error:", error);
1003
+ }
1004
+ const handled = errorConfig.onRenderError?.(error, fiber.type);
1005
+ if (!handled && errorConfig.showOverlay) {
1006
+ showErrorOverlay(error);
1007
+ }
1008
+ rendered = h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(error)}`);
1009
+ }
1010
+ ctx._accessedStates.forEach((state) => {
1011
+ if (!ctx._subscriptions.has(state)) {
1012
+ if (!ctx.rerender)
1013
+ ctx.rerender = createRerender(ctx);
1014
+ const unsub = state.subscribe(() => {
1015
+ if (ctx.rerender && ctx._isMounted) {
1016
+ ctx.rerender();
1017
+ }
1018
+ });
1019
+ ctx._subscriptions.add(state);
1020
+ ctx._subscriptionCleanups.push(unsub);
1021
+ }
1022
+ });
1023
+ setActiveContext(null);
1024
+ ctx._isMounted = true;
1025
+ vnode._rendered = rendered;
1026
+ if (!ctx.rerender)
1027
+ ctx.rerender = createRerender(ctx);
1028
+ const children = rendered ? [rendered] : [];
1029
+ this.reconcileChildren(fiber, children);
1030
+ }
1031
+ updateHostFiber(fiber) {
1032
+ if (fiber.type === TEXT ||
1033
+ (typeof fiber.type === "symbol" &&
1034
+ fiber.type.description?.toLowerCase() === "text")) {
1035
+ if (!fiber._domNode) {
1036
+ fiber._domNode = document.createTextNode(String(fiber.props.nodeValue ?? ""));
1037
+ }
1038
+ else if (fiber.alternate) {
1039
+ const oldText = fiber.alternate.props.nodeValue ?? "";
1040
+ const newText = fiber.props.nodeValue ?? "";
1041
+ if (oldText !== newText)
1042
+ fiber._domNode.nodeValue = String(newText);
1043
+ }
1044
+ return;
1087
1045
  }
1088
- unmountVNode(oldVN);
1089
- return;
1090
- }
1091
- updateProps(el, newVN.props, oldVN.props);
1092
- newVN._domNode = el;
1093
- const newChildren = newVN.props?.children || [];
1094
- const oldChildren = oldVN.props?.children || [];
1095
- await patchChildren(el, newChildren, oldChildren);
1096
- }
1097
- async function patchChildren(parent, newChildren, oldChildren) {
1098
- if (!(parent instanceof Node))
1099
- return;
1100
- const hasKeys = newChildren.some((c) => c?.key != null) ||
1101
- oldChildren.some((c) => c?.key != null);
1102
- if (!hasKeys) {
1103
- const maxLen = Math.max(newChildren.length, oldChildren.length);
1104
- for (let i = 0; i < maxLen; i++) {
1105
- const newChild = newChildren[i];
1106
- const oldChild = oldChildren[i];
1107
- if (i >= newChildren.length) {
1108
- const dom = oldChild?._domNode;
1109
- if (dom?.parentNode) {
1110
- dom.parentNode.removeChild(dom);
1046
+ if (fiber.type === Fragment ||
1047
+ (typeof fiber.type === "symbol" &&
1048
+ fiber.type.description?.toLowerCase() === "fragment")) {
1049
+ if (!fiber._domNode) {
1050
+ const start = document.createTextNode("");
1051
+ const end = document.createTextNode("");
1052
+ fiber._domNode = start;
1053
+ const vnode = fiber._vnode;
1054
+ if (vnode) {
1055
+ vnode._fragmentStart = start;
1056
+ vnode._fragmentEnd = end;
1057
+ fiber._fragmentEnd = end;
1111
1058
  }
1112
- unmountVNode(oldChild);
1113
- }
1114
- else if (i >= oldChildren.length) {
1115
- const newDom = await createDom(newChild);
1116
- if (newDom instanceof Node) {
1117
- parent.appendChild(newDom);
1059
+ else {
1060
+ console.warn("[FynixReconciler] Fragment fiber created without backing VNode. This may cause cleanup issues.");
1118
1061
  }
1119
1062
  }
1120
- else {
1121
- await patch(parent, newChild, oldChild);
1063
+ this.reconcileChildren(fiber, fiber.props.children || []);
1064
+ return;
1065
+ }
1066
+ if (typeof fiber.type === "string") {
1067
+ if (!fiber._domNode) {
1068
+ fiber._domNode = createDomElement(fiber.type, fiber.props);
1069
+ fiber.effectTag = "PLACEMENT";
1122
1070
  }
1071
+ else if (fiber.alternate) {
1072
+ updateProps(fiber._domNode, fiber.props, fiber.alternate.props);
1073
+ fiber.effectTag = "UPDATE";
1074
+ }
1075
+ this.reconcileChildren(fiber, fiber.props.children || []);
1123
1076
  }
1124
- return;
1125
1077
  }
1126
- const oldKeyMap = new Map();
1127
- oldChildren.forEach((child) => {
1128
- if (child?.key != null) {
1129
- oldKeyMap.set(child.key, child);
1078
+ reconcileChildren(wipFiber, elements) {
1079
+ const hasKeys = elements.some((e) => e?.key != null);
1080
+ const oldMap = new Map();
1081
+ const oldByIndex = [];
1082
+ let oldFiber = wipFiber.alternate?.child ?? null;
1083
+ let idx = 0;
1084
+ while (oldFiber) {
1085
+ if (hasKeys && oldFiber.key != null)
1086
+ oldMap.set(oldFiber.key, oldFiber);
1087
+ else
1088
+ oldByIndex[idx] = oldFiber;
1089
+ oldFiber = oldFiber.sibling;
1090
+ idx++;
1091
+ }
1092
+ if (hasKeys) {
1093
+ const newKeys = new Set(elements.filter((e) => e?.key != null).map((e) => e.key));
1094
+ oldMap.forEach((f, k) => {
1095
+ if (!newKeys.has(k)) {
1096
+ f.effectTag = "DELETION";
1097
+ this.deletions.push(f);
1098
+ }
1099
+ });
1130
1100
  }
1131
- });
1132
- const newKeySet = new Set(newChildren.filter((c) => c?.key != null).map((c) => c.key));
1133
- oldChildren.forEach((oldChild) => {
1134
- if (oldChild?.key != null && !newKeySet.has(oldChild.key)) {
1135
- const dom = oldChild._domNode;
1136
- if (dom?.parentNode) {
1137
- dom.parentNode.removeChild(dom);
1101
+ let prevSibling = null;
1102
+ for (let i = 0; i < elements.length; i++) {
1103
+ const el = elements[i];
1104
+ if (el == null)
1105
+ continue;
1106
+ const matchFiber = hasKeys && el.key != null
1107
+ ? (oldMap.get(el.key) ?? null)
1108
+ : (oldByIndex[i] ?? null);
1109
+ let newFiber;
1110
+ if (matchFiber && matchFiber.type === el.type) {
1111
+ newFiber = this.cloneFiber(matchFiber);
1112
+ newFiber.props = el.props;
1113
+ newFiber.key = el.key;
1114
+ newFiber.alternate = matchFiber;
1115
+ newFiber.parent = wipFiber;
1116
+ newFiber._vnode = el;
1117
+ el._fiber = newFiber;
1118
+ newFiber.effectTag = typeof el.type === "function" ? null : "UPDATE";
1138
1119
  }
1139
- unmountVNode(oldChild);
1140
- }
1141
- });
1142
- for (let i = 0; i < newChildren.length; i++) {
1143
- const newChild = newChildren[i];
1144
- const key = newChild?.key;
1145
- if (key != null && oldKeyMap.has(key)) {
1146
- const oldChild = oldKeyMap.get(key);
1147
- const oldDom = oldChild._domNode;
1148
- const childNodes = Array.from(parent.childNodes);
1149
- const currentPos = childNodes.indexOf(oldDom);
1150
- const desiredPos = i;
1151
- if (currentPos !== desiredPos) {
1152
- const refNode = childNodes[desiredPos] || null;
1153
- if (oldDom && oldDom.parentNode === parent) {
1154
- parent.insertBefore(oldDom, refNode);
1120
+ else {
1121
+ if (matchFiber) {
1122
+ matchFiber.effectTag = "DELETION";
1123
+ this.deletions.push(matchFiber);
1155
1124
  }
1125
+ newFiber = this.vnodeToFiber(el, wipFiber, null);
1126
+ newFiber.effectTag = "PLACEMENT";
1156
1127
  }
1157
- await patch(parent, newChild, oldChild);
1128
+ if (i === 0)
1129
+ wipFiber.child = newFiber;
1130
+ else if (prevSibling)
1131
+ prevSibling.sibling = newFiber;
1132
+ prevSibling = newFiber;
1158
1133
  }
1159
- else {
1160
- const newDom = await createDom(newChild);
1161
- if (newDom instanceof Node) {
1162
- const childNodes = Array.from(parent.childNodes);
1163
- const refNode = childNodes[i] || null;
1164
- parent.insertBefore(newDom, refNode);
1134
+ if (!hasKeys) {
1135
+ for (let j = elements.length; j < oldByIndex.length; j++) {
1136
+ const dead = oldByIndex[j];
1137
+ if (dead) {
1138
+ dead.effectTag = "DELETION";
1139
+ this.deletions.push(dead);
1140
+ }
1165
1141
  }
1166
1142
  }
1167
1143
  }
1168
- }
1169
- function unmountVNode(vnode) {
1170
- if (!vnode)
1171
- return;
1172
- if (vnode._cleanup && typeof vnode._cleanup === "function") {
1173
- try {
1174
- vnode._cleanup();
1144
+ commitRoot() {
1145
+ if (!this.wipRoot)
1146
+ return;
1147
+ this.deletions.forEach((f) => this.commitDeletion(f));
1148
+ this.deletions = [];
1149
+ if (this.wipEntry) {
1150
+ this.commitWork(this.wipEntry);
1175
1151
  }
1176
- catch (e) {
1177
- console.error("[Fynix] Text vnode cleanup error:", e);
1152
+ else if (this.wipRoot.child) {
1153
+ this.commitWork(this.wipRoot.child);
1178
1154
  }
1179
- vnode._cleanup = null;
1155
+ this.wipRoot = null;
1156
+ this.wipEntry = null;
1157
+ this.nextWork = null;
1180
1158
  }
1181
- if (typeof vnode.type === "function") {
1182
- const ctx = componentInstances.get(vnode);
1183
- if (ctx) {
1184
- ctx._isMounted = false;
1185
- ctx._subscriptionCleanups.forEach((u) => {
1186
- try {
1187
- u();
1188
- }
1189
- catch (e) {
1190
- console.error("[Fynix] Cleanup error:", e);
1191
- }
1192
- });
1193
- ctx.cleanups.forEach((c) => {
1194
- try {
1195
- c?.();
1159
+ commitWork(fiber) {
1160
+ if (!fiber)
1161
+ return;
1162
+ const domParent = this.findDomParent(fiber);
1163
+ if (fiber.effectTag === "PLACEMENT" && fiber._domNode) {
1164
+ if (!domParent) {
1165
+ console.warn("[FynixReconciler] No valid DOM parent found for fiber; skipping insertion");
1166
+ }
1167
+ else {
1168
+ const validContainers = domParent.nodeType === Node.ELEMENT_NODE ||
1169
+ domParent.nodeType === Node.DOCUMENT_NODE ||
1170
+ domParent.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
1171
+ if (!validContainers) {
1172
+ console.warn("[FynixReconciler] Parent node type (" +
1173
+ domParent.nodeType +
1174
+ ") cannot accept children; skipping insertion");
1196
1175
  }
1197
- catch (e) {
1198
- console.error("[Fynix] Effect cleanup error:", e);
1176
+ else {
1177
+ try {
1178
+ if (fiber._domNode.parentNode &&
1179
+ fiber._domNode.parentNode !== domParent) {
1180
+ try {
1181
+ fiber._domNode.parentNode.removeChild(fiber._domNode);
1182
+ }
1183
+ catch (e) {
1184
+ }
1185
+ }
1186
+ const refNode = this.findNextDomSibling(fiber);
1187
+ if (refNode && refNode.parentNode === domParent) {
1188
+ try {
1189
+ domParent.insertBefore(fiber._domNode, refNode);
1190
+ }
1191
+ catch (insertErr) {
1192
+ try {
1193
+ domParent.appendChild(fiber._domNode);
1194
+ }
1195
+ catch (appendErr) {
1196
+ console.error("[FynixReconciler] Failed to insert node:", appendErr);
1197
+ }
1198
+ }
1199
+ }
1200
+ else {
1201
+ try {
1202
+ domParent.appendChild(fiber._domNode);
1203
+ }
1204
+ catch (appendErr) {
1205
+ console.error("[FynixReconciler] Failed to append node:", appendErr);
1206
+ }
1207
+ }
1208
+ }
1209
+ catch (e) {
1210
+ console.error("[FynixReconciler] Unexpected error during node insertion:", e);
1211
+ }
1199
1212
  }
1200
- });
1201
- ctx._subscriptions.clear();
1202
- ctx._accessedStates.clear();
1203
- ctx._subscriptionCleanups = [];
1204
- ctx.cleanups = [];
1205
- ctx.hooks = [];
1206
- ctx.effects = [];
1207
- ctx.rerender = null;
1208
- ctx._vnode = null;
1209
- componentInstances.delete(vnode);
1210
- pendingRerenders.delete(ctx);
1211
- }
1212
- unmountVNode(vnode._rendered);
1213
- return;
1214
- }
1215
- if (vnode._domNode && vnode._domNode.nodeType === 1) {
1216
- const anyNode = vnode._domNode;
1217
- const eid = anyNode._rest_eid;
1218
- if (eid) {
1219
- delegatedEvents.forEach((map) => map.delete(eid));
1213
+ }
1220
1214
  }
1221
- if (anyNode._fynixCleanups) {
1222
- anyNode._fynixCleanups.forEach((cleanup) => {
1223
- try {
1224
- cleanup();
1225
- }
1226
- catch (e) {
1227
- console.error("[Fynix] Element cleanup error:", e);
1228
- }
1229
- });
1230
- anyNode._fynixCleanups = null;
1215
+ else if (fiber.effectTag === "UPDATE") {
1216
+ if (fiber._domNode && typeof fiber.type === "string" && fiber.alternate) {
1217
+ updateProps(fiber._domNode, fiber.props, fiber.alternate.props);
1218
+ }
1231
1219
  }
1220
+ this.runEffects(fiber);
1221
+ fiber.effectTag = null;
1222
+ this.commitWork(fiber.child);
1223
+ this.commitWork(fiber.sibling);
1232
1224
  }
1233
- if (vnode.props?.children) {
1234
- vnode.props.children.forEach((c) => unmountVNode(c));
1225
+ commitDeletion(fiber) {
1226
+ this.unmountFiber(fiber);
1227
+ const domNode = this.findNearestDom(fiber);
1228
+ if (domNode?.parentNode)
1229
+ domNode.parentNode.removeChild(domNode);
1235
1230
  }
1236
- vnode._domNode = null;
1237
- vnode._rendered = null;
1238
- }
1239
- function updateProps(el, newProps = {}, oldProps = {}) {
1240
- if (!el || el.nodeType !== 1)
1241
- return;
1242
- for (const k of Object.keys(oldProps)) {
1243
- if (k === "children")
1244
- continue;
1245
- if (!(k in newProps)) {
1246
- if (k.startsWith("r-")) {
1247
- const anyEl = el;
1248
- const eid = anyEl._rest_eid;
1249
- if (eid && delegatedEvents.has(k.slice(2).toLowerCase())) {
1250
- delegatedEvents.get(k.slice(2).toLowerCase()).delete(eid);
1231
+ unmountFiber(fiber) {
1232
+ if (!fiber)
1233
+ return;
1234
+ const stack = [fiber];
1235
+ while (stack.length > 0) {
1236
+ const current = stack.pop();
1237
+ if (!current)
1238
+ continue;
1239
+ if (current.sibling)
1240
+ stack.push(current.sibling);
1241
+ if (current.child)
1242
+ stack.push(current.child);
1243
+ try {
1244
+ if (current.ctx) {
1245
+ unmountCtx(current.ctx);
1246
+ if (current._vnode)
1247
+ componentInstances.delete(current._vnode);
1251
1248
  }
1249
+ if (current._domNode?.nodeType === 1)
1250
+ removeDomCleanups(current._domNode);
1252
1251
  }
1253
- else if (BOOLEAN_ATTRS.has(k.toLowerCase())) {
1254
- el.removeAttribute(k);
1255
- el[k] = false;
1252
+ catch (e) {
1253
+ console.error("[FynixReconciler] Error unmounting fiber:", e);
1256
1254
  }
1257
- else if (DOM_PROPERTIES.has(k)) {
1258
- el[k] = "";
1255
+ }
1256
+ }
1257
+ runEffects(fiber) {
1258
+ if (!fiber.ctx)
1259
+ return;
1260
+ const ctx = fiber.ctx;
1261
+ ctx.effects.forEach((effect) => {
1262
+ try {
1263
+ const cleanup = effect();
1264
+ if (typeof cleanup === "function")
1265
+ ctx.cleanups.push(cleanup);
1259
1266
  }
1260
- else {
1261
- el.removeAttribute(k);
1267
+ catch (e) {
1268
+ console.error("[Fynix] Effect error:", e);
1262
1269
  }
1270
+ });
1271
+ ctx.effects = [];
1272
+ }
1273
+ vnodeToFiber(vnode, parent, alternate) {
1274
+ const fiber = {
1275
+ type: vnode.type,
1276
+ props: vnode.props,
1277
+ key: vnode.key,
1278
+ child: null,
1279
+ sibling: null,
1280
+ parent,
1281
+ alternate,
1282
+ effectTag: null,
1283
+ updatePriority: "normal",
1284
+ _domNode: vnode._domNode ?? null,
1285
+ ctx: null,
1286
+ _vnode: vnode,
1287
+ };
1288
+ vnode._fiber = fiber;
1289
+ return fiber;
1290
+ }
1291
+ cloneFiber(fiber) {
1292
+ return {
1293
+ type: fiber.type,
1294
+ props: fiber.props,
1295
+ key: fiber.key,
1296
+ child: null,
1297
+ sibling: null,
1298
+ parent: fiber.parent,
1299
+ alternate: fiber,
1300
+ effectTag: null,
1301
+ updatePriority: fiber.updatePriority,
1302
+ _domNode: fiber._domNode,
1303
+ ctx: fiber.ctx,
1304
+ _vnode: fiber._vnode,
1305
+ };
1306
+ }
1307
+ findDomParent(fiber) {
1308
+ let p = fiber.parent;
1309
+ while (p && !p._domNode)
1310
+ p = p.parent;
1311
+ let domNode = p?._domNode ?? null;
1312
+ while (domNode &&
1313
+ (domNode.nodeType === Node.TEXT_NODE ||
1314
+ domNode.nodeType === Node.COMMENT_NODE ||
1315
+ domNode.nodeType === Node.PROCESSING_INSTRUCTION_NODE)) {
1316
+ domNode = domNode.parentNode;
1317
+ }
1318
+ return domNode;
1319
+ }
1320
+ findNearestDom(fiber) {
1321
+ if (fiber._domNode)
1322
+ return fiber._domNode;
1323
+ if (fiber.child)
1324
+ return this.findNearestDom(fiber.child);
1325
+ return null;
1326
+ }
1327
+ findNextDomSibling(fiber) {
1328
+ let sib = fiber.sibling;
1329
+ while (sib) {
1330
+ const dom = this.findNearestDom(sib);
1331
+ if (dom)
1332
+ return dom;
1333
+ sib = sib.sibling;
1263
1334
  }
1335
+ return null;
1264
1336
  }
1265
- for (const [k, v] of Object.entries(newProps)) {
1266
- if (k === "children")
1267
- continue;
1268
- if (oldProps[k] !== v) {
1269
- setProperty(el, k, v);
1337
+ }
1338
+ const fiberReconciler = new FiberReconciler();
1339
+ export const __debug__ = {
1340
+ getSchedulerState: () => scheduler.getState(),
1341
+ getQueueMetrics: () => scheduler.getQueueMetrics(),
1342
+ getFiberReconciler: () => fiberReconciler,
1343
+ getErrorConfig: () => getErrorConfig(),
1344
+ getPerfConfig: () => getPerfConfig(),
1345
+ collectGarbage: () => {
1346
+ if (typeof global !== "undefined" && global.gc) {
1347
+ global.gc();
1348
+ }
1349
+ else if (typeof window !== "undefined" && window.gc) {
1350
+ window.gc();
1351
+ }
1352
+ },
1353
+ clearSchedulerQueue: () => scheduler.clearQueue(),
1354
+ getAsyncContext: () => ({
1355
+ currentBatchStore: getCurrentBatchStore(),
1356
+ batchingStorageAvailable: batchingStorage !== null,
1357
+ }),
1358
+ };
1359
+ class HierarchicalStore {
1360
+ constructor() {
1361
+ this.root = new Map();
1362
+ this.selectorCache = new Map();
1363
+ this.stateSnapshot = {};
1364
+ }
1365
+ select(selector) {
1366
+ const k = selector.toString();
1367
+ if (this.selectorCache.has(k))
1368
+ return this.selectorCache.get(k);
1369
+ const r = selector(this.stateSnapshot);
1370
+ this.selectorCache.set(k, r);
1371
+ return r;
1372
+ }
1373
+ optimisticUpdate(path, update, onRollback) {
1374
+ const node = this.root.get(path);
1375
+ const originalValue = node?.value;
1376
+ const originalVersion = node?.version ?? 0;
1377
+ this.set(path, update);
1378
+ return {
1379
+ commit: () => console.log(`[HierarchicalStore] Committed: ${path}`),
1380
+ rollback: () => {
1381
+ const cur = this.root.get(path);
1382
+ if (cur && cur.version !== originalVersion + 1) {
1383
+ console.warn(`[HierarchicalStore] Rollback skipped for "${path}": concurrent update.`);
1384
+ return;
1385
+ }
1386
+ this.set(path, originalValue);
1387
+ onRollback?.();
1388
+ },
1389
+ };
1390
+ }
1391
+ set(path, value) {
1392
+ let node = this.root.get(path);
1393
+ if (!node) {
1394
+ node = {
1395
+ path,
1396
+ value,
1397
+ version: 0,
1398
+ children: new Map(),
1399
+ subscribers: new Set(),
1400
+ };
1401
+ this.root.set(path, node);
1270
1402
  }
1403
+ node.value = value;
1404
+ node.version++;
1405
+ this.stateSnapshot = { ...this.stateSnapshot, [path]: value };
1406
+ node.subscribers.forEach((fn) => {
1407
+ try {
1408
+ fn();
1409
+ }
1410
+ catch { }
1411
+ });
1412
+ this.selectorCache.clear();
1271
1413
  }
1272
1414
  }
1273
- export function mount(AppComponent, root, props = {}) {
1415
+ const hierarchicalStore = new HierarchicalStore();
1416
+ export function useHierarchicalStore() {
1417
+ return hierarchicalStore;
1418
+ }
1419
+ let rootRenderFn = null;
1420
+ function mount(AppComponent, root, props = {}) {
1274
1421
  if (typeof root === "string") {
1275
- const element = document.querySelector(root);
1276
- if (!element) {
1277
- console.error("[Fynix] Mount error: Element not found for selector:", root);
1422
+ const el = document.querySelector(root);
1423
+ if (!el) {
1424
+ console.error(`[Fynix] mount: selector "${root}" not found`);
1278
1425
  return;
1279
1426
  }
1280
- root = element;
1427
+ root = el;
1281
1428
  }
1282
- if (!(root instanceof Element)) {
1283
- console.error("[Fynix] Mount error: Invalid root element", root);
1284
- return;
1285
- }
1286
- let Component = AppComponent;
1287
- let oldVNode = null;
1288
- let currentProps = props;
1289
- let appVNode = null;
1290
- let isRendering = false;
1291
- async function renderApp() {
1292
- if (isRendering)
1293
- return;
1294
- isRendering = true;
1295
- try {
1296
- removeErrorOverlay();
1297
- const win = window;
1298
- const propsToUse = win.__lastRouteProps || win.__fynix__?.lastRouteProps || currentProps;
1299
- if (!appVNode) {
1300
- appVNode = { type: Component, props: propsToUse, key: null };
1301
- if (root instanceof Element) {
1302
- root.innerHTML = "";
1303
- const dom = await createDom(appVNode);
1304
- if (dom instanceof Node) {
1305
- root.appendChild(dom);
1306
- }
1307
- }
1308
- else {
1309
- console.error("[Fynix] Mount error: root is not a DOM Element", root);
1310
- return;
1429
+ const container = root;
1430
+ container.innerHTML = "";
1431
+ const win = window;
1432
+ const propsToUse = win.__lastRouteProps || win.__fynix__?.lastRouteProps || props;
1433
+ const appVNode = { type: AppComponent, props: propsToUse, key: null };
1434
+ fiberReconciler.mountRoot(appVNode, container);
1435
+ rootRenderFn = () => {
1436
+ const fiber = appVNode._fiber;
1437
+ if (fiber)
1438
+ fiberReconciler.scheduleUpdate(fiber, "high");
1439
+ };
1440
+ win.__fynix__ = win.__fynix__ || {};
1441
+ win.__fynix__.rerender = rootRenderFn;
1442
+ if (import.meta.hot && !win.__fynix__.hmr) {
1443
+ win.__fynix__.hmr = async ({ mod }) => {
1444
+ try {
1445
+ const UpdatedComponent = mod.App || mod.default;
1446
+ if (UpdatedComponent && appVNode._fiber) {
1447
+ appVNode._fiber.type = UpdatedComponent;
1448
+ fiberReconciler.scheduleUpdate(appVNode._fiber, "high");
1311
1449
  }
1312
- oldVNode = appVNode;
1313
1450
  }
1314
- else {
1315
- appVNode.props = propsToUse;
1316
- if (root instanceof Node) {
1317
- await patch(root, appVNode, oldVNode);
1318
- oldVNode = appVNode;
1319
- }
1320
- else {
1321
- console.error("[Fynix] Patch error: root is not a DOM Node", root);
1322
- return;
1323
- }
1451
+ catch (err) {
1452
+ console.error("[Fynix HMR]", err);
1453
+ showErrorOverlay(err);
1324
1454
  }
1455
+ };
1456
+ import.meta.hot.accept();
1457
+ }
1458
+ }
1459
+ function hydrate(AppComponent, root, props = {}) {
1460
+ if (typeof root === "string") {
1461
+ const el = document.querySelector(root);
1462
+ if (!el) {
1463
+ console.error(`[Fynix] hydrate: selector "${root}" not found`);
1464
+ return;
1325
1465
  }
1326
- catch (err) {
1327
- console.error("[Fynix] Mount error:", err);
1328
- showErrorOverlay(err);
1329
- }
1330
- finally {
1331
- isRendering = false;
1332
- }
1466
+ root = el;
1333
1467
  }
1334
- rootRenderFn = renderApp;
1468
+ const container = root;
1335
1469
  const win = window;
1470
+ const propsToUse = win.__lastRouteProps || win.__fynix__?.lastRouteProps || props;
1471
+ const appVNode = { type: AppComponent, props: propsToUse, key: null };
1472
+ if (container.firstChild)
1473
+ appVNode._domNode = container.firstChild;
1474
+ fiberReconciler.mountRoot(appVNode, container);
1475
+ rootRenderFn = () => {
1476
+ const fiber = appVNode._fiber;
1477
+ if (fiber)
1478
+ fiberReconciler.scheduleUpdate(fiber, "high");
1479
+ };
1336
1480
  win.__fynix__ = win.__fynix__ || {};
1337
- win.__fynix__.rerender = renderApp;
1338
- renderApp();
1339
- if (import.meta.hot) {
1340
- if (!win.__fynix__.hmr) {
1341
- win.__fynix__.hmr = async ({ mod }) => {
1342
- try {
1343
- const UpdatedComponent = mod.App || mod.default;
1344
- if (UpdatedComponent) {
1345
- Component = UpdatedComponent;
1346
- if (appVNode) {
1347
- appVNode.type = UpdatedComponent;
1348
- }
1349
- win.__fynix__.rerender?.();
1350
- }
1351
- }
1352
- catch (err) {
1353
- console.error("[Fynix HMR] update error:", err);
1354
- showErrorOverlay(err);
1355
- }
1356
- };
1357
- import.meta.hot.accept();
1481
+ win.__fynix__.rerender = rootRenderFn;
1482
+ }
1483
+ const MEMO_VNODE_KEY = Symbol("fynix.memoVNode");
1484
+ export function memo(Component, propsAreEqual) {
1485
+ const isEqual = propsAreEqual || shallowEqual;
1486
+ const instanceCache = new WeakMap();
1487
+ return function MemoizedComponent(props) {
1488
+ const vnodeKey = props[MEMO_VNODE_KEY] ?? null;
1489
+ let cleanProps = props;
1490
+ if (vnodeKey) {
1491
+ cleanProps = { ...props };
1492
+ delete cleanProps[MEMO_VNODE_KEY];
1493
+ }
1494
+ if (vnodeKey) {
1495
+ const cached = instanceCache.get(vnodeKey);
1496
+ if (cached && isEqual(cached.props, cleanProps))
1497
+ return cached.result;
1498
+ }
1499
+ const result = Component(cleanProps);
1500
+ if (vnodeKey)
1501
+ instanceCache.set(vnodeKey, { props: cleanProps, result });
1502
+ return result;
1503
+ };
1504
+ }
1505
+ const renderComponentCache = new WeakMap();
1506
+ export function renderComponent(Component, props = {}) {
1507
+ const vnode = { type: Component, props, key: null };
1508
+ const ctx = beginComponent(vnode);
1509
+ ctx.Component = Component;
1510
+ if (!ctx.rerender)
1511
+ ctx.rerender = createRerender(ctx);
1512
+ try {
1513
+ removeErrorOverlay();
1514
+ const result = Component(props);
1515
+ if (result instanceof Promise) {
1516
+ const placeholder = h("div", null, "Loading...");
1517
+ ctx._vnode = vnode;
1518
+ vnode._rendered = placeholder;
1519
+ ctx._isMounted = true;
1520
+ result
1521
+ .then((resolved) => {
1522
+ vnode._rendered = resolved;
1523
+ renderComponentCache.set(vnode, {
1524
+ props,
1525
+ result: resolved,
1526
+ timestamp: performance.now(),
1527
+ });
1528
+ if (ctx.rerender)
1529
+ ctx.rerender();
1530
+ })
1531
+ .catch((err) => publishAsyncError(err instanceof Error ? err : new Error(String(err))));
1532
+ return placeholder;
1533
+ }
1534
+ ctx._vnode = vnode;
1535
+ vnode._rendered = result;
1536
+ ctx._isMounted = true;
1537
+ renderComponentCache.set(vnode, {
1538
+ props,
1539
+ result,
1540
+ timestamp: performance.now(),
1541
+ });
1542
+ return result;
1543
+ }
1544
+ catch (err) {
1545
+ const error = err instanceof Error ? err : new Error(String(err));
1546
+ if (errorConfig.logToConsole) {
1547
+ console.error("[Fynix] Component render error:", error);
1548
+ }
1549
+ const handled = errorConfig.onRenderError?.(error, Component);
1550
+ if (!handled && errorConfig.showOverlay) {
1551
+ showErrorOverlay(error);
1552
+ }
1553
+ return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(error)}`);
1554
+ }
1555
+ finally {
1556
+ endComponent();
1557
+ }
1558
+ }
1559
+ export function ErrorBoundary({ fallback, children, }) {
1560
+ let asyncError = null;
1561
+ const handleAsyncError = (error) => {
1562
+ asyncError = error;
1563
+ console.error("[Fynix] ErrorBoundary caught async error:", error);
1564
+ };
1565
+ asyncErrorHandlers.push(handleAsyncError);
1566
+ const removeHandler = () => {
1567
+ const idx = asyncErrorHandlers.indexOf(handleAsyncError);
1568
+ if (idx !== -1)
1569
+ asyncErrorHandlers.splice(idx, 1);
1570
+ };
1571
+ const ctx = activeContext;
1572
+ if (ctx)
1573
+ ctx.cleanups.push(removeHandler);
1574
+ try {
1575
+ if (asyncError) {
1576
+ removeHandler();
1577
+ return fallback(asyncError);
1578
+ }
1579
+ if (!children || children.length === 0)
1580
+ return h(Fragment, null);
1581
+ return h(Fragment, null, ...children);
1582
+ }
1583
+ catch (err) {
1584
+ removeHandler();
1585
+ const error = err instanceof Error ? err : new Error(String(err));
1586
+ console.error("[Fynix] ErrorBoundary caught:", error);
1587
+ try {
1588
+ return fallback(error);
1589
+ }
1590
+ catch (fe) {
1591
+ console.error("[Fynix] ErrorBoundary fallback also threw:", fe);
1592
+ return h("div", { style: "color:red" }, "[ErrorBoundary] Fatal render error");
1358
1593
  }
1359
1594
  }
1360
1595
  }
1361
- export { Button, createFynix, nixAsync, nixAsyncCached, nixAsyncDebounce, nixAsyncQuery, nixCallback, nixComputed, nixDebounce, nixEffect, nixEffectAlways, nixEffectOnce, nixForm, nixFormAsync, nixInterval, nixLazy, nixLazyAsync, nixLazyFormAsync, nixLocalStorage, nixMemo, nixPrevious, nixRef, nixState, nixStore, Path, Suspense, };
1596
+ export { Button, createFynix, nixAsync, nixAsyncCached, nixAsyncDebounce, nixAsyncQuery, nixCallback, nixComputed, nixDebounce, nixEffect, nixEffectAlways, nixEffectOnce, nixForm, nixFormAsync, nixInterval, nixLazy, nixLazyAsync, nixLazyFormAsync, nixLocalStorage, nixMemo, nixPrevious, nixRef, nixState, nixStore, Path, Suspense, mount, hydrate, fiberReconciler, };