@vmz/core 0.0.1 → 0.0.3

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