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.
- package/dist/custom/DataTable.js +1 -0
- package/dist/custom/button.js +241 -1
- package/dist/custom/index.js +1 -1
- package/dist/error/errorOverlay.js +1 -1
- package/dist/hooks/nixFor.js +6 -4
- package/dist/package.json +34 -37
- package/dist/plugins/vite-plugin-res.js +26 -4
- package/dist/router/router.js +108 -217
- package/dist/runtime.js +1251 -1028
- package/{dist → dist-types}/context/context.d.ts +1 -2
- package/dist-types/custom/DataTable.d.ts +0 -0
- package/dist-types/custom/button.d.ts +35 -0
- package/dist-types/custom/index.d.ts +2 -0
- package/{dist → dist-types}/custom/path.d.ts +0 -1
- package/{dist → dist-types}/error/errorOverlay.d.ts +0 -1
- package/{dist → dist-types}/fynix/index.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixAsync.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixAsyncCache.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixAsyncDebounce.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixAsyncQuery.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixCallback.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixComputed.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixDebounce.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixEffect.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixFor.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixForm.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixFormAsync.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixInterval.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixLazy.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixLazyAsync.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixLazyFormAsync.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixLocalStorage.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixMemo.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixPrevious.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixRef.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixState.d.ts +0 -1
- package/{dist → dist-types}/hooks/nixStore.d.ts +0 -1
- package/{dist → dist-types}/plugins/vite-plugin-res.d.ts +0 -1
- package/{dist → dist-types}/router/router.d.ts +14 -11
- package/dist-types/runtime.d.ts +200 -0
- package/package.json +43 -41
- package/dist/context/context.d.ts.map +0 -1
- package/dist/context/context.js.map +0 -7
- package/dist/custom/button.d.ts +0 -2
- package/dist/custom/button.d.ts.map +0 -1
- package/dist/custom/button.js.map +0 -7
- package/dist/custom/index.d.ts +0 -3
- package/dist/custom/index.d.ts.map +0 -1
- package/dist/custom/index.js.map +0 -7
- package/dist/custom/path.d.ts.map +0 -1
- package/dist/custom/path.js.map +0 -7
- package/dist/error/errorOverlay.d.ts.map +0 -1
- package/dist/error/errorOverlay.js.map +0 -7
- package/dist/fynix/index.d.ts.map +0 -1
- package/dist/fynix/index.js.map +0 -7
- package/dist/hooks/nixAsync.d.ts.map +0 -1
- package/dist/hooks/nixAsync.js.map +0 -7
- package/dist/hooks/nixAsyncCache.d.ts.map +0 -1
- package/dist/hooks/nixAsyncCache.js.map +0 -7
- package/dist/hooks/nixAsyncDebounce.d.ts.map +0 -1
- package/dist/hooks/nixAsyncDebounce.js.map +0 -7
- package/dist/hooks/nixAsyncQuery.d.ts.map +0 -1
- package/dist/hooks/nixAsyncQuery.js.map +0 -7
- package/dist/hooks/nixCallback.d.ts.map +0 -1
- package/dist/hooks/nixCallback.js.map +0 -7
- package/dist/hooks/nixComputed.d.ts.map +0 -1
- package/dist/hooks/nixComputed.js.map +0 -7
- package/dist/hooks/nixDebounce.d.ts.map +0 -1
- package/dist/hooks/nixDebounce.js.map +0 -7
- package/dist/hooks/nixEffect.d.ts.map +0 -1
- package/dist/hooks/nixEffect.js.map +0 -7
- package/dist/hooks/nixFor.d.ts.map +0 -1
- package/dist/hooks/nixFor.js.map +0 -7
- package/dist/hooks/nixForm.d.ts.map +0 -1
- package/dist/hooks/nixForm.js.map +0 -7
- package/dist/hooks/nixFormAsync.d.ts.map +0 -1
- package/dist/hooks/nixFormAsync.js.map +0 -7
- package/dist/hooks/nixInterval.d.ts.map +0 -1
- package/dist/hooks/nixInterval.js.map +0 -7
- package/dist/hooks/nixLazy.d.ts.map +0 -1
- package/dist/hooks/nixLazy.js.map +0 -7
- package/dist/hooks/nixLazyAsync.d.ts.map +0 -1
- package/dist/hooks/nixLazyAsync.js.map +0 -7
- package/dist/hooks/nixLazyFormAsync.d.ts.map +0 -1
- package/dist/hooks/nixLazyFormAsync.js.map +0 -7
- package/dist/hooks/nixLocalStorage.d.ts.map +0 -1
- package/dist/hooks/nixLocalStorage.js.map +0 -7
- package/dist/hooks/nixMemo.d.ts.map +0 -1
- package/dist/hooks/nixMemo.js.map +0 -7
- package/dist/hooks/nixPrevious.d.ts.map +0 -1
- package/dist/hooks/nixPrevious.js.map +0 -7
- package/dist/hooks/nixRef.d.ts.map +0 -1
- package/dist/hooks/nixRef.js.map +0 -7
- package/dist/hooks/nixState.d.ts.map +0 -1
- package/dist/hooks/nixState.js.map +0 -7
- package/dist/hooks/nixStore.d.ts.map +0 -1
- package/dist/hooks/nixStore.js.map +0 -7
- package/dist/plugins/vite-plugin-res.d.ts.map +0 -1
- package/dist/plugins/vite-plugin-res.js.map +0 -7
- package/dist/router/router.d.ts.map +0 -1
- package/dist/router/router.js.map +0 -7
- package/dist/runtime.d.ts +0 -124
- package/dist/runtime.d.ts.map +0 -1
- 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.
|
|
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,13 @@ 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
|
+
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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 (
|
|
640
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
};
|
|
770
|
-
|
|
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);
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
874
|
+
}
|
|
875
|
+
class FiberReconciler {
|
|
876
|
+
constructor() {
|
|
877
|
+
this.wipRoot = null;
|
|
878
|
+
this.nextWork = null;
|
|
879
|
+
this.deletions = [];
|
|
816
880
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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 (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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 (
|
|
846
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
873
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
if (
|
|
917
|
-
|
|
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
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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 (
|
|
1009
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1114
|
+
if (matchFiber) {
|
|
1115
|
+
matchFiber.effectTag = "DELETION";
|
|
1116
|
+
this.deletions.push(matchFiber);
|
|
1073
1117
|
}
|
|
1074
|
-
|
|
1118
|
+
newFiber = this.vnodeToFiber(el, wipFiber, null);
|
|
1119
|
+
newFiber.effectTag = "PLACEMENT";
|
|
1075
1120
|
}
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1182
|
-
const
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
1447
|
+
function hydrate(AppComponent, root, props = {}) {
|
|
1274
1448
|
if (typeof root === "string") {
|
|
1275
|
-
const
|
|
1276
|
-
if (!
|
|
1277
|
-
console.error(
|
|
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 =
|
|
1454
|
+
root = el;
|
|
1281
1455
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
showErrorOverlay(
|
|
1537
|
+
const handled = errorConfig.onRenderError?.(error, Component);
|
|
1538
|
+
if (!handled && errorConfig.showOverlay) {
|
|
1539
|
+
showErrorOverlay(error);
|
|
1329
1540
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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, };
|