tova 0.3.4 → 0.3.6
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/bin/tova.js +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +172 -32
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +10 -15
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +144 -150
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
|
@@ -36,11 +36,35 @@ function flush() {
|
|
|
36
36
|
pendingEffects.clear();
|
|
37
37
|
break;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
|
|
40
|
+
// Invoke onBeforeUpdate callbacks for owners that have pending effects
|
|
41
|
+
const ownersNotified = new Set();
|
|
42
|
+
for (const effect of pendingEffects) {
|
|
43
|
+
const owner = effect._owner;
|
|
44
|
+
if (owner && owner._beforeUpdate && !ownersNotified.has(owner)) {
|
|
45
|
+
ownersNotified.add(owner);
|
|
46
|
+
for (const cb of owner._beforeUpdate) {
|
|
47
|
+
try { cb(); } catch (e) { console.error('Tova: onBeforeUpdate error:', e); }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toRun = pendingEffects;
|
|
53
|
+
pendingEffects = new Set();
|
|
54
|
+
// Sort by depth (parents first) to avoid redundant child re-runs
|
|
55
|
+
if (toRun.size > 1) {
|
|
56
|
+
const sorted = Array.from(toRun);
|
|
57
|
+
sorted.sort((a, b) => (a._depth || 0) - (b._depth || 0));
|
|
58
|
+
for (const effect of sorted) {
|
|
59
|
+
if (!effect._disposed) {
|
|
60
|
+
effect();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
for (const effect of toRun) {
|
|
65
|
+
if (!effect._disposed) {
|
|
66
|
+
effect();
|
|
67
|
+
}
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
}
|
|
@@ -73,10 +97,10 @@ export function createRoot(fn) {
|
|
|
73
97
|
dispose() {
|
|
74
98
|
if (root._disposed) return;
|
|
75
99
|
root._disposed = true;
|
|
76
|
-
// Dispose children in reverse order
|
|
100
|
+
// Dispose children in reverse order (skip already-disposed)
|
|
77
101
|
for (let i = root._children.length - 1; i >= 0; i--) {
|
|
78
102
|
const child = root._children[i];
|
|
79
|
-
if (typeof child.dispose === 'function') child.dispose();
|
|
103
|
+
if (!child._disposed && typeof child.dispose === 'function') child.dispose();
|
|
80
104
|
}
|
|
81
105
|
root._children.length = 0;
|
|
82
106
|
// Run cleanups in reverse order
|
|
@@ -144,7 +168,7 @@ export function createSignal(initialValue, name) {
|
|
|
144
168
|
if (__devtools_hooks && signalId != null) {
|
|
145
169
|
__devtools_hooks.onSignalUpdate(signalId, oldValue, newValue);
|
|
146
170
|
}
|
|
147
|
-
for (const sub of
|
|
171
|
+
for (const sub of subscribers) {
|
|
148
172
|
if (sub._isComputed) {
|
|
149
173
|
sub(); // propagate dirty flags synchronously through computed graph
|
|
150
174
|
} else {
|
|
@@ -218,6 +242,8 @@ export function createEffect(fn) {
|
|
|
218
242
|
effect._cleanup = null;
|
|
219
243
|
effect._cleanups = [];
|
|
220
244
|
effect._owner = currentOwner;
|
|
245
|
+
// Compute depth for priority scheduling (parents flush before children)
|
|
246
|
+
effect._depth = currentOwner ? (currentOwner._depth || 0) + 1 : 0;
|
|
221
247
|
|
|
222
248
|
if (__devtools_hooks) {
|
|
223
249
|
__devtools_hooks.onEffectCreate(effect);
|
|
@@ -232,11 +258,6 @@ export function createEffect(fn) {
|
|
|
232
258
|
runCleanups(effect);
|
|
233
259
|
cleanupDeps(effect);
|
|
234
260
|
pendingEffects.delete(effect);
|
|
235
|
-
// Remove from owner's children
|
|
236
|
-
if (effect._owner) {
|
|
237
|
-
const idx = effect._owner._children.indexOf(effect);
|
|
238
|
-
if (idx >= 0) effect._owner._children.splice(idx, 1);
|
|
239
|
-
}
|
|
240
261
|
};
|
|
241
262
|
|
|
242
263
|
// Run immediately (synchronous first run)
|
|
@@ -256,9 +277,10 @@ export function createComputed(fn) {
|
|
|
256
277
|
function notify() {
|
|
257
278
|
if (!dirty) {
|
|
258
279
|
dirty = true;
|
|
259
|
-
|
|
280
|
+
notify._dirty = true;
|
|
281
|
+
for (const sub of subscribers) {
|
|
260
282
|
if (sub._isComputed) {
|
|
261
|
-
sub(); //
|
|
283
|
+
if (!sub._dirty) sub(); // skip already-dirty computeds
|
|
262
284
|
} else {
|
|
263
285
|
pendingEffects.add(sub);
|
|
264
286
|
}
|
|
@@ -278,10 +300,6 @@ export function createComputed(fn) {
|
|
|
278
300
|
notify.dispose = function () {
|
|
279
301
|
notify._disposed = true;
|
|
280
302
|
cleanupDeps(notify);
|
|
281
|
-
if (notify._owner) {
|
|
282
|
-
const idx = notify._owner._children.indexOf(notify);
|
|
283
|
-
if (idx >= 0) notify._owner._children.splice(idx, 1);
|
|
284
|
-
}
|
|
285
303
|
};
|
|
286
304
|
|
|
287
305
|
function recompute() {
|
|
@@ -292,6 +310,7 @@ export function createComputed(fn) {
|
|
|
292
310
|
try {
|
|
293
311
|
value = fn();
|
|
294
312
|
dirty = false;
|
|
313
|
+
notify._dirty = false;
|
|
295
314
|
} finally {
|
|
296
315
|
effectStack.pop();
|
|
297
316
|
currentEffect = effectStack[effectStack.length - 1] || null;
|
|
@@ -339,6 +358,13 @@ export function onCleanup(fn) {
|
|
|
339
358
|
}
|
|
340
359
|
}
|
|
341
360
|
|
|
361
|
+
export function onBeforeUpdate(fn) {
|
|
362
|
+
if (currentOwner && !currentOwner._disposed) {
|
|
363
|
+
if (!currentOwner._beforeUpdate) currentOwner._beforeUpdate = [];
|
|
364
|
+
currentOwner._beforeUpdate.push(fn);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
342
368
|
// ─── Untrack ─────────────────────────────────────────────
|
|
343
369
|
// Run a function without tracking any signal reads (opt out of reactivity)
|
|
344
370
|
|
|
@@ -610,6 +636,40 @@ export function Portal({ target, children }) {
|
|
|
610
636
|
};
|
|
611
637
|
}
|
|
612
638
|
|
|
639
|
+
// ─── Suspense ────────────────────────────────────────────
|
|
640
|
+
// Renders fallback while any child lazy() component is loading.
|
|
641
|
+
// Usage: Suspense({ fallback: loadingEl, children: [LazyComp(props)] })
|
|
642
|
+
|
|
643
|
+
const SuspenseContext = createContext(null);
|
|
644
|
+
|
|
645
|
+
export function Suspense({ fallback, children }) {
|
|
646
|
+
const [pending, setPending] = createSignal(0);
|
|
647
|
+
const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);
|
|
648
|
+
|
|
649
|
+
const boundary = {
|
|
650
|
+
register() {
|
|
651
|
+
setPending(p => p + 1);
|
|
652
|
+
},
|
|
653
|
+
resolve() {
|
|
654
|
+
setPending(p => Math.max(0, p - 1));
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
__tova: true,
|
|
660
|
+
tag: '__dynamic',
|
|
661
|
+
props: {},
|
|
662
|
+
children: [],
|
|
663
|
+
compute: () => {
|
|
664
|
+
provide(SuspenseContext, boundary);
|
|
665
|
+
if (pending() > 0) {
|
|
666
|
+
return typeof fallback === 'function' ? fallback() : fallback;
|
|
667
|
+
}
|
|
668
|
+
return childContent;
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
613
673
|
// ─── Lazy ───────────────────────────────────────────────
|
|
614
674
|
// Async component loading with optional fallback.
|
|
615
675
|
// Usage: const LazyComp = lazy(() => import('./HeavyComponent.js'))
|
|
@@ -624,12 +684,20 @@ export function lazy(loader) {
|
|
|
624
684
|
return resolved(props);
|
|
625
685
|
}
|
|
626
686
|
|
|
687
|
+
// Check for Suspense boundary
|
|
688
|
+
const suspense = inject(SuspenseContext);
|
|
689
|
+
|
|
627
690
|
if (!promise) {
|
|
691
|
+
if (suspense) suspense.register();
|
|
628
692
|
promise = loader()
|
|
629
693
|
.then(mod => {
|
|
630
694
|
resolved = mod.default || mod;
|
|
695
|
+
if (suspense) suspense.resolve();
|
|
631
696
|
})
|
|
632
|
-
.catch(e => {
|
|
697
|
+
.catch(e => {
|
|
698
|
+
loadError = e;
|
|
699
|
+
if (suspense) suspense.resolve();
|
|
700
|
+
});
|
|
633
701
|
}
|
|
634
702
|
|
|
635
703
|
const [tick, setTick] = createSignal(0);
|
|
@@ -646,7 +714,7 @@ export function lazy(loader) {
|
|
|
646
714
|
tick(); // Track for reactivity
|
|
647
715
|
if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);
|
|
648
716
|
if (resolved) return resolved(props);
|
|
649
|
-
// Fallback while loading
|
|
717
|
+
// Fallback while loading (individual or Suspense-level)
|
|
650
718
|
return props && props.fallback ? props.fallback : null;
|
|
651
719
|
},
|
|
652
720
|
};
|
|
@@ -754,34 +822,92 @@ function getTransitionCSS(name, config, phase) {
|
|
|
754
822
|
}
|
|
755
823
|
}
|
|
756
824
|
|
|
757
|
-
export function tova_transition(vnode,
|
|
825
|
+
export function tova_transition(vnode, nameOrConfig, config = {}) {
|
|
826
|
+
if (!vnode || !vnode.__tova) return vnode;
|
|
827
|
+
|
|
828
|
+
// Directional transitions: tova_transition(vnode, { in: {...}, out: {...} })
|
|
829
|
+
if (typeof nameOrConfig === 'object' && nameOrConfig !== null && !nameOrConfig.__tova && (nameOrConfig.in || nameOrConfig.out)) {
|
|
830
|
+
vnode._transition = { directional: true, in: nameOrConfig.in, out: nameOrConfig.out };
|
|
831
|
+
return vnode;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Custom transition function: tova_transition(vnode, myTransitionFn, config)
|
|
835
|
+
if (typeof nameOrConfig === 'function') {
|
|
836
|
+
vnode._transition = { custom: nameOrConfig, config };
|
|
837
|
+
return vnode;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Built-in transition: tova_transition(vnode, "fade", config)
|
|
841
|
+
vnode._transition = { name: nameOrConfig, config };
|
|
842
|
+
return vnode;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ─── Actions ──────────────────────────────────────────────
|
|
846
|
+
// use: directive support. Calls actionFn(el, param) after render.
|
|
847
|
+
// Returns the wrapped vnode. The action lifecycle (update/destroy) is managed.
|
|
848
|
+
|
|
849
|
+
export function __tova_action(vnode, actionFn, param) {
|
|
758
850
|
if (!vnode || !vnode.__tova) return vnode;
|
|
759
|
-
vnode.
|
|
851
|
+
if (!vnode._actions) vnode._actions = [];
|
|
852
|
+
vnode._actions.push({ fn: actionFn, param });
|
|
760
853
|
return vnode;
|
|
761
854
|
}
|
|
762
855
|
|
|
763
856
|
// Apply enter transition to a DOM element after render
|
|
764
857
|
function applyEnterTransition(el, trans) {
|
|
765
858
|
if (!trans) return;
|
|
766
|
-
|
|
767
|
-
|
|
859
|
+
|
|
860
|
+
// Custom transition function
|
|
861
|
+
if (trans.custom) {
|
|
862
|
+
const result = trans.custom(el, trans.config || {}, 'enter');
|
|
863
|
+
if (result && typeof result === 'object' && !result.then) {
|
|
864
|
+
Object.assign(el.style, result);
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Directional: use 'in' config for enter
|
|
870
|
+
const name = trans.directional ? (trans.in ? trans.in.name : null) : trans.name;
|
|
871
|
+
const config = trans.directional ? (trans.in ? trans.in.config : {}) : trans.config;
|
|
872
|
+
if (!name) return;
|
|
873
|
+
|
|
874
|
+
const fromStyles = getTransitionCSS(name, config, 'enter-from');
|
|
875
|
+
const toStyles = getTransitionCSS(name, config, 'enter-to');
|
|
768
876
|
|
|
769
877
|
// Set initial state
|
|
770
878
|
Object.assign(el.style, fromStyles);
|
|
771
879
|
|
|
772
880
|
// Force reflow, then apply target state
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
Object.assign(el.style, toStyles);
|
|
776
|
-
});
|
|
777
|
-
});
|
|
881
|
+
void el.offsetHeight;
|
|
882
|
+
Object.assign(el.style, toStyles);
|
|
778
883
|
}
|
|
779
884
|
|
|
780
885
|
// Apply leave transition and return a Promise that resolves when done
|
|
781
886
|
function applyLeaveTransition(el, trans) {
|
|
782
887
|
if (!trans) return Promise.resolve();
|
|
783
|
-
|
|
784
|
-
|
|
888
|
+
|
|
889
|
+
// Custom transition function
|
|
890
|
+
if (trans.custom) {
|
|
891
|
+
const result = trans.custom(el, trans.config || {}, 'leave');
|
|
892
|
+
if (result && typeof result.then === 'function') {
|
|
893
|
+
// Race with timeout to prevent leaked promises from custom transitions
|
|
894
|
+
const dur = (trans.config && trans.config.duration) || 5000;
|
|
895
|
+
return Promise.race([result, new Promise(r => setTimeout(r, dur + 100))]);
|
|
896
|
+
}
|
|
897
|
+
if (result && typeof result === 'object') {
|
|
898
|
+
Object.assign(el.style, result);
|
|
899
|
+
}
|
|
900
|
+
const dur = (trans.config && trans.config.duration) || 200;
|
|
901
|
+
return new Promise(resolve => setTimeout(resolve, dur));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Directional: use 'out' config for leave
|
|
905
|
+
const name = trans.directional ? (trans.out ? trans.out.name : null) : trans.name;
|
|
906
|
+
const config = trans.directional ? (trans.out ? trans.out.config : {}) : trans.config;
|
|
907
|
+
if (!name) return Promise.resolve();
|
|
908
|
+
|
|
909
|
+
const duration = (config && config.duration) || TRANSITION_DEFAULTS[name]?.duration || 200;
|
|
910
|
+
const toStyles = getTransitionCSS(name, config, 'leave-to');
|
|
785
911
|
Object.assign(el.style, toStyles);
|
|
786
912
|
|
|
787
913
|
return new Promise(resolve => {
|
|
@@ -911,6 +1037,9 @@ function clearMarkerContent(marker) {
|
|
|
911
1037
|
applyLeaveTransition(el, el.__tovaTransition).then(() => {
|
|
912
1038
|
disposeNode(el);
|
|
913
1039
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
1040
|
+
}).catch(() => {
|
|
1041
|
+
disposeNode(el);
|
|
1042
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
914
1043
|
});
|
|
915
1044
|
} else {
|
|
916
1045
|
disposeNode(node);
|
|
@@ -1112,6 +1241,29 @@ export function render(vnode) {
|
|
|
1112
1241
|
applyEnterTransition(el, vnode._transition);
|
|
1113
1242
|
}
|
|
1114
1243
|
|
|
1244
|
+
// Apply use: actions if present
|
|
1245
|
+
if (vnode._actions && vnode._actions.length > 0) {
|
|
1246
|
+
for (const action of vnode._actions) {
|
|
1247
|
+
const paramValue = typeof action.param === 'function' ? action.param() : action.param;
|
|
1248
|
+
const result = action.fn(el, paramValue);
|
|
1249
|
+
if (result) {
|
|
1250
|
+
// If param is reactive, set up effect for updates
|
|
1251
|
+
if (typeof action.param === 'function') {
|
|
1252
|
+
createEffect(() => {
|
|
1253
|
+
const newVal = action.param();
|
|
1254
|
+
if (result.update) result.update(newVal);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
// Register destroy on cleanup
|
|
1258
|
+
if (result.destroy) {
|
|
1259
|
+
if (currentOwner && !currentOwner._disposed) {
|
|
1260
|
+
currentOwner._cleanups.push(result.destroy);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1115
1267
|
return el;
|
|
1116
1268
|
}
|
|
1117
1269
|
|
|
@@ -1126,9 +1278,17 @@ function applyReactiveProps(el, props) {
|
|
|
1126
1278
|
}
|
|
1127
1279
|
} else if (key.startsWith('on')) {
|
|
1128
1280
|
const eventName = key.slice(2).toLowerCase();
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1281
|
+
if (typeof value === 'object' && value !== null && value.handler) {
|
|
1282
|
+
el.addEventListener(eventName, value.handler, value.options);
|
|
1283
|
+
if (!el.__handlers) el.__handlers = {};
|
|
1284
|
+
el.__handlers[eventName] = value.handler;
|
|
1285
|
+
el.__handlerOptions = el.__handlerOptions || {};
|
|
1286
|
+
el.__handlerOptions[eventName] = value.options;
|
|
1287
|
+
} else {
|
|
1288
|
+
el.addEventListener(eventName, value);
|
|
1289
|
+
if (!el.__handlers) el.__handlers = {};
|
|
1290
|
+
el.__handlers[eventName] = value;
|
|
1291
|
+
}
|
|
1132
1292
|
} else if (key === 'key') {
|
|
1133
1293
|
// Skip
|
|
1134
1294
|
} else if (typeof value === 'function' && !key.startsWith('on')) {
|
|
@@ -1148,6 +1308,9 @@ function applyPropValue(el, key, val) {
|
|
|
1148
1308
|
if (el.className !== val) el.className = val || '';
|
|
1149
1309
|
} else if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {
|
|
1150
1310
|
const html = typeof val === 'object' && val !== null ? val.__html || '' : val || '';
|
|
1311
|
+
if (__DEV__ && html) {
|
|
1312
|
+
console.warn('Tova: Setting innerHTML can expose your app to XSS attacks. Ensure the content is sanitized.');
|
|
1313
|
+
}
|
|
1151
1314
|
if (el.innerHTML !== html) el.innerHTML = html;
|
|
1152
1315
|
} else if (key === 'value') {
|
|
1153
1316
|
if (el !== document.activeElement && el.value !== val) {
|
|
@@ -1158,14 +1321,13 @@ function applyPropValue(el, key, val) {
|
|
|
1158
1321
|
} else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {
|
|
1159
1322
|
el[key] = !!val;
|
|
1160
1323
|
} else if (key === 'style' && typeof val === 'object') {
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1163
|
-
const prop
|
|
1164
|
-
|
|
1165
|
-
if (!(prop in val) && !(camel in val)) {
|
|
1166
|
-
el.style.removeProperty(prop);
|
|
1324
|
+
// Delta update: only remove properties that were in previous style but not in new
|
|
1325
|
+
if (el.__prevStyle) {
|
|
1326
|
+
for (const prop of Object.keys(el.__prevStyle)) {
|
|
1327
|
+
if (!(prop in val)) el.style.removeProperty(prop);
|
|
1167
1328
|
}
|
|
1168
1329
|
}
|
|
1330
|
+
el.__prevStyle = { ...val };
|
|
1169
1331
|
Object.assign(el.style, val);
|
|
1170
1332
|
} else {
|
|
1171
1333
|
const s = val == null ? '' : String(val);
|
|
@@ -1209,12 +1371,25 @@ function applyProps(el, newProps, oldProps) {
|
|
|
1209
1371
|
}
|
|
1210
1372
|
} else if (key.startsWith('on')) {
|
|
1211
1373
|
const eventName = key.slice(2).toLowerCase();
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
if (oldHandler
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1374
|
+
if (typeof value === 'object' && value !== null && value.handler) {
|
|
1375
|
+
const oldHandler = el.__handlers && el.__handlers[eventName];
|
|
1376
|
+
if (oldHandler !== value.handler) {
|
|
1377
|
+
const oldOpts = el.__handlerOptions && el.__handlerOptions[eventName];
|
|
1378
|
+
if (oldHandler) el.removeEventListener(eventName, oldHandler, oldOpts);
|
|
1379
|
+
el.addEventListener(eventName, value.handler, value.options);
|
|
1380
|
+
if (!el.__handlers) el.__handlers = {};
|
|
1381
|
+
el.__handlers[eventName] = value.handler;
|
|
1382
|
+
el.__handlerOptions = el.__handlerOptions || {};
|
|
1383
|
+
el.__handlerOptions[eventName] = value.options;
|
|
1384
|
+
}
|
|
1385
|
+
} else {
|
|
1386
|
+
const oldHandler = el.__handlers && el.__handlers[eventName];
|
|
1387
|
+
if (oldHandler !== value) {
|
|
1388
|
+
if (oldHandler) el.removeEventListener(eventName, oldHandler);
|
|
1389
|
+
el.addEventListener(eventName, value);
|
|
1390
|
+
if (!el.__handlers) el.__handlers = {};
|
|
1391
|
+
el.__handlers[eventName] = value;
|
|
1392
|
+
}
|
|
1218
1393
|
}
|
|
1219
1394
|
} else if (key === 'style' && typeof value === 'object') {
|
|
1220
1395
|
Object.assign(el.style, value);
|
|
@@ -1236,6 +1411,51 @@ function applyProps(el, newProps, oldProps) {
|
|
|
1236
1411
|
}
|
|
1237
1412
|
}
|
|
1238
1413
|
|
|
1414
|
+
// ─── Longest Increasing Subsequence (O(n log n)) ────────
|
|
1415
|
+
// Used by keyed reconciliation to minimize DOM moves.
|
|
1416
|
+
|
|
1417
|
+
function longestIncreasingSubsequence(arr) {
|
|
1418
|
+
const n = arr.length;
|
|
1419
|
+
if (n === 0) return [];
|
|
1420
|
+
|
|
1421
|
+
// tails[i] = index in arr of smallest tail element for IS of length i+1
|
|
1422
|
+
const tails = [];
|
|
1423
|
+
// parent[i] = index in arr of predecessor of arr[i] in the LIS
|
|
1424
|
+
const parent = new Array(n).fill(-1);
|
|
1425
|
+
// indices[i] = index in arr of tails[i]
|
|
1426
|
+
const indices = [];
|
|
1427
|
+
|
|
1428
|
+
for (let i = 0; i < n; i++) {
|
|
1429
|
+
const val = arr[i];
|
|
1430
|
+
if (val < 0) continue; // skip removed items (marker -1)
|
|
1431
|
+
|
|
1432
|
+
// Binary search for the insertion point
|
|
1433
|
+
let lo = 0, hi = tails.length;
|
|
1434
|
+
while (lo < hi) {
|
|
1435
|
+
const mid = (lo + hi) >> 1;
|
|
1436
|
+
if (tails[mid] < val) lo = mid + 1;
|
|
1437
|
+
else hi = mid;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
tails[lo] = val;
|
|
1441
|
+
indices[lo] = i;
|
|
1442
|
+
|
|
1443
|
+
if (lo > 0) {
|
|
1444
|
+
parent[i] = indices[lo - 1];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Reconstruct
|
|
1449
|
+
const result = new Array(tails.length);
|
|
1450
|
+
let k = indices[tails.length - 1];
|
|
1451
|
+
for (let i = tails.length - 1; i >= 0; i--) {
|
|
1452
|
+
result[i] = k;
|
|
1453
|
+
k = parent[k];
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1239
1459
|
// ─── Keyed Reconciliation ────────────────────────────────
|
|
1240
1460
|
|
|
1241
1461
|
function getKey(vnode) {
|
|
@@ -1311,10 +1531,19 @@ function patchKeyedInMarker(marker, newVNodes) {
|
|
|
1311
1531
|
}
|
|
1312
1532
|
}
|
|
1313
1533
|
|
|
1314
|
-
//
|
|
1534
|
+
// LIS-based reorder: compute old positions, find LIS, only move non-LIS nodes
|
|
1535
|
+
const oldPosMap = new Map();
|
|
1536
|
+
for (let i = 0; i < oldNodes.length; i++) {
|
|
1537
|
+
oldPosMap.set(oldNodes[i], i);
|
|
1538
|
+
}
|
|
1539
|
+
const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);
|
|
1540
|
+
const lisIndices = new Set(longestIncreasingSubsequence(positions));
|
|
1541
|
+
|
|
1542
|
+
// Insert nodes: only move nodes not in the LIS
|
|
1315
1543
|
let cursor = marker.nextSibling;
|
|
1316
|
-
for (
|
|
1317
|
-
|
|
1544
|
+
for (let i = 0; i < newNodes.length; i++) {
|
|
1545
|
+
const node = newNodes[i];
|
|
1546
|
+
if (lisIndices.has(i) && node === cursor) {
|
|
1318
1547
|
cursor = node.nextSibling;
|
|
1319
1548
|
} else {
|
|
1320
1549
|
parent.insertBefore(node, cursor);
|
|
@@ -1331,9 +1560,10 @@ function patchPositionalInMarker(marker, newChildren) {
|
|
|
1331
1560
|
const oldCount = oldNodes.length;
|
|
1332
1561
|
const newCount = newChildren.length;
|
|
1333
1562
|
|
|
1334
|
-
// Patch in place
|
|
1563
|
+
// Patch in place (skip identical vnodes)
|
|
1335
1564
|
const patchCount = Math.min(oldCount, newCount);
|
|
1336
1565
|
for (let i = 0; i < patchCount; i++) {
|
|
1566
|
+
if (oldNodes[i] === newChildren[i]) continue;
|
|
1337
1567
|
patchSingle(parent, oldNodes[i], newChildren[i]);
|
|
1338
1568
|
}
|
|
1339
1569
|
|
|
@@ -1396,13 +1626,23 @@ function patchKeyedChildren(parent, newVNodes) {
|
|
|
1396
1626
|
}
|
|
1397
1627
|
}
|
|
1398
1628
|
|
|
1399
|
-
//
|
|
1629
|
+
// LIS-based reorder for element children
|
|
1630
|
+
const logicalAfterRemove = getLogicalChildren(parent);
|
|
1631
|
+
const oldPosMap = new Map();
|
|
1632
|
+
for (let i = 0; i < logicalAfterRemove.length; i++) {
|
|
1633
|
+
oldPosMap.set(logicalAfterRemove[i], i);
|
|
1634
|
+
}
|
|
1635
|
+
const positions = newNodes.map(n => oldPosMap.has(n) ? oldPosMap.get(n) : -1);
|
|
1636
|
+
const lisIndices = new Set(longestIncreasingSubsequence(positions));
|
|
1637
|
+
|
|
1400
1638
|
for (let i = 0; i < newNodes.length; i++) {
|
|
1401
1639
|
const expected = newNodes[i];
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1640
|
+
if (!lisIndices.has(i)) {
|
|
1641
|
+
const logicalNow = getLogicalChildren(parent);
|
|
1642
|
+
const current = logicalNow[i];
|
|
1643
|
+
if (current !== expected) {
|
|
1644
|
+
parent.insertBefore(expected, current || null);
|
|
1645
|
+
}
|
|
1406
1646
|
}
|
|
1407
1647
|
}
|
|
1408
1648
|
}
|
|
@@ -1713,9 +1953,17 @@ function hydrateProps(el, props) {
|
|
|
1713
1953
|
}
|
|
1714
1954
|
} else if (key.startsWith('on')) {
|
|
1715
1955
|
const eventName = key.slice(2).toLowerCase();
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1956
|
+
if (typeof value === 'object' && value !== null && value.handler) {
|
|
1957
|
+
el.addEventListener(eventName, value.handler, value.options);
|
|
1958
|
+
if (!el.__handlers) el.__handlers = {};
|
|
1959
|
+
el.__handlers[eventName] = value.handler;
|
|
1960
|
+
el.__handlerOptions = el.__handlerOptions || {};
|
|
1961
|
+
el.__handlerOptions[eventName] = value.options;
|
|
1962
|
+
} else {
|
|
1963
|
+
el.addEventListener(eventName, value);
|
|
1964
|
+
if (!el.__handlers) el.__handlers = {};
|
|
1965
|
+
el.__handlers[eventName] = value;
|
|
1966
|
+
}
|
|
1719
1967
|
} else if (key === 'key') {
|
|
1720
1968
|
// Skip
|
|
1721
1969
|
} else if (typeof value === 'function' && !key.startsWith('on')) {
|