@vmz/core 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dom.js CHANGED
@@ -1,4381 +1,7 @@
1
1
  // @ts-nocheck
2
2
  /**
3
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.
4
+ * Compat barrel for `@vmz/core/dom` (full client + SSR).
9
5
  */
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 = {}, opts = {}) {
170
- const signal = opts && opts.signal;
171
- if (signal && signal.aborted)
172
- return '';
173
- const inst = createInstance(Component, props);
174
- if (typeof inst.onMount === 'function') {
175
- await inst.onMount();
176
- }
177
- if (signal && signal.aborted)
178
- return '';
179
- // production Direct emit: SSR only via Direct serialize schedule — never `render`.
180
- if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
181
- throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
182
- }
183
- const root = await runDirectSerializeTreeWithMounts(Component, inst);
184
- if (opts && opts.slotHtml != null)
185
- injectDefaultSlotHtml(root, opts.slotHtml);
186
- return flattenSerializeNode(root);
187
- }
188
- /**
189
- * Stream SSR via the same Direct serialize schedule as `renderToString`.
190
- * Yields HTML chunks (open tag → children → close). Joining chunks equals `renderToString`.
191
- * Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
192
- * @param {new (props?: object) => any} Component
193
- * @param {object} [props]
194
- * @param {{ signal?: AbortSignal, slotHtml?: string }} [opts]
195
- * @returns {AsyncGenerator<string, void, void>}
196
- */
197
- export async function* renderToStream(Component, props = {}, opts = {}) {
198
- const signal = opts && opts.signal;
199
- const aborted = () => Boolean(signal && signal.aborted);
200
- if (aborted())
201
- return;
202
- const inst = createInstance(Component, props);
203
- try {
204
- if (typeof inst.onMount === 'function') {
205
- await inst.onMount();
206
- }
207
- if (aborted())
208
- return;
209
- if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
210
- throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
211
- }
212
- const root = await runDirectSerializeTreeWithMounts(Component, inst);
213
- if (opts && opts.slotHtml != null)
214
- injectDefaultSlotHtml(root, opts.slotHtml);
215
- if (aborted())
216
- return;
217
- for (const chunk of streamSerializeChunks(root)) {
218
- if (aborted())
219
- return;
220
- yield chunk;
221
- // Allow consumers / HTTP hosts to flush between chunks (backpressure point).
222
- await Promise.resolve();
223
- }
224
- }
225
- finally {
226
- // Abort and normal completion both dispose the SSR instance (lifetime).
227
- destroy(inst);
228
- }
229
- }
230
- /**
231
- * Fill the layout-owned default `<slot>` with pre-rendered HTML (layout SSR wrap).
232
- * Skips nested component hosts (`data-vmz`) — their slots are for child projection
233
- * (e.g. Button label), not the page outlet. Without this, DFS hits LocaleToggle→Button
234
- * before Layout's `<main><slot>`, and the entire page HTML lands inside a button.
235
- * @param {any} node
236
- * @param {string} html
237
- */
238
- function injectDefaultSlotHtml(node, html) {
239
- if (!node || typeof node !== 'object')
240
- return false;
241
- if (node.__kind === 'el' && node.tag === 'slot' && !(node.attrs && node.attrs.name)) {
242
- node.__rawHtml = String(html ?? '');
243
- node.children = [];
244
- return true;
245
- }
246
- // Nested Direct component wrapper from serializeApi.component — do not search inside.
247
- if (node.__kind === 'el' && node.attrs && node.attrs['data-vmz'] != null) {
248
- return false;
249
- }
250
- const kids = node.children;
251
- if (Array.isArray(kids)) {
252
- for (const c of kids) {
253
- if (injectDefaultSlotHtml(c, html))
254
- return true;
255
- }
256
- }
257
- return false;
258
- }
259
- /**
260
- * Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
261
- * `[data-vmz]` component (Button/Link labels, etc.).
262
- * @param {Element | null | undefined} root
263
- * @returns {Element | null}
264
- */
265
- export function findOwnedDefaultSlot(root) {
266
- if (!root || root.nodeType !== 1)
267
- return null;
268
- const tag = String(root.tagName || '').toLowerCase();
269
- if (tag === 'slot' && !root.getAttribute('name'))
270
- return root;
271
- const kids = root.children;
272
- if (!kids || !kids.length)
273
- return null;
274
- for (let i = 0; i < kids.length; i++) {
275
- const c = kids[i];
276
- if (c.nodeType !== 1)
277
- continue;
278
- // Nested component host — its slots are not the layout page outlet.
279
- if (c.hasAttribute('data-vmz'))
280
- continue;
281
- const hit = findOwnedDefaultSlot(c);
282
- if (hit)
283
- return hit;
284
- }
285
- return null;
286
- }
287
- /**
288
- * Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
289
- * Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
290
- * outlet for the next layout or the page. Retains layout instances on `container`
291
- * so SPA transitions can dispose only the page host.
292
- * @param {new (props?: object) => any} Page
293
- * @param {Element} container
294
- * @param {object} [props]
295
- * @param {Array<new (props?: object) => any>} [layoutCtors] outer → inner
296
- * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
297
- */
298
- export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
299
- if (typeof document === 'undefined') {
300
- throw new Error('vmz:dom hydrateRoute() requires a document (browser)');
301
- }
302
- if (container.__vmzInst) {
303
- destroy(container.__vmzInst);
304
- container.__vmzInst = null;
305
- }
306
- container.__vmzPageHost = null;
307
- container.__vmzLayoutInsts = null;
308
- /** @type {object[]} */
309
- const layoutInsts = [];
310
- let host = container;
311
- const ctors = Array.isArray(layoutCtors) ? layoutCtors.filter(Boolean) : [];
312
- for (const Layout of ctors) {
313
- const inst = await mount(Layout, host, {});
314
- layoutInsts.push(inst);
315
- const slot = findOwnedDefaultSlot(inst.__vmzDomRoot);
316
- const outlet = document.createElement('div');
317
- outlet.setAttribute('data-vmz-outlet', '');
318
- if (slot && slot.parentNode)
319
- slot.replaceWith(outlet);
320
- else if (inst.__vmzDomRoot && typeof inst.__vmzDomRoot.appendChild === 'function') {
321
- inst.__vmzDomRoot.appendChild(outlet);
322
- }
323
- else {
324
- host.appendChild(outlet);
325
- }
326
- host = outlet;
327
- }
328
- const pageInst = await hydrate(Page, host, props, opts);
329
- container.__vmzPageHost = host;
330
- container.__vmzLayoutInsts = layoutInsts;
331
- // Outer layout (or page if no layouts) owns the #app instance for destroy().
332
- container.__vmzInst = layoutInsts[0] || pageInst;
333
- return pageInst;
334
- }
335
- /**
336
- * Mount once; later updates are dep patches only (never re-run structure).
337
- * Requires compiler `__vmzCreate` (production Direct emit — no blueprint fallback).
338
- * @param {new (props?: object) => any} Component
339
- * @param {Element} container
340
- * @param {object} [props]
341
- */
342
- export async function mount(Component, container, props = {}) {
343
- if (container.__vmzInst) {
344
- destroy(container.__vmzInst);
345
- container.__vmzInst = null;
346
- }
347
- const inst = createInstance(Component, props);
348
- inst.__vmzBinders = Object.create(null);
349
- inst.__vmzBindings = Object.create(null);
350
- inst.__vmzDepToBindings = Object.create(null);
351
- container.replaceChildren();
352
- const node = await createFromComponent(Component, inst);
353
- if (node) {
354
- inst.__vmzDomRoot = node;
355
- container.appendChild(node);
356
- }
357
- if (typeof inst.onMount === 'function') {
358
- await inst.onMount();
359
- }
360
- await settlePendingChildMounts(inst);
361
- container.__vmzInst = inst;
362
- return inst;
363
- }
364
- /**
365
- * Nested Direct `component` schedules child onMount asynchronously; drain before return
366
- * so SSR/hydrate callers see post-mount DOM (e.g. UserCard Ada, not Loading…).
367
- * @param {object} inst
368
- */
369
- async function settlePendingChildMounts(inst) {
370
- if (!inst || !Array.isArray(inst.__vmzPendingChildMounts) || !inst.__vmzPendingChildMounts.length)
371
- return;
372
- await Promise.all(inst.__vmzPendingChildMounts);
373
- inst.__vmzPendingChildMounts = [];
374
- const hosts = [];
375
- const root = inst.__vmzDomRoot;
376
- if (root && root.nodeType === 1) {
377
- if (root.__vmzInst)
378
- hosts.push(root.__vmzInst);
379
- for (const el of root.querySelectorAll('[data-vmz]')) {
380
- if (el.__vmzInst)
381
- hosts.push(el.__vmzInst);
382
- }
383
- }
384
- for (const child of hosts) {
385
- await flushPending(child);
386
- await settlePendingChildMounts(child);
387
- }
388
- }
389
- /**
390
- * Direct create only (production Direct emit).
391
- * @param {new (props?: object) => any} Component
392
- * @param {object} inst
393
- */
394
- async function createFromComponent(Component, inst) {
395
- if (Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
396
- return runDirectCreate(Component, inst);
397
- }
398
- throw new Error(`vmz:dom mount requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
399
- }
400
- /**
401
- * @param {new (props?: object) => any} Component
402
- * @param {object} inst
403
- */
404
- function runDirectCreate(Component, inst) {
405
- // Nested component creates (e.g. Button inside parent ifBlock branch) must not
406
- // leak bindAttr/bindText into the parent's `_branchBinds` / `_itemPatches` sink —
407
- // that steals numeric BindingIds (0) and corrupts parent deps (density → type).
408
- const prevInst = directApi._inst;
409
- const prevBranch = directApi._branchBinds;
410
- const prevItems = directApi._itemPatches;
411
- const prevEach = directApi._eachCtx;
412
- directApi._inst = inst;
413
- directApi._branchBinds = null;
414
- directApi._itemPatches = null;
415
- directApi._eachCtx = null;
416
- try {
417
- return Component.__vmzCreate.call(inst, directApi);
418
- }
419
- finally {
420
- directApi._inst = prevInst;
421
- directApi._branchBinds = prevBranch;
422
- directApi._itemPatches = prevItems;
423
- directApi._eachCtx = prevEach;
424
- }
425
- }
426
- /**
427
- * SSR: run the same __vmzCreate schedule against a serialize host (no render).
428
- * @param {new (props?: object) => any} Component
429
- * @param {object} inst
430
- */
431
- function runDirectSerializeTree(Component, inst) {
432
- serializeApi._inst = inst;
433
- try {
434
- return Component.__vmzCreate.call(inst, serializeApi);
435
- }
436
- finally {
437
- serializeApi._inst = null;
438
- }
439
- }
440
- /**
441
- * SSR child onMount: sync `__vmzCreate` cannot await nested mounts.
442
- * Expand rounds — reuse prior child instances, await newly discovered onMounts, re-emit.
443
- * @param {new (props?: object) => any} Component
444
- * @param {object} inst
445
- */
446
- async function runDirectSerializeTreeWithMounts(Component, inst) {
447
- /** @type {object[]} */
448
- let preMounted = [];
449
- /** @type {any} */
450
- let tree = null;
451
- for (let round = 0; round < 32; round++) {
452
- serializeApi._ssrPreMounted = preMounted;
453
- serializeApi._ssrPreIdx = 0;
454
- serializeApi._ssrCollected = [];
455
- tree = runDirectSerializeTree(Component, inst);
456
- if (serializeApi._ssrPreIdx !== preMounted.length) {
457
- throw new Error(`vmz:dom SSR child mount queue desync (used ${serializeApi._ssrPreIdx}, had ${preMounted.length})`);
458
- }
459
- const collected = serializeApi._ssrCollected;
460
- serializeApi._ssrPreMounted = null;
461
- serializeApi._ssrCollected = null;
462
- if (!collected.length)
463
- return tree;
464
- for (const child of collected) {
465
- if (typeof child.onMount === 'function') {
466
- await child.onMount();
467
- }
468
- }
469
- preMounted = preMounted.concat(collected);
470
- }
471
- throw new Error('vmz:dom SSR child onMount expansion exceeded 32 rounds');
472
- }
473
- function serializeOpenTag(node) {
474
- const tag = node.tag || 'div';
475
- let attrs = '';
476
- for (const [k, v] of Object.entries(node.attrs || {})) {
477
- if (v == null || v === false)
478
- continue;
479
- if (k === 'className')
480
- attrs += ` class="${escapeHtml(v)}"`;
481
- else
482
- attrs += ` ${k}="${escapeHtml(v)}"`;
483
- }
484
- return { tag, open: `<${tag}${attrs}>` };
485
- }
486
- function flattenSerializeNode(node) {
487
- if (node == null || node === false)
488
- return '';
489
- if (typeof node === 'string' || typeof node === 'number')
490
- return escapeHtml(node);
491
- if (node.__kind === 'text')
492
- return escapeHtml(node.value);
493
- if (node.__kind === 'frag') {
494
- return (node.children || []).map(flattenSerializeNode).join('');
495
- }
496
- if (node.__kind === 'el') {
497
- const tag = node.tag || 'div';
498
- if (tag === 'slot') {
499
- if (node.__rawHtml != null)
500
- return String(node.__rawHtml);
501
- return (node.children || []).map(flattenSerializeNode).join('');
502
- }
503
- const { open } = serializeOpenTag(node);
504
- if (node.__rawHtml != null) {
505
- return `${open}${String(node.__rawHtml)}</${tag}>`;
506
- }
507
- const inner = (node.children || []).map(flattenSerializeNode).join('');
508
- return `${open}${inner}</${tag}>`;
509
- }
510
- return '';
511
- }
512
- /**
513
- * Progressive HTML chunks from a serialize tree (same nodes as flattenSerializeNode).
514
- * @param {any} node
515
- * @returns {Generator<string, void, void>}
516
- */
517
- function* streamSerializeChunks(node) {
518
- if (node == null || node === false)
519
- return;
520
- if (typeof node === 'string' || typeof node === 'number') {
521
- yield escapeHtml(node);
522
- return;
523
- }
524
- if (node.__kind === 'text') {
525
- yield escapeHtml(node.value);
526
- return;
527
- }
528
- if (node.__kind === 'frag') {
529
- for (const c of node.children || [])
530
- yield* streamSerializeChunks(c);
531
- return;
532
- }
533
- if (node.__kind === 'el') {
534
- const tag = node.tag || 'div';
535
- if (tag === 'slot') {
536
- if (node.__rawHtml != null) {
537
- yield String(node.__rawHtml);
538
- return;
539
- }
540
- for (const c of node.children || [])
541
- yield* streamSerializeChunks(c);
542
- return;
543
- }
544
- const { open } = serializeOpenTag(node);
545
- yield open;
546
- if (node.__rawHtml != null) {
547
- yield String(node.__rawHtml);
548
- }
549
- else {
550
- for (const c of node.children || [])
551
- yield* streamSerializeChunks(c);
552
- }
553
- yield `</${tag}>`;
554
- }
555
- }
556
- /** Serialize host mirroring directApi — returns virtual nodes, not DOM. */
557
- const serializeApi = {
558
- /** @type {object | null} */
559
- _inst: null,
560
- /** @type {null} */
561
- _branchBinds: null,
562
- /** @type {null} */
563
- _itemPatches: null,
564
- /** @type {object[] | null} reused child instances from prior SSR mount rounds */
565
- _ssrPreMounted: null,
566
- /** @type {number} */
567
- _ssrPreIdx: 0,
568
- /** @type {object[] | null} newly created child instances this round */
569
- _ssrCollected: null,
570
- /**
571
- * @param {new (props?: object) => any} Ctor
572
- * @param {object} resolved
573
- */
574
- _ssrChildInstance(Ctor, resolved) {
575
- const pre = serializeApi._ssrPreMounted;
576
- if (pre && serializeApi._ssrPreIdx < pre.length) {
577
- return pre[serializeApi._ssrPreIdx++];
578
- }
579
- const child = createInstance(Ctor, resolved);
580
- if (serializeApi._ssrCollected)
581
- serializeApi._ssrCollected.push(child);
582
- return child;
583
- },
584
- el(tag) {
585
- return {
586
- __kind: 'el',
587
- tag: tag || 'div',
588
- attrs: {},
589
- children: [],
590
- appendChild(c) {
591
- if (c != null)
592
- this.children.push(c);
593
- },
594
- };
595
- },
596
- text(value) {
597
- return { __kind: 'text', value: value == null ? '' : String(value) };
598
- },
599
- frag() {
600
- return {
601
- __kind: 'frag',
602
- children: [],
603
- appendChild(c) {
604
- if (c != null)
605
- this.children.push(c);
606
- },
607
- };
608
- },
609
- attr(el, name, value) {
610
- if (!el || el.__kind !== 'el')
611
- return;
612
- applySerializeAttr(el, name, value);
613
- },
614
- on() {
615
- /* events are no-ops during SSR */
616
- },
617
- bindText(inst, bindingId, deps, get, textNode) {
618
- let raw = '';
619
- try {
620
- raw = get.call(inst);
621
- }
622
- catch {
623
- raw = '';
624
- }
625
- textNode.value = String(raw ?? '');
626
- },
627
- bindAttr(inst, bindingId, deps, get, el, name) {
628
- let raw;
629
- try {
630
- raw = get.call(inst);
631
- }
632
- catch {
633
- raw = null;
634
- }
635
- applySerializeAttr(el, name, raw);
636
- },
637
- bindComponentProp() {
638
- /* SSR: props already resolved into the child instance at create */
639
- },
640
- projectDefaultSlot(hostEl, node) {
641
- if (!hostEl || node == null)
642
- return;
643
- // serializeApi.component returns a serialize el tree (or island shell).
644
- const root = hostEl.__kind === 'el' ? hostEl : null;
645
- const findSlot = (n) => {
646
- if (!n || n.__kind !== 'el')
647
- return null;
648
- if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
649
- return n;
650
- for (const c of n.children || []) {
651
- const hit = findSlot(c);
652
- if (hit)
653
- return hit;
654
- }
655
- return null;
656
- };
657
- // Prefer searching the component body (first child of host wrapper).
658
- let slot = null;
659
- if (root) {
660
- for (const c of root.children || []) {
661
- slot = findSlot(c);
662
- if (slot)
663
- break;
664
- }
665
- if (!slot)
666
- slot = findSlot(root);
667
- }
668
- if (slot) {
669
- slot.__rawHtml = null;
670
- if (!Array.isArray(slot.children))
671
- slot.children = [];
672
- // Append — multiple projectDefaultSlot calls must accumulate (SSR).
673
- // Client path replaces the live <slot> then appends siblings; serialize must push.
674
- slot.children.push(node);
675
- return;
676
- }
677
- if (root)
678
- root.appendChild(node);
679
- },
680
- setHtml(el, value) {
681
- if (!el || el.__kind !== 'el')
682
- return;
683
- el.__rawHtml = value == null ? '' : String(value);
684
- el.children = [];
685
- },
686
- bindHtml(inst, bindingId, deps, get, el) {
687
- let raw = '';
688
- try {
689
- raw = get.call(inst);
690
- }
691
- catch {
692
- raw = '';
693
- }
694
- el.__rawHtml = raw == null ? '' : String(raw);
695
- el.children = [];
696
- },
697
- ifBlock(inst, bindingId, deps, branches) {
698
- const host = {
699
- __kind: 'el',
700
- tag: 'span',
701
- attrs: { 'data-vmz-if': '' },
702
- children: [],
703
- appendChild(c) {
704
- if (c != null)
705
- this.children.push(c);
706
- },
707
- };
708
- let idx = -1;
709
- for (let i = 0; i < branches.length; i++) {
710
- const b = branches[i];
711
- if (!b.cond) {
712
- idx = i;
713
- break;
714
- }
715
- try {
716
- if (b.cond.call(inst)) {
717
- idx = i;
718
- break;
719
- }
720
- }
721
- catch {
722
- /* continue */
723
- }
724
- }
725
- if (idx >= 0 && branches[idx].create) {
726
- const created = branches[idx].create.call(inst, serializeApi);
727
- if (created)
728
- host.children.push(created);
729
- }
730
- return host;
731
- },
732
- eachBlock(inst, bindingId, deps, spec) {
733
- const frag = serializeApi.frag();
734
- let list = [];
735
- try {
736
- list = spec.list.call(inst) || [];
737
- }
738
- catch {
739
- list = [];
740
- }
741
- if (!Array.isArray(list))
742
- list = [...list];
743
- for (let i = 0; i < list.length; i++) {
744
- const box = { item: list[i], index: i };
745
- let k = i;
746
- if (typeof spec.key === 'function') {
747
- try {
748
- k = spec.key.call(inst, box);
749
- }
750
- catch {
751
- k = i;
752
- }
753
- }
754
- const dom = spec.createItem.call(inst, serializeApi, box);
755
- if (dom) {
756
- // SSR only: serialize key into HTML for hydrate/debug. Direct client does not write this attr.
757
- if (dom.__kind === 'el')
758
- serializeApi.attr(dom, 'data-vmz-key', String(k));
759
- frag.appendChild(dom);
760
- }
761
- }
762
- return frag;
763
- },
764
- component(hostInst, name, props, client) {
765
- const Ctor = components[name];
766
- if (!Ctor)
767
- throw new Error(`vmz:dom unknown component <${name} />`);
768
- /** @type {Record<string, any>} */
769
- const resolved = {};
770
- for (const [k, v] of Object.entries(props || {})) {
771
- if (typeof v === 'function' && isEventPropName(k))
772
- continue;
773
- else if (typeof v === 'function')
774
- resolved[k] = v.call(hostInst);
775
- else
776
- resolved[k] = v;
777
- }
778
- if (client) {
779
- // resume: Island SSR includes body + ResumeEntry slice (same Direct schedule).
780
- const child = serializeApi._ssrChildInstance(Ctor, resolved);
781
- let body = null;
782
- if (Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function') {
783
- const prev = serializeApi._inst;
784
- serializeApi._inst = child;
785
- try {
786
- body = Ctor.__vmzCreate.call(child, serializeApi);
787
- }
788
- finally {
789
- serializeApi._inst = prev;
790
- }
791
- }
792
- const state = snapshotInstanceState(child) || {};
793
- const plan = Ctor.__vmzPlan || null;
794
- const resume = {
795
- schema: 'vmz.resume.v0',
796
- component: name,
797
- strategy: String(client),
798
- props: stripFns(resolved),
799
- state,
800
- planSchema: plan?.schema || null,
801
- planRootIds: plan?.root_ids || [],
802
- };
803
- /** @type {Record<string, string>} */
804
- const attrs = {
805
- 'data-vmz': name,
806
- 'data-vmz-island': name,
807
- 'data-vmz-client': String(client),
808
- 'data-vmz-props': JSON.stringify(stripFns(resolved)),
809
- 'data-vmz-resume': JSON.stringify(resume),
810
- };
811
- if (isEventEntryStrategy(String(client))) {
812
- attrs['data-vmz-entry'] = 'event';
813
- }
814
- return {
815
- __kind: 'el',
816
- tag: 'div',
817
- attrs,
818
- children: body ? [body] : [],
819
- appendChild(c) {
820
- if (c != null)
821
- this.children.push(c);
822
- },
823
- };
824
- }
825
- const child = serializeApi._ssrChildInstance(Ctor, resolved);
826
- if (Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function') {
827
- const prev = serializeApi._inst;
828
- serializeApi._inst = child;
829
- try {
830
- const node = Ctor.__vmzCreate.call(child, serializeApi);
831
- return {
832
- __kind: 'el',
833
- tag: 'div',
834
- attrs: { 'data-vmz': name },
835
- children: node ? [node] : [],
836
- appendChild(c) {
837
- if (c != null)
838
- this.children.push(c);
839
- },
840
- };
841
- }
842
- finally {
843
- serializeApi._inst = prev;
844
- }
845
- }
846
- throw new Error(`vmz:dom serialize component <${name}> requires __vmzCreate (rebuild child with Direct)`);
847
- },
848
- };
849
- /** Host API for compiler-emitted `__vmzCreate` (direct path, Program IR B). */
850
- const directApi = {
851
- /** @type {object | null} */
852
- _inst: null,
853
- /** @type {Array<{ deps: string[], fn: => any, bindingId?: number|string|null }> | null} */
854
- _branchBinds: null,
855
- /** @type {Array< => void> | null} */
856
- _itemPatches: null,
857
- /**
858
- * Active keyed-each context (/): item binds + event delegation.
859
- * @type {null | {
860
- * noteItemBind: (bindingId: number|string|null, deps: string[], fn: => void) => void,
861
- * needDelegate: (type: string) => void,
862
- * }}
863
- */
864
- _eachCtx: null,
865
- el(tag) {
866
- noteDomCreate();
867
- return document.createElement(tag || 'div');
868
- },
869
- text(value) {
870
- noteDomCreate();
871
- return document.createTextNode(value == null ? '' : String(value));
872
- },
873
- frag() {
874
- noteDomCreate();
875
- return document.createDocumentFragment();
876
- },
877
- attr(el, name, value) {
878
- applyDomAttr(el, name, value);
879
- },
880
- on(el, type, handler) {
881
- const inst = directApi._inst;
882
- if (directApi._eachCtx && typeof handler === 'function') {
883
- /** @type {Record<string, Function>} */
884
- const bag = el.__vmzEvt || (el.__vmzEvt = Object.create(null));
885
- bag[type] = handler;
886
- directApi._eachCtx.needDelegate(type);
887
- return;
888
- }
889
- el.addEventListener(type, (ev) => {
890
- // Belt-and-suspenders: form submit must not navigate before handler runs.
891
- if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
892
- ev.preventDefault();
893
- }
894
- if (typeof handler === 'function')
895
- handler.call(inst, ev);
896
- });
897
- },
898
- /**
899
- * @param {object} inst
900
- * @param {number|string|null} bindingId
901
- * @param {string[]} deps
902
- * @param {() => any} get
903
- * @param {Text} textNode
904
- * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
905
- */
906
- bindText(inst, bindingId, deps, get, textNode, cf) {
907
- wireDirectBind(inst, bindingId, deps, get, (raw) => {
908
- textNode.textContent = String(raw ?? '');
909
- }, cf);
910
- },
911
- /**
912
- * @param {object} inst
913
- * @param {number|string|null} bindingId
914
- * @param {string[]} deps
915
- * @param {() => any} get
916
- * @param {Element} el
917
- * @param {string} name
918
- * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
919
- */
920
- bindAttr(inst, bindingId, deps, get, el, name, cf) {
921
- wireDirectBind(inst, bindingId, deps, get, (raw) => {
922
- if (name === 'class' || name === 'className') {
923
- const s = String(raw ?? '');
924
- if (s)
925
- el.setAttribute('class', s);
926
- else if (el.hasAttribute('class'))
927
- el.removeAttribute('class');
928
- }
929
- else {
930
- applyDomAttr(el, name, raw);
931
- }
932
- }, cf);
933
- },
934
- setHtml(el, value) {
935
- el.innerHTML = value == null ? '' : String(value);
936
- },
937
- /**
938
- * Trusted HTML binding (`html={expr}`). Author/plugin responsibility.
939
- * @param {object} inst
940
- * @param {number|string|null} bindingId
941
- * @param {string[]} deps
942
- * @param {() => any} get
943
- * @param {Element} el
944
- * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
945
- */
946
- bindHtml(inst, bindingId, deps, get, el, cf) {
947
- wireDirectBind(inst, bindingId, deps, get, (raw) => {
948
- el.innerHTML = raw == null ? '' : String(raw);
949
- }, cf);
950
- },
951
- /**
952
- * Nested component (sync Direct child or island schedule).
953
- * @param {object} hostInst
954
- * @param {string} name
955
- * @param {Record<string, any>} props
956
- * @param {string | null} client
957
- */
958
- component(hostInst, name, props, client) {
959
- noteDomCreate();
960
- const host = document.createElement('div');
961
- host.setAttribute('data-vmz', name);
962
- /** @type {Record<string, any>} */
963
- const resolved = {};
964
- for (const [k, v] of Object.entries(props || {})) {
965
- if (typeof v === 'function' && isEventPropName(k))
966
- resolved[k] = v;
967
- else if (typeof v === 'function')
968
- resolved[k] = v.call(hostInst);
969
- else
970
- resolved[k] = v;
971
- }
972
- if (client) {
973
- host.setAttribute('data-vmz-island', name);
974
- host.setAttribute('data-vmz-client', String(client));
975
- host.setAttribute('data-vmz-props', JSON.stringify(stripFns(resolved)));
976
- if (isEventEntryStrategy(String(client))) {
977
- host.setAttribute('data-vmz-entry', 'event');
978
- }
979
- // resume: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
980
- scheduleClientOn(host, String(client), async () => {
981
- const Ctor = await resolveComponent(name);
982
- if (!Ctor)
983
- throw new Error(`vmz:dom unknown component <${name} />`);
984
- await resume(Ctor, host, { props: resolved, state: {} });
985
- });
986
- return host;
987
- }
988
- const Ctor = components[name];
989
- if (!Ctor)
990
- throw new Error(`vmz:dom unknown component <${name} />`);
991
- const child = createInstance(Ctor, resolved);
992
- if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
993
- throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
994
- }
995
- const node = runDirectCreate(Ctor, child);
996
- if (node) {
997
- child.__vmzDomRoot = node;
998
- host.appendChild(node);
999
- }
1000
- host.__vmzInst = child;
1001
- if (typeof child.onMount === 'function') {
1002
- const pending = Promise.resolve().then(() => {
1003
- if (!child.__vmzDestroyed)
1004
- return child.onMount();
1005
- });
1006
- const bag = hostInst.__vmzPendingChildMounts || (hostInst.__vmzPendingChildMounts = []);
1007
- bag.push(pending);
1008
- }
1009
- return host;
1010
- },
1011
- /**
1012
- * Keep nested Direct child props live with parent field writes.
1013
- * @param {object} hostInst
1014
- * @param {HTMLElement} hostEl
1015
- * @param {string} propName
1016
- * @param {string[]} deps
1017
- * @param {() => any} get
1018
- */
1019
- bindComponentProp(hostInst, hostEl, propName, deps, get) {
1020
- // Use a stable BindingId so flushPending schedules this patch via the IR
1021
- // path. A null bindingId only lands in `__vmzBinders` and was skipped when
1022
- // the same parent field also had bindText/bindAttr BindingIds.
1023
- if (hostEl && hostEl.__vmzPropBindSeq == null) {
1024
- hostEl.__vmzPropBindSeq = ++directPropBindSeq;
1025
- }
1026
- const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
1027
- const bindingId = `pc:${seq}:${propName}`;
1028
- wireDirectBind(hostInst, bindingId, deps, get, (raw) => {
1029
- const child = hostEl && hostEl.__vmzInst;
1030
- if (!child || child.__vmzDestroyed)
1031
- return;
1032
- if (typeof propName !== 'string' || !propName || propName.startsWith('#'))
1033
- return;
1034
- child[propName] = raw;
1035
- scheduleRefresh(child, { type: 'replace', root: propName });
1036
- });
1037
- },
1038
- /**
1039
- * Project parent children into nested Direct component default `<slot>`.
1040
- * @param {HTMLElement} hostEl
1041
- * @param {Node} node
1042
- */
1043
- projectDefaultSlot(hostEl, node) {
1044
- if (!hostEl || node == null)
1045
- return;
1046
- const child = hostEl.__vmzInst;
1047
- const root = (child && child.__vmzDomRoot) || hostEl;
1048
- /** @type {Element | null} */
1049
- let slot = null;
1050
- if (root && root.nodeType === 1) {
1051
- if (String(root.tagName || '').toLowerCase() === 'slot' && !root.getAttribute('name')) {
1052
- slot = root;
1053
- }
1054
- else if (typeof root.querySelector === 'function') {
1055
- slot = root.querySelector('slot:not([name])');
1056
- }
1057
- }
1058
- if (slot && slot.parentNode) {
1059
- slot.replaceWith(node);
1060
- return;
1061
- }
1062
- if (root && typeof root.appendChild === 'function')
1063
- root.appendChild(node);
1064
- else
1065
- hostEl.appendChild(node);
1066
- },
1067
- /**
1068
- * Direct if/else — no blueprint `kind: "if"` dispatch.
1069
- * @param {object} inst
1070
- * @param {number|string|null} bindingId
1071
- * @param {string[]} deps
1072
- * @param {Array<{ cond?: => any, create: (api: typeof directApi) => Node }>} branches
1073
- * @param {number|string|null} [regionId]
1074
- */
1075
- ifBlock(inst, bindingId, deps, branches, regionId = null) {
1076
- noteDomCreate();
1077
- const host = document.createElement('span');
1078
- host.setAttribute('data-vmz-if', '');
1079
- if (regionId != null)
1080
- host.setAttribute('data-vmz-region', String(regionId));
1081
- /** @type {Array<Node | null>} */
1082
- const cached = branches.map(() => null);
1083
- /** @type {Array<Array<{ deps: string[], fn: => any, bindingId?: number|string|null }>>} */
1084
- const branchBinds = branches.map(() => []);
1085
- let active = -1;
1086
- let gen = 0;
1087
- const pick = () => {
1088
- for (let i = 0; i < branches.length; i++) {
1089
- const b = branches[i];
1090
- if (!b.cond)
1091
- return i;
1092
- try {
1093
- if (b.cond.call(inst))
1094
- return i;
1095
- }
1096
- catch {
1097
- /* continue */
1098
- }
1099
- }
1100
- return -1;
1101
- };
1102
- const wireBranch = (idx) => {
1103
- if (idx < 0)
1104
- return;
1105
- for (const { deps: d, fn, bindingId: bid } of branchBinds[idx]) {
1106
- registerBind(inst, d, fn, bid);
1107
- try {
1108
- runPatch(fn, (d && d[0]) || null, bid ?? null);
1109
- }
1110
- catch (err) {
1111
- console.error('vmz:dom if branch', err);
1112
- }
1113
- }
1114
- };
1115
- const unwireBranch = (idx) => {
1116
- if (idx < 0)
1117
- return;
1118
- for (const { deps: d, fn, bindingId: bid } of branchBinds[idx]) {
1119
- unregisterBind(inst, d, fn, bid);
1120
- }
1121
- };
1122
- const apply = () => {
1123
- if (inst.__vmzDestroyed)
1124
- return;
1125
- const applied = ++gen;
1126
- const next = pick();
1127
- if (next === active)
1128
- return;
1129
- if (next >= 0 && !cached[next]) {
1130
- const binds = [];
1131
- const prevSink = directApi._branchBinds;
1132
- const prevInst = directApi._inst;
1133
- directApi._branchBinds = binds;
1134
- directApi._inst = inst;
1135
- let created = null;
1136
- try {
1137
- created = branches[next].create.call(inst, directApi);
1138
- }
1139
- finally {
1140
- directApi._branchBinds = prevSink;
1141
- directApi._inst = prevInst;
1142
- }
1143
- if (applied !== gen || inst.__vmzDestroyed)
1144
- return;
1145
- if (!cached[next]) {
1146
- cached[next] = created;
1147
- branchBinds[next] = binds;
1148
- }
1149
- }
1150
- if (applied !== gen || inst.__vmzDestroyed)
1151
- return;
1152
- if (active >= 0) {
1153
- unwireBranch(active);
1154
- if (cached[active] && cached[active].parentNode) {
1155
- noteDomRemove();
1156
- cached[active].remove();
1157
- }
1158
- }
1159
- active = next;
1160
- if (next < 0)
1161
- return;
1162
- wireBranch(next);
1163
- if (cached[next])
1164
- host.appendChild(cached[next]);
1165
- };
1166
- registerBind(inst, deps || [], apply, bindingId);
1167
- if (directApi._itemPatches)
1168
- directApi._itemPatches.push(apply);
1169
- // parent destroy disposes all cached branch trees (pause ≠ destroy on switch).
1170
- host.__vmzDispose = () => {
1171
- for (let i = 0; i < cached.length; i++) {
1172
- unwireBranch(i);
1173
- if (cached[i])
1174
- disposeDomTree(cached[i]);
1175
- cached[i] = null;
1176
- }
1177
- active = -1;
1178
- };
1179
- apply();
1180
- return host;
1181
- },
1182
- /**
1183
- * Direct keyed each — no blueprint `kind: "each"` dispatch.
1184
- * /: Set/Map + Fragment batch insert; item-local binds; host field dispatch; event delegate.
1185
- * @param {object} inst
1186
- * @param {number|string|null} bindingId
1187
- * @param {string[]} deps
1188
- * @param {{ as?: string, list: => any, key?: (box: {item:any,index:number}) => any, createItem: (api: typeof directApi, box: {item:any,index:number}) => Node }} spec
1189
- * @param {number|string|null} [regionId]
1190
- */
1191
- eachBlock(inst, bindingId, deps, spec, regionId = null) {
1192
- const start = document.createComment(`vmz-each:${spec.as || ''}`);
1193
- const end = document.createComment('/vmz-each');
1194
- if (regionId != null)
1195
- start.__vmzRegion = regionId;
1196
- const frag = document.createDocumentFragment();
1197
- frag.appendChild(start);
1198
- frag.appendChild(end);
1199
- /** @type {Map<any, { box: { item: any, index: number }, dom: Node, patches: Array< => void> }>} */
1200
- const keyed = new Map();
1201
- let gen = 0;
1202
- /** @type {Map<string, => void>} */
1203
- const listDispatchers = new Map();
1204
- /** @type {Set<string>} */
1205
- const hostDispatchers = new Set();
1206
- /** @type {Record<string, any>} */
1207
- const hostPrev = Object.create(null);
1208
- /** @type {Set<string>} */
1209
- const delegateTypes = new Set();
1210
- /** @type {Record<string, EventListener>} */
1211
- const delegateListeners = Object.create(null);
1212
- /** @type {Element | null} */
1213
- let delegateRoot = null;
1214
- const itemKey = (box) => {
1215
- if (rowKeyField != null && box && box.item != null) {
1216
- return box.item[rowKeyField];
1217
- }
1218
- if (typeof spec.key === 'function') {
1219
- try {
1220
- return spec.key.call(inst, box);
1221
- }
1222
- catch {
1223
- return box.index;
1224
- }
1225
- }
1226
- return box.index;
1227
- };
1228
- /** Reused for keyed lookups — avoid per-row `{item,index}` alloc on create/update. */
1229
- const keyScratch = { item: null, index: 0 };
1230
- const keyOf = (item, index) => {
1231
- keyScratch.item = item;
1232
- keyScratch.index = index;
1233
- return itemKey(keyScratch);
1234
- };
1235
- const readList = () => {
1236
- let list = [];
1237
- try {
1238
- list = spec.list.call(inst) || [];
1239
- }
1240
- catch {
1241
- list = [];
1242
- }
1243
- if (!Array.isArray(list))
1244
- list = [...list];
1245
- return list;
1246
- };
1247
- const runEntryPatches = (entry, depKey, onlyBindingId) => {
1248
- if (!entry)
1249
- return;
1250
- if (entryIsBp(entry) && applyBp) {
1251
- if (onlyBindingId != null && blueprintBindIds && !blueprintBindIds.has(String(onlyBindingId))) {
1252
- return;
1253
- }
1254
- try {
1255
- applyBp(entry);
1256
- }
1257
- catch (err) {
1258
- console.error('vmz:dom each item', err);
1259
- }
1260
- return;
1261
- }
1262
- if (!entry.patches)
1263
- return;
1264
- for (const p of entry.patches) {
1265
- if (onlyBindingId != null) {
1266
- if (p.__vmzBindingIds) {
1267
- if (!p.__vmzBindingIds.has(String(onlyBindingId)))
1268
- continue;
1269
- }
1270
- else if (p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
1271
- continue;
1272
- }
1273
- }
1274
- try {
1275
- runPatch(p, depKey, onlyBindingId);
1276
- }
1277
- catch (err) {
1278
- console.error('vmz:dom each item', err);
1279
- }
1280
- }
1281
- };
1282
- const refreshByListIndex = (onlyBindingId, leafDeps, trie) => {
1283
- const list = readList();
1284
- const allowIdx = itemIndicesAllowedForDeps(trie, leafDeps);
1285
- const runAt = (i) => {
1286
- if (i < 0 || i >= list.length)
1287
- return;
1288
- const item = list[i];
1289
- const k = rowKeyOf(item, i);
1290
- const entry = keyed.get(k);
1291
- if (!entry)
1292
- return;
1293
- if (entry.nodeType === 1)
1294
- entry.__vmzBox = item;
1295
- else {
1296
- entry.item = item;
1297
- entry.index = i;
1298
- if (entry.dom && entry.bp)
1299
- entry.dom.__vmzBox = item;
1300
- }
1301
- if (entry.patches)
1302
- tagItemPatches(entry.patches, i);
1303
- runEntryPatches(entry, (leafDeps && leafDeps[0]) || null, onlyBindingId);
1304
- };
1305
- if (allowIdx) {
1306
- for (const idx of allowIdx)
1307
- runAt(Number(idx));
1308
- return;
1309
- }
1310
- for (let i = 0; i < list.length; i++)
1311
- runAt(i);
1312
- };
1313
- const refreshHostKeyed = (fields, onlyBindingId) => {
1314
- for (const field of fields) {
1315
- const next = inst[field];
1316
- const prev = hostPrev[field];
1317
- hostPrev[field] = next;
1318
- const todo = [];
1319
- if (prev !== undefined && prev !== null)
1320
- todo.push(prev);
1321
- if (next !== undefined && next !== null && next !== prev)
1322
- todo.push(next);
1323
- for (const k of todo) {
1324
- const entry = keyed.get(k);
1325
- if (!entry)
1326
- continue;
1327
- runEntryPatches(entry, field, onlyBindingId);
1328
- }
1329
- }
1330
- };
1331
- const ensureListDispatcher = (bId, leafDeps) => {
1332
- if (bId == null)
1333
- return;
1334
- const idKey = String(bId);
1335
- if (listDispatchers.has(idKey))
1336
- return;
1337
- const dispatch = () => {
1338
- if (inst.__vmzDestroyed)
1339
- return;
1340
- const trie = inst.__vmzFlushTrie;
1341
- const hostFields = [];
1342
- let listReplaced = false;
1343
- for (const d of leafDeps || []) {
1344
- if (!d)
1345
- continue;
1346
- if (d.includes('.*') || (d.includes('[') && d.includes(']'))) {
1347
- const root = depRootField(d);
1348
- if (trie && root && trie[root] && trie[root].replace)
1349
- listReplaced = true;
1350
- continue;
1351
- }
1352
- hostFields.push(depRootField(d) || d);
1353
- }
1354
- const hostDirty = !!trie &&
1355
- hostFields.some((f) => {
1356
- const n = trie[f];
1357
- return n && (n.replace || n.dirty);
1358
- });
1359
- // Full list replace is owned by eachBlock apply() — skip leaf re-walk.
1360
- if (listReplaced && !hostDirty)
1361
- return;
1362
- if (hostDirty && hostFields.length) {
1363
- refreshHostKeyed(hostFields, bId);
1364
- return;
1365
- }
1366
- refreshByListIndex(bId, leafDeps, trie);
1367
- };
1368
- listDispatchers.set(idKey, dispatch);
1369
- registerBind(inst, leafDeps || [], dispatch, bId);
1370
- };
1371
- const ensureHostDispatcher = (field) => {
1372
- if (!field || hostDispatchers.has(field))
1373
- return;
1374
- hostDispatchers.add(field);
1375
- const dispatch = () => {
1376
- if (inst.__vmzDestroyed)
1377
- return;
1378
- refreshHostKeyed([field], null);
1379
- };
1380
- registerBind(inst, [field], dispatch, null);
1381
- };
1382
- const noteItemBind = (bId, bindDeps, fn) => {
1383
- fn.__vmzItemDeps = Array.isArray(bindDeps) ? bindDeps.slice() : [];
1384
- fn.__vmzBindingId = bId;
1385
- const leaf = fn.__vmzItemDeps;
1386
- if (bId != null) {
1387
- // One dispatcher per BindingId also covers bare host fields (e.g. selected).
1388
- ensureListDispatcher(bId, leaf);
1389
- return;
1390
- }
1391
- for (const d of leaf) {
1392
- if (!d)
1393
- continue;
1394
- if (d.includes('.*') || (d.includes('[') && d.includes(']')))
1395
- continue;
1396
- const root = depRootField(d) || d;
1397
- if (root && root.indexOf('.') < 0)
1398
- ensureHostDispatcher(root);
1399
- }
1400
- };
1401
- const teardownDelegate = () => {
1402
- if (!delegateRoot)
1403
- return;
1404
- for (const type of Object.keys(delegateListeners)) {
1405
- delegateRoot.removeEventListener(type, delegateListeners[type]);
1406
- delete delegateListeners[type];
1407
- }
1408
- delegateRoot = null;
1409
- };
1410
- const ensureDelegateAttached = () => {
1411
- const parent = end.parentNode;
1412
- if (!parent || parent.nodeType !== 1)
1413
- return;
1414
- if (delegateRoot && delegateRoot !== parent)
1415
- teardownDelegate();
1416
- delegateRoot = /** @type {Element} */ (parent);
1417
- for (const type of delegateTypes) {
1418
- if (delegateListeners[type])
1419
- continue;
1420
- const listener = (ev) => {
1421
- if (type === 'submit' && ev && typeof ev.preventDefault === 'function') {
1422
- ev.preventDefault();
1423
- }
1424
- let n = /** @type {Node | null} */ (ev.target);
1425
- while (n && n !== delegateRoot) {
1426
- if (n.nodeType === 1) {
1427
- const el = /** @type {Element} */ (n);
1428
- const act = el.__vmzAct || el.getAttribute('data-vmz-act');
1429
- if (typeof act === 'string' && act) {
1430
- actionHandler(act).call(inst, ev, el);
1431
- return;
1432
- }
1433
- const bag = el.__vmzEvt;
1434
- if (bag && typeof bag[type] === 'function') {
1435
- // Pass the element so shared each-item handlers can read __vmzBox.
1436
- bag[type].call(inst, ev, el);
1437
- return;
1438
- }
1439
- }
1440
- n = n.parentNode;
1441
- }
1442
- };
1443
- delegateListeners[type] = listener;
1444
- delegateRoot.addEventListener(type, listener);
1445
- }
1446
- };
1447
- const needDelegate = (type) => {
1448
- if (!type)
1449
- return;
1450
- delegateTypes.add(type);
1451
- ensureDelegateAttached();
1452
- };
1453
- const eachCtx = { noteItemBind, needDelegate };
1454
- const clearDomEvt = (root) => {
1455
- if (!root || root.nodeType !== 1)
1456
- return;
1457
- const walk = (node) => {
1458
- if (node.nodeType === 1) {
1459
- if (node.__vmzEvt)
1460
- node.__vmzEvt = null;
1461
- if (node.__vmzBox)
1462
- node.__vmzBox = null;
1463
- if (node.__vmzAct)
1464
- node.__vmzAct = null;
1465
- if (node.__vmzKey != null)
1466
- node.__vmzKey = null;
1467
- for (let c = node.firstChild; c; c = c.nextSibling)
1468
- walk(c);
1469
- }
1470
- };
1471
- walk(root);
1472
- };
1473
- const pathFromRoot = (root, node) => {
1474
- /** @type {number[]} */
1475
- const path = [];
1476
- let n = /** @type {Node | null} */ (node);
1477
- while (n && n !== root) {
1478
- const parent = n.parentNode;
1479
- if (!parent)
1480
- return null;
1481
- let i = 0;
1482
- for (let c = parent.firstChild; c; c = c.nextSibling) {
1483
- if (c === n)
1484
- break;
1485
- i++;
1486
- }
1487
- path.push(i);
1488
- n = parent;
1489
- }
1490
- if (n !== root)
1491
- return null;
1492
- path.reverse();
1493
- return path;
1494
- };
1495
- const nodeAtPath = (root, path) => {
1496
- let n = /** @type {Node | null} */ (root);
1497
- for (let i = 0; i < path.length; i++) {
1498
- if (!n)
1499
- return null;
1500
- n = n.childNodes[path[i]] || null;
1501
- }
1502
- return n;
1503
- };
1504
- /**
1505
- * Shared each-item event handlers (one per method name for the whole block).
1506
- * Element carries `__vmzBox`; delegate passes the element as 2nd arg.
1507
- * @type {Record<string, (ev: Event, el: Element) => void>}
1508
- */
1509
- const sharedActions = Object.create(null);
1510
- /** @type {Record<string, string>} method → item field for action arg (fallback blueprint). */
1511
- const actionArgFields = Object.create(null);
1512
- const actionHandler = (method) => {
1513
- if (!sharedActions[method]) {
1514
- sharedActions[method] = function (ev, el) {
1515
- let n = /** @type {Node | null} */ (el);
1516
- while (n && n.nodeType === 1) {
1517
- const box = /** @type {Element} */ (n).__vmzBox;
1518
- if (box) {
1519
- const item = box.item != null ? box.item : box;
1520
- const argField = rowActArgField != null ? rowActArgField : actionArgFields[method] != null ? actionArgFields[method] : null;
1521
- if (argField == null || item == null)
1522
- return;
1523
- const arg = item[argField];
1524
- const fn = this[method];
1525
- if (typeof fn === 'function')
1526
- fn.call(this, arg);
1527
- return;
1528
- }
1529
- n = n.parentNode;
1530
- }
1531
- };
1532
- }
1533
- return sharedActions[method];
1534
- };
1535
- /**
1536
- * @returns {{ method: string, argField: string } | null}
1537
- */
1538
- const parseActionMethod = (handler) => {
1539
- if (typeof handler !== 'function')
1540
- return null;
1541
- try {
1542
- const src = Function.prototype.toString.call(handler);
1543
- // this.m(box.item.<field>) — field from author surface.
1544
- const m = src.match(/this\.([A-Za-z_$][\w$]*)\s*\(\s*[A-Za-z_$][\w$]*\.item\.([A-Za-z_$][\w$]*)\s*\)/);
1545
- return m ? { method: m[1], argField: m[2] } : null;
1546
- }
1547
- catch {
1548
- return null;
1549
- }
1550
- };
1551
- /**
1552
- * Row blueprint: first createItem records dynamic slots; later rows clone + hydrate
1553
- * without re-running compiled createItem (avoids per-row get/CF/on closures).
1554
- * @type {null | {
1555
- * tpl: Element,
1556
- * texts: Array<{ path: number[], bindingId: any, deps: string[], field: string, get: (root: Element) => Node }>,
1557
- * attrs: Array<{ path: number[], bindingId: any, deps: string[], name: string, onVal: string, offVal: string, get: (root: Element) => Element }>,
1558
- * ons: Array<{ path: number[], type: string, method: string, get: (root: Element) => Element }>,
1559
- * bindIds: Set<string>,
1560
- * }}
1561
- */
1562
- let blueprint = null;
1563
- let blueprintOk = true;
1564
- /** @type {Set<string> | null} */
1565
- let blueprintBindIds = null;
1566
- /** @type {null | ((root: Element, entry: any) => void)} */
1567
- let hydrateBp = null;
1568
- /** @type {null | ((entry: any) => void)} */
1569
- let applyBp = null;
1570
- /**
1571
- * Compile-time rowKernel installed — static HTML rows, no nested component dispose.
1572
- * Shape-specific walks live in emitted hydrate/apply (Rust), not here.
1573
- */
1574
- let hasRowKernel = false;
1575
- /** @type {string | null} item field used as key when rowKernel.keyField is set */
1576
- let rowKeyField = null;
1577
- /** @type {string | null} item field passed to delegated actions (from rowKernel.actArgField) */
1578
- let rowActArgField = null;
1579
- /** Recycle object bp entries (runtime-recorded blueprint fallback). */
1580
- /** @type {any[]} */
1581
- const entryPool = [];
1582
- const allocBpEntry = () => {
1583
- const e = entryPool.pop();
1584
- if (e)
1585
- return e;
1586
- return { item: null, dom: null, bp: 1, t0: null, t1: null };
1587
- };
1588
- const releaseBpEntry = (entry) => {
1589
- if (!entry)
1590
- return;
1591
- // DOM-as-entry (Element): drop expandos.
1592
- if (entry.nodeType === 1) {
1593
- entry.__vmzBox = null;
1594
- entry.__vmzT0 = null;
1595
- entry.__vmzT1 = null;
1596
- entry.__vmzBp = null;
1597
- return;
1598
- }
1599
- if (!entry.bp || entryPool.length >= 4096)
1600
- return;
1601
- entry.item = null;
1602
- entry.dom = null;
1603
- entry.t0 = null;
1604
- entry.t1 = null;
1605
- entry.a0 = null;
1606
- entry.patches = null;
1607
- entryPool.push(entry);
1608
- };
1609
- const entryDom = (entry) => (entry && entry.nodeType === 1 ? entry : entry && entry.dom);
1610
- const entryIsBp = (entry) => !!(entry && (entry.nodeType === 1 || entry.bp || entry.__vmzBp));
1611
- const entryItem = (entry) => {
1612
- if (!entry)
1613
- return null;
1614
- if (entry.nodeType === 1)
1615
- return entry.__vmzBox;
1616
- if (entry.bp)
1617
- return entry.item;
1618
- return entry.box && entry.box.item;
1619
- };
1620
- const rowKeyOf = (item, index) => {
1621
- if (rowKeyField != null && item != null)
1622
- return item[rowKeyField];
1623
- return keyOf(item, index);
1624
- };
1625
- /** Drop all row DOM between markers; rowKernel rows skip per-node dispose walks. */
1626
- const fastWipeRows = () => {
1627
- const parent = end.parentNode;
1628
- if (!parent) {
1629
- keyed.clear();
1630
- return;
1631
- }
1632
- let node = start.nextSibling;
1633
- if (node && node !== end) {
1634
- if (hasRowKernel || (blueprint && blueprintOk)) {
1635
- const range = document.createRange();
1636
- range.setStartBefore(node);
1637
- range.setEndBefore(end);
1638
- range.deleteContents();
1639
- }
1640
- else {
1641
- while (node && node !== end) {
1642
- const next = node.nextSibling;
1643
- noteDomRemove();
1644
- clearDomEvt(node);
1645
- disposeDomTree(node);
1646
- node.remove();
1647
- node = next;
1648
- }
1649
- }
1650
- }
1651
- for (const [, entry] of keyed)
1652
- releaseBpEntry(entry);
1653
- keyed.clear();
1654
- };
1655
- const makeChildGetter = (path) => {
1656
- const len = path.length;
1657
- if (len === 0)
1658
- return (root) => root;
1659
- if (len === 1) {
1660
- const a = path[0];
1661
- return (root) => root.childNodes[a];
1662
- }
1663
- if (len === 2) {
1664
- const a = path[0];
1665
- const b = path[1];
1666
- return (root) => root.childNodes[a].childNodes[b];
1667
- }
1668
- if (len === 3) {
1669
- const a = path[0];
1670
- const b = path[1];
1671
- const c = path[2];
1672
- return (root) => root.childNodes[a].childNodes[b].childNodes[c];
1673
- }
1674
- return (root) => {
1675
- let n = /** @type {Node} */ (root);
1676
- for (let i = 0; i < len; i++)
1677
- n = n.childNodes[path[i]];
1678
- return n;
1679
- };
1680
- };
1681
- const userCreateItem = spec.createItem;
1682
- // Compile-time row kernel (Rust Direct emit) — skip runtime blueprint recording.
1683
- if (spec.rowKernel && typeof spec.rowKernel.html === 'string' && typeof spec.rowKernel.hydrate === 'function') {
1684
- try {
1685
- const tplHost = document.createElement('template');
1686
- tplHost.innerHTML = spec.rowKernel.html;
1687
- const row = tplHost.content.firstElementChild;
1688
- if (row && row.nodeType === 1) {
1689
- blueprint = {
1690
- tpl: /** @type {Element} */ (row.cloneNode(true)),
1691
- texts: [],
1692
- attrs: [],
1693
- ons: [],
1694
- bindIds: new Set(),
1695
- };
1696
- blueprintOk = true;
1697
- hasRowKernel = true;
1698
- rowKeyField = typeof spec.rowKernel.keyField === 'string' && spec.rowKernel.keyField ? spec.rowKernel.keyField : null;
1699
- rowActArgField =
1700
- typeof spec.rowKernel.actArgField === 'string' && spec.rowKernel.actArgField ? spec.rowKernel.actArgField : null;
1701
- blueprintBindIds = new Set(['__vmzRk']);
1702
- for (const ev of spec.rowKernel.events || [])
1703
- needDelegate(ev);
1704
- for (const hf of spec.rowKernel.hostFields || []) {
1705
- if (typeof hf === 'string' && hf)
1706
- ensureHostDispatcher(hf);
1707
- }
1708
- // Leaf path writes (`rows.0.label`) need `rows.*.label`, not bare `rows.*`.
1709
- {
1710
- const listRoot = depRootField((deps && deps[0]) || '') || (deps && deps[0]) || '';
1711
- if (listRoot) {
1712
- /** @type {string[]} */
1713
- const leafDeps = [`${listRoot}.*`];
1714
- const fields = Array.isArray(spec.rowKernel.itemFields) ? spec.rowKernel.itemFields : [];
1715
- for (const f of fields) {
1716
- if (typeof f === 'string' && f)
1717
- leafDeps.push(`${listRoot}.*.${f}`);
1718
- }
1719
- ensureListDispatcher('__vmzRk', leafDeps);
1720
- }
1721
- }
1722
- const rkHydrate = spec.rowKernel.hydrate;
1723
- const rkApply = spec.rowKernel.apply;
1724
- hydrateBp = (root, entry) => {
1725
- const item = entry && typeof entry === 'object' && 'item' in entry && entry.item != null
1726
- ? entry.item
1727
- : entry && entry.__vmzBox != null
1728
- ? entry.__vmzBox
1729
- : entry;
1730
- rkHydrate.call(inst, root, item);
1731
- };
1732
- applyBp = (entry) => {
1733
- const root = entry && entry.nodeType === 1 ? entry : entry.dom;
1734
- const item = entry && entry.nodeType === 1 ? entry.__vmzBox : entry.item;
1735
- if (typeof rkApply === 'function')
1736
- rkApply.call(inst, root, item);
1737
- };
1738
- }
1739
- }
1740
- catch (err) {
1741
- console.error('vmz:dom rowKernel', err);
1742
- blueprint = null;
1743
- blueprintOk = true;
1744
- hasRowKernel = false;
1745
- rowKeyField = null;
1746
- rowActArgField = null;
1747
- hydrateBp = null;
1748
- applyBp = null;
1749
- }
1750
- }
1751
- const probeItemField = (get, box) => {
1752
- const item = box.item;
1753
- if (!item || (typeof item !== 'object' && typeof item !== 'function'))
1754
- return null;
1755
- let field = null;
1756
- const proxy = new Proxy(item, {
1757
- get(t, p, r) {
1758
- if (typeof p === 'string' || typeof p === 'symbol')
1759
- field = String(p);
1760
- return Reflect.get(t, p, r);
1761
- },
1762
- });
1763
- const prev = box.item;
1764
- box.item = proxy;
1765
- try {
1766
- get.call(inst);
1767
- }
1768
- catch {
1769
- /* ignore */
1770
- }
1771
- box.item = prev;
1772
- return field;
1773
- };
1774
- /**
1775
- * Probe on/off class strings for `this.<host> === item.<itemField> ? … : …`.
1776
- * Host/item field names come from binding deps — not hardcoded.
1777
- */
1778
- const probeHostItemClass = (get, box, hostField, itemField) => {
1779
- if (!hostField || !itemField)
1780
- return { onVal: '', offVal: '' };
1781
- const prev = inst[hostField];
1782
- const matchVal = box.item != null ? box.item[itemField] : undefined;
1783
- let onVal = '';
1784
- let offVal = '';
1785
- const quiet = !!inst.__vmzQuiet;
1786
- inst.__vmzQuiet = true;
1787
- try {
1788
- inst[hostField] = matchVal;
1789
- onVal = String(get.call(inst) ?? '');
1790
- // Distinct off value for number / other keys.
1791
- if (typeof matchVal === 'number') {
1792
- inst[hostField] = matchVal === 0 ? -1 : 0;
1793
- if (inst[hostField] === matchVal)
1794
- inst[hostField] = undefined;
1795
- }
1796
- else {
1797
- inst[hostField] = matchVal === '' ? '__vmz_off__' : '';
1798
- if (inst[hostField] === matchVal)
1799
- inst[hostField] = undefined;
1800
- }
1801
- offVal = String(get.call(inst) ?? '');
1802
- }
1803
- catch {
1804
- onVal = '';
1805
- offVal = '';
1806
- }
1807
- finally {
1808
- inst[hostField] = prev;
1809
- inst.__vmzQuiet = quiet;
1810
- }
1811
- return { onVal, offVal };
1812
- };
1813
- const sealBlueprintDispatchers = () => {
1814
- if (!blueprint || blueprintBindIds)
1815
- return;
1816
- /** @type {Set<string>} */
1817
- const ids = new Set();
1818
- for (const s of blueprint.texts) {
1819
- if (s.bindingId != null) {
1820
- ids.add(String(s.bindingId));
1821
- ensureListDispatcher(s.bindingId, s.deps);
1822
- }
1823
- }
1824
- for (const s of blueprint.attrs) {
1825
- if (s.bindingId != null) {
1826
- ids.add(String(s.bindingId));
1827
- ensureListDispatcher(s.bindingId, s.deps);
1828
- }
1829
- for (const d of s.deps || []) {
1830
- if (!d || d.includes('.*') || (d.includes('[') && d.includes(']')))
1831
- continue;
1832
- const rootField = depRootField(d) || d;
1833
- if (rootField && rootField.indexOf('.') < 0)
1834
- ensureHostDispatcher(rootField);
1835
- }
1836
- }
1837
- for (const s of blueprint.ons) {
1838
- needDelegate(s.type);
1839
- }
1840
- blueprintBindIds = ids;
1841
- blueprint.bindIds = ids;
1842
- compileBlueprintKernels();
1843
- };
1844
- const compileBlueprintKernels = () => {
1845
- if (!blueprint || hydrateBp)
1846
- return;
1847
- const textSlots = blueprint.texts;
1848
- const attrSlots = blueprint.attrs;
1849
- const onSlots = blueprint.ons;
1850
- const nText = textSlots.length;
1851
- const nAttr = attrSlots.length;
1852
- const nOn = onSlots.length;
1853
- // Fallback only (no compile-time rowKernel). Field/path walks come from
1854
- // recorded slots — shape-specific kernels belong in row_kernel.rs.
1855
- hydrateBp = (root, entry) => {
1856
- const item = entry && entry.item != null ? entry.item : entry;
1857
- root.__vmzBox = item;
1858
- /** @type {Array<Text>} */
1859
- const textNodes = new Array(nText);
1860
- /** @type {Array<Element>} */
1861
- const attrEls = new Array(nAttr);
1862
- for (let i = 0; i < nText; i++)
1863
- textNodes[i] = /** @type {Text} */ (textSlots[i].get(root));
1864
- for (let i = 0; i < nAttr; i++)
1865
- attrEls[i] = attrSlots[i].get(root);
1866
- for (let i = 0; i < nOn; i++) {
1867
- const el = onSlots[i].get(root);
1868
- if (!el.getAttribute('data-vmz-act')) {
1869
- el.setAttribute('data-vmz-act', onSlots[i].method);
1870
- el.__vmzAct = onSlots[i].method;
1871
- }
1872
- }
1873
- for (let i = 0; i < nText; i++) {
1874
- const v = item == null ? '' : item[textSlots[i].field];
1875
- textNodes[i].nodeValue = v == null ? '' : v + '';
1876
- }
1877
- for (let i = 0; i < nAttr; i++) {
1878
- const s = attrSlots[i];
1879
- const el = attrEls[i];
1880
- const host = s.hostField;
1881
- const itemKey = s.itemField;
1882
- if (!host || !itemKey)
1883
- continue;
1884
- const hv = inst[host];
1885
- if (hv != null && item && hv === item[itemKey]) {
1886
- if (s.name === 'class' || s.name === 'className')
1887
- el.className = s.onVal;
1888
- else
1889
- applyDomAttr(el, s.name, s.onVal);
1890
- }
1891
- else if (s.offVal) {
1892
- if (s.name === 'class' || s.name === 'className')
1893
- el.className = s.offVal;
1894
- else
1895
- applyDomAttr(el, s.name, s.offVal);
1896
- }
1897
- }
1898
- entry.tn = textNodes;
1899
- entry.ae = attrEls;
1900
- entry.dom = root;
1901
- entry.bp = true;
1902
- };
1903
- applyBp = (entry) => {
1904
- const item = entry.item != null ? entry.item : entry.__vmzBox;
1905
- const textNodes = entry.tn;
1906
- const attrEls = entry.ae;
1907
- for (let i = 0; i < nText; i++) {
1908
- const v = item == null ? '' : item[textSlots[i].field];
1909
- textNodes[i].nodeValue = v == null ? '' : v + '';
1910
- }
1911
- for (let i = 0; i < nAttr; i++) {
1912
- const s = attrSlots[i];
1913
- const el = attrEls[i];
1914
- if (!s.hostField || !s.itemField)
1915
- continue;
1916
- const raw = item && inst[s.hostField] === item[s.itemField] ? s.onVal : s.offVal;
1917
- if (s.name === 'class' || s.name === 'className')
1918
- el.className = raw;
1919
- else
1920
- applyDomAttr(el, s.name, raw);
1921
- }
1922
- };
1923
- };
1924
- const wireBlueprintItem = (root, box, patches) => {
1925
- if (!blueprint)
1926
- return null;
1927
- sealBlueprintDispatchers();
1928
- const entry = {
1929
- item: box.item,
1930
- index: box.index,
1931
- dom: root,
1932
- bp: true,
1933
- t0: null,
1934
- t1: null,
1935
- a0: null,
1936
- tn: null,
1937
- ae: null,
1938
- patches: patches || null,
1939
- };
1940
- hydrateBp(root, entry);
1941
- if (patches) {
1942
- const applyAll = () => applyBp(entry);
1943
- applyAll.__vmzBindingIds = blueprintBindIds;
1944
- applyAll.__vmzBindingId = null;
1945
- applyAll.__vmzItemLocal = true;
1946
- applyAll.__vmzBpEntry = entry;
1947
- patches.push(applyAll);
1948
- }
1949
- return entry;
1950
- };
1951
- const recordFirstItem = (api, box, patches) => {
1952
- /** @type {Element | null} */
1953
- let root = null;
1954
- /** Pending slots keep live node refs — Direct emit binds before appendChild. */
1955
- /** @type {{ texts: any[], attrs: any[], ons: any[] }} */
1956
- const pending = { texts: [], attrs: [], ons: [] };
1957
- let recordFailed = false;
1958
- const recordingApi = Object.assign({}, api, {
1959
- el(tag) {
1960
- const el = api.el(tag);
1961
- if (!root)
1962
- root = el;
1963
- return el;
1964
- },
1965
- // Capture only — do not wireDirectBind (would orphan first-row binders).
1966
- bindText(i, bindingId, deps, get, textNode, cf) {
1967
- if (!root)
1968
- return;
1969
- // Blueprint recording aborted: fall back to normal wiring for remaining binds.
1970
- if (recordFailed) {
1971
- api.bindText(i, bindingId, deps, get, textNode, cf);
1972
- return;
1973
- }
1974
- try {
1975
- const raw = get.call(inst);
1976
- if (textNode.nodeType === 3) /** @type {Text} */
1977
- (textNode).nodeValue = String(raw ?? '');
1978
- else
1979
- textNode.textContent = String(raw ?? '');
1980
- }
1981
- catch {
1982
- /* ignore */
1983
- }
1984
- pending.texts.push({
1985
- node: textNode,
1986
- bindingId,
1987
- deps: Array.isArray(deps) ? deps.slice() : [],
1988
- getFn: get,
1989
- });
1990
- },
1991
- bindAttr(i, bindingId, deps, get, el, name, cf) {
1992
- if (!root)
1993
- return;
1994
- if (recordFailed) {
1995
- api.bindAttr(i, bindingId, deps, get, el, name, cf);
1996
- return;
1997
- }
1998
- // Class bind eligible when deps include a bare host field (any name).
1999
- const hasHostDep = (deps || []).some((d) => d && !d.includes('.*') && !d.includes('[') && String(d).indexOf('.') < 0);
2000
- if ((name === 'class' || name === 'className') && hasHostDep) {
2001
- try {
2002
- const raw = get.call(inst);
2003
- el.className = raw == null ? '' : String(raw);
2004
- }
2005
- catch {
2006
- /* ignore */
2007
- }
2008
- pending.attrs.push({
2009
- node: el,
2010
- bindingId,
2011
- deps: Array.isArray(deps) ? deps.slice() : [],
2012
- name,
2013
- getFn: get,
2014
- });
2015
- }
2016
- else {
2017
- recordFailed = true;
2018
- api.bindAttr(i, bindingId, deps, get, el, name, cf);
2019
- }
2020
- },
2021
- on(el, type, handler) {
2022
- if (recordFailed) {
2023
- api.on(el, type, handler);
2024
- return;
2025
- }
2026
- const parsed = parseActionMethod(handler);
2027
- if (parsed && root) {
2028
- pending.ons.push({
2029
- node: el,
2030
- type,
2031
- method: parsed.method,
2032
- argField: parsed.argField,
2033
- });
2034
- actionArgFields[parsed.method] = parsed.argField;
2035
- if (rowActArgField == null)
2036
- rowActArgField = parsed.argField;
2037
- root.__vmzBox = box;
2038
- el.__vmzAct = parsed.method;
2039
- // Attribute survives cloneNode — hydrate skips per-row __vmzAct writes.
2040
- el.setAttribute('data-vmz-act', parsed.method);
2041
- needDelegate(type);
2042
- return;
2043
- }
2044
- api.on(el, type, handler);
2045
- recordFailed = true;
2046
- },
2047
- });
2048
- const dom = userCreateItem.call(inst, recordingApi, box);
2049
- if (!recordFailed && dom && dom.nodeType === 1 && root === dom && (pending.texts.length > 0 || pending.attrs.length > 0)) {
2050
- /** @type {any[]} */
2051
- const texts = [];
2052
- /** @type {any[]} */
2053
- const attrs = [];
2054
- /** @type {any[]} */
2055
- const ons = [];
2056
- for (const p of pending.texts) {
2057
- const path = pathFromRoot(root, p.node);
2058
- const field = probeItemField(p.getFn, box);
2059
- if (!path || !field) {
2060
- recordFailed = true;
2061
- break;
2062
- }
2063
- texts.push({
2064
- path,
2065
- bindingId: p.bindingId,
2066
- deps: p.deps,
2067
- field,
2068
- get: makeChildGetter(path),
2069
- });
2070
- }
2071
- if (!recordFailed) {
2072
- for (const p of pending.attrs) {
2073
- const path = pathFromRoot(root, p.node);
2074
- if (!path) {
2075
- recordFailed = true;
2076
- break;
2077
- }
2078
- const hostField = (() => {
2079
- for (const d of p.deps || []) {
2080
- if (!d)
2081
- continue;
2082
- if (d.includes('.*') || d.includes('['))
2083
- continue;
2084
- if (String(d).indexOf('.') < 0)
2085
- return String(d);
2086
- }
2087
- return null;
2088
- })();
2089
- let itemField = null;
2090
- for (const d of p.deps || []) {
2091
- if (!d)
2092
- continue;
2093
- if (d.includes('.*')) {
2094
- const m = String(d).match(/\*\.([A-Za-z_$][\w$]*)$/);
2095
- if (m)
2096
- itemField = m[1];
2097
- }
2098
- }
2099
- if (!hostField || !itemField) {
2100
- recordFailed = true;
2101
- break;
2102
- }
2103
- const { onVal, offVal } = probeHostItemClass(p.getFn, box, hostField, itemField);
2104
- attrs.push({
2105
- path,
2106
- bindingId: p.bindingId,
2107
- deps: p.deps,
2108
- name: p.name,
2109
- onVal,
2110
- offVal,
2111
- hostField,
2112
- itemField,
2113
- get: makeChildGetter(path),
2114
- });
2115
- }
2116
- }
2117
- if (!recordFailed) {
2118
- for (const p of pending.ons) {
2119
- const path = pathFromRoot(root, p.node);
2120
- if (!path) {
2121
- recordFailed = true;
2122
- break;
2123
- }
2124
- ons.push({
2125
- path,
2126
- type: p.type,
2127
- method: p.method,
2128
- get: makeChildGetter(path),
2129
- });
2130
- }
2131
- }
2132
- if (!recordFailed) {
2133
- const tpl = /** @type {Element} */ (dom.cloneNode(true));
2134
- clearDomEvt(tpl);
2135
- blueprint = {
2136
- tpl,
2137
- texts,
2138
- attrs,
2139
- ons,
2140
- bindIds: new Set(),
2141
- };
2142
- }
2143
- }
2144
- if (!blueprint)
2145
- blueprintOk = false;
2146
- return dom;
2147
- };
2148
- const createItem = (api, box, patches) => {
2149
- if (blueprint && blueprintOk) {
2150
- const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
2151
- wireBlueprintItem(root, box, patches);
2152
- return root;
2153
- }
2154
- if (blueprintOk && typeof userCreateItem === 'function') {
2155
- const dom = recordFirstItem(api, box, patches);
2156
- if (blueprint && dom && dom.nodeType === 1) {
2157
- patches.length = 0;
2158
- wireBlueprintItem(dom, box, patches);
2159
- }
2160
- return dom;
2161
- }
2162
- return userCreateItem.call(inst, api, box);
2163
- };
2164
- /**
2165
- * Reorder / place item DOM with minimal mutations.
2166
- * - already-correct → no-op
2167
- * - pure 2-node swap → 1–2 insertBefore (common keyed list swap)
2168
- * - append prefix → Fragment insert only new tail
2169
- * - create / replace / complex → Fragment rebuild before `end`
2170
- */
2171
- const reconcileDomOrder = (nextNodes) => {
2172
- const parent = end.parentNode;
2173
- if (!parent)
2174
- return;
2175
- /** @type {ChildNode[]} */
2176
- const curr = [];
2177
- for (let n = start.nextSibling; n && n !== end; n = n.nextSibling) {
2178
- curr.push(n);
2179
- }
2180
- if (curr.length === nextNodes.length) {
2181
- let same = true;
2182
- for (let i = 0; i < curr.length; i++) {
2183
- if (curr[i] !== nextNodes[i]) {
2184
- same = false;
2185
- break;
2186
- }
2187
- }
2188
- if (same)
2189
- return;
2190
- // Fast path: exactly two positions swapped (benchmark swaprows).
2191
- /** @type {number[]} */
2192
- const diff = [];
2193
- for (let i = 0; i < curr.length; i++) {
2194
- if (curr[i] !== nextNodes[i])
2195
- diff.push(i);
2196
- }
2197
- if (diff.length === 2 && curr[diff[0]] === nextNodes[diff[1]] && curr[diff[1]] === nextNodes[diff[0]]) {
2198
- const a = curr[diff[0]];
2199
- const b = curr[diff[1]];
2200
- const aNext = a.nextSibling;
2201
- const bNext = b.nextSibling;
2202
- noteDomMove();
2203
- noteDomMove();
2204
- if (aNext === b) {
2205
- parent.insertBefore(b, a);
2206
- }
2207
- else if (bNext === a) {
2208
- parent.insertBefore(a, b);
2209
- }
2210
- else {
2211
- parent.insertBefore(b, aNext);
2212
- parent.insertBefore(a, bNext);
2213
- }
2214
- return;
2215
- }
2216
- }
2217
- // Append-only: existing live prefix unchanged, only new tail detached.
2218
- if (curr.length < nextNodes.length && curr.length > 0) {
2219
- let prefix = true;
2220
- for (let i = 0; i < curr.length; i++) {
2221
- if (curr[i] !== nextNodes[i]) {
2222
- prefix = false;
2223
- break;
2224
- }
2225
- }
2226
- if (prefix) {
2227
- const batch = document.createDocumentFragment();
2228
- for (let i = curr.length; i < nextNodes.length; i++) {
2229
- batch.appendChild(nextNodes[i]);
2230
- }
2231
- parent.insertBefore(batch, end);
2232
- return;
2233
- }
2234
- }
2235
- // Create / replace / complex reorder: one Fragment write.
2236
- const batch = document.createDocumentFragment();
2237
- for (const dom of nextNodes) {
2238
- if (dom.parentNode)
2239
- noteDomMove();
2240
- batch.appendChild(dom);
2241
- }
2242
- parent.insertBefore(batch, end);
2243
- };
2244
- const apply = () => {
2245
- if (inst.__vmzDestroyed)
2246
- return;
2247
- const applied = ++gen;
2248
- const list = readList();
2249
- const n = list.length;
2250
- // Clear all rows.
2251
- if (n === 0) {
2252
- if (keyed.size)
2253
- fastWipeRows();
2254
- return;
2255
- }
2256
- // Full replace (no key reuse): wipe then fall into fresh create.
2257
- if (keyed.size > 0 && hasRowKernel) {
2258
- let reuse = false;
2259
- for (let i = 0; i < n; i++) {
2260
- if (keyed.has(rowKeyOf(list[i], i))) {
2261
- reuse = true;
2262
- break;
2263
- }
2264
- }
2265
- if (!reuse)
2266
- fastWipeRows();
2267
- }
2268
- // Fresh create into empty each: record blueprint once, then clone-only.
2269
- if (keyed.size === 0 && n > 0) {
2270
- const parent = end.parentNode;
2271
- const batch = document.createDocumentFragment();
2272
- let startIdx = 0;
2273
- if (!blueprint || !blueprintOk) {
2274
- const box0 = { item: list[0], index: 0 };
2275
- const patches0 = [];
2276
- const prevPatches = directApi._itemPatches;
2277
- const prevCtx = directApi._eachCtx;
2278
- directApi._itemPatches = patches0;
2279
- directApi._eachCtx = eachCtx;
2280
- let dom0 = null;
2281
- try {
2282
- dom0 = createItem(directApi, box0, patches0);
2283
- }
2284
- finally {
2285
- directApi._itemPatches = prevPatches;
2286
- directApi._eachCtx = prevCtx;
2287
- }
2288
- if (applied !== gen || inst.__vmzDestroyed)
2289
- return;
2290
- if (!dom0)
2291
- return;
2292
- const k0 = itemKey(box0);
2293
- if (dom0.nodeType === 1) /** @type {Element} */
2294
- (dom0).__vmzKey = k0;
2295
- // First row may already be blueprint-wired (patches cleared + hydrate).
2296
- let entry0 = keyed.get(k0);
2297
- if (!entry0) {
2298
- if (patches0.length && patches0[0] && patches0[0].__vmzBpEntry) {
2299
- entry0 = patches0[0].__vmzBpEntry;
2300
- entry0.patches = patches0;
2301
- }
2302
- else if (blueprint && blueprintOk) {
2303
- entry0 = wireBlueprintItem(/** @type {Element} */ (dom0), box0, patches0);
2304
- }
2305
- else {
2306
- tagItemPatches(patches0, 0);
2307
- entry0 = { box: box0, dom: dom0, patches: patches0 };
2308
- }
2309
- keyed.set(k0, entry0);
2310
- }
2311
- batch.appendChild(dom0);
2312
- startIdx = 1;
2313
- }
2314
- if (blueprint && blueprintOk) {
2315
- sealBlueprintDispatchers();
2316
- const tpl = blueprint.tpl;
2317
- if (hasRowKernel && spec.rowKernel && typeof spec.rowKernel.create === 'function') {
2318
- // Shape-specific create loop is Rust-emitted (rowKernel.create).
2319
- // Direct parent.insertBefore (no Fragment). When parent has only the
2320
- // each markers as children, detach parent for the fill then reattach —
2321
- // same structural trick as hand-tuned keyed apps (not app-specific).
2322
- if (parent) {
2323
- let detached = null;
2324
- let reinsertAt = null;
2325
- if (parent.nodeType === 1 && parent.parentNode) {
2326
- let onlyMarkers = true;
2327
- for (let c = parent.firstChild; c; c = c.nextSibling) {
2328
- if (c !== start && c !== end) {
2329
- onlyMarkers = false;
2330
- break;
2331
- }
2332
- }
2333
- if (onlyMarkers) {
2334
- detached = parent.parentNode;
2335
- reinsertAt = parent.nextSibling;
2336
- detached.removeChild(parent);
2337
- }
2338
- }
2339
- spec.rowKernel.create.call(inst, list, startIdx, tpl, keyed, parent, end, rowKeyOf);
2340
- if (detached)
2341
- detached.insertBefore(parent, reinsertAt);
2342
- }
2343
- else {
2344
- const hydrate = spec.rowKernel.hydrate;
2345
- for (let i = startIdx; i < n; i++) {
2346
- if (applied !== gen || inst.__vmzDestroyed)
2347
- return;
2348
- const item = list[i];
2349
- const root = /** @type {Element} */ (tpl.cloneNode(true));
2350
- hydrate.call(inst, root, item);
2351
- const k = rowKeyOf(item, i);
2352
- root.__vmzKey = k;
2353
- keyed.set(k, root);
2354
- batch.appendChild(root);
2355
- }
2356
- }
2357
- }
2358
- else if (hasRowKernel && hydrateBp) {
2359
- const hydrate = spec.rowKernel.hydrate;
2360
- for (let i = startIdx; i < n; i++) {
2361
- if (applied !== gen || inst.__vmzDestroyed)
2362
- return;
2363
- const item = list[i];
2364
- const root = /** @type {Element} */ (tpl.cloneNode(true));
2365
- hydrate.call(inst, root, item);
2366
- const k = rowKeyOf(item, i);
2367
- root.__vmzKey = k;
2368
- keyed.set(k, root);
2369
- batch.appendChild(root);
2370
- }
2371
- }
2372
- else {
2373
- for (let i = startIdx; i < n; i++) {
2374
- if (applied !== gen || inst.__vmzDestroyed)
2375
- return;
2376
- const item = list[i];
2377
- const k = keyOf(item, i);
2378
- const root = /** @type {Element} */ (tpl.cloneNode(true));
2379
- const entry = {
2380
- item,
2381
- index: i,
2382
- dom: root,
2383
- bp: true,
2384
- t0: null,
2385
- t1: null,
2386
- a0: null,
2387
- patches: null,
2388
- };
2389
- hydrateBp(root, entry);
2390
- root.__vmzKey = k;
2391
- keyed.set(k, entry);
2392
- batch.appendChild(root);
2393
- }
2394
- }
2395
- }
2396
- else {
2397
- for (let i = startIdx; i < n; i++) {
2398
- if (applied !== gen || inst.__vmzDestroyed)
2399
- return;
2400
- const box = { item: list[i], index: i };
2401
- const k = itemKey(box);
2402
- const patches = [];
2403
- const prevPatches = directApi._itemPatches;
2404
- const prevCtx = directApi._eachCtx;
2405
- directApi._itemPatches = patches;
2406
- directApi._eachCtx = eachCtx;
2407
- let dom = null;
2408
- try {
2409
- dom = createItem(directApi, box, patches);
2410
- }
2411
- finally {
2412
- directApi._itemPatches = prevPatches;
2413
- directApi._eachCtx = prevCtx;
2414
- }
2415
- if (applied !== gen || inst.__vmzDestroyed)
2416
- return;
2417
- if (!dom)
2418
- continue;
2419
- tagItemPatches(patches, i);
2420
- if (dom.nodeType === 1) /** @type {Element} */
2421
- (dom).__vmzKey = k;
2422
- keyed.set(k, { box, dom, patches });
2423
- batch.appendChild(dom);
2424
- }
2425
- }
2426
- if (applied !== gen || inst.__vmzDestroyed)
2427
- return;
2428
- if (parent && batch.firstChild)
2429
- parent.insertBefore(batch, end);
2430
- if (end.isConnected)
2431
- ensureDelegateAttached();
2432
- else
2433
- queueMicrotask(() => {
2434
- if (!inst.__vmzDestroyed)
2435
- ensureDelegateAttached();
2436
- });
2437
- return;
2438
- }
2439
- const seen = new Set();
2440
- /** @type {Node[]} */
2441
- const nextNodes = [];
2442
- for (let i = 0; i < n; i++) {
2443
- if (applied !== gen || inst.__vmzDestroyed)
2444
- return;
2445
- const item = list[i];
2446
- const k = rowKeyOf(item, i);
2447
- seen.add(k);
2448
- let entry = keyed.get(k);
2449
- if (!entry) {
2450
- if (hasRowKernel && blueprint && blueprintOk && hydrateBp) {
2451
- const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
2452
- spec.rowKernel.hydrate.call(inst, root, item);
2453
- root.__vmzKey = k;
2454
- keyed.set(k, root);
2455
- entry = root;
2456
- }
2457
- else {
2458
- const box = { item, index: i };
2459
- const patches = [];
2460
- const prevPatches = directApi._itemPatches;
2461
- const prevCtx = directApi._eachCtx;
2462
- directApi._itemPatches = patches;
2463
- directApi._eachCtx = eachCtx;
2464
- let dom = null;
2465
- try {
2466
- dom = createItem(directApi, box, patches);
2467
- }
2468
- finally {
2469
- directApi._itemPatches = prevPatches;
2470
- directApi._eachCtx = prevCtx;
2471
- }
2472
- if (applied !== gen || inst.__vmzDestroyed)
2473
- return;
2474
- tagItemPatches(patches, i);
2475
- if (dom) {
2476
- if (dom.nodeType === 1) {
2477
- // Client identity: expando only (see 01 each identity). SSR uses data-vmz-key.
2478
- /** @type {Element} */ (dom).__vmzKey = k;
2479
- }
2480
- entry = { box, dom, patches };
2481
- keyed.set(k, entry);
2482
- }
2483
- }
2484
- }
2485
- else {
2486
- const sameItem = entryItem(entry) === item;
2487
- if (entry.nodeType === 1) {
2488
- entry.__vmzBox = item;
2489
- }
2490
- else if (entry.bp) {
2491
- entry.item = item;
2492
- entry.index = i;
2493
- if (entry.dom)
2494
- entry.dom.__vmzBox = entry.item;
2495
- }
2496
- else {
2497
- entry.box.item = item;
2498
- entry.box.index = i;
2499
- tagItemPatches(entry.patches, i);
2500
- }
2501
- // Pure reorder (swap / move) keeps object identity — skip leaf patches.
2502
- if (!sameItem) {
2503
- if (entryIsBp(entry) && applyBp) {
2504
- try {
2505
- applyBp(entry);
2506
- }
2507
- catch (err) {
2508
- console.error('vmz:dom each item', err);
2509
- }
2510
- }
2511
- else if (entry.patches) {
2512
- for (const p of entry.patches)
2513
- runPatch(p, null);
2514
- }
2515
- }
2516
- }
2517
- if (entry)
2518
- nextNodes.push(entryDom(entry));
2519
- }
2520
- if (applied !== gen || inst.__vmzDestroyed)
2521
- return;
2522
- for (const [k, entry] of [...keyed.entries()]) {
2523
- if (seen.has(k))
2524
- continue;
2525
- noteDomRemove();
2526
- const dom = entryDom(entry);
2527
- if (hasRowKernel) {
2528
- if (dom && dom.parentNode)
2529
- dom.remove();
2530
- }
2531
- else {
2532
- clearDomEvt(dom);
2533
- disposeDomTree(dom);
2534
- if (dom && dom.parentNode)
2535
- dom.remove();
2536
- }
2537
- keyed.delete(k);
2538
- releaseBpEntry(entry);
2539
- }
2540
- reconcileDomOrder(nextNodes);
2541
- // First apply may run while start/end still sit in a DocumentFragment
2542
- // (before mount appends). Defer until connected so clicks work.
2543
- if (end.isConnected)
2544
- ensureDelegateAttached();
2545
- else
2546
- queueMicrotask(() => {
2547
- if (!inst.__vmzDestroyed)
2548
- ensureDelegateAttached();
2549
- });
2550
- };
2551
- registerBind(inst, deps || [], apply, bindingId);
2552
- if (directApi._itemPatches)
2553
- directApi._itemPatches.push(apply);
2554
- start.__vmzDispose = () => {
2555
- teardownDelegate();
2556
- fastWipeRows();
2557
- };
2558
- const softDeps = [...new Set((deps || []).map((d) => `${depRootField(d)}.*`))];
2559
- const softRefresh = () => {
2560
- if (inst.__vmzDestroyed)
2561
- return;
2562
- const trie = inst.__vmzFlushTrie;
2563
- const listRoot = depRootField((deps && deps[0]) || '') || '';
2564
- // Full list replace is owned by apply(); soft channel is item/structure churn.
2565
- if (trie && listRoot && trie[listRoot] && trie[listRoot].replace)
2566
- return;
2567
- const list = readList();
2568
- const softKey = softDeps[0] || `${listRoot}.*`;
2569
- for (let i = 0; i < list.length; i++) {
2570
- const item = list[i];
2571
- const k = rowKeyOf(item, i);
2572
- const entry = keyed.get(k);
2573
- if (!entry)
2574
- continue;
2575
- if (entryIsBp(entry)) {
2576
- if (entry.nodeType === 1)
2577
- entry.__vmzBox = item;
2578
- else {
2579
- entry.item = item;
2580
- entry.index = i;
2581
- if (entry.dom)
2582
- entry.dom.__vmzBox = item;
2583
- }
2584
- if (applyBp)
2585
- applyBp(entry);
2586
- else if (hydrateBp)
2587
- hydrateBp(entryDom(entry), entry);
2588
- continue;
2589
- }
2590
- entry.box.item = item;
2591
- entry.box.index = i;
2592
- tagItemPatches(entry.patches, i);
2593
- for (const p of entry.patches) {
2594
- // Leaf BindingId patches are owned by list/host dispatchers .
2595
- if (p.__vmzBindingId != null)
2596
- continue;
2597
- if (patchHasBindingId(inst, p))
2598
- continue;
2599
- try {
2600
- runPatch(p, softKey, null);
2601
- }
2602
- catch (err) {
2603
- console.error('vmz:dom each soft', err);
2604
- }
2605
- }
2606
- }
2607
- };
2608
- registerBind(inst, softDeps, softRefresh, null);
2609
- apply();
2610
- return frag;
2611
- },
2612
- };
2613
- /**
2614
- * @param {object} inst
2615
- * @param {string[]} deps
2616
- * @param {() => any} fn
2617
- * @param {number|string|null|undefined} bindingId
2618
- */
2619
- function trackDirectBind(inst, deps, fn, bindingId = null) {
2620
- if (directApi._branchBinds) {
2621
- directApi._branchBinds.push({ deps, fn, bindingId });
2622
- if (directApi._itemPatches)
2623
- directApi._itemPatches.push(fn);
2624
- return;
2625
- }
2626
- // item binds stay on entry.patches; eachBlock registers one dispatcher per BindingId.
2627
- if (directApi._itemPatches) {
2628
- fn.__vmzItemLocal = true;
2629
- directApi._itemPatches.push(fn);
2630
- if (directApi._eachCtx) {
2631
- directApi._eachCtx.noteItemBind(bindingId, deps || [], fn);
2632
- }
2633
- return;
2634
- }
2635
- registerBind(inst, deps, fn, bindingId);
2636
- }
2637
- /**
2638
- * @param {object} inst
2639
- * @param {number|string|null} bindingId
2640
- * @param {string[]} deps
2641
- * @param {() => any} get
2642
- * @param {(raw: any) => void} write
2643
- * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
2644
- */
2645
- function wireDirectBind(inst, bindingId, deps, get, write, cf) {
2646
- let activeBranch = -1;
2647
- /** @type {string[]} */
2648
- let liveDeps = Array.isArray(deps) ? deps.slice() : [];
2649
- const pickCf = () => {
2650
- if (!cf || !Array.isArray(cf.branches))
2651
- return -1;
2652
- for (let i = 0; i < cf.branches.length; i++) {
2653
- const b = cf.branches[i];
2654
- if (!b.cond)
2655
- return i;
2656
- try {
2657
- if (b.cond.call(inst))
2658
- return i;
2659
- }
2660
- catch {
2661
- /* continue */
2662
- }
2663
- }
2664
- return cf.branches.length - 1;
2665
- };
2666
- // Item-local CF whose branches only gate the same stable deps: skip branch switching.
2667
- let simpleCf = false;
2668
- if (cf && Array.isArray(cf.branches) && directApi._itemPatches) {
2669
- simpleCf = cf.branches.every((b) => !b.deps || b.deps.length === 0);
2670
- }
2671
- const apply = () => {
2672
- if (precision.enabled) {
2673
- precision.bindingEvals++;
2674
- for (const d of liveDeps || [])
2675
- bumpMap(precision.bindingEvalsByDep, d);
2676
- if (bindingId != null) {
2677
- bumpMap(precision.bindingEvalsByBinding, String(bindingId));
2678
- }
2679
- }
2680
- let raw;
2681
- try {
2682
- raw = get.call(inst);
2683
- }
2684
- catch {
2685
- raw = null;
2686
- }
2687
- write(raw);
2688
- if (!cf || !Array.isArray(cf.branches) || simpleCf || apply.__vmzItemLocal)
2689
- return;
2690
- const next = pickCf();
2691
- if (next === activeBranch)
2692
- return;
2693
- activeBranch = next;
2694
- const branch = cf.branches[next];
2695
- const nextDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
2696
- const uniq = [...new Set(nextDeps)];
2697
- unregisterBind(inst, liveDeps, apply, bindingId);
2698
- liveDeps = uniq;
2699
- registerBind(inst, liveDeps, apply, bindingId);
2700
- };
2701
- if (cf && Array.isArray(cf.branches) && !simpleCf) {
2702
- activeBranch = pickCf();
2703
- const branch = cf.branches[activeBranch];
2704
- liveDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
2705
- liveDeps = [...new Set(liveDeps)];
2706
- }
2707
- else if (cf && Array.isArray(cf.branches) && simpleCf) {
2708
- liveDeps = Array.isArray(cf.stable) && cf.stable.length ? cf.stable : liveDeps;
2709
- }
2710
- // Mark before first apply so CF branch switches never hit global registerBind.
2711
- if (directApi._itemPatches)
2712
- apply.__vmzItemLocal = true;
2713
- apply();
2714
- trackDirectBind(inst, liveDeps, apply, bindingId);
2715
- }
2716
- function isEventPropName(name) {
2717
- return typeof name === 'string' && /^on[A-Z]/.test(name);
2718
- }
2719
- /** Monotonic id for `bindComponentProp` BindingIds (per process). */
2720
- let directPropBindSeq = 0;
2721
- /** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
2722
- const BOOLEAN_HTML_ATTRS = new Set([
2723
- 'disabled',
2724
- 'checked',
2725
- 'selected',
2726
- 'readonly',
2727
- 'required',
2728
- 'multiple',
2729
- 'hidden',
2730
- 'autofocus',
2731
- 'autoplay',
2732
- 'controls',
2733
- 'loop',
2734
- 'muted',
2735
- 'open',
2736
- 'novalidate',
2737
- 'formnovalidate',
2738
- 'defer',
2739
- 'async',
2740
- 'ismap',
2741
- 'default',
2742
- 'inert',
2743
- ]);
2744
- /**
2745
- * @param {Element} el
2746
- * @param {string} name
2747
- * @param {any} value
2748
- */
2749
- function applyDomAttr(el, name, value) {
2750
- const key = name === 'className' ? 'class' : name;
2751
- if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
2752
- if (value === false || value == null || value === '') {
2753
- el.removeAttribute(key);
2754
- }
2755
- else {
2756
- el.setAttribute(key, value === true ? '' : String(value));
2757
- }
2758
- return;
2759
- }
2760
- if (value == null || value === false)
2761
- el.removeAttribute(key);
2762
- else
2763
- el.setAttribute(key, value === true ? '' : String(value));
2764
- }
2765
- /**
2766
- * Serialize-tree attr write (SSR).
2767
- * @param {any} el
2768
- * @param {string} name
2769
- * @param {any} value
2770
- */
2771
- function applySerializeAttr(el, name, value) {
2772
- if (!el || el.__kind !== 'el')
2773
- return;
2774
- const key = name === 'className' ? 'class' : name;
2775
- if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
2776
- if (value === false || value == null || value === '')
2777
- delete el.attrs[key];
2778
- else
2779
- el.attrs[key] = value === true ? '' : String(value);
2780
- return;
2781
- }
2782
- if (value == null || value === false)
2783
- delete el.attrs[key];
2784
- else
2785
- el.attrs[key] = value === true ? '' : String(value);
2786
- }
2787
- function stripFns(obj) {
2788
- /** @type {Record<string, unknown>} */
2789
- const out = {};
2790
- for (const [k, v] of Object.entries(obj || {})) {
2791
- if (typeof v === 'function')
2792
- continue;
2793
- out[k] = v;
2794
- }
2795
- return out;
2796
- }
2797
- /**
2798
- * Marker range host for Direct eachBlock (insert before end comment).
2799
- * @param {Comment} start
2800
- * @param {Comment} end
2801
- */
2802
- function eachHostApi(start, end) {
2803
- return {
2804
- insert(dom) {
2805
- if (dom.parentNode)
2806
- noteDomMove();
2807
- end.parentNode.insertBefore(dom, end);
2808
- },
2809
- childrenBetween() {
2810
- const out = [];
2811
- let n = start.nextSibling;
2812
- while (n && n !== end) {
2813
- if (n.nodeType === 1)
2814
- out.push(n);
2815
- n = n.nextSibling;
2816
- }
2817
- return out;
2818
- },
2819
- };
2820
- }
2821
- /**
2822
- * Snapshot plain state/prop field values for Island HMR (session).
2823
- * @param {object} inst
2824
- * @returns {Record<string, unknown> | null}
2825
- */
2826
- export function snapshotInstanceState(inst) {
2827
- if (!inst || inst.__vmzDestroyed)
2828
- return null;
2829
- const Ctor = inst.constructor;
2830
- const keys = [...(Ctor.__vmzState || []), ...(Ctor.__vmzProps || [])];
2831
- /** @type {Record<string, unknown>} */
2832
- const out = {};
2833
- for (const key of keys) {
2834
- if (!key || String(key).startsWith('__'))
2835
- continue;
2836
- try {
2837
- out[key] = inst[key];
2838
- }
2839
- catch {
2840
- /* ignore accessors that throw */
2841
- }
2842
- }
2843
- return out;
2844
- }
2845
- /**
2846
- * @param {object} inst
2847
- * @param {Record<string, unknown> | null | undefined} state
2848
- */
2849
- export function applyPreservedState(inst, state) {
2850
- if (!inst || !state)
2851
- return;
2852
- for (const [key, value] of Object.entries(state)) {
2853
- try {
2854
- inst[key] = value;
2855
- }
2856
- catch {
2857
- /* ignore */
2858
- }
2859
- }
2860
- }
2861
- /**
2862
- * resume: attach to existing Island DOM without re-running construct structure or onMount.
2863
- * Consumes ResumeEntry product (`data-vmz-resume`) derived from the same Execution Plan.
2864
- * @param {new (props?: object) => any} Component
2865
- * @param {HTMLElement} container
2866
- * @param {{ props?: object, state?: Record<string, unknown>, strategy?: string } | null} [slice]
2867
- */
2868
- export async function resume(Component, container, slice = null) {
2869
- if (typeof document === 'undefined') {
2870
- throw new Error('vmz:dom resume() requires a document (browser)');
2871
- }
2872
- let parsed = slice;
2873
- if (!parsed) {
2874
- const raw = container.getAttribute('data-vmz-resume');
2875
- if (raw) {
2876
- try {
2877
- parsed = JSON.parse(raw);
2878
- }
2879
- catch {
2880
- parsed = null;
2881
- }
2882
- }
2883
- }
2884
- if (!parsed) {
2885
- let props = {};
2886
- try {
2887
- props = JSON.parse(container.getAttribute('data-vmz-props') || '{}');
2888
- }
2889
- catch {
2890
- props = {};
2891
- }
2892
- parsed = { props, state: {} };
2893
- }
2894
- if (container.__vmzInst) {
2895
- destroy(container.__vmzInst);
2896
- container.__vmzInst = null;
2897
- }
2898
- const props = parsed.props || {};
2899
- const inst = createInstance(Component, props);
2900
- if (parsed.state)
2901
- applyPreservedState(inst, parsed.state);
2902
- // Intentionally never call onMount — SSR already completed that work.
2903
- if (Component.__vmzDirect && typeof Component.__vmzCreate === 'function') {
2904
- if (!hasMeaningfulChild(container)) {
2905
- const node = runDirectCreate(Component, inst);
2906
- if (node) {
2907
- inst.__vmzDomRoot = node;
2908
- container.appendChild(node);
2909
- }
2910
- }
2911
- else {
2912
- // Island leaf adopt: preserve Element identity (resume nodeIdentity).
2913
- const node = runDirectResume(Component, inst, container);
2914
- if (node)
2915
- inst.__vmzDomRoot = node;
2916
- }
2917
- }
2918
- else {
2919
- throw new Error(`vmz:dom resume() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
2920
- }
2921
- container.__vmzInst = inst;
2922
- container.__vmzResumed = true;
2923
- return inst;
2924
- }
2925
- /**
2926
- * Resume all `[data-vmz-island]` hosts (prefer ResumeEntry / EventEntry over mount).
2927
- * Event strategy islands wait for the DOM event before attach (lazy EventEntry).
2928
- * @param {ParentNode} [root]
2929
- */
2930
- export function resumeIslands(root = globalThis.document) {
2931
- if (!root || typeof root.querySelectorAll !== 'function') {
2932
- throw new Error('vmz:dom resumeIslands() requires a DOM root');
2933
- }
2934
- const nodes = [...root.querySelectorAll('[data-vmz-island]')];
2935
- for (const el of nodes) {
2936
- const name = el.getAttribute('data-vmz-island');
2937
- const strategy = el.getAttribute('data-vmz-client') || 'load';
2938
- scheduleClientOn(el, strategy, async () => {
2939
- const Ctor = await resolveComponent(name);
2940
- if (!Ctor) {
2941
- console.error(`vmz:dom resume: unknown component ${name}`);
2942
- return;
2943
- }
2944
- await resume(Ctor, el);
2945
- });
2946
- }
2947
- }
2948
- /**
2949
- * EventEntry attach: only wire `client:event` / `client:event:*` islands.
2950
- * Idle/load/visible ResumeEntries are left alone (static shell can defer framework work).
2951
- * @param {ParentNode} [root]
2952
- */
2953
- export function attachEventEntries(root = globalThis.document) {
2954
- if (!root || typeof root.querySelectorAll !== 'function') {
2955
- throw new Error('vmz:dom attachEventEntries() requires a DOM root');
2956
- }
2957
- const nodes = [...root.querySelectorAll('[data-vmz-island]')];
2958
- for (const el of nodes) {
2959
- const strategy = el.getAttribute('data-vmz-client') || '';
2960
- if (!isEventEntryStrategy(strategy))
2961
- continue;
2962
- const name = el.getAttribute('data-vmz-island');
2963
- el.setAttribute('data-vmz-entry', 'event');
2964
- scheduleClientOn(el, strategy, async () => {
2965
- const Ctor = await resolveComponent(name);
2966
- if (!Ctor) {
2967
- console.error(`vmz:dom EventEntry: unknown component ${name}`);
2968
- return;
2969
- }
2970
- await resume(Ctor, el);
2971
- });
2972
- }
2973
- }
2974
- /**
2975
- * Adopt existing Island DOM while running the same `__vmzCreate` schedule (resume).
2976
- * @param {new (props?: object) => any} Component
2977
- * @param {object} inst
2978
- * @param {Element} container
2979
- */
2980
- function runDirectResume(Component, inst, container) {
2981
- const rootEl = [...container.childNodes].find((n) => n.nodeType === 1 || (n.nodeType === 3 && String(n.textContent).trim() !== ''));
2982
- if (!rootEl || rootEl.nodeType !== 1) {
2983
- return runDirectCreate(Component, inst);
2984
- }
2985
- let textI = 0;
2986
- const api = {
2987
- _inst: inst,
2988
- _branchBinds: null,
2989
- _itemPatches: null,
2990
- el(tag) {
2991
- if (String(rootEl.tagName).toLowerCase() !== String(tag).toLowerCase()) {
2992
- noteDomCreate();
2993
- return document.createElement(tag);
2994
- }
2995
- return rootEl;
2996
- },
2997
- frag() {
2998
- return document.createDocumentFragment();
2999
- },
3000
- text(s) {
3001
- while (textI < rootEl.childNodes.length) {
3002
- const n = rootEl.childNodes[textI++];
3003
- if (n.nodeType === 3) {
3004
- if (s != null && s !== '')
3005
- n.textContent = String(s);
3006
- return n;
3007
- }
3008
- }
3009
- noteDomCreate();
3010
- return document.createTextNode(String(s ?? ''));
3011
- },
3012
- attr(el, name, value) {
3013
- applyDomAttr(el, name, value);
3014
- },
3015
- on(el, type, handler) {
3016
- el.addEventListener(type, handler);
3017
- },
3018
- bindText: directApi.bindText,
3019
- bindAttr: directApi.bindAttr,
3020
- bindComponentProp: directApi.bindComponentProp,
3021
- projectDefaultSlot: directApi.projectDefaultSlot,
3022
- setHtml: directApi.setHtml,
3023
- bindHtml: directApi.bindHtml,
3024
- ifBlock: directApi.ifBlock,
3025
- eachBlock: directApi.eachBlock,
3026
- component: directApi.component,
3027
- };
3028
- return Component.__vmzCreate.call(inst, api);
3029
- }
3030
- /**
3031
- * @param {new (props?: object) => any} Component
3032
- * @param {HTMLElement} container
3033
- * @param {object} [props]
3034
- * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
3035
- */
3036
- export async function hydrate(Component, container, props = {}, opts = {}) {
3037
- if (typeof document === 'undefined') {
3038
- throw new Error('vmz:dom hydrate() requires a document (browser)');
3039
- }
3040
- /** @type {Record<string, unknown> | null} */
3041
- let preserved = null;
3042
- if (opts.preserveState && typeof opts.preserveState === 'object') {
3043
- preserved = opts.preserveState;
3044
- }
3045
- else if (opts.preserveState === true && container.__vmzInst) {
3046
- preserved = snapshotInstanceState(container.__vmzInst);
3047
- }
3048
- if (container.__vmzInst) {
3049
- destroy(container.__vmzInst);
3050
- container.__vmzInst = null;
3051
- }
3052
- const inst = createInstance(Component, props);
3053
- if (preserved) {
3054
- applyPreservedState(inst, preserved);
3055
- }
3056
- // production Direct emit: hydrate uses the same Direct schedule as resume (no render).
3057
- if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
3058
- throw new Error(`vmz:dom hydrate() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
3059
- }
3060
- // Wire DOM + events BEFORE awaiting onMount. SSR shell is already visible; if we
3061
- // wait on RPC/bootstrap first, buttons look real but have no listeners (dead UI).
3062
- // onMount may still patch state / redirect afterwards (same end state as SSR order).
3063
- if (!hasMeaningfulChild(container)) {
3064
- const node = runDirectCreate(Component, inst);
3065
- if (node) {
3066
- inst.__vmzDomRoot = node;
3067
- container.appendChild(node);
3068
- }
3069
- }
3070
- else {
3071
- // Interim: shallow resume leaves if/each binders unbound. Recreate against live
3072
- // DOM so patches attach. Leaf nodeIdentity (same Element) remains TODO for deep adopt.
3073
- container.replaceChildren();
3074
- const node = runDirectCreate(Component, inst);
3075
- if (node) {
3076
- inst.__vmzDomRoot = node;
3077
- container.appendChild(node);
3078
- }
3079
- }
3080
- await settlePendingChildMounts(inst);
3081
- container.__vmzInst = inst;
3082
- const runMount = opts.skipOnMount !== true && !preserved && typeof inst.onMount === 'function';
3083
- if (runMount) {
3084
- await inst.onMount();
3085
- }
3086
- return inst;
3087
- }
3088
- /**
3089
- * Tear down binders and stop patches. Safe to call more than once.
3090
- * Field writes after destroy no longer update DOM (values may still change).
3091
- *: also dispose owned DOM trees (child __vmzInst / region __vmzDispose).
3092
- * @param {object} inst
3093
- */
3094
- export function destroy(inst) {
3095
- if (!inst || inst.__vmzDestroyed)
3096
- return;
3097
- inst.__vmzDestroyed = true;
3098
- inst.__vmzFlushScheduled = false;
3099
- // async cancel: abort in-flight tasks before tearing down DOM.
3100
- __vmzCancelTasks(inst);
3101
- if (inst.__vmzDomRoot) {
3102
- disposeDomTree(inst.__vmzDomRoot);
3103
- inst.__vmzDomRoot = null;
3104
- }
3105
- if (inst.__vmzDirtyNotices)
3106
- inst.__vmzDirtyNotices.length = 0;
3107
- if (inst.__vmzDirtyTrie)
3108
- inst.__vmzDirtyTrie = Object.create(null);
3109
- if (inst.__vmzDirty)
3110
- inst.__vmzDirty.clear();
3111
- inst.__vmzBinders = Object.create(null);
3112
- inst.__vmzBindings = Object.create(null);
3113
- inst.__vmzDepToBindings = Object.create(null);
3114
- if (typeof inst.onDestroy === 'function') {
3115
- try {
3116
- inst.onDestroy();
3117
- }
3118
- catch (err) {
3119
- console.error('vmz:dom onDestroy', err);
3120
- }
3121
- }
3122
- }
3123
- /**
3124
- *: walk a DOM subtree and run lifetime dispose hooks + nested instance destroy.
3125
- * Does not mark the *calling* parent destroyed; safe from destroy(inst).
3126
- * @param {Node | null | undefined} root
3127
- */
3128
- export function disposeDomTree(root) {
3129
- if (!root)
3130
- return;
3131
- const seen = new Set();
3132
- const visit = (node) => {
3133
- if (!node || seen.has(node))
3134
- return;
3135
- seen.add(node);
3136
- if (typeof node.__vmzDispose === 'function') {
3137
- try {
3138
- node.__vmzDispose();
3139
- }
3140
- catch (err) {
3141
- console.error('vmz:dom __vmzDispose', err);
3142
- }
3143
- node.__vmzDispose = null;
3144
- }
3145
- if (node.__vmzInst) {
3146
- const child = node.__vmzInst;
3147
- node.__vmzInst = null;
3148
- destroy(child);
3149
- }
3150
- let child = node.firstChild;
3151
- while (child) {
3152
- const next = child.nextSibling;
3153
- visit(child);
3154
- child = next;
3155
- }
3156
- };
3157
- visit(root);
3158
- }
3159
- /**
3160
- * @param {ParentNode} [root]
3161
- */
3162
- export function hydrateIslands(root = globalThis.document) {
3163
- // resume: hydrateIslands is an alias for resumeIslands (same Plan attach).
3164
- return resumeIslands(root);
3165
- }
3166
- export function scheduleClient(strategy, fn) {
3167
- scheduleClientOn(null, strategy, fn);
3168
- }
3169
- /** @param {string} strategy */
3170
- function isEventEntryStrategy(strategy) {
3171
- const s = String(strategy || '');
3172
- return s === 'event' || s.startsWith('event:') || s === 'click';
3173
- }
3174
- /** @param {string} strategy */
3175
- function eventEntryType(strategy) {
3176
- const s = String(strategy || 'event');
3177
- if (s.startsWith('event:'))
3178
- return s.slice(6) || 'click';
3179
- if (s === 'click')
3180
- return 'click';
3181
- return 'click';
3182
- }
3183
- function scheduleClientOn(el, strategy, fn) {
3184
- const run = () => {
3185
- Promise.resolve(fn()).catch((err) => console.error('vmz:dom island', err));
3186
- };
3187
- if (isEventEntryStrategy(strategy)) {
3188
- if (!el || typeof el.addEventListener !== 'function') {
3189
- run();
3190
- return;
3191
- }
3192
- const type = eventEntryType(strategy);
3193
- const once = () => {
3194
- el.removeEventListener(type, once);
3195
- run();
3196
- };
3197
- el.addEventListener(type, once);
3198
- return;
3199
- }
3200
- if (strategy === 'idle') {
3201
- if (typeof requestIdleCallback === 'function') {
3202
- requestIdleCallback(() => run(), { timeout: 2000 });
3203
- }
3204
- else {
3205
- setTimeout(run, 1);
3206
- }
3207
- return;
3208
- }
3209
- if (strategy === 'visible' && el && typeof IntersectionObserver === 'function') {
3210
- const io = new IntersectionObserver((entries) => {
3211
- if (entries.some((e) => e.isIntersecting)) {
3212
- io.disconnect();
3213
- run();
3214
- }
3215
- });
3216
- io.observe(el);
3217
- return;
3218
- }
3219
- run();
3220
- }
3221
- /**
3222
- * AsyncTask cancel protocol (first slice): keyed generation + AbortSignal.
3223
- * Superseded runs and `destroy(inst)` abort prior work; stale results must not apply.
3224
- * @param {object} inst
3225
- * @param {string} key
3226
- * @param {(signal: AbortSignal, meta: { generation: number }) => any | Promise<any>} fn
3227
- * @returns {Promise<any>}
3228
- */
3229
- export function __vmzRunTask(inst, key, fn) {
3230
- if (!inst)
3231
- throw new Error('vmz:dom __vmzRunTask requires inst');
3232
- const k = String(key || 'default');
3233
- if (!inst.__vmzTasks)
3234
- inst.__vmzTasks = Object.create(null);
3235
- const prev = inst.__vmzTasks[k];
3236
- if (prev) {
3237
- prev.generation += 1;
3238
- try {
3239
- prev.controller.abort();
3240
- }
3241
- catch {
3242
- /* ignore */
3243
- }
3244
- prev.status = 'cancelled';
3245
- }
3246
- const controller = typeof AbortController !== 'undefined'
3247
- ? new AbortController()
3248
- : {
3249
- signal: { aborted: false },
3250
- abort() {
3251
- this.signal.aborted = true;
3252
- },
3253
- };
3254
- const generation = (prev?.generation || 0) + 1;
3255
- /** @type {{ generation: number, controller: any, status: string, result?: any, error?: any, promise?: Promise<any> }} */
3256
- const entry = {
3257
- generation,
3258
- controller,
3259
- status: 'pending',
3260
- };
3261
- inst.__vmzTasks[k] = entry;
3262
- // Invoke synchronously so event handlers can call preventDefault before
3263
- // the browser continues the default action (form submit → native navigation).
3264
- // Async work still continues via the returned Promise.
3265
- let syncResult;
3266
- let syncErr;
3267
- let threw = false;
3268
- try {
3269
- syncResult = fn(controller.signal, { generation });
3270
- }
3271
- catch (err) {
3272
- threw = true;
3273
- syncErr = err;
3274
- }
3275
- const settleOk = (result) => {
3276
- if (inst.__vmzDestroyed || controller.signal.aborted || inst.__vmzTasks[k] !== entry) {
3277
- entry.status = 'cancelled';
3278
- return undefined;
3279
- }
3280
- entry.status = 'success';
3281
- entry.result = result;
3282
- return result;
3283
- };
3284
- const settleErr = (err) => {
3285
- if (inst.__vmzDestroyed || controller.signal.aborted || inst.__vmzTasks[k] !== entry) {
3286
- entry.status = 'cancelled';
3287
- return undefined;
3288
- }
3289
- entry.status = 'error';
3290
- entry.error = err;
3291
- throw err;
3292
- };
3293
- if (threw) {
3294
- const promise = Promise.resolve().then(() => settleErr(syncErr));
3295
- entry.promise = promise;
3296
- return promise;
3297
- }
3298
- const promise = Promise.resolve(syncResult).then(settleOk, settleErr);
3299
- entry.promise = promise;
3300
- return promise;
3301
- }
3302
- /** Abort all keyed tasks on an instance (also called from destroy). */
3303
- export function __vmzCancelTasks(inst) {
3304
- const tasks = inst?.__vmzTasks;
3305
- if (!tasks)
3306
- return;
3307
- for (const key of Object.keys(tasks)) {
3308
- const t = tasks[key];
3309
- t.generation += 1;
3310
- try {
3311
- t.controller.abort();
3312
- }
3313
- catch {
3314
- /* ignore */
3315
- }
3316
- t.status = 'cancelled';
3317
- }
3318
- }
3319
- /** @returns {'pending'|'success'|'error'|'cancelled'|null} */
3320
- export function __vmzTaskStatus(inst, key) {
3321
- const t = inst?.__vmzTasks?.[String(key || 'default')];
3322
- return t ? t.status : null;
3323
- }
3324
- function createInstance(Component, props = {}) {
3325
- if (precision.enabled)
3326
- precision.componentExecs++;
3327
- const inst = new Component(props || {});
3328
- if (typeof inst.__vmzApplyProps === 'function' && !Component.__vmzCtorAppliesProps) {
3329
- inst.__vmzApplyProps(props || {});
3330
- }
3331
- inst.__vmzBinders = Object.create(null);
3332
- inst.__vmzBindings = Object.create(null);
3333
- inst.__vmzDepToBindings = Object.create(null);
3334
- makeReactive(inst, Component.__vmzState || []);
3335
- makeReactive(inst, Component.__vmzProps || []);
3336
- // WriteBarrier: path / array writes call Component helpers (no import needed).
3337
- Component.__vmzWritePath = __vmzWritePath;
3338
- Component.__vmzWritePathLogical = __vmzWritePathLogical;
3339
- Component.__vmzReadPath = __vmzReadPath;
3340
- Component.__vmzArrayMutate = __vmzArrayMutate;
3341
- Component.__vmzAllowShared = __vmzAllowShared;
3342
- Component.__vmzTakeShared = __vmzTakeShared;
3343
- return inst;
3344
- }
3345
- /** Shared plain-object owners under WriteBarrier (no Proxy). */
3346
- const wbSharedOwners = new WeakMap();
3347
- /** Objects explicitly marked OK to share across component instances (13 ). */
3348
- const wbAllowShared = new WeakSet();
3349
- /** @type {Array<{ kind: string, message: string }>} */
3350
- const wbCrossComponentDiags = [];
3351
- /**
3352
- * Mark a plain object as intentionally shared across ownership boundaries.
3353
- * @param {any} value
3354
- */
3355
- export function __vmzAllowShared(value) {
3356
- if (value != null && typeof value === 'object')
3357
- wbAllowShared.add(value);
3358
- return value;
3359
- }
3360
- /**
3361
- * Take exclusive ownership intent: clear multi-owner registry for this object.
3362
- * Subsequent field assigns re-register from the assigning instance only.
3363
- * @param {any} value
3364
- */
3365
- export function __vmzTakeShared(value) {
3366
- if (value != null && typeof value === 'object') {
3367
- wbSharedOwners.delete(value);
3368
- wbAllowShared.delete(value);
3369
- }
3370
- return value;
3371
- }
3372
- /**
3373
- * @returns {Array<{ kind: string, message: string }>}
3374
- */
3375
- export function __vmzSharedCrossComponentDiagnostics() {
3376
- return wbCrossComponentDiags.slice();
3377
- }
3378
- export function __vmzSharedCrossComponentDiagnosticsReset() {
3379
- wbCrossComponentDiags.length = 0;
3380
- }
3381
- /**
3382
- * @param {any} value
3383
- * @param {(segs: string[] | null) => void} report
3384
- * @param {string[]} baseSegs
3385
- * @param {any} [inst]
3386
- */
3387
- function registerWbOwner(value, report, baseSegs = [], inst = null) {
3388
- if (value == null || typeof value !== 'object')
3389
- return;
3390
- let entry = wbSharedOwners.get(value);
3391
- if (!entry) {
3392
- entry = { owners: [] };
3393
- wbSharedOwners.set(value, entry);
3394
- }
3395
- if (entry.owners.some((o) => o.report === report && sameSegs(o.baseSegs, baseSegs))) {
3396
- return;
3397
- }
3398
- entry.owners.push({ report, baseSegs: baseSegs.slice(), inst });
3399
- // Cross-component share without explicit allow → diagnose (13 ).
3400
- if (!wbAllowShared.has(value) && inst) {
3401
- const other = entry.owners.find((o) => o.inst && o.inst !== inst);
3402
- if (other) {
3403
- if (!wbCrossComponentDiags.some((d) => d.message === msg)) {
3404
- wbCrossComponentDiags.push({ kind: 'shared_cross_component', message: msg });
3405
- }
3406
- }
3407
- }
3408
- }
3409
- /**
3410
- * Notify all registered owners of a shared plain object after a barrier write.
3411
- * @param {any} rootObj field-root value that was written under
3412
- * @param {string[] | null} localSegs path under that object (null = replace)
3413
- * @returns {boolean} true when at least one owner was notified
3414
- */
3415
- function notifyWbShared(rootObj, localSegs) {
3416
- const entry = rootObj && typeof rootObj === 'object' ? wbSharedOwners.get(rootObj) : null;
3417
- if (!entry || !entry.owners.length)
3418
- return false;
3419
- for (const o of entry.owners) {
3420
- if (localSegs == null) {
3421
- o.report(o.baseSegs.length ? o.baseSegs.slice() : null);
3422
- }
3423
- else {
3424
- o.report([...o.baseSegs, ...localSegs]);
3425
- }
3426
- }
3427
- return true;
3428
- }
3429
- /**
3430
- * Read a nested path under a field root (for compound / update expansion).
3431
- * @param {any} inst
3432
- * @param {string} root
3433
- * @param {string[]} segs
3434
- */
3435
- export function __vmzReadPath(inst, root, segs) {
3436
- if (!inst || !root)
3437
- return undefined;
3438
- let obj = inst[root];
3439
- if (!Array.isArray(segs) || segs.length === 0)
3440
- return obj;
3441
- for (let i = 0; i < segs.length; i++) {
3442
- if (obj == null || typeof obj !== 'object')
3443
- return undefined;
3444
- obj = obj[segs[i]];
3445
- }
3446
- return obj;
3447
- }
3448
- /**
3449
- * Short-circuit logical path assign (`||=` / `&&=` / `??=`).
3450
- * @param {any} inst
3451
- * @param {string} root
3452
- * @param {string[]} segs
3453
- * @param {'||'|'&&'|'??'} kind
3454
- * @param {any} rhs
3455
- */
3456
- export function __vmzWritePathLogical(inst, root, segs, kind, rhs) {
3457
- const cur = __vmzReadPath(inst, root, segs);
3458
- if (kind === '||') {
3459
- if (cur)
3460
- return cur;
3461
- }
3462
- else if (kind === '&&') {
3463
- if (!cur)
3464
- return cur;
3465
- }
3466
- else if (kind === '??') {
3467
- if (cur != null)
3468
- return cur;
3469
- }
3470
- else {
3471
- return cur;
3472
- }
3473
- return __vmzWritePath(inst, root, segs, rhs);
3474
- }
3475
- /**
3476
- * Mutates a plain owned object/array and schedules the same path notice Proxy would.
3477
- *
3478
- * Root-array index assigns (`tags[0] = x`) notify as field replace (structural),
3479
- * matching the transitional Proxy wrapArray behavior.
3480
- * Shared multi-owner: writing through one field notifies all owners of the same raw object.
3481
- *
3482
- * @param {any} inst
3483
- * @param {string} root field root
3484
- * @param {string[]} segs path under root (non-empty); dynamic indices already String(...)'d
3485
- * @param {any} value
3486
- */
3487
- export function __vmzWritePath(inst, root, segs, value) {
3488
- if (!inst || inst.__vmzDestroyed)
3489
- return value;
3490
- if (!root || !Array.isArray(segs) || segs.length === 0)
3491
- return value;
3492
- const normSegs = segs.map((s) => String(s));
3493
- let obj = inst[root];
3494
- if (obj == null || typeof obj !== 'object')
3495
- return value;
3496
- for (let i = 0; i < normSegs.length - 1; i++) {
3497
- obj = obj[normSegs[i]];
3498
- if (obj == null || typeof obj !== 'object')
3499
- return value;
3500
- }
3501
- const leaf = normSegs[normSegs.length - 1];
3502
- if (Object.is(obj[leaf], value))
3503
- return value;
3504
- obj[leaf] = value;
3505
- // Register newly assigned nested objects under this field for future shared writes.
3506
- if (value != null && typeof value === 'object') {
3507
- const report = (local) => {
3508
- if (!local || local.length === 0) {
3509
- scheduleRefresh(inst, { type: 'replace', root });
3510
- }
3511
- else {
3512
- scheduleRefresh(inst, { type: 'path', root, segs: local });
3513
- }
3514
- };
3515
- registerWbOwner(value, report, normSegs.slice(), inst);
3516
- }
3517
- const rootObj = inst[root];
3518
- const rootArr = rootObj;
3519
- const isRootIndex = normSegs.length === 1 && Array.isArray(rootArr) && leaf !== 'length' && String(Number(leaf)) === leaf;
3520
- if (isRootIndex) {
3521
- if (!notifyWbShared(rootObj, null)) {
3522
- scheduleRefresh(inst, { type: 'replace', root });
3523
- }
3524
- }
3525
- else if (!notifyWbShared(rootObj, normSegs)) {
3526
- scheduleRefresh(inst, { type: 'path', root, segs: normSegs.slice() });
3527
- }
3528
- return value;
3529
- }
3530
- /**
3531
- * Compiler-inserted array mutator barrier (push/pop/splice/…).
3532
- * Applies the mutator on the plain array and schedules a structural notice
3533
- * at `root` + `baseSegs` (empty baseSegs → field replace).
3534
- *
3535
- * @param {any} inst
3536
- * @param {string} root
3537
- * @param {string[]} baseSegs
3538
- * @param {string} method
3539
- * @param {any[]} args
3540
- */
3541
- export function __vmzArrayMutate(inst, root, baseSegs, method, args) {
3542
- if (!inst || inst.__vmzDestroyed)
3543
- return undefined;
3544
- if (!root || typeof method !== 'string')
3545
- return undefined;
3546
- const segs = Array.isArray(baseSegs) ? baseSegs.map((s) => String(s)) : [];
3547
- let arr = inst[root];
3548
- if (arr == null || typeof arr !== 'object')
3549
- return undefined;
3550
- for (let i = 0; i < segs.length; i++) {
3551
- arr = arr[segs[i]];
3552
- if (arr == null || typeof arr !== 'object')
3553
- return undefined;
3554
- }
3555
- if (!Array.isArray(arr) || typeof arr[method] !== 'function')
3556
- return undefined;
3557
- const list = Array.isArray(args) ? args : [];
3558
- const ret = arr[method](...list);
3559
- const rootObj = inst[root];
3560
- if (segs.length === 0) {
3561
- if (!notifyWbShared(rootObj, null)) {
3562
- scheduleRefresh(inst, { type: 'replace', root });
3563
- }
3564
- }
3565
- else if (!notifyWbShared(rootObj, segs)) {
3566
- scheduleRefresh(inst, { type: 'path', root, segs: segs.slice() });
3567
- }
3568
- return ret;
3569
- }
3570
- function makeReactive(inst, stateKeys) {
3571
- const barrier = !!inst.constructor.__vmzWriteBarrier;
3572
- for (const key of stateKeys) {
3573
- if (!key || key.startsWith('#'))
3574
- continue;
3575
- const desc = Object.getOwnPropertyDescriptor(inst, key);
3576
- if (desc && desc.set && desc.get && !desc.writable)
3577
- continue;
3578
- /** @param {string[] | null} segs null/empty → replace field */
3579
- const report = (segs) => {
3580
- if (!segs || segs.length === 0) {
3581
- scheduleRefresh(inst, { type: 'replace', root: key });
3582
- }
3583
- else {
3584
- scheduleRefresh(inst, { type: 'path', root: key, segs });
3585
- }
3586
- };
3587
- // WriteBarrier components keep plain objects — nested writes go through __vmzWritePath.
3588
- let value = barrier ? inst[key] : wrapReactive(inst[key], report, []);
3589
- if (barrier)
3590
- registerWbOwner(value, report, [], inst);
3591
- Object.defineProperty(inst, key, {
3592
- configurable: true,
3593
- enumerable: true,
3594
- get() {
3595
- return value;
3596
- },
3597
- set(next) {
3598
- const wrapped = barrier ? next : wrapReactive(next, report, []);
3599
- if (Object.is(value, wrapped))
3600
- return;
3601
- value = wrapped;
3602
- if (barrier)
3603
- registerWbOwner(value, report, [], inst);
3604
- report(null);
3605
- },
3606
- });
3607
- }
3608
- }
3609
- /** Targets already wrapped: raw|proxy|barrier → { proxy, owners[], kind }. */
3610
- const reactiveProxies = new WeakMap();
3611
- /** Plain objects using defineProperty write barriers (not Proxy). */
3612
- const writeBarrierOwned = new WeakSet();
3613
- /**
3614
- * WriteBarrier: true when value is an owned plain object with path barriers (no Proxy).
3615
- * @param {any} value
3616
- */
3617
- export function __vmzIsWriteBarrierOwned(value) {
3618
- return writeBarrierOwned.has(value);
3619
- }
3620
- /**
3621
- * True when value is the Proxy wrapper from array (or residual) reactive wrap.
3622
- * @param {any} value
3623
- */
3624
- export function __vmzIsReactiveProxy(value) {
3625
- const e = reactiveProxies.get(value);
3626
- return !!(e && e.kind === 'proxy' && e.proxy === value);
3627
- }
3628
- const ARRAY_MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin']);
3629
- /**
3630
- * @typedef {{ report: (segs: string[] | null) => void, baseSegs: string[] }} ReactiveOwner
3631
- * @typedef {{ proxy: object, owners: ReactiveOwner[], kind: 'barrier'|'proxy' }} ReactiveEntry
3632
- */
3633
- function sameSegs(a, b) {
3634
- if (a.length !== b.length)
3635
- return false;
3636
- for (let i = 0; i < a.length; i++) {
3637
- if (a[i] !== b[i])
3638
- return false;
3639
- }
3640
- return true;
3641
- }
3642
- /**
3643
- * @param {ReactiveEntry} entry
3644
- * @param {(segs: string[] | null) => void} report
3645
- * @param {string[]} baseSegs
3646
- */
3647
- function addOwner(entry, report, baseSegs) {
3648
- if (entry.owners.some((o) => o.report === report && sameSegs(o.baseSegs, baseSegs))) {
3649
- return;
3650
- }
3651
- entry.owners.push({
3652
- report,
3653
- baseSegs: baseSegs.slice(),
3654
- });
3655
- }
3656
- /**
3657
- * @param {ReactiveOwner[]} owners
3658
- * @param {string[] | null} localSegs null = structural replace of this node
3659
- */
3660
- function notifyOwners(owners, localSegs) {
3661
- for (const o of owners) {
3662
- if (localSegs == null) {
3663
- o.report(o.baseSegs.length ? o.baseSegs.slice() : null);
3664
- }
3665
- else {
3666
- o.report([...o.baseSegs, ...localSegs]);
3667
- }
3668
- }
3669
- }
3670
- /**
3671
- * Field-owned write traps for plain objects / arrays on state fields.
3672
- * Plain objects: WriteBarrier via defineProperty (no Proxy).
3673
- * Arrays: transitional Proxy tracks list identity/mutators only; elements stay plain
3674
- * (no per-item wrap on large assign — nested notifies via `__vmzWritePath`).
3675
- * Shared raw objects notify **all** current owners.
3676
- *
3677
- * @param {any} value
3678
- * @param {(segs: string[] | null) => void} report
3679
- * @param {string[]} pathSegs path under the field root to this value
3680
- */
3681
- function wrapReactive(value, report, pathSegs = []) {
3682
- if (value == null || typeof value !== 'object')
3683
- return value;
3684
- const existing = reactiveProxies.get(value);
3685
- if (existing) {
3686
- addOwner(existing, report, pathSegs);
3687
- return existing.proxy;
3688
- }
3689
- if (Array.isArray(value))
3690
- return wrapArray(value, report, pathSegs);
3691
- if (isPlainObject(value))
3692
- return wrapOwnedObject(value, report, pathSegs);
3693
- return value;
3694
- }
3695
- function isPlainObject(value) {
3696
- const proto = Object.getPrototypeOf(value);
3697
- return proto === Object.prototype || proto === null;
3698
- }
3699
- /**
3700
- * Path-level write barrier for owned plain objects (no Proxy).
3701
- */
3702
- function wrapOwnedObject(obj, report, pathSegs) {
3703
- const existing = reactiveProxies.get(obj);
3704
- if (existing) {
3705
- addOwner(existing, report, pathSegs);
3706
- return existing.proxy;
3707
- }
3708
- /** @type {ReactiveEntry} */
3709
- const entry = {
3710
- proxy: obj,
3711
- owners: [],
3712
- kind: 'barrier',
3713
- };
3714
- addOwner(entry, report, pathSegs);
3715
- writeBarrierOwned.add(obj);
3716
- reactiveProxies.set(obj, entry);
3717
- for (const prop of Object.keys(obj)) {
3718
- installOwnedProp(obj, prop, entry);
3719
- }
3720
- return obj;
3721
- }
3722
- /**
3723
- * @param {object} obj
3724
- * @param {string} prop
3725
- * @param {ReactiveEntry} entry
3726
- */
3727
- function installOwnedProp(obj, prop, entry) {
3728
- const desc = Object.getOwnPropertyDescriptor(obj, prop);
3729
- if (!desc || !desc.configurable)
3730
- return;
3731
- if (desc.get || desc.set)
3732
- return;
3733
- let current = obj[prop];
3734
- for (const o of entry.owners) {
3735
- current = wrapReactive(current, o.report, [...o.baseSegs, prop]);
3736
- }
3737
- Object.defineProperty(obj, prop, {
3738
- configurable: true,
3739
- enumerable: desc.enumerable !== false,
3740
- get() {
3741
- return current;
3742
- },
3743
- set(next) {
3744
- const local = [prop];
3745
- let wrapped = next;
3746
- for (const o of entry.owners) {
3747
- wrapped = wrapReactive(next, o.report, [...o.baseSegs, ...local]);
3748
- }
3749
- if (Object.is(current, wrapped))
3750
- return;
3751
- current = wrapped;
3752
- notifyOwners(entry.owners, local);
3753
- },
3754
- });
3755
- }
3756
- /**
3757
- * Transitional array Proxy: track list identity / mutators only.
3758
- * Elements stay plain — no per-item defineProperty on `this.rows = largeArray`
3759
- * (design: WriteBarrier / list replace must not wrap 1k items). Nested field
3760
- * notifies go through `__vmzWritePath` or whole-array replace.
3761
- */
3762
- function wrapArray(arr, report, pathSegs) {
3763
- const existing = reactiveProxies.get(arr);
3764
- if (existing) {
3765
- addOwner(existing, report, pathSegs);
3766
- return existing.proxy;
3767
- }
3768
- /** @type {ReactiveEntry} */
3769
- const entry = {
3770
- proxy: null,
3771
- owners: [],
3772
- kind: 'proxy',
3773
- };
3774
- addOwner(entry, report, pathSegs);
3775
- const isArrayIndex = (prop) => typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
3776
- const proxy = new Proxy(arr, {
3777
- get(target, prop, receiver) {
3778
- if (typeof prop === 'string' && ARRAY_MUTATORS.has(prop)) {
3779
- const fn = target[prop];
3780
- return (...args) => {
3781
- const ret = fn.apply(target, args);
3782
- notifyOwners(entry.owners, null);
3783
- return ret;
3784
- };
3785
- }
3786
- // Indices / length / methods: return as-is (plain elements).
3787
- return Reflect.get(target, prop, receiver);
3788
- },
3789
- set(target, prop, next, receiver) {
3790
- const prev = target[prop];
3791
- if (Object.is(prev, next))
3792
- return true;
3793
- const ok = Reflect.set(target, prop, next, receiver);
3794
- if (ok) {
3795
- if (prop === 'length' || isArrayIndex(prop))
3796
- notifyOwners(entry.owners, null);
3797
- else if (typeof prop === 'string')
3798
- notifyOwners(entry.owners, [prop]);
3799
- else
3800
- notifyOwners(entry.owners, null);
3801
- }
3802
- return ok;
3803
- },
3804
- deleteProperty(target, prop) {
3805
- if (!(prop in target))
3806
- return true;
3807
- const ok = Reflect.deleteProperty(target, prop);
3808
- if (ok) {
3809
- notifyOwners(entry.owners, typeof prop === 'string' ? [prop] : null);
3810
- }
3811
- return ok;
3812
- },
3813
- });
3814
- entry.proxy = proxy;
3815
- reactiveProxies.set(arr, entry);
3816
- reactiveProxies.set(proxy, entry);
3817
- return proxy;
3818
- }
3819
- /**
3820
- * Coalesce field/path patches in the same turn via a dirty path trie.
3821
- * Still precise deps — never a full-tree re-render. Flush runs as a microtask;
3822
- * call `await flushPending(inst)` to apply synchronously (tests / immediate UI).
3823
- *
3824
- *
3825
- * @param {object} inst
3826
- * @param {{ type: 'replace', root: string } | { type: 'path', root: string, segs: string[] } | string} notice
3827
- * string form is transitional field-root alias for replace.
3828
- */
3829
- function scheduleRefresh(inst, notice) {
3830
- if (!inst || inst.__vmzDestroyed || inst.__vmzQuiet)
3831
- return;
3832
- const n = typeof notice === 'string' ? { type: 'replace', root: notice } : notice;
3833
- if (!n || !n.root)
3834
- return;
3835
- if (precision.enabled) {
3836
- precision.writes++;
3837
- bumpMap(precision.writesByRoot, n.root);
3838
- }
3839
- pushTrace('write', 'field', n.root, n.root);
3840
- if (!inst.__vmzDirtyTrie)
3841
- inst.__vmzDirtyTrie = Object.create(null);
3842
- insertDirtyNotice(inst.__vmzDirtyTrie, n);
3843
- // Transitional: keep notice list for flush loop emptiness check / compat.
3844
- if (!inst.__vmzDirtyNotices)
3845
- inst.__vmzDirtyNotices = [];
3846
- inst.__vmzDirtyNotices.push(n);
3847
- if (inst.__vmzFlushScheduled)
3848
- return;
3849
- inst.__vmzFlushScheduled = true;
3850
- queueMicrotask(() => {
3851
- inst.__vmzFlushScheduled = false;
3852
- Promise.resolve(flushPending(inst)).catch((err) => console.error('vmz:dom flush', err));
3853
- });
3854
- }
3855
- /**
3856
- * @param {Record<string, any>} trie
3857
- * @param {{ type: string, root: string, segs?: string[] }} notice
3858
- */
3859
- function insertDirtyNotice(trie, notice) {
3860
- if (notice.type === 'replace') {
3861
- trie[notice.root] = { replace: true };
3862
- return;
3863
- }
3864
- const segs = notice.segs || [];
3865
- let node = trie[notice.root];
3866
- if (node && node.replace)
3867
- return;
3868
- if (!node) {
3869
- node = { children: Object.create(null) };
3870
- trie[notice.root] = node;
3871
- }
3872
- if (!segs.length) {
3873
- trie[notice.root] = { replace: true };
3874
- return;
3875
- }
3876
- if (!node.children)
3877
- node.children = Object.create(null);
3878
- let cur = node;
3879
- for (let i = 0; i < segs.length; i++) {
3880
- const seg = segs[i];
3881
- if (cur.dirty)
3882
- return; // ancestor already dirty
3883
- if (!cur.children)
3884
- cur.children = Object.create(null);
3885
- if (i === segs.length - 1) {
3886
- cur.children[seg] = { dirty: true };
3887
- return;
3888
- }
3889
- let next = cur.children[seg];
3890
- if (!next) {
3891
- next = { children: Object.create(null) };
3892
- cur.children[seg] = next;
3893
- }
3894
- else if (next.dirty) {
3895
- return;
3896
- }
3897
- else if (!next.children) {
3898
- next.children = Object.create(null);
3899
- }
3900
- cur = next;
3901
- }
3902
- }
3903
- /** @param {object} inst */
3904
- export async function flushPending(inst) {
3905
- if (!inst || inst.__vmzDestroyed)
3906
- return;
3907
- inst.__vmzFlushScheduled = false;
3908
- let guard = 0;
3909
- while (!inst.__vmzDestroyed &&
3910
- ((inst.__vmzDirtyTrie && Object.keys(inst.__vmzDirtyTrie).length > 0) ||
3911
- (inst.__vmzDirtyNotices && inst.__vmzDirtyNotices.length > 0)) &&
3912
- guard++ < 64) {
3913
- const trie = inst.__vmzDirtyTrie || Object.create(null);
3914
- inst.__vmzDirtyTrie = Object.create(null);
3915
- if (inst.__vmzDirtyNotices)
3916
- inst.__vmzDirtyNotices.length = 0;
3917
- inst.__vmzFlushTrie = trie;
3918
- const jobs = [];
3919
- // Prefer BindingId scheduling (IR). String `__vmzBinders` is adapter-only.
3920
- // Pass `trie` into refresh — dirty map is cleared above before patches run.
3921
- try {
3922
- const bindingIds = bindingIdsMatchingTrie(inst, trie);
3923
- const coveredDeps = Object.create(null);
3924
- for (const id of bindingIds) {
3925
- const entry = inst.__vmzBindings && inst.__vmzBindings[id];
3926
- if (entry) {
3927
- for (const d of entry.deps || [])
3928
- coveredDeps[d] = true;
3929
- }
3930
- jobs.push(...refreshBinding(inst, id, trie));
3931
- }
3932
- for (const key of binderKeysMatchingTrie(inst, trie)) {
3933
- if (coveredDeps[key] || (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length)) {
3934
- // BindingId path already flushed IR patches for this dep.
3935
- // Still run binder-only patches (bindComponentProp uses bindingId null).
3936
- jobs.push(...refreshFieldBinderOnly(inst, key));
3937
- continue;
3938
- }
3939
- jobs.push(...refreshField(inst, key));
3940
- }
3941
- if (jobs.length)
3942
- await Promise.all(jobs);
3943
- }
3944
- finally {
3945
- inst.__vmzFlushTrie = null;
3946
- }
3947
- }
3948
- }
3949
- /**
3950
- * @param {object} inst
3951
- * @param {Record<string, any>} trie
3952
- * @returns {Array<number|string>}
3953
- */
3954
- function bindingIdsMatchingTrie(inst, trie) {
3955
- const index = inst.__vmzDepToBindings;
3956
- if (!index)
3957
- return [];
3958
- const out = [];
3959
- const seen = Object.create(null);
3960
- for (const key of Object.keys(index)) {
3961
- if (!depMatchesTrie(trie, key))
3962
- continue;
3963
- for (const id of index[key]) {
3964
- const k = String(id);
3965
- if (seen[k])
3966
- continue;
3967
- seen[k] = true;
3968
- out.push(id);
3969
- }
3970
- }
3971
- return out;
3972
- }
3973
- /**
3974
- * @param {object} inst
3975
- * @param {Record<string, any>} trie
3976
- * @returns {string[]}
3977
- */
3978
- function binderKeysMatchingTrie(inst, trie) {
3979
- const binders = inst.__vmzBinders;
3980
- if (!binders)
3981
- return [];
3982
- const out = [];
3983
- for (const key of Object.keys(binders)) {
3984
- if (depMatchesTrie(trie, key))
3985
- out.push(key);
3986
- }
3987
- return out;
3988
- }
3989
- /**
3990
- * @param {Record<string, any>} trie
3991
- * @param {string} key
3992
- */
3993
- function depMatchesTrie(trie, key) {
3994
- const root = depRootField(key);
3995
- const node = trie[root];
3996
- if (!node)
3997
- return false;
3998
- if (node.replace) {
3999
- return key === root || key === `${root}.*` || key.startsWith(`${root}.`) || key.startsWith(`${root}[`);
4000
- }
4001
- if (key === `${root}.*`) {
4002
- // Bare `field.*` soft/structure channel: item replace / array structure only —
4003
- // NOT deep leaf writes (`tags.0.label`); those use `tags.*.label` BindingId.
4004
- return structureStarMatches(node);
4005
- }
4006
- // Bare field: replace-only.
4007
- if (key === root)
4008
- return false;
4009
- // Path channel: `tags.*.label` — wildcard index under list root.
4010
- const starPrefix = `${root}.*`;
4011
- if (key === starPrefix || key.startsWith(`${starPrefix}.`)) {
4012
- const rest = key === starPrefix
4013
- ? []
4014
- : key
4015
- .slice(starPrefix.length + 1)
4016
- .split('.')
4017
- .filter(Boolean);
4018
- return wildcardIndexDirtyCovers(node, rest);
4019
- }
4020
- // Stable ListItem form `tags[key=…].label` — treat `[key=…]` as wildcard index.
4021
- if (key.startsWith(`${root}[`)) {
4022
- const afterBracket = key.indexOf(']');
4023
- if (afterBracket > root.length) {
4024
- const rest = key.length > afterBracket + 1 && key[afterBracket + 1] === '.'
4025
- ? key
4026
- .slice(afterBracket + 2)
4027
- .split('.')
4028
- .filter(Boolean)
4029
- : [];
4030
- return wildcardIndexDirtyCovers(node, rest);
4031
- }
4032
- }
4033
- const segs = key
4034
- .slice(root.length + 1)
4035
- .split('.')
4036
- .filter(Boolean);
4037
- return pathDirtyCovers(node, segs);
4038
- }
4039
- /** `tags.*` structure soft-refresh: replace or index-level dirty, not leaf-only. */
4040
- function structureStarMatches(node) {
4041
- if (!node)
4042
- return false;
4043
- if (node.replace || node.dirty)
4044
- return true;
4045
- if (!node.children)
4046
- return false;
4047
- for (const idx of Object.keys(node.children)) {
4048
- const child = node.children[idx];
4049
- // Index node dirty/replace → item identity changed.
4050
- if (child && (child.replace || child.dirty))
4051
- return true;
4052
- }
4053
- return false;
4054
- }
4055
- /** `tags.*.label` / `tags[key=x].label` vs dirty trie under `tags`. */
4056
- function wildcardIndexDirtyCovers(node, restSegs) {
4057
- if (!node || node.replace)
4058
- return !!node?.replace;
4059
- if (node.dirty)
4060
- return true;
4061
- if (!node.children)
4062
- return false;
4063
- for (const idx of Object.keys(node.children)) {
4064
- const child = node.children[idx];
4065
- if (restSegs.length === 0) {
4066
- if (trieHasAnyDirty(child))
4067
- return true;
4068
- }
4069
- else if (pathDirtyCovers(child, restSegs)) {
4070
- return true;
4071
- }
4072
- }
4073
- return false;
4074
- }
4075
- function trieHasAnyDirty(node) {
4076
- if (!node || node.replace)
4077
- return !!node;
4078
- if (node.dirty)
4079
- return true;
4080
- if (!node.children)
4081
- return false;
4082
- for (const k of Object.keys(node.children)) {
4083
- if (trieHasAnyDirty(node.children[k]))
4084
- return true;
4085
- }
4086
- return false;
4087
- }
4088
- /**
4089
- * Wake if write is at/under dep, or dep is under write (parent covers children).
4090
- * @param {any} node root trie node for field
4091
- * @param {string[]} depSegs
4092
- */
4093
- function pathDirtyCovers(node, depSegs) {
4094
- let cur = node;
4095
- for (let i = 0; i < depSegs.length; i++) {
4096
- if (!cur || cur.replace)
4097
- return !!cur?.replace;
4098
- if (cur.dirty)
4099
- return true; // write parent covers this dep
4100
- if (!cur.children)
4101
- return false;
4102
- const next = cur.children[depSegs[i]];
4103
- if (!next) {
4104
- // No write along this dep path — but a write under a prefix?
4105
- return false;
4106
- }
4107
- cur = next;
4108
- }
4109
- // Reached dep node: wake if dirty here or any dirty descendant (write under dep).
4110
- return trieHasAnyDirty(cur);
4111
- }
4112
- /**
4113
- * Dual-track match retained for tests / tooling.
4114
- * @param {{ type: string, root: string, segs?: string[] }} notice
4115
- * @param {string} key
4116
- */
4117
- function noticeMatchesDepKey(notice, key) {
4118
- const trie = Object.create(null);
4119
- insertDirtyNotice(trie, notice);
4120
- return depMatchesTrie(trie, key);
4121
- }
4122
- /** Root field name from a dep key string (`user.name` → `user`, `tags.*` → `tags`). */
4123
- function depRootField(dep) {
4124
- if (!dep)
4125
- return '';
4126
- const star = dep.indexOf('.*');
4127
- if (star >= 0)
4128
- return dep.slice(0, star);
4129
- const dot = dep.indexOf('.');
4130
- if (dot >= 0)
4131
- return dep.slice(0, dot);
4132
- const bracket = dep.indexOf('[');
4133
- if (bracket >= 0)
4134
- return dep.slice(0, bracket);
4135
- return dep;
4136
- }
4137
- /** Precise patches only — no full-tree fallback. @returns {Promise[]} */
4138
- function refreshBinding(inst, bindingId, dirtyTrie = null) {
4139
- const entry = inst.__vmzBindings && inst.__vmzBindings[bindingId];
4140
- const jobs = [];
4141
- if (!inst || inst.__vmzDestroyed || bindingId == null || !entry) {
4142
- return jobs;
4143
- }
4144
- const depKey = (entry.deps && entry.deps[0]) || null;
4145
- const trie = dirtyTrie || inst.__vmzDirtyTrie;
4146
- const allowIdx = itemIndicesAllowedForDeps(trie, entry.deps);
4147
- for (const fn of entry.patches) {
4148
- if (allowIdx && !patchMatchesDirtyIndex(fn, allowIdx))
4149
- continue;
4150
- try {
4151
- const ret = runPatch(fn, depKey, bindingId);
4152
- if (ret && typeof ret.then === 'function')
4153
- jobs.push(ret);
4154
- }
4155
- catch (err) {
4156
- console.error('vmz:dom patch', err);
4157
- }
4158
- }
4159
- return jobs;
4160
- }
4161
- /**
4162
- * For ListItem path-channel deps (`tags.*.label`), restrict to dirty indices.
4163
- * @param {Record<string, any>|null|undefined} trie
4164
- * @param {string[]|null|undefined} deps
4165
- * @returns {Set<string>|null} null = run all patches (replace / non-list deps)
4166
- */
4167
- function itemIndicesAllowedForDeps(trie, deps) {
4168
- if (!trie || !deps || !deps.length)
4169
- return null;
4170
- let sawListChannel = false;
4171
- /** @type {Set<string>|null} */
4172
- let allow = null;
4173
- for (const dep of deps) {
4174
- const root = depRootField(dep);
4175
- if (!root)
4176
- continue;
4177
- const starPrefix = `${root}.*`;
4178
- const isListChannel = dep === starPrefix || dep.startsWith(`${starPrefix}.`) || (dep.startsWith(`${root}[`) && dep.includes(']'));
4179
- if (!isListChannel)
4180
- return null;
4181
- sawListChannel = true;
4182
- const node = trie[root];
4183
- if (!node)
4184
- continue;
4185
- if (node.replace || node.dirty)
4186
- return null; // whole list
4187
- if (!node.children)
4188
- continue;
4189
- if (!allow)
4190
- allow = new Set();
4191
- for (const idx of Object.keys(node.children)) {
4192
- const child = node.children[idx];
4193
- if (!child)
4194
- continue;
4195
- if (child.replace || child.dirty || trieHasAnyDirty(child)) {
4196
- allow.add(String(idx));
4197
- }
4198
- }
4199
- }
4200
- if (!sawListChannel)
4201
- return null;
4202
- return allow && allow.size ? allow : null;
4203
- }
4204
- function patchMatchesDirtyIndex(fn, allowIdx) {
4205
- const idx = fn && fn.__vmzItemIndex;
4206
- if (idx == null || idx === '')
4207
- return true;
4208
- return allowIdx.has(String(idx));
4209
- }
4210
- /** Legacy string-key patches (hand blueprints without BindingId). @returns {Promise[]} */
4211
- function refreshField(inst, field) {
4212
- const binders = inst.__vmzBinders;
4213
- const jobs = [];
4214
- if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
4215
- return jobs;
4216
- }
4217
- for (const fn of binders[field]) {
4218
- try {
4219
- const ret = runPatch(fn, field, null);
4220
- if (ret && typeof ret.then === 'function')
4221
- jobs.push(ret);
4222
- }
4223
- catch (err) {
4224
- console.error('vmz:dom patch', err);
4225
- }
4226
- }
4227
- return jobs;
4228
- }
4229
- /**
4230
- * Run `__vmzBinders` patches that are not owned by a BindingId entry.
4231
- * Needed so `bindComponentProp` (bindingId null) still flushes when the same
4232
- * dep also has IR bindText/bindAttr BindingIds.
4233
- * @returns {Promise[]}
4234
- */
4235
- function refreshFieldBinderOnly(inst, field) {
4236
- const binders = inst.__vmzBinders;
4237
- const jobs = [];
4238
- if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
4239
- return jobs;
4240
- }
4241
- for (const fn of binders[field]) {
4242
- if (patchHasBindingId(inst, fn))
4243
- continue;
4244
- try {
4245
- const ret = runPatch(fn, field, null);
4246
- if (ret && typeof ret.then === 'function')
4247
- jobs.push(ret);
4248
- }
4249
- catch (err) {
4250
- console.error('vmz:dom patch', err);
4251
- }
4252
- }
4253
- return jobs;
4254
- }
4255
- /**
4256
- * @param {object} inst
4257
- * @param {number|string} bindingId
4258
- * @param {string[]} deps
4259
- */
4260
- function reindexBindingDeps(inst, bindingId, deps) {
4261
- if (!inst.__vmzDepToBindings)
4262
- inst.__vmzDepToBindings = Object.create(null);
4263
- const entry = inst.__vmzBindings[bindingId];
4264
- if (!entry)
4265
- return;
4266
- for (const dep of entry.deps || []) {
4267
- const list = inst.__vmzDepToBindings[dep];
4268
- if (!list)
4269
- continue;
4270
- const j = list.indexOf(bindingId);
4271
- if (j >= 0)
4272
- list.splice(j, 1);
4273
- if (list.length === 0)
4274
- delete inst.__vmzDepToBindings[dep];
4275
- }
4276
- entry.deps = [...(deps || [])];
4277
- for (const dep of entry.deps) {
4278
- if (!inst.__vmzDepToBindings[dep])
4279
- inst.__vmzDepToBindings[dep] = [];
4280
- if (!inst.__vmzDepToBindings[dep].includes(bindingId)) {
4281
- inst.__vmzDepToBindings[dep].push(bindingId);
4282
- }
4283
- }
4284
- }
4285
- /**
4286
- * @param {object} inst
4287
- * @param {string[]} deps
4288
- * @param {() => any} fn
4289
- * @param {number|string|null|undefined} [bindingId]
4290
- */
4291
- function registerBind(inst, deps, fn, bindingId = null) {
4292
- if (!inst.__vmzBinders)
4293
- inst.__vmzBinders = Object.create(null);
4294
- for (const dep of deps || []) {
4295
- if (!inst.__vmzBinders[dep])
4296
- inst.__vmzBinders[dep] = [];
4297
- inst.__vmzBinders[dep].push(fn);
4298
- }
4299
- if (bindingId == null)
4300
- return;
4301
- if (!inst.__vmzBindings)
4302
- inst.__vmzBindings = Object.create(null);
4303
- let entry = inst.__vmzBindings[bindingId];
4304
- if (!entry) {
4305
- entry = { id: bindingId, deps: [], patches: [] };
4306
- inst.__vmzBindings[bindingId] = entry;
4307
- }
4308
- if (!entry.patches.includes(fn))
4309
- entry.patches.push(fn);
4310
- reindexBindingDeps(inst, bindingId, deps || []);
4311
- }
4312
- /**
4313
- * @param {object} inst
4314
- * @param {string[]} deps
4315
- * @param {() => any} fn
4316
- * @param {number|string|null|undefined} [bindingId]
4317
- */
4318
- function unregisterBind(inst, deps, fn, bindingId = null) {
4319
- const binders = inst.__vmzBinders;
4320
- if (binders) {
4321
- for (const dep of deps || []) {
4322
- const list = binders[dep];
4323
- if (!list)
4324
- continue;
4325
- const i = list.indexOf(fn);
4326
- if (i >= 0)
4327
- list.splice(i, 1);
4328
- if (list.length === 0)
4329
- delete binders[dep];
4330
- }
4331
- }
4332
- if (bindingId == null || !inst.__vmzBindings)
4333
- return;
4334
- const entry = inst.__vmzBindings[bindingId];
4335
- if (!entry)
4336
- return;
4337
- const i = entry.patches.indexOf(fn);
4338
- if (i >= 0)
4339
- entry.patches.splice(i, 1);
4340
- if (entry.patches.length === 0) {
4341
- reindexBindingDeps(inst, bindingId, []);
4342
- delete inst.__vmzBindings[bindingId];
4343
- }
4344
- }
4345
- /**
4346
- * True when the container already has meaningful DOM (SSR / resume shell).
4347
- * @param {Element} el
4348
- */
4349
- function hasMeaningfulChild(el) {
4350
- for (const n of el.childNodes) {
4351
- if (n.nodeType === 1)
4352
- return true;
4353
- if (n.nodeType === 3 && String(n.textContent).trim() !== '')
4354
- return true;
4355
- }
4356
- return false;
4357
- }
4358
- function patchHasBindingId(inst, fn) {
4359
- const bindings = inst && inst.__vmzBindings;
4360
- if (!bindings)
4361
- return false;
4362
- for (const id of Object.keys(bindings)) {
4363
- const patches = bindings[id].patches;
4364
- if (patches && patches.includes(fn))
4365
- return true;
4366
- }
4367
- return false;
4368
- }
4369
- /** Tag each-item patches with list index for ListItem path-channel filtering. */
4370
- function tagItemPatches(patches, index) {
4371
- if (!patches)
4372
- return;
4373
- const idx = String(index);
4374
- for (const p of patches) {
4375
- if (typeof p === 'function')
4376
- p.__vmzItemIndex = idx;
4377
- }
4378
- }
4379
- function escapeHtml(s) {
4380
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4381
- }
6
+ export * from './dom-core.js';
7
+ export * from './dom-ssr.js';