@vmz/core 0.1.32 → 0.2.1

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