@vmz/core 0.0.0 → 0.0.2

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 ADDED
@@ -0,0 +1,3067 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * VMZ DOM / SSR runtime — precise patches, no VDOM diff.
4
+ *
5
+ * Design: 规划设计/vmz/04 · Gate 3 (no production `render()`)
6
+ *
7
+ * Direct components expose `__vmzCreate` / `__vmzSerialize` / `__vmzPlan`.
8
+ * Mount, SSR, hydrate, and resume all run that same schedule.
9
+ * Field writes only run registered dep patches — never re-create structure.
10
+ */
11
+ /** @type {Record<string, new (props?: object) => any>} */
12
+ const components = Object.create(null);
13
+ /**
14
+ * Precision lab counters (test / MCP / benchmarks — not a user API).
15
+ * Design: 规划设计/vmz/12 §7
16
+ * Primary keys: BindingId (IR). `*ByDep` is transitional stable-string adapter.
17
+ */
18
+ const precision = {
19
+ enabled: false,
20
+ writes: 0,
21
+ bindingEvals: 0,
22
+ patchExecs: 0,
23
+ domCreates: 0,
24
+ domMoves: 0,
25
+ domRemoves: 0,
26
+ componentExecs: 0,
27
+ /** @type {Record<string, number>} */
28
+ writesByRoot: Object.create(null),
29
+ /** @type {Record<string, number>} */
30
+ bindingEvalsByDep: Object.create(null),
31
+ /** @type {Record<string, number>} */
32
+ patchesByDep: Object.create(null),
33
+ /** @type {Record<string, number>} BindingId → count */
34
+ bindingEvalsByBinding: Object.create(null),
35
+ /** @type {Record<string, number>} BindingId → count */
36
+ patchesByBinding: Object.create(null),
37
+ };
38
+ /** X5: optional StableId event ring (enabled with precision or __vmzTraceEnable). */
39
+ const TRACE_CAP = 256;
40
+ /** @type {{ enabled: boolean, events: Array<{ kind: string, stableId: { kind: string, id: string }, dep?: string|null, t?: number, chunkId?: string|null }> }} */
41
+ const traceBuf = {
42
+ enabled: false,
43
+ events: [],
44
+ };
45
+ function pushTrace(kind, stableKind, stableId, dep = null) {
46
+ if (!traceBuf.enabled && !precision.enabled)
47
+ return;
48
+ traceBuf.events.push({
49
+ kind,
50
+ stableId: { kind: stableKind, id: String(stableId) },
51
+ dep: dep == null ? undefined : String(dep),
52
+ t: Date.now(),
53
+ });
54
+ if (traceBuf.events.length > TRACE_CAP) {
55
+ traceBuf.events.splice(0, traceBuf.events.length - TRACE_CAP);
56
+ }
57
+ }
58
+ function bumpMap(map, key, n = 1) {
59
+ if (key == null || key === '')
60
+ return;
61
+ map[key] = (map[key] || 0) + n;
62
+ }
63
+ /** @param {boolean} [on] */
64
+ export function __vmzPrecisionEnable(on = true) {
65
+ precision.enabled = !!on;
66
+ }
67
+ /** @param {boolean} [on] */
68
+ export function __vmzTraceEnable(on = true) {
69
+ traceBuf.enabled = !!on;
70
+ }
71
+ export function __vmzPrecisionReset() {
72
+ precision.writes = 0;
73
+ precision.bindingEvals = 0;
74
+ precision.patchExecs = 0;
75
+ precision.domCreates = 0;
76
+ precision.domMoves = 0;
77
+ precision.domRemoves = 0;
78
+ precision.componentExecs = 0;
79
+ precision.writesByRoot = Object.create(null);
80
+ precision.bindingEvalsByDep = Object.create(null);
81
+ precision.patchesByDep = Object.create(null);
82
+ precision.bindingEvalsByBinding = Object.create(null);
83
+ precision.patchesByBinding = Object.create(null);
84
+ }
85
+ export function __vmzTraceReset() {
86
+ traceBuf.events = [];
87
+ }
88
+ /**
89
+ * X5 StableId event snapshot (`vmz.dx.trace.v0` shape without schema stamp —
90
+ * host may wrap via ingestRuntimeTrace).
91
+ * @returns {{ schema: string, events: typeof traceBuf.events, status: string }}
92
+ */
93
+ export function __vmzTraceSnapshot() {
94
+ const events = traceBuf.events.map((e) => ({ ...e, stableId: { ...e.stableId } }));
95
+ return {
96
+ schema: 'vmz.dx.trace.v0',
97
+ events,
98
+ status: events.length ? 'ready' : 'empty',
99
+ };
100
+ }
101
+ /** @returns {typeof precision} */
102
+ export function __vmzPrecisionSnapshot() {
103
+ return {
104
+ enabled: precision.enabled,
105
+ writes: precision.writes,
106
+ bindingEvals: precision.bindingEvals,
107
+ patchExecs: precision.patchExecs,
108
+ domCreates: precision.domCreates,
109
+ domMoves: precision.domMoves,
110
+ domRemoves: precision.domRemoves,
111
+ componentExecs: precision.componentExecs,
112
+ writesByRoot: { ...precision.writesByRoot },
113
+ bindingEvalsByDep: { ...precision.bindingEvalsByDep },
114
+ patchesByDep: { ...precision.patchesByDep },
115
+ bindingEvalsByBinding: { ...precision.bindingEvalsByBinding },
116
+ patchesByBinding: { ...precision.patchesByBinding },
117
+ };
118
+ }
119
+ /**
120
+ * @param {() => any} fn
121
+ * @param {string | null} [depKey]
122
+ * @param {number | string | null} [bindingId]
123
+ */
124
+ function runPatch(fn, depKey = null, bindingId = null) {
125
+ if (precision.enabled) {
126
+ precision.patchExecs++;
127
+ if (depKey)
128
+ bumpMap(precision.patchesByDep, depKey);
129
+ if (bindingId != null)
130
+ bumpMap(precision.patchesByBinding, String(bindingId));
131
+ }
132
+ if (bindingId != null) {
133
+ pushTrace('patch', 'binding', bindingId, depKey);
134
+ }
135
+ return fn();
136
+ }
137
+ function noteDomCreate() {
138
+ if (precision.enabled)
139
+ precision.domCreates++;
140
+ }
141
+ function noteDomRemove() {
142
+ if (precision.enabled)
143
+ precision.domRemoves++;
144
+ }
145
+ function noteDomMove() {
146
+ if (precision.enabled)
147
+ precision.domMoves++;
148
+ }
149
+ /** @param {Record<string, any>} map */
150
+ export function registerComponents(map) {
151
+ Object.assign(components, map);
152
+ }
153
+ /**
154
+ * Lazy component loader for EventEntry mixed packs (set by entry-client / entry-event).
155
+ * @param {string} name
156
+ * @returns {Promise<any>}
157
+ */
158
+ async function resolveComponent(name) {
159
+ let Ctor = components[name];
160
+ if (!Ctor && typeof globalThis.__vmzLoadComponent === 'function') {
161
+ Ctor = await globalThis.__vmzLoadComponent(name);
162
+ if (Ctor)
163
+ registerComponents({ [name]: Ctor });
164
+ }
165
+ return Ctor || null;
166
+ }
167
+ /**
168
+ * @param {new (props?: object) => any} Component
169
+ * @param {object} [props]
170
+ */
171
+ export async function renderToString(Component, props = {}) {
172
+ const inst = createInstance(Component, props);
173
+ if (typeof inst.onMount === 'function') {
174
+ await inst.onMount();
175
+ }
176
+ // Gate 3: SSR only via Direct serialize schedule — never `render()`.
177
+ if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
178
+ throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (Gate 3)`);
179
+ }
180
+ const root = await runDirectSerializeTreeWithMounts(Component, inst);
181
+ return flattenSerializeNode(root);
182
+ }
183
+ /**
184
+ * Stream SSR via the same Direct serialize schedule as `renderToString`.
185
+ * Yields HTML chunks (open tag → children → close). Joining chunks equals `renderToString`.
186
+ * Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
187
+ * @param {new (props?: object) => any} Component
188
+ * @param {object} [props]
189
+ * @param {{ signal?: AbortSignal }} [opts]
190
+ * @returns {AsyncGenerator<string, void, void>}
191
+ */
192
+ export async function* renderToStream(Component, props = {}, opts = {}) {
193
+ const signal = opts && opts.signal;
194
+ const aborted = () => Boolean(signal && signal.aborted);
195
+ if (aborted())
196
+ return;
197
+ const inst = createInstance(Component, props);
198
+ try {
199
+ if (typeof inst.onMount === 'function') {
200
+ await inst.onMount();
201
+ }
202
+ if (aborted())
203
+ return;
204
+ if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
205
+ throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (Gate 3)`);
206
+ }
207
+ const root = await runDirectSerializeTreeWithMounts(Component, inst);
208
+ if (aborted())
209
+ return;
210
+ for (const chunk of streamSerializeChunks(root)) {
211
+ if (aborted())
212
+ return;
213
+ yield chunk;
214
+ // Allow consumers / HTTP hosts to flush between chunks (backpressure point).
215
+ await Promise.resolve();
216
+ }
217
+ }
218
+ finally {
219
+ // Abort and normal completion both dispose the SSR instance (L4 lifetime).
220
+ destroy(inst);
221
+ }
222
+ }
223
+ /**
224
+ * Mount once; later updates are dep patches only (never re-run structure).
225
+ * Requires compiler `__vmzCreate` (Gate 3 — no blueprint fallback).
226
+ * @param {new (props?: object) => any} Component
227
+ * @param {Element} container
228
+ * @param {object} [props]
229
+ */
230
+ export async function mount(Component, container, props = {}) {
231
+ if (container.__vmzInst) {
232
+ destroy(container.__vmzInst);
233
+ container.__vmzInst = null;
234
+ }
235
+ const inst = createInstance(Component, props);
236
+ inst.__vmzBinders = Object.create(null);
237
+ inst.__vmzBindings = Object.create(null);
238
+ inst.__vmzDepToBindings = Object.create(null);
239
+ container.replaceChildren();
240
+ const node = await createFromComponent(Component, inst);
241
+ if (node) {
242
+ inst.__vmzDomRoot = node;
243
+ container.appendChild(node);
244
+ }
245
+ if (typeof inst.onMount === 'function') {
246
+ await inst.onMount();
247
+ }
248
+ await settlePendingChildMounts(inst);
249
+ container.__vmzInst = inst;
250
+ return inst;
251
+ }
252
+ /**
253
+ * Nested Direct `component()` schedules child onMount asynchronously; drain before return
254
+ * so SSR/hydrate callers see post-mount DOM (e.g. UserCard Ada, not Loading…).
255
+ * @param {object} inst
256
+ */
257
+ async function settlePendingChildMounts(inst) {
258
+ if (!inst || !Array.isArray(inst.__vmzPendingChildMounts) || !inst.__vmzPendingChildMounts.length)
259
+ return;
260
+ await Promise.all(inst.__vmzPendingChildMounts);
261
+ inst.__vmzPendingChildMounts = [];
262
+ const hosts = [];
263
+ const root = inst.__vmzDomRoot;
264
+ if (root && root.nodeType === 1) {
265
+ if (root.__vmzInst)
266
+ hosts.push(root.__vmzInst);
267
+ for (const el of root.querySelectorAll('[data-vmz]')) {
268
+ if (el.__vmzInst)
269
+ hosts.push(el.__vmzInst);
270
+ }
271
+ }
272
+ for (const child of hosts) {
273
+ await flushPending(child);
274
+ await settlePendingChildMounts(child);
275
+ }
276
+ }
277
+ /**
278
+ * Direct create only (Gate 3).
279
+ * @param {new (props?: object) => any} Component
280
+ * @param {object} inst
281
+ */
282
+ async function createFromComponent(Component, inst) {
283
+ if (Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
284
+ return runDirectCreate(Component, inst);
285
+ }
286
+ throw new Error(`vmz:dom mount requires __vmzCreate (Direct); blueprint render() removed (Gate 3)`);
287
+ }
288
+ /**
289
+ * @param {new (props?: object) => any} Component
290
+ * @param {object} inst
291
+ */
292
+ function runDirectCreate(Component, inst) {
293
+ directApi._inst = inst;
294
+ try {
295
+ return Component.__vmzCreate.call(inst, directApi);
296
+ }
297
+ finally {
298
+ directApi._inst = null;
299
+ }
300
+ }
301
+ /**
302
+ * L3 SSR: run the same __vmzCreate schedule against a serialize host (no render()).
303
+ * @param {new (props?: object) => any} Component
304
+ * @param {object} inst
305
+ */
306
+ function runDirectSerializeTree(Component, inst) {
307
+ serializeApi._inst = inst;
308
+ try {
309
+ return Component.__vmzCreate.call(inst, serializeApi);
310
+ }
311
+ finally {
312
+ serializeApi._inst = null;
313
+ }
314
+ }
315
+ /**
316
+ * SSR child onMount: sync `__vmzCreate` cannot await nested mounts.
317
+ * Expand rounds — reuse prior child instances, await newly discovered onMounts, re-emit.
318
+ * @param {new (props?: object) => any} Component
319
+ * @param {object} inst
320
+ */
321
+ async function runDirectSerializeTreeWithMounts(Component, inst) {
322
+ /** @type {object[]} */
323
+ let preMounted = [];
324
+ /** @type {any} */
325
+ let tree = null;
326
+ for (let round = 0; round < 32; round++) {
327
+ serializeApi._ssrPreMounted = preMounted;
328
+ serializeApi._ssrPreIdx = 0;
329
+ serializeApi._ssrCollected = [];
330
+ tree = runDirectSerializeTree(Component, inst);
331
+ if (serializeApi._ssrPreIdx !== preMounted.length) {
332
+ throw new Error(`vmz:dom SSR child mount queue desync (used ${serializeApi._ssrPreIdx}, had ${preMounted.length})`);
333
+ }
334
+ const collected = serializeApi._ssrCollected;
335
+ serializeApi._ssrPreMounted = null;
336
+ serializeApi._ssrCollected = null;
337
+ if (!collected.length)
338
+ return tree;
339
+ for (const child of collected) {
340
+ if (typeof child.onMount === 'function') {
341
+ await child.onMount();
342
+ }
343
+ }
344
+ preMounted = preMounted.concat(collected);
345
+ }
346
+ throw new Error('vmz:dom SSR child onMount expansion exceeded 32 rounds');
347
+ }
348
+ function serializeOpenTag(node) {
349
+ const tag = node.tag || 'div';
350
+ let attrs = '';
351
+ for (const [k, v] of Object.entries(node.attrs || {})) {
352
+ if (v == null || v === false)
353
+ continue;
354
+ if (k === 'className')
355
+ attrs += ` class="${escapeHtml(v)}"`;
356
+ else
357
+ attrs += ` ${k}="${escapeHtml(v)}"`;
358
+ }
359
+ return { tag, open: `<${tag}${attrs}>` };
360
+ }
361
+ function flattenSerializeNode(node) {
362
+ if (node == null || node === false)
363
+ return '';
364
+ if (typeof node === 'string' || typeof node === 'number')
365
+ return escapeHtml(node);
366
+ if (node.__kind === 'text')
367
+ return escapeHtml(node.value);
368
+ if (node.__kind === 'frag') {
369
+ return (node.children || []).map(flattenSerializeNode).join('');
370
+ }
371
+ if (node.__kind === 'el') {
372
+ const tag = node.tag || 'div';
373
+ if (tag === 'slot') {
374
+ return (node.children || []).map(flattenSerializeNode).join('');
375
+ }
376
+ const { open } = serializeOpenTag(node);
377
+ if (node.__rawHtml != null) {
378
+ return `${open}${String(node.__rawHtml)}</${tag}>`;
379
+ }
380
+ const inner = (node.children || []).map(flattenSerializeNode).join('');
381
+ return `${open}${inner}</${tag}>`;
382
+ }
383
+ return '';
384
+ }
385
+ /**
386
+ * Progressive HTML chunks from a serialize tree (same nodes as flattenSerializeNode).
387
+ * @param {any} node
388
+ * @returns {Generator<string, void, void>}
389
+ */
390
+ function* streamSerializeChunks(node) {
391
+ if (node == null || node === false)
392
+ return;
393
+ if (typeof node === 'string' || typeof node === 'number') {
394
+ yield escapeHtml(node);
395
+ return;
396
+ }
397
+ if (node.__kind === 'text') {
398
+ yield escapeHtml(node.value);
399
+ return;
400
+ }
401
+ if (node.__kind === 'frag') {
402
+ for (const c of node.children || [])
403
+ yield* streamSerializeChunks(c);
404
+ return;
405
+ }
406
+ if (node.__kind === 'el') {
407
+ const tag = node.tag || 'div';
408
+ if (tag === 'slot') {
409
+ for (const c of node.children || [])
410
+ yield* streamSerializeChunks(c);
411
+ return;
412
+ }
413
+ const { open } = serializeOpenTag(node);
414
+ yield open;
415
+ if (node.__rawHtml != null) {
416
+ yield String(node.__rawHtml);
417
+ }
418
+ else {
419
+ for (const c of node.children || [])
420
+ yield* streamSerializeChunks(c);
421
+ }
422
+ yield `</${tag}>`;
423
+ }
424
+ }
425
+ /** Serialize host mirroring directApi — returns virtual nodes, not DOM. */
426
+ const serializeApi = {
427
+ /** @type {object | null} */
428
+ _inst: null,
429
+ /** @type {null} */
430
+ _branchBinds: null,
431
+ /** @type {null} */
432
+ _itemPatches: null,
433
+ /** @type {object[] | null} reused child instances from prior SSR mount rounds */
434
+ _ssrPreMounted: null,
435
+ /** @type {number} */
436
+ _ssrPreIdx: 0,
437
+ /** @type {object[] | null} newly created child instances this round */
438
+ _ssrCollected: null,
439
+ /**
440
+ * @param {new (props?: object) => any} Ctor
441
+ * @param {object} resolved
442
+ */
443
+ _ssrChildInstance(Ctor, resolved) {
444
+ const pre = serializeApi._ssrPreMounted;
445
+ if (pre && serializeApi._ssrPreIdx < pre.length) {
446
+ return pre[serializeApi._ssrPreIdx++];
447
+ }
448
+ const child = createInstance(Ctor, resolved);
449
+ if (serializeApi._ssrCollected)
450
+ serializeApi._ssrCollected.push(child);
451
+ return child;
452
+ },
453
+ el(tag) {
454
+ return {
455
+ __kind: 'el',
456
+ tag: tag || 'div',
457
+ attrs: {},
458
+ children: [],
459
+ appendChild(c) {
460
+ if (c != null)
461
+ this.children.push(c);
462
+ },
463
+ };
464
+ },
465
+ text(value) {
466
+ return { __kind: 'text', value: value == null ? '' : String(value) };
467
+ },
468
+ frag() {
469
+ return {
470
+ __kind: 'frag',
471
+ children: [],
472
+ appendChild(c) {
473
+ if (c != null)
474
+ this.children.push(c);
475
+ },
476
+ };
477
+ },
478
+ attr(el, name, value) {
479
+ if (!el || el.__kind !== 'el')
480
+ return;
481
+ if (value == null)
482
+ delete el.attrs[name];
483
+ else
484
+ el.attrs[name] = String(value);
485
+ },
486
+ on() {
487
+ /* events are no-ops during SSR */
488
+ },
489
+ bindText(inst, bindingId, deps, get, textNode) {
490
+ let raw = '';
491
+ try {
492
+ raw = get.call(inst);
493
+ }
494
+ catch {
495
+ raw = '';
496
+ }
497
+ textNode.value = String(raw ?? '');
498
+ },
499
+ bindAttr(inst, bindingId, deps, get, el, name) {
500
+ let raw;
501
+ try {
502
+ raw = get.call(inst);
503
+ }
504
+ catch {
505
+ raw = null;
506
+ }
507
+ const key = name === 'className' ? 'class' : name;
508
+ if (raw == null)
509
+ delete el.attrs[key];
510
+ else
511
+ el.attrs[key] = String(raw);
512
+ },
513
+ setHtml(el, value) {
514
+ if (!el || el.__kind !== 'el')
515
+ return;
516
+ el.__rawHtml = value == null ? '' : String(value);
517
+ el.children = [];
518
+ },
519
+ bindHtml(inst, bindingId, deps, get, el) {
520
+ let raw = '';
521
+ try {
522
+ raw = get.call(inst);
523
+ }
524
+ catch {
525
+ raw = '';
526
+ }
527
+ el.__rawHtml = raw == null ? '' : String(raw);
528
+ el.children = [];
529
+ },
530
+ ifBlock(inst, bindingId, deps, branches) {
531
+ const host = {
532
+ __kind: 'el',
533
+ tag: 'span',
534
+ attrs: { 'data-vmz-if': '' },
535
+ children: [],
536
+ appendChild(c) {
537
+ if (c != null)
538
+ this.children.push(c);
539
+ },
540
+ };
541
+ let idx = -1;
542
+ for (let i = 0; i < branches.length; i++) {
543
+ const b = branches[i];
544
+ if (!b.cond) {
545
+ idx = i;
546
+ break;
547
+ }
548
+ try {
549
+ if (b.cond.call(inst)) {
550
+ idx = i;
551
+ break;
552
+ }
553
+ }
554
+ catch {
555
+ /* continue */
556
+ }
557
+ }
558
+ if (idx >= 0 && branches[idx].create) {
559
+ const created = branches[idx].create.call(inst, serializeApi);
560
+ if (created)
561
+ host.children.push(created);
562
+ }
563
+ return host;
564
+ },
565
+ eachBlock(inst, bindingId, deps, spec) {
566
+ const frag = serializeApi.frag();
567
+ let list = [];
568
+ try {
569
+ list = spec.list.call(inst) || [];
570
+ }
571
+ catch {
572
+ list = [];
573
+ }
574
+ if (!Array.isArray(list))
575
+ list = [...list];
576
+ for (let i = 0; i < list.length; i++) {
577
+ const box = { item: list[i], index: i };
578
+ let k = i;
579
+ if (typeof spec.key === 'function') {
580
+ try {
581
+ k = spec.key.call(inst, box);
582
+ }
583
+ catch {
584
+ k = i;
585
+ }
586
+ }
587
+ const dom = spec.createItem.call(inst, serializeApi, box);
588
+ if (dom) {
589
+ // Parity with Direct eachBlock: keyed items expose data-vmz-key for hydrate/tests.
590
+ if (dom.__kind === 'el')
591
+ serializeApi.attr(dom, 'data-vmz-key', String(k));
592
+ frag.appendChild(dom);
593
+ }
594
+ }
595
+ return frag;
596
+ },
597
+ component(hostInst, name, props, client) {
598
+ const Ctor = components[name];
599
+ if (!Ctor)
600
+ throw new Error(`vmz:dom unknown component <${name} />`);
601
+ /** @type {Record<string, any>} */
602
+ const resolved = {};
603
+ for (const [k, v] of Object.entries(props || {})) {
604
+ if (typeof v === 'function' && isEventPropName(k))
605
+ continue;
606
+ else if (typeof v === 'function')
607
+ resolved[k] = v.call(hostInst);
608
+ else
609
+ resolved[k] = v;
610
+ }
611
+ if (client) {
612
+ // L5: Island SSR includes body + ResumeEntry slice (same Direct schedule).
613
+ const child = serializeApi._ssrChildInstance(Ctor, resolved);
614
+ let body = null;
615
+ if (Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function') {
616
+ const prev = serializeApi._inst;
617
+ serializeApi._inst = child;
618
+ try {
619
+ body = Ctor.__vmzCreate.call(child, serializeApi);
620
+ }
621
+ finally {
622
+ serializeApi._inst = prev;
623
+ }
624
+ }
625
+ const state = snapshotInstanceState(child) || {};
626
+ const plan = Ctor.__vmzPlan || null;
627
+ const resume = {
628
+ schema: 'vmz.resume.v0',
629
+ component: name,
630
+ strategy: String(client),
631
+ props: stripFns(resolved),
632
+ state,
633
+ planSchema: plan?.schema || null,
634
+ planRootIds: plan?.root_ids || [],
635
+ };
636
+ /** @type {Record<string, string>} */
637
+ const attrs = {
638
+ 'data-vmz': name,
639
+ 'data-vmz-island': name,
640
+ 'data-vmz-client': String(client),
641
+ 'data-vmz-props': JSON.stringify(stripFns(resolved)),
642
+ 'data-vmz-resume': JSON.stringify(resume),
643
+ };
644
+ if (isEventEntryStrategy(String(client))) {
645
+ attrs['data-vmz-entry'] = 'event';
646
+ }
647
+ return {
648
+ __kind: 'el',
649
+ tag: 'div',
650
+ attrs,
651
+ children: body ? [body] : [],
652
+ appendChild(c) {
653
+ if (c != null)
654
+ this.children.push(c);
655
+ },
656
+ };
657
+ }
658
+ const child = serializeApi._ssrChildInstance(Ctor, resolved);
659
+ if (Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function') {
660
+ const prev = serializeApi._inst;
661
+ serializeApi._inst = child;
662
+ try {
663
+ const node = Ctor.__vmzCreate.call(child, serializeApi);
664
+ return {
665
+ __kind: 'el',
666
+ tag: 'div',
667
+ attrs: { 'data-vmz': name },
668
+ children: node ? [node] : [],
669
+ appendChild(c) {
670
+ if (c != null)
671
+ this.children.push(c);
672
+ },
673
+ };
674
+ }
675
+ finally {
676
+ serializeApi._inst = prev;
677
+ }
678
+ }
679
+ throw new Error(`vmz:dom serialize component <${name}> requires __vmzCreate (rebuild child with Direct)`);
680
+ },
681
+ };
682
+ /** Host API for compiler-emitted `__vmzCreate` (direct path, Program IR B). */
683
+ const directApi = {
684
+ /** @type {object | null} */
685
+ _inst: null,
686
+ /** @type {Array<{ deps: string[], fn: () => any, bindingId?: number|string|null }> | null} */
687
+ _branchBinds: null,
688
+ /** @type {Array<() => void> | null} */
689
+ _itemPatches: null,
690
+ /**
691
+ * Active keyed-each context (P0/P1): item binds + event delegation.
692
+ * @type {null | {
693
+ * noteItemBind: (bindingId: number|string|null, deps: string[], fn: () => void) => void,
694
+ * needDelegate: (type: string) => void,
695
+ * }}
696
+ */
697
+ _eachCtx: null,
698
+ el(tag) {
699
+ noteDomCreate();
700
+ return document.createElement(tag || 'div');
701
+ },
702
+ text(value) {
703
+ noteDomCreate();
704
+ return document.createTextNode(value == null ? '' : String(value));
705
+ },
706
+ frag() {
707
+ noteDomCreate();
708
+ return document.createDocumentFragment();
709
+ },
710
+ attr(el, name, value) {
711
+ if (value == null)
712
+ el.removeAttribute(name);
713
+ else
714
+ el.setAttribute(name, String(value));
715
+ },
716
+ on(el, type, handler) {
717
+ const inst = directApi._inst;
718
+ if (directApi._eachCtx && typeof handler === 'function') {
719
+ /** @type {Record<string, Function>} */
720
+ const bag = el.__vmzEvt || (el.__vmzEvt = Object.create(null));
721
+ bag[type] = handler;
722
+ directApi._eachCtx.needDelegate(type);
723
+ return;
724
+ }
725
+ el.addEventListener(type, (ev) => {
726
+ // Belt-and-suspenders: form submit must not navigate before handler runs.
727
+ if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
728
+ ev.preventDefault();
729
+ }
730
+ if (typeof handler === 'function')
731
+ handler.call(inst, ev);
732
+ });
733
+ },
734
+ /**
735
+ * @param {object} inst
736
+ * @param {number|string|null} bindingId
737
+ * @param {string[]} deps
738
+ * @param {() => any} get
739
+ * @param {Text} textNode
740
+ * @param {{ stable: string[], branches: Array<{ cond?: () => any, deps: string[] }> } | null | undefined} [cf]
741
+ */
742
+ bindText(inst, bindingId, deps, get, textNode, cf) {
743
+ wireDirectBind(inst, bindingId, deps, get, (raw) => {
744
+ textNode.textContent = String(raw ?? '');
745
+ }, cf);
746
+ },
747
+ /**
748
+ * @param {object} inst
749
+ * @param {number|string|null} bindingId
750
+ * @param {string[]} deps
751
+ * @param {() => any} get
752
+ * @param {Element} el
753
+ * @param {string} name
754
+ * @param {{ stable: string[], branches: Array<{ cond?: () => any, deps: string[] }> } | null | undefined} [cf]
755
+ */
756
+ bindAttr(inst, bindingId, deps, get, el, name, cf) {
757
+ wireDirectBind(inst, bindingId, deps, get, (raw) => {
758
+ if (name === 'class' || name === 'className') {
759
+ el.setAttribute('class', String(raw ?? ''));
760
+ }
761
+ else if (raw == null) {
762
+ el.removeAttribute(name);
763
+ }
764
+ else {
765
+ el.setAttribute(name, String(raw));
766
+ }
767
+ }, cf);
768
+ },
769
+ setHtml(el, value) {
770
+ el.innerHTML = value == null ? '' : String(value);
771
+ },
772
+ /**
773
+ * Trusted HTML binding (`html={expr}`). Author/plugin responsibility.
774
+ * @param {object} inst
775
+ * @param {number|string|null} bindingId
776
+ * @param {string[]} deps
777
+ * @param {() => any} get
778
+ * @param {Element} el
779
+ * @param {{ stable: string[], branches: Array<{ cond?: () => any, deps: string[] }> } | null | undefined} [cf]
780
+ */
781
+ bindHtml(inst, bindingId, deps, get, el, cf) {
782
+ wireDirectBind(inst, bindingId, deps, get, (raw) => {
783
+ el.innerHTML = raw == null ? '' : String(raw);
784
+ }, cf);
785
+ },
786
+ /**
787
+ * Nested component (sync Direct child or island schedule).
788
+ * @param {object} hostInst
789
+ * @param {string} name
790
+ * @param {Record<string, any>} props
791
+ * @param {string | null} client
792
+ */
793
+ component(hostInst, name, props, client) {
794
+ noteDomCreate();
795
+ const host = document.createElement('div');
796
+ host.setAttribute('data-vmz', name);
797
+ /** @type {Record<string, any>} */
798
+ const resolved = {};
799
+ for (const [k, v] of Object.entries(props || {})) {
800
+ if (typeof v === 'function' && isEventPropName(k))
801
+ resolved[k] = v;
802
+ else if (typeof v === 'function')
803
+ resolved[k] = v.call(hostInst);
804
+ else
805
+ resolved[k] = v;
806
+ }
807
+ if (client) {
808
+ host.setAttribute('data-vmz-island', name);
809
+ host.setAttribute('data-vmz-client', String(client));
810
+ host.setAttribute('data-vmz-props', JSON.stringify(stripFns(resolved)));
811
+ if (isEventEntryStrategy(String(client))) {
812
+ host.setAttribute('data-vmz-entry', 'event');
813
+ }
814
+ // L5: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
815
+ scheduleClientOn(host, String(client), async () => {
816
+ const Ctor = await resolveComponent(name);
817
+ if (!Ctor)
818
+ throw new Error(`vmz:dom unknown component <${name} />`);
819
+ await resume(Ctor, host, { props: resolved, state: {} });
820
+ });
821
+ return host;
822
+ }
823
+ const Ctor = components[name];
824
+ if (!Ctor)
825
+ throw new Error(`vmz:dom unknown component <${name} />`);
826
+ const child = createInstance(Ctor, resolved);
827
+ if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
828
+ throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
829
+ }
830
+ const node = runDirectCreate(Ctor, child);
831
+ if (node) {
832
+ child.__vmzDomRoot = node;
833
+ host.appendChild(node);
834
+ }
835
+ host.__vmzInst = child;
836
+ if (typeof child.onMount === 'function') {
837
+ const pending = Promise.resolve().then(() => {
838
+ if (!child.__vmzDestroyed)
839
+ return child.onMount();
840
+ });
841
+ const bag = hostInst.__vmzPendingChildMounts || (hostInst.__vmzPendingChildMounts = []);
842
+ bag.push(pending);
843
+ }
844
+ return host;
845
+ },
846
+ /**
847
+ * Direct if/else — no blueprint `kind: "if"` dispatch.
848
+ * @param {object} inst
849
+ * @param {number|string|null} bindingId
850
+ * @param {string[]} deps
851
+ * @param {Array<{ cond?: () => any, create: (api: typeof directApi) => Node }>} branches
852
+ * @param {number|string|null} [regionId]
853
+ */
854
+ ifBlock(inst, bindingId, deps, branches, regionId = null) {
855
+ noteDomCreate();
856
+ const host = document.createElement('span');
857
+ host.setAttribute('data-vmz-if', '');
858
+ if (regionId != null)
859
+ host.setAttribute('data-vmz-region', String(regionId));
860
+ /** @type {Array<Node | null>} */
861
+ const cached = branches.map(() => null);
862
+ /** @type {Array<Array<{ deps: string[], fn: () => any, bindingId?: number|string|null }>>} */
863
+ const branchBinds = branches.map(() => []);
864
+ let active = -1;
865
+ let gen = 0;
866
+ const pick = () => {
867
+ for (let i = 0; i < branches.length; i++) {
868
+ const b = branches[i];
869
+ if (!b.cond)
870
+ return i;
871
+ try {
872
+ if (b.cond.call(inst))
873
+ return i;
874
+ }
875
+ catch {
876
+ /* continue */
877
+ }
878
+ }
879
+ return -1;
880
+ };
881
+ const wireBranch = (idx) => {
882
+ if (idx < 0)
883
+ return;
884
+ for (const { deps: d, fn, bindingId: bid } of branchBinds[idx]) {
885
+ registerBind(inst, d, fn, bid);
886
+ try {
887
+ runPatch(fn, (d && d[0]) || null, bid ?? null);
888
+ }
889
+ catch (err) {
890
+ console.error('vmz:dom if branch', err);
891
+ }
892
+ }
893
+ };
894
+ const unwireBranch = (idx) => {
895
+ if (idx < 0)
896
+ return;
897
+ for (const { deps: d, fn, bindingId: bid } of branchBinds[idx]) {
898
+ unregisterBind(inst, d, fn, bid);
899
+ }
900
+ };
901
+ const apply = () => {
902
+ if (inst.__vmzDestroyed)
903
+ return;
904
+ const applied = ++gen;
905
+ const next = pick();
906
+ if (next === active)
907
+ return;
908
+ if (next >= 0 && !cached[next]) {
909
+ const binds = [];
910
+ const prevSink = directApi._branchBinds;
911
+ const prevInst = directApi._inst;
912
+ directApi._branchBinds = binds;
913
+ directApi._inst = inst;
914
+ let created = null;
915
+ try {
916
+ created = branches[next].create.call(inst, directApi);
917
+ }
918
+ finally {
919
+ directApi._branchBinds = prevSink;
920
+ directApi._inst = prevInst;
921
+ }
922
+ if (applied !== gen || inst.__vmzDestroyed)
923
+ return;
924
+ if (!cached[next]) {
925
+ cached[next] = created;
926
+ branchBinds[next] = binds;
927
+ }
928
+ }
929
+ if (applied !== gen || inst.__vmzDestroyed)
930
+ return;
931
+ if (active >= 0) {
932
+ unwireBranch(active);
933
+ if (cached[active] && cached[active].parentNode) {
934
+ noteDomRemove();
935
+ cached[active].remove();
936
+ }
937
+ }
938
+ active = next;
939
+ if (next < 0)
940
+ return;
941
+ wireBranch(next);
942
+ if (cached[next])
943
+ host.appendChild(cached[next]);
944
+ };
945
+ registerBind(inst, deps || [], apply, bindingId);
946
+ if (directApi._itemPatches)
947
+ directApi._itemPatches.push(apply);
948
+ // L4: parent destroy disposes all cached branch trees (pause ≠ destroy on switch).
949
+ host.__vmzDispose = () => {
950
+ for (let i = 0; i < cached.length; i++) {
951
+ unwireBranch(i);
952
+ if (cached[i])
953
+ disposeDomTree(cached[i]);
954
+ cached[i] = null;
955
+ }
956
+ active = -1;
957
+ };
958
+ apply();
959
+ return host;
960
+ },
961
+ /**
962
+ * Direct keyed each — no blueprint `kind: "each"` dispatch.
963
+ * P0/P1: Set/Map + Fragment batch insert; item-local binds; host selected; event delegate.
964
+ * @param {object} inst
965
+ * @param {number|string|null} bindingId
966
+ * @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
968
+ * @param {number|string|null} [regionId]
969
+ */
970
+ eachBlock(inst, bindingId, deps, spec, regionId = null) {
971
+ const start = document.createComment(`vmz-each:${spec.as || ''}`);
972
+ const end = document.createComment('/vmz-each');
973
+ if (regionId != null)
974
+ start.__vmzRegion = regionId;
975
+ const frag = document.createDocumentFragment();
976
+ frag.appendChild(start);
977
+ frag.appendChild(end);
978
+ /** @type {Map<any, { box: { item: any, index: number }, dom: Node, patches: Array<() => void> }>} */
979
+ const keyed = new Map();
980
+ let gen = 0;
981
+ /** @type {Map<string, () => void>} */
982
+ const listDispatchers = new Map();
983
+ /** @type {Set<string>} */
984
+ const hostDispatchers = new Set();
985
+ /** @type {Record<string, any>} */
986
+ const hostPrev = Object.create(null);
987
+ /** @type {Set<string>} */
988
+ const delegateTypes = new Set();
989
+ /** @type {Record<string, EventListener>} */
990
+ const delegateListeners = Object.create(null);
991
+ /** @type {Element | null} */
992
+ let delegateRoot = null;
993
+ const itemKey = (box) => {
994
+ if (typeof spec.key === 'function') {
995
+ try {
996
+ return spec.key.call(inst, box);
997
+ }
998
+ catch {
999
+ return box.index;
1000
+ }
1001
+ }
1002
+ return box.index;
1003
+ };
1004
+ const readList = () => {
1005
+ let list = [];
1006
+ try {
1007
+ list = spec.list.call(inst) || [];
1008
+ }
1009
+ catch {
1010
+ list = [];
1011
+ }
1012
+ if (!Array.isArray(list))
1013
+ list = [...list];
1014
+ return list;
1015
+ };
1016
+ const runEntryPatches = (entry, depKey, onlyBindingId) => {
1017
+ if (!entry || !entry.patches)
1018
+ return;
1019
+ for (const p of entry.patches) {
1020
+ if (onlyBindingId != null && p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
1021
+ continue;
1022
+ }
1023
+ try {
1024
+ runPatch(p, depKey, onlyBindingId);
1025
+ }
1026
+ catch (err) {
1027
+ console.error('vmz:dom each item', err);
1028
+ }
1029
+ }
1030
+ };
1031
+ const refreshByListIndex = (onlyBindingId, leafDeps, trie) => {
1032
+ const list = readList();
1033
+ const allowIdx = itemIndicesAllowedForDeps(trie, leafDeps);
1034
+ const runAt = (i) => {
1035
+ if (i < 0 || i >= list.length)
1036
+ return;
1037
+ const box = { item: list[i], index: i };
1038
+ const k = itemKey(box);
1039
+ const entry = keyed.get(k);
1040
+ if (!entry)
1041
+ return;
1042
+ entry.box.item = list[i];
1043
+ entry.box.index = i;
1044
+ tagItemPatches(entry.patches, i);
1045
+ runEntryPatches(entry, (leafDeps && leafDeps[0]) || null, onlyBindingId);
1046
+ };
1047
+ if (allowIdx) {
1048
+ for (const idx of allowIdx)
1049
+ runAt(Number(idx));
1050
+ return;
1051
+ }
1052
+ for (let i = 0; i < list.length; i++)
1053
+ runAt(i);
1054
+ };
1055
+ const refreshHostKeyed = (fields, onlyBindingId) => {
1056
+ for (const field of fields) {
1057
+ const next = inst[field];
1058
+ const prev = hostPrev[field];
1059
+ hostPrev[field] = next;
1060
+ const todo = [];
1061
+ if (prev !== undefined && prev !== null)
1062
+ todo.push(prev);
1063
+ if (next !== undefined && next !== null && next !== prev)
1064
+ todo.push(next);
1065
+ for (const k of todo) {
1066
+ const entry = keyed.get(k);
1067
+ if (!entry)
1068
+ continue;
1069
+ runEntryPatches(entry, field, onlyBindingId);
1070
+ }
1071
+ }
1072
+ };
1073
+ const ensureListDispatcher = (bId, leafDeps) => {
1074
+ if (bId == null)
1075
+ return;
1076
+ const idKey = String(bId);
1077
+ if (listDispatchers.has(idKey))
1078
+ return;
1079
+ const dispatch = () => {
1080
+ if (inst.__vmzDestroyed)
1081
+ return;
1082
+ const trie = inst.__vmzFlushTrie;
1083
+ const hostFields = [];
1084
+ for (const d of leafDeps || []) {
1085
+ if (!d)
1086
+ continue;
1087
+ if (d.includes('.*') || (d.includes('[') && d.includes(']')))
1088
+ continue;
1089
+ hostFields.push(depRootField(d) || d);
1090
+ }
1091
+ const hostDirty = !!trie &&
1092
+ hostFields.some((f) => {
1093
+ const n = trie[f];
1094
+ return n && (n.replace || n.dirty);
1095
+ });
1096
+ if (hostDirty && hostFields.length) {
1097
+ refreshHostKeyed(hostFields, bId);
1098
+ return;
1099
+ }
1100
+ refreshByListIndex(bId, leafDeps, trie);
1101
+ };
1102
+ listDispatchers.set(idKey, dispatch);
1103
+ registerBind(inst, leafDeps || [], dispatch, bId);
1104
+ };
1105
+ const ensureHostDispatcher = (field) => {
1106
+ if (!field || hostDispatchers.has(field))
1107
+ return;
1108
+ hostDispatchers.add(field);
1109
+ const dispatch = () => {
1110
+ if (inst.__vmzDestroyed)
1111
+ return;
1112
+ refreshHostKeyed([field], null);
1113
+ };
1114
+ registerBind(inst, [field], dispatch, null);
1115
+ };
1116
+ const noteItemBind = (bId, bindDeps, fn) => {
1117
+ fn.__vmzItemDeps = Array.isArray(bindDeps) ? bindDeps.slice() : [];
1118
+ fn.__vmzBindingId = bId;
1119
+ const leaf = fn.__vmzItemDeps;
1120
+ if (bId != null) {
1121
+ // One dispatcher per BindingId also covers bare host fields (e.g. selected).
1122
+ ensureListDispatcher(bId, leaf);
1123
+ return;
1124
+ }
1125
+ for (const d of leaf) {
1126
+ if (!d)
1127
+ continue;
1128
+ if (d.includes('.*') || (d.includes('[') && d.includes(']')))
1129
+ continue;
1130
+ const root = depRootField(d) || d;
1131
+ if (root && root.indexOf('.') < 0)
1132
+ ensureHostDispatcher(root);
1133
+ }
1134
+ };
1135
+ const teardownDelegate = () => {
1136
+ if (!delegateRoot)
1137
+ return;
1138
+ for (const type of Object.keys(delegateListeners)) {
1139
+ delegateRoot.removeEventListener(type, delegateListeners[type]);
1140
+ delete delegateListeners[type];
1141
+ }
1142
+ delegateRoot = null;
1143
+ };
1144
+ const ensureDelegateAttached = () => {
1145
+ const parent = end.parentNode;
1146
+ if (!parent || parent.nodeType !== 1)
1147
+ return;
1148
+ if (delegateRoot && delegateRoot !== parent)
1149
+ teardownDelegate();
1150
+ delegateRoot = /** @type {Element} */ (parent);
1151
+ for (const type of delegateTypes) {
1152
+ if (delegateListeners[type])
1153
+ continue;
1154
+ const listener = (ev) => {
1155
+ if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
1156
+ ev.preventDefault();
1157
+ }
1158
+ let n = /** @type {Node | null} */ (ev.target);
1159
+ while (n && n !== delegateRoot) {
1160
+ if (n.nodeType === 1) {
1161
+ const bag = /** @type {Element} */ (n).__vmzEvt;
1162
+ if (bag && typeof bag[type] === 'function') {
1163
+ bag[type].call(inst, ev);
1164
+ return;
1165
+ }
1166
+ }
1167
+ n = n.parentNode;
1168
+ }
1169
+ };
1170
+ delegateListeners[type] = listener;
1171
+ delegateRoot.addEventListener(type, listener);
1172
+ }
1173
+ };
1174
+ const needDelegate = (type) => {
1175
+ if (!type)
1176
+ return;
1177
+ delegateTypes.add(type);
1178
+ ensureDelegateAttached();
1179
+ };
1180
+ const eachCtx = { noteItemBind, needDelegate };
1181
+ const clearDomEvt = (root) => {
1182
+ if (!root || root.nodeType !== 1)
1183
+ return;
1184
+ const walk = (node) => {
1185
+ if (node.nodeType === 1) {
1186
+ if (node.__vmzEvt)
1187
+ node.__vmzEvt = null;
1188
+ for (let c = node.firstChild; c; c = c.nextSibling)
1189
+ walk(c);
1190
+ }
1191
+ };
1192
+ walk(root);
1193
+ };
1194
+ const reconcileDomOrder = (nextNodes) => {
1195
+ const parent = end.parentNode;
1196
+ if (!parent)
1197
+ 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;
1204
+ }
1205
+ n = n.nextSibling;
1206
+ }
1207
+ if (ok && n === end)
1208
+ return;
1209
+ const batch = document.createDocumentFragment();
1210
+ for (const dom of nextNodes) {
1211
+ if (dom.parentNode)
1212
+ noteDomMove();
1213
+ batch.appendChild(dom);
1214
+ }
1215
+ parent.insertBefore(batch, end);
1216
+ };
1217
+ const apply = () => {
1218
+ if (inst.__vmzDestroyed)
1219
+ return;
1220
+ const applied = ++gen;
1221
+ 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)`);
1231
+ }
1232
+ seen.add(k);
1233
+ let entry = keyed.get(k);
1234
+ if (!entry) {
1235
+ const patches = [];
1236
+ const prevPatches = directApi._itemPatches;
1237
+ const prevCtx = directApi._eachCtx;
1238
+ directApi._itemPatches = patches;
1239
+ directApi._eachCtx = eachCtx;
1240
+ let dom = null;
1241
+ try {
1242
+ dom = spec.createItem.call(inst, directApi, box);
1243
+ }
1244
+ finally {
1245
+ directApi._itemPatches = prevPatches;
1246
+ directApi._eachCtx = prevCtx;
1247
+ }
1248
+ if (applied !== gen || inst.__vmzDestroyed)
1249
+ return;
1250
+ tagItemPatches(patches, i);
1251
+ if (dom) {
1252
+ if (dom.nodeType === 1) {
1253
+ dom.setAttribute('data-vmz-key', String(k));
1254
+ }
1255
+ entry = { box, dom, patches };
1256
+ keyed.set(k, entry);
1257
+ }
1258
+ }
1259
+ 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);
1265
+ }
1266
+ if (entry)
1267
+ nextNodes.push(entry.dom);
1268
+ }
1269
+ if (applied !== gen || inst.__vmzDestroyed)
1270
+ return;
1271
+ for (const [k, entry] of [...keyed.entries()]) {
1272
+ if (seen.has(k))
1273
+ continue;
1274
+ noteDomRemove();
1275
+ clearDomEvt(entry.dom);
1276
+ disposeDomTree(entry.dom);
1277
+ if (entry.dom && entry.dom.parentNode)
1278
+ entry.dom.remove();
1279
+ keyed.delete(k);
1280
+ }
1281
+ reconcileDomOrder(nextNodes);
1282
+ // First apply may run while start/end still sit in a DocumentFragment
1283
+ // (before mount appends). Defer until connected so clicks work.
1284
+ if (end.isConnected)
1285
+ ensureDelegateAttached();
1286
+ else
1287
+ queueMicrotask(() => {
1288
+ if (!inst.__vmzDestroyed)
1289
+ ensureDelegateAttached();
1290
+ });
1291
+ };
1292
+ registerBind(inst, deps || [], apply, bindingId);
1293
+ if (directApi._itemPatches)
1294
+ directApi._itemPatches.push(apply);
1295
+ start.__vmzDispose = () => {
1296
+ 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();
1304
+ };
1305
+ const softDeps = [...new Set((deps || []).map((d) => `${depRootField(d)}.*`))];
1306
+ const softRefresh = () => {
1307
+ if (inst.__vmzDestroyed)
1308
+ return;
1309
+ const list = readList();
1310
+ const softKey = softDeps[0] || `${depRootField((deps && deps[0]) || '')}.*`;
1311
+ for (let i = 0; i < list.length; i++) {
1312
+ const box = { item: list[i], index: i };
1313
+ const k = itemKey(box);
1314
+ const entry = keyed.get(k);
1315
+ if (!entry)
1316
+ continue;
1317
+ entry.box.item = list[i];
1318
+ entry.box.index = i;
1319
+ tagItemPatches(entry.patches, i);
1320
+ for (const p of entry.patches) {
1321
+ // Leaf BindingId patches are owned by list/host dispatchers (P1).
1322
+ if (p.__vmzBindingId != null)
1323
+ continue;
1324
+ if (patchHasBindingId(inst, p))
1325
+ continue;
1326
+ try {
1327
+ runPatch(p, softKey, null);
1328
+ }
1329
+ catch (err) {
1330
+ console.error('vmz:dom each soft', err);
1331
+ }
1332
+ }
1333
+ }
1334
+ };
1335
+ registerBind(inst, softDeps, softRefresh, null);
1336
+ apply();
1337
+ return frag;
1338
+ },
1339
+ };
1340
+ /**
1341
+ * @param {object} inst
1342
+ * @param {string[]} deps
1343
+ * @param {() => any} fn
1344
+ * @param {number|string|null|undefined} bindingId
1345
+ */
1346
+ function trackDirectBind(inst, deps, fn, bindingId = null) {
1347
+ if (directApi._branchBinds) {
1348
+ directApi._branchBinds.push({ deps, fn, bindingId });
1349
+ if (directApi._itemPatches)
1350
+ directApi._itemPatches.push(fn);
1351
+ return;
1352
+ }
1353
+ // P1: item binds stay on entry.patches; eachBlock registers one dispatcher per BindingId.
1354
+ if (directApi._itemPatches) {
1355
+ fn.__vmzItemLocal = true;
1356
+ directApi._itemPatches.push(fn);
1357
+ if (directApi._eachCtx) {
1358
+ directApi._eachCtx.noteItemBind(bindingId, deps || [], fn);
1359
+ }
1360
+ return;
1361
+ }
1362
+ registerBind(inst, deps, fn, bindingId);
1363
+ }
1364
+ /**
1365
+ * @param {object} inst
1366
+ * @param {number|string|null} bindingId
1367
+ * @param {string[]} deps
1368
+ * @param {() => any} get
1369
+ * @param {(raw: any) => void} write
1370
+ * @param {{ stable: string[], branches: Array<{ cond?: () => any, deps: string[] }> } | null | undefined} [cf]
1371
+ */
1372
+ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
1373
+ let activeBranch = -1;
1374
+ /** @type {string[]} */
1375
+ let liveDeps = Array.isArray(deps) ? [...deps] : [];
1376
+ const pickCf = () => {
1377
+ if (!cf || !Array.isArray(cf.branches))
1378
+ return -1;
1379
+ for (let i = 0; i < cf.branches.length; i++) {
1380
+ const b = cf.branches[i];
1381
+ if (!b.cond)
1382
+ return i;
1383
+ try {
1384
+ if (b.cond.call(inst))
1385
+ return i;
1386
+ }
1387
+ catch {
1388
+ /* continue */
1389
+ }
1390
+ }
1391
+ return cf.branches.length - 1;
1392
+ };
1393
+ const apply = () => {
1394
+ if (precision.enabled) {
1395
+ precision.bindingEvals++;
1396
+ for (const d of liveDeps || [])
1397
+ bumpMap(precision.bindingEvalsByDep, d);
1398
+ if (bindingId != null) {
1399
+ bumpMap(precision.bindingEvalsByBinding, String(bindingId));
1400
+ }
1401
+ }
1402
+ let raw;
1403
+ try {
1404
+ raw = get.call(inst);
1405
+ }
1406
+ catch {
1407
+ raw = null;
1408
+ }
1409
+ write(raw);
1410
+ if (!cf || !Array.isArray(cf.branches))
1411
+ return;
1412
+ const next = pickCf();
1413
+ if (next === activeBranch)
1414
+ return;
1415
+ activeBranch = next;
1416
+ const branch = cf.branches[next];
1417
+ const nextDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
1418
+ 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
+ unregisterBind(inst, liveDeps, apply, bindingId);
1425
+ liveDeps = uniq;
1426
+ registerBind(inst, liveDeps, apply, bindingId);
1427
+ };
1428
+ if (cf && Array.isArray(cf.branches)) {
1429
+ activeBranch = pickCf();
1430
+ const branch = cf.branches[activeBranch];
1431
+ liveDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
1432
+ liveDeps = [...new Set(liveDeps)];
1433
+ }
1434
+ // Mark before first apply so CF branch switches never hit global registerBind.
1435
+ if (directApi._itemPatches)
1436
+ apply.__vmzItemLocal = true;
1437
+ apply();
1438
+ trackDirectBind(inst, liveDeps, apply, bindingId);
1439
+ }
1440
+ function isEventPropName(name) {
1441
+ return typeof name === 'string' && /^on[A-Z]/.test(name);
1442
+ }
1443
+ function stripFns(obj) {
1444
+ /** @type {Record<string, unknown>} */
1445
+ const out = {};
1446
+ for (const [k, v] of Object.entries(obj || {})) {
1447
+ if (typeof v === 'function')
1448
+ continue;
1449
+ out[k] = v;
1450
+ }
1451
+ return out;
1452
+ }
1453
+ /**
1454
+ * Marker range host for Direct eachBlock (insert before end comment).
1455
+ * @param {Comment} start
1456
+ * @param {Comment} end
1457
+ */
1458
+ function eachHostApi(start, end) {
1459
+ return {
1460
+ insert(dom) {
1461
+ if (dom.parentNode)
1462
+ noteDomMove();
1463
+ end.parentNode.insertBefore(dom, end);
1464
+ },
1465
+ childrenBetween() {
1466
+ const out = [];
1467
+ let n = start.nextSibling;
1468
+ while (n && n !== end) {
1469
+ if (n.nodeType === 1)
1470
+ out.push(n);
1471
+ n = n.nextSibling;
1472
+ }
1473
+ return out;
1474
+ },
1475
+ };
1476
+ }
1477
+ /**
1478
+ * Snapshot plain state/prop field values for Island HMR (N4.3).
1479
+ * @param {object} inst
1480
+ * @returns {Record<string, unknown> | null}
1481
+ */
1482
+ export function snapshotInstanceState(inst) {
1483
+ if (!inst || inst.__vmzDestroyed)
1484
+ return null;
1485
+ const Ctor = inst.constructor;
1486
+ const keys = [...(Ctor.__vmzState || []), ...(Ctor.__vmzProps || [])];
1487
+ /** @type {Record<string, unknown>} */
1488
+ const out = {};
1489
+ for (const key of keys) {
1490
+ if (!key || String(key).startsWith('__'))
1491
+ continue;
1492
+ try {
1493
+ out[key] = inst[key];
1494
+ }
1495
+ catch {
1496
+ /* ignore accessors that throw */
1497
+ }
1498
+ }
1499
+ return out;
1500
+ }
1501
+ /**
1502
+ * @param {object} inst
1503
+ * @param {Record<string, unknown> | null | undefined} state
1504
+ */
1505
+ export function applyPreservedState(inst, state) {
1506
+ if (!inst || !state)
1507
+ return;
1508
+ for (const [key, value] of Object.entries(state)) {
1509
+ try {
1510
+ inst[key] = value;
1511
+ }
1512
+ catch {
1513
+ /* ignore */
1514
+ }
1515
+ }
1516
+ }
1517
+ /**
1518
+ * L5: attach to existing Island DOM without re-running construct structure or onMount.
1519
+ * Consumes ResumeEntry product (`data-vmz-resume`) derived from the same Execution Plan.
1520
+ * @param {new (props?: object) => any} Component
1521
+ * @param {HTMLElement} container
1522
+ * @param {{ props?: object, state?: Record<string, unknown>, strategy?: string } | null} [slice]
1523
+ */
1524
+ export async function resume(Component, container, slice = null) {
1525
+ if (typeof document === 'undefined') {
1526
+ throw new Error('vmz:dom resume() requires a document (browser)');
1527
+ }
1528
+ let parsed = slice;
1529
+ if (!parsed) {
1530
+ const raw = container.getAttribute('data-vmz-resume');
1531
+ if (raw) {
1532
+ try {
1533
+ parsed = JSON.parse(raw);
1534
+ }
1535
+ catch {
1536
+ parsed = null;
1537
+ }
1538
+ }
1539
+ }
1540
+ if (!parsed) {
1541
+ let props = {};
1542
+ try {
1543
+ props = JSON.parse(container.getAttribute('data-vmz-props') || '{}');
1544
+ }
1545
+ catch {
1546
+ props = {};
1547
+ }
1548
+ parsed = { props, state: {} };
1549
+ }
1550
+ if (container.__vmzInst) {
1551
+ destroy(container.__vmzInst);
1552
+ container.__vmzInst = null;
1553
+ }
1554
+ const props = parsed.props || {};
1555
+ const inst = createInstance(Component, props);
1556
+ if (parsed.state)
1557
+ applyPreservedState(inst, parsed.state);
1558
+ // Intentionally never call onMount — SSR already completed that work.
1559
+ if (Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
1560
+ if (!hasMeaningfulChild(container)) {
1561
+ const node = runDirectCreate(Component, inst);
1562
+ if (node) {
1563
+ inst.__vmzDomRoot = node;
1564
+ container.appendChild(node);
1565
+ }
1566
+ }
1567
+ else {
1568
+ // Island leaf adopt: preserve Element identity (L5 nodeIdentity).
1569
+ const node = runDirectResume(Component, inst, container);
1570
+ if (node)
1571
+ inst.__vmzDomRoot = node;
1572
+ }
1573
+ }
1574
+ else {
1575
+ throw new Error(`vmz:dom resume() requires __vmzCreate (Direct); blueprint render() removed (Gate 3)`);
1576
+ }
1577
+ container.__vmzInst = inst;
1578
+ container.__vmzResumed = true;
1579
+ return inst;
1580
+ }
1581
+ /**
1582
+ * Resume all `[data-vmz-island]` hosts (prefer ResumeEntry / EventEntry over mount).
1583
+ * Event strategy islands wait for the DOM event before attach (lazy EventEntry).
1584
+ * @param {ParentNode} [root]
1585
+ */
1586
+ export function resumeIslands(root = globalThis.document) {
1587
+ if (!root || typeof root.querySelectorAll !== 'function') {
1588
+ throw new Error('vmz:dom resumeIslands() requires a DOM root');
1589
+ }
1590
+ const nodes = [...root.querySelectorAll('[data-vmz-island]')];
1591
+ for (const el of nodes) {
1592
+ const name = el.getAttribute('data-vmz-island');
1593
+ const strategy = el.getAttribute('data-vmz-client') || 'load';
1594
+ scheduleClientOn(el, strategy, async () => {
1595
+ const Ctor = await resolveComponent(name);
1596
+ if (!Ctor) {
1597
+ console.error(`vmz:dom resume: unknown component ${name}`);
1598
+ return;
1599
+ }
1600
+ await resume(Ctor, el);
1601
+ });
1602
+ }
1603
+ }
1604
+ /**
1605
+ * EventEntry attach: only wire `client:event` / `client:event:*` islands.
1606
+ * Idle/load/visible ResumeEntries are left alone (static shell can defer framework work).
1607
+ * @param {ParentNode} [root]
1608
+ */
1609
+ export function attachEventEntries(root = globalThis.document) {
1610
+ if (!root || typeof root.querySelectorAll !== 'function') {
1611
+ throw new Error('vmz:dom attachEventEntries() requires a DOM root');
1612
+ }
1613
+ const nodes = [...root.querySelectorAll('[data-vmz-island]')];
1614
+ for (const el of nodes) {
1615
+ const strategy = el.getAttribute('data-vmz-client') || '';
1616
+ if (!isEventEntryStrategy(strategy))
1617
+ continue;
1618
+ const name = el.getAttribute('data-vmz-island');
1619
+ el.setAttribute('data-vmz-entry', 'event');
1620
+ scheduleClientOn(el, strategy, async () => {
1621
+ const Ctor = await resolveComponent(name);
1622
+ if (!Ctor) {
1623
+ console.error(`vmz:dom EventEntry: unknown component ${name}`);
1624
+ return;
1625
+ }
1626
+ await resume(Ctor, el);
1627
+ });
1628
+ }
1629
+ }
1630
+ /**
1631
+ * Adopt existing Island DOM while running the same `__vmzCreate` schedule (L5).
1632
+ * @param {new (props?: object) => any} Component
1633
+ * @param {object} inst
1634
+ * @param {Element} container
1635
+ */
1636
+ function runDirectResume(Component, inst, container) {
1637
+ const rootEl = [...container.childNodes].find((n) => n.nodeType === 1 || (n.nodeType === 3 && String(n.textContent).trim() !== ''));
1638
+ if (!rootEl || rootEl.nodeType !== 1) {
1639
+ return runDirectCreate(Component, inst);
1640
+ }
1641
+ let textI = 0;
1642
+ const api = {
1643
+ _inst: inst,
1644
+ _branchBinds: null,
1645
+ _itemPatches: null,
1646
+ el(tag) {
1647
+ if (String(rootEl.tagName).toLowerCase() !== String(tag).toLowerCase()) {
1648
+ noteDomCreate();
1649
+ return document.createElement(tag);
1650
+ }
1651
+ return rootEl;
1652
+ },
1653
+ frag() {
1654
+ return document.createDocumentFragment();
1655
+ },
1656
+ text(s) {
1657
+ while (textI < rootEl.childNodes.length) {
1658
+ const n = rootEl.childNodes[textI++];
1659
+ if (n.nodeType === 3) {
1660
+ if (s != null && s !== '')
1661
+ n.textContent = String(s);
1662
+ return n;
1663
+ }
1664
+ }
1665
+ noteDomCreate();
1666
+ return document.createTextNode(String(s ?? ''));
1667
+ },
1668
+ 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));
1675
+ },
1676
+ on(el, type, handler) {
1677
+ el.addEventListener(type, handler);
1678
+ },
1679
+ bindText: directApi.bindText,
1680
+ bindAttr: directApi.bindAttr,
1681
+ setHtml: directApi.setHtml,
1682
+ bindHtml: directApi.bindHtml,
1683
+ ifBlock: directApi.ifBlock,
1684
+ eachBlock: directApi.eachBlock,
1685
+ component: directApi.component,
1686
+ };
1687
+ return Component.__vmzCreate.call(inst, api);
1688
+ }
1689
+ /**
1690
+ * @param {new (props?: object) => any} Component
1691
+ * @param {HTMLElement} container
1692
+ * @param {object} [props]
1693
+ * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
1694
+ */
1695
+ export async function hydrate(Component, container, props = {}, opts = {}) {
1696
+ if (typeof document === 'undefined') {
1697
+ throw new Error('vmz:dom hydrate() requires a document (browser)');
1698
+ }
1699
+ /** @type {Record<string, unknown> | null} */
1700
+ let preserved = null;
1701
+ if (opts.preserveState && typeof opts.preserveState === 'object') {
1702
+ preserved = opts.preserveState;
1703
+ }
1704
+ else if (opts.preserveState === true && container.__vmzInst) {
1705
+ preserved = snapshotInstanceState(container.__vmzInst);
1706
+ }
1707
+ if (container.__vmzInst) {
1708
+ destroy(container.__vmzInst);
1709
+ container.__vmzInst = null;
1710
+ }
1711
+ const inst = createInstance(Component, props);
1712
+ if (preserved) {
1713
+ applyPreservedState(inst, preserved);
1714
+ }
1715
+ // Gate 3: hydrate uses the same Direct schedule as resume (no render()).
1716
+ if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
1717
+ throw new Error(`vmz:dom hydrate() requires __vmzCreate (Direct); blueprint render() removed (Gate 3)`);
1718
+ }
1719
+ // Wire DOM + events BEFORE awaiting onMount. SSR shell is already visible; if we
1720
+ // wait on RPC/bootstrap first, buttons look real but have no listeners (dead UI).
1721
+ // onMount may still patch state / redirect afterwards (same end state as SSR order).
1722
+ if (!hasMeaningfulChild(container)) {
1723
+ const node = runDirectCreate(Component, inst);
1724
+ if (node) {
1725
+ inst.__vmzDomRoot = node;
1726
+ container.appendChild(node);
1727
+ }
1728
+ }
1729
+ else {
1730
+ // Interim: shallow resume leaves if/each binders unbound. Recreate against live
1731
+ // DOM so patches attach. Leaf nodeIdentity (same Element) remains TODO for deep adopt.
1732
+ container.replaceChildren();
1733
+ const node = runDirectCreate(Component, inst);
1734
+ if (node) {
1735
+ inst.__vmzDomRoot = node;
1736
+ container.appendChild(node);
1737
+ }
1738
+ }
1739
+ await settlePendingChildMounts(inst);
1740
+ container.__vmzInst = inst;
1741
+ const runMount = opts.skipOnMount !== true && !preserved && typeof inst.onMount === 'function';
1742
+ if (runMount) {
1743
+ await inst.onMount();
1744
+ }
1745
+ return inst;
1746
+ }
1747
+ /**
1748
+ * Tear down binders and stop patches. Safe to call more than once.
1749
+ * Field writes after destroy no longer update DOM (values may still change).
1750
+ * L4: also dispose owned DOM trees (child __vmzInst / region __vmzDispose).
1751
+ * @param {object} inst
1752
+ */
1753
+ export function destroy(inst) {
1754
+ if (!inst || inst.__vmzDestroyed)
1755
+ return;
1756
+ inst.__vmzDestroyed = true;
1757
+ inst.__vmzFlushScheduled = false;
1758
+ // L4 async cancel: abort in-flight tasks before tearing down DOM.
1759
+ __vmzCancelTasks(inst);
1760
+ if (inst.__vmzDomRoot) {
1761
+ disposeDomTree(inst.__vmzDomRoot);
1762
+ inst.__vmzDomRoot = null;
1763
+ }
1764
+ if (inst.__vmzDirtyNotices)
1765
+ inst.__vmzDirtyNotices.length = 0;
1766
+ if (inst.__vmzDirtyTrie)
1767
+ inst.__vmzDirtyTrie = Object.create(null);
1768
+ if (inst.__vmzDirty)
1769
+ inst.__vmzDirty.clear();
1770
+ inst.__vmzBinders = Object.create(null);
1771
+ inst.__vmzBindings = Object.create(null);
1772
+ inst.__vmzDepToBindings = Object.create(null);
1773
+ if (typeof inst.onDestroy === 'function') {
1774
+ try {
1775
+ inst.onDestroy();
1776
+ }
1777
+ catch (err) {
1778
+ console.error('vmz:dom onDestroy', err);
1779
+ }
1780
+ }
1781
+ }
1782
+ /**
1783
+ * L4: walk a DOM subtree and run lifetime dispose hooks + nested instance destroy.
1784
+ * Does not mark the *calling* parent destroyed; safe from destroy(inst).
1785
+ * @param {Node | null | undefined} root
1786
+ */
1787
+ export function disposeDomTree(root) {
1788
+ if (!root)
1789
+ return;
1790
+ const seen = new Set();
1791
+ const visit = (node) => {
1792
+ if (!node || seen.has(node))
1793
+ return;
1794
+ seen.add(node);
1795
+ if (typeof node.__vmzDispose === 'function') {
1796
+ try {
1797
+ node.__vmzDispose();
1798
+ }
1799
+ catch (err) {
1800
+ console.error('vmz:dom __vmzDispose', err);
1801
+ }
1802
+ node.__vmzDispose = null;
1803
+ }
1804
+ if (node.__vmzInst) {
1805
+ const child = node.__vmzInst;
1806
+ node.__vmzInst = null;
1807
+ destroy(child);
1808
+ }
1809
+ let child = node.firstChild;
1810
+ while (child) {
1811
+ const next = child.nextSibling;
1812
+ visit(child);
1813
+ child = next;
1814
+ }
1815
+ };
1816
+ visit(root);
1817
+ }
1818
+ /**
1819
+ * @param {ParentNode} [root]
1820
+ */
1821
+ export function hydrateIslands(root = globalThis.document) {
1822
+ // L5: hydrateIslands is an alias for resumeIslands (same Plan attach).
1823
+ return resumeIslands(root);
1824
+ }
1825
+ export function scheduleClient(strategy, fn) {
1826
+ scheduleClientOn(null, strategy, fn);
1827
+ }
1828
+ /** @param {string} strategy */
1829
+ function isEventEntryStrategy(strategy) {
1830
+ const s = String(strategy || '');
1831
+ return s === 'event' || s.startsWith('event:') || s === 'click';
1832
+ }
1833
+ /** @param {string} strategy */
1834
+ function eventEntryType(strategy) {
1835
+ const s = String(strategy || 'event');
1836
+ if (s.startsWith('event:'))
1837
+ return s.slice(6) || 'click';
1838
+ if (s === 'click')
1839
+ return 'click';
1840
+ return 'click';
1841
+ }
1842
+ function scheduleClientOn(el, strategy, fn) {
1843
+ const run = () => {
1844
+ Promise.resolve(fn()).catch((err) => console.error('vmz:dom island', err));
1845
+ };
1846
+ if (isEventEntryStrategy(strategy)) {
1847
+ if (!el || typeof el.addEventListener !== 'function') {
1848
+ run();
1849
+ return;
1850
+ }
1851
+ const type = eventEntryType(strategy);
1852
+ const once = () => {
1853
+ el.removeEventListener(type, once);
1854
+ run();
1855
+ };
1856
+ el.addEventListener(type, once);
1857
+ return;
1858
+ }
1859
+ if (strategy === 'idle') {
1860
+ if (typeof requestIdleCallback === 'function') {
1861
+ requestIdleCallback(() => run(), { timeout: 2000 });
1862
+ }
1863
+ else {
1864
+ setTimeout(run, 1);
1865
+ }
1866
+ return;
1867
+ }
1868
+ if (strategy === 'visible' && el && typeof IntersectionObserver === 'function') {
1869
+ const io = new IntersectionObserver((entries) => {
1870
+ if (entries.some((e) => e.isIntersecting)) {
1871
+ io.disconnect();
1872
+ run();
1873
+ }
1874
+ });
1875
+ io.observe(el);
1876
+ return;
1877
+ }
1878
+ run();
1879
+ }
1880
+ /**
1881
+ * AsyncTask cancel protocol (first slice): keyed generation + AbortSignal.
1882
+ * Superseded runs and `destroy(inst)` abort prior work; stale results must not apply.
1883
+ * @param {object} inst
1884
+ * @param {string} key
1885
+ * @param {(signal: AbortSignal, meta: { generation: number }) => any | Promise<any>} fn
1886
+ * @returns {Promise<any>}
1887
+ */
1888
+ export function __vmzRunTask(inst, key, fn) {
1889
+ if (!inst)
1890
+ throw new Error('vmz:dom __vmzRunTask requires inst');
1891
+ const k = String(key || 'default');
1892
+ if (!inst.__vmzTasks)
1893
+ inst.__vmzTasks = Object.create(null);
1894
+ const prev = inst.__vmzTasks[k];
1895
+ if (prev) {
1896
+ prev.generation += 1;
1897
+ try {
1898
+ prev.controller.abort();
1899
+ }
1900
+ catch {
1901
+ /* ignore */
1902
+ }
1903
+ prev.status = 'cancelled';
1904
+ }
1905
+ const controller = typeof AbortController !== 'undefined'
1906
+ ? new AbortController()
1907
+ : {
1908
+ signal: { aborted: false },
1909
+ abort() {
1910
+ this.signal.aborted = true;
1911
+ },
1912
+ };
1913
+ const generation = (prev?.generation || 0) + 1;
1914
+ /** @type {{ generation: number, controller: any, status: string, result?: any, error?: any, promise?: Promise<any> }} */
1915
+ const entry = {
1916
+ generation,
1917
+ controller,
1918
+ status: 'pending',
1919
+ };
1920
+ inst.__vmzTasks[k] = entry;
1921
+ // Invoke synchronously so event handlers can call preventDefault() before
1922
+ // the browser continues the default action (form submit → native navigation).
1923
+ // Async work still continues via the returned Promise.
1924
+ let syncResult;
1925
+ let syncErr;
1926
+ let threw = false;
1927
+ try {
1928
+ syncResult = fn(controller.signal, { generation });
1929
+ }
1930
+ catch (err) {
1931
+ threw = true;
1932
+ syncErr = err;
1933
+ }
1934
+ const settleOk = (result) => {
1935
+ if (inst.__vmzDestroyed || controller.signal.aborted || inst.__vmzTasks[k] !== entry) {
1936
+ entry.status = 'cancelled';
1937
+ return undefined;
1938
+ }
1939
+ entry.status = 'success';
1940
+ entry.result = result;
1941
+ return result;
1942
+ };
1943
+ const settleErr = (err) => {
1944
+ if (inst.__vmzDestroyed || controller.signal.aborted || inst.__vmzTasks[k] !== entry) {
1945
+ entry.status = 'cancelled';
1946
+ return undefined;
1947
+ }
1948
+ entry.status = 'error';
1949
+ entry.error = err;
1950
+ throw err;
1951
+ };
1952
+ if (threw) {
1953
+ const promise = Promise.resolve().then(() => settleErr(syncErr));
1954
+ entry.promise = promise;
1955
+ return promise;
1956
+ }
1957
+ const promise = Promise.resolve(syncResult).then(settleOk, settleErr);
1958
+ entry.promise = promise;
1959
+ return promise;
1960
+ }
1961
+ /** Abort all keyed tasks on an instance (also called from destroy). */
1962
+ export function __vmzCancelTasks(inst) {
1963
+ const tasks = inst?.__vmzTasks;
1964
+ if (!tasks)
1965
+ return;
1966
+ for (const key of Object.keys(tasks)) {
1967
+ const t = tasks[key];
1968
+ t.generation += 1;
1969
+ try {
1970
+ t.controller.abort();
1971
+ }
1972
+ catch {
1973
+ /* ignore */
1974
+ }
1975
+ t.status = 'cancelled';
1976
+ }
1977
+ }
1978
+ /** @returns {'pending'|'success'|'error'|'cancelled'|null} */
1979
+ export function __vmzTaskStatus(inst, key) {
1980
+ const t = inst?.__vmzTasks?.[String(key || 'default')];
1981
+ return t ? t.status : null;
1982
+ }
1983
+ function createInstance(Component, props = {}) {
1984
+ if (precision.enabled)
1985
+ precision.componentExecs++;
1986
+ const inst = new Component(props || {});
1987
+ if (typeof inst.__vmzApplyProps === 'function' && !Component.__vmzCtorAppliesProps) {
1988
+ inst.__vmzApplyProps(props || {});
1989
+ }
1990
+ inst.__vmzBinders = Object.create(null);
1991
+ inst.__vmzBindings = Object.create(null);
1992
+ inst.__vmzDepToBindings = Object.create(null);
1993
+ makeReactive(inst, Component.__vmzState || []);
1994
+ makeReactive(inst, Component.__vmzProps || []);
1995
+ // L4 WriteBarrier: path / array writes call Component helpers (no import needed).
1996
+ Component.__vmzWritePath = __vmzWritePath;
1997
+ Component.__vmzWritePathLogical = __vmzWritePathLogical;
1998
+ Component.__vmzReadPath = __vmzReadPath;
1999
+ Component.__vmzArrayMutate = __vmzArrayMutate;
2000
+ Component.__vmzAllowShared = __vmzAllowShared;
2001
+ Component.__vmzTakeShared = __vmzTakeShared;
2002
+ return inst;
2003
+ }
2004
+ /** Shared plain-object owners under WriteBarrier (no Proxy). */
2005
+ const wbSharedOwners = new WeakMap();
2006
+ /** Objects explicitly marked OK to share across component instances (13 §7.3). */
2007
+ const wbAllowShared = new WeakSet();
2008
+ /** @type {Array<{ kind: string, message: string }>} */
2009
+ const wbCrossComponentDiags = [];
2010
+ /**
2011
+ * Mark a plain object as intentionally shared across ownership boundaries.
2012
+ * Suppresses cross-component shared diagnostics (规划设计/vmz/13 §7.3).
2013
+ * @param {any} value
2014
+ */
2015
+ export function __vmzAllowShared(value) {
2016
+ if (value != null && typeof value === 'object')
2017
+ wbAllowShared.add(value);
2018
+ return value;
2019
+ }
2020
+ /**
2021
+ * Take exclusive ownership intent: clear multi-owner registry for this object.
2022
+ * Subsequent field assigns re-register from the assigning instance only.
2023
+ * @param {any} value
2024
+ */
2025
+ export function __vmzTakeShared(value) {
2026
+ if (value != null && typeof value === 'object') {
2027
+ wbSharedOwners.delete(value);
2028
+ wbAllowShared.delete(value);
2029
+ }
2030
+ return value;
2031
+ }
2032
+ /**
2033
+ * @returns {Array<{ kind: string, message: string }>}
2034
+ */
2035
+ export function __vmzSharedCrossComponentDiagnostics() {
2036
+ return wbCrossComponentDiags.slice();
2037
+ }
2038
+ export function __vmzSharedCrossComponentDiagnosticsReset() {
2039
+ wbCrossComponentDiags.length = 0;
2040
+ }
2041
+ /**
2042
+ * @param {any} value
2043
+ * @param {(segs: string[] | null) => void} report
2044
+ * @param {string[]} baseSegs
2045
+ * @param {any} [inst]
2046
+ */
2047
+ function registerWbOwner(value, report, baseSegs = [], inst = null) {
2048
+ if (value == null || typeof value !== 'object')
2049
+ return;
2050
+ let entry = wbSharedOwners.get(value);
2051
+ if (!entry) {
2052
+ entry = { owners: [] };
2053
+ wbSharedOwners.set(value, entry);
2054
+ }
2055
+ if (entry.owners.some((o) => o.report === report && sameSegs(o.baseSegs, baseSegs))) {
2056
+ return;
2057
+ }
2058
+ entry.owners.push({ report, baseSegs: baseSegs.slice(), inst });
2059
+ // Cross-component share without explicit allow → diagnose (13 §7.3).
2060
+ if (!wbAllowShared.has(value) && inst) {
2061
+ const other = entry.owners.find((o) => o.inst && o.inst !== inst);
2062
+ if (other) {
2063
+ const msg = 'vmz: plain object shared across component instances without __vmzAllowShared (规划设计/vmz/13 §7.3)';
2064
+ if (!wbCrossComponentDiags.some((d) => d.message === msg)) {
2065
+ wbCrossComponentDiags.push({ kind: 'shared_cross_component', message: msg });
2066
+ }
2067
+ }
2068
+ }
2069
+ }
2070
+ /**
2071
+ * Notify all registered owners of a shared plain object after a barrier write.
2072
+ * @param {any} rootObj field-root value that was written under
2073
+ * @param {string[] | null} localSegs path under that object (null = replace)
2074
+ * @returns {boolean} true when at least one owner was notified
2075
+ */
2076
+ function notifyWbShared(rootObj, localSegs) {
2077
+ const entry = rootObj && typeof rootObj === 'object' ? wbSharedOwners.get(rootObj) : null;
2078
+ if (!entry || !entry.owners.length)
2079
+ return false;
2080
+ for (const o of entry.owners) {
2081
+ if (localSegs == null) {
2082
+ o.report(o.baseSegs.length ? o.baseSegs.slice() : null);
2083
+ }
2084
+ else {
2085
+ o.report([...o.baseSegs, ...localSegs]);
2086
+ }
2087
+ }
2088
+ return true;
2089
+ }
2090
+ /**
2091
+ * Read a nested path under a field root (for compound / update expansion).
2092
+ * @param {any} inst
2093
+ * @param {string} root
2094
+ * @param {string[]} segs
2095
+ */
2096
+ export function __vmzReadPath(inst, root, segs) {
2097
+ if (!inst || !root)
2098
+ return undefined;
2099
+ let obj = inst[root];
2100
+ if (!Array.isArray(segs) || segs.length === 0)
2101
+ return obj;
2102
+ for (let i = 0; i < segs.length; i++) {
2103
+ if (obj == null || typeof obj !== 'object')
2104
+ return undefined;
2105
+ obj = obj[segs[i]];
2106
+ }
2107
+ return obj;
2108
+ }
2109
+ /**
2110
+ * Short-circuit logical path assign (`||=` / `&&=` / `??=`).
2111
+ * @param {any} inst
2112
+ * @param {string} root
2113
+ * @param {string[]} segs
2114
+ * @param {'||'|'&&'|'??'} kind
2115
+ * @param {any} rhs
2116
+ */
2117
+ export function __vmzWritePathLogical(inst, root, segs, kind, rhs) {
2118
+ const cur = __vmzReadPath(inst, root, segs);
2119
+ if (kind === '||') {
2120
+ if (cur)
2121
+ return cur;
2122
+ }
2123
+ else if (kind === '&&') {
2124
+ if (!cur)
2125
+ return cur;
2126
+ }
2127
+ else if (kind === '??') {
2128
+ if (cur != null)
2129
+ return cur;
2130
+ }
2131
+ else {
2132
+ return cur;
2133
+ }
2134
+ return __vmzWritePath(inst, root, segs, rhs);
2135
+ }
2136
+ /**
2137
+ * Compiler-inserted path write barrier (规划设计/vmz/13 §7.3).
2138
+ * Mutates a plain owned object/array and schedules the same path notice Proxy would.
2139
+ *
2140
+ * Root-array index assigns (`tags[0] = x`) notify as field replace (structural),
2141
+ * matching the transitional Proxy wrapArray behavior.
2142
+ * Shared multi-owner: writing through one field notifies all owners of the same raw object.
2143
+ *
2144
+ * @param {any} inst
2145
+ * @param {string} root field root
2146
+ * @param {string[]} segs path under root (non-empty); dynamic indices already String(...)'d
2147
+ * @param {any} value
2148
+ */
2149
+ export function __vmzWritePath(inst, root, segs, value) {
2150
+ if (!inst || inst.__vmzDestroyed)
2151
+ return value;
2152
+ if (!root || !Array.isArray(segs) || segs.length === 0)
2153
+ return value;
2154
+ const normSegs = segs.map((s) => String(s));
2155
+ let obj = inst[root];
2156
+ if (obj == null || typeof obj !== 'object')
2157
+ return value;
2158
+ for (let i = 0; i < normSegs.length - 1; i++) {
2159
+ obj = obj[normSegs[i]];
2160
+ if (obj == null || typeof obj !== 'object')
2161
+ return value;
2162
+ }
2163
+ const leaf = normSegs[normSegs.length - 1];
2164
+ if (Object.is(obj[leaf], value))
2165
+ return value;
2166
+ obj[leaf] = value;
2167
+ // Register newly assigned nested objects under this field for future shared writes.
2168
+ if (value != null && typeof value === 'object') {
2169
+ const report = (local) => {
2170
+ if (!local || local.length === 0) {
2171
+ scheduleRefresh(inst, { type: 'replace', root });
2172
+ }
2173
+ else {
2174
+ scheduleRefresh(inst, { type: 'path', root, segs: local });
2175
+ }
2176
+ };
2177
+ registerWbOwner(value, report, normSegs.slice(), inst);
2178
+ }
2179
+ const rootObj = inst[root];
2180
+ const rootArr = rootObj;
2181
+ const isRootIndex = normSegs.length === 1 && Array.isArray(rootArr) && leaf !== 'length' && String(Number(leaf)) === leaf;
2182
+ if (isRootIndex) {
2183
+ if (!notifyWbShared(rootObj, null)) {
2184
+ scheduleRefresh(inst, { type: 'replace', root });
2185
+ }
2186
+ }
2187
+ else if (!notifyWbShared(rootObj, normSegs)) {
2188
+ scheduleRefresh(inst, { type: 'path', root, segs: normSegs.slice() });
2189
+ }
2190
+ return value;
2191
+ }
2192
+ /**
2193
+ * Compiler-inserted array mutator barrier (push/pop/splice/…).
2194
+ * Applies the mutator on the plain array and schedules a structural notice
2195
+ * at `root` + `baseSegs` (empty baseSegs → field replace).
2196
+ *
2197
+ * @param {any} inst
2198
+ * @param {string} root
2199
+ * @param {string[]} baseSegs
2200
+ * @param {string} method
2201
+ * @param {any[]} args
2202
+ */
2203
+ export function __vmzArrayMutate(inst, root, baseSegs, method, args) {
2204
+ if (!inst || inst.__vmzDestroyed)
2205
+ return undefined;
2206
+ if (!root || typeof method !== 'string')
2207
+ return undefined;
2208
+ const segs = Array.isArray(baseSegs) ? baseSegs.map((s) => String(s)) : [];
2209
+ let arr = inst[root];
2210
+ if (arr == null || typeof arr !== 'object')
2211
+ return undefined;
2212
+ for (let i = 0; i < segs.length; i++) {
2213
+ arr = arr[segs[i]];
2214
+ if (arr == null || typeof arr !== 'object')
2215
+ return undefined;
2216
+ }
2217
+ if (!Array.isArray(arr) || typeof arr[method] !== 'function')
2218
+ return undefined;
2219
+ const list = Array.isArray(args) ? args : [];
2220
+ const ret = arr[method](...list);
2221
+ const rootObj = inst[root];
2222
+ if (segs.length === 0) {
2223
+ if (!notifyWbShared(rootObj, null)) {
2224
+ scheduleRefresh(inst, { type: 'replace', root });
2225
+ }
2226
+ }
2227
+ else if (!notifyWbShared(rootObj, segs)) {
2228
+ scheduleRefresh(inst, { type: 'path', root, segs: segs.slice() });
2229
+ }
2230
+ return ret;
2231
+ }
2232
+ function makeReactive(inst, stateKeys) {
2233
+ const barrier = !!inst.constructor.__vmzWriteBarrier;
2234
+ for (const key of stateKeys) {
2235
+ if (!key || key.startsWith('#'))
2236
+ continue;
2237
+ const desc = Object.getOwnPropertyDescriptor(inst, key);
2238
+ if (desc && desc.set && desc.get && !desc.writable)
2239
+ continue;
2240
+ /** @param {string[] | null} segs null/empty → replace field */
2241
+ const report = (segs) => {
2242
+ if (!segs || segs.length === 0) {
2243
+ scheduleRefresh(inst, { type: 'replace', root: key });
2244
+ }
2245
+ else {
2246
+ scheduleRefresh(inst, { type: 'path', root: key, segs });
2247
+ }
2248
+ };
2249
+ // WriteBarrier components keep plain objects — nested writes go through __vmzWritePath.
2250
+ let value = barrier ? inst[key] : wrapReactive(inst[key], report, []);
2251
+ if (barrier)
2252
+ registerWbOwner(value, report, [], inst);
2253
+ Object.defineProperty(inst, key, {
2254
+ configurable: true,
2255
+ enumerable: true,
2256
+ get() {
2257
+ return value;
2258
+ },
2259
+ set(next) {
2260
+ const wrapped = barrier ? next : wrapReactive(next, report, []);
2261
+ if (Object.is(value, wrapped))
2262
+ return;
2263
+ value = wrapped;
2264
+ if (barrier)
2265
+ registerWbOwner(value, report, [], inst);
2266
+ report(null);
2267
+ },
2268
+ });
2269
+ }
2270
+ }
2271
+ /** Targets already wrapped: raw|proxy|barrier → { proxy, owners[], kind }. */
2272
+ const reactiveProxies = new WeakMap();
2273
+ /** Plain objects using defineProperty write barriers (not Proxy). */
2274
+ const writeBarrierOwned = new WeakSet();
2275
+ /**
2276
+ * L4 WriteBarrier: true when value is an owned plain object with path barriers (no Proxy).
2277
+ * @param {any} value
2278
+ */
2279
+ export function __vmzIsWriteBarrierOwned(value) {
2280
+ return writeBarrierOwned.has(value);
2281
+ }
2282
+ /**
2283
+ * True when value is the Proxy wrapper from array (or residual) reactive wrap.
2284
+ * @param {any} value
2285
+ */
2286
+ export function __vmzIsReactiveProxy(value) {
2287
+ const e = reactiveProxies.get(value);
2288
+ return !!(e && e.kind === 'proxy' && e.proxy === value);
2289
+ }
2290
+ const ARRAY_MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin']);
2291
+ /**
2292
+ * @typedef {{ report: (segs: string[] | null) => void, baseSegs: string[] }} ReactiveOwner
2293
+ * @typedef {{ proxy: object, owners: ReactiveOwner[], kind: 'barrier'|'proxy' }} ReactiveEntry
2294
+ */
2295
+ function sameSegs(a, b) {
2296
+ if (a.length !== b.length)
2297
+ return false;
2298
+ for (let i = 0; i < a.length; i++) {
2299
+ if (a[i] !== b[i])
2300
+ return false;
2301
+ }
2302
+ return true;
2303
+ }
2304
+ /**
2305
+ * @param {ReactiveEntry} entry
2306
+ * @param {(segs: string[] | null) => void} report
2307
+ * @param {string[]} baseSegs
2308
+ */
2309
+ function addOwner(entry, report, baseSegs) {
2310
+ if (entry.owners.some((o) => o.report === report && sameSegs(o.baseSegs, baseSegs))) {
2311
+ return;
2312
+ }
2313
+ entry.owners.push({
2314
+ report,
2315
+ baseSegs: baseSegs.slice(),
2316
+ });
2317
+ }
2318
+ /**
2319
+ * @param {ReactiveOwner[]} owners
2320
+ * @param {string[] | null} localSegs null = structural replace of this node
2321
+ */
2322
+ function notifyOwners(owners, localSegs) {
2323
+ for (const o of owners) {
2324
+ if (localSegs == null) {
2325
+ o.report(o.baseSegs.length ? o.baseSegs.slice() : null);
2326
+ }
2327
+ else {
2328
+ o.report([...o.baseSegs, ...localSegs]);
2329
+ }
2330
+ }
2331
+ }
2332
+ /**
2333
+ * 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.
2336
+ * Shared raw objects notify **all** current owners.
2337
+ *
2338
+ * @param {any} value
2339
+ * @param {(segs: string[] | null) => void} report
2340
+ * @param {string[]} pathSegs path under the field root to this value
2341
+ */
2342
+ function wrapReactive(value, report, pathSegs = []) {
2343
+ if (value == null || typeof value !== 'object')
2344
+ return value;
2345
+ const existing = reactiveProxies.get(value);
2346
+ if (existing) {
2347
+ addOwner(existing, report, pathSegs);
2348
+ return existing.proxy;
2349
+ }
2350
+ if (Array.isArray(value))
2351
+ return wrapArray(value, report, pathSegs);
2352
+ if (isPlainObject(value))
2353
+ return wrapOwnedObject(value, report, pathSegs);
2354
+ return value;
2355
+ }
2356
+ function isPlainObject(value) {
2357
+ const proto = Object.getPrototypeOf(value);
2358
+ return proto === Object.prototype || proto === null;
2359
+ }
2360
+ /**
2361
+ * Path-level write barrier for owned plain objects (no Proxy).
2362
+ */
2363
+ function wrapOwnedObject(obj, report, pathSegs) {
2364
+ const existing = reactiveProxies.get(obj);
2365
+ if (existing) {
2366
+ addOwner(existing, report, pathSegs);
2367
+ return existing.proxy;
2368
+ }
2369
+ /** @type {ReactiveEntry} */
2370
+ const entry = {
2371
+ proxy: obj,
2372
+ owners: [],
2373
+ kind: 'barrier',
2374
+ };
2375
+ addOwner(entry, report, pathSegs);
2376
+ writeBarrierOwned.add(obj);
2377
+ reactiveProxies.set(obj, entry);
2378
+ for (const prop of Object.keys(obj)) {
2379
+ installOwnedProp(obj, prop, entry);
2380
+ }
2381
+ return obj;
2382
+ }
2383
+ /**
2384
+ * @param {object} obj
2385
+ * @param {string} prop
2386
+ * @param {ReactiveEntry} entry
2387
+ */
2388
+ function installOwnedProp(obj, prop, entry) {
2389
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
2390
+ if (!desc || !desc.configurable)
2391
+ return;
2392
+ if (desc.get || desc.set)
2393
+ return;
2394
+ let current = obj[prop];
2395
+ for (const o of entry.owners) {
2396
+ current = wrapReactive(current, o.report, [...o.baseSegs, prop]);
2397
+ }
2398
+ Object.defineProperty(obj, prop, {
2399
+ configurable: true,
2400
+ enumerable: desc.enumerable !== false,
2401
+ get() {
2402
+ return current;
2403
+ },
2404
+ set(next) {
2405
+ const local = [prop];
2406
+ let wrapped = next;
2407
+ for (const o of entry.owners) {
2408
+ wrapped = wrapReactive(next, o.report, [...o.baseSegs, ...local]);
2409
+ }
2410
+ if (Object.is(current, wrapped))
2411
+ return;
2412
+ current = wrapped;
2413
+ notifyOwners(entry.owners, local);
2414
+ },
2415
+ });
2416
+ }
2417
+ function wrapArray(arr, report, pathSegs) {
2418
+ const existing = reactiveProxies.get(arr);
2419
+ if (existing) {
2420
+ addOwner(existing, report, pathSegs);
2421
+ return existing.proxy;
2422
+ }
2423
+ /** @type {ReactiveEntry} */
2424
+ const entry = {
2425
+ proxy: null,
2426
+ owners: [],
2427
+ kind: 'proxy',
2428
+ };
2429
+ 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
+ }
2436
+ const proxy = new Proxy(arr, {
2437
+ get(target, prop, receiver) {
2438
+ if (typeof prop === 'string' && ARRAY_MUTATORS.has(prop)) {
2439
+ const fn = target[prop];
2440
+ 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);
2466
+ notifyOwners(entry.owners, null);
2467
+ return ret;
2468
+ };
2469
+ }
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;
2479
+ },
2480
+ 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
+ const prev = target[prop];
2504
+ if (Object.is(prev, wrapped))
2505
+ return true;
2506
+ const ok = Reflect.set(target, prop, wrapped, receiver);
2507
+ if (ok) {
2508
+ if (prop === 'length')
2509
+ notifyOwners(entry.owners, null);
2510
+ else
2511
+ notifyOwners(entry.owners, local);
2512
+ }
2513
+ return ok;
2514
+ },
2515
+ deleteProperty(target, prop) {
2516
+ if (!(prop in target))
2517
+ return true;
2518
+ const ok = Reflect.deleteProperty(target, prop);
2519
+ if (ok) {
2520
+ notifyOwners(entry.owners, typeof prop === 'string' ? [prop] : null);
2521
+ }
2522
+ return ok;
2523
+ },
2524
+ });
2525
+ entry.proxy = proxy;
2526
+ reactiveProxies.set(arr, entry);
2527
+ reactiveProxies.set(proxy, entry);
2528
+ return proxy;
2529
+ }
2530
+ /**
2531
+ * Coalesce field/path patches in the same turn via a dirty path trie.
2532
+ * Still precise deps — never a full-tree re-render. Flush runs as a microtask;
2533
+ * call `await flushPending(inst)` to apply synchronously (tests / immediate UI).
2534
+ *
2535
+ * Design: 规划设计/vmz/12 §6 — parent write covers children; siblings stay separate.
2536
+ *
2537
+ * @param {object} inst
2538
+ * @param {{ type: 'replace', root: string } | { type: 'path', root: string, segs: string[] } | string} notice
2539
+ * string form is transitional field-root alias for replace.
2540
+ */
2541
+ function scheduleRefresh(inst, notice) {
2542
+ if (!inst || inst.__vmzDestroyed)
2543
+ return;
2544
+ const n = typeof notice === 'string' ? { type: 'replace', root: notice } : notice;
2545
+ if (!n || !n.root)
2546
+ return;
2547
+ if (precision.enabled) {
2548
+ precision.writes++;
2549
+ bumpMap(precision.writesByRoot, n.root);
2550
+ }
2551
+ pushTrace('write', 'field', n.root, n.root);
2552
+ if (!inst.__vmzDirtyTrie)
2553
+ inst.__vmzDirtyTrie = Object.create(null);
2554
+ insertDirtyNotice(inst.__vmzDirtyTrie, n);
2555
+ // Transitional: keep notice list for flush loop emptiness check / compat.
2556
+ if (!inst.__vmzDirtyNotices)
2557
+ inst.__vmzDirtyNotices = [];
2558
+ inst.__vmzDirtyNotices.push(n);
2559
+ if (inst.__vmzFlushScheduled)
2560
+ return;
2561
+ inst.__vmzFlushScheduled = true;
2562
+ queueMicrotask(() => {
2563
+ inst.__vmzFlushScheduled = false;
2564
+ Promise.resolve(flushPending(inst)).catch((err) => console.error('vmz:dom flush', err));
2565
+ });
2566
+ }
2567
+ /**
2568
+ * @param {Record<string, any>} trie
2569
+ * @param {{ type: string, root: string, segs?: string[] }} notice
2570
+ */
2571
+ function insertDirtyNotice(trie, notice) {
2572
+ if (notice.type === 'replace') {
2573
+ trie[notice.root] = { replace: true };
2574
+ return;
2575
+ }
2576
+ const segs = notice.segs || [];
2577
+ let node = trie[notice.root];
2578
+ if (node && node.replace)
2579
+ return;
2580
+ if (!node) {
2581
+ node = { children: Object.create(null) };
2582
+ trie[notice.root] = node;
2583
+ }
2584
+ if (!segs.length) {
2585
+ trie[notice.root] = { replace: true };
2586
+ return;
2587
+ }
2588
+ if (!node.children)
2589
+ node.children = Object.create(null);
2590
+ let cur = node;
2591
+ for (let i = 0; i < segs.length; i++) {
2592
+ const seg = segs[i];
2593
+ if (cur.dirty)
2594
+ return; // ancestor already dirty
2595
+ if (!cur.children)
2596
+ cur.children = Object.create(null);
2597
+ if (i === segs.length - 1) {
2598
+ cur.children[seg] = { dirty: true };
2599
+ return;
2600
+ }
2601
+ let next = cur.children[seg];
2602
+ if (!next) {
2603
+ next = { children: Object.create(null) };
2604
+ cur.children[seg] = next;
2605
+ }
2606
+ else if (next.dirty) {
2607
+ return;
2608
+ }
2609
+ else if (!next.children) {
2610
+ next.children = Object.create(null);
2611
+ }
2612
+ cur = next;
2613
+ }
2614
+ }
2615
+ /** @param {object} inst */
2616
+ export async function flushPending(inst) {
2617
+ if (!inst || inst.__vmzDestroyed)
2618
+ return;
2619
+ inst.__vmzFlushScheduled = false;
2620
+ let guard = 0;
2621
+ while (!inst.__vmzDestroyed &&
2622
+ ((inst.__vmzDirtyTrie && Object.keys(inst.__vmzDirtyTrie).length > 0) ||
2623
+ (inst.__vmzDirtyNotices && inst.__vmzDirtyNotices.length > 0)) &&
2624
+ guard++ < 64) {
2625
+ const trie = inst.__vmzDirtyTrie || Object.create(null);
2626
+ inst.__vmzDirtyTrie = Object.create(null);
2627
+ if (inst.__vmzDirtyNotices)
2628
+ inst.__vmzDirtyNotices.length = 0;
2629
+ inst.__vmzFlushTrie = trie;
2630
+ const jobs = [];
2631
+ // Prefer BindingId scheduling (IR). String `__vmzBinders` is adapter-only.
2632
+ // Pass `trie` into refresh — dirty map is cleared above before patches run.
2633
+ try {
2634
+ const bindingIds = bindingIdsMatchingTrie(inst, trie);
2635
+ const coveredDeps = Object.create(null);
2636
+ for (const id of bindingIds) {
2637
+ const entry = inst.__vmzBindings && inst.__vmzBindings[id];
2638
+ if (entry) {
2639
+ for (const d of entry.deps || [])
2640
+ coveredDeps[d] = true;
2641
+ }
2642
+ jobs.push(...refreshBinding(inst, id, trie));
2643
+ }
2644
+ for (const key of binderKeysMatchingTrie(inst, trie)) {
2645
+ if (coveredDeps[key])
2646
+ continue;
2647
+ if (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length) {
2648
+ continue;
2649
+ }
2650
+ jobs.push(...refreshField(inst, key));
2651
+ }
2652
+ if (jobs.length)
2653
+ await Promise.all(jobs);
2654
+ }
2655
+ finally {
2656
+ inst.__vmzFlushTrie = null;
2657
+ }
2658
+ }
2659
+ }
2660
+ /**
2661
+ * @param {object} inst
2662
+ * @param {Record<string, any>} trie
2663
+ * @returns {Array<number|string>}
2664
+ */
2665
+ function bindingIdsMatchingTrie(inst, trie) {
2666
+ const index = inst.__vmzDepToBindings;
2667
+ if (!index)
2668
+ return [];
2669
+ const out = [];
2670
+ const seen = Object.create(null);
2671
+ for (const key of Object.keys(index)) {
2672
+ if (!depMatchesTrie(trie, key))
2673
+ continue;
2674
+ for (const id of index[key]) {
2675
+ const k = String(id);
2676
+ if (seen[k])
2677
+ continue;
2678
+ seen[k] = true;
2679
+ out.push(id);
2680
+ }
2681
+ }
2682
+ return out;
2683
+ }
2684
+ /**
2685
+ * @param {object} inst
2686
+ * @param {Record<string, any>} trie
2687
+ * @returns {string[]}
2688
+ */
2689
+ function binderKeysMatchingTrie(inst, trie) {
2690
+ const binders = inst.__vmzBinders;
2691
+ if (!binders)
2692
+ return [];
2693
+ const out = [];
2694
+ for (const key of Object.keys(binders)) {
2695
+ if (depMatchesTrie(trie, key))
2696
+ out.push(key);
2697
+ }
2698
+ return out;
2699
+ }
2700
+ /**
2701
+ * @param {Record<string, any>} trie
2702
+ * @param {string} key
2703
+ */
2704
+ function depMatchesTrie(trie, key) {
2705
+ const root = depRootField(key);
2706
+ const node = trie[root];
2707
+ if (!node)
2708
+ return false;
2709
+ if (node.replace) {
2710
+ return key === root || key === `${root}.*` || key.startsWith(`${root}.`) || key.startsWith(`${root}[`);
2711
+ }
2712
+ if (key === `${root}.*`) {
2713
+ // Bare `field.*` soft/structure channel: item replace / array structure only —
2714
+ // NOT deep leaf writes (`tags.0.label`); those use `tags.*.label` BindingId.
2715
+ return structureStarMatches(node);
2716
+ }
2717
+ // Bare field: replace-only.
2718
+ if (key === root)
2719
+ return false;
2720
+ // Path channel: `tags.*.label` — wildcard index under list root.
2721
+ const starPrefix = `${root}.*`;
2722
+ if (key === starPrefix || key.startsWith(`${starPrefix}.`)) {
2723
+ const rest = key === starPrefix
2724
+ ? []
2725
+ : key
2726
+ .slice(starPrefix.length + 1)
2727
+ .split('.')
2728
+ .filter(Boolean);
2729
+ return wildcardIndexDirtyCovers(node, rest);
2730
+ }
2731
+ // Stable ListItem form `tags[key=…].label` — treat `[key=…]` as wildcard index.
2732
+ if (key.startsWith(`${root}[`)) {
2733
+ const afterBracket = key.indexOf(']');
2734
+ if (afterBracket > root.length) {
2735
+ const rest = key.length > afterBracket + 1 && key[afterBracket + 1] === '.'
2736
+ ? key
2737
+ .slice(afterBracket + 2)
2738
+ .split('.')
2739
+ .filter(Boolean)
2740
+ : [];
2741
+ return wildcardIndexDirtyCovers(node, rest);
2742
+ }
2743
+ }
2744
+ const segs = key
2745
+ .slice(root.length + 1)
2746
+ .split('.')
2747
+ .filter(Boolean);
2748
+ return pathDirtyCovers(node, segs);
2749
+ }
2750
+ /** `tags.*` structure soft-refresh: replace or index-level dirty, not leaf-only. */
2751
+ function structureStarMatches(node) {
2752
+ if (!node)
2753
+ return false;
2754
+ if (node.replace || node.dirty)
2755
+ return true;
2756
+ if (!node.children)
2757
+ return false;
2758
+ for (const idx of Object.keys(node.children)) {
2759
+ const child = node.children[idx];
2760
+ // Index node dirty/replace → item identity changed.
2761
+ if (child && (child.replace || child.dirty))
2762
+ return true;
2763
+ }
2764
+ return false;
2765
+ }
2766
+ /** `tags.*.label` / `tags[key=x].label` vs dirty trie under `tags`. */
2767
+ function wildcardIndexDirtyCovers(node, restSegs) {
2768
+ if (!node || node.replace)
2769
+ return !!node?.replace;
2770
+ if (node.dirty)
2771
+ return true;
2772
+ if (!node.children)
2773
+ return false;
2774
+ for (const idx of Object.keys(node.children)) {
2775
+ const child = node.children[idx];
2776
+ if (restSegs.length === 0) {
2777
+ if (trieHasAnyDirty(child))
2778
+ return true;
2779
+ }
2780
+ else if (pathDirtyCovers(child, restSegs)) {
2781
+ return true;
2782
+ }
2783
+ }
2784
+ return false;
2785
+ }
2786
+ function trieHasAnyDirty(node) {
2787
+ if (!node || node.replace)
2788
+ return !!node;
2789
+ if (node.dirty)
2790
+ return true;
2791
+ if (!node.children)
2792
+ return false;
2793
+ for (const k of Object.keys(node.children)) {
2794
+ if (trieHasAnyDirty(node.children[k]))
2795
+ return true;
2796
+ }
2797
+ return false;
2798
+ }
2799
+ /**
2800
+ * Wake if write is at/under dep, or dep is under write (parent covers children).
2801
+ * @param {any} node root trie node for field
2802
+ * @param {string[]} depSegs
2803
+ */
2804
+ function pathDirtyCovers(node, depSegs) {
2805
+ let cur = node;
2806
+ for (let i = 0; i < depSegs.length; i++) {
2807
+ if (!cur || cur.replace)
2808
+ return !!cur?.replace;
2809
+ if (cur.dirty)
2810
+ return true; // write parent covers this dep
2811
+ if (!cur.children)
2812
+ return false;
2813
+ const next = cur.children[depSegs[i]];
2814
+ if (!next) {
2815
+ // No write along this dep path — but a write under a prefix?
2816
+ return false;
2817
+ }
2818
+ cur = next;
2819
+ }
2820
+ // Reached dep node: wake if dirty here or any dirty descendant (write under dep).
2821
+ return trieHasAnyDirty(cur);
2822
+ }
2823
+ /**
2824
+ * Dual-track match retained for tests / tooling.
2825
+ * Design: 规划设计/vmz/11 §2.2 + 12 §6 parent-covers-children.
2826
+ * @param {{ type: string, root: string, segs?: string[] }} notice
2827
+ * @param {string} key
2828
+ */
2829
+ function noticeMatchesDepKey(notice, key) {
2830
+ const trie = Object.create(null);
2831
+ insertDirtyNotice(trie, notice);
2832
+ return depMatchesTrie(trie, key);
2833
+ }
2834
+ /** Root field name from a dep key string (`user.name` → `user`, `tags.*` → `tags`). */
2835
+ function depRootField(dep) {
2836
+ if (!dep)
2837
+ return '';
2838
+ const star = dep.indexOf('.*');
2839
+ if (star >= 0)
2840
+ return dep.slice(0, star);
2841
+ const dot = dep.indexOf('.');
2842
+ if (dot >= 0)
2843
+ return dep.slice(0, dot);
2844
+ const bracket = dep.indexOf('[');
2845
+ if (bracket >= 0)
2846
+ return dep.slice(0, bracket);
2847
+ return dep;
2848
+ }
2849
+ /** Precise patches only — no full-tree fallback. @returns {Promise[]} */
2850
+ function refreshBinding(inst, bindingId, dirtyTrie = null) {
2851
+ const entry = inst.__vmzBindings && inst.__vmzBindings[bindingId];
2852
+ const jobs = [];
2853
+ if (!inst || inst.__vmzDestroyed || bindingId == null || !entry) {
2854
+ return jobs;
2855
+ }
2856
+ const depKey = (entry.deps && entry.deps[0]) || null;
2857
+ const trie = dirtyTrie || inst.__vmzDirtyTrie;
2858
+ const allowIdx = itemIndicesAllowedForDeps(trie, entry.deps);
2859
+ for (const fn of entry.patches) {
2860
+ if (allowIdx && !patchMatchesDirtyIndex(fn, allowIdx))
2861
+ continue;
2862
+ try {
2863
+ const ret = runPatch(fn, depKey, bindingId);
2864
+ if (ret && typeof ret.then === 'function')
2865
+ jobs.push(ret);
2866
+ }
2867
+ catch (err) {
2868
+ console.error('vmz:dom patch', err);
2869
+ }
2870
+ }
2871
+ return jobs;
2872
+ }
2873
+ /**
2874
+ * For ListItem path-channel deps (`tags.*.label`), restrict to dirty indices.
2875
+ * @param {Record<string, any>|null|undefined} trie
2876
+ * @param {string[]|null|undefined} deps
2877
+ * @returns {Set<string>|null} null = run all patches (replace / non-list deps)
2878
+ */
2879
+ function itemIndicesAllowedForDeps(trie, deps) {
2880
+ if (!trie || !deps || !deps.length)
2881
+ return null;
2882
+ let sawListChannel = false;
2883
+ /** @type {Set<string>|null} */
2884
+ let allow = null;
2885
+ for (const dep of deps) {
2886
+ const root = depRootField(dep);
2887
+ if (!root)
2888
+ continue;
2889
+ const starPrefix = `${root}.*`;
2890
+ const isListChannel = dep === starPrefix || dep.startsWith(`${starPrefix}.`) || (dep.startsWith(`${root}[`) && dep.includes(']'));
2891
+ if (!isListChannel)
2892
+ return null;
2893
+ sawListChannel = true;
2894
+ const node = trie[root];
2895
+ if (!node)
2896
+ continue;
2897
+ if (node.replace || node.dirty)
2898
+ return null; // whole list
2899
+ if (!node.children)
2900
+ continue;
2901
+ if (!allow)
2902
+ allow = new Set();
2903
+ for (const idx of Object.keys(node.children)) {
2904
+ const child = node.children[idx];
2905
+ if (!child)
2906
+ continue;
2907
+ if (child.replace || child.dirty || trieHasAnyDirty(child)) {
2908
+ allow.add(String(idx));
2909
+ }
2910
+ }
2911
+ }
2912
+ if (!sawListChannel)
2913
+ return null;
2914
+ return allow && allow.size ? allow : null;
2915
+ }
2916
+ function patchMatchesDirtyIndex(fn, allowIdx) {
2917
+ const idx = fn && fn.__vmzItemIndex;
2918
+ if (idx == null || idx === '')
2919
+ return true;
2920
+ return allowIdx.has(String(idx));
2921
+ }
2922
+ /** Legacy string-key patches (hand blueprints without BindingId). @returns {Promise[]} */
2923
+ function refreshField(inst, field) {
2924
+ const binders = inst.__vmzBinders;
2925
+ const jobs = [];
2926
+ if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
2927
+ return jobs;
2928
+ }
2929
+ for (const fn of binders[field]) {
2930
+ try {
2931
+ const ret = runPatch(fn, field, null);
2932
+ if (ret && typeof ret.then === 'function')
2933
+ jobs.push(ret);
2934
+ }
2935
+ catch (err) {
2936
+ console.error('vmz:dom patch', err);
2937
+ }
2938
+ }
2939
+ return jobs;
2940
+ }
2941
+ /**
2942
+ * @param {object} inst
2943
+ * @param {number|string} bindingId
2944
+ * @param {string[]} deps
2945
+ */
2946
+ function reindexBindingDeps(inst, bindingId, deps) {
2947
+ if (!inst.__vmzDepToBindings)
2948
+ inst.__vmzDepToBindings = Object.create(null);
2949
+ const entry = inst.__vmzBindings[bindingId];
2950
+ if (!entry)
2951
+ return;
2952
+ for (const dep of entry.deps || []) {
2953
+ const list = inst.__vmzDepToBindings[dep];
2954
+ if (!list)
2955
+ continue;
2956
+ const j = list.indexOf(bindingId);
2957
+ if (j >= 0)
2958
+ list.splice(j, 1);
2959
+ if (list.length === 0)
2960
+ delete inst.__vmzDepToBindings[dep];
2961
+ }
2962
+ entry.deps = [...(deps || [])];
2963
+ for (const dep of entry.deps) {
2964
+ if (!inst.__vmzDepToBindings[dep])
2965
+ inst.__vmzDepToBindings[dep] = [];
2966
+ if (!inst.__vmzDepToBindings[dep].includes(bindingId)) {
2967
+ inst.__vmzDepToBindings[dep].push(bindingId);
2968
+ }
2969
+ }
2970
+ }
2971
+ /**
2972
+ * @param {object} inst
2973
+ * @param {string[]} deps
2974
+ * @param {() => any} fn
2975
+ * @param {number|string|null|undefined} [bindingId]
2976
+ */
2977
+ function registerBind(inst, deps, fn, bindingId = null) {
2978
+ if (!inst.__vmzBinders)
2979
+ inst.__vmzBinders = Object.create(null);
2980
+ for (const dep of deps || []) {
2981
+ if (!inst.__vmzBinders[dep])
2982
+ inst.__vmzBinders[dep] = [];
2983
+ inst.__vmzBinders[dep].push(fn);
2984
+ }
2985
+ if (bindingId == null)
2986
+ return;
2987
+ if (!inst.__vmzBindings)
2988
+ inst.__vmzBindings = Object.create(null);
2989
+ let entry = inst.__vmzBindings[bindingId];
2990
+ if (!entry) {
2991
+ entry = { id: bindingId, deps: [], patches: [] };
2992
+ inst.__vmzBindings[bindingId] = entry;
2993
+ }
2994
+ if (!entry.patches.includes(fn))
2995
+ entry.patches.push(fn);
2996
+ reindexBindingDeps(inst, bindingId, deps || []);
2997
+ }
2998
+ /**
2999
+ * @param {object} inst
3000
+ * @param {string[]} deps
3001
+ * @param {() => any} fn
3002
+ * @param {number|string|null|undefined} [bindingId]
3003
+ */
3004
+ function unregisterBind(inst, deps, fn, bindingId = null) {
3005
+ const binders = inst.__vmzBinders;
3006
+ if (binders) {
3007
+ for (const dep of deps || []) {
3008
+ const list = binders[dep];
3009
+ if (!list)
3010
+ continue;
3011
+ const i = list.indexOf(fn);
3012
+ if (i >= 0)
3013
+ list.splice(i, 1);
3014
+ if (list.length === 0)
3015
+ delete binders[dep];
3016
+ }
3017
+ }
3018
+ if (bindingId == null || !inst.__vmzBindings)
3019
+ return;
3020
+ const entry = inst.__vmzBindings[bindingId];
3021
+ if (!entry)
3022
+ return;
3023
+ const i = entry.patches.indexOf(fn);
3024
+ if (i >= 0)
3025
+ entry.patches.splice(i, 1);
3026
+ if (entry.patches.length === 0) {
3027
+ reindexBindingDeps(inst, bindingId, []);
3028
+ delete inst.__vmzBindings[bindingId];
3029
+ }
3030
+ }
3031
+ /**
3032
+ * True when the container already has meaningful DOM (SSR / resume shell).
3033
+ * @param {Element} el
3034
+ */
3035
+ function hasMeaningfulChild(el) {
3036
+ for (const n of el.childNodes) {
3037
+ if (n.nodeType === 1)
3038
+ return true;
3039
+ if (n.nodeType === 3 && String(n.textContent).trim() !== '')
3040
+ return true;
3041
+ }
3042
+ return false;
3043
+ }
3044
+ function patchHasBindingId(inst, fn) {
3045
+ const bindings = inst && inst.__vmzBindings;
3046
+ if (!bindings)
3047
+ return false;
3048
+ for (const id of Object.keys(bindings)) {
3049
+ const patches = bindings[id].patches;
3050
+ if (patches && patches.includes(fn))
3051
+ return true;
3052
+ }
3053
+ return false;
3054
+ }
3055
+ /** Tag each-item patches with list index for ListItem path-channel filtering. */
3056
+ function tagItemPatches(patches, index) {
3057
+ if (!patches)
3058
+ return;
3059
+ const idx = String(index);
3060
+ for (const p of patches) {
3061
+ if (typeof p === 'function')
3062
+ p.__vmzItemIndex = idx;
3063
+ }
3064
+ }
3065
+ function escapeHtml(s) {
3066
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3067
+ }