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/LICENSE +21 -21
- package/README.md +396 -0
- package/dist/README.md +396 -0
- package/dist/custom/DataTable.js +359 -0
- package/dist/custom/button.js +241 -1
- package/dist/custom/index.js +2 -1
- package/dist/error/errorOverlay.js +1 -1
- package/dist/hooks/nixFor.js +15 -17
- package/dist/package.json +2 -3
- package/dist/plugins/vite-plugin-res.js +26 -4
- package/dist/router/router.js +108 -217
- package/dist/runtime.js +1261 -1026
- package/dist-types/context/context.d.ts +1 -1
- package/dist-types/custom/DataTable.d.ts +54 -0
- package/dist-types/custom/button.d.ts +35 -1
- package/dist-types/custom/index.d.ts +2 -1
- package/dist-types/hooks/nixFor.d.ts +1 -1
- package/dist-types/router/router.d.ts +14 -10
- package/dist-types/runtime.d.ts +118 -40
- package/package.json +256 -254
- package/types/fnx.d.ts +34 -34
- package/types/global.d.ts +277 -279
- package/types/jsx.d.ts +1046 -993
- package/types/vite-env.d.ts +545 -545
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.
|
|
28
|
-
this.
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
if (!this.heap.length)
|
|
150
|
+
return undefined;
|
|
151
|
+
return this.heap[0]?.item;
|
|
45
152
|
}
|
|
46
153
|
size() {
|
|
47
|
-
return this.
|
|
154
|
+
return this.heap.length;
|
|
48
155
|
}
|
|
49
156
|
isEmpty() {
|
|
50
|
-
return this.
|
|
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.
|
|
167
|
+
this.idCounter = 0;
|
|
61
168
|
}
|
|
62
169
|
schedule(update, priority = "normal") {
|
|
63
|
-
update.id = `
|
|
170
|
+
update.id = `u_${this.idCounter++}`;
|
|
64
171
|
update.priority = priority;
|
|
65
172
|
update.timestamp = performance.now();
|
|
66
173
|
if (priority === "immediate") {
|
|
67
|
-
this.
|
|
174
|
+
this.flushOne(update);
|
|
68
175
|
}
|
|
69
176
|
else {
|
|
70
177
|
this.updateQueue.push(update, priority);
|
|
71
|
-
this.
|
|
178
|
+
this.kick();
|
|
72
179
|
}
|
|
73
180
|
}
|
|
74
181
|
batchUpdates(updates) {
|
|
75
|
-
updates.forEach((
|
|
76
|
-
this.
|
|
182
|
+
updates.forEach((u) => this.batchedUpdates.add(u));
|
|
183
|
+
this.kick();
|
|
77
184
|
}
|
|
78
185
|
timeSlice(deadline) {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
while (!this.updateQueue.isEmpty() &&
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 =
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
const
|
|
119
|
-
this.currentPriority =
|
|
218
|
+
flushOne(u) {
|
|
219
|
+
const prev = this.currentPriority;
|
|
220
|
+
this.currentPriority = u.priority;
|
|
120
221
|
try {
|
|
121
|
-
|
|
222
|
+
u.callback();
|
|
122
223
|
}
|
|
123
|
-
catch (
|
|
124
|
-
console.error("[FynixScheduler]
|
|
125
|
-
showErrorOverlay(
|
|
224
|
+
catch (e) {
|
|
225
|
+
console.error("[FynixScheduler]", e);
|
|
226
|
+
showErrorOverlay(e);
|
|
126
227
|
}
|
|
127
228
|
finally {
|
|
128
|
-
this.currentPriority =
|
|
229
|
+
this.currentPriority = prev;
|
|
129
230
|
}
|
|
130
231
|
}
|
|
131
|
-
|
|
232
|
+
kick() {
|
|
132
233
|
if (this.isScheduled)
|
|
133
234
|
return;
|
|
134
235
|
this.isScheduled = true;
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
if (hasMoreWork) {
|
|
251
|
+
loop(deadline) {
|
|
252
|
+
if (!this.timeSlice(deadline)) {
|
|
155
253
|
this.isScheduled = false;
|
|
156
|
-
this.
|
|
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
|
|
167
|
-
if (!
|
|
264
|
+
const next = this.updateQueue.peek();
|
|
265
|
+
if (!next)
|
|
168
266
|
return false;
|
|
169
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
272
|
+
executeBatchedCallbacks(cbs) {
|
|
273
|
+
const store = getCurrentBatchStore();
|
|
274
|
+
if (store) {
|
|
275
|
+
store.callbacks.push(...cbs);
|
|
205
276
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
382
|
-
return
|
|
355
|
+
catch {
|
|
356
|
+
return 0;
|
|
383
357
|
}
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
+
"<": "<",
|
|
441
|
+
">": ">",
|
|
442
|
+
'"': """,
|
|
443
|
+
"'": "'",
|
|
444
|
+
"&": "&",
|
|
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) => ({ '"': """, "'": "'", "<": "<", ">": ">" })[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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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.
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
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
|
-
"<": "<",
|
|
765
|
-
">": ">",
|
|
766
|
-
'"': """,
|
|
767
|
-
"'": "'",
|
|
768
|
-
"&": "&",
|
|
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
|
-
'"': """,
|
|
782
|
-
"'": "'",
|
|
783
|
-
"<": "<",
|
|
784
|
-
">": ">",
|
|
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
|
|
798
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
838
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
836
|
+
ctx._subscriptionCleanups.forEach((u) => {
|
|
837
|
+
try {
|
|
838
|
+
u();
|
|
880
839
|
}
|
|
881
|
-
|
|
882
|
-
|
|
840
|
+
catch { }
|
|
841
|
+
});
|
|
842
|
+
ctx.cleanups.forEach((c) => {
|
|
843
|
+
try {
|
|
844
|
+
c?.();
|
|
883
845
|
}
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
947
|
+
else {
|
|
948
|
+
this.updateHostFiber(fiber);
|
|
1010
949
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
componentInstances.
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
-
|
|
1128
|
+
if (i === 0)
|
|
1129
|
+
wipFiber.child = newFiber;
|
|
1130
|
+
else if (prevSibling)
|
|
1131
|
+
prevSibling.sibling = newFiber;
|
|
1132
|
+
prevSibling = newFiber;
|
|
1158
1133
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1152
|
+
else if (this.wipRoot.child) {
|
|
1153
|
+
this.commitWork(this.wipRoot.child);
|
|
1178
1154
|
}
|
|
1179
|
-
|
|
1155
|
+
this.wipRoot = null;
|
|
1156
|
+
this.wipEntry = null;
|
|
1157
|
+
this.nextWork = null;
|
|
1180
1158
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
|
|
1198
|
-
|
|
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 (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (
|
|
1250
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
el[k] = false;
|
|
1252
|
+
catch (e) {
|
|
1253
|
+
console.error("[FynixReconciler] Error unmounting fiber:", e);
|
|
1256
1254
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
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
|
|
1276
|
-
if (!
|
|
1277
|
-
console.error(
|
|
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 =
|
|
1427
|
+
root = el;
|
|
1281
1428
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
1327
|
-
console.error("[Fynix] Mount error:", err);
|
|
1328
|
-
showErrorOverlay(err);
|
|
1329
|
-
}
|
|
1330
|
-
finally {
|
|
1331
|
-
isRendering = false;
|
|
1332
|
-
}
|
|
1466
|
+
root = el;
|
|
1333
1467
|
}
|
|
1334
|
-
|
|
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 =
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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, };
|