@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/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
- /** X5: optional StableId event ring (enabled with precision or __vmzTraceEnable). */
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
- * X5 StableId event snapshot (`vmz.dx.trace.v0` shape without schema stamp —
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
- // Gate 3: SSR only via Direct serialize schedule — never `render()`.
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 (Gate 3)`);
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 (Gate 3)`);
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 (L4 lifetime).
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` (Gate 3 — no blueprint fallback).
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()` schedules child onMount asynchronously; drain before return
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 (Gate 3).
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 (Gate 3)`);
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 = null;
420
+ directApi._inst = prevInst;
421
+ directApi._branchBinds = prevBranch;
422
+ directApi._itemPatches = prevItems;
423
+ directApi._eachCtx = prevEach;
299
424
  }
300
425
  }
301
426
  /**
302
- * L3 SSR: run the same __vmzCreate schedule against a serialize host (no render()).
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
- if (value == null)
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
- const key = name === 'className' ? 'class' : name;
508
- if (raw == null)
509
- delete el.attrs[key];
510
- else
511
- el.attrs[key] = String(raw);
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
- // Parity with Direct eachBlock: keyed items expose data-vmz-key for hydrate/tests.
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
- // L5: Island SSR includes body + ResumeEntry slice (same Direct schedule).
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: () => any, bindingId?: number|string|null }> | null} */
853
+ /** @type {Array<{ deps: string[], fn: => any, bindingId?: number|string|null }> | null} */
687
854
  _branchBinds: null,
688
- /** @type {Array<() => void> | null} */
855
+ /** @type {Array< => void> | null} */
689
856
  _itemPatches: null,
690
857
  /**
691
- * Active keyed-each context (P0/P1): item binds + event delegation.
858
+ * Active keyed-each context (/): item binds + event delegation.
692
859
  * @type {null | {
693
- * noteItemBind: (bindingId: number|string|null, deps: string[], fn: () => void) => void,
694
- * needDelegate: (type: string) => void,
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
- if (value == null)
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?: () => any, deps: string[] }> } | null | undefined} [cf]
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?: () => any, deps: string[] }> } | null | undefined} [cf]
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
- el.setAttribute('class', String(raw ?? ''));
760
- }
761
- else if (raw == null) {
762
- el.removeAttribute(name);
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.setAttribute(name, String(raw));
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?: () => any, deps: string[] }> } | null | undefined} [cf]
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
- // L5: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
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?: () => any, create: (api: typeof directApi) => Node }>} branches
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: () => any, bindingId?: number|string|null }>>} */
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
- // L4: parent destroy disposes all cached branch trees (pause ≠ destroy on switch).
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
- * P0/P1: Set/Map + Fragment batch insert; item-local binds; host selected; event delegate.
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: () => any, key?: (box: {item:any,index:number}) => any, createItem: (api: typeof directApi, box: {item:any,index:number}) => Node }} spec
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<() => void> }>} */
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, () => void>} */
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 || !entry.patches)
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 && p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
1021
- continue;
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 box = { item: list[i], index: i };
1038
- const k = itemKey(box);
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.box.item = list[i];
1043
- entry.box.index = i;
1044
- tagItemPatches(entry.patches, i);
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 bag = /** @type {Element} */ (n).__vmzEvt;
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
- bag[type].call(inst, ev);
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
- let ok = true;
1199
- let n = start.nextSibling;
1200
- for (let i = 0; i < nextNodes.length; i++) {
1201
- if (n !== nextNodes[i]) {
1202
- ok = false;
1203
- break;
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
- if (ok && n === end)
1208
- return;
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 seen = new Set();
1223
- const nextNodes = [];
1224
- for (let i = 0; i < list.length; i++) {
1225
- if (applied !== gen || inst.__vmzDestroyed)
1226
- return;
1227
- const box = { item: list[i], index: i };
1228
- const k = itemKey(box);
1229
- if (seen.has(k)) {
1230
- console.error(`vmz:dom each: duplicate key ${String(k)} — undefined reuse; fix the key expression (规划设计/vmz/10 §7)`);
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
- seen.add(k);
1233
- let entry = keyed.get(k);
1234
- if (!entry) {
1235
- const patches = [];
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 = patches;
2278
+ directApi._itemPatches = patches0;
1239
2279
  directApi._eachCtx = eachCtx;
1240
- let dom = null;
2280
+ let dom0 = null;
1241
2281
  try {
1242
- dom = spec.createItem.call(inst, directApi, box);
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
- tagItemPatches(patches, i);
1251
- if (dom) {
1252
- if (dom.nodeType === 1) {
1253
- dom.setAttribute('data-vmz-key', String(k));
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
- entry.box.item = list[i];
1261
- entry.box.index = i;
1262
- tagItemPatches(entry.patches, i);
1263
- for (const p of entry.patches)
1264
- runPatch(p, null);
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.dom);
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
- clearDomEvt(entry.dom);
1276
- disposeDomTree(entry.dom);
1277
- if (entry.dom && entry.dom.parentNode)
1278
- entry.dom.remove();
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
- for (const [, entry] of [...keyed.entries()]) {
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] || `${depRootField((deps && deps[0]) || '')}.*`;
2568
+ const softKey = softDeps[0] || `${listRoot}.*`;
1311
2569
  for (let i = 0; i < list.length; i++) {
1312
- const box = { item: list[i], index: i };
1313
- const k = itemKey(box);
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.box.item = list[i];
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 (P1).
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
- // P1: item binds stay on entry.patches; eachBlock registers one dispatcher per BindingId.
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?: () => any, deps: string[] }> } | null | undefined} [cf]
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) ? [...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 (N4.3).
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
- * L5: attach to existing Island DOM without re-running construct structure or onMount.
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 (L5 nodeIdentity).
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 (Gate 3)`);
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 (L5).
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
- if (name === 'className')
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
- // Gate 3: hydrate uses the same Direct schedule as resume (no render()).
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 (Gate 3)`);
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
- * L4: also dispose owned DOM trees (child __vmzInst / region __vmzDispose).
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
- // L4 async cancel: abort in-flight tasks before tearing down DOM.
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
- * L4: walk a DOM subtree and run lifetime dispose hooks + nested instance destroy.
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
- // L5: hydrateIslands is an alias for resumeIslands (same Plan attach).
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() before
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
- // L4 WriteBarrier: path / array writes call Component helpers (no import needed).
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 §7.3). */
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 §7.3).
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
- * L4 WriteBarrier: true when value is an owned plain object with path barriers (no Proxy).
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) — L4 / 13 §7.3.
2335
- * Arrays: transitional Proxy until keyed collection barriers land.
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
- for (let i = 0; i < arr.length; i++) {
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 wrappedArgs = args.map((a, idx) => {
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
- const v = Reflect.get(target, prop, receiver);
2471
- if (v && typeof v === 'object' && typeof prop === 'string') {
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, wrapped))
3791
+ if (Object.is(prev, next))
2505
3792
  return true;
2506
- const ok = Reflect.set(target, prop, wrapped, receiver);
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, local);
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
- * string form is transitional field-root alias for replace.
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
- continue;
2647
- if (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length) {
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