@vmz/core 0.0.2 → 0.0.4
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/README.md +4 -2
- package/dist/client-nav.d.ts +46 -0
- package/dist/client-nav.js +186 -0
- package/dist/dom.d.ts +28 -12
- package/dist/dom.js +1540 -226
- package/dist/serve-host.mjs +546 -90
- package/dist/server.d.ts +1 -2
- package/dist/server.js +103 -18
- package/package.json +6 -2
- package/dist/serve-host.js +0 -736
package/dist/dom.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* VMZ DOM / SSR runtime — precise patches, no VDOM diff.
|
|
4
4
|
*
|
|
5
|
-
* Design: 规划设计/vmz/04 · Gate 3 (no production `render()`)
|
|
6
5
|
*
|
|
7
6
|
* Direct components expose `__vmzCreate` / `__vmzSerialize` / `__vmzPlan`.
|
|
8
7
|
* Mount, SSR, hydrate, and resume all run that same schedule.
|
|
@@ -12,7 +11,6 @@
|
|
|
12
11
|
const components = Object.create(null);
|
|
13
12
|
/**
|
|
14
13
|
* Precision lab counters (test / MCP / benchmarks — not a user API).
|
|
15
|
-
* Design: 规划设计/vmz/12 §7
|
|
16
14
|
* Primary keys: BindingId (IR). `*ByDep` is transitional stable-string adapter.
|
|
17
15
|
*/
|
|
18
16
|
const precision = {
|
|
@@ -35,7 +33,7 @@ const precision = {
|
|
|
35
33
|
/** @type {Record<string, number>} BindingId → count */
|
|
36
34
|
patchesByBinding: Object.create(null),
|
|
37
35
|
};
|
|
38
|
-
/**
|
|
36
|
+
/** optional StableId event ring (enabled with precision or __vmzTraceEnable). */
|
|
39
37
|
const TRACE_CAP = 256;
|
|
40
38
|
/** @type {{ enabled: boolean, events: Array<{ kind: string, stableId: { kind: string, id: string }, dep?: string|null, t?: number, chunkId?: string|null }> }} */
|
|
41
39
|
const traceBuf = {
|
|
@@ -86,7 +84,7 @@ export function __vmzTraceReset() {
|
|
|
86
84
|
traceBuf.events = [];
|
|
87
85
|
}
|
|
88
86
|
/**
|
|
89
|
-
*
|
|
87
|
+
* StableId event snapshot (`vmz.dx.trace.v0` shape without schema stamp —
|
|
90
88
|
* host may wrap via ingestRuntimeTrace).
|
|
91
89
|
* @returns {{ schema: string, events: typeof traceBuf.events, status: string }}
|
|
92
90
|
*/
|
|
@@ -168,16 +166,23 @@ async function resolveComponent(name) {
|
|
|
168
166
|
* @param {new (props?: object) => any} Component
|
|
169
167
|
* @param {object} [props]
|
|
170
168
|
*/
|
|
171
|
-
export async function renderToString(Component, props = {}) {
|
|
169
|
+
export async function renderToString(Component, props = {}, opts = {}) {
|
|
170
|
+
const signal = opts && opts.signal;
|
|
171
|
+
if (signal && signal.aborted)
|
|
172
|
+
return '';
|
|
172
173
|
const inst = createInstance(Component, props);
|
|
173
174
|
if (typeof inst.onMount === 'function') {
|
|
174
175
|
await inst.onMount();
|
|
175
176
|
}
|
|
176
|
-
|
|
177
|
+
if (signal && signal.aborted)
|
|
178
|
+
return '';
|
|
179
|
+
// production Direct emit: SSR only via Direct serialize schedule — never `render`.
|
|
177
180
|
if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
|
|
178
|
-
throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (
|
|
181
|
+
throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
179
182
|
}
|
|
180
183
|
const root = await runDirectSerializeTreeWithMounts(Component, inst);
|
|
184
|
+
if (opts && opts.slotHtml != null)
|
|
185
|
+
injectDefaultSlotHtml(root, opts.slotHtml);
|
|
181
186
|
return flattenSerializeNode(root);
|
|
182
187
|
}
|
|
183
188
|
/**
|
|
@@ -186,7 +191,7 @@ export async function renderToString(Component, props = {}) {
|
|
|
186
191
|
* Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
|
|
187
192
|
* @param {new (props?: object) => any} Component
|
|
188
193
|
* @param {object} [props]
|
|
189
|
-
* @param {{ signal?: AbortSignal }} [opts]
|
|
194
|
+
* @param {{ signal?: AbortSignal, slotHtml?: string }} [opts]
|
|
190
195
|
* @returns {AsyncGenerator<string, void, void>}
|
|
191
196
|
*/
|
|
192
197
|
export async function* renderToStream(Component, props = {}, opts = {}) {
|
|
@@ -202,9 +207,11 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
|
|
|
202
207
|
if (aborted())
|
|
203
208
|
return;
|
|
204
209
|
if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
|
|
205
|
-
throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (
|
|
210
|
+
throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
206
211
|
}
|
|
207
212
|
const root = await runDirectSerializeTreeWithMounts(Component, inst);
|
|
213
|
+
if (opts && opts.slotHtml != null)
|
|
214
|
+
injectDefaultSlotHtml(root, opts.slotHtml);
|
|
208
215
|
if (aborted())
|
|
209
216
|
return;
|
|
210
217
|
for (const chunk of streamSerializeChunks(root)) {
|
|
@@ -216,13 +223,118 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
|
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
finally {
|
|
219
|
-
// Abort and normal completion both dispose the SSR instance (
|
|
226
|
+
// Abort and normal completion both dispose the SSR instance (lifetime).
|
|
220
227
|
destroy(inst);
|
|
221
228
|
}
|
|
222
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Fill the layout-owned default `<slot>` with pre-rendered HTML (layout SSR wrap).
|
|
232
|
+
* Skips nested component hosts (`data-vmz`) — their slots are for child projection
|
|
233
|
+
* (e.g. Button label), not the page outlet. Without this, DFS hits LocaleToggle→Button
|
|
234
|
+
* before Layout's `<main><slot>`, and the entire page HTML lands inside a button.
|
|
235
|
+
* @param {any} node
|
|
236
|
+
* @param {string} html
|
|
237
|
+
*/
|
|
238
|
+
function injectDefaultSlotHtml(node, html) {
|
|
239
|
+
if (!node || typeof node !== 'object')
|
|
240
|
+
return false;
|
|
241
|
+
if (node.__kind === 'el' && node.tag === 'slot' && !(node.attrs && node.attrs.name)) {
|
|
242
|
+
node.__rawHtml = String(html ?? '');
|
|
243
|
+
node.children = [];
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
// Nested Direct component wrapper from serializeApi.component — do not search inside.
|
|
247
|
+
if (node.__kind === 'el' && node.attrs && node.attrs['data-vmz'] != null) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const kids = node.children;
|
|
251
|
+
if (Array.isArray(kids)) {
|
|
252
|
+
for (const c of kids) {
|
|
253
|
+
if (injectDefaultSlotHtml(c, html))
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
|
|
261
|
+
* `[data-vmz]` component (Button/Link labels, etc.).
|
|
262
|
+
* @param {Element | null | undefined} root
|
|
263
|
+
* @returns {Element | null}
|
|
264
|
+
*/
|
|
265
|
+
export function findOwnedDefaultSlot(root) {
|
|
266
|
+
if (!root || root.nodeType !== 1)
|
|
267
|
+
return null;
|
|
268
|
+
const tag = String(root.tagName || '').toLowerCase();
|
|
269
|
+
if (tag === 'slot' && !root.getAttribute('name'))
|
|
270
|
+
return root;
|
|
271
|
+
const kids = root.children;
|
|
272
|
+
if (!kids || !kids.length)
|
|
273
|
+
return null;
|
|
274
|
+
for (let i = 0; i < kids.length; i++) {
|
|
275
|
+
const c = kids[i];
|
|
276
|
+
if (c.nodeType !== 1)
|
|
277
|
+
continue;
|
|
278
|
+
// Nested component host — its slots are not the layout page outlet.
|
|
279
|
+
if (c.hasAttribute('data-vmz'))
|
|
280
|
+
continue;
|
|
281
|
+
const hit = findOwnedDefaultSlot(c);
|
|
282
|
+
if (hit)
|
|
283
|
+
return hit;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
|
|
289
|
+
* Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
|
|
290
|
+
* outlet for the next layout or the page. Retains layout instances on `container`
|
|
291
|
+
* so SPA transitions can dispose only the page host.
|
|
292
|
+
* @param {new (props?: object) => any} Page
|
|
293
|
+
* @param {Element} container
|
|
294
|
+
* @param {object} [props]
|
|
295
|
+
* @param {Array<new (props?: object) => any>} [layoutCtors] outer → inner
|
|
296
|
+
* @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
|
|
297
|
+
*/
|
|
298
|
+
export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
|
|
299
|
+
if (typeof document === 'undefined') {
|
|
300
|
+
throw new Error('vmz:dom hydrateRoute() requires a document (browser)');
|
|
301
|
+
}
|
|
302
|
+
if (container.__vmzInst) {
|
|
303
|
+
destroy(container.__vmzInst);
|
|
304
|
+
container.__vmzInst = null;
|
|
305
|
+
}
|
|
306
|
+
container.__vmzPageHost = null;
|
|
307
|
+
container.__vmzLayoutInsts = null;
|
|
308
|
+
/** @type {object[]} */
|
|
309
|
+
const layoutInsts = [];
|
|
310
|
+
let host = container;
|
|
311
|
+
const ctors = Array.isArray(layoutCtors) ? layoutCtors.filter(Boolean) : [];
|
|
312
|
+
for (const Layout of ctors) {
|
|
313
|
+
const inst = await mount(Layout, host, {});
|
|
314
|
+
layoutInsts.push(inst);
|
|
315
|
+
const slot = findOwnedDefaultSlot(inst.__vmzDomRoot);
|
|
316
|
+
const outlet = document.createElement('div');
|
|
317
|
+
outlet.setAttribute('data-vmz-outlet', '');
|
|
318
|
+
if (slot && slot.parentNode)
|
|
319
|
+
slot.replaceWith(outlet);
|
|
320
|
+
else if (inst.__vmzDomRoot && typeof inst.__vmzDomRoot.appendChild === 'function') {
|
|
321
|
+
inst.__vmzDomRoot.appendChild(outlet);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
host.appendChild(outlet);
|
|
325
|
+
}
|
|
326
|
+
host = outlet;
|
|
327
|
+
}
|
|
328
|
+
const pageInst = await hydrate(Page, host, props, opts);
|
|
329
|
+
container.__vmzPageHost = host;
|
|
330
|
+
container.__vmzLayoutInsts = layoutInsts;
|
|
331
|
+
// Outer layout (or page if no layouts) owns the #app instance for destroy().
|
|
332
|
+
container.__vmzInst = layoutInsts[0] || pageInst;
|
|
333
|
+
return pageInst;
|
|
334
|
+
}
|
|
223
335
|
/**
|
|
224
336
|
* Mount once; later updates are dep patches only (never re-run structure).
|
|
225
|
-
* Requires compiler `__vmzCreate` (
|
|
337
|
+
* Requires compiler `__vmzCreate` (production Direct emit — no blueprint fallback).
|
|
226
338
|
* @param {new (props?: object) => any} Component
|
|
227
339
|
* @param {Element} container
|
|
228
340
|
* @param {object} [props]
|
|
@@ -250,7 +362,7 @@ export async function mount(Component, container, props = {}) {
|
|
|
250
362
|
return inst;
|
|
251
363
|
}
|
|
252
364
|
/**
|
|
253
|
-
* Nested Direct `component
|
|
365
|
+
* Nested Direct `component` schedules child onMount asynchronously; drain before return
|
|
254
366
|
* so SSR/hydrate callers see post-mount DOM (e.g. UserCard Ada, not Loading…).
|
|
255
367
|
* @param {object} inst
|
|
256
368
|
*/
|
|
@@ -275,7 +387,7 @@ async function settlePendingChildMounts(inst) {
|
|
|
275
387
|
}
|
|
276
388
|
}
|
|
277
389
|
/**
|
|
278
|
-
* Direct create only (
|
|
390
|
+
* Direct create only (production Direct emit).
|
|
279
391
|
* @param {new (props?: object) => any} Component
|
|
280
392
|
* @param {object} inst
|
|
281
393
|
*/
|
|
@@ -283,23 +395,36 @@ async function createFromComponent(Component, inst) {
|
|
|
283
395
|
if (Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
|
|
284
396
|
return runDirectCreate(Component, inst);
|
|
285
397
|
}
|
|
286
|
-
throw new Error(`vmz:dom mount requires __vmzCreate (Direct); blueprint render() removed (
|
|
398
|
+
throw new Error(`vmz:dom mount requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
287
399
|
}
|
|
288
400
|
/**
|
|
289
401
|
* @param {new (props?: object) => any} Component
|
|
290
402
|
* @param {object} inst
|
|
291
403
|
*/
|
|
292
404
|
function runDirectCreate(Component, inst) {
|
|
405
|
+
// Nested component creates (e.g. Button inside parent ifBlock branch) must not
|
|
406
|
+
// leak bindAttr/bindText into the parent's `_branchBinds` / `_itemPatches` sink —
|
|
407
|
+
// that steals numeric BindingIds (0) and corrupts parent deps (density → type).
|
|
408
|
+
const prevInst = directApi._inst;
|
|
409
|
+
const prevBranch = directApi._branchBinds;
|
|
410
|
+
const prevItems = directApi._itemPatches;
|
|
411
|
+
const prevEach = directApi._eachCtx;
|
|
293
412
|
directApi._inst = inst;
|
|
413
|
+
directApi._branchBinds = null;
|
|
414
|
+
directApi._itemPatches = null;
|
|
415
|
+
directApi._eachCtx = null;
|
|
294
416
|
try {
|
|
295
417
|
return Component.__vmzCreate.call(inst, directApi);
|
|
296
418
|
}
|
|
297
419
|
finally {
|
|
298
|
-
directApi._inst =
|
|
420
|
+
directApi._inst = prevInst;
|
|
421
|
+
directApi._branchBinds = prevBranch;
|
|
422
|
+
directApi._itemPatches = prevItems;
|
|
423
|
+
directApi._eachCtx = prevEach;
|
|
299
424
|
}
|
|
300
425
|
}
|
|
301
426
|
/**
|
|
302
|
-
*
|
|
427
|
+
* SSR: run the same __vmzCreate schedule against a serialize host (no render).
|
|
303
428
|
* @param {new (props?: object) => any} Component
|
|
304
429
|
* @param {object} inst
|
|
305
430
|
*/
|
|
@@ -371,6 +496,8 @@ function flattenSerializeNode(node) {
|
|
|
371
496
|
if (node.__kind === 'el') {
|
|
372
497
|
const tag = node.tag || 'div';
|
|
373
498
|
if (tag === 'slot') {
|
|
499
|
+
if (node.__rawHtml != null)
|
|
500
|
+
return String(node.__rawHtml);
|
|
374
501
|
return (node.children || []).map(flattenSerializeNode).join('');
|
|
375
502
|
}
|
|
376
503
|
const { open } = serializeOpenTag(node);
|
|
@@ -406,6 +533,10 @@ function* streamSerializeChunks(node) {
|
|
|
406
533
|
if (node.__kind === 'el') {
|
|
407
534
|
const tag = node.tag || 'div';
|
|
408
535
|
if (tag === 'slot') {
|
|
536
|
+
if (node.__rawHtml != null) {
|
|
537
|
+
yield String(node.__rawHtml);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
409
540
|
for (const c of node.children || [])
|
|
410
541
|
yield* streamSerializeChunks(c);
|
|
411
542
|
return;
|
|
@@ -478,10 +609,7 @@ const serializeApi = {
|
|
|
478
609
|
attr(el, name, value) {
|
|
479
610
|
if (!el || el.__kind !== 'el')
|
|
480
611
|
return;
|
|
481
|
-
|
|
482
|
-
delete el.attrs[name];
|
|
483
|
-
else
|
|
484
|
-
el.attrs[name] = String(value);
|
|
612
|
+
applySerializeAttr(el, name, value);
|
|
485
613
|
},
|
|
486
614
|
on() {
|
|
487
615
|
/* events are no-ops during SSR */
|
|
@@ -504,11 +632,50 @@ const serializeApi = {
|
|
|
504
632
|
catch {
|
|
505
633
|
raw = null;
|
|
506
634
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
635
|
+
applySerializeAttr(el, name, raw);
|
|
636
|
+
},
|
|
637
|
+
bindComponentProp() {
|
|
638
|
+
/* SSR: props already resolved into the child instance at create */
|
|
639
|
+
},
|
|
640
|
+
projectDefaultSlot(hostEl, node) {
|
|
641
|
+
if (!hostEl || node == null)
|
|
642
|
+
return;
|
|
643
|
+
// serializeApi.component returns a serialize el tree (or island shell).
|
|
644
|
+
const root = hostEl.__kind === 'el' ? hostEl : null;
|
|
645
|
+
const findSlot = (n) => {
|
|
646
|
+
if (!n || n.__kind !== 'el')
|
|
647
|
+
return null;
|
|
648
|
+
if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
|
|
649
|
+
return n;
|
|
650
|
+
for (const c of n.children || []) {
|
|
651
|
+
const hit = findSlot(c);
|
|
652
|
+
if (hit)
|
|
653
|
+
return hit;
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
};
|
|
657
|
+
// Prefer searching the component body (first child of host wrapper).
|
|
658
|
+
let slot = null;
|
|
659
|
+
if (root) {
|
|
660
|
+
for (const c of root.children || []) {
|
|
661
|
+
slot = findSlot(c);
|
|
662
|
+
if (slot)
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
if (!slot)
|
|
666
|
+
slot = findSlot(root);
|
|
667
|
+
}
|
|
668
|
+
if (slot) {
|
|
669
|
+
slot.__rawHtml = null;
|
|
670
|
+
if (!Array.isArray(slot.children))
|
|
671
|
+
slot.children = [];
|
|
672
|
+
// Append — multiple projectDefaultSlot calls must accumulate (SSR).
|
|
673
|
+
// Client path replaces the live <slot> then appends siblings; serialize must push.
|
|
674
|
+
slot.children.push(node);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (root)
|
|
678
|
+
root.appendChild(node);
|
|
512
679
|
},
|
|
513
680
|
setHtml(el, value) {
|
|
514
681
|
if (!el || el.__kind !== 'el')
|
|
@@ -586,7 +753,7 @@ const serializeApi = {
|
|
|
586
753
|
}
|
|
587
754
|
const dom = spec.createItem.call(inst, serializeApi, box);
|
|
588
755
|
if (dom) {
|
|
589
|
-
//
|
|
756
|
+
// SSR only: serialize key into HTML for hydrate/debug. Direct client does not write this attr.
|
|
590
757
|
if (dom.__kind === 'el')
|
|
591
758
|
serializeApi.attr(dom, 'data-vmz-key', String(k));
|
|
592
759
|
frag.appendChild(dom);
|
|
@@ -609,7 +776,7 @@ const serializeApi = {
|
|
|
609
776
|
resolved[k] = v;
|
|
610
777
|
}
|
|
611
778
|
if (client) {
|
|
612
|
-
//
|
|
779
|
+
// resume: Island SSR includes body + ResumeEntry slice (same Direct schedule).
|
|
613
780
|
const child = serializeApi._ssrChildInstance(Ctor, resolved);
|
|
614
781
|
let body = null;
|
|
615
782
|
if (Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function') {
|
|
@@ -683,15 +850,15 @@ const serializeApi = {
|
|
|
683
850
|
const directApi = {
|
|
684
851
|
/** @type {object | null} */
|
|
685
852
|
_inst: null,
|
|
686
|
-
/** @type {Array<{ deps: string[], fn:
|
|
853
|
+
/** @type {Array<{ deps: string[], fn: => any, bindingId?: number|string|null }> | null} */
|
|
687
854
|
_branchBinds: null,
|
|
688
|
-
/** @type {Array<
|
|
855
|
+
/** @type {Array< => void> | null} */
|
|
689
856
|
_itemPatches: null,
|
|
690
857
|
/**
|
|
691
|
-
* Active keyed-each context (
|
|
858
|
+
* Active keyed-each context (/): item binds + event delegation.
|
|
692
859
|
* @type {null | {
|
|
693
|
-
*
|
|
694
|
-
*
|
|
860
|
+
* noteItemBind: (bindingId: number|string|null, deps: string[], fn: => void) => void,
|
|
861
|
+
* needDelegate: (type: string) => void,
|
|
695
862
|
* }}
|
|
696
863
|
*/
|
|
697
864
|
_eachCtx: null,
|
|
@@ -708,10 +875,7 @@ const directApi = {
|
|
|
708
875
|
return document.createDocumentFragment();
|
|
709
876
|
},
|
|
710
877
|
attr(el, name, value) {
|
|
711
|
-
|
|
712
|
-
el.removeAttribute(name);
|
|
713
|
-
else
|
|
714
|
-
el.setAttribute(name, String(value));
|
|
878
|
+
applyDomAttr(el, name, value);
|
|
715
879
|
},
|
|
716
880
|
on(el, type, handler) {
|
|
717
881
|
const inst = directApi._inst;
|
|
@@ -737,7 +901,7 @@ const directApi = {
|
|
|
737
901
|
* @param {string[]} deps
|
|
738
902
|
* @param {() => any} get
|
|
739
903
|
* @param {Text} textNode
|
|
740
|
-
* @param {{ stable: string[], branches: Array<{ cond?:
|
|
904
|
+
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
741
905
|
*/
|
|
742
906
|
bindText(inst, bindingId, deps, get, textNode, cf) {
|
|
743
907
|
wireDirectBind(inst, bindingId, deps, get, (raw) => {
|
|
@@ -751,18 +915,19 @@ const directApi = {
|
|
|
751
915
|
* @param {() => any} get
|
|
752
916
|
* @param {Element} el
|
|
753
917
|
* @param {string} name
|
|
754
|
-
* @param {{ stable: string[], branches: Array<{ cond?:
|
|
918
|
+
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
755
919
|
*/
|
|
756
920
|
bindAttr(inst, bindingId, deps, get, el, name, cf) {
|
|
757
921
|
wireDirectBind(inst, bindingId, deps, get, (raw) => {
|
|
758
922
|
if (name === 'class' || name === 'className') {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
el.
|
|
923
|
+
const s = String(raw ?? '');
|
|
924
|
+
if (s)
|
|
925
|
+
el.setAttribute('class', s);
|
|
926
|
+
else if (el.hasAttribute('class'))
|
|
927
|
+
el.removeAttribute('class');
|
|
763
928
|
}
|
|
764
929
|
else {
|
|
765
|
-
el
|
|
930
|
+
applyDomAttr(el, name, raw);
|
|
766
931
|
}
|
|
767
932
|
}, cf);
|
|
768
933
|
},
|
|
@@ -776,7 +941,7 @@ const directApi = {
|
|
|
776
941
|
* @param {string[]} deps
|
|
777
942
|
* @param {() => any} get
|
|
778
943
|
* @param {Element} el
|
|
779
|
-
* @param {{ stable: string[], branches: Array<{ cond?:
|
|
944
|
+
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
780
945
|
*/
|
|
781
946
|
bindHtml(inst, bindingId, deps, get, el, cf) {
|
|
782
947
|
wireDirectBind(inst, bindingId, deps, get, (raw) => {
|
|
@@ -811,7 +976,7 @@ const directApi = {
|
|
|
811
976
|
if (isEventEntryStrategy(String(client))) {
|
|
812
977
|
host.setAttribute('data-vmz-entry', 'event');
|
|
813
978
|
}
|
|
814
|
-
//
|
|
979
|
+
// resume: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
|
|
815
980
|
scheduleClientOn(host, String(client), async () => {
|
|
816
981
|
const Ctor = await resolveComponent(name);
|
|
817
982
|
if (!Ctor)
|
|
@@ -843,12 +1008,68 @@ const directApi = {
|
|
|
843
1008
|
}
|
|
844
1009
|
return host;
|
|
845
1010
|
},
|
|
1011
|
+
/**
|
|
1012
|
+
* Keep nested Direct child props live with parent field writes.
|
|
1013
|
+
* @param {object} hostInst
|
|
1014
|
+
* @param {HTMLElement} hostEl
|
|
1015
|
+
* @param {string} propName
|
|
1016
|
+
* @param {string[]} deps
|
|
1017
|
+
* @param {() => any} get
|
|
1018
|
+
*/
|
|
1019
|
+
bindComponentProp(hostInst, hostEl, propName, deps, get) {
|
|
1020
|
+
// Use a stable BindingId so flushPending schedules this patch via the IR
|
|
1021
|
+
// path. A null bindingId only lands in `__vmzBinders` and was skipped when
|
|
1022
|
+
// the same parent field also had bindText/bindAttr BindingIds.
|
|
1023
|
+
if (hostEl && hostEl.__vmzPropBindSeq == null) {
|
|
1024
|
+
hostEl.__vmzPropBindSeq = ++directPropBindSeq;
|
|
1025
|
+
}
|
|
1026
|
+
const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
|
|
1027
|
+
const bindingId = `pc:${seq}:${propName}`;
|
|
1028
|
+
wireDirectBind(hostInst, bindingId, deps, get, (raw) => {
|
|
1029
|
+
const child = hostEl && hostEl.__vmzInst;
|
|
1030
|
+
if (!child || child.__vmzDestroyed)
|
|
1031
|
+
return;
|
|
1032
|
+
if (typeof propName !== 'string' || !propName || propName.startsWith('#'))
|
|
1033
|
+
return;
|
|
1034
|
+
child[propName] = raw;
|
|
1035
|
+
scheduleRefresh(child, { type: 'replace', root: propName });
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
/**
|
|
1039
|
+
* Project parent children into nested Direct component default `<slot>`.
|
|
1040
|
+
* @param {HTMLElement} hostEl
|
|
1041
|
+
* @param {Node} node
|
|
1042
|
+
*/
|
|
1043
|
+
projectDefaultSlot(hostEl, node) {
|
|
1044
|
+
if (!hostEl || node == null)
|
|
1045
|
+
return;
|
|
1046
|
+
const child = hostEl.__vmzInst;
|
|
1047
|
+
const root = (child && child.__vmzDomRoot) || hostEl;
|
|
1048
|
+
/** @type {Element | null} */
|
|
1049
|
+
let slot = null;
|
|
1050
|
+
if (root && root.nodeType === 1) {
|
|
1051
|
+
if (String(root.tagName || '').toLowerCase() === 'slot' && !root.getAttribute('name')) {
|
|
1052
|
+
slot = root;
|
|
1053
|
+
}
|
|
1054
|
+
else if (typeof root.querySelector === 'function') {
|
|
1055
|
+
slot = root.querySelector('slot:not([name])');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (slot && slot.parentNode) {
|
|
1059
|
+
slot.replaceWith(node);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (root && typeof root.appendChild === 'function')
|
|
1063
|
+
root.appendChild(node);
|
|
1064
|
+
else
|
|
1065
|
+
hostEl.appendChild(node);
|
|
1066
|
+
},
|
|
846
1067
|
/**
|
|
847
1068
|
* Direct if/else — no blueprint `kind: "if"` dispatch.
|
|
848
1069
|
* @param {object} inst
|
|
849
1070
|
* @param {number|string|null} bindingId
|
|
850
1071
|
* @param {string[]} deps
|
|
851
|
-
* @param {Array<{ cond?:
|
|
1072
|
+
* @param {Array<{ cond?: => any, create: (api: typeof directApi) => Node }>} branches
|
|
852
1073
|
* @param {number|string|null} [regionId]
|
|
853
1074
|
*/
|
|
854
1075
|
ifBlock(inst, bindingId, deps, branches, regionId = null) {
|
|
@@ -859,7 +1080,7 @@ const directApi = {
|
|
|
859
1080
|
host.setAttribute('data-vmz-region', String(regionId));
|
|
860
1081
|
/** @type {Array<Node | null>} */
|
|
861
1082
|
const cached = branches.map(() => null);
|
|
862
|
-
/** @type {Array<Array<{ deps: string[], fn:
|
|
1083
|
+
/** @type {Array<Array<{ deps: string[], fn: => any, bindingId?: number|string|null }>>} */
|
|
863
1084
|
const branchBinds = branches.map(() => []);
|
|
864
1085
|
let active = -1;
|
|
865
1086
|
let gen = 0;
|
|
@@ -945,7 +1166,7 @@ const directApi = {
|
|
|
945
1166
|
registerBind(inst, deps || [], apply, bindingId);
|
|
946
1167
|
if (directApi._itemPatches)
|
|
947
1168
|
directApi._itemPatches.push(apply);
|
|
948
|
-
//
|
|
1169
|
+
// parent destroy disposes all cached branch trees (pause ≠ destroy on switch).
|
|
949
1170
|
host.__vmzDispose = () => {
|
|
950
1171
|
for (let i = 0; i < cached.length; i++) {
|
|
951
1172
|
unwireBranch(i);
|
|
@@ -960,11 +1181,11 @@ const directApi = {
|
|
|
960
1181
|
},
|
|
961
1182
|
/**
|
|
962
1183
|
* Direct keyed each — no blueprint `kind: "each"` dispatch.
|
|
963
|
-
*
|
|
1184
|
+
* /: Set/Map + Fragment batch insert; item-local binds; host field dispatch; event delegate.
|
|
964
1185
|
* @param {object} inst
|
|
965
1186
|
* @param {number|string|null} bindingId
|
|
966
1187
|
* @param {string[]} deps
|
|
967
|
-
* @param {{ as?: string, list:
|
|
1188
|
+
* @param {{ as?: string, list: => any, key?: (box: {item:any,index:number}) => any, createItem: (api: typeof directApi, box: {item:any,index:number}) => Node }} spec
|
|
968
1189
|
* @param {number|string|null} [regionId]
|
|
969
1190
|
*/
|
|
970
1191
|
eachBlock(inst, bindingId, deps, spec, regionId = null) {
|
|
@@ -975,10 +1196,10 @@ const directApi = {
|
|
|
975
1196
|
const frag = document.createDocumentFragment();
|
|
976
1197
|
frag.appendChild(start);
|
|
977
1198
|
frag.appendChild(end);
|
|
978
|
-
/** @type {Map<any, { box: { item: any, index: number }, dom: Node, patches: Array<
|
|
1199
|
+
/** @type {Map<any, { box: { item: any, index: number }, dom: Node, patches: Array< => void> }>} */
|
|
979
1200
|
const keyed = new Map();
|
|
980
1201
|
let gen = 0;
|
|
981
|
-
/** @type {Map<string,
|
|
1202
|
+
/** @type {Map<string, => void>} */
|
|
982
1203
|
const listDispatchers = new Map();
|
|
983
1204
|
/** @type {Set<string>} */
|
|
984
1205
|
const hostDispatchers = new Set();
|
|
@@ -991,6 +1212,9 @@ const directApi = {
|
|
|
991
1212
|
/** @type {Element | null} */
|
|
992
1213
|
let delegateRoot = null;
|
|
993
1214
|
const itemKey = (box) => {
|
|
1215
|
+
if (rowKeyField != null && box && box.item != null) {
|
|
1216
|
+
return box.item[rowKeyField];
|
|
1217
|
+
}
|
|
994
1218
|
if (typeof spec.key === 'function') {
|
|
995
1219
|
try {
|
|
996
1220
|
return spec.key.call(inst, box);
|
|
@@ -1001,6 +1225,13 @@ const directApi = {
|
|
|
1001
1225
|
}
|
|
1002
1226
|
return box.index;
|
|
1003
1227
|
};
|
|
1228
|
+
/** Reused for keyed lookups — avoid per-row `{item,index}` alloc on create/update. */
|
|
1229
|
+
const keyScratch = { item: null, index: 0 };
|
|
1230
|
+
const keyOf = (item, index) => {
|
|
1231
|
+
keyScratch.item = item;
|
|
1232
|
+
keyScratch.index = index;
|
|
1233
|
+
return itemKey(keyScratch);
|
|
1234
|
+
};
|
|
1004
1235
|
const readList = () => {
|
|
1005
1236
|
let list = [];
|
|
1006
1237
|
try {
|
|
@@ -1014,11 +1245,31 @@ const directApi = {
|
|
|
1014
1245
|
return list;
|
|
1015
1246
|
};
|
|
1016
1247
|
const runEntryPatches = (entry, depKey, onlyBindingId) => {
|
|
1017
|
-
if (!entry
|
|
1248
|
+
if (!entry)
|
|
1249
|
+
return;
|
|
1250
|
+
if (entryIsBp(entry) && applyBp) {
|
|
1251
|
+
if (onlyBindingId != null && blueprintBindIds && !blueprintBindIds.has(String(onlyBindingId))) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
applyBp(entry);
|
|
1256
|
+
}
|
|
1257
|
+
catch (err) {
|
|
1258
|
+
console.error('vmz:dom each item', err);
|
|
1259
|
+
}
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!entry.patches)
|
|
1018
1263
|
return;
|
|
1019
1264
|
for (const p of entry.patches) {
|
|
1020
|
-
if (onlyBindingId != null
|
|
1021
|
-
|
|
1265
|
+
if (onlyBindingId != null) {
|
|
1266
|
+
if (p.__vmzBindingIds) {
|
|
1267
|
+
if (!p.__vmzBindingIds.has(String(onlyBindingId)))
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
else if (p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1022
1273
|
}
|
|
1023
1274
|
try {
|
|
1024
1275
|
runPatch(p, depKey, onlyBindingId);
|
|
@@ -1034,14 +1285,21 @@ const directApi = {
|
|
|
1034
1285
|
const runAt = (i) => {
|
|
1035
1286
|
if (i < 0 || i >= list.length)
|
|
1036
1287
|
return;
|
|
1037
|
-
const
|
|
1038
|
-
const k =
|
|
1288
|
+
const item = list[i];
|
|
1289
|
+
const k = rowKeyOf(item, i);
|
|
1039
1290
|
const entry = keyed.get(k);
|
|
1040
1291
|
if (!entry)
|
|
1041
1292
|
return;
|
|
1042
|
-
entry.
|
|
1043
|
-
|
|
1044
|
-
|
|
1293
|
+
if (entry.nodeType === 1)
|
|
1294
|
+
entry.__vmzBox = item;
|
|
1295
|
+
else {
|
|
1296
|
+
entry.item = item;
|
|
1297
|
+
entry.index = i;
|
|
1298
|
+
if (entry.dom && entry.bp)
|
|
1299
|
+
entry.dom.__vmzBox = item;
|
|
1300
|
+
}
|
|
1301
|
+
if (entry.patches)
|
|
1302
|
+
tagItemPatches(entry.patches, i);
|
|
1045
1303
|
runEntryPatches(entry, (leafDeps && leafDeps[0]) || null, onlyBindingId);
|
|
1046
1304
|
};
|
|
1047
1305
|
if (allowIdx) {
|
|
@@ -1081,11 +1339,16 @@ const directApi = {
|
|
|
1081
1339
|
return;
|
|
1082
1340
|
const trie = inst.__vmzFlushTrie;
|
|
1083
1341
|
const hostFields = [];
|
|
1342
|
+
let listReplaced = false;
|
|
1084
1343
|
for (const d of leafDeps || []) {
|
|
1085
1344
|
if (!d)
|
|
1086
1345
|
continue;
|
|
1087
|
-
if (d.includes('.*') || (d.includes('[') && d.includes(']')))
|
|
1346
|
+
if (d.includes('.*') || (d.includes('[') && d.includes(']'))) {
|
|
1347
|
+
const root = depRootField(d);
|
|
1348
|
+
if (trie && root && trie[root] && trie[root].replace)
|
|
1349
|
+
listReplaced = true;
|
|
1088
1350
|
continue;
|
|
1351
|
+
}
|
|
1089
1352
|
hostFields.push(depRootField(d) || d);
|
|
1090
1353
|
}
|
|
1091
1354
|
const hostDirty = !!trie &&
|
|
@@ -1093,6 +1356,9 @@ const directApi = {
|
|
|
1093
1356
|
const n = trie[f];
|
|
1094
1357
|
return n && (n.replace || n.dirty);
|
|
1095
1358
|
});
|
|
1359
|
+
// Full list replace is owned by eachBlock apply() — skip leaf re-walk.
|
|
1360
|
+
if (listReplaced && !hostDirty)
|
|
1361
|
+
return;
|
|
1096
1362
|
if (hostDirty && hostFields.length) {
|
|
1097
1363
|
refreshHostKeyed(hostFields, bId);
|
|
1098
1364
|
return;
|
|
@@ -1158,9 +1424,16 @@ const directApi = {
|
|
|
1158
1424
|
let n = /** @type {Node | null} */ (ev.target);
|
|
1159
1425
|
while (n && n !== delegateRoot) {
|
|
1160
1426
|
if (n.nodeType === 1) {
|
|
1161
|
-
const
|
|
1427
|
+
const el = /** @type {Element} */ (n);
|
|
1428
|
+
const act = el.__vmzAct || el.getAttribute('data-vmz-act');
|
|
1429
|
+
if (typeof act === 'string' && act) {
|
|
1430
|
+
actionHandler(act).call(inst, ev, el);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const bag = el.__vmzEvt;
|
|
1162
1434
|
if (bag && typeof bag[type] === 'function') {
|
|
1163
|
-
|
|
1435
|
+
// Pass the element so shared each-item handlers can read __vmzBox.
|
|
1436
|
+
bag[type].call(inst, ev, el);
|
|
1164
1437
|
return;
|
|
1165
1438
|
}
|
|
1166
1439
|
}
|
|
@@ -1185,27 +1458,781 @@ const directApi = {
|
|
|
1185
1458
|
if (node.nodeType === 1) {
|
|
1186
1459
|
if (node.__vmzEvt)
|
|
1187
1460
|
node.__vmzEvt = null;
|
|
1461
|
+
if (node.__vmzBox)
|
|
1462
|
+
node.__vmzBox = null;
|
|
1463
|
+
if (node.__vmzAct)
|
|
1464
|
+
node.__vmzAct = null;
|
|
1465
|
+
if (node.__vmzKey != null)
|
|
1466
|
+
node.__vmzKey = null;
|
|
1188
1467
|
for (let c = node.firstChild; c; c = c.nextSibling)
|
|
1189
1468
|
walk(c);
|
|
1190
1469
|
}
|
|
1191
1470
|
};
|
|
1192
1471
|
walk(root);
|
|
1193
1472
|
};
|
|
1473
|
+
const pathFromRoot = (root, node) => {
|
|
1474
|
+
/** @type {number[]} */
|
|
1475
|
+
const path = [];
|
|
1476
|
+
let n = /** @type {Node | null} */ (node);
|
|
1477
|
+
while (n && n !== root) {
|
|
1478
|
+
const parent = n.parentNode;
|
|
1479
|
+
if (!parent)
|
|
1480
|
+
return null;
|
|
1481
|
+
let i = 0;
|
|
1482
|
+
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
|
1483
|
+
if (c === n)
|
|
1484
|
+
break;
|
|
1485
|
+
i++;
|
|
1486
|
+
}
|
|
1487
|
+
path.push(i);
|
|
1488
|
+
n = parent;
|
|
1489
|
+
}
|
|
1490
|
+
if (n !== root)
|
|
1491
|
+
return null;
|
|
1492
|
+
path.reverse();
|
|
1493
|
+
return path;
|
|
1494
|
+
};
|
|
1495
|
+
const nodeAtPath = (root, path) => {
|
|
1496
|
+
let n = /** @type {Node | null} */ (root);
|
|
1497
|
+
for (let i = 0; i < path.length; i++) {
|
|
1498
|
+
if (!n)
|
|
1499
|
+
return null;
|
|
1500
|
+
n = n.childNodes[path[i]] || null;
|
|
1501
|
+
}
|
|
1502
|
+
return n;
|
|
1503
|
+
};
|
|
1504
|
+
/**
|
|
1505
|
+
* Shared each-item event handlers (one per method name for the whole block).
|
|
1506
|
+
* Element carries `__vmzBox`; delegate passes the element as 2nd arg.
|
|
1507
|
+
* @type {Record<string, (ev: Event, el: Element) => void>}
|
|
1508
|
+
*/
|
|
1509
|
+
const sharedActions = Object.create(null);
|
|
1510
|
+
/** @type {Record<string, string>} method → item field for action arg (fallback blueprint). */
|
|
1511
|
+
const actionArgFields = Object.create(null);
|
|
1512
|
+
const actionHandler = (method) => {
|
|
1513
|
+
if (!sharedActions[method]) {
|
|
1514
|
+
sharedActions[method] = function (ev, el) {
|
|
1515
|
+
let n = /** @type {Node | null} */ (el);
|
|
1516
|
+
while (n && n.nodeType === 1) {
|
|
1517
|
+
const box = /** @type {Element} */ (n).__vmzBox;
|
|
1518
|
+
if (box) {
|
|
1519
|
+
const item = box.item != null ? box.item : box;
|
|
1520
|
+
const argField = rowActArgField != null ? rowActArgField : actionArgFields[method] != null ? actionArgFields[method] : null;
|
|
1521
|
+
if (argField == null || item == null)
|
|
1522
|
+
return;
|
|
1523
|
+
const arg = item[argField];
|
|
1524
|
+
const fn = this[method];
|
|
1525
|
+
if (typeof fn === 'function')
|
|
1526
|
+
fn.call(this, arg);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
n = n.parentNode;
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
return sharedActions[method];
|
|
1534
|
+
};
|
|
1535
|
+
/**
|
|
1536
|
+
* @returns {{ method: string, argField: string } | null}
|
|
1537
|
+
*/
|
|
1538
|
+
const parseActionMethod = (handler) => {
|
|
1539
|
+
if (typeof handler !== 'function')
|
|
1540
|
+
return null;
|
|
1541
|
+
try {
|
|
1542
|
+
const src = Function.prototype.toString.call(handler);
|
|
1543
|
+
// this.m(box.item.<field>) — field from author surface.
|
|
1544
|
+
const m = src.match(/this\.([A-Za-z_$][\w$]*)\s*\(\s*[A-Za-z_$][\w$]*\.item\.([A-Za-z_$][\w$]*)\s*\)/);
|
|
1545
|
+
return m ? { method: m[1], argField: m[2] } : null;
|
|
1546
|
+
}
|
|
1547
|
+
catch {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
/**
|
|
1552
|
+
* Row blueprint: first createItem records dynamic slots; later rows clone + hydrate
|
|
1553
|
+
* without re-running compiled createItem (avoids per-row get/CF/on closures).
|
|
1554
|
+
* @type {null | {
|
|
1555
|
+
* tpl: Element,
|
|
1556
|
+
* texts: Array<{ path: number[], bindingId: any, deps: string[], field: string, get: (root: Element) => Node }>,
|
|
1557
|
+
* attrs: Array<{ path: number[], bindingId: any, deps: string[], name: string, onVal: string, offVal: string, get: (root: Element) => Element }>,
|
|
1558
|
+
* ons: Array<{ path: number[], type: string, method: string, get: (root: Element) => Element }>,
|
|
1559
|
+
* bindIds: Set<string>,
|
|
1560
|
+
* }}
|
|
1561
|
+
*/
|
|
1562
|
+
let blueprint = null;
|
|
1563
|
+
let blueprintOk = true;
|
|
1564
|
+
/** @type {Set<string> | null} */
|
|
1565
|
+
let blueprintBindIds = null;
|
|
1566
|
+
/** @type {null | ((root: Element, entry: any) => void)} */
|
|
1567
|
+
let hydrateBp = null;
|
|
1568
|
+
/** @type {null | ((entry: any) => void)} */
|
|
1569
|
+
let applyBp = null;
|
|
1570
|
+
/**
|
|
1571
|
+
* Compile-time rowKernel installed — static HTML rows, no nested component dispose.
|
|
1572
|
+
* Shape-specific walks live in emitted hydrate/apply (Rust), not here.
|
|
1573
|
+
*/
|
|
1574
|
+
let hasRowKernel = false;
|
|
1575
|
+
/** @type {string | null} item field used as key when rowKernel.keyField is set */
|
|
1576
|
+
let rowKeyField = null;
|
|
1577
|
+
/** @type {string | null} item field passed to delegated actions (from rowKernel.actArgField) */
|
|
1578
|
+
let rowActArgField = null;
|
|
1579
|
+
/** Recycle object bp entries (runtime-recorded blueprint fallback). */
|
|
1580
|
+
/** @type {any[]} */
|
|
1581
|
+
const entryPool = [];
|
|
1582
|
+
const allocBpEntry = () => {
|
|
1583
|
+
const e = entryPool.pop();
|
|
1584
|
+
if (e)
|
|
1585
|
+
return e;
|
|
1586
|
+
return { item: null, dom: null, bp: 1, t0: null, t1: null };
|
|
1587
|
+
};
|
|
1588
|
+
const releaseBpEntry = (entry) => {
|
|
1589
|
+
if (!entry)
|
|
1590
|
+
return;
|
|
1591
|
+
// DOM-as-entry (Element): drop expandos.
|
|
1592
|
+
if (entry.nodeType === 1) {
|
|
1593
|
+
entry.__vmzBox = null;
|
|
1594
|
+
entry.__vmzT0 = null;
|
|
1595
|
+
entry.__vmzT1 = null;
|
|
1596
|
+
entry.__vmzBp = null;
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (!entry.bp || entryPool.length >= 4096)
|
|
1600
|
+
return;
|
|
1601
|
+
entry.item = null;
|
|
1602
|
+
entry.dom = null;
|
|
1603
|
+
entry.t0 = null;
|
|
1604
|
+
entry.t1 = null;
|
|
1605
|
+
entry.a0 = null;
|
|
1606
|
+
entry.patches = null;
|
|
1607
|
+
entryPool.push(entry);
|
|
1608
|
+
};
|
|
1609
|
+
const entryDom = (entry) => (entry && entry.nodeType === 1 ? entry : entry && entry.dom);
|
|
1610
|
+
const entryIsBp = (entry) => !!(entry && (entry.nodeType === 1 || entry.bp || entry.__vmzBp));
|
|
1611
|
+
const entryItem = (entry) => {
|
|
1612
|
+
if (!entry)
|
|
1613
|
+
return null;
|
|
1614
|
+
if (entry.nodeType === 1)
|
|
1615
|
+
return entry.__vmzBox;
|
|
1616
|
+
if (entry.bp)
|
|
1617
|
+
return entry.item;
|
|
1618
|
+
return entry.box && entry.box.item;
|
|
1619
|
+
};
|
|
1620
|
+
const rowKeyOf = (item, index) => {
|
|
1621
|
+
if (rowKeyField != null && item != null)
|
|
1622
|
+
return item[rowKeyField];
|
|
1623
|
+
return keyOf(item, index);
|
|
1624
|
+
};
|
|
1625
|
+
/** Drop all row DOM between markers; rowKernel rows skip per-node dispose walks. */
|
|
1626
|
+
const fastWipeRows = () => {
|
|
1627
|
+
const parent = end.parentNode;
|
|
1628
|
+
if (!parent) {
|
|
1629
|
+
keyed.clear();
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
let node = start.nextSibling;
|
|
1633
|
+
if (node && node !== end) {
|
|
1634
|
+
if (hasRowKernel || (blueprint && blueprintOk)) {
|
|
1635
|
+
const range = document.createRange();
|
|
1636
|
+
range.setStartBefore(node);
|
|
1637
|
+
range.setEndBefore(end);
|
|
1638
|
+
range.deleteContents();
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
while (node && node !== end) {
|
|
1642
|
+
const next = node.nextSibling;
|
|
1643
|
+
noteDomRemove();
|
|
1644
|
+
clearDomEvt(node);
|
|
1645
|
+
disposeDomTree(node);
|
|
1646
|
+
node.remove();
|
|
1647
|
+
node = next;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
for (const [, entry] of keyed)
|
|
1652
|
+
releaseBpEntry(entry);
|
|
1653
|
+
keyed.clear();
|
|
1654
|
+
};
|
|
1655
|
+
const makeChildGetter = (path) => {
|
|
1656
|
+
const len = path.length;
|
|
1657
|
+
if (len === 0)
|
|
1658
|
+
return (root) => root;
|
|
1659
|
+
if (len === 1) {
|
|
1660
|
+
const a = path[0];
|
|
1661
|
+
return (root) => root.childNodes[a];
|
|
1662
|
+
}
|
|
1663
|
+
if (len === 2) {
|
|
1664
|
+
const a = path[0];
|
|
1665
|
+
const b = path[1];
|
|
1666
|
+
return (root) => root.childNodes[a].childNodes[b];
|
|
1667
|
+
}
|
|
1668
|
+
if (len === 3) {
|
|
1669
|
+
const a = path[0];
|
|
1670
|
+
const b = path[1];
|
|
1671
|
+
const c = path[2];
|
|
1672
|
+
return (root) => root.childNodes[a].childNodes[b].childNodes[c];
|
|
1673
|
+
}
|
|
1674
|
+
return (root) => {
|
|
1675
|
+
let n = /** @type {Node} */ (root);
|
|
1676
|
+
for (let i = 0; i < len; i++)
|
|
1677
|
+
n = n.childNodes[path[i]];
|
|
1678
|
+
return n;
|
|
1679
|
+
};
|
|
1680
|
+
};
|
|
1681
|
+
const userCreateItem = spec.createItem;
|
|
1682
|
+
// Compile-time row kernel (Rust Direct emit) — skip runtime blueprint recording.
|
|
1683
|
+
if (spec.rowKernel && typeof spec.rowKernel.html === 'string' && typeof spec.rowKernel.hydrate === 'function') {
|
|
1684
|
+
try {
|
|
1685
|
+
const tplHost = document.createElement('template');
|
|
1686
|
+
tplHost.innerHTML = spec.rowKernel.html;
|
|
1687
|
+
const row = tplHost.content.firstElementChild;
|
|
1688
|
+
if (row && row.nodeType === 1) {
|
|
1689
|
+
blueprint = {
|
|
1690
|
+
tpl: /** @type {Element} */ (row.cloneNode(true)),
|
|
1691
|
+
texts: [],
|
|
1692
|
+
attrs: [],
|
|
1693
|
+
ons: [],
|
|
1694
|
+
bindIds: new Set(),
|
|
1695
|
+
};
|
|
1696
|
+
blueprintOk = true;
|
|
1697
|
+
hasRowKernel = true;
|
|
1698
|
+
rowKeyField = typeof spec.rowKernel.keyField === 'string' && spec.rowKernel.keyField ? spec.rowKernel.keyField : null;
|
|
1699
|
+
rowActArgField =
|
|
1700
|
+
typeof spec.rowKernel.actArgField === 'string' && spec.rowKernel.actArgField ? spec.rowKernel.actArgField : null;
|
|
1701
|
+
blueprintBindIds = new Set(['__vmzRk']);
|
|
1702
|
+
for (const ev of spec.rowKernel.events || [])
|
|
1703
|
+
needDelegate(ev);
|
|
1704
|
+
for (const hf of spec.rowKernel.hostFields || []) {
|
|
1705
|
+
if (typeof hf === 'string' && hf)
|
|
1706
|
+
ensureHostDispatcher(hf);
|
|
1707
|
+
}
|
|
1708
|
+
// Leaf path writes (`rows.0.label`) need `rows.*.label`, not bare `rows.*`.
|
|
1709
|
+
{
|
|
1710
|
+
const listRoot = depRootField((deps && deps[0]) || '') || (deps && deps[0]) || '';
|
|
1711
|
+
if (listRoot) {
|
|
1712
|
+
/** @type {string[]} */
|
|
1713
|
+
const leafDeps = [`${listRoot}.*`];
|
|
1714
|
+
const fields = Array.isArray(spec.rowKernel.itemFields) ? spec.rowKernel.itemFields : [];
|
|
1715
|
+
for (const f of fields) {
|
|
1716
|
+
if (typeof f === 'string' && f)
|
|
1717
|
+
leafDeps.push(`${listRoot}.*.${f}`);
|
|
1718
|
+
}
|
|
1719
|
+
ensureListDispatcher('__vmzRk', leafDeps);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const rkHydrate = spec.rowKernel.hydrate;
|
|
1723
|
+
const rkApply = spec.rowKernel.apply;
|
|
1724
|
+
hydrateBp = (root, entry) => {
|
|
1725
|
+
const item = entry && typeof entry === 'object' && 'item' in entry && entry.item != null
|
|
1726
|
+
? entry.item
|
|
1727
|
+
: entry && entry.__vmzBox != null
|
|
1728
|
+
? entry.__vmzBox
|
|
1729
|
+
: entry;
|
|
1730
|
+
rkHydrate.call(inst, root, item);
|
|
1731
|
+
};
|
|
1732
|
+
applyBp = (entry) => {
|
|
1733
|
+
const root = entry && entry.nodeType === 1 ? entry : entry.dom;
|
|
1734
|
+
const item = entry && entry.nodeType === 1 ? entry.__vmzBox : entry.item;
|
|
1735
|
+
if (typeof rkApply === 'function')
|
|
1736
|
+
rkApply.call(inst, root, item);
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
catch (err) {
|
|
1741
|
+
console.error('vmz:dom rowKernel', err);
|
|
1742
|
+
blueprint = null;
|
|
1743
|
+
blueprintOk = true;
|
|
1744
|
+
hasRowKernel = false;
|
|
1745
|
+
rowKeyField = null;
|
|
1746
|
+
rowActArgField = null;
|
|
1747
|
+
hydrateBp = null;
|
|
1748
|
+
applyBp = null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const probeItemField = (get, box) => {
|
|
1752
|
+
const item = box.item;
|
|
1753
|
+
if (!item || (typeof item !== 'object' && typeof item !== 'function'))
|
|
1754
|
+
return null;
|
|
1755
|
+
let field = null;
|
|
1756
|
+
const proxy = new Proxy(item, {
|
|
1757
|
+
get(t, p, r) {
|
|
1758
|
+
if (typeof p === 'string' || typeof p === 'symbol')
|
|
1759
|
+
field = String(p);
|
|
1760
|
+
return Reflect.get(t, p, r);
|
|
1761
|
+
},
|
|
1762
|
+
});
|
|
1763
|
+
const prev = box.item;
|
|
1764
|
+
box.item = proxy;
|
|
1765
|
+
try {
|
|
1766
|
+
get.call(inst);
|
|
1767
|
+
}
|
|
1768
|
+
catch {
|
|
1769
|
+
/* ignore */
|
|
1770
|
+
}
|
|
1771
|
+
box.item = prev;
|
|
1772
|
+
return field;
|
|
1773
|
+
};
|
|
1774
|
+
/**
|
|
1775
|
+
* Probe on/off class strings for `this.<host> === item.<itemField> ? … : …`.
|
|
1776
|
+
* Host/item field names come from binding deps — not hardcoded.
|
|
1777
|
+
*/
|
|
1778
|
+
const probeHostItemClass = (get, box, hostField, itemField) => {
|
|
1779
|
+
if (!hostField || !itemField)
|
|
1780
|
+
return { onVal: '', offVal: '' };
|
|
1781
|
+
const prev = inst[hostField];
|
|
1782
|
+
const matchVal = box.item != null ? box.item[itemField] : undefined;
|
|
1783
|
+
let onVal = '';
|
|
1784
|
+
let offVal = '';
|
|
1785
|
+
const quiet = !!inst.__vmzQuiet;
|
|
1786
|
+
inst.__vmzQuiet = true;
|
|
1787
|
+
try {
|
|
1788
|
+
inst[hostField] = matchVal;
|
|
1789
|
+
onVal = String(get.call(inst) ?? '');
|
|
1790
|
+
// Distinct off value for number / other keys.
|
|
1791
|
+
if (typeof matchVal === 'number') {
|
|
1792
|
+
inst[hostField] = matchVal === 0 ? -1 : 0;
|
|
1793
|
+
if (inst[hostField] === matchVal)
|
|
1794
|
+
inst[hostField] = undefined;
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
inst[hostField] = matchVal === '' ? '__vmz_off__' : '';
|
|
1798
|
+
if (inst[hostField] === matchVal)
|
|
1799
|
+
inst[hostField] = undefined;
|
|
1800
|
+
}
|
|
1801
|
+
offVal = String(get.call(inst) ?? '');
|
|
1802
|
+
}
|
|
1803
|
+
catch {
|
|
1804
|
+
onVal = '';
|
|
1805
|
+
offVal = '';
|
|
1806
|
+
}
|
|
1807
|
+
finally {
|
|
1808
|
+
inst[hostField] = prev;
|
|
1809
|
+
inst.__vmzQuiet = quiet;
|
|
1810
|
+
}
|
|
1811
|
+
return { onVal, offVal };
|
|
1812
|
+
};
|
|
1813
|
+
const sealBlueprintDispatchers = () => {
|
|
1814
|
+
if (!blueprint || blueprintBindIds)
|
|
1815
|
+
return;
|
|
1816
|
+
/** @type {Set<string>} */
|
|
1817
|
+
const ids = new Set();
|
|
1818
|
+
for (const s of blueprint.texts) {
|
|
1819
|
+
if (s.bindingId != null) {
|
|
1820
|
+
ids.add(String(s.bindingId));
|
|
1821
|
+
ensureListDispatcher(s.bindingId, s.deps);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
for (const s of blueprint.attrs) {
|
|
1825
|
+
if (s.bindingId != null) {
|
|
1826
|
+
ids.add(String(s.bindingId));
|
|
1827
|
+
ensureListDispatcher(s.bindingId, s.deps);
|
|
1828
|
+
}
|
|
1829
|
+
for (const d of s.deps || []) {
|
|
1830
|
+
if (!d || d.includes('.*') || (d.includes('[') && d.includes(']')))
|
|
1831
|
+
continue;
|
|
1832
|
+
const rootField = depRootField(d) || d;
|
|
1833
|
+
if (rootField && rootField.indexOf('.') < 0)
|
|
1834
|
+
ensureHostDispatcher(rootField);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
for (const s of blueprint.ons) {
|
|
1838
|
+
needDelegate(s.type);
|
|
1839
|
+
}
|
|
1840
|
+
blueprintBindIds = ids;
|
|
1841
|
+
blueprint.bindIds = ids;
|
|
1842
|
+
compileBlueprintKernels();
|
|
1843
|
+
};
|
|
1844
|
+
const compileBlueprintKernels = () => {
|
|
1845
|
+
if (!blueprint || hydrateBp)
|
|
1846
|
+
return;
|
|
1847
|
+
const textSlots = blueprint.texts;
|
|
1848
|
+
const attrSlots = blueprint.attrs;
|
|
1849
|
+
const onSlots = blueprint.ons;
|
|
1850
|
+
const nText = textSlots.length;
|
|
1851
|
+
const nAttr = attrSlots.length;
|
|
1852
|
+
const nOn = onSlots.length;
|
|
1853
|
+
// Fallback only (no compile-time rowKernel). Field/path walks come from
|
|
1854
|
+
// recorded slots — shape-specific kernels belong in row_kernel.rs.
|
|
1855
|
+
hydrateBp = (root, entry) => {
|
|
1856
|
+
const item = entry && entry.item != null ? entry.item : entry;
|
|
1857
|
+
root.__vmzBox = item;
|
|
1858
|
+
/** @type {Array<Text>} */
|
|
1859
|
+
const textNodes = new Array(nText);
|
|
1860
|
+
/** @type {Array<Element>} */
|
|
1861
|
+
const attrEls = new Array(nAttr);
|
|
1862
|
+
for (let i = 0; i < nText; i++)
|
|
1863
|
+
textNodes[i] = /** @type {Text} */ (textSlots[i].get(root));
|
|
1864
|
+
for (let i = 0; i < nAttr; i++)
|
|
1865
|
+
attrEls[i] = attrSlots[i].get(root);
|
|
1866
|
+
for (let i = 0; i < nOn; i++) {
|
|
1867
|
+
const el = onSlots[i].get(root);
|
|
1868
|
+
if (!el.getAttribute('data-vmz-act')) {
|
|
1869
|
+
el.setAttribute('data-vmz-act', onSlots[i].method);
|
|
1870
|
+
el.__vmzAct = onSlots[i].method;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
for (let i = 0; i < nText; i++) {
|
|
1874
|
+
const v = item == null ? '' : item[textSlots[i].field];
|
|
1875
|
+
textNodes[i].nodeValue = v == null ? '' : v + '';
|
|
1876
|
+
}
|
|
1877
|
+
for (let i = 0; i < nAttr; i++) {
|
|
1878
|
+
const s = attrSlots[i];
|
|
1879
|
+
const el = attrEls[i];
|
|
1880
|
+
const host = s.hostField;
|
|
1881
|
+
const itemKey = s.itemField;
|
|
1882
|
+
if (!host || !itemKey)
|
|
1883
|
+
continue;
|
|
1884
|
+
const hv = inst[host];
|
|
1885
|
+
if (hv != null && item && hv === item[itemKey]) {
|
|
1886
|
+
if (s.name === 'class' || s.name === 'className')
|
|
1887
|
+
el.className = s.onVal;
|
|
1888
|
+
else
|
|
1889
|
+
applyDomAttr(el, s.name, s.onVal);
|
|
1890
|
+
}
|
|
1891
|
+
else if (s.offVal) {
|
|
1892
|
+
if (s.name === 'class' || s.name === 'className')
|
|
1893
|
+
el.className = s.offVal;
|
|
1894
|
+
else
|
|
1895
|
+
applyDomAttr(el, s.name, s.offVal);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
entry.tn = textNodes;
|
|
1899
|
+
entry.ae = attrEls;
|
|
1900
|
+
entry.dom = root;
|
|
1901
|
+
entry.bp = true;
|
|
1902
|
+
};
|
|
1903
|
+
applyBp = (entry) => {
|
|
1904
|
+
const item = entry.item != null ? entry.item : entry.__vmzBox;
|
|
1905
|
+
const textNodes = entry.tn;
|
|
1906
|
+
const attrEls = entry.ae;
|
|
1907
|
+
for (let i = 0; i < nText; i++) {
|
|
1908
|
+
const v = item == null ? '' : item[textSlots[i].field];
|
|
1909
|
+
textNodes[i].nodeValue = v == null ? '' : v + '';
|
|
1910
|
+
}
|
|
1911
|
+
for (let i = 0; i < nAttr; i++) {
|
|
1912
|
+
const s = attrSlots[i];
|
|
1913
|
+
const el = attrEls[i];
|
|
1914
|
+
if (!s.hostField || !s.itemField)
|
|
1915
|
+
continue;
|
|
1916
|
+
const raw = item && inst[s.hostField] === item[s.itemField] ? s.onVal : s.offVal;
|
|
1917
|
+
if (s.name === 'class' || s.name === 'className')
|
|
1918
|
+
el.className = raw;
|
|
1919
|
+
else
|
|
1920
|
+
applyDomAttr(el, s.name, raw);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
};
|
|
1924
|
+
const wireBlueprintItem = (root, box, patches) => {
|
|
1925
|
+
if (!blueprint)
|
|
1926
|
+
return null;
|
|
1927
|
+
sealBlueprintDispatchers();
|
|
1928
|
+
const entry = {
|
|
1929
|
+
item: box.item,
|
|
1930
|
+
index: box.index,
|
|
1931
|
+
dom: root,
|
|
1932
|
+
bp: true,
|
|
1933
|
+
t0: null,
|
|
1934
|
+
t1: null,
|
|
1935
|
+
a0: null,
|
|
1936
|
+
tn: null,
|
|
1937
|
+
ae: null,
|
|
1938
|
+
patches: patches || null,
|
|
1939
|
+
};
|
|
1940
|
+
hydrateBp(root, entry);
|
|
1941
|
+
if (patches) {
|
|
1942
|
+
const applyAll = () => applyBp(entry);
|
|
1943
|
+
applyAll.__vmzBindingIds = blueprintBindIds;
|
|
1944
|
+
applyAll.__vmzBindingId = null;
|
|
1945
|
+
applyAll.__vmzItemLocal = true;
|
|
1946
|
+
applyAll.__vmzBpEntry = entry;
|
|
1947
|
+
patches.push(applyAll);
|
|
1948
|
+
}
|
|
1949
|
+
return entry;
|
|
1950
|
+
};
|
|
1951
|
+
const recordFirstItem = (api, box, patches) => {
|
|
1952
|
+
/** @type {Element | null} */
|
|
1953
|
+
let root = null;
|
|
1954
|
+
/** Pending slots keep live node refs — Direct emit binds before appendChild. */
|
|
1955
|
+
/** @type {{ texts: any[], attrs: any[], ons: any[] }} */
|
|
1956
|
+
const pending = { texts: [], attrs: [], ons: [] };
|
|
1957
|
+
let recordFailed = false;
|
|
1958
|
+
const recordingApi = Object.assign({}, api, {
|
|
1959
|
+
el(tag) {
|
|
1960
|
+
const el = api.el(tag);
|
|
1961
|
+
if (!root)
|
|
1962
|
+
root = el;
|
|
1963
|
+
return el;
|
|
1964
|
+
},
|
|
1965
|
+
// Capture only — do not wireDirectBind (would orphan first-row binders).
|
|
1966
|
+
bindText(i, bindingId, deps, get, textNode, cf) {
|
|
1967
|
+
if (!root)
|
|
1968
|
+
return;
|
|
1969
|
+
// Blueprint recording aborted: fall back to normal wiring for remaining binds.
|
|
1970
|
+
if (recordFailed) {
|
|
1971
|
+
api.bindText(i, bindingId, deps, get, textNode, cf);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
const raw = get.call(inst);
|
|
1976
|
+
if (textNode.nodeType === 3) /** @type {Text} */
|
|
1977
|
+
(textNode).nodeValue = String(raw ?? '');
|
|
1978
|
+
else
|
|
1979
|
+
textNode.textContent = String(raw ?? '');
|
|
1980
|
+
}
|
|
1981
|
+
catch {
|
|
1982
|
+
/* ignore */
|
|
1983
|
+
}
|
|
1984
|
+
pending.texts.push({
|
|
1985
|
+
node: textNode,
|
|
1986
|
+
bindingId,
|
|
1987
|
+
deps: Array.isArray(deps) ? deps.slice() : [],
|
|
1988
|
+
getFn: get,
|
|
1989
|
+
});
|
|
1990
|
+
},
|
|
1991
|
+
bindAttr(i, bindingId, deps, get, el, name, cf) {
|
|
1992
|
+
if (!root)
|
|
1993
|
+
return;
|
|
1994
|
+
if (recordFailed) {
|
|
1995
|
+
api.bindAttr(i, bindingId, deps, get, el, name, cf);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
// Class bind eligible when deps include a bare host field (any name).
|
|
1999
|
+
const hasHostDep = (deps || []).some((d) => d && !d.includes('.*') && !d.includes('[') && String(d).indexOf('.') < 0);
|
|
2000
|
+
if ((name === 'class' || name === 'className') && hasHostDep) {
|
|
2001
|
+
try {
|
|
2002
|
+
const raw = get.call(inst);
|
|
2003
|
+
el.className = raw == null ? '' : String(raw);
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
/* ignore */
|
|
2007
|
+
}
|
|
2008
|
+
pending.attrs.push({
|
|
2009
|
+
node: el,
|
|
2010
|
+
bindingId,
|
|
2011
|
+
deps: Array.isArray(deps) ? deps.slice() : [],
|
|
2012
|
+
name,
|
|
2013
|
+
getFn: get,
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
else {
|
|
2017
|
+
recordFailed = true;
|
|
2018
|
+
api.bindAttr(i, bindingId, deps, get, el, name, cf);
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
on(el, type, handler) {
|
|
2022
|
+
if (recordFailed) {
|
|
2023
|
+
api.on(el, type, handler);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const parsed = parseActionMethod(handler);
|
|
2027
|
+
if (parsed && root) {
|
|
2028
|
+
pending.ons.push({
|
|
2029
|
+
node: el,
|
|
2030
|
+
type,
|
|
2031
|
+
method: parsed.method,
|
|
2032
|
+
argField: parsed.argField,
|
|
2033
|
+
});
|
|
2034
|
+
actionArgFields[parsed.method] = parsed.argField;
|
|
2035
|
+
if (rowActArgField == null)
|
|
2036
|
+
rowActArgField = parsed.argField;
|
|
2037
|
+
root.__vmzBox = box;
|
|
2038
|
+
el.__vmzAct = parsed.method;
|
|
2039
|
+
// Attribute survives cloneNode — hydrate skips per-row __vmzAct writes.
|
|
2040
|
+
el.setAttribute('data-vmz-act', parsed.method);
|
|
2041
|
+
needDelegate(type);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
api.on(el, type, handler);
|
|
2045
|
+
recordFailed = true;
|
|
2046
|
+
},
|
|
2047
|
+
});
|
|
2048
|
+
const dom = userCreateItem.call(inst, recordingApi, box);
|
|
2049
|
+
if (!recordFailed && dom && dom.nodeType === 1 && root === dom && (pending.texts.length > 0 || pending.attrs.length > 0)) {
|
|
2050
|
+
/** @type {any[]} */
|
|
2051
|
+
const texts = [];
|
|
2052
|
+
/** @type {any[]} */
|
|
2053
|
+
const attrs = [];
|
|
2054
|
+
/** @type {any[]} */
|
|
2055
|
+
const ons = [];
|
|
2056
|
+
for (const p of pending.texts) {
|
|
2057
|
+
const path = pathFromRoot(root, p.node);
|
|
2058
|
+
const field = probeItemField(p.getFn, box);
|
|
2059
|
+
if (!path || !field) {
|
|
2060
|
+
recordFailed = true;
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
texts.push({
|
|
2064
|
+
path,
|
|
2065
|
+
bindingId: p.bindingId,
|
|
2066
|
+
deps: p.deps,
|
|
2067
|
+
field,
|
|
2068
|
+
get: makeChildGetter(path),
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
if (!recordFailed) {
|
|
2072
|
+
for (const p of pending.attrs) {
|
|
2073
|
+
const path = pathFromRoot(root, p.node);
|
|
2074
|
+
if (!path) {
|
|
2075
|
+
recordFailed = true;
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
const hostField = (() => {
|
|
2079
|
+
for (const d of p.deps || []) {
|
|
2080
|
+
if (!d)
|
|
2081
|
+
continue;
|
|
2082
|
+
if (d.includes('.*') || d.includes('['))
|
|
2083
|
+
continue;
|
|
2084
|
+
if (String(d).indexOf('.') < 0)
|
|
2085
|
+
return String(d);
|
|
2086
|
+
}
|
|
2087
|
+
return null;
|
|
2088
|
+
})();
|
|
2089
|
+
let itemField = null;
|
|
2090
|
+
for (const d of p.deps || []) {
|
|
2091
|
+
if (!d)
|
|
2092
|
+
continue;
|
|
2093
|
+
if (d.includes('.*')) {
|
|
2094
|
+
const m = String(d).match(/\*\.([A-Za-z_$][\w$]*)$/);
|
|
2095
|
+
if (m)
|
|
2096
|
+
itemField = m[1];
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (!hostField || !itemField) {
|
|
2100
|
+
recordFailed = true;
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
const { onVal, offVal } = probeHostItemClass(p.getFn, box, hostField, itemField);
|
|
2104
|
+
attrs.push({
|
|
2105
|
+
path,
|
|
2106
|
+
bindingId: p.bindingId,
|
|
2107
|
+
deps: p.deps,
|
|
2108
|
+
name: p.name,
|
|
2109
|
+
onVal,
|
|
2110
|
+
offVal,
|
|
2111
|
+
hostField,
|
|
2112
|
+
itemField,
|
|
2113
|
+
get: makeChildGetter(path),
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
if (!recordFailed) {
|
|
2118
|
+
for (const p of pending.ons) {
|
|
2119
|
+
const path = pathFromRoot(root, p.node);
|
|
2120
|
+
if (!path) {
|
|
2121
|
+
recordFailed = true;
|
|
2122
|
+
break;
|
|
2123
|
+
}
|
|
2124
|
+
ons.push({
|
|
2125
|
+
path,
|
|
2126
|
+
type: p.type,
|
|
2127
|
+
method: p.method,
|
|
2128
|
+
get: makeChildGetter(path),
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (!recordFailed) {
|
|
2133
|
+
const tpl = /** @type {Element} */ (dom.cloneNode(true));
|
|
2134
|
+
clearDomEvt(tpl);
|
|
2135
|
+
blueprint = {
|
|
2136
|
+
tpl,
|
|
2137
|
+
texts,
|
|
2138
|
+
attrs,
|
|
2139
|
+
ons,
|
|
2140
|
+
bindIds: new Set(),
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
if (!blueprint)
|
|
2145
|
+
blueprintOk = false;
|
|
2146
|
+
return dom;
|
|
2147
|
+
};
|
|
2148
|
+
const createItem = (api, box, patches) => {
|
|
2149
|
+
if (blueprint && blueprintOk) {
|
|
2150
|
+
const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
|
|
2151
|
+
wireBlueprintItem(root, box, patches);
|
|
2152
|
+
return root;
|
|
2153
|
+
}
|
|
2154
|
+
if (blueprintOk && typeof userCreateItem === 'function') {
|
|
2155
|
+
const dom = recordFirstItem(api, box, patches);
|
|
2156
|
+
if (blueprint && dom && dom.nodeType === 1) {
|
|
2157
|
+
patches.length = 0;
|
|
2158
|
+
wireBlueprintItem(dom, box, patches);
|
|
2159
|
+
}
|
|
2160
|
+
return dom;
|
|
2161
|
+
}
|
|
2162
|
+
return userCreateItem.call(inst, api, box);
|
|
2163
|
+
};
|
|
2164
|
+
/**
|
|
2165
|
+
* Reorder / place item DOM with minimal mutations.
|
|
2166
|
+
* - already-correct → no-op
|
|
2167
|
+
* - pure 2-node swap → 1–2 insertBefore (common keyed list swap)
|
|
2168
|
+
* - append prefix → Fragment insert only new tail
|
|
2169
|
+
* - create / replace / complex → Fragment rebuild before `end`
|
|
2170
|
+
*/
|
|
1194
2171
|
const reconcileDomOrder = (nextNodes) => {
|
|
1195
2172
|
const parent = end.parentNode;
|
|
1196
2173
|
if (!parent)
|
|
1197
2174
|
return;
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
for (let
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
2175
|
+
/** @type {ChildNode[]} */
|
|
2176
|
+
const curr = [];
|
|
2177
|
+
for (let n = start.nextSibling; n && n !== end; n = n.nextSibling) {
|
|
2178
|
+
curr.push(n);
|
|
2179
|
+
}
|
|
2180
|
+
if (curr.length === nextNodes.length) {
|
|
2181
|
+
let same = true;
|
|
2182
|
+
for (let i = 0; i < curr.length; i++) {
|
|
2183
|
+
if (curr[i] !== nextNodes[i]) {
|
|
2184
|
+
same = false;
|
|
2185
|
+
break;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (same)
|
|
2189
|
+
return;
|
|
2190
|
+
// Fast path: exactly two positions swapped (benchmark swaprows).
|
|
2191
|
+
/** @type {number[]} */
|
|
2192
|
+
const diff = [];
|
|
2193
|
+
for (let i = 0; i < curr.length; i++) {
|
|
2194
|
+
if (curr[i] !== nextNodes[i])
|
|
2195
|
+
diff.push(i);
|
|
2196
|
+
}
|
|
2197
|
+
if (diff.length === 2 && curr[diff[0]] === nextNodes[diff[1]] && curr[diff[1]] === nextNodes[diff[0]]) {
|
|
2198
|
+
const a = curr[diff[0]];
|
|
2199
|
+
const b = curr[diff[1]];
|
|
2200
|
+
const aNext = a.nextSibling;
|
|
2201
|
+
const bNext = b.nextSibling;
|
|
2202
|
+
noteDomMove();
|
|
2203
|
+
noteDomMove();
|
|
2204
|
+
if (aNext === b) {
|
|
2205
|
+
parent.insertBefore(b, a);
|
|
2206
|
+
}
|
|
2207
|
+
else if (bNext === a) {
|
|
2208
|
+
parent.insertBefore(a, b);
|
|
2209
|
+
}
|
|
2210
|
+
else {
|
|
2211
|
+
parent.insertBefore(b, aNext);
|
|
2212
|
+
parent.insertBefore(a, bNext);
|
|
2213
|
+
}
|
|
2214
|
+
return;
|
|
1204
2215
|
}
|
|
1205
|
-
n = n.nextSibling;
|
|
1206
2216
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
2217
|
+
// Append-only: existing live prefix unchanged, only new tail detached.
|
|
2218
|
+
if (curr.length < nextNodes.length && curr.length > 0) {
|
|
2219
|
+
let prefix = true;
|
|
2220
|
+
for (let i = 0; i < curr.length; i++) {
|
|
2221
|
+
if (curr[i] !== nextNodes[i]) {
|
|
2222
|
+
prefix = false;
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (prefix) {
|
|
2227
|
+
const batch = document.createDocumentFragment();
|
|
2228
|
+
for (let i = curr.length; i < nextNodes.length; i++) {
|
|
2229
|
+
batch.appendChild(nextNodes[i]);
|
|
2230
|
+
}
|
|
2231
|
+
parent.insertBefore(batch, end);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
// Create / replace / complex reorder: one Fragment write.
|
|
1209
2236
|
const batch = document.createDocumentFragment();
|
|
1210
2237
|
for (const dom of nextNodes) {
|
|
1211
2238
|
if (dom.parentNode)
|
|
@@ -1219,27 +2246,40 @@ const directApi = {
|
|
|
1219
2246
|
return;
|
|
1220
2247
|
const applied = ++gen;
|
|
1221
2248
|
const list = readList();
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
if (
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
2249
|
+
const n = list.length;
|
|
2250
|
+
// Clear all rows.
|
|
2251
|
+
if (n === 0) {
|
|
2252
|
+
if (keyed.size)
|
|
2253
|
+
fastWipeRows();
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
// Full replace (no key reuse): wipe then fall into fresh create.
|
|
2257
|
+
if (keyed.size > 0 && hasRowKernel) {
|
|
2258
|
+
let reuse = false;
|
|
2259
|
+
for (let i = 0; i < n; i++) {
|
|
2260
|
+
if (keyed.has(rowKeyOf(list[i], i))) {
|
|
2261
|
+
reuse = true;
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
1231
2264
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
2265
|
+
if (!reuse)
|
|
2266
|
+
fastWipeRows();
|
|
2267
|
+
}
|
|
2268
|
+
// Fresh create into empty each: record blueprint once, then clone-only.
|
|
2269
|
+
if (keyed.size === 0 && n > 0) {
|
|
2270
|
+
const parent = end.parentNode;
|
|
2271
|
+
const batch = document.createDocumentFragment();
|
|
2272
|
+
let startIdx = 0;
|
|
2273
|
+
if (!blueprint || !blueprintOk) {
|
|
2274
|
+
const box0 = { item: list[0], index: 0 };
|
|
2275
|
+
const patches0 = [];
|
|
1236
2276
|
const prevPatches = directApi._itemPatches;
|
|
1237
2277
|
const prevCtx = directApi._eachCtx;
|
|
1238
|
-
directApi._itemPatches =
|
|
2278
|
+
directApi._itemPatches = patches0;
|
|
1239
2279
|
directApi._eachCtx = eachCtx;
|
|
1240
|
-
let
|
|
2280
|
+
let dom0 = null;
|
|
1241
2281
|
try {
|
|
1242
|
-
|
|
2282
|
+
dom0 = createItem(directApi, box0, patches0);
|
|
1243
2283
|
}
|
|
1244
2284
|
finally {
|
|
1245
2285
|
directApi._itemPatches = prevPatches;
|
|
@@ -1247,24 +2287,235 @@ const directApi = {
|
|
|
1247
2287
|
}
|
|
1248
2288
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
1249
2289
|
return;
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
2290
|
+
if (!dom0)
|
|
2291
|
+
return;
|
|
2292
|
+
const k0 = itemKey(box0);
|
|
2293
|
+
if (dom0.nodeType === 1) /** @type {Element} */
|
|
2294
|
+
(dom0).__vmzKey = k0;
|
|
2295
|
+
// First row may already be blueprint-wired (patches cleared + hydrate).
|
|
2296
|
+
let entry0 = keyed.get(k0);
|
|
2297
|
+
if (!entry0) {
|
|
2298
|
+
if (patches0.length && patches0[0] && patches0[0].__vmzBpEntry) {
|
|
2299
|
+
entry0 = patches0[0].__vmzBpEntry;
|
|
2300
|
+
entry0.patches = patches0;
|
|
2301
|
+
}
|
|
2302
|
+
else if (blueprint && blueprintOk) {
|
|
2303
|
+
entry0 = wireBlueprintItem(/** @type {Element} */ (dom0), box0, patches0);
|
|
2304
|
+
}
|
|
2305
|
+
else {
|
|
2306
|
+
tagItemPatches(patches0, 0);
|
|
2307
|
+
entry0 = { box: box0, dom: dom0, patches: patches0 };
|
|
2308
|
+
}
|
|
2309
|
+
keyed.set(k0, entry0);
|
|
2310
|
+
}
|
|
2311
|
+
batch.appendChild(dom0);
|
|
2312
|
+
startIdx = 1;
|
|
2313
|
+
}
|
|
2314
|
+
if (blueprint && blueprintOk) {
|
|
2315
|
+
sealBlueprintDispatchers();
|
|
2316
|
+
const tpl = blueprint.tpl;
|
|
2317
|
+
if (hasRowKernel && spec.rowKernel && typeof spec.rowKernel.create === 'function') {
|
|
2318
|
+
// Shape-specific create loop is Rust-emitted (rowKernel.create).
|
|
2319
|
+
// Direct parent.insertBefore (no Fragment). When parent has only the
|
|
2320
|
+
// each markers as children, detach parent for the fill then reattach —
|
|
2321
|
+
// same structural trick as hand-tuned keyed apps (not app-specific).
|
|
2322
|
+
if (parent) {
|
|
2323
|
+
let detached = null;
|
|
2324
|
+
let reinsertAt = null;
|
|
2325
|
+
if (parent.nodeType === 1 && parent.parentNode) {
|
|
2326
|
+
let onlyMarkers = true;
|
|
2327
|
+
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
|
2328
|
+
if (c !== start && c !== end) {
|
|
2329
|
+
onlyMarkers = false;
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (onlyMarkers) {
|
|
2334
|
+
detached = parent.parentNode;
|
|
2335
|
+
reinsertAt = parent.nextSibling;
|
|
2336
|
+
detached.removeChild(parent);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
spec.rowKernel.create.call(inst, list, startIdx, tpl, keyed, parent, end, rowKeyOf);
|
|
2340
|
+
if (detached)
|
|
2341
|
+
detached.insertBefore(parent, reinsertAt);
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
const hydrate = spec.rowKernel.hydrate;
|
|
2345
|
+
for (let i = startIdx; i < n; i++) {
|
|
2346
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2347
|
+
return;
|
|
2348
|
+
const item = list[i];
|
|
2349
|
+
const root = /** @type {Element} */ (tpl.cloneNode(true));
|
|
2350
|
+
hydrate.call(inst, root, item);
|
|
2351
|
+
const k = rowKeyOf(item, i);
|
|
2352
|
+
root.__vmzKey = k;
|
|
2353
|
+
keyed.set(k, root);
|
|
2354
|
+
batch.appendChild(root);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
else if (hasRowKernel && hydrateBp) {
|
|
2359
|
+
const hydrate = spec.rowKernel.hydrate;
|
|
2360
|
+
for (let i = startIdx; i < n; i++) {
|
|
2361
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2362
|
+
return;
|
|
2363
|
+
const item = list[i];
|
|
2364
|
+
const root = /** @type {Element} */ (tpl.cloneNode(true));
|
|
2365
|
+
hydrate.call(inst, root, item);
|
|
2366
|
+
const k = rowKeyOf(item, i);
|
|
2367
|
+
root.__vmzKey = k;
|
|
2368
|
+
keyed.set(k, root);
|
|
2369
|
+
batch.appendChild(root);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
else {
|
|
2373
|
+
for (let i = startIdx; i < n; i++) {
|
|
2374
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2375
|
+
return;
|
|
2376
|
+
const item = list[i];
|
|
2377
|
+
const k = keyOf(item, i);
|
|
2378
|
+
const root = /** @type {Element} */ (tpl.cloneNode(true));
|
|
2379
|
+
const entry = {
|
|
2380
|
+
item,
|
|
2381
|
+
index: i,
|
|
2382
|
+
dom: root,
|
|
2383
|
+
bp: true,
|
|
2384
|
+
t0: null,
|
|
2385
|
+
t1: null,
|
|
2386
|
+
a0: null,
|
|
2387
|
+
patches: null,
|
|
2388
|
+
};
|
|
2389
|
+
hydrateBp(root, entry);
|
|
2390
|
+
root.__vmzKey = k;
|
|
2391
|
+
keyed.set(k, entry);
|
|
2392
|
+
batch.appendChild(root);
|
|
1254
2393
|
}
|
|
1255
|
-
entry = { box, dom, patches };
|
|
1256
|
-
keyed.set(k, entry);
|
|
1257
2394
|
}
|
|
1258
2395
|
}
|
|
1259
2396
|
else {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
2397
|
+
for (let i = startIdx; i < n; i++) {
|
|
2398
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2399
|
+
return;
|
|
2400
|
+
const box = { item: list[i], index: i };
|
|
2401
|
+
const k = itemKey(box);
|
|
2402
|
+
const patches = [];
|
|
2403
|
+
const prevPatches = directApi._itemPatches;
|
|
2404
|
+
const prevCtx = directApi._eachCtx;
|
|
2405
|
+
directApi._itemPatches = patches;
|
|
2406
|
+
directApi._eachCtx = eachCtx;
|
|
2407
|
+
let dom = null;
|
|
2408
|
+
try {
|
|
2409
|
+
dom = createItem(directApi, box, patches);
|
|
2410
|
+
}
|
|
2411
|
+
finally {
|
|
2412
|
+
directApi._itemPatches = prevPatches;
|
|
2413
|
+
directApi._eachCtx = prevCtx;
|
|
2414
|
+
}
|
|
2415
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2416
|
+
return;
|
|
2417
|
+
if (!dom)
|
|
2418
|
+
continue;
|
|
2419
|
+
tagItemPatches(patches, i);
|
|
2420
|
+
if (dom.nodeType === 1) /** @type {Element} */
|
|
2421
|
+
(dom).__vmzKey = k;
|
|
2422
|
+
keyed.set(k, { box, dom, patches });
|
|
2423
|
+
batch.appendChild(dom);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2427
|
+
return;
|
|
2428
|
+
if (parent && batch.firstChild)
|
|
2429
|
+
parent.insertBefore(batch, end);
|
|
2430
|
+
if (end.isConnected)
|
|
2431
|
+
ensureDelegateAttached();
|
|
2432
|
+
else
|
|
2433
|
+
queueMicrotask(() => {
|
|
2434
|
+
if (!inst.__vmzDestroyed)
|
|
2435
|
+
ensureDelegateAttached();
|
|
2436
|
+
});
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
const seen = new Set();
|
|
2440
|
+
/** @type {Node[]} */
|
|
2441
|
+
const nextNodes = [];
|
|
2442
|
+
for (let i = 0; i < n; i++) {
|
|
2443
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2444
|
+
return;
|
|
2445
|
+
const item = list[i];
|
|
2446
|
+
const k = rowKeyOf(item, i);
|
|
2447
|
+
seen.add(k);
|
|
2448
|
+
let entry = keyed.get(k);
|
|
2449
|
+
if (!entry) {
|
|
2450
|
+
if (hasRowKernel && blueprint && blueprintOk && hydrateBp) {
|
|
2451
|
+
const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
|
|
2452
|
+
spec.rowKernel.hydrate.call(inst, root, item);
|
|
2453
|
+
root.__vmzKey = k;
|
|
2454
|
+
keyed.set(k, root);
|
|
2455
|
+
entry = root;
|
|
2456
|
+
}
|
|
2457
|
+
else {
|
|
2458
|
+
const box = { item, index: i };
|
|
2459
|
+
const patches = [];
|
|
2460
|
+
const prevPatches = directApi._itemPatches;
|
|
2461
|
+
const prevCtx = directApi._eachCtx;
|
|
2462
|
+
directApi._itemPatches = patches;
|
|
2463
|
+
directApi._eachCtx = eachCtx;
|
|
2464
|
+
let dom = null;
|
|
2465
|
+
try {
|
|
2466
|
+
dom = createItem(directApi, box, patches);
|
|
2467
|
+
}
|
|
2468
|
+
finally {
|
|
2469
|
+
directApi._itemPatches = prevPatches;
|
|
2470
|
+
directApi._eachCtx = prevCtx;
|
|
2471
|
+
}
|
|
2472
|
+
if (applied !== gen || inst.__vmzDestroyed)
|
|
2473
|
+
return;
|
|
2474
|
+
tagItemPatches(patches, i);
|
|
2475
|
+
if (dom) {
|
|
2476
|
+
if (dom.nodeType === 1) {
|
|
2477
|
+
// Client identity: expando only (see 01 each identity). SSR uses data-vmz-key.
|
|
2478
|
+
/** @type {Element} */ (dom).__vmzKey = k;
|
|
2479
|
+
}
|
|
2480
|
+
entry = { box, dom, patches };
|
|
2481
|
+
keyed.set(k, entry);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
else {
|
|
2486
|
+
const sameItem = entryItem(entry) === item;
|
|
2487
|
+
if (entry.nodeType === 1) {
|
|
2488
|
+
entry.__vmzBox = item;
|
|
2489
|
+
}
|
|
2490
|
+
else if (entry.bp) {
|
|
2491
|
+
entry.item = item;
|
|
2492
|
+
entry.index = i;
|
|
2493
|
+
if (entry.dom)
|
|
2494
|
+
entry.dom.__vmzBox = entry.item;
|
|
2495
|
+
}
|
|
2496
|
+
else {
|
|
2497
|
+
entry.box.item = item;
|
|
2498
|
+
entry.box.index = i;
|
|
2499
|
+
tagItemPatches(entry.patches, i);
|
|
2500
|
+
}
|
|
2501
|
+
// Pure reorder (swap / move) keeps object identity — skip leaf patches.
|
|
2502
|
+
if (!sameItem) {
|
|
2503
|
+
if (entryIsBp(entry) && applyBp) {
|
|
2504
|
+
try {
|
|
2505
|
+
applyBp(entry);
|
|
2506
|
+
}
|
|
2507
|
+
catch (err) {
|
|
2508
|
+
console.error('vmz:dom each item', err);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
else if (entry.patches) {
|
|
2512
|
+
for (const p of entry.patches)
|
|
2513
|
+
runPatch(p, null);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
1265
2516
|
}
|
|
1266
2517
|
if (entry)
|
|
1267
|
-
nextNodes.push(entry
|
|
2518
|
+
nextNodes.push(entryDom(entry));
|
|
1268
2519
|
}
|
|
1269
2520
|
if (applied !== gen || inst.__vmzDestroyed)
|
|
1270
2521
|
return;
|
|
@@ -1272,11 +2523,19 @@ const directApi = {
|
|
|
1272
2523
|
if (seen.has(k))
|
|
1273
2524
|
continue;
|
|
1274
2525
|
noteDomRemove();
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
2526
|
+
const dom = entryDom(entry);
|
|
2527
|
+
if (hasRowKernel) {
|
|
2528
|
+
if (dom && dom.parentNode)
|
|
2529
|
+
dom.remove();
|
|
2530
|
+
}
|
|
2531
|
+
else {
|
|
2532
|
+
clearDomEvt(dom);
|
|
2533
|
+
disposeDomTree(dom);
|
|
2534
|
+
if (dom && dom.parentNode)
|
|
2535
|
+
dom.remove();
|
|
2536
|
+
}
|
|
1279
2537
|
keyed.delete(k);
|
|
2538
|
+
releaseBpEntry(entry);
|
|
1280
2539
|
}
|
|
1281
2540
|
reconcileDomOrder(nextNodes);
|
|
1282
2541
|
// First apply may run while start/end still sit in a DocumentFragment
|
|
@@ -1294,31 +2553,45 @@ const directApi = {
|
|
|
1294
2553
|
directApi._itemPatches.push(apply);
|
|
1295
2554
|
start.__vmzDispose = () => {
|
|
1296
2555
|
teardownDelegate();
|
|
1297
|
-
|
|
1298
|
-
clearDomEvt(entry.dom);
|
|
1299
|
-
disposeDomTree(entry.dom);
|
|
1300
|
-
if (entry.dom && entry.dom.parentNode)
|
|
1301
|
-
entry.dom.remove();
|
|
1302
|
-
}
|
|
1303
|
-
keyed.clear();
|
|
2556
|
+
fastWipeRows();
|
|
1304
2557
|
};
|
|
1305
2558
|
const softDeps = [...new Set((deps || []).map((d) => `${depRootField(d)}.*`))];
|
|
1306
2559
|
const softRefresh = () => {
|
|
1307
2560
|
if (inst.__vmzDestroyed)
|
|
1308
2561
|
return;
|
|
2562
|
+
const trie = inst.__vmzFlushTrie;
|
|
2563
|
+
const listRoot = depRootField((deps && deps[0]) || '') || '';
|
|
2564
|
+
// Full list replace is owned by apply(); soft channel is item/structure churn.
|
|
2565
|
+
if (trie && listRoot && trie[listRoot] && trie[listRoot].replace)
|
|
2566
|
+
return;
|
|
1309
2567
|
const list = readList();
|
|
1310
|
-
const softKey = softDeps[0] || `${
|
|
2568
|
+
const softKey = softDeps[0] || `${listRoot}.*`;
|
|
1311
2569
|
for (let i = 0; i < list.length; i++) {
|
|
1312
|
-
const
|
|
1313
|
-
const k =
|
|
2570
|
+
const item = list[i];
|
|
2571
|
+
const k = rowKeyOf(item, i);
|
|
1314
2572
|
const entry = keyed.get(k);
|
|
1315
2573
|
if (!entry)
|
|
1316
2574
|
continue;
|
|
1317
|
-
entry
|
|
2575
|
+
if (entryIsBp(entry)) {
|
|
2576
|
+
if (entry.nodeType === 1)
|
|
2577
|
+
entry.__vmzBox = item;
|
|
2578
|
+
else {
|
|
2579
|
+
entry.item = item;
|
|
2580
|
+
entry.index = i;
|
|
2581
|
+
if (entry.dom)
|
|
2582
|
+
entry.dom.__vmzBox = item;
|
|
2583
|
+
}
|
|
2584
|
+
if (applyBp)
|
|
2585
|
+
applyBp(entry);
|
|
2586
|
+
else if (hydrateBp)
|
|
2587
|
+
hydrateBp(entryDom(entry), entry);
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2590
|
+
entry.box.item = item;
|
|
1318
2591
|
entry.box.index = i;
|
|
1319
2592
|
tagItemPatches(entry.patches, i);
|
|
1320
2593
|
for (const p of entry.patches) {
|
|
1321
|
-
// Leaf BindingId patches are owned by list/host dispatchers
|
|
2594
|
+
// Leaf BindingId patches are owned by list/host dispatchers .
|
|
1322
2595
|
if (p.__vmzBindingId != null)
|
|
1323
2596
|
continue;
|
|
1324
2597
|
if (patchHasBindingId(inst, p))
|
|
@@ -1350,7 +2623,7 @@ function trackDirectBind(inst, deps, fn, bindingId = null) {
|
|
|
1350
2623
|
directApi._itemPatches.push(fn);
|
|
1351
2624
|
return;
|
|
1352
2625
|
}
|
|
1353
|
-
//
|
|
2626
|
+
// item binds stay on entry.patches; eachBlock registers one dispatcher per BindingId.
|
|
1354
2627
|
if (directApi._itemPatches) {
|
|
1355
2628
|
fn.__vmzItemLocal = true;
|
|
1356
2629
|
directApi._itemPatches.push(fn);
|
|
@@ -1367,12 +2640,12 @@ function trackDirectBind(inst, deps, fn, bindingId = null) {
|
|
|
1367
2640
|
* @param {string[]} deps
|
|
1368
2641
|
* @param {() => any} get
|
|
1369
2642
|
* @param {(raw: any) => void} write
|
|
1370
|
-
* @param {{ stable: string[], branches: Array<{ cond?:
|
|
2643
|
+
* @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
|
|
1371
2644
|
*/
|
|
1372
2645
|
function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
1373
2646
|
let activeBranch = -1;
|
|
1374
2647
|
/** @type {string[]} */
|
|
1375
|
-
let liveDeps = Array.isArray(deps) ?
|
|
2648
|
+
let liveDeps = Array.isArray(deps) ? deps.slice() : [];
|
|
1376
2649
|
const pickCf = () => {
|
|
1377
2650
|
if (!cf || !Array.isArray(cf.branches))
|
|
1378
2651
|
return -1;
|
|
@@ -1390,6 +2663,11 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1390
2663
|
}
|
|
1391
2664
|
return cf.branches.length - 1;
|
|
1392
2665
|
};
|
|
2666
|
+
// Item-local CF whose branches only gate the same stable deps: skip branch switching.
|
|
2667
|
+
let simpleCf = false;
|
|
2668
|
+
if (cf && Array.isArray(cf.branches) && directApi._itemPatches) {
|
|
2669
|
+
simpleCf = cf.branches.every((b) => !b.deps || b.deps.length === 0);
|
|
2670
|
+
}
|
|
1393
2671
|
const apply = () => {
|
|
1394
2672
|
if (precision.enabled) {
|
|
1395
2673
|
precision.bindingEvals++;
|
|
@@ -1407,7 +2685,7 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1407
2685
|
raw = null;
|
|
1408
2686
|
}
|
|
1409
2687
|
write(raw);
|
|
1410
|
-
if (!cf || !Array.isArray(cf.branches))
|
|
2688
|
+
if (!cf || !Array.isArray(cf.branches) || simpleCf || apply.__vmzItemLocal)
|
|
1411
2689
|
return;
|
|
1412
2690
|
const next = pickCf();
|
|
1413
2691
|
if (next === activeBranch)
|
|
@@ -1416,21 +2694,19 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1416
2694
|
const branch = cf.branches[next];
|
|
1417
2695
|
const nextDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
|
|
1418
2696
|
const uniq = [...new Set(nextDeps)];
|
|
1419
|
-
// Item-local binds must never enter the global binder table (P1 / jfb select).
|
|
1420
|
-
if (apply.__vmzItemLocal) {
|
|
1421
|
-
liveDeps = uniq;
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
2697
|
unregisterBind(inst, liveDeps, apply, bindingId);
|
|
1425
2698
|
liveDeps = uniq;
|
|
1426
2699
|
registerBind(inst, liveDeps, apply, bindingId);
|
|
1427
2700
|
};
|
|
1428
|
-
if (cf && Array.isArray(cf.branches)) {
|
|
2701
|
+
if (cf && Array.isArray(cf.branches) && !simpleCf) {
|
|
1429
2702
|
activeBranch = pickCf();
|
|
1430
2703
|
const branch = cf.branches[activeBranch];
|
|
1431
2704
|
liveDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
|
|
1432
2705
|
liveDeps = [...new Set(liveDeps)];
|
|
1433
2706
|
}
|
|
2707
|
+
else if (cf && Array.isArray(cf.branches) && simpleCf) {
|
|
2708
|
+
liveDeps = Array.isArray(cf.stable) && cf.stable.length ? cf.stable : liveDeps;
|
|
2709
|
+
}
|
|
1434
2710
|
// Mark before first apply so CF branch switches never hit global registerBind.
|
|
1435
2711
|
if (directApi._itemPatches)
|
|
1436
2712
|
apply.__vmzItemLocal = true;
|
|
@@ -1440,6 +2716,74 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
1440
2716
|
function isEventPropName(name) {
|
|
1441
2717
|
return typeof name === 'string' && /^on[A-Z]/.test(name);
|
|
1442
2718
|
}
|
|
2719
|
+
/** Monotonic id for `bindComponentProp` BindingIds (per process). */
|
|
2720
|
+
let directPropBindSeq = 0;
|
|
2721
|
+
/** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
|
|
2722
|
+
const BOOLEAN_HTML_ATTRS = new Set([
|
|
2723
|
+
'disabled',
|
|
2724
|
+
'checked',
|
|
2725
|
+
'selected',
|
|
2726
|
+
'readonly',
|
|
2727
|
+
'required',
|
|
2728
|
+
'multiple',
|
|
2729
|
+
'hidden',
|
|
2730
|
+
'autofocus',
|
|
2731
|
+
'autoplay',
|
|
2732
|
+
'controls',
|
|
2733
|
+
'loop',
|
|
2734
|
+
'muted',
|
|
2735
|
+
'open',
|
|
2736
|
+
'novalidate',
|
|
2737
|
+
'formnovalidate',
|
|
2738
|
+
'defer',
|
|
2739
|
+
'async',
|
|
2740
|
+
'ismap',
|
|
2741
|
+
'default',
|
|
2742
|
+
'inert',
|
|
2743
|
+
]);
|
|
2744
|
+
/**
|
|
2745
|
+
* @param {Element} el
|
|
2746
|
+
* @param {string} name
|
|
2747
|
+
* @param {any} value
|
|
2748
|
+
*/
|
|
2749
|
+
function applyDomAttr(el, name, value) {
|
|
2750
|
+
const key = name === 'className' ? 'class' : name;
|
|
2751
|
+
if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
|
|
2752
|
+
if (value === false || value == null || value === '') {
|
|
2753
|
+
el.removeAttribute(key);
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
el.setAttribute(key, value === true ? '' : String(value));
|
|
2757
|
+
}
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
if (value == null || value === false)
|
|
2761
|
+
el.removeAttribute(key);
|
|
2762
|
+
else
|
|
2763
|
+
el.setAttribute(key, value === true ? '' : String(value));
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Serialize-tree attr write (SSR).
|
|
2767
|
+
* @param {any} el
|
|
2768
|
+
* @param {string} name
|
|
2769
|
+
* @param {any} value
|
|
2770
|
+
*/
|
|
2771
|
+
function applySerializeAttr(el, name, value) {
|
|
2772
|
+
if (!el || el.__kind !== 'el')
|
|
2773
|
+
return;
|
|
2774
|
+
const key = name === 'className' ? 'class' : name;
|
|
2775
|
+
if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
|
|
2776
|
+
if (value === false || value == null || value === '')
|
|
2777
|
+
delete el.attrs[key];
|
|
2778
|
+
else
|
|
2779
|
+
el.attrs[key] = value === true ? '' : String(value);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
if (value == null || value === false)
|
|
2783
|
+
delete el.attrs[key];
|
|
2784
|
+
else
|
|
2785
|
+
el.attrs[key] = value === true ? '' : String(value);
|
|
2786
|
+
}
|
|
1443
2787
|
function stripFns(obj) {
|
|
1444
2788
|
/** @type {Record<string, unknown>} */
|
|
1445
2789
|
const out = {};
|
|
@@ -1475,7 +2819,7 @@ function eachHostApi(start, end) {
|
|
|
1475
2819
|
};
|
|
1476
2820
|
}
|
|
1477
2821
|
/**
|
|
1478
|
-
* Snapshot plain state/prop field values for Island HMR (
|
|
2822
|
+
* Snapshot plain state/prop field values for Island HMR (session).
|
|
1479
2823
|
* @param {object} inst
|
|
1480
2824
|
* @returns {Record<string, unknown> | null}
|
|
1481
2825
|
*/
|
|
@@ -1515,7 +2859,7 @@ export function applyPreservedState(inst, state) {
|
|
|
1515
2859
|
}
|
|
1516
2860
|
}
|
|
1517
2861
|
/**
|
|
1518
|
-
*
|
|
2862
|
+
* resume: attach to existing Island DOM without re-running construct structure or onMount.
|
|
1519
2863
|
* Consumes ResumeEntry product (`data-vmz-resume`) derived from the same Execution Plan.
|
|
1520
2864
|
* @param {new (props?: object) => any} Component
|
|
1521
2865
|
* @param {HTMLElement} container
|
|
@@ -1565,14 +2909,14 @@ export async function resume(Component, container, slice = null) {
|
|
|
1565
2909
|
}
|
|
1566
2910
|
}
|
|
1567
2911
|
else {
|
|
1568
|
-
// Island leaf adopt: preserve Element identity (
|
|
2912
|
+
// Island leaf adopt: preserve Element identity (resume nodeIdentity).
|
|
1569
2913
|
const node = runDirectResume(Component, inst, container);
|
|
1570
2914
|
if (node)
|
|
1571
2915
|
inst.__vmzDomRoot = node;
|
|
1572
2916
|
}
|
|
1573
2917
|
}
|
|
1574
2918
|
else {
|
|
1575
|
-
throw new Error(`vmz:dom resume() requires __vmzCreate (Direct); blueprint render() removed (
|
|
2919
|
+
throw new Error(`vmz:dom resume() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
1576
2920
|
}
|
|
1577
2921
|
container.__vmzInst = inst;
|
|
1578
2922
|
container.__vmzResumed = true;
|
|
@@ -1628,7 +2972,7 @@ export function attachEventEntries(root = globalThis.document) {
|
|
|
1628
2972
|
}
|
|
1629
2973
|
}
|
|
1630
2974
|
/**
|
|
1631
|
-
* Adopt existing Island DOM while running the same `__vmzCreate` schedule (
|
|
2975
|
+
* Adopt existing Island DOM while running the same `__vmzCreate` schedule (resume).
|
|
1632
2976
|
* @param {new (props?: object) => any} Component
|
|
1633
2977
|
* @param {object} inst
|
|
1634
2978
|
* @param {Element} container
|
|
@@ -1666,18 +3010,15 @@ function runDirectResume(Component, inst, container) {
|
|
|
1666
3010
|
return document.createTextNode(String(s ?? ''));
|
|
1667
3011
|
},
|
|
1668
3012
|
attr(el, name, value) {
|
|
1669
|
-
|
|
1670
|
-
el.setAttribute('class', String(value ?? ''));
|
|
1671
|
-
else if (value == null || value === false)
|
|
1672
|
-
el.removeAttribute(name);
|
|
1673
|
-
else
|
|
1674
|
-
el.setAttribute(name, value === true ? '' : String(value));
|
|
3013
|
+
applyDomAttr(el, name, value);
|
|
1675
3014
|
},
|
|
1676
3015
|
on(el, type, handler) {
|
|
1677
3016
|
el.addEventListener(type, handler);
|
|
1678
3017
|
},
|
|
1679
3018
|
bindText: directApi.bindText,
|
|
1680
3019
|
bindAttr: directApi.bindAttr,
|
|
3020
|
+
bindComponentProp: directApi.bindComponentProp,
|
|
3021
|
+
projectDefaultSlot: directApi.projectDefaultSlot,
|
|
1681
3022
|
setHtml: directApi.setHtml,
|
|
1682
3023
|
bindHtml: directApi.bindHtml,
|
|
1683
3024
|
ifBlock: directApi.ifBlock,
|
|
@@ -1712,9 +3053,9 @@ export async function hydrate(Component, container, props = {}, opts = {}) {
|
|
|
1712
3053
|
if (preserved) {
|
|
1713
3054
|
applyPreservedState(inst, preserved);
|
|
1714
3055
|
}
|
|
1715
|
-
//
|
|
3056
|
+
// production Direct emit: hydrate uses the same Direct schedule as resume (no render).
|
|
1716
3057
|
if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
|
|
1717
|
-
throw new Error(`vmz:dom hydrate() requires __vmzCreate (Direct); blueprint render() removed (
|
|
3058
|
+
throw new Error(`vmz:dom hydrate() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
|
|
1718
3059
|
}
|
|
1719
3060
|
// Wire DOM + events BEFORE awaiting onMount. SSR shell is already visible; if we
|
|
1720
3061
|
// wait on RPC/bootstrap first, buttons look real but have no listeners (dead UI).
|
|
@@ -1747,7 +3088,7 @@ export async function hydrate(Component, container, props = {}, opts = {}) {
|
|
|
1747
3088
|
/**
|
|
1748
3089
|
* Tear down binders and stop patches. Safe to call more than once.
|
|
1749
3090
|
* Field writes after destroy no longer update DOM (values may still change).
|
|
1750
|
-
|
|
3091
|
+
*: also dispose owned DOM trees (child __vmzInst / region __vmzDispose).
|
|
1751
3092
|
* @param {object} inst
|
|
1752
3093
|
*/
|
|
1753
3094
|
export function destroy(inst) {
|
|
@@ -1755,7 +3096,7 @@ export function destroy(inst) {
|
|
|
1755
3096
|
return;
|
|
1756
3097
|
inst.__vmzDestroyed = true;
|
|
1757
3098
|
inst.__vmzFlushScheduled = false;
|
|
1758
|
-
//
|
|
3099
|
+
// async cancel: abort in-flight tasks before tearing down DOM.
|
|
1759
3100
|
__vmzCancelTasks(inst);
|
|
1760
3101
|
if (inst.__vmzDomRoot) {
|
|
1761
3102
|
disposeDomTree(inst.__vmzDomRoot);
|
|
@@ -1780,7 +3121,7 @@ export function destroy(inst) {
|
|
|
1780
3121
|
}
|
|
1781
3122
|
}
|
|
1782
3123
|
/**
|
|
1783
|
-
|
|
3124
|
+
*: walk a DOM subtree and run lifetime dispose hooks + nested instance destroy.
|
|
1784
3125
|
* Does not mark the *calling* parent destroyed; safe from destroy(inst).
|
|
1785
3126
|
* @param {Node | null | undefined} root
|
|
1786
3127
|
*/
|
|
@@ -1819,7 +3160,7 @@ export function disposeDomTree(root) {
|
|
|
1819
3160
|
* @param {ParentNode} [root]
|
|
1820
3161
|
*/
|
|
1821
3162
|
export function hydrateIslands(root = globalThis.document) {
|
|
1822
|
-
//
|
|
3163
|
+
// resume: hydrateIslands is an alias for resumeIslands (same Plan attach).
|
|
1823
3164
|
return resumeIslands(root);
|
|
1824
3165
|
}
|
|
1825
3166
|
export function scheduleClient(strategy, fn) {
|
|
@@ -1918,7 +3259,7 @@ export function __vmzRunTask(inst, key, fn) {
|
|
|
1918
3259
|
status: 'pending',
|
|
1919
3260
|
};
|
|
1920
3261
|
inst.__vmzTasks[k] = entry;
|
|
1921
|
-
// Invoke synchronously so event handlers can call preventDefault
|
|
3262
|
+
// Invoke synchronously so event handlers can call preventDefault before
|
|
1922
3263
|
// the browser continues the default action (form submit → native navigation).
|
|
1923
3264
|
// Async work still continues via the returned Promise.
|
|
1924
3265
|
let syncResult;
|
|
@@ -1992,7 +3333,7 @@ function createInstance(Component, props = {}) {
|
|
|
1992
3333
|
inst.__vmzDepToBindings = Object.create(null);
|
|
1993
3334
|
makeReactive(inst, Component.__vmzState || []);
|
|
1994
3335
|
makeReactive(inst, Component.__vmzProps || []);
|
|
1995
|
-
//
|
|
3336
|
+
// WriteBarrier: path / array writes call Component helpers (no import needed).
|
|
1996
3337
|
Component.__vmzWritePath = __vmzWritePath;
|
|
1997
3338
|
Component.__vmzWritePathLogical = __vmzWritePathLogical;
|
|
1998
3339
|
Component.__vmzReadPath = __vmzReadPath;
|
|
@@ -2003,13 +3344,12 @@ function createInstance(Component, props = {}) {
|
|
|
2003
3344
|
}
|
|
2004
3345
|
/** Shared plain-object owners under WriteBarrier (no Proxy). */
|
|
2005
3346
|
const wbSharedOwners = new WeakMap();
|
|
2006
|
-
/** Objects explicitly marked OK to share across component instances (13
|
|
3347
|
+
/** Objects explicitly marked OK to share across component instances (13 ). */
|
|
2007
3348
|
const wbAllowShared = new WeakSet();
|
|
2008
3349
|
/** @type {Array<{ kind: string, message: string }>} */
|
|
2009
3350
|
const wbCrossComponentDiags = [];
|
|
2010
3351
|
/**
|
|
2011
3352
|
* Mark a plain object as intentionally shared across ownership boundaries.
|
|
2012
|
-
* Suppresses cross-component shared diagnostics (规划设计/vmz/13 §7.3).
|
|
2013
3353
|
* @param {any} value
|
|
2014
3354
|
*/
|
|
2015
3355
|
export function __vmzAllowShared(value) {
|
|
@@ -2056,11 +3396,10 @@ function registerWbOwner(value, report, baseSegs = [], inst = null) {
|
|
|
2056
3396
|
return;
|
|
2057
3397
|
}
|
|
2058
3398
|
entry.owners.push({ report, baseSegs: baseSegs.slice(), inst });
|
|
2059
|
-
// Cross-component share without explicit allow → diagnose (13
|
|
3399
|
+
// Cross-component share without explicit allow → diagnose (13 ).
|
|
2060
3400
|
if (!wbAllowShared.has(value) && inst) {
|
|
2061
3401
|
const other = entry.owners.find((o) => o.inst && o.inst !== inst);
|
|
2062
3402
|
if (other) {
|
|
2063
|
-
const msg = 'vmz: plain object shared across component instances without __vmzAllowShared (规划设计/vmz/13 §7.3)';
|
|
2064
3403
|
if (!wbCrossComponentDiags.some((d) => d.message === msg)) {
|
|
2065
3404
|
wbCrossComponentDiags.push({ kind: 'shared_cross_component', message: msg });
|
|
2066
3405
|
}
|
|
@@ -2134,7 +3473,6 @@ export function __vmzWritePathLogical(inst, root, segs, kind, rhs) {
|
|
|
2134
3473
|
return __vmzWritePath(inst, root, segs, rhs);
|
|
2135
3474
|
}
|
|
2136
3475
|
/**
|
|
2137
|
-
* Compiler-inserted path write barrier (规划设计/vmz/13 §7.3).
|
|
2138
3476
|
* Mutates a plain owned object/array and schedules the same path notice Proxy would.
|
|
2139
3477
|
*
|
|
2140
3478
|
* Root-array index assigns (`tags[0] = x`) notify as field replace (structural),
|
|
@@ -2273,7 +3611,7 @@ const reactiveProxies = new WeakMap();
|
|
|
2273
3611
|
/** Plain objects using defineProperty write barriers (not Proxy). */
|
|
2274
3612
|
const writeBarrierOwned = new WeakSet();
|
|
2275
3613
|
/**
|
|
2276
|
-
*
|
|
3614
|
+
* WriteBarrier: true when value is an owned plain object with path barriers (no Proxy).
|
|
2277
3615
|
* @param {any} value
|
|
2278
3616
|
*/
|
|
2279
3617
|
export function __vmzIsWriteBarrierOwned(value) {
|
|
@@ -2331,8 +3669,9 @@ function notifyOwners(owners, localSegs) {
|
|
|
2331
3669
|
}
|
|
2332
3670
|
/**
|
|
2333
3671
|
* Field-owned write traps for plain objects / arrays on state fields.
|
|
2334
|
-
* Plain objects: WriteBarrier via defineProperty (no Proxy)
|
|
2335
|
-
* Arrays: transitional Proxy
|
|
3672
|
+
* Plain objects: WriteBarrier via defineProperty (no Proxy).
|
|
3673
|
+
* Arrays: transitional Proxy tracks list identity/mutators only; elements stay plain
|
|
3674
|
+
* (no per-item wrap on large assign — nested notifies via `__vmzWritePath`).
|
|
2336
3675
|
* Shared raw objects notify **all** current owners.
|
|
2337
3676
|
*
|
|
2338
3677
|
* @param {any} value
|
|
@@ -2414,6 +3753,12 @@ function installOwnedProp(obj, prop, entry) {
|
|
|
2414
3753
|
},
|
|
2415
3754
|
});
|
|
2416
3755
|
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Transitional array Proxy: track list identity / mutators only.
|
|
3758
|
+
* Elements stay plain — no per-item defineProperty on `this.rows = largeArray`
|
|
3759
|
+
* (design: WriteBarrier / list replace must not wrap 1k items). Nested field
|
|
3760
|
+
* notifies go through `__vmzWritePath` or whole-array replace.
|
|
3761
|
+
*/
|
|
2417
3762
|
function wrapArray(arr, report, pathSegs) {
|
|
2418
3763
|
const existing = reactiveProxies.get(arr);
|
|
2419
3764
|
if (existing) {
|
|
@@ -2427,88 +3772,32 @@ function wrapArray(arr, report, pathSegs) {
|
|
|
2427
3772
|
kind: 'proxy',
|
|
2428
3773
|
};
|
|
2429
3774
|
addOwner(entry, report, pathSegs);
|
|
2430
|
-
|
|
2431
|
-
const item = arr[i];
|
|
2432
|
-
for (const o of entry.owners) {
|
|
2433
|
-
arr[i] = wrapReactive(item, o.report, [...o.baseSegs, String(i)]);
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
3775
|
+
const isArrayIndex = (prop) => typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
|
|
2436
3776
|
const proxy = new Proxy(arr, {
|
|
2437
3777
|
get(target, prop, receiver) {
|
|
2438
3778
|
if (typeof prop === 'string' && ARRAY_MUTATORS.has(prop)) {
|
|
2439
3779
|
const fn = target[prop];
|
|
2440
3780
|
return (...args) => {
|
|
2441
|
-
const
|
|
2442
|
-
if (prop === 'splice' && idx >= 2) {
|
|
2443
|
-
let w = a;
|
|
2444
|
-
for (const o of entry.owners) {
|
|
2445
|
-
w = wrapReactive(a, o.report, o.baseSegs.slice());
|
|
2446
|
-
}
|
|
2447
|
-
return w;
|
|
2448
|
-
}
|
|
2449
|
-
if ((prop === 'push' || prop === 'unshift') && typeof a === 'object') {
|
|
2450
|
-
let w = a;
|
|
2451
|
-
for (const o of entry.owners) {
|
|
2452
|
-
w = wrapReactive(a, o.report, o.baseSegs.slice());
|
|
2453
|
-
}
|
|
2454
|
-
return w;
|
|
2455
|
-
}
|
|
2456
|
-
if (prop === 'fill') {
|
|
2457
|
-
let w = a;
|
|
2458
|
-
for (const o of entry.owners) {
|
|
2459
|
-
w = wrapReactive(a, o.report, o.baseSegs.slice());
|
|
2460
|
-
}
|
|
2461
|
-
return w;
|
|
2462
|
-
}
|
|
2463
|
-
return a;
|
|
2464
|
-
});
|
|
2465
|
-
const ret = fn.apply(target, wrappedArgs);
|
|
3781
|
+
const ret = fn.apply(target, args);
|
|
2466
3782
|
notifyOwners(entry.owners, null);
|
|
2467
3783
|
return ret;
|
|
2468
3784
|
};
|
|
2469
3785
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
let nested = null;
|
|
2473
|
-
for (const o of entry.owners) {
|
|
2474
|
-
nested = wrapReactive(v, o.report, [...o.baseSegs, prop]);
|
|
2475
|
-
}
|
|
2476
|
-
return nested;
|
|
2477
|
-
}
|
|
2478
|
-
return v;
|
|
3786
|
+
// Indices / length / methods: return as-is (plain elements).
|
|
3787
|
+
return Reflect.get(target, prop, receiver);
|
|
2479
3788
|
},
|
|
2480
3789
|
set(target, prop, next, receiver) {
|
|
2481
|
-
const isIndex = typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
|
|
2482
|
-
const allRoot = entry.owners.every((o) => o.baseSegs.length === 0);
|
|
2483
|
-
if (isIndex && allRoot) {
|
|
2484
|
-
let wrapped = next;
|
|
2485
|
-
for (const o of entry.owners) {
|
|
2486
|
-
wrapped = wrapReactive(next, o.report, [...o.baseSegs, prop]);
|
|
2487
|
-
}
|
|
2488
|
-
const prev = target[prop];
|
|
2489
|
-
if (Object.is(prev, wrapped))
|
|
2490
|
-
return true;
|
|
2491
|
-
const ok = Reflect.set(target, prop, wrapped, receiver);
|
|
2492
|
-
if (ok)
|
|
2493
|
-
notifyOwners(entry.owners, null);
|
|
2494
|
-
return ok;
|
|
2495
|
-
}
|
|
2496
|
-
const local = prop === 'length' || typeof prop !== 'string' ? [] : [prop];
|
|
2497
|
-
let wrapped = next;
|
|
2498
|
-
if (prop !== 'length') {
|
|
2499
|
-
for (const o of entry.owners) {
|
|
2500
|
-
wrapped = wrapReactive(next, o.report, [...o.baseSegs, ...local]);
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
3790
|
const prev = target[prop];
|
|
2504
|
-
if (Object.is(prev,
|
|
3791
|
+
if (Object.is(prev, next))
|
|
2505
3792
|
return true;
|
|
2506
|
-
const ok = Reflect.set(target, prop,
|
|
3793
|
+
const ok = Reflect.set(target, prop, next, receiver);
|
|
2507
3794
|
if (ok) {
|
|
2508
|
-
if (prop === 'length')
|
|
3795
|
+
if (prop === 'length' || isArrayIndex(prop))
|
|
2509
3796
|
notifyOwners(entry.owners, null);
|
|
3797
|
+
else if (typeof prop === 'string')
|
|
3798
|
+
notifyOwners(entry.owners, [prop]);
|
|
2510
3799
|
else
|
|
2511
|
-
notifyOwners(entry.owners,
|
|
3800
|
+
notifyOwners(entry.owners, null);
|
|
2512
3801
|
}
|
|
2513
3802
|
return ok;
|
|
2514
3803
|
},
|
|
@@ -2532,14 +3821,13 @@ function wrapArray(arr, report, pathSegs) {
|
|
|
2532
3821
|
* Still precise deps — never a full-tree re-render. Flush runs as a microtask;
|
|
2533
3822
|
* call `await flushPending(inst)` to apply synchronously (tests / immediate UI).
|
|
2534
3823
|
*
|
|
2535
|
-
* Design: 规划设计/vmz/12 §6 — parent write covers children; siblings stay separate.
|
|
2536
3824
|
*
|
|
2537
3825
|
* @param {object} inst
|
|
2538
3826
|
* @param {{ type: 'replace', root: string } | { type: 'path', root: string, segs: string[] } | string} notice
|
|
2539
|
-
*
|
|
3827
|
+
* string form is transitional field-root alias for replace.
|
|
2540
3828
|
*/
|
|
2541
3829
|
function scheduleRefresh(inst, notice) {
|
|
2542
|
-
if (!inst || inst.__vmzDestroyed)
|
|
3830
|
+
if (!inst || inst.__vmzDestroyed || inst.__vmzQuiet)
|
|
2543
3831
|
return;
|
|
2544
3832
|
const n = typeof notice === 'string' ? { type: 'replace', root: notice } : notice;
|
|
2545
3833
|
if (!n || !n.root)
|
|
@@ -2642,9 +3930,10 @@ export async function flushPending(inst) {
|
|
|
2642
3930
|
jobs.push(...refreshBinding(inst, id, trie));
|
|
2643
3931
|
}
|
|
2644
3932
|
for (const key of binderKeysMatchingTrie(inst, trie)) {
|
|
2645
|
-
if (coveredDeps[key])
|
|
2646
|
-
|
|
2647
|
-
|
|
3933
|
+
if (coveredDeps[key] || (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length)) {
|
|
3934
|
+
// BindingId path already flushed IR patches for this dep.
|
|
3935
|
+
// Still run binder-only patches (bindComponentProp uses bindingId null).
|
|
3936
|
+
jobs.push(...refreshFieldBinderOnly(inst, key));
|
|
2648
3937
|
continue;
|
|
2649
3938
|
}
|
|
2650
3939
|
jobs.push(...refreshField(inst, key));
|
|
@@ -2822,7 +4111,6 @@ function pathDirtyCovers(node, depSegs) {
|
|
|
2822
4111
|
}
|
|
2823
4112
|
/**
|
|
2824
4113
|
* Dual-track match retained for tests / tooling.
|
|
2825
|
-
* Design: 规划设计/vmz/11 §2.2 + 12 §6 parent-covers-children.
|
|
2826
4114
|
* @param {{ type: string, root: string, segs?: string[] }} notice
|
|
2827
4115
|
* @param {string} key
|
|
2828
4116
|
*/
|
|
@@ -2938,6 +4226,32 @@ function refreshField(inst, field) {
|
|
|
2938
4226
|
}
|
|
2939
4227
|
return jobs;
|
|
2940
4228
|
}
|
|
4229
|
+
/**
|
|
4230
|
+
* Run `__vmzBinders` patches that are not owned by a BindingId entry.
|
|
4231
|
+
* Needed so `bindComponentProp` (bindingId null) still flushes when the same
|
|
4232
|
+
* dep also has IR bindText/bindAttr BindingIds.
|
|
4233
|
+
* @returns {Promise[]}
|
|
4234
|
+
*/
|
|
4235
|
+
function refreshFieldBinderOnly(inst, field) {
|
|
4236
|
+
const binders = inst.__vmzBinders;
|
|
4237
|
+
const jobs = [];
|
|
4238
|
+
if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
|
|
4239
|
+
return jobs;
|
|
4240
|
+
}
|
|
4241
|
+
for (const fn of binders[field]) {
|
|
4242
|
+
if (patchHasBindingId(inst, fn))
|
|
4243
|
+
continue;
|
|
4244
|
+
try {
|
|
4245
|
+
const ret = runPatch(fn, field, null);
|
|
4246
|
+
if (ret && typeof ret.then === 'function')
|
|
4247
|
+
jobs.push(ret);
|
|
4248
|
+
}
|
|
4249
|
+
catch (err) {
|
|
4250
|
+
console.error('vmz:dom patch', err);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
return jobs;
|
|
4254
|
+
}
|
|
2941
4255
|
/**
|
|
2942
4256
|
* @param {object} inst
|
|
2943
4257
|
* @param {number|string} bindingId
|