fynixui 1.0.10 → 1.0.12

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.
Files changed (104) hide show
  1. package/dist/custom/DataTable.js +1 -0
  2. package/dist/custom/button.js +241 -1
  3. package/dist/custom/index.js +1 -1
  4. package/dist/error/errorOverlay.js +1 -1
  5. package/dist/hooks/nixFor.js +6 -4
  6. package/dist/package.json +34 -37
  7. package/dist/plugins/vite-plugin-res.js +26 -4
  8. package/dist/router/router.js +108 -217
  9. package/dist/runtime.js +1251 -1028
  10. package/{dist → dist-types}/context/context.d.ts +1 -2
  11. package/dist-types/custom/DataTable.d.ts +0 -0
  12. package/dist-types/custom/button.d.ts +35 -0
  13. package/dist-types/custom/index.d.ts +2 -0
  14. package/{dist → dist-types}/custom/path.d.ts +0 -1
  15. package/{dist → dist-types}/error/errorOverlay.d.ts +0 -1
  16. package/{dist → dist-types}/fynix/index.d.ts +0 -1
  17. package/{dist → dist-types}/hooks/nixAsync.d.ts +0 -1
  18. package/{dist → dist-types}/hooks/nixAsyncCache.d.ts +0 -1
  19. package/{dist → dist-types}/hooks/nixAsyncDebounce.d.ts +0 -1
  20. package/{dist → dist-types}/hooks/nixAsyncQuery.d.ts +0 -1
  21. package/{dist → dist-types}/hooks/nixCallback.d.ts +0 -1
  22. package/{dist → dist-types}/hooks/nixComputed.d.ts +0 -1
  23. package/{dist → dist-types}/hooks/nixDebounce.d.ts +0 -1
  24. package/{dist → dist-types}/hooks/nixEffect.d.ts +0 -1
  25. package/{dist → dist-types}/hooks/nixFor.d.ts +0 -1
  26. package/{dist → dist-types}/hooks/nixForm.d.ts +0 -1
  27. package/{dist → dist-types}/hooks/nixFormAsync.d.ts +0 -1
  28. package/{dist → dist-types}/hooks/nixInterval.d.ts +0 -1
  29. package/{dist → dist-types}/hooks/nixLazy.d.ts +0 -1
  30. package/{dist → dist-types}/hooks/nixLazyAsync.d.ts +0 -1
  31. package/{dist → dist-types}/hooks/nixLazyFormAsync.d.ts +0 -1
  32. package/{dist → dist-types}/hooks/nixLocalStorage.d.ts +0 -1
  33. package/{dist → dist-types}/hooks/nixMemo.d.ts +0 -1
  34. package/{dist → dist-types}/hooks/nixPrevious.d.ts +0 -1
  35. package/{dist → dist-types}/hooks/nixRef.d.ts +0 -1
  36. package/{dist → dist-types}/hooks/nixState.d.ts +0 -1
  37. package/{dist → dist-types}/hooks/nixStore.d.ts +0 -1
  38. package/{dist → dist-types}/plugins/vite-plugin-res.d.ts +0 -1
  39. package/{dist → dist-types}/router/router.d.ts +14 -11
  40. package/dist-types/runtime.d.ts +200 -0
  41. package/package.json +43 -41
  42. package/dist/context/context.d.ts.map +0 -1
  43. package/dist/context/context.js.map +0 -7
  44. package/dist/custom/button.d.ts +0 -2
  45. package/dist/custom/button.d.ts.map +0 -1
  46. package/dist/custom/button.js.map +0 -7
  47. package/dist/custom/index.d.ts +0 -3
  48. package/dist/custom/index.d.ts.map +0 -1
  49. package/dist/custom/index.js.map +0 -7
  50. package/dist/custom/path.d.ts.map +0 -1
  51. package/dist/custom/path.js.map +0 -7
  52. package/dist/error/errorOverlay.d.ts.map +0 -1
  53. package/dist/error/errorOverlay.js.map +0 -7
  54. package/dist/fynix/index.d.ts.map +0 -1
  55. package/dist/fynix/index.js.map +0 -7
  56. package/dist/hooks/nixAsync.d.ts.map +0 -1
  57. package/dist/hooks/nixAsync.js.map +0 -7
  58. package/dist/hooks/nixAsyncCache.d.ts.map +0 -1
  59. package/dist/hooks/nixAsyncCache.js.map +0 -7
  60. package/dist/hooks/nixAsyncDebounce.d.ts.map +0 -1
  61. package/dist/hooks/nixAsyncDebounce.js.map +0 -7
  62. package/dist/hooks/nixAsyncQuery.d.ts.map +0 -1
  63. package/dist/hooks/nixAsyncQuery.js.map +0 -7
  64. package/dist/hooks/nixCallback.d.ts.map +0 -1
  65. package/dist/hooks/nixCallback.js.map +0 -7
  66. package/dist/hooks/nixComputed.d.ts.map +0 -1
  67. package/dist/hooks/nixComputed.js.map +0 -7
  68. package/dist/hooks/nixDebounce.d.ts.map +0 -1
  69. package/dist/hooks/nixDebounce.js.map +0 -7
  70. package/dist/hooks/nixEffect.d.ts.map +0 -1
  71. package/dist/hooks/nixEffect.js.map +0 -7
  72. package/dist/hooks/nixFor.d.ts.map +0 -1
  73. package/dist/hooks/nixFor.js.map +0 -7
  74. package/dist/hooks/nixForm.d.ts.map +0 -1
  75. package/dist/hooks/nixForm.js.map +0 -7
  76. package/dist/hooks/nixFormAsync.d.ts.map +0 -1
  77. package/dist/hooks/nixFormAsync.js.map +0 -7
  78. package/dist/hooks/nixInterval.d.ts.map +0 -1
  79. package/dist/hooks/nixInterval.js.map +0 -7
  80. package/dist/hooks/nixLazy.d.ts.map +0 -1
  81. package/dist/hooks/nixLazy.js.map +0 -7
  82. package/dist/hooks/nixLazyAsync.d.ts.map +0 -1
  83. package/dist/hooks/nixLazyAsync.js.map +0 -7
  84. package/dist/hooks/nixLazyFormAsync.d.ts.map +0 -1
  85. package/dist/hooks/nixLazyFormAsync.js.map +0 -7
  86. package/dist/hooks/nixLocalStorage.d.ts.map +0 -1
  87. package/dist/hooks/nixLocalStorage.js.map +0 -7
  88. package/dist/hooks/nixMemo.d.ts.map +0 -1
  89. package/dist/hooks/nixMemo.js.map +0 -7
  90. package/dist/hooks/nixPrevious.d.ts.map +0 -1
  91. package/dist/hooks/nixPrevious.js.map +0 -7
  92. package/dist/hooks/nixRef.d.ts.map +0 -1
  93. package/dist/hooks/nixRef.js.map +0 -7
  94. package/dist/hooks/nixState.d.ts.map +0 -1
  95. package/dist/hooks/nixState.js.map +0 -7
  96. package/dist/hooks/nixStore.d.ts.map +0 -1
  97. package/dist/hooks/nixStore.js.map +0 -7
  98. package/dist/plugins/vite-plugin-res.d.ts.map +0 -1
  99. package/dist/plugins/vite-plugin-res.js.map +0 -7
  100. package/dist/router/router.d.ts.map +0 -1
  101. package/dist/router/router.js.map +0 -7
  102. package/dist/runtime.d.ts +0 -124
  103. package/dist/runtime.d.ts.map +0 -1
  104. package/dist/runtime.js.map +0 -7
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,13 @@ 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
+ typeof queueMicrotask === "function"
762
+ ? queueMicrotask(() => ctx.rerender())
763
+ : setTimeout(ctx.rerender, 0);
622
764
  }
623
765
  });
624
766
  ctx._subscriptions.add(state);
@@ -627,735 +769,816 @@ function endComponent() {
627
769
  });
628
770
  setActiveContext(null);
629
771
  }
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))
772
+ function createRerender(ctx) {
773
+ return function rerender() {
774
+ if (ctx._isRerendering || pendingRerenders.has(ctx))
775
+ return;
776
+ if (ctx._rerenderTimeout !== null) {
777
+ clearTimeout(ctx._rerenderTimeout);
778
+ ctx._rerenderTimeout = null;
779
+ }
780
+ ctx._rerenderTimeout = setTimeout(() => {
781
+ ctx._rerenderTimeout = null;
782
+ if (ctx._isRerendering || !ctx._isMounted)
638
783
  return;
639
- if (rerenderTimeout) {
640
- clearTimeout(rerenderTimeout);
784
+ if (ctx._fiber) {
785
+ fiberReconciler.scheduleUpdate(ctx._fiber, "normal");
786
+ }
787
+ else if (ctx._vnode) {
788
+ console.warn("[Fynix] Rerender triggered before fiber assigned — skipping.");
789
+ }
790
+ }, 0);
791
+ };
792
+ }
793
+ function updateProps(el, newProps = {}, oldProps = {}) {
794
+ if (!el || el.nodeType !== 1)
795
+ return;
796
+ for (const k of Object.keys(oldProps)) {
797
+ if (k === "children")
798
+ continue;
799
+ if (!(k in newProps)) {
800
+ if (k.startsWith("r-")) {
801
+ const eid = el._rest_eid;
802
+ if (eid)
803
+ delegatedEvents.get(k.slice(2).toLowerCase())?.delete(eid);
804
+ }
805
+ else if (BOOLEAN_ATTRS.has(k.toLowerCase())) {
806
+ el.removeAttribute(k);
807
+ el[k] = false;
808
+ }
809
+ else if (DOM_PROPERTIES.has(k)) {
810
+ el[k] = "";
811
+ }
812
+ else {
813
+ el.removeAttribute(k);
641
814
  }
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;
705
815
  }
706
- ctx._vnode = vnode;
707
- vnode._rendered = result;
708
- ctx._isMounted = true;
709
- return result;
710
816
  }
711
- catch (err) {
712
- console.error("[Fynix] Component render error:", err);
713
- showErrorOverlay(err);
714
- return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
817
+ for (const [k, v] of Object.entries(newProps)) {
818
+ if (k === "children")
819
+ continue;
820
+ if (oldProps[k] !== v)
821
+ setProperty(el, k, v);
715
822
  }
716
- finally {
717
- endComponent();
823
+ }
824
+ function createDomElement(type, props) {
825
+ const el = document.createElement(type);
826
+ for (const [k, v] of Object.entries(props)) {
827
+ if (k !== "children")
828
+ setProperty(el, k, v);
718
829
  }
830
+ return el;
719
831
  }
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);
736
- return;
737
- }
738
- cur = cur.parentElement;
832
+ function unmountCtx(ctx) {
833
+ ctx._isMounted = false;
834
+ if (ctx._rerenderTimeout !== null) {
835
+ clearTimeout(ctx._rerenderTimeout);
836
+ ctx._rerenderTimeout = null;
837
+ }
838
+ ctx._subscriptionCleanups.forEach((u) => {
839
+ try {
840
+ u();
739
841
  }
842
+ catch { }
740
843
  });
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) => {
844
+ ctx.cleanups.forEach((c) => {
749
845
  try {
750
- fn.call(el, e);
751
- }
752
- catch (err) {
753
- console.error("[Fynix] Event handler error:", err);
754
- showErrorOverlay(err);
846
+ c?.();
755
847
  }
848
+ catch { }
756
849
  });
850
+ ctx._subscriptions.clear();
851
+ ctx._accessedStates.clear();
852
+ ctx._subscriptionCleanups = [];
853
+ ctx.cleanups = [];
854
+ ctx.hooks = [];
855
+ ctx.effects = [];
856
+ ctx.rerender = null;
857
+ ctx._vnode = null;
858
+ ctx._fiber = null;
757
859
  }
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);
796
- }
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;
860
+ function removeDomCleanups(node) {
861
+ const any = node;
862
+ const eid = any._rest_eid;
863
+ if (eid)
864
+ delegatedEvents.forEach((m) => m.delete(eid));
865
+ if (any._fynixCleanups) {
866
+ any._fynixCleanups.forEach((fn) => {
867
+ try {
868
+ fn();
869
+ }
870
+ catch { }
871
+ });
872
+ any._fynixCleanups = null;
812
873
  }
813
- if (key.startsWith("r-")) {
814
- registerDelegatedHandler(el, key.slice(2).toLowerCase(), value);
815
- return;
874
+ }
875
+ class FiberReconciler {
876
+ constructor() {
877
+ this.wipRoot = null;
878
+ this.nextWork = null;
879
+ this.deletions = [];
816
880
  }
817
- if (key === "style" && typeof value === "object") {
818
- Object.assign(el.style, value);
819
- return;
881
+ mountRoot(vnode, container) {
882
+ const rootFiber = this.vnodeToFiber(vnode, null, null);
883
+ rootFiber._domNode = container;
884
+ this.wipRoot = rootFiber;
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.nextWork = wip;
897
+ this.deletions = [];
898
+ this.scheduleRender(priority);
820
899
  }
821
- if (DANGEROUS_HTML_PROPS.has(key)) {
822
- console.error(`[Fynix] Security: ${key} is blocked for security reasons. Use textContent or children instead.`);
823
- return;
900
+ scheduleRender(priority) {
901
+ scheduler.schedule({
902
+ id: "",
903
+ type: "layout",
904
+ priority,
905
+ callback: () => this.workLoop(priority === "high" ? 16 : 5),
906
+ timestamp: performance.now(),
907
+ }, priority);
824
908
  }
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;
835
- }
909
+ workLoop(deadline) {
910
+ perfMark("workloop-start");
911
+ const t0 = performance.now();
912
+ while (this.nextWork && performance.now() - t0 < deadline) {
913
+ this.nextWork = this.performWork(this.nextWork);
836
914
  }
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;
915
+ if (!this.nextWork && this.wipRoot) {
916
+ perfMark("render-complete");
917
+ this.commitRoot();
918
+ perfMark("commit-complete");
919
+ if (perfConfig.enabled) {
920
+ const renderTime = perfMeasure("render", "workloop-start", "render-complete");
921
+ const commitTime = perfMeasure("commit", "render-complete", "commit-complete");
922
+ if (perfConfig.slowRenderThreshold &&
923
+ renderTime + commitTime > perfConfig.slowRenderThreshold) {
924
+ console.warn(`[Fynix] Slow render: ${(renderTime + commitTime).toFixed(2)}ms`);
925
+ }
926
+ if (perfConfig.onMetrics) {
927
+ perfConfig.onMetrics({
928
+ renderTime,
929
+ commitTime,
930
+ totalTime: renderTime + commitTime,
931
+ updateCount: 0,
932
+ fiberCount: 0,
933
+ timestamp: performance.now(),
934
+ });
935
+ }
843
936
  }
844
937
  }
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;
850
- }
938
+ else if (this.nextWork) {
939
+ this.scheduleRender("normal");
851
940
  }
852
941
  }
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;
856
- }
857
- if (BOOLEAN_ATTRS.has(k)) {
858
- if (value) {
859
- el.setAttribute(k, "");
860
- el[k] = true;
942
+ performWork(fiber) {
943
+ if (typeof fiber.type === "function") {
944
+ this.updateComponentFiber(fiber);
861
945
  }
862
946
  else {
863
- el.removeAttribute(k);
864
- el[k] = false;
865
- }
866
- return;
867
- }
868
- if (DOM_PROPERTIES.has(key) && !DANGEROUS_HTML_PROPS.has(key)) {
869
- if (key === "textContent" || key === "innerText") {
870
- el[key] = sanitizeText(value ?? "");
947
+ this.updateHostFiber(fiber);
871
948
  }
872
- else {
873
- el[key] = value ?? "";
949
+ if (fiber.child)
950
+ return fiber.child;
951
+ let f = fiber;
952
+ while (f) {
953
+ if (f.sibling)
954
+ return f.sibling;
955
+ f = f.parent;
874
956
  }
875
- return;
957
+ return null;
876
958
  }
877
- if (key.startsWith("data-") || key.startsWith("aria-")) {
878
- if (value != null && value !== false) {
879
- el.setAttribute(key, sanitizeAttributeValue(String(value)));
880
- }
881
- else {
882
- el.removeAttribute(key);
959
+ updateComponentFiber(fiber) {
960
+ const vnode = fiber._vnode;
961
+ let ctx = componentInstances.get(vnode);
962
+ if (!ctx) {
963
+ ctx = makeContext(vnode, fiber.type);
964
+ componentInstances.set(vnode, ctx);
965
+ }
966
+ ctx._fiber = fiber;
967
+ fiber.ctx = ctx;
968
+ ctx.hookIndex = 0;
969
+ ctx._accessedStates.clear();
970
+ setActiveContext(ctx);
971
+ ctx.version++;
972
+ let rendered = null;
973
+ try {
974
+ removeErrorOverlay();
975
+ const result = fiber.type(fiber.props);
976
+ if (result instanceof Promise) {
977
+ rendered = h("div", null, "Loading...");
978
+ result
979
+ .then((resolved) => {
980
+ vnode._rendered = resolved;
981
+ if (ctx.rerender)
982
+ ctx.rerender();
983
+ })
984
+ .catch((err) => publishAsyncError(err instanceof Error ? err : new Error(String(err))));
985
+ }
986
+ else {
987
+ rendered = result;
988
+ }
883
989
  }
884
- return;
885
- }
886
- if (value != null && value !== false) {
887
- el.setAttribute(key, value);
888
- }
889
- }
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) => {
901
- try {
902
- const dom = await createDom(resolved);
903
- if (placeholder.parentNode) {
904
- placeholder.replaceWith(dom);
905
- }
990
+ catch (err) {
991
+ const error = err instanceof Error ? err : new Error(String(err));
992
+ if (errorConfig.logToConsole) {
993
+ console.error("[Fynix] Component render error:", error);
906
994
  }
907
- catch (err) {
908
- console.error("[Fynix] Async component error:", err);
909
- if (placeholder.parentNode) {
910
- placeholder.textContent = "Error loading component";
911
- }
995
+ const handled = errorConfig.onRenderError?.(error, fiber.type);
996
+ if (!handled && errorConfig.showOverlay) {
997
+ showErrorOverlay(error);
912
998
  }
913
- })
914
- .catch((err) => {
915
- console.error("[Fynix] Async component promise error:", err);
916
- if (placeholder.parentNode) {
917
- placeholder.textContent = "Error loading async component";
999
+ rendered = h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(error)}`);
1000
+ }
1001
+ ctx._accessedStates.forEach((state) => {
1002
+ if (!ctx._subscriptions.has(state)) {
1003
+ if (!ctx.rerender)
1004
+ ctx.rerender = createRerender(ctx);
1005
+ const unsub = state.subscribe(() => {
1006
+ if (ctx.rerender && ctx._isMounted) {
1007
+ typeof queueMicrotask === "function"
1008
+ ? queueMicrotask(() => ctx.rerender())
1009
+ : setTimeout(ctx.rerender, 0);
1010
+ }
1011
+ });
1012
+ ctx._subscriptions.add(state);
1013
+ ctx._subscriptionCleanups.push(unsub);
918
1014
  }
919
1015
  });
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
- }
948
- }
949
- for (const child of vnodeObj.props?.children || []) {
950
- el.appendChild(await createDom(child));
951
- }
952
- vnodeObj._domNode = el;
953
- return el;
954
- }
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;
1016
+ setActiveContext(null);
962
1017
  ctx._isMounted = true;
963
- endComponent();
964
- return result ?? null;
965
- }
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)}`);
972
- }
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;
978
- }
979
- if (!newVNode && !oldVNode)
980
- return;
981
- if (!newVNode && oldVNode) {
982
- const domNode = oldVNode._domNode;
983
- if (domNode?.parentNode) {
984
- domNode.parentNode.removeChild(domNode);
985
- }
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);
993
- }
994
- return;
995
- }
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))
1018
+ vnode._rendered = rendered;
1019
+ if (!ctx.rerender)
1020
+ ctx.rerender = createRerender(ctx);
1021
+ const children = rendered ? [rendered] : [];
1022
+ this.reconcileChildren(fiber, children);
1023
+ }
1024
+ updateHostFiber(fiber) {
1025
+ if (fiber.type === TEXT ||
1026
+ (typeof fiber.type === "symbol" &&
1027
+ fiber.type.description?.toLowerCase() === "text")) {
1028
+ if (!fiber._domNode) {
1029
+ fiber._domNode = document.createTextNode(String(fiber.props.nodeValue ?? ""));
1030
+ }
1031
+ else if (fiber.alternate) {
1032
+ const oldText = fiber.alternate.props.nodeValue ?? "";
1033
+ const newText = fiber.props.nodeValue ?? "";
1034
+ if (oldText !== newText)
1035
+ fiber._domNode.nodeValue = String(newText);
1036
+ }
1002
1037
  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);
1007
1038
  }
1008
- if (oldVNode && typeof oldVNode === "object") {
1009
- unmountVNode(oldVNode);
1039
+ if (fiber.type === Fragment ||
1040
+ (typeof fiber.type === "symbol" &&
1041
+ fiber.type.description?.toLowerCase() === "fragment")) {
1042
+ if (!fiber._domNode) {
1043
+ const start = document.createTextNode("");
1044
+ const end = document.createTextNode("");
1045
+ fiber._domNode = start;
1046
+ const vnode = fiber._vnode;
1047
+ if (vnode) {
1048
+ vnode._fragmentStart = start;
1049
+ vnode._fragmentEnd = end;
1050
+ fiber._fragmentEnd = end;
1051
+ }
1052
+ else {
1053
+ console.warn("[FynixReconciler] Fragment fiber created without backing VNode. This may cause cleanup issues.");
1054
+ }
1055
+ }
1056
+ this.reconcileChildren(fiber, fiber.props.children || []);
1057
+ return;
1010
1058
  }
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;
1059
+ if (typeof fiber.type === "string") {
1060
+ if (!fiber._domNode) {
1061
+ fiber._domNode = createDomElement(fiber.type, fiber.props);
1062
+ fiber.effectTag = "PLACEMENT";
1063
+ }
1064
+ else if (fiber.alternate) {
1065
+ updateProps(fiber._domNode, fiber.props, fiber.alternate.props);
1066
+ fiber.effectTag = "UPDATE";
1067
+ }
1068
+ this.reconcileChildren(fiber, fiber.props.children || []);
1032
1069
  }
1033
- newVN._domNode = oldDom;
1034
- return;
1035
1070
  }
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;
1042
- }
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;
1058
- }
1071
+ reconcileChildren(wipFiber, elements) {
1072
+ const hasKeys = elements.some((e) => e?.key != null);
1073
+ const oldMap = new Map();
1074
+ const oldByIndex = [];
1075
+ let oldFiber = wipFiber.alternate?.child ?? null;
1076
+ let idx = 0;
1077
+ while (oldFiber) {
1078
+ if (hasKeys && oldFiber.key != null)
1079
+ oldMap.set(oldFiber.key, oldFiber);
1080
+ else
1081
+ oldByIndex[idx] = oldFiber;
1082
+ oldFiber = oldFiber.sibling;
1083
+ idx++;
1084
+ }
1085
+ if (hasKeys) {
1086
+ const newKeys = new Set(elements.filter((e) => e?.key != null).map((e) => e.key));
1087
+ oldMap.forEach((f, k) => {
1088
+ if (!newKeys.has(k)) {
1089
+ f.effectTag = "DELETION";
1090
+ this.deletions.push(f);
1091
+ }
1092
+ });
1059
1093
  }
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;
1094
+ let prevSibling = null;
1095
+ for (let i = 0; i < elements.length; i++) {
1096
+ const el = elements[i];
1097
+ if (el == null)
1098
+ continue;
1099
+ const matchFiber = hasKeys && el.key != null
1100
+ ? (oldMap.get(el.key) ?? null)
1101
+ : (oldByIndex[i] ?? null);
1102
+ let newFiber;
1103
+ if (matchFiber && matchFiber.type === el.type) {
1104
+ newFiber = this.cloneFiber(matchFiber);
1105
+ newFiber.props = el.props;
1106
+ newFiber.key = el.key;
1107
+ newFiber.alternate = matchFiber;
1108
+ newFiber.effectTag = "UPDATE";
1109
+ newFiber.parent = wipFiber;
1110
+ newFiber._vnode = el;
1111
+ el._fiber = newFiber;
1068
1112
  }
1069
1113
  else {
1070
- const newDom = await createDom(rendered);
1071
- if (parent && newDom instanceof Node) {
1072
- parent.appendChild(newDom);
1114
+ if (matchFiber) {
1115
+ matchFiber.effectTag = "DELETION";
1116
+ this.deletions.push(matchFiber);
1073
1117
  }
1074
- newVN._domNode = newDom;
1118
+ newFiber = this.vnodeToFiber(el, wipFiber, null);
1119
+ newFiber.effectTag = "PLACEMENT";
1075
1120
  }
1076
- if (oldCtx && newType !== oldType) {
1077
- unmountVNode(oldVN);
1121
+ if (i === 0)
1122
+ wipFiber.child = newFiber;
1123
+ else if (prevSibling)
1124
+ prevSibling.sibling = newFiber;
1125
+ prevSibling = newFiber;
1126
+ }
1127
+ if (!hasKeys) {
1128
+ for (let j = elements.length; j < oldByIndex.length; j++) {
1129
+ const dead = oldByIndex[j];
1130
+ if (dead) {
1131
+ dead.effectTag = "DELETION";
1132
+ this.deletions.push(dead);
1133
+ }
1078
1134
  }
1079
1135
  }
1080
- return;
1081
1136
  }
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);
1087
- }
1088
- unmountVNode(oldVN);
1089
- return;
1137
+ commitRoot() {
1138
+ if (!this.wipRoot)
1139
+ return;
1140
+ this.deletions.forEach((f) => this.commitDeletion(f));
1141
+ this.deletions = [];
1142
+ if (this.wipRoot.child)
1143
+ this.commitWork(this.wipRoot.child);
1144
+ this.wipRoot = null;
1145
+ this.nextWork = null;
1090
1146
  }
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);
1111
- }
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);
1118
- }
1147
+ commitWork(fiber) {
1148
+ if (!fiber)
1149
+ return;
1150
+ const domParent = this.findDomParent(fiber);
1151
+ if (fiber.effectTag === "PLACEMENT" && fiber._domNode) {
1152
+ if (!domParent) {
1153
+ console.warn("[FynixReconciler] No valid DOM parent found for fiber; skipping insertion");
1119
1154
  }
1120
1155
  else {
1121
- await patch(parent, newChild, oldChild);
1156
+ const validContainers = domParent.nodeType === Node.ELEMENT_NODE ||
1157
+ domParent.nodeType === Node.DOCUMENT_NODE ||
1158
+ domParent.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
1159
+ if (!validContainers) {
1160
+ console.warn("[FynixReconciler] Parent node type (" +
1161
+ domParent.nodeType +
1162
+ ") cannot accept children; skipping insertion");
1163
+ }
1164
+ else {
1165
+ try {
1166
+ if (fiber._domNode.parentNode &&
1167
+ fiber._domNode.parentNode !== domParent) {
1168
+ try {
1169
+ fiber._domNode.parentNode.removeChild(fiber._domNode);
1170
+ }
1171
+ catch (e) {
1172
+ }
1173
+ }
1174
+ const refNode = this.findNextDomSibling(fiber);
1175
+ if (refNode && refNode.parentNode === domParent) {
1176
+ try {
1177
+ domParent.insertBefore(fiber._domNode, refNode);
1178
+ }
1179
+ catch (insertErr) {
1180
+ try {
1181
+ domParent.appendChild(fiber._domNode);
1182
+ }
1183
+ catch (appendErr) {
1184
+ console.error("[FynixReconciler] Failed to insert node:", appendErr);
1185
+ }
1186
+ }
1187
+ }
1188
+ else {
1189
+ try {
1190
+ domParent.appendChild(fiber._domNode);
1191
+ }
1192
+ catch (appendErr) {
1193
+ console.error("[FynixReconciler] Failed to append node:", appendErr);
1194
+ }
1195
+ }
1196
+ }
1197
+ catch (e) {
1198
+ console.error("[FynixReconciler] Unexpected error during node insertion:", e);
1199
+ }
1200
+ }
1122
1201
  }
1123
1202
  }
1124
- return;
1125
- }
1126
- const oldKeyMap = new Map();
1127
- oldChildren.forEach((child) => {
1128
- if (child?.key != null) {
1129
- oldKeyMap.set(child.key, child);
1130
- }
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);
1203
+ else if (fiber.effectTag === "UPDATE") {
1204
+ if (fiber._domNode && typeof fiber.type === "string" && fiber.alternate) {
1205
+ updateProps(fiber._domNode, fiber.props, fiber.alternate.props);
1138
1206
  }
1139
- unmountVNode(oldChild);
1140
1207
  }
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);
1208
+ this.runEffects(fiber);
1209
+ fiber.effectTag = null;
1210
+ this.commitWork(fiber.child);
1211
+ this.commitWork(fiber.sibling);
1212
+ }
1213
+ commitDeletion(fiber) {
1214
+ this.unmountFiber(fiber);
1215
+ const domNode = this.findNearestDom(fiber);
1216
+ if (domNode?.parentNode)
1217
+ domNode.parentNode.removeChild(domNode);
1218
+ }
1219
+ unmountFiber(fiber) {
1220
+ if (!fiber)
1221
+ return;
1222
+ const stack = [fiber];
1223
+ while (stack.length > 0) {
1224
+ const current = stack.pop();
1225
+ if (!current)
1226
+ continue;
1227
+ if (current.sibling)
1228
+ stack.push(current.sibling);
1229
+ if (current.child)
1230
+ stack.push(current.child);
1231
+ try {
1232
+ if (current.ctx) {
1233
+ unmountCtx(current.ctx);
1234
+ if (current._vnode)
1235
+ componentInstances.delete(current._vnode);
1155
1236
  }
1237
+ if (current._domNode?.nodeType === 1)
1238
+ removeDomCleanups(current._domNode);
1239
+ }
1240
+ catch (e) {
1241
+ console.error("[FynixReconciler] Error unmounting fiber:", e);
1156
1242
  }
1157
- await patch(parent, newChild, oldChild);
1158
1243
  }
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);
1244
+ }
1245
+ runEffects(fiber) {
1246
+ if (!fiber.ctx)
1247
+ return;
1248
+ const ctx = fiber.ctx;
1249
+ ctx.effects.forEach((effect) => {
1250
+ try {
1251
+ const cleanup = effect();
1252
+ if (typeof cleanup === "function")
1253
+ ctx.cleanups.push(cleanup);
1165
1254
  }
1255
+ catch (e) {
1256
+ console.error("[Fynix] Effect error:", e);
1257
+ }
1258
+ });
1259
+ ctx.effects = [];
1260
+ }
1261
+ vnodeToFiber(vnode, parent, alternate) {
1262
+ const fiber = {
1263
+ type: vnode.type,
1264
+ props: vnode.props,
1265
+ key: vnode.key,
1266
+ child: null,
1267
+ sibling: null,
1268
+ parent,
1269
+ alternate,
1270
+ effectTag: null,
1271
+ updatePriority: "normal",
1272
+ _domNode: vnode._domNode ?? null,
1273
+ ctx: null,
1274
+ _vnode: vnode,
1275
+ };
1276
+ vnode._fiber = fiber;
1277
+ return fiber;
1278
+ }
1279
+ cloneFiber(fiber) {
1280
+ return {
1281
+ type: fiber.type,
1282
+ props: fiber.props,
1283
+ key: fiber.key,
1284
+ child: null,
1285
+ sibling: null,
1286
+ parent: fiber.parent,
1287
+ alternate: fiber,
1288
+ effectTag: null,
1289
+ updatePriority: fiber.updatePriority,
1290
+ _domNode: fiber._domNode,
1291
+ ctx: fiber.ctx,
1292
+ _vnode: fiber._vnode,
1293
+ };
1294
+ }
1295
+ findDomParent(fiber) {
1296
+ let p = fiber.parent;
1297
+ while (p && !p._domNode)
1298
+ p = p.parent;
1299
+ let domNode = p?._domNode ?? null;
1300
+ while (domNode &&
1301
+ (domNode.nodeType === Node.TEXT_NODE ||
1302
+ domNode.nodeType === Node.COMMENT_NODE ||
1303
+ domNode.nodeType === Node.PROCESSING_INSTRUCTION_NODE)) {
1304
+ domNode = domNode.parentNode;
1305
+ }
1306
+ return domNode;
1307
+ }
1308
+ findNearestDom(fiber) {
1309
+ if (fiber._domNode)
1310
+ return fiber._domNode;
1311
+ if (fiber.child)
1312
+ return this.findNearestDom(fiber.child);
1313
+ return null;
1314
+ }
1315
+ findNextDomSibling(fiber) {
1316
+ let sib = fiber.sibling;
1317
+ while (sib) {
1318
+ const dom = this.findNearestDom(sib);
1319
+ if (dom)
1320
+ return dom;
1321
+ sib = sib.sibling;
1166
1322
  }
1323
+ return null;
1167
1324
  }
1168
1325
  }
1169
- function unmountVNode(vnode) {
1170
- if (!vnode)
1171
- return;
1172
- if (vnode._cleanup && typeof vnode._cleanup === "function") {
1173
- try {
1174
- vnode._cleanup();
1175
- }
1176
- catch (e) {
1177
- console.error("[Fynix] Text vnode cleanup error:", e);
1178
- }
1179
- vnode._cleanup = null;
1326
+ const fiberReconciler = new FiberReconciler();
1327
+ export const __debug__ = {
1328
+ getSchedulerState: () => scheduler.getState(),
1329
+ getQueueMetrics: () => scheduler.getQueueMetrics(),
1330
+ getFiberReconciler: () => fiberReconciler,
1331
+ getErrorConfig: () => getErrorConfig(),
1332
+ getPerfConfig: () => getPerfConfig(),
1333
+ collectGarbage: () => {
1334
+ if (typeof global !== "undefined" && global.gc) {
1335
+ global.gc();
1336
+ }
1337
+ else if (typeof window !== "undefined" && window.gc) {
1338
+ window.gc();
1339
+ }
1340
+ },
1341
+ clearSchedulerQueue: () => scheduler.clearQueue(),
1342
+ getAsyncContext: () => ({
1343
+ currentBatchStore: getCurrentBatchStore(),
1344
+ batchingStorageAvailable: batchingStorage !== null,
1345
+ }),
1346
+ };
1347
+ class HierarchicalStore {
1348
+ constructor() {
1349
+ this.root = new Map();
1350
+ this.selectorCache = new Map();
1351
+ this.stateSnapshot = {};
1180
1352
  }
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?.();
1196
- }
1197
- catch (e) {
1198
- console.error("[Fynix] Effect cleanup error:", e);
1353
+ select(selector) {
1354
+ const k = selector.toString();
1355
+ if (this.selectorCache.has(k))
1356
+ return this.selectorCache.get(k);
1357
+ const r = selector(this.stateSnapshot);
1358
+ this.selectorCache.set(k, r);
1359
+ return r;
1360
+ }
1361
+ optimisticUpdate(path, update, onRollback) {
1362
+ const node = this.root.get(path);
1363
+ const originalValue = node?.value;
1364
+ const originalVersion = node?.version ?? 0;
1365
+ this.set(path, update);
1366
+ return {
1367
+ commit: () => console.log(`[HierarchicalStore] Committed: ${path}`),
1368
+ rollback: () => {
1369
+ const cur = this.root.get(path);
1370
+ if (cur && cur.version !== originalVersion + 1) {
1371
+ console.warn(`[HierarchicalStore] Rollback skipped for "${path}": concurrent update.`);
1372
+ return;
1199
1373
  }
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;
1374
+ this.set(path, originalValue);
1375
+ onRollback?.();
1376
+ },
1377
+ };
1214
1378
  }
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));
1220
- }
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;
1379
+ set(path, value) {
1380
+ let node = this.root.get(path);
1381
+ if (!node) {
1382
+ node = {
1383
+ path,
1384
+ value,
1385
+ version: 0,
1386
+ children: new Map(),
1387
+ subscribers: new Set(),
1388
+ };
1389
+ this.root.set(path, node);
1231
1390
  }
1391
+ node.value = value;
1392
+ node.version++;
1393
+ this.stateSnapshot = { ...this.stateSnapshot, [path]: value };
1394
+ node.subscribers.forEach((fn) => {
1395
+ try {
1396
+ fn();
1397
+ }
1398
+ catch { }
1399
+ });
1400
+ this.selectorCache.clear();
1232
1401
  }
1233
- if (vnode.props?.children) {
1234
- vnode.props.children.forEach((c) => unmountVNode(c));
1235
- }
1236
- vnode._domNode = null;
1237
- vnode._rendered = null;
1238
1402
  }
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);
1403
+ const hierarchicalStore = new HierarchicalStore();
1404
+ export function useHierarchicalStore() {
1405
+ return hierarchicalStore;
1406
+ }
1407
+ let rootRenderFn = null;
1408
+ function mount(AppComponent, root, props = {}) {
1409
+ if (typeof root === "string") {
1410
+ const el = document.querySelector(root);
1411
+ if (!el) {
1412
+ console.error(`[Fynix] mount: selector "${root}" not found`);
1413
+ return;
1414
+ }
1415
+ root = el;
1416
+ }
1417
+ const container = root;
1418
+ container.innerHTML = "";
1419
+ const win = window;
1420
+ const propsToUse = win.__lastRouteProps || win.__fynix__?.lastRouteProps || props;
1421
+ const appVNode = { type: AppComponent, props: propsToUse, key: null };
1422
+ fiberReconciler.mountRoot(appVNode, container);
1423
+ rootRenderFn = () => {
1424
+ const fiber = appVNode._fiber;
1425
+ if (fiber)
1426
+ fiberReconciler.scheduleUpdate(fiber, "high");
1427
+ };
1428
+ win.__fynix__ = win.__fynix__ || {};
1429
+ win.__fynix__.rerender = rootRenderFn;
1430
+ if (import.meta.hot && !win.__fynix__.hmr) {
1431
+ win.__fynix__.hmr = async ({ mod }) => {
1432
+ try {
1433
+ const UpdatedComponent = mod.App || mod.default;
1434
+ if (UpdatedComponent && appVNode._fiber) {
1435
+ appVNode._fiber.type = UpdatedComponent;
1436
+ fiberReconciler.scheduleUpdate(appVNode._fiber, "high");
1251
1437
  }
1252
1438
  }
1253
- else if (BOOLEAN_ATTRS.has(k.toLowerCase())) {
1254
- el.removeAttribute(k);
1255
- el[k] = false;
1256
- }
1257
- else if (DOM_PROPERTIES.has(k)) {
1258
- el[k] = "";
1259
- }
1260
- else {
1261
- el.removeAttribute(k);
1439
+ catch (err) {
1440
+ console.error("[Fynix HMR]", err);
1441
+ showErrorOverlay(err);
1262
1442
  }
1263
- }
1264
- }
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);
1270
- }
1443
+ };
1444
+ import.meta.hot.accept();
1271
1445
  }
1272
1446
  }
1273
- export function mount(AppComponent, root, props = {}) {
1447
+ function hydrate(AppComponent, root, props = {}) {
1274
1448
  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);
1449
+ const el = document.querySelector(root);
1450
+ if (!el) {
1451
+ console.error(`[Fynix] hydrate: selector "${root}" not found`);
1278
1452
  return;
1279
1453
  }
1280
- root = element;
1454
+ root = el;
1281
1455
  }
1282
- if (!(root instanceof Element)) {
1283
- console.error("[Fynix] Mount error: Invalid root element", root);
1284
- return;
1456
+ const container = root;
1457
+ const win = window;
1458
+ const propsToUse = win.__lastRouteProps || win.__fynix__?.lastRouteProps || props;
1459
+ const appVNode = { type: AppComponent, props: propsToUse, key: null };
1460
+ if (container.firstChild)
1461
+ appVNode._domNode = container.firstChild;
1462
+ fiberReconciler.mountRoot(appVNode, container);
1463
+ rootRenderFn = () => {
1464
+ const fiber = appVNode._fiber;
1465
+ if (fiber)
1466
+ fiberReconciler.scheduleUpdate(fiber, "high");
1467
+ };
1468
+ win.__fynix__ = win.__fynix__ || {};
1469
+ win.__fynix__.rerender = rootRenderFn;
1470
+ }
1471
+ const MEMO_VNODE_KEY = Symbol("fynix.memoVNode");
1472
+ export function memo(Component, propsAreEqual) {
1473
+ const isEqual = propsAreEqual || shallowEqual;
1474
+ const instanceCache = new WeakMap();
1475
+ return function MemoizedComponent(props) {
1476
+ const vnodeKey = props[MEMO_VNODE_KEY] ?? null;
1477
+ let cleanProps = props;
1478
+ if (vnodeKey) {
1479
+ cleanProps = { ...props };
1480
+ delete cleanProps[MEMO_VNODE_KEY];
1481
+ }
1482
+ if (vnodeKey) {
1483
+ const cached = instanceCache.get(vnodeKey);
1484
+ if (cached && isEqual(cached.props, cleanProps))
1485
+ return cached.result;
1486
+ }
1487
+ const result = Component(cleanProps);
1488
+ if (vnodeKey)
1489
+ instanceCache.set(vnodeKey, { props: cleanProps, result });
1490
+ return result;
1491
+ };
1492
+ }
1493
+ const renderComponentCache = new WeakMap();
1494
+ export function renderComponent(Component, props = {}) {
1495
+ const vnode = { type: Component, props, key: null };
1496
+ const ctx = beginComponent(vnode);
1497
+ ctx.Component = Component;
1498
+ if (!ctx.rerender)
1499
+ ctx.rerender = createRerender(ctx);
1500
+ try {
1501
+ removeErrorOverlay();
1502
+ const result = Component(props);
1503
+ if (result instanceof Promise) {
1504
+ const placeholder = h("div", null, "Loading...");
1505
+ ctx._vnode = vnode;
1506
+ vnode._rendered = placeholder;
1507
+ ctx._isMounted = true;
1508
+ result
1509
+ .then((resolved) => {
1510
+ vnode._rendered = resolved;
1511
+ renderComponentCache.set(vnode, {
1512
+ props,
1513
+ result: resolved,
1514
+ timestamp: performance.now(),
1515
+ });
1516
+ if (ctx.rerender)
1517
+ ctx.rerender();
1518
+ })
1519
+ .catch((err) => publishAsyncError(err instanceof Error ? err : new Error(String(err))));
1520
+ return placeholder;
1521
+ }
1522
+ ctx._vnode = vnode;
1523
+ vnode._rendered = result;
1524
+ ctx._isMounted = true;
1525
+ renderComponentCache.set(vnode, {
1526
+ props,
1527
+ result,
1528
+ timestamp: performance.now(),
1529
+ });
1530
+ return result;
1285
1531
  }
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;
1311
- }
1312
- oldVNode = appVNode;
1313
- }
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
- }
1324
- }
1532
+ catch (err) {
1533
+ const error = err instanceof Error ? err : new Error(String(err));
1534
+ if (errorConfig.logToConsole) {
1535
+ console.error("[Fynix] Component render error:", error);
1325
1536
  }
1326
- catch (err) {
1327
- console.error("[Fynix] Mount error:", err);
1328
- showErrorOverlay(err);
1537
+ const handled = errorConfig.onRenderError?.(error, Component);
1538
+ if (!handled && errorConfig.showOverlay) {
1539
+ showErrorOverlay(error);
1329
1540
  }
1330
- finally {
1331
- isRendering = false;
1541
+ return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(error)}`);
1542
+ }
1543
+ finally {
1544
+ endComponent();
1545
+ }
1546
+ }
1547
+ export function ErrorBoundary({ fallback, children, }) {
1548
+ let asyncError = null;
1549
+ const handleAsyncError = (error) => {
1550
+ asyncError = error;
1551
+ console.error("[Fynix] ErrorBoundary caught async error:", error);
1552
+ };
1553
+ asyncErrorHandlers.push(handleAsyncError);
1554
+ const removeHandler = () => {
1555
+ const idx = asyncErrorHandlers.indexOf(handleAsyncError);
1556
+ if (idx !== -1)
1557
+ asyncErrorHandlers.splice(idx, 1);
1558
+ };
1559
+ const ctx = activeContext;
1560
+ if (ctx)
1561
+ ctx.cleanups.push(removeHandler);
1562
+ try {
1563
+ if (asyncError) {
1564
+ removeHandler();
1565
+ return fallback(asyncError);
1332
1566
  }
1567
+ if (!children || children.length === 0)
1568
+ return h(Fragment, null);
1569
+ return h(Fragment, null, ...children);
1333
1570
  }
1334
- rootRenderFn = renderApp;
1335
- const win = window;
1336
- 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();
1571
+ catch (err) {
1572
+ removeHandler();
1573
+ const error = err instanceof Error ? err : new Error(String(err));
1574
+ console.error("[Fynix] ErrorBoundary caught:", error);
1575
+ try {
1576
+ return fallback(error);
1577
+ }
1578
+ catch (fe) {
1579
+ console.error("[Fynix] ErrorBoundary fallback also threw:", fe);
1580
+ return h("div", { style: "color:red" }, "[ErrorBoundary] Fatal render error");
1358
1581
  }
1359
1582
  }
1360
1583
  }
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, };
1584
+ 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, };