@vmz/core 0.2.5 → 0.2.6

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 (77) hide show
  1. package/dist/browser/client-nav.d.ts +5 -55
  2. package/dist/browser/client-nav.js +5 -769
  3. package/dist/browser/diagnostics/precision-trace.d.ts +47 -0
  4. package/dist/browser/diagnostics/precision-trace.js +86 -0
  5. package/dist/browser/dom/attributes.d.ts +5 -0
  6. package/dist/browser/dom/attributes.js +102 -0
  7. package/dist/browser/dom/component.d.ts +11 -0
  8. package/dist/browser/dom/component.js +255 -0
  9. package/dist/browser/dom/direct-api.d.ts +66 -0
  10. package/dist/browser/dom/direct-api.js +446 -0
  11. package/dist/browser/dom/lifecycle.d.ts +9 -0
  12. package/dist/browser/dom/lifecycle.js +161 -0
  13. package/dist/browser/dom/registry.d.ts +5 -0
  14. package/dist/browser/dom/registry.js +16 -0
  15. package/dist/browser/dom-core.d.ts +51 -249
  16. package/dist/browser/dom-core.js +51 -2521
  17. package/dist/browser/navigation/client-navigation.d.ts +44 -0
  18. package/dist/browser/navigation/client-navigation.js +277 -0
  19. package/dist/browser/navigation/extract-html.d.ts +1 -0
  20. package/dist/browser/navigation/extract-html.js +5 -0
  21. package/dist/browser/navigation/layout-retain.d.ts +3 -0
  22. package/dist/browser/navigation/layout-retain.js +34 -0
  23. package/dist/browser/navigation/locale-transition.d.ts +24 -0
  24. package/dist/browser/navigation/locale-transition.js +323 -0
  25. package/dist/browser/navigation/scroll-focus.d.ts +14 -0
  26. package/dist/browser/navigation/scroll-focus.js +87 -0
  27. package/dist/browser/reactivity/patch-scheduler.d.ts +12 -0
  28. package/dist/browser/reactivity/patch-scheduler.js +720 -0
  29. package/dist/browser/reactivity/write-barrier.d.ts +16 -0
  30. package/dist/browser/reactivity/write-barrier.js +546 -0
  31. package/dist/browser/resume/adopt.d.ts +1 -0
  32. package/dist/browser/resume/adopt.js +168 -0
  33. package/dist/browser/resume/hydrate.d.ts +4 -0
  34. package/dist/browser/resume/hydrate.js +107 -0
  35. package/dist/browser/resume/resume.d.ts +3 -0
  36. package/dist/browser/resume/resume.js +102 -0
  37. package/dist/faces/dom.browser.d.ts +2 -1
  38. package/dist/faces/dom.browser.js +2 -1
  39. package/dist/host/{deployment-registry.d.ts → deployment/deployment-registry.d.ts} +1 -1
  40. package/dist/host/{deployment-registry.js → deployment/deployment-registry.js} +1 -1
  41. package/dist/host/list-client-components.d.ts +2 -2
  42. package/dist/host/list-client-components.js +4 -4
  43. package/dist/host/{render-host.d.ts → render/render-host.d.ts} +2 -2
  44. package/dist/host/{render-host.js → render/render-host.js} +1 -1
  45. package/dist/host/{route-layout-chain.js → render/route-layout-chain.js} +1 -1
  46. package/dist/host/serve/bootstrap.d.ts +1 -0
  47. package/dist/host/serve/bootstrap.js +25 -0
  48. package/dist/host/serve/constants.d.ts +6 -0
  49. package/dist/host/serve/constants.js +6 -0
  50. package/dist/host/serve/hmr-dev.d.ts +34 -0
  51. package/dist/host/serve/hmr-dev.js +294 -0
  52. package/dist/host/serve/http.d.ts +1 -0
  53. package/dist/host/serve/http.js +124 -0
  54. package/dist/host/serve/locale-theme.d.ts +19 -0
  55. package/dist/host/serve/locale-theme.js +169 -0
  56. package/dist/host/serve/page-catalog.d.ts +14 -0
  57. package/dist/host/serve/page-catalog.js +105 -0
  58. package/dist/host/serve/page-render.d.ts +17 -0
  59. package/dist/host/serve/page-render.js +419 -0
  60. package/dist/host/serve/paths.d.ts +3 -0
  61. package/dist/host/serve/paths.js +74 -0
  62. package/dist/host/serve/route-catalog.d.ts +6 -0
  63. package/dist/host/serve/route-catalog.js +108 -0
  64. package/dist/host/serve/state.d.ts +30 -0
  65. package/dist/host/serve/state.js +36 -0
  66. package/dist/host/serve-host.d.ts +0 -17
  67. package/dist/host/serve-host.mjs +4 -1420
  68. package/dist/ssr/document.d.ts +2 -0
  69. package/dist/ssr/document.js +71 -0
  70. package/dist/ssr/dom-ssr.d.ts +10 -47
  71. package/dist/ssr/dom-ssr.js +10 -1284
  72. package/dist/ssr/render.d.ts +2 -0
  73. package/dist/ssr/render.js +78 -0
  74. package/dist/ssr/serialize.d.ts +3 -0
  75. package/dist/ssr/serialize.js +678 -0
  76. package/package.json +11 -11
  77. /package/dist/host/{route-layout-chain.d.ts → render/route-layout-chain.d.ts} +0 -0
@@ -0,0 +1,720 @@
1
+ import { bumpMap, precision, pushTrace } from '../diagnostics/precision-trace.js';
2
+ export function runPatch(inst, fn, depKey = null, bindingId = null) {
3
+ if (precision.enabled) {
4
+ // Specialized trackPatch runs are both patch execs and binding evals (0.2.0).
5
+ precision.patchExecs++;
6
+ precision.bindingEvals++;
7
+ if (depKey) {
8
+ bumpMap(precision.patchesByDep, depKey);
9
+ bumpMap(precision.bindingEvalsByDep, depKey);
10
+ }
11
+ if (bindingId != null) {
12
+ const id = String(bindingId);
13
+ bumpMap(precision.patchesByBinding, id);
14
+ bumpMap(precision.bindingEvalsByBinding, id);
15
+ }
16
+ }
17
+ if (bindingId != null) {
18
+ pushTrace('patch', 'binding', bindingId, depKey);
19
+ }
20
+ return fn.call(inst);
21
+ }
22
+ export function scheduleRefresh(inst, notice) {
23
+ if (!inst || inst.__vmzDestroyed || inst.__vmzQuiet)
24
+ return;
25
+ const n = typeof notice === 'string' ? { type: 'replace', root: notice } : notice;
26
+ if (!n || !n.root)
27
+ return;
28
+ if (precision.enabled) {
29
+ precision.writes++;
30
+ bumpMap(precision.writesByRoot, n.root);
31
+ }
32
+ pushTrace('write', 'field', n.root, n.root);
33
+ if (!inst.__vmzDirtyTrie)
34
+ inst.__vmzDirtyTrie = Object.create(null);
35
+ insertDirtyNotice(inst.__vmzDirtyTrie, n);
36
+ // Transitional: keep notice list for flush loop emptiness check / compat.
37
+ if (!inst.__vmzDirtyNotices)
38
+ inst.__vmzDirtyNotices = [];
39
+ inst.__vmzDirtyNotices.push(n);
40
+ // Inside a UI event (or its sync flush): coalesce; endEventFlush drains without microtask hop.
41
+ if ((inst.__vmzEventDepth || 0) > 0 || inst.__vmzFlushSync) {
42
+ inst.__vmzFlushScheduled = true;
43
+ return;
44
+ }
45
+ if (inst.__vmzFlushScheduled)
46
+ return;
47
+ inst.__vmzFlushScheduled = true;
48
+ queueMicrotask(() => {
49
+ inst.__vmzFlushScheduled = false;
50
+ const p = flushPending(inst);
51
+ if (p && typeof p.then === 'function') {
52
+ p.catch((err) => console.error('vmz:dom flush', err));
53
+ }
54
+ });
55
+ }
56
+ export function inferHandlerMethod(handler) {
57
+ if (typeof handler !== 'function')
58
+ return null;
59
+ if (Object.hasOwn(handler, '__vmzMethod')) {
60
+ return handler.__vmzMethod;
61
+ }
62
+ let name = null;
63
+ try {
64
+ const src = Function.prototype.toString.call(handler);
65
+ const m = src.match(/this\.([A-Za-z_$][\w$]*)\s*\(/);
66
+ name = m ? m[1] : null;
67
+ }
68
+ catch {
69
+ name = null;
70
+ }
71
+ try {
72
+ handler.__vmzMethod = name;
73
+ }
74
+ catch {
75
+ /* non-extensible function */
76
+ }
77
+ return name;
78
+ }
79
+ export function methodAllowsSyncEventFlush(inst, methodName) {
80
+ if (!inst)
81
+ return false;
82
+ if (!methodName)
83
+ return true;
84
+ const table = inst.constructor && inst.constructor.__vmzMethodRw;
85
+ const rw = table && table[methodName];
86
+ if (!rw)
87
+ return true;
88
+ if (rw.async || rw.opaque)
89
+ return false;
90
+ return true;
91
+ }
92
+ export function runDomEventHandler(inst, methodHint, fn) {
93
+ const sync = methodAllowsSyncEventFlush(inst, methodHint);
94
+ if (sync)
95
+ beginEventFlush(inst);
96
+ try {
97
+ return fn();
98
+ }
99
+ finally {
100
+ if (sync)
101
+ endEventFlush(inst);
102
+ }
103
+ }
104
+ export function beginEventFlush(inst) {
105
+ if (!inst)
106
+ return;
107
+ inst.__vmzEventDepth = (inst.__vmzEventDepth || 0) + 1;
108
+ }
109
+ export function endEventFlush(inst) {
110
+ if (!inst)
111
+ return;
112
+ inst.__vmzEventDepth = Math.max(0, (inst.__vmzEventDepth || 0) - 1);
113
+ if (inst.__vmzEventDepth !== 0 || !inst.__vmzFlushScheduled)
114
+ return;
115
+ // Keep __vmzFlushSync so nested writes during flush stay off the microtask path.
116
+ inst.__vmzFlushSync = true;
117
+ try {
118
+ inst.__vmzFlushScheduled = false;
119
+ const ret = flushPending(inst);
120
+ if (ret && typeof ret.then === 'function') {
121
+ ret.catch((err) => console.error('vmz:dom flush', err));
122
+ }
123
+ }
124
+ finally {
125
+ inst.__vmzFlushSync = false;
126
+ // Async binder left dirties: fall back to microtask coalesce.
127
+ if (inst.__vmzFlushScheduled) {
128
+ const again = inst.__vmzFlushScheduled;
129
+ inst.__vmzFlushScheduled = false;
130
+ if (again) {
131
+ inst.__vmzFlushScheduled = true;
132
+ queueMicrotask(() => {
133
+ inst.__vmzFlushScheduled = false;
134
+ const p = flushPending(inst);
135
+ if (p && typeof p.then === 'function') {
136
+ p.catch((err) => console.error('vmz:dom flush', err));
137
+ }
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ export function promoteLeafDirtyToTrie(inst) {
144
+ const ld = inst.__vmzLeafDirty;
145
+ if (!ld || !ld.idxs || !ld.idxs.length) {
146
+ inst.__vmzLeafDirty = null;
147
+ return;
148
+ }
149
+ inst.__vmzLeafDirty = null;
150
+ if (!inst.__vmzDirtyTrie)
151
+ inst.__vmzDirtyTrie = Object.create(null);
152
+ if (!inst.__vmzDirtyNotices)
153
+ inst.__vmzDirtyNotices = [];
154
+ const field = ld.field;
155
+ const root = ld.root;
156
+ for (let k = 0; k < ld.idxs.length; k++) {
157
+ const segs = [String(ld.idxs[k]), field];
158
+ insertDirtyNotice(inst.__vmzDirtyTrie, { type: 'path', root, segs });
159
+ inst.__vmzDirtyNotices.push({ type: 'path', root, segs });
160
+ }
161
+ inst.__vmzFlushScheduled = true;
162
+ }
163
+ function insertDirtyNotice(trie, notice) {
164
+ if (notice.type === 'replace') {
165
+ trie[notice.root] = { replace: true };
166
+ return;
167
+ }
168
+ const segs = notice.segs || [];
169
+ let node = trie[notice.root];
170
+ if (node && node.replace)
171
+ return;
172
+ if (!node) {
173
+ node = { children: Object.create(null) };
174
+ trie[notice.root] = node;
175
+ }
176
+ if (!segs.length) {
177
+ trie[notice.root] = { replace: true };
178
+ return;
179
+ }
180
+ if (!node.children)
181
+ node.children = Object.create(null);
182
+ let cur = node;
183
+ for (let i = 0; i < segs.length; i++) {
184
+ const seg = segs[i];
185
+ if (cur.dirty)
186
+ return; // ancestor already dirty
187
+ if (!cur.children)
188
+ cur.children = Object.create(null);
189
+ if (i === segs.length - 1) {
190
+ cur.children[seg] = { dirty: true };
191
+ return;
192
+ }
193
+ let next = cur.children[seg];
194
+ if (!next) {
195
+ next = { children: Object.create(null) };
196
+ cur.children[seg] = next;
197
+ }
198
+ else if (next.dirty) {
199
+ return;
200
+ }
201
+ else if (!next.children) {
202
+ next.children = Object.create(null);
203
+ }
204
+ cur = next;
205
+ }
206
+ }
207
+ export function flushPending(inst) {
208
+ if (!inst || inst.__vmzDestroyed)
209
+ return undefined;
210
+ inst.__vmzFlushScheduled = false;
211
+ // rowKernel leaf batch (event update): apply before trie emptiness check.
212
+ if (typeof inst.__vmzDrainLeafDirty === 'function') {
213
+ try {
214
+ inst.__vmzDrainLeafDirty();
215
+ }
216
+ catch (err) {
217
+ console.error('vmz:dom leafDirty', err);
218
+ }
219
+ }
220
+ let guard = 0;
221
+ while (!inst.__vmzDestroyed &&
222
+ (dirtyTrieHasEntries(inst.__vmzDirtyTrie) || (inst.__vmzDirtyNotices && inst.__vmzDirtyNotices.length > 0)) &&
223
+ guard++ < 64) {
224
+ const trie = inst.__vmzDirtyTrie || Object.create(null);
225
+ inst.__vmzDirtyTrie = Object.create(null);
226
+ if (inst.__vmzDirtyNotices)
227
+ inst.__vmzDirtyNotices.length = 0;
228
+ inst.__vmzFlushTrie = trie;
229
+ const jobs = [];
230
+ // Prefer BindingId scheduling (IR). String `__vmzBinders` is adapter-only.
231
+ // Pass `trie` into refresh ??dirty map is cleared above before patches run.
232
+ try {
233
+ const bindingIds = bindingIdsMatchingTrie(inst, trie);
234
+ const coveredDeps = Object.create(null);
235
+ for (const id of bindingIds) {
236
+ const entry = inst.__vmzBindings && inst.__vmzBindings[id];
237
+ if (entry) {
238
+ for (const d of entry.deps || [])
239
+ coveredDeps[d] = true;
240
+ }
241
+ jobs.push(...refreshBinding(inst, id, trie));
242
+ }
243
+ for (const key of binderKeysMatchingTrie(inst, trie)) {
244
+ if (coveredDeps[key] || (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length)) {
245
+ // BindingId path already flushed IR patches for this dep.
246
+ // Still run binder-only patches (bindComponentProp uses bindingId null).
247
+ jobs.push(...refreshFieldBinderOnly(inst, key));
248
+ continue;
249
+ }
250
+ jobs.push(...refreshField(inst, key));
251
+ }
252
+ const pending = jobs.filter((j) => j && typeof j.then === 'function');
253
+ if (pending.length) {
254
+ // Async binders: resume after they settle (default microtask path after).
255
+ return Promise.all(pending).then(() => flushPending(inst));
256
+ }
257
+ }
258
+ finally {
259
+ inst.__vmzFlushTrie = null;
260
+ }
261
+ }
262
+ return undefined;
263
+ }
264
+ function dirtyTrieHasEntries(trie) {
265
+ if (!trie)
266
+ return false;
267
+ for (const _ in trie)
268
+ return true;
269
+ return false;
270
+ }
271
+ function bindingIdsMatchingTrie(inst, trie) {
272
+ const index = inst.__vmzDepToBindings;
273
+ if (!index)
274
+ return [];
275
+ const out = [];
276
+ const seen = Object.create(null);
277
+ for (const key of Object.keys(index)) {
278
+ if (!depMatchesTrie(trie, key))
279
+ continue;
280
+ for (const id of index[key]) {
281
+ const k = String(id);
282
+ if (seen[k])
283
+ continue;
284
+ seen[k] = true;
285
+ out.push(id);
286
+ }
287
+ }
288
+ return out;
289
+ }
290
+ function binderKeysMatchingTrie(inst, trie) {
291
+ const binders = inst.__vmzBinders;
292
+ if (!binders)
293
+ return [];
294
+ const out = [];
295
+ for (const key of Object.keys(binders)) {
296
+ if (depMatchesTrie(trie, key))
297
+ out.push(key);
298
+ }
299
+ return out;
300
+ }
301
+ function depMatchesTrie(trie, key) {
302
+ const root = depRootField(key);
303
+ const node = trie[root];
304
+ if (!node)
305
+ return false;
306
+ if (node.replace) {
307
+ return key === root || key === `${root}.*` || key.startsWith(`${root}.`) || key.startsWith(`${root}[`);
308
+ }
309
+ if (key === `${root}.*`) {
310
+ // Bare `field.*` soft/structure channel: item replace / array structure only ?? // NOT deep leaf writes (`tags.0.label`); those use `tags.*.label` BindingId.
311
+ return structureStarMatches(node);
312
+ }
313
+ // Bare field: replace-only.
314
+ if (key === root)
315
+ return false;
316
+ // Path channel: `tags.*.label` ??wildcard index under list root.
317
+ const starPrefix = `${root}.*`;
318
+ if (key === starPrefix || key.startsWith(`${starPrefix}.`)) {
319
+ const rest = key === starPrefix
320
+ ? []
321
+ : key
322
+ .slice(starPrefix.length + 1)
323
+ .split('.')
324
+ .filter(Boolean);
325
+ return wildcardIndexDirtyCovers(node, rest);
326
+ }
327
+ // Stable ListItem form `tags[key=?].label` ??treat `[key=?]` as wildcard index.
328
+ if (key.startsWith(`${root}[`)) {
329
+ const afterBracket = key.indexOf(']');
330
+ if (afterBracket > root.length) {
331
+ const rest = key.length > afterBracket + 1 && key[afterBracket + 1] === '.'
332
+ ? key
333
+ .slice(afterBracket + 2)
334
+ .split('.')
335
+ .filter(Boolean)
336
+ : [];
337
+ return wildcardIndexDirtyCovers(node, rest);
338
+ }
339
+ }
340
+ const segs = key
341
+ .slice(root.length + 1)
342
+ .split('.')
343
+ .filter(Boolean);
344
+ return pathDirtyCovers(node, segs);
345
+ }
346
+ function structureStarMatches(node) {
347
+ if (!node)
348
+ return false;
349
+ if (node.replace || node.dirty)
350
+ return true;
351
+ if (!node.children)
352
+ return false;
353
+ for (const idx of Object.keys(node.children)) {
354
+ const child = node.children[idx];
355
+ // Index node dirty/replace ??item identity changed.
356
+ if (child && (child.replace || child.dirty))
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+ function wildcardIndexDirtyCovers(node, restSegs) {
362
+ if (!node || node.replace)
363
+ return !!node?.replace;
364
+ if (node.dirty)
365
+ return true;
366
+ if (!node.children)
367
+ return false;
368
+ for (const idx of Object.keys(node.children)) {
369
+ const child = node.children[idx];
370
+ if (restSegs.length === 0) {
371
+ if (trieHasAnyDirty(child))
372
+ return true;
373
+ }
374
+ else if (pathDirtyCovers(child, restSegs)) {
375
+ return true;
376
+ }
377
+ }
378
+ return false;
379
+ }
380
+ function trieHasAnyDirty(node) {
381
+ if (!node || node.replace)
382
+ return !!node;
383
+ if (node.dirty)
384
+ return true;
385
+ if (!node.children)
386
+ return false;
387
+ for (const k of Object.keys(node.children)) {
388
+ if (trieHasAnyDirty(node.children[k]))
389
+ return true;
390
+ }
391
+ return false;
392
+ }
393
+ function pathDirtyCovers(node, depSegs) {
394
+ let cur = node;
395
+ for (let i = 0; i < depSegs.length; i++) {
396
+ if (!cur || cur.replace)
397
+ return !!cur?.replace;
398
+ if (cur.dirty)
399
+ return true; // write parent covers this dep
400
+ if (!cur.children)
401
+ return false;
402
+ const next = cur.children[depSegs[i]];
403
+ if (!next) {
404
+ // No write along this dep path ??but a write under a prefix?
405
+ return false;
406
+ }
407
+ cur = next;
408
+ }
409
+ // Reached dep node: wake if dirty here or any dirty descendant (write under dep).
410
+ return trieHasAnyDirty(cur);
411
+ }
412
+ function noticeMatchesDepKey(notice, key) {
413
+ const trie = Object.create(null);
414
+ insertDirtyNotice(trie, notice);
415
+ return depMatchesTrie(trie, key);
416
+ }
417
+ function depRootField(dep) {
418
+ if (!dep)
419
+ return '';
420
+ const star = dep.indexOf('.*');
421
+ if (star >= 0)
422
+ return dep.slice(0, star);
423
+ const dot = dep.indexOf('.');
424
+ if (dot >= 0)
425
+ return dep.slice(0, dot);
426
+ const bracket = dep.indexOf('[');
427
+ if (bracket >= 0)
428
+ return dep.slice(0, bracket);
429
+ return dep;
430
+ }
431
+ function refreshBinding(inst, bindingId, dirtyTrie = null) {
432
+ const entry = inst.__vmzBindings && inst.__vmzBindings[bindingId];
433
+ const jobs = [];
434
+ if (!inst || inst.__vmzDestroyed || bindingId == null || !entry) {
435
+ return jobs;
436
+ }
437
+ const depKey = (entry.deps && entry.deps[0]) || null;
438
+ const trie = dirtyTrie || inst.__vmzDirtyTrie;
439
+ const allowIdx = itemIndicesAllowedForDeps(trie, entry.deps);
440
+ // Snapshot: keyed-each items share IR BindingIds; cf patches may untrack/retrack
441
+ // mid-flush and mutate `entry.patches` (would skip sibling item attrs like aria-selected).
442
+ const patches = entry.patches.slice();
443
+ for (const fn of patches) {
444
+ if (allowIdx && !patchMatchesDirtyIndex(fn, allowIdx))
445
+ continue;
446
+ try {
447
+ const ret = runPatch(inst, fn, depKey, bindingId);
448
+ if (ret && typeof ret.then === 'function')
449
+ jobs.push(ret);
450
+ }
451
+ catch (err) {
452
+ console.error('vmz:dom patch', err);
453
+ }
454
+ }
455
+ return jobs;
456
+ }
457
+ function itemIndicesAllowedForDeps(trie, deps) {
458
+ if (!trie || !deps || !deps.length)
459
+ return null;
460
+ let sawListChannel = false;
461
+ let allow = null;
462
+ for (const dep of deps) {
463
+ const root = depRootField(dep);
464
+ if (!root)
465
+ continue;
466
+ const starPrefix = `${root}.*`;
467
+ const isListChannel = dep === starPrefix || dep.startsWith(`${starPrefix}.`) || (dep.startsWith(`${root}[`) && dep.includes(']'));
468
+ if (!isListChannel)
469
+ return null;
470
+ sawListChannel = true;
471
+ const node = trie[root];
472
+ if (!node)
473
+ continue;
474
+ if (node.replace || node.dirty)
475
+ return null; // whole list
476
+ if (!node.children)
477
+ continue;
478
+ if (!allow)
479
+ allow = new Set();
480
+ for (const idx of Object.keys(node.children)) {
481
+ const child = node.children[idx];
482
+ if (!child)
483
+ continue;
484
+ if (child.replace || child.dirty || trieHasAnyDirty(child)) {
485
+ allow.add(String(idx));
486
+ }
487
+ }
488
+ }
489
+ if (!sawListChannel)
490
+ return null;
491
+ return allow && allow.size ? allow : null;
492
+ }
493
+ function soleDirtyItemField(trie, listRoot, allowIdx) {
494
+ if (!trie || !listRoot || !allowIdx || !allowIdx.size)
495
+ return null;
496
+ const node = trie[listRoot];
497
+ if (!node || node.replace || node.dirty || !node.children)
498
+ return null;
499
+ // Peek first index for the sole dirty field, then verify the rest match.
500
+ // Fast path: index child has a single key (typical WritePath leaf) ??no sibling scan.
501
+ let field = null;
502
+ for (const idx of allowIdx) {
503
+ const child = node.children[String(idx)];
504
+ if (!child || child.replace || child.dirty || !child.children)
505
+ return null;
506
+ const kids = child.children;
507
+ const keys = Object.keys(kids);
508
+ if (keys.length === 1) {
509
+ const f = keys[0];
510
+ const n = kids[f];
511
+ if (!n || !(n.dirty || n.replace || trieHasAnyDirty(n)))
512
+ return null;
513
+ if (field == null)
514
+ field = f;
515
+ else if (field !== f)
516
+ return null;
517
+ continue;
518
+ }
519
+ if (field == null) {
520
+ for (let k = 0; k < keys.length; k++) {
521
+ const f = keys[k];
522
+ const n = kids[f];
523
+ if (n && (n.dirty || n.replace || trieHasAnyDirty(n))) {
524
+ if (field != null)
525
+ return null;
526
+ field = f;
527
+ }
528
+ }
529
+ if (field == null)
530
+ return null;
531
+ continue;
532
+ }
533
+ const n = kids[field];
534
+ if (!n || !(n.dirty || n.replace || trieHasAnyDirty(n)))
535
+ return null;
536
+ for (let k = 0; k < keys.length; k++) {
537
+ const f = keys[k];
538
+ if (f === field)
539
+ continue;
540
+ const o = kids[f];
541
+ if (o && (o.dirty || o.replace || trieHasAnyDirty(o)))
542
+ return null;
543
+ }
544
+ }
545
+ return field;
546
+ }
547
+ function dirtyItemFieldsAt(trie, listRoot, idx) {
548
+ if (!trie || !listRoot)
549
+ return null;
550
+ const node = trie[listRoot];
551
+ if (!node)
552
+ return [];
553
+ if (node.replace || node.dirty)
554
+ return null;
555
+ const child = node.children && node.children[String(idx)];
556
+ if (!child)
557
+ return [];
558
+ if (child.replace || child.dirty)
559
+ return null;
560
+ if (!child.children)
561
+ return null;
562
+ const out = [];
563
+ for (const field of Object.keys(child.children)) {
564
+ const n = child.children[field];
565
+ if (n && (n.dirty || n.replace || trieHasAnyDirty(n)))
566
+ out.push(field);
567
+ }
568
+ return out;
569
+ }
570
+ function patchMatchesDirtyIndex(fn, allowIdx) {
571
+ const idx = fn && fn.__vmzItemIndex;
572
+ if (idx == null || idx === '')
573
+ return true;
574
+ return allowIdx.has(String(idx));
575
+ }
576
+ function refreshField(inst, field) {
577
+ const binders = inst.__vmzBinders;
578
+ const jobs = [];
579
+ if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
580
+ return jobs;
581
+ }
582
+ // Snapshot: patches may untrack/retrack and mutate the binder list mid-loop.
583
+ const list = binders[field].slice();
584
+ for (const fn of list) {
585
+ try {
586
+ const ret = runPatch(inst, fn, field, null);
587
+ if (ret && typeof ret.then === 'function')
588
+ jobs.push(ret);
589
+ }
590
+ catch (err) {
591
+ console.error('vmz:dom patch', err);
592
+ }
593
+ }
594
+ return jobs;
595
+ }
596
+ function refreshFieldBinderOnly(inst, field) {
597
+ const binders = inst.__vmzBinders;
598
+ const jobs = [];
599
+ if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
600
+ return jobs;
601
+ }
602
+ const list = binders[field].slice();
603
+ for (const fn of list) {
604
+ if (patchHasBindingId(inst, fn))
605
+ continue;
606
+ try {
607
+ const ret = runPatch(inst, fn, field, null);
608
+ if (ret && typeof ret.then === 'function')
609
+ jobs.push(ret);
610
+ }
611
+ catch (err) {
612
+ console.error('vmz:dom patch', err);
613
+ }
614
+ }
615
+ return jobs;
616
+ }
617
+ function reindexBindingDeps(inst, bindingId, deps) {
618
+ if (!inst.__vmzDepToBindings)
619
+ inst.__vmzDepToBindings = Object.create(null);
620
+ const entry = inst.__vmzBindings[bindingId];
621
+ if (!entry)
622
+ return;
623
+ for (const dep of entry.deps || []) {
624
+ const list = inst.__vmzDepToBindings[dep];
625
+ if (!list)
626
+ continue;
627
+ const j = list.indexOf(bindingId);
628
+ if (j >= 0)
629
+ list.splice(j, 1);
630
+ if (list.length === 0)
631
+ delete inst.__vmzDepToBindings[dep];
632
+ }
633
+ entry.deps = [...(deps || [])];
634
+ for (const dep of entry.deps) {
635
+ if (!inst.__vmzDepToBindings[dep])
636
+ inst.__vmzDepToBindings[dep] = [];
637
+ if (!inst.__vmzDepToBindings[dep].includes(bindingId)) {
638
+ inst.__vmzDepToBindings[dep].push(bindingId);
639
+ }
640
+ }
641
+ }
642
+ export function registerBind(inst, deps, fn, bindingId = null) {
643
+ if (!inst.__vmzBinders)
644
+ inst.__vmzBinders = Object.create(null);
645
+ for (const dep of deps || []) {
646
+ if (!inst.__vmzBinders[dep])
647
+ inst.__vmzBinders[dep] = [];
648
+ // Dedup: cf untrack/retrack with identical deps must not grow the binder list.
649
+ if (!inst.__vmzBinders[dep].includes(fn))
650
+ inst.__vmzBinders[dep].push(fn);
651
+ }
652
+ if (bindingId == null)
653
+ return;
654
+ if (!inst.__vmzBindings)
655
+ inst.__vmzBindings = Object.create(null);
656
+ let entry = inst.__vmzBindings[bindingId];
657
+ if (!entry) {
658
+ entry = { id: bindingId, deps: [], patches: [] };
659
+ inst.__vmzBindings[bindingId] = entry;
660
+ }
661
+ if (!entry.patches.includes(fn))
662
+ entry.patches.push(fn);
663
+ reindexBindingDeps(inst, bindingId, deps || []);
664
+ }
665
+ export function unregisterBind(inst, deps, fn, bindingId = null) {
666
+ const binders = inst.__vmzBinders;
667
+ if (binders) {
668
+ for (const dep of deps || []) {
669
+ const list = binders[dep];
670
+ if (!list)
671
+ continue;
672
+ const i = list.indexOf(fn);
673
+ if (i >= 0)
674
+ list.splice(i, 1);
675
+ if (list.length === 0)
676
+ delete binders[dep];
677
+ }
678
+ }
679
+ if (bindingId == null || !inst.__vmzBindings)
680
+ return;
681
+ const entry = inst.__vmzBindings[bindingId];
682
+ if (!entry)
683
+ return;
684
+ const i = entry.patches.indexOf(fn);
685
+ if (i >= 0)
686
+ entry.patches.splice(i, 1);
687
+ if (entry.patches.length === 0) {
688
+ reindexBindingDeps(inst, bindingId, []);
689
+ delete inst.__vmzBindings[bindingId];
690
+ }
691
+ }
692
+ export function hasMeaningfulChild(el) {
693
+ for (const n of el.childNodes) {
694
+ if (n.nodeType === 1)
695
+ return true;
696
+ if (n.nodeType === 3 && String(n.textContent).trim() !== '')
697
+ return true;
698
+ }
699
+ return false;
700
+ }
701
+ function patchHasBindingId(inst, fn) {
702
+ const bindings = inst && inst.__vmzBindings;
703
+ if (!bindings)
704
+ return false;
705
+ for (const id of Object.keys(bindings)) {
706
+ const patches = bindings[id].patches;
707
+ if (patches && patches.includes(fn))
708
+ return true;
709
+ }
710
+ return false;
711
+ }
712
+ function tagItemPatches(patches, index) {
713
+ if (!patches)
714
+ return;
715
+ const idx = String(index);
716
+ for (const p of patches) {
717
+ if (typeof p === 'function')
718
+ p.__vmzItemIndex = idx;
719
+ }
720
+ }