@ape-egg/vibe 4.0.1 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/boot.js +10 -9
- package/component.js +0 -29
- package/hot-module-refresh.js +0 -0
- package/index.js +0 -28
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +0 -56
- package/runtime/affected.js +6 -189
- package/runtime/cleanup.js +22 -71
- package/runtime/component-cache.js +0 -53
- package/runtime/component.js +6 -407
- package/runtime/conditionals.js +1 -143
- package/runtime/constants.js +19 -82
- package/runtime/debug.js +22 -47
- package/runtime/dispatch.js +0 -46
- package/runtime/hydrate.js +11 -130
- package/runtime/index.js +9 -382
- package/runtime/inert.js +18 -0
- package/runtime/iterate.js +7 -595
- package/runtime/iteration-utils.js +18 -71
- package/runtime/loop-scope.js +0 -58
- package/runtime/manifest.js +0 -27
- package/runtime/parse.js +2 -116
- package/runtime/pre-compiled-iterations.js +3 -51
- package/runtime/pre-compiled-manifest.js +6 -169
- package/runtime/raw-html.js +0 -5
- package/runtime/reconcile.js +4 -159
- package/runtime/staging.js +0 -57
- package/runtime/state.js +0 -61
- package/runtime/this-scope.js +0 -17
- package/runtime/tracking.js +0 -65
- package/runtime/utils.js +1 -144
- package/runtime/vibe-css.js +37 -0
- package/spa.js +0 -76
- package/vibe.css +5 -44
package/runtime/iterate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import parse from './parse.js';
|
|
2
2
|
import affected, { nodeSubscriberOf } from './affected.js';
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
|
-
import { resolvePath, getItemKey, computeDiff, iterPropWrappersOf, bumpIterPropGeneration } from './iteration-utils.js';
|
|
4
|
+
import { resolvePath, getItemKey, computeDiff, iterPropWrappersOf, bumpIterPropGeneration, markBoundValues } from './iteration-utils.js';
|
|
5
5
|
import { resolveThisPath, evalInScope, findComponentIdForElement, rememberScopedKeys, overlayKeysOf } from './utils.js';
|
|
6
6
|
import { recordRead, beginTracking, endTracking, pruneDisconnected, markAlways } from './tracking.js';
|
|
7
7
|
import { managedNodes, extractDependencies } from './conditionals.js';
|
|
@@ -15,25 +15,12 @@ import {
|
|
|
15
15
|
THIS_PROP_REGEX,
|
|
16
16
|
} from './constants.js';
|
|
17
17
|
|
|
18
|
-
// Pre-compiled iteration optimization (production)
|
|
19
18
|
import * as compiled from './pre-compiled-iterations.js';
|
|
20
19
|
|
|
21
20
|
import { generateComponentId, executeCompiledComponentScriptsIn } from './component.js';
|
|
22
21
|
|
|
23
|
-
// Runtime batch-render helpers for full-replacement of simple templates.
|
|
24
|
-
// Build an HTML string via template-literal compilation, then parse once —
|
|
25
|
-
// avoids per-item clone/parse/hydrate in the hot path.
|
|
26
|
-
// Only used for templates without nested <!-- each --> / <!-- if -->.
|
|
27
|
-
//
|
|
28
|
-
// Invariant: batch produces identical DOM to the clone+hydrate path for any
|
|
29
|
-
// input. New binding forms must be supported in both paths simultaneously,
|
|
30
|
-
// with equivalence tests under tests/e2e/batch-vs-clone-equivalence.spec.js
|
|
31
|
-
// — the test harness flips a force-clone flag and compares region-by-region,
|
|
32
|
-
// so silent divergence between the two paths fails loudly.
|
|
33
22
|
const batchParseTemplate = document.createElement('template');
|
|
34
23
|
|
|
35
|
-
// innerHTML serialization encodes <, >, &, ", ' inside attribute values.
|
|
36
|
-
// Decode them back before wrapping @[expr] in ${...} for the template literal.
|
|
37
24
|
const decodeEntities = (s) => s
|
|
38
25
|
.replace(/</g, '<')
|
|
39
26
|
.replace(/>/g, '>')
|
|
@@ -52,27 +39,12 @@ const hasNestedStructures = (tree) => {
|
|
|
52
39
|
return false;
|
|
53
40
|
};
|
|
54
41
|
|
|
55
|
-
// Batch render template-literal-interpolates `@[expr]` as `${expr}` — that
|
|
56
|
-
// stringifies object/array values, which breaks `<component src>` props that
|
|
57
|
-
// rely on resolveIterationComponentProps to stash non-primitives in the
|
|
58
|
-
// registry. Templates carrying any `<component src>` go through the
|
|
59
|
-
// clone+hydrate path instead.
|
|
60
42
|
const hasComponentSrc = (templateEl) =>
|
|
61
43
|
!!templateEl.querySelector?.('component[src], div.component[src]');
|
|
62
44
|
|
|
63
|
-
// A compiled inlined component with its own setup script carries component-local
|
|
64
|
-
// state (`component({...})` → `$._cN`). Batch render emits one shared HTML string
|
|
65
|
-
// per row, which would duplicate the baked `_cN` id across rows and collapse
|
|
66
|
-
// their state into one bucket. Route these through the clone path, where
|
|
67
|
-
// initializeBlock isolates each row's component ids.
|
|
68
45
|
const hasInlinedComponentScript = (templateEl) =>
|
|
69
46
|
!!templateEl.querySelector?.('script[type="vibe-module"]');
|
|
70
47
|
|
|
71
|
-
// `__vibe.forceClonePath` is a debug/test escape hatch — set it to
|
|
72
|
-
// route every iteration through the clone+hydrate path, even templates that
|
|
73
|
-
// would otherwise qualify for batch. Used by the batch-vs-clone-equivalence
|
|
74
|
-
// test harness so the same templates can be rendered through both paths and
|
|
75
|
-
// compared. Not part of the public API.
|
|
76
48
|
const canUseBatchRender = (template) =>
|
|
77
49
|
!globalThis.__vibe?.forceClonePath &&
|
|
78
50
|
!hasNestedStructures(template) &&
|
|
@@ -80,17 +52,7 @@ const canUseBatchRender = (template) =>
|
|
|
80
52
|
!hasComponentSrc(template.element) &&
|
|
81
53
|
!hasInlinedComponentScript(template.element);
|
|
82
54
|
|
|
83
|
-
// Patterns used by compileBatchFn to recognize bindings in attribute-name and
|
|
84
|
-
// attribute-value positions. The inner alternation mirrors BINDING_INNER from
|
|
85
|
-
// constants.js — it has to allow nested brackets and quoted strings inside the
|
|
86
|
-
// expression so things like `items[0]` or `x.replace(',', '')` round-trip
|
|
87
|
-
// safely. Kept inline rather than re-exported because they live entirely
|
|
88
|
-
// within this compile-time string transformation.
|
|
89
55
|
const BATCH_BINDING_INNER = String.raw`(?:[^\[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+`;
|
|
90
|
-
// Note: lookahead allows `=` because innerHTML serializes a bare attribute
|
|
91
|
-
// name (no value) as `name=""`, so a name-binding written `<icon @[attrName]>`
|
|
92
|
-
// arrives here as `@[attrname]=""`. The leading `\s` keeps this from matching
|
|
93
|
-
// attribute-value bindings like `attr="@[x]"` (preceded by `"`, not whitespace).
|
|
94
56
|
const BATCH_NAME_BINDING_REGEX = new RegExp(
|
|
95
57
|
String.raw`(\s)@\[(${BATCH_BINDING_INNER})\](?:="")?(?=[\s/>])`,
|
|
96
58
|
'g',
|
|
@@ -100,10 +62,6 @@ const BATCH_ATTR_BINDING_REGEX = new RegExp(
|
|
|
100
62
|
'g',
|
|
101
63
|
);
|
|
102
64
|
|
|
103
|
-
// Apply fn to tag spans only (`<el ...>`, quote-aware so a `>` inside an
|
|
104
|
-
// attribute value doesn't end the span), leaving text spans and comments
|
|
105
|
-
// untouched. Used to scope the name-binding rewrite to positions where a
|
|
106
|
-
// name binding can actually occur.
|
|
107
65
|
const mapTagSpans = (html, fn) => {
|
|
108
66
|
let out = '';
|
|
109
67
|
let i = 0;
|
|
@@ -134,58 +92,22 @@ const mapTagSpans = (html, fn) => {
|
|
|
134
92
|
return out;
|
|
135
93
|
};
|
|
136
94
|
|
|
137
|
-
// Whether a hydrate'd attribute is a "value-style" string attr (kept verbatim)
|
|
138
|
-
// rather than a boolean-coerced attr (added/removed by truthiness). Mirrors
|
|
139
|
-
// the predicate hydrate.js uses, so batch and clone classify identically.
|
|
140
95
|
const isValueStyleAttr = (attrName) =>
|
|
141
96
|
VALUE_ATTRS.includes(attrName) ||
|
|
142
97
|
attrName.startsWith('data-') ||
|
|
143
98
|
attrName.startsWith('aria-') ||
|
|
144
99
|
attrName.startsWith('on');
|
|
145
100
|
|
|
146
|
-
// Compile the iteration template into a single function that emits the full
|
|
147
|
-
// HTML for any input array. The function produced here must match the DOM
|
|
148
|
-
// shape of the clone+hydrate path for any binding form — gaps documented in
|
|
149
|
-
// the prior iteration of `do-this-job.md` were closed here:
|
|
150
|
-
//
|
|
151
|
-
// 1. `this.X` rewriting: pre-rewritten at compile time using the iteration's
|
|
152
|
-
// anchor element to resolve the owning componentId. No per-item cost.
|
|
153
|
-
// 2. Boolean-coerced attributes: emitted as conditional template literal
|
|
154
|
-
// expressions so the attribute is absent when the binding evaluates falsy
|
|
155
|
-
// and present (with empty string value, matching hydrate.js) when truthy.
|
|
156
|
-
// 3. DOM properties (value/checked/selected): emitted as attributes for
|
|
157
|
-
// runtime DOM accuracy AND collected into `domPropertyWrites` so
|
|
158
|
-
// renderBatch can run a small post-stamp loop that sets the actual DOM
|
|
159
|
-
// property on each instance — the attribute alone isn't enough.
|
|
160
|
-
// 4. Name bindings (`<el @[expr]>`): emitted as conditional template literal
|
|
161
|
-
// expressions that produce ` resolvedName=""` when truthy and nothing
|
|
162
|
-
// when falsy.
|
|
163
|
-
//
|
|
164
|
-
// Anything the clone path renders correctly, the function returned here has
|
|
165
|
-
// to render identically. New binding forms must update both paths in lockstep
|
|
166
|
-
// or the equivalence harness in tests/e2e/batch-vs-clone-equivalence.spec.js
|
|
167
|
-
// will catch the divergence.
|
|
168
|
-
// Root-position `_cN` component references inside a binding expression —
|
|
169
|
-
// string literals are consumed whole and left untouched (an `_cN` inside a
|
|
170
|
-
// quoted string is content, not a reference — same discipline as
|
|
171
|
-
// compileExpression's alternation in utils.js; the two renderers are
|
|
172
|
-
// lockstep-equivalent by contract). Already-canonical `$['_cN']` forms and
|
|
173
|
-
// property accesses stay untouched via the lookbehind.
|
|
174
101
|
const CID_ROOT_REGEX =
|
|
175
102
|
/('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|(?<![\w$.])_c(\d+)\b/g;
|
|
176
103
|
|
|
177
104
|
export const canonicalizeComponentIds = (expr) =>
|
|
178
105
|
expr.replace(CID_ROOT_REGEX, (m, literal, id) => (literal !== undefined ? literal : `$['_c${id}']`));
|
|
179
106
|
|
|
180
|
-
// Name-binding emission: resolve the expression under the `_e` guard, emit
|
|
181
|
-
// ` value=""` when truthy, nothing otherwise.
|
|
182
107
|
const nameBindingEmit = (exprSrc) =>
|
|
183
108
|
'${(()=>{const _v=_e(()=>(' + exprSrc + '));return _v?\' \'+_v+\'=""\':\'\';})()}';
|
|
184
109
|
|
|
185
110
|
export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, anchorEl) => {
|
|
186
|
-
// Walk a clone of the template DOM so we can stash a transient
|
|
187
|
-
// `data-vibe-batch` marker on every element with DOM-property bindings
|
|
188
|
-
// without polluting the cached template that subsequent renders read.
|
|
189
111
|
const tplClone = template.element.cloneNode(true);
|
|
190
112
|
const componentId = anchorEl ? findComponentIdForElement(anchorEl) : null;
|
|
191
113
|
const domPropertyWrites = [];
|
|
@@ -212,20 +134,12 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
212
134
|
|
|
213
135
|
let templateHtml = tplClone.innerHTML.trim();
|
|
214
136
|
|
|
215
|
-
// A name-binding the compiler relocated into `data-vibe-namebind` (its expression has
|
|
216
|
-
// whitespace, so it can't be an HTML attribute name) — restore the `@[expr]=""` form
|
|
217
|
-
// the name-binding pass below understands. Safe in this template STRING: only the
|
|
218
|
-
// resolved value (no whitespace) ever reaches innerHTML. Mirrors the clone path, which
|
|
219
|
-
// reads the same binding from the manifest's nameBindings.
|
|
220
137
|
templateHtml = templateHtml.replace(
|
|
221
138
|
/\sdata-vibe-namebind="((?:@\[(?:[^[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\])+)"/g,
|
|
222
139
|
(_, bindings) =>
|
|
223
140
|
bindings.replace(/@\[(?:[^[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\]/g, (b) => ' ' + b + '=""'),
|
|
224
141
|
);
|
|
225
142
|
|
|
226
|
-
// Rewrite `this.X` only inside @[…] expressions so literal occurrences in
|
|
227
|
-
// text content (e.g. a code example explaining `this.foo`) aren't mangled.
|
|
228
|
-
// Mirrors evalInScope's behavior in the clone path.
|
|
229
143
|
if (componentId) {
|
|
230
144
|
BINDING_REGEX.lastIndex = 0;
|
|
231
145
|
templateHtml = templateHtml.replace(BINDING_REGEX, (_, expr) =>
|
|
@@ -233,12 +147,6 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
233
147
|
);
|
|
234
148
|
}
|
|
235
149
|
|
|
236
|
-
// Compiler-stamped component references (`_cN.x` in SPA fragments) resolve
|
|
237
|
-
// through `$` at call time — the mirror of evalInScope's id canonicalization.
|
|
238
|
-
// Baked as bare identifiers they'd be a ReferenceError whenever the id is
|
|
239
|
-
// missing from the compile-time state-key snapshot (a fragment iteration can
|
|
240
|
-
// hydrate before its component script registers), and a state key that
|
|
241
|
-
// arrives later would never be picked up by the cached batch function.
|
|
242
150
|
BINDING_REGEX.lastIndex = 0;
|
|
243
151
|
templateHtml = templateHtml.replace(BINDING_REGEX, (_, expr) =>
|
|
244
152
|
'@[' + canonicalizeComponentIds(expr) + ']',
|
|
@@ -251,25 +159,6 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
251
159
|
|
|
252
160
|
let code = escaped;
|
|
253
161
|
|
|
254
|
-
// Name bindings: `<el @[expr]>` — emit ` resolvedName=""` when truthy, else
|
|
255
|
-
// emit nothing. The lookahead `(?=[\s/>])` distinguishes name-position
|
|
256
|
-
// bindings from attribute-value-position bindings (which are followed by `=`)
|
|
257
|
-
// — but a TEXT-position binding on its own line is also whitespace-bounded,
|
|
258
|
-
// so the pass runs only over tag spans (mapTagSpans): name bindings can only
|
|
259
|
-
// exist inside a tag.
|
|
260
|
-
//
|
|
261
|
-
// HTML parses attribute names lowercase, so a binding like
|
|
262
|
-
// `<icon @[attrName]>` arrives here as `@[attrname]` and a dotted form like
|
|
263
|
-
// `<icon @[obj.iconName]>` arrives as `@[obj.iconname]`. We recover the
|
|
264
|
-
// proper case in two places:
|
|
265
|
-
// - Single identifier → resolve against `stateKeys` at compile time and
|
|
266
|
-
// emit a direct identifier reference.
|
|
267
|
-
// - Dotted path → split off the first segment (resolved at compile time
|
|
268
|
-
// against `stateKeys` + iteration aliases), then emit a runtime call to
|
|
269
|
-
// `_walkCi` (injected into the function body below) for the remaining
|
|
270
|
-
// segments. Mirrors `resolveCaseInsensitivePath` from utils.js.
|
|
271
|
-
// Bracket / call expressions pass through unchanged — they need a real
|
|
272
|
-
// evaluator and aren't worth special-casing here.
|
|
273
162
|
let needsCiWalker = false;
|
|
274
163
|
code = mapTagSpans(code, (tag) => tag.replace(BATCH_NAME_BINDING_REGEX, (_, _ws, expr) => {
|
|
275
164
|
let decExpr = decodeEntities(expr);
|
|
@@ -291,11 +180,6 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
291
180
|
return nameBindingEmit('_walkCi(' + ciHead + ',' + tailJSON + ')');
|
|
292
181
|
}));
|
|
293
182
|
|
|
294
|
-
// Pure-binding attributes (`attr="@[expr]"`) — classify by attribute name:
|
|
295
|
-
// DOM property → emit attribute (post-stamp also writes the property)
|
|
296
|
-
// value-style → emit attribute as-is
|
|
297
|
-
// boolean-coerced → emit conditional ` attr=""` so the attribute is
|
|
298
|
-
// absent when expr is falsy
|
|
299
183
|
code = code.replace(BATCH_ATTR_BINDING_REGEX, (_, _ws, attrName, expr) => {
|
|
300
184
|
const decExpr = decodeEntities(expr);
|
|
301
185
|
if (DOM_PROPERTIES.includes(attrName) || isValueStyleAttr(attrName)) {
|
|
@@ -304,21 +188,8 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
304
188
|
return '${_e(()=>(' + decExpr + ')) ? \' ' + attrName + '=""\' : \'\'}';
|
|
305
189
|
});
|
|
306
190
|
|
|
307
|
-
// Remaining @[…] markers — text content and partial-binding attribute
|
|
308
|
-
// values like `href="/items/@[id]/edit"`.
|
|
309
191
|
code = code.replace(BINDING_REGEX, (_, expr) => '${_e(()=>(' + decodeEntities(expr) + '))}');
|
|
310
192
|
|
|
311
|
-
// `$` is supplied as the last parameter so component-scoped expressions like
|
|
312
|
-
// `$['_c0'].chosen` (produced by the this.X rewrite above) can resolve
|
|
313
|
-
// against the live state. Mirrors evalInScope, which also exposes `$`.
|
|
314
|
-
// `_walkCi` is only emitted when at least one dotted-path name binding was
|
|
315
|
-
// rewritten above — keeps the function body free of dead code for the
|
|
316
|
-
// common no-name-binding case.
|
|
317
|
-
// Per-expression failure contract, mirroring evalInScope: an expression
|
|
318
|
-
// that can't evaluate yet (component state not registered, alias mid-swap)
|
|
319
|
-
// renders '' and the next flush corrects it — it must never throw out of
|
|
320
|
-
// the batch loop and abort every row after it. Non-throwing values pass
|
|
321
|
-
// through untouched, so template-literal coercion stays identical.
|
|
322
193
|
const evalGuardSrc = "const _e = (f) => { try { return f(); } catch { return ''; } };";
|
|
323
194
|
const ciWalkerSrc = needsCiWalker
|
|
324
195
|
? `const _walkCi = (cur, segs) => {
|
|
@@ -354,27 +225,11 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys, ancho
|
|
|
354
225
|
return { fn, domPropertyWrites };
|
|
355
226
|
};
|
|
356
227
|
|
|
357
|
-
// Registry for non-primitive iteration prop snapshots. Lives on `window` (not
|
|
358
|
-
// on `$`) so it doesn't pollute user-visible state enumeration, but is still
|
|
359
|
-
// reachable from binding expressions because `window` is in evalInScope's
|
|
360
|
-
// known-globals list. Each entry is freed when the owning component element
|
|
361
|
-
// is detached (see releaseOrphanedIterationProps).
|
|
362
|
-
//
|
|
363
|
-
// The name is intentionally all-lowercase. resolveIterationComponentProps
|
|
364
|
-
// injects `@[window.__vibe.iterProps._pN]` into the component's bindings, and
|
|
365
|
-
// prop substitution carries that accessor into the template's own bindings —
|
|
366
|
-
// including name-bindings (`<icon @[props.element]>`). HTML lowercases
|
|
367
|
-
// attribute names, so a camelCase accessor would arrive at hydrate as
|
|
368
|
-
// `window.__vibe.iterProps` and resolve to undefined, silently dropping the
|
|
369
|
-
// attribute. Keeping the global lowercase makes it survive that normalization.
|
|
370
228
|
let __vibeIterPropCounter = 0;
|
|
371
229
|
const ensureIterPropsRegistry = () => {
|
|
372
230
|
return ((window.__vibe ??= {}).iterProps ??= {});
|
|
373
231
|
};
|
|
374
232
|
|
|
375
|
-
// Walk a removed subtree and free any iteration-prop registry slots stashed
|
|
376
|
-
// on `<component>` elements inside it. Called from the mutation-observer
|
|
377
|
-
// cleanup path after DOM detachment.
|
|
378
233
|
export const releaseOrphanedIterationProps = (nodes) => {
|
|
379
234
|
if (!window.__vibe?.iterProps) return;
|
|
380
235
|
for (const node of nodes) {
|
|
@@ -390,22 +245,8 @@ export const releaseOrphanedIterationProps = (nodes) => {
|
|
|
390
245
|
}
|
|
391
246
|
};
|
|
392
247
|
|
|
393
|
-
// Slot content projected into a <component src> is captured raw (`_vibeSlotContent`)
|
|
394
|
-
// and inlined only when processComponent runs — by which point the row's iteration
|
|
395
|
-
// scope is gone. Any `@[...]` in that content rooted in a loop alias (item/index/
|
|
396
|
-
// outer) or `this` therefore can't resolve later: value bindings render undefined
|
|
397
|
-
// and name-bindings (`<icon @[row.icon]>`) never set their attribute. Pre-resolve
|
|
398
|
-
// those into registry-backed global refs here — the same snapshot mechanism the
|
|
399
|
-
// component's own prop attributes use — so the inlined slot hydrates against the
|
|
400
|
-
// right values and the row's update path refreshes them in place (the wrapper
|
|
401
|
-
// inherits `_vibeIterPropExprs`/`data-vibe-iter-prop`, so refreshIterationComponentProps
|
|
402
|
-
// re-evaluates them on each item change). Globals-only bindings are left raw; they
|
|
403
|
-
// resolve through the normal reactive path against the inlined component's scope.
|
|
404
248
|
const SLOT_DIRECTIVE_REGEX = /<!--\s*(if|else if|each)\s+([^]*?)\s*-->/g;
|
|
405
249
|
|
|
406
|
-
// Substitute an identifier only outside string literals — the same contract as
|
|
407
|
-
// component.js's prop substitution: an alias that also appears inside a quoted
|
|
408
|
-
// string ('materials' containing the alias name) must be left intact.
|
|
409
250
|
const STRING_LITERAL_REGEX = /(['"`])(?:\\.|(?!\1)[^\\])*\1/g;
|
|
410
251
|
const substituteOutsideStrings = (expr, idRegex, replacement) => {
|
|
411
252
|
let out = '';
|
|
@@ -424,9 +265,6 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
424
265
|
if (!html || (!html.includes('@[') && !html.includes('<!--'))) return;
|
|
425
266
|
const registry = ensureIterPropsRegistry();
|
|
426
267
|
const idByExpr = new Map();
|
|
427
|
-
// Snapshot one expression into a registry slot, reusing the slot for repeat
|
|
428
|
-
// occurrences. Returns undefined (leave raw) when evaluation fails or yields
|
|
429
|
-
// undefined — same fail-safe as resolveIterationComponentProps.
|
|
430
268
|
const snapshot = (expr) => {
|
|
431
269
|
let id = idByExpr.get(expr);
|
|
432
270
|
if (id === undefined) {
|
|
@@ -453,25 +291,8 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
453
291
|
const id = snapshot(expr);
|
|
454
292
|
return id === undefined ? whole : `@[window.__vibe.iterProps.${id}]`;
|
|
455
293
|
});
|
|
456
|
-
// Directive comments in slot content — <!-- if -->, <!-- else if -->,
|
|
457
|
-
// <!-- each --> — hit the same wall as @[...] bindings: after processComponent
|
|
458
|
-
// inlines the template, the wrapper's subtree is re-processed against global
|
|
459
|
-
// state, where the loop aliases don't resolve and every branch renders empty
|
|
460
|
-
// (the battleborn rewards-window icons). Route each alias the expression
|
|
461
|
-
// reads through its own registry slot (`drop.kind === 'materials'` →
|
|
462
|
-
// `window.__vibe.iterProps._pN.kind === ...`), so the inlined directive
|
|
463
|
-
// evaluates against the row's values in any scope. Updates need no extra
|
|
464
|
-
// wiring: refreshIterationComponentProps re-snapshots the alias per row
|
|
465
|
-
// update, the rewritten expression records no state read so its subscriber
|
|
466
|
-
// re-dispatches from the tracking always-bucket on every flush, and the
|
|
467
|
-
// affected() pass over the wrapper's tree stamps that subscriber's lastScope
|
|
468
|
-
// with the row's merged scope — so a branch flip mounts with the loop
|
|
469
|
-
// aliases reachable. A registry-backed <!-- each --> is driven by
|
|
470
|
-
// forceRegistryBackedIterationUpdates like any other registry iteration.
|
|
471
294
|
if (aliases?.size) {
|
|
472
295
|
rewritten = rewritten.replace(SLOT_DIRECTIVE_REGEX, (whole, kw, expr) => {
|
|
473
|
-
// An each's alias clause introduces fresh names — only its array
|
|
474
|
-
// expression is rewritable.
|
|
475
296
|
const asMatch = kw === 'each' && expr.match(/^([^]*?)\s+as\s+([^]*)$/);
|
|
476
297
|
const original = asMatch ? asMatch[1] : expr;
|
|
477
298
|
let code = original;
|
|
@@ -493,12 +314,6 @@ const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
|
493
314
|
}
|
|
494
315
|
};
|
|
495
316
|
|
|
496
|
-
// Substitute the loop-scoped `@[expr]` parts of a component's src with their
|
|
497
|
-
// values from this block's scope. Only exprs whose dependencies include a loop
|
|
498
|
-
// alias are resolved — everything else keeps its binding form (see the caller
|
|
499
|
-
// in initializeBlock for why). An expr that evaluates to undefined stays raw
|
|
500
|
-
// too, so the unresolved-src invariant keeps the wrapper unfetchable instead
|
|
501
|
-
// of composing a garbage URL.
|
|
502
317
|
const resolveScopedSrc = (el, scopedState, scopeKeys) => {
|
|
503
318
|
const src = el.getAttribute('src');
|
|
504
319
|
BINDING_REGEX.lastIndex = 0;
|
|
@@ -511,21 +326,6 @@ const resolveScopedSrc = (el, scopedState, scopeKeys) => {
|
|
|
511
326
|
if (resolved !== src) el.setAttribute('src', resolved);
|
|
512
327
|
};
|
|
513
328
|
|
|
514
|
-
// For <component src> elements inside an iteration instance, evaluate any
|
|
515
|
-
// `@[expr]` attribute bindings against the iteration's scoped state and route
|
|
516
|
-
// every resolved value through the global iteration-prop registry. The prop
|
|
517
|
-
// attribute becomes `@[window.__vibe.iterProps._pN]` — a live binding into the
|
|
518
|
-
// registry slot — for both primitives and objects. The original expression is
|
|
519
|
-
// stashed on the element so the iteration's update path can re-evaluate it
|
|
520
|
-
// against the new scope and refresh the slot, propagating the change into the
|
|
521
|
-
// inlined component's bindings without rebuilding the row's DOM.
|
|
522
|
-
//
|
|
523
|
-
// Component[src] attributes intentionally bypass hydrate (parse.js) so they
|
|
524
|
-
// reach processComponent as bindings — but bindings that depend on
|
|
525
|
-
// iteration-local vars (item, index) can't resolve later when processComponent
|
|
526
|
-
// inlines the component, since by then iteration scope is gone. Only called
|
|
527
|
-
// from iteration code paths; conditionals don't need this because their branch
|
|
528
|
-
// content is registered in the global manifest and reacts to state updates.
|
|
529
329
|
export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
530
330
|
for (let n = 0; n < nodes.length; n++) {
|
|
531
331
|
const node = nodes[n];
|
|
@@ -542,18 +342,6 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
542
342
|
const match = attr.value.match(/^@\[(.+)\]$/);
|
|
543
343
|
if (!match) continue;
|
|
544
344
|
const expr = match[1];
|
|
545
|
-
// The registry snapshot only exists to carry iteration-local values
|
|
546
|
-
// (item/index/outer aliases, or component-local `this.X`) past the point
|
|
547
|
-
// where processComponent inlines the component and that scope is gone.
|
|
548
|
-
// A prop whose expression references ONLY globals doesn't need it — and
|
|
549
|
-
// routing it through the snapshot would freeze it, since the slot is
|
|
550
|
-
// refreshed solely on array diffs (the Brawling-loader freeze: a prop
|
|
551
|
-
// bound to `elapsedMilliseconds` in a row whose array never changes).
|
|
552
|
-
// Leave such a binding raw so the normal reactive path tracks the
|
|
553
|
-
// global, but still tag the wrapper: `_vibeIterPropExprs` makes index.js
|
|
554
|
-
// stamp `_vibeIterTree`, which is what lets affected.js's
|
|
555
|
-
// walkInlinedComponentTrees descend in and re-hydrate the raw binding on
|
|
556
|
-
// a global-state change.
|
|
557
345
|
const usesLocalScope =
|
|
558
346
|
/\bthis\b/.test(expr) ||
|
|
559
347
|
(aliases && extractDependencies(expr).some((d) => aliases.has(d)));
|
|
@@ -575,7 +363,6 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
575
363
|
(el._vibeIterPropIds = el._vibeIterPropIds || []).push(id);
|
|
576
364
|
(el._vibeIterPropExprs = el._vibeIterPropExprs || []).push({ id, attrName: attr.name, expr });
|
|
577
365
|
} catch {
|
|
578
|
-
// Leave binding raw — processComponent will handle it as a binding
|
|
579
366
|
}
|
|
580
367
|
}
|
|
581
368
|
resolveSlotContentBindings(el, scopedState, aliases);
|
|
@@ -583,13 +370,6 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
583
370
|
}
|
|
584
371
|
};
|
|
585
372
|
|
|
586
|
-
// Walk a row's live cloned nodes and invoke `fn` for every inlined-component
|
|
587
|
-
// wrapper carrying `marker` — the resolved node itself plus any
|
|
588
|
-
// `[data-vibe-iter-prop]` descendant that carries it. The prop-refresh and
|
|
589
|
-
// inlined-hydrate passes are identical except for this marker and the per-wrapper
|
|
590
|
-
// work, so they share this walk. (`[data-vibe-iter-prop]` always implies
|
|
591
|
-
// `_vibeIterPropExprs`, set together at mount, so filtering descendants by the
|
|
592
|
-
// marker matches the historical "take all, skip those without exprs" behavior.)
|
|
593
373
|
const forEachIterWrapper = (clonedNodes, marker, fn) => {
|
|
594
374
|
for (let n = 0; n < clonedNodes.length; n++) {
|
|
595
375
|
const node = liveNode(clonedNodes[n]);
|
|
@@ -602,17 +382,6 @@ const forEachIterWrapper = (clonedNodes, marker, fn) => {
|
|
|
602
382
|
}
|
|
603
383
|
};
|
|
604
384
|
|
|
605
|
-
// Walk a row's clones for any element tagged as iteration-prop owner —
|
|
606
|
-
// pre-process `<component src>` (still has the src attribute) and post-process
|
|
607
|
-
// `<component>` wrappers both carry `_vibeIterPropExprs`. For each tracked
|
|
608
|
-
// expression, re-evaluate against the row's new scoped state and write into
|
|
609
|
-
// the registry slot the inlined bindings already reference. Idempotent: same
|
|
610
|
-
// scoped state → same value → no-op write.
|
|
611
|
-
// Returns the set of registry slot ids whose value actually changed this
|
|
612
|
-
// refresh. A slot holding the same reference (e.g. a static ability array on a
|
|
613
|
-
// combatant that only moved) is a no-op, so forceRegistryBackedIterationUpdates
|
|
614
|
-
// can skip re-diffing the nested iteration it feeds — the dominant cost when a
|
|
615
|
-
// row carries large static nested iterations.
|
|
616
385
|
const refreshIterationComponentProps = (clonedNodes, scopedState) => {
|
|
617
386
|
const changed = new Set();
|
|
618
387
|
const registry = ensureIterPropsRegistry();
|
|
@@ -620,54 +389,24 @@ const refreshIterationComponentProps = (clonedNodes, scopedState) => {
|
|
|
620
389
|
for (const { id, expr } of el._vibeIterPropExprs) {
|
|
621
390
|
try {
|
|
622
391
|
const value = evalInScope(expr, scopedState, el);
|
|
623
|
-
// Don't clobber a slot with undefined — mirrors the mount-time guard in
|
|
624
|
-
// resolveIterationComponentProps. forEachIterWrapper reaches every
|
|
625
|
-
// [data-vibe-iter-prop] descendant, including components owned by a
|
|
626
|
-
// DEEPER iteration (e.g. a cell component inside a nested each). Their
|
|
627
|
-
// prop expressions reference the inner each's alias, which isn't in this
|
|
628
|
-
// (outer) row's scope, so they evaluate to undefined here. Skipping keeps
|
|
629
|
-
// the value the inner iteration's own update already set with the correct
|
|
630
|
-
// scope, instead of wiping it to undefined and leaving raw @[...] bindings.
|
|
631
392
|
if (value === undefined) continue;
|
|
632
393
|
if (registry[id] !== value) {
|
|
633
394
|
registry[id] = value;
|
|
634
395
|
changed.add(id);
|
|
635
396
|
}
|
|
636
397
|
} catch {
|
|
637
|
-
// Leave previous registry value in place — same fail-safe as
|
|
638
|
-
// resolveIterationComponentProps's mount-time path.
|
|
639
398
|
}
|
|
640
399
|
}
|
|
641
400
|
});
|
|
642
401
|
return changed;
|
|
643
402
|
};
|
|
644
403
|
|
|
645
|
-
// Walk an inlined component's parsed tree and force `updateIteration` on any
|
|
646
|
-
// iteration node whose arrayPath resolves through the iteration-prop registry
|
|
647
|
-
// (`window.__vibe.iterProps._pN`). The registry slot was just refreshed in
|
|
648
|
-
// place by `refreshIterationComponentProps`, so `affected()` can't notice
|
|
649
|
-
// the change — both old/new evaluations of the path read the same updated
|
|
650
|
-
// value. `updateIteration` is the only place equipped to diff against
|
|
651
|
-
// `iterationNode.runtime.instances` (which still hold the previously rendered
|
|
652
|
-
// items) and emit ADD / REMOVE / UPDATE / MOVE ops to bring the inlined DOM
|
|
653
|
-
// in sync. Without this, an `<inner-component>` whose template iterates over
|
|
654
|
-
// an array prop stays frozen on its initial-render items when the prop's
|
|
655
|
-
// contents change.
|
|
656
|
-
// Registry-backed CONDITIONALS (rewritten by resolveSlotContentBindings) need
|
|
657
|
-
// no counterpart here: their evaluation records no state read, so they sit in
|
|
658
|
-
// the tracking always-bucket and re-dispatch on every flush — and the
|
|
659
|
-
// affected() pass below stamps their subscriber's lastScope with this row's
|
|
660
|
-
// merged scope, so the dispatch flips the branch with the loop aliases in
|
|
661
|
-
// reach (letting the mounting branch hydrate alias bindings whose build-time
|
|
662
|
-
// snapshot was undefined, e.g. drop.item.icon before an icon exists).
|
|
663
404
|
const REGISTRY_SLOT_REGEX = /__vibe\.iterProps\.(_p\d+)/;
|
|
664
405
|
const forceRegistryBackedIterationUpdates = (tree, state, manifest, parentScope, changedSlots) => {
|
|
665
406
|
if (!tree) return;
|
|
666
407
|
if (tree.type === 'iteration') {
|
|
667
408
|
const arrPath = tree.meta?.arrayPath;
|
|
668
409
|
const slot = arrPath && arrPath.match(REGISTRY_SLOT_REGEX);
|
|
669
|
-
// Skip iterations whose backing slot didn't change this cycle. changedSlots
|
|
670
|
-
// is undefined only on legacy/unguarded calls — fall back to always-update.
|
|
671
410
|
if (slot && (!changedSlots || changedSlots.has(slot[1]))) {
|
|
672
411
|
updateIteration(tree, state, state, manifest, parentScope);
|
|
673
412
|
}
|
|
@@ -679,12 +418,6 @@ const forceRegistryBackedIterationUpdates = (tree, state, manifest, parentScope,
|
|
|
679
418
|
}
|
|
680
419
|
};
|
|
681
420
|
|
|
682
|
-
// After registry slots are refreshed, re-hydrate the bindings inside each
|
|
683
|
-
// inlined component wrapper so the new values reach the DOM. Each post-process
|
|
684
|
-
// wrapper carries a `_vibeIterTree` snapshot captured at inline time (in
|
|
685
|
-
// component.js) — that tree retains the original `@[...]` binding text even
|
|
686
|
-
// after the wrapper's live DOM has been hydrated, so subsequent affected→
|
|
687
|
-
// hydrate passes work the same way they would on initial render.
|
|
688
421
|
const hydrateInlinedIterationComponents = (clonedNodes, oldState, newState, manifest, changedSlots) => {
|
|
689
422
|
forEachIterWrapper(clonedNodes, '_vibeIterTree', (wrapper) => {
|
|
690
423
|
const tree = wrapper._vibeIterTree;
|
|
@@ -692,20 +425,10 @@ const hydrateInlinedIterationComponents = (clonedNodes, oldState, newState, mani
|
|
|
692
425
|
if (affectedList.length > 0) {
|
|
693
426
|
hydrate(affectedList, newState, manifest, oldState);
|
|
694
427
|
}
|
|
695
|
-
// Bindings into the iteration-prop registry are visited above, but iteration
|
|
696
|
-
// nodes whose arrayPath resolves through the registry need explicit driving:
|
|
697
|
-
// the refreshed slot is a side effect `affected()` can't see. Update only the
|
|
698
|
-
// iterations whose slot actually changed this cycle (changedSlots).
|
|
699
428
|
forceRegistryBackedIterationUpdates(tree, newState, manifest, {}, changedSlots);
|
|
700
429
|
});
|
|
701
430
|
};
|
|
702
431
|
|
|
703
|
-
// Apply DOM-property writes that compileBatchFn collected. Each batch row
|
|
704
|
-
// gets a built tiny scope (item + index + every state key) which is then
|
|
705
|
-
// passed to evalInScope — same evaluator the clone path uses, so identifier
|
|
706
|
-
// resolution rules (this.X already pre-rewritten, undefined-tolerant lookups)
|
|
707
|
-
// stay aligned. The transient `data-vibe-batch` marker is removed once its
|
|
708
|
-
// expressions have been applied so it doesn't leak into the live DOM.
|
|
709
432
|
const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias, domPropertyWrites) => {
|
|
710
433
|
for (let i = 0; i < instances.length; i++) {
|
|
711
434
|
const root = instances[i].element;
|
|
@@ -730,8 +453,6 @@ const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias,
|
|
|
730
453
|
if (el.getAttribute(prop) !== str) el.setAttribute(prop, str);
|
|
731
454
|
}
|
|
732
455
|
} else if (value) {
|
|
733
|
-
// checked/selected are boolean — presence/absence is the truthful
|
|
734
|
-
// attribute form, matching the clone path in hydrate.js.
|
|
735
456
|
if (el.getAttribute(prop) !== '') el.setAttribute(prop, '');
|
|
736
457
|
} else if (el.hasAttribute(prop)) {
|
|
737
458
|
el.removeAttribute(prop);
|
|
@@ -742,14 +463,6 @@ const applyDomPropertyWrites = (instances, array, state, itemAlias, indexAlias,
|
|
|
742
463
|
}
|
|
743
464
|
};
|
|
744
465
|
|
|
745
|
-
// Resolve the loop's array against the LIVE `$` proxy root (not the plain
|
|
746
|
-
// diff-snapshot the pipeline renders against — see extractPlainValue in
|
|
747
|
-
// index.js). The plain clones are never reference-identical to the proxy
|
|
748
|
-
// elements the app sees through `$`, so a loop var that flows into an `on*`
|
|
749
|
-
// handler must come from here for `item === $.arr[i]` to hold. Returns null
|
|
750
|
-
// when unresolvable (no live root yet, a derivation that builds fresh objects,
|
|
751
|
-
// or a nested loop whose source hangs off an outer plain item) — callers fall
|
|
752
|
-
// back to the plain item, which is no worse than the pre-fix behavior.
|
|
753
466
|
const resolveLiveArray = (iterationNode, manifest, parentScope = {}) => {
|
|
754
467
|
const liveRoot = (manifest && manifest.__live) || globalThis.$;
|
|
755
468
|
if (!liveRoot) return null;
|
|
@@ -768,11 +481,6 @@ const resolveLiveArray = (iterationNode, manifest, parentScope = {}) => {
|
|
|
768
481
|
const liveItemAt = (liveArray, index, fallback) =>
|
|
769
482
|
liveArray && index < liveArray.length ? liveArray[index] : fallback;
|
|
770
483
|
|
|
771
|
-
// Refresh each instance's `liveItem` (the live `$`-proxy element handed to
|
|
772
|
-
// loop-scoped `$scope` handlers, set when the instance was built) and stamp
|
|
773
|
-
// scope. Re-resolving here keeps `liveItem` correct after the diff reorders or
|
|
774
|
-
// updates instances. Diffing still keys off the plain `inst.item`; only the
|
|
775
|
-
// handler-facing `$scope` value is live.
|
|
776
484
|
const stampScopes = (iterationNode, manifest, parentScope = {}) => {
|
|
777
485
|
const liveArray = resolveLiveArray(iterationNode, manifest, parentScope);
|
|
778
486
|
const instances = iterationNode.runtime.instances;
|
|
@@ -797,17 +505,13 @@ const renderBatch = (iterationNode, array, state, parent, endComment, parentScop
|
|
|
797
505
|
const { batchFn, stateKeys, domPropertyWrites } = iterationNode.runtime;
|
|
798
506
|
const stateValues = stateKeys.map((k) => state[k]);
|
|
799
507
|
const html = batchFn(array, ...stateValues, state);
|
|
800
|
-
// Remember the rendered output so a later update can skip re-rendering when an
|
|
801
|
-
// unrelated state change produces identical HTML (see updateIteration).
|
|
802
508
|
iterationNode.runtime.lastBatchHtml = html;
|
|
803
509
|
|
|
804
|
-
// Batch rows close over every state key (stateKeys = Object.keys(state) at
|
|
805
|
-
// compile time) — "re-render on any change" is their honest dependency
|
|
806
|
-
// set; the identical-HTML guard above absorbs the no-ops (walk parity).
|
|
807
510
|
markAlways(nodeSubscriberOf(iterationNode, 'iteration'));
|
|
808
511
|
|
|
809
512
|
batchParseTemplate.innerHTML = html;
|
|
810
513
|
const frag = batchParseTemplate.content;
|
|
514
|
+
markBoundValues(frag, html);
|
|
811
515
|
const kids = frag.children;
|
|
812
516
|
|
|
813
517
|
const arrayLen = array.length;
|
|
@@ -823,13 +527,9 @@ const renderBatch = (iterationNode, array, state, parent, endComment, parentScop
|
|
|
823
527
|
applyDomPropertyWrites(instances, array, state, itemAlias, indexAlias, domPropertyWrites);
|
|
824
528
|
}
|
|
825
529
|
|
|
826
|
-
// Stamp loop scope so `$scope(this,'alias')` handlers resolve the live item.
|
|
827
530
|
stampScopes(iterationNode, manifest, parentScope);
|
|
828
531
|
};
|
|
829
532
|
|
|
830
|
-
/**
|
|
831
|
-
* Find a comment node with matching text content in the given nodes.
|
|
832
|
-
*/
|
|
833
533
|
const findComment = (nodes, text) => {
|
|
834
534
|
const trimmedText = text.trim();
|
|
835
535
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -841,10 +541,6 @@ const findComment = (nodes, text) => {
|
|
|
841
541
|
return null;
|
|
842
542
|
};
|
|
843
543
|
|
|
844
|
-
/**
|
|
845
|
-
* Clone a parsed tree, mapping element references from original to cloned DOM.
|
|
846
|
-
* Walks both trees in lockstep - no map building needed since structure is identical.
|
|
847
|
-
*/
|
|
848
544
|
const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
849
545
|
if (!originalTree) return null;
|
|
850
546
|
|
|
@@ -854,7 +550,6 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
854
550
|
children: {},
|
|
855
551
|
};
|
|
856
552
|
|
|
857
|
-
// Avoid spread operator for performance
|
|
858
553
|
if (originalTree.attributes) {
|
|
859
554
|
cloned.attributes = originalTree.attributes;
|
|
860
555
|
}
|
|
@@ -866,16 +561,13 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
866
561
|
|
|
867
562
|
const clonedChildNodes = clonedRoot?.childNodes;
|
|
868
563
|
|
|
869
|
-
// Clone children recursively - use index from key to find cloned element
|
|
870
564
|
for (const key in originalTree.children) {
|
|
871
565
|
const child = originalTree.children[key];
|
|
872
566
|
if (!child || typeof child !== 'object') continue;
|
|
873
567
|
|
|
874
|
-
// Handle conditional nodes - need to update comment references
|
|
875
568
|
if (child.type === 'conditional' && clonedChildNodes) {
|
|
876
569
|
const { startComment, elseComment, endComment, branches } = child.meta;
|
|
877
570
|
|
|
878
|
-
// Find corresponding comments in cloned DOM
|
|
879
571
|
const clonedStart = findComment(clonedChildNodes, startComment.textContent);
|
|
880
572
|
const clonedElse = elseComment
|
|
881
573
|
? findComment(clonedChildNodes, elseComment.textContent)
|
|
@@ -889,7 +581,7 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
889
581
|
startComment: clonedStart || startComment,
|
|
890
582
|
elseComment: clonedElse,
|
|
891
583
|
endComment: clonedEnd || endComment,
|
|
892
|
-
branches,
|
|
584
|
+
branches,
|
|
893
585
|
},
|
|
894
586
|
runtime: {
|
|
895
587
|
activeBranch: undefined,
|
|
@@ -901,11 +593,9 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
901
593
|
continue;
|
|
902
594
|
}
|
|
903
595
|
|
|
904
|
-
// Handle iteration nodes - need to update comment references and fresh runtime
|
|
905
596
|
if (child.type === 'iteration' && clonedChildNodes) {
|
|
906
597
|
const { startComment, endComment, template } = child.meta;
|
|
907
598
|
|
|
908
|
-
// Find corresponding comments in cloned DOM
|
|
909
599
|
const clonedStart = findComment(clonedChildNodes, startComment.textContent);
|
|
910
600
|
const clonedEnd = findComment(clonedChildNodes, endComment.textContent);
|
|
911
601
|
|
|
@@ -917,7 +607,7 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
917
607
|
indexAlias: child.meta.indexAlias,
|
|
918
608
|
startComment: clonedStart || startComment,
|
|
919
609
|
endComment: clonedEnd || endComment,
|
|
920
|
-
template,
|
|
610
|
+
template,
|
|
921
611
|
},
|
|
922
612
|
runtime: {
|
|
923
613
|
instances: [],
|
|
@@ -928,7 +618,6 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
928
618
|
continue;
|
|
929
619
|
}
|
|
930
620
|
|
|
931
|
-
// Extract index from key (format: "nodename_index") - avoid regex for speed
|
|
932
621
|
const underscoreIdx = key.lastIndexOf('_');
|
|
933
622
|
const childIndex = underscoreIdx >= 0 ? parseInt(key.slice(underscoreIdx + 1), 10) : -1;
|
|
934
623
|
const clonedChild = childIndex >= 0 && clonedChildNodes ? clonedChildNodes[childIndex] : null;
|
|
@@ -939,28 +628,6 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
939
628
|
return cloned;
|
|
940
629
|
};
|
|
941
630
|
|
|
942
|
-
/**
|
|
943
|
-
* Initialize a block instance from template nodes.
|
|
944
|
-
* Uses cached template tree when available for speed.
|
|
945
|
-
*
|
|
946
|
-
* @param {NodeList} templateNodes - Template nodes to clone
|
|
947
|
-
* @param {Object} scopedState - Scoped state (with localVars/parentScope already applied)
|
|
948
|
-
* @param {Object} cachedTree - Optional cached parsed tree from template
|
|
949
|
-
* @param {string|null} componentId - Component the row belongs to, when the
|
|
950
|
-
* iteration sits inside a component. Stashed on the parseContainer so
|
|
951
|
-
* `findComponentIdForElement` can resolve `this.X` during the hydrate pass
|
|
952
|
-
* that runs while clones are still detached.
|
|
953
|
-
* @returns {Object} { element, tree, clonedNodes }
|
|
954
|
-
*/
|
|
955
|
-
// Compiled mode bakes a fixed `data-vibe-component-id` into each inlined
|
|
956
|
-
// component and stamps its `@[this.x]` bindings / `$.this.x` handlers to that
|
|
957
|
-
// id. An iteration clones its template once per row, so every row would
|
|
958
|
-
// otherwise share that one id — and thus one local-state bucket. (Two brawler
|
|
959
|
-
// slots both resolving to `_c2.open` is why opening one ability drawer opened
|
|
960
|
-
// them all.) Per row, remap every baked id in the clone to a fresh one and
|
|
961
|
-
// rewrite the references to it; the caller then runs the row's component
|
|
962
|
-
// scripts so each registers isolated state under its fresh id. Runtime mode has
|
|
963
|
-
// no baked ids here (components are still `<component src>`), so this no-ops.
|
|
964
631
|
const COMPONENT_ID = /^_c\d+$/;
|
|
965
632
|
|
|
966
633
|
const isolateInlinedComponentIds = (container) => {
|
|
@@ -973,9 +640,6 @@ const isolateInlinedComponentIds = (container) => {
|
|
|
973
640
|
}
|
|
974
641
|
if (!remap.size) return false;
|
|
975
642
|
|
|
976
|
-
// `_c2` must not match inside `_c20` or a longer identifier, so anchor on a
|
|
977
|
-
// non-word/`$` boundary before and a non-digit/word after. Bindings always
|
|
978
|
-
// read the id as `_cN.prop`, so the trailing `.` satisfies the lookahead.
|
|
979
643
|
const refs = [...remap].map(([oldId, newId]) => [
|
|
980
644
|
new RegExp(`(?<![\\w$])${oldId}(?![\\w\\d])`, 'g'),
|
|
981
645
|
newId,
|
|
@@ -1007,19 +671,11 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
1007
671
|
let clonedNodes = [];
|
|
1008
672
|
let firstElement = null;
|
|
1009
673
|
|
|
1010
|
-
// Use cached tree when available: cloneTreeWithElements maps the existing parsed structure
|
|
1011
|
-
// onto cloned DOM nodes, avoiding a full parse() call per iteration item.
|
|
1012
|
-
// Only fall back to parse() when no cached tree exists (first parse of a new template).
|
|
1013
|
-
// cloneTreeWithElements has a mapping bug with compiled mode's tree structure.
|
|
1014
|
-
// Keep disabled until the root cause is fixed — the other optimizations
|
|
1015
|
-
// (bulk replacement, evalInScope caching, DocumentFragment) cover the hot paths.
|
|
1016
674
|
const useCachedTree = false;
|
|
1017
675
|
|
|
1018
|
-
// Create a container for parsing (to capture all nodes including comment nodes like <!-- if -->)
|
|
1019
676
|
const parseContainer = document.createElement('div');
|
|
1020
677
|
if (componentId) parseContainer._vibeComponentId = componentId;
|
|
1021
678
|
|
|
1022
|
-
// Clone template nodes into container
|
|
1023
679
|
for (let i = 0; i < templateNodes.length; i++) {
|
|
1024
680
|
const cloned = templateNodes[i].cloneNode(true);
|
|
1025
681
|
parseContainer.appendChild(cloned);
|
|
@@ -1028,22 +684,6 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
1028
684
|
}
|
|
1029
685
|
}
|
|
1030
686
|
|
|
1031
|
-
// Preserve raw slot content of nested <component src> elements before renderAllConditionals /
|
|
1032
|
-
// renderAllIterations runs on this branch/iteration instance — those paths strip <!-- if -->
|
|
1033
|
-
// and <!-- each --> templates from the live DOM, so by the time processComponent reads
|
|
1034
|
-
// el.innerHTML (next microtask, when MutationObserver fires) the inactive branch templates
|
|
1035
|
-
// would be gone. cloneNode(true) does not copy expando JS properties, so we must (re)capture
|
|
1036
|
-
// _vibeSlotContent on every clone.
|
|
1037
|
-
//
|
|
1038
|
-
// Also resolve loop-scoped src bindings here, BEFORE parse() reads the
|
|
1039
|
-
// attribute: the loop locals die with this render, so a src composed from
|
|
1040
|
-
// them (`src="/x/@[item.slug].html"`) must become a literal URL now. Left as
|
|
1041
|
-
// a binding it registers in this block's tree, and the remount transport
|
|
1042
|
-
// (_vibeSrcBinding → data-vibe-src on the finalized wrapper) re-evaluates it
|
|
1043
|
-
// against GLOBAL state on reparse — clobbering the mounted row with a fetch
|
|
1044
|
-
// of a garbage URL. Exprs that read no loop alias stay raw: a reactive
|
|
1045
|
-
// outlet (`src="@[page.src]"`) keeps its global binding, and an alias owned
|
|
1046
|
-
// by a deeper loop resolves when that loop clones its own instances.
|
|
1047
687
|
const components = parseContainer.querySelectorAll('component[src], div.component[src]');
|
|
1048
688
|
const scopeKeys = overlayKeysOf(scopedState);
|
|
1049
689
|
for (let i = 0; i < components.length; i++) {
|
|
@@ -1052,32 +692,22 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
1052
692
|
if (scopeKeys) resolveScopedSrc(el, scopedState, scopeKeys);
|
|
1053
693
|
}
|
|
1054
694
|
|
|
1055
|
-
// Give this row its own component ids (compiled mode) before parse() reads the
|
|
1056
|
-
// bindings, then run the row's inlined setup scripts so each registers its own
|
|
1057
|
-
// local state under the fresh id — mirroring the conditional-branch path.
|
|
1058
695
|
if (isolateComponents && isolateInlinedComponentIds(parseContainer)) {
|
|
1059
696
|
executeCompiledComponentScriptsIn([...parseContainer.childNodes]);
|
|
1060
697
|
}
|
|
1061
698
|
|
|
1062
699
|
if (useCachedTree) {
|
|
1063
|
-
// Fast path: map cached tree structure onto cloned DOM (no regex, no DOM walking)
|
|
1064
700
|
tree = cloneTreeWithElements(cachedTree, parseContainer);
|
|
1065
701
|
} else {
|
|
1066
|
-
// Full parse: walk DOM, extract bindings, build tree from scratch. Pass this
|
|
1067
|
-
// loop's aliases so loop-scoped `on*` handlers (and nested ones, via the
|
|
1068
|
-
// parser's child-alias accumulation) rewrite to `$scope(this,'alias')`.
|
|
1069
702
|
tree = parse(parseContainer, undefined, aliasSet);
|
|
1070
703
|
}
|
|
1071
704
|
|
|
1072
|
-
// Extract the cloned nodes from the container (these are the same nodes the tree references)
|
|
1073
|
-
// Avoid Array.from for performance
|
|
1074
705
|
const childNodes = parseContainer.childNodes;
|
|
1075
706
|
clonedNodes = [];
|
|
1076
707
|
for (let i = 0; i < childNodes.length; i++) {
|
|
1077
708
|
clonedNodes.push(childNodes[i]);
|
|
1078
709
|
}
|
|
1079
710
|
|
|
1080
|
-
// If no firstElement found, use parseContainer as fallback
|
|
1081
711
|
if (!firstElement) {
|
|
1082
712
|
firstElement = parseContainer;
|
|
1083
713
|
}
|
|
@@ -1094,19 +724,13 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
1094
724
|
};
|
|
1095
725
|
};
|
|
1096
726
|
|
|
1097
|
-
// Create a proxied state with scoped variables (item, index, array)
|
|
1098
727
|
export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
1099
|
-
// Pre-compute the combined key list once at creation time.
|
|
1100
|
-
// Avoids rebuilding 3 arrays + Set on every Object.keys() call.
|
|
1101
728
|
const cachedKeys = [...new Set([
|
|
1102
729
|
...Object.keys(localVars),
|
|
1103
730
|
...Object.keys(parentScope),
|
|
1104
731
|
...Reflect.ownKeys(globalState),
|
|
1105
732
|
])];
|
|
1106
733
|
|
|
1107
|
-
// Overlay = the aliases this proxy resolves locally (localVars wins over
|
|
1108
|
-
// parentScope, matching the get order below). affected's descent reuses it for
|
|
1109
|
-
// a cheap plain merge instead of materializing the proxy.
|
|
1110
734
|
const overlay =
|
|
1111
735
|
Object.keys(parentScope).length === 0 ? localVars : { ...parentScope, ...localVars };
|
|
1112
736
|
|
|
@@ -1114,10 +738,6 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
|
1114
738
|
get(target, prop) {
|
|
1115
739
|
if (prop in localVars) return localVars[prop];
|
|
1116
740
|
if (prop in parentScope) return parentScope[prop];
|
|
1117
|
-
// Global fallthrough is a root-key read; alias hits above are the
|
|
1118
|
-
// row's own scope (owned by its iteration's diff) and never record.
|
|
1119
|
-
// When the target is the live root the proxy trap records too — the
|
|
1120
|
-
// window's read Set dedupes.
|
|
1121
741
|
recordRead(prop);
|
|
1122
742
|
return Reflect.get(target, prop);
|
|
1123
743
|
},
|
|
@@ -1154,19 +774,14 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
|
1154
774
|
return rememberScopedKeys(proxy, cachedKeys, overlay);
|
|
1155
775
|
};
|
|
1156
776
|
|
|
1157
|
-
// Render all iterations in the parsed tree
|
|
1158
777
|
export const renderAllIterations = (tree, state, manifest, parentScope = {}) => {
|
|
1159
778
|
let count = 0;
|
|
1160
779
|
|
|
1161
|
-
// If this is an iteration node, render it
|
|
1162
780
|
if (tree.type === 'iteration') {
|
|
1163
781
|
renderIteration(tree, state, manifest, parentScope);
|
|
1164
782
|
return 1;
|
|
1165
783
|
}
|
|
1166
784
|
|
|
1167
|
-
// Recursively render iterations in child nodes — except under an outgoing
|
|
1168
|
-
// wrapper (reactive-src remount in flight): its subtree is frozen until
|
|
1169
|
-
// the swap replaces it.
|
|
1170
785
|
if (tree.children && !isOutgoing(tree.element)) {
|
|
1171
786
|
for (const key in tree.children) {
|
|
1172
787
|
const child = tree.children[key];
|
|
@@ -1179,44 +794,32 @@ export const renderAllIterations = (tree, state, manifest, parentScope = {}) =>
|
|
|
1179
794
|
return count;
|
|
1180
795
|
};
|
|
1181
796
|
|
|
1182
|
-
// Callback for rendering conditionals - set by conditionals.js to avoid circular import
|
|
1183
797
|
let _renderAllConditionals = () => {};
|
|
1184
798
|
export const setRenderAllConditionals = (fn) => {
|
|
1185
799
|
_renderAllConditionals = fn;
|
|
1186
800
|
};
|
|
1187
801
|
|
|
1188
|
-
// Initial render of an iteration block
|
|
1189
802
|
export const renderIteration = (iterationNode, state, manifest, parentScope = {}) => {
|
|
1190
803
|
const { arrayPath, startComment, endComment } = iterationNode.meta;
|
|
1191
804
|
|
|
1192
|
-
// Already rendered - updates go through updateIteration
|
|
1193
805
|
if (iterationNode.runtime.instances?.length > 0) {
|
|
1194
806
|
return;
|
|
1195
807
|
}
|
|
1196
808
|
|
|
1197
|
-
// Check if this iteration has already been rendered
|
|
1198
|
-
// We use a marker on the startComment node itself (survives re-parsing)
|
|
1199
|
-
// @ts-ignore - adding custom property to comment node
|
|
1200
809
|
if (startComment.__vibeRendered) {
|
|
1201
810
|
return;
|
|
1202
811
|
}
|
|
1203
812
|
|
|
1204
|
-
// Recovery for lost comment markers: a <component src> item re-processes its node
|
|
1205
|
-
// out of managedNodes, dropping startComment.__vibeRendered. templateRemoved lives
|
|
1206
|
-
// on runtime (not the DOM), so it survives — a true value means a prior render
|
|
1207
|
-
// already cleared the template, so if content is still present between the
|
|
1208
|
-
// comments, it's rendered; don't rebuild. Falls through only if content was lost.
|
|
1209
813
|
if (iterationNode.runtime.templateRemoved) {
|
|
1210
814
|
let currentNode = startComment.nextSibling;
|
|
1211
815
|
while (currentNode && currentNode !== endComment) {
|
|
1212
816
|
if (currentNode.nodeType === 1) {
|
|
1213
|
-
return;
|
|
817
|
+
return;
|
|
1214
818
|
}
|
|
1215
819
|
currentNode = currentNode.nextSibling;
|
|
1216
820
|
}
|
|
1217
821
|
}
|
|
1218
822
|
|
|
1219
|
-
// Remove template nodes from DOM on first render
|
|
1220
823
|
if (!iterationNode.runtime.templateRemoved) {
|
|
1221
824
|
let node = startComment.nextSibling;
|
|
1222
825
|
while (node && node !== endComment) {
|
|
@@ -1229,12 +832,7 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
1229
832
|
|
|
1230
833
|
const parent = startComment.parentNode;
|
|
1231
834
|
|
|
1232
|
-
// Evaluate the array expression — supports state paths (items),
|
|
1233
|
-
// window globals (window.fights), method calls (items.filter(...)),
|
|
1234
|
-
// and inline literals (['a', 'b']). Falls back to resolvePath for
|
|
1235
|
-
// simple paths that evalInScope might miss in scoped contexts.
|
|
1236
835
|
const resolvedExpr = resolveThisPath(arrayPath, startComment.parentElement);
|
|
1237
|
-
// First render doubles as the iteration's subscription registration.
|
|
1238
836
|
const trackSub = nodeSubscriberOf(iterationNode, 'iteration');
|
|
1239
837
|
trackSub.lastScope = state;
|
|
1240
838
|
beginTracking(trackSub, overlayKeysOf(state));
|
|
@@ -1245,35 +843,22 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
1245
843
|
return;
|
|
1246
844
|
}
|
|
1247
845
|
|
|
1248
|
-
// Compiled path: Check for pre-compiled batch function in manifest
|
|
1249
846
|
if (compiled.canUseCompiled(iterationNode)) {
|
|
1250
847
|
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
1251
848
|
if (compiled.renderCompiled(iterationNode, array, state, compiledMeta, parent, endComment)) {
|
|
1252
|
-
// Compiled rows close over every state key — "any change" is their
|
|
1253
|
-
// honest dependency set (walk parity for treeless instances).
|
|
1254
849
|
markAlways(trackSub);
|
|
1255
|
-
// Mark as rendered
|
|
1256
850
|
startComment.__vibeRendered = true;
|
|
1257
851
|
startComment.__vibeIterationRuntime = iterationNode.runtime;
|
|
1258
852
|
return;
|
|
1259
853
|
}
|
|
1260
|
-
// Fall through to runtime path if compiled failed
|
|
1261
854
|
}
|
|
1262
855
|
|
|
1263
|
-
// Standard path: one shared clone+hydrate render loop (also used by bulkReplace).
|
|
1264
856
|
renderInstances(iterationNode, array, state, manifest, parentScope);
|
|
1265
857
|
|
|
1266
|
-
// Mark comment as rendered (survives re-parsing since comment nodes persist in DOM)
|
|
1267
|
-
// Also store runtime data on the DOM node so it persists across re-parses
|
|
1268
|
-
// @ts-ignore - adding custom property to comment node
|
|
1269
858
|
startComment.__vibeRendered = true;
|
|
1270
|
-
// @ts-ignore - adding custom property to comment node
|
|
1271
859
|
startComment.__vibeIterationRuntime = iterationNode.runtime;
|
|
1272
860
|
};
|
|
1273
861
|
|
|
1274
|
-
// Evaluate the iteration's optional key expression for one item.
|
|
1275
|
-
// Returns undefined when no keyExpr is declared, falling back to the default
|
|
1276
|
-
// heuristic in getItemKey.
|
|
1277
862
|
const evalKeyForItem = (iterationNode, item, index, state, parentScope) => {
|
|
1278
863
|
const { keyExpr, itemAlias, indexAlias } = iterationNode.meta;
|
|
1279
864
|
if (!keyExpr) return undefined;
|
|
@@ -1286,9 +871,6 @@ const evalKeyForItem = (iterationNode, item, index, state, parentScope) => {
|
|
|
1286
871
|
}
|
|
1287
872
|
};
|
|
1288
873
|
|
|
1289
|
-
// One-shot warning when an unkeyed iteration produces index-coupled keys —
|
|
1290
|
-
// i.e. the fallback `hash_..._<index>` path. Only emits in debug mode and only
|
|
1291
|
-
// once per iteration block, so console doesn't drown.
|
|
1292
874
|
const warnIndexCoupledKey = (iterationNode) => {
|
|
1293
875
|
if (iterationNode.runtime.warnedIndexCoupled) return;
|
|
1294
876
|
if (!globalThis.__vibe?.debug) return;
|
|
@@ -1299,7 +881,6 @@ const warnIndexCoupledKey = (iterationNode) => {
|
|
|
1299
881
|
);
|
|
1300
882
|
};
|
|
1301
883
|
|
|
1302
|
-
// Update an iteration block when array changes
|
|
1303
884
|
export const updateIteration = (iterationNode, newState, oldState, manifest, parentScope = {}) => {
|
|
1304
885
|
if (!iterationNode.runtime.instances || !iterationNode.runtime.templateRemoved) return;
|
|
1305
886
|
|
|
@@ -1308,32 +889,12 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
1308
889
|
const resolvedExpr = resolveThisPath(arrayPath, startComment.parentElement);
|
|
1309
890
|
|
|
1310
891
|
const stateOldArray = evalInScope(resolvedExpr, oldState, startComment.parentElement) ?? resolvePath(oldState, resolvedExpr) ?? [];
|
|
1311
|
-
// New-side eval re-records the iteration's subscription (self-healing).
|
|
1312
892
|
const trackSub = nodeSubscriberOf(iterationNode, 'iteration');
|
|
1313
893
|
trackSub.lastScope = newState;
|
|
1314
894
|
beginTracking(trackSub, overlayKeysOf(newState));
|
|
1315
895
|
const newArray = evalInScope(resolvedExpr, newState, startComment.parentElement) ?? resolvePath(newState, resolvedExpr) ?? [];
|
|
1316
896
|
endTracking();
|
|
1317
897
|
|
|
1318
|
-
// Use instances (what's actually rendered) as ground truth for old array
|
|
1319
|
-
// whenever the only signal of change is a registry side-effect, or when
|
|
1320
|
-
// the rendered count doesn't match the freshly evaluated state. Three
|
|
1321
|
-
// cases collapse to "trust the rendered snapshot":
|
|
1322
|
-
// 1. oldState === newState — forceRegistryBackedIterationUpdates calls
|
|
1323
|
-
// updateIteration with the same state on both sides because the only
|
|
1324
|
-
// mutation was a registry slot rewrite. The previously rendered
|
|
1325
|
-
// items are the only honest record of what was there before.
|
|
1326
|
-
// 2. stateOldArray === newArray — the iteration's arrayPath resolves
|
|
1327
|
-
// directly to a registry slot (`window.__vibe.iterProps._pN`); the
|
|
1328
|
-
// slot was swapped in place, so both reads return the same NEW
|
|
1329
|
-
// array.
|
|
1330
|
-
// 3. length mismatch — oldState predates the current render.
|
|
1331
|
-
// Otherwise the freshly evaluated state is a trustworthy "old".
|
|
1332
|
-
//
|
|
1333
|
-
// Literal-wrap expressions like `[s]` would otherwise slip through this
|
|
1334
|
-
// net: they build different array refs each eval but both contain the
|
|
1335
|
-
// just-rewritten registry value, so a naive diff sees no change. Case 1
|
|
1336
|
-
// catches them.
|
|
1337
898
|
const instances = iterationNode.runtime.instances;
|
|
1338
899
|
const useInstancesAsOld =
|
|
1339
900
|
oldState === newState ||
|
|
@@ -1343,7 +904,6 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
1343
904
|
? instances.map((inst) => inst.item)
|
|
1344
905
|
: stateOldArray;
|
|
1345
906
|
|
|
1346
|
-
// Compiled path: Use pre-compiled batch function when available
|
|
1347
907
|
if (compiled.canUseCompiled(iterationNode)) {
|
|
1348
908
|
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
1349
909
|
if (
|
|
@@ -1356,49 +916,25 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
1356
916
|
endComment,
|
|
1357
917
|
)
|
|
1358
918
|
) {
|
|
1359
|
-
// Still treeless — keep the honest any-change subscription.
|
|
1360
919
|
markAlways(trackSub);
|
|
1361
920
|
return;
|
|
1362
921
|
}
|
|
1363
|
-
// Fall through to runtime path if compiled failed
|
|
1364
922
|
}
|
|
1365
923
|
|
|
1366
|
-
// Bulk path: skip O(n²) LCS when arrays share no common items
|
|
1367
|
-
// Handles empty→full, full→empty, and full replacement (no shared keys)
|
|
1368
924
|
if (oldArray.length === 0 || newArray.length === 0) {
|
|
1369
925
|
bulkReplace(iterationNode, newArray, newState, manifest, parentScope);
|
|
1370
926
|
return;
|
|
1371
927
|
}
|
|
1372
928
|
|
|
1373
|
-
// Treeless rows (batch-rendered) can't be patched in place — they have no
|
|
1374
|
-
// parsed tree for updateInstance to walk. When affected pushes them as
|
|
1375
|
-
// iteration-affected (see the same predicate in affected.js), do a full
|
|
1376
|
-
// re-batch so row bindings reflect current state. Clone-rendered iterations
|
|
1377
|
-
// skip this — they take the diff path below.
|
|
1378
929
|
const hasTreelessInstances = instances.length > 0 && !instances[0].tree;
|
|
1379
930
|
if (hasTreelessInstances && oldState !== newState) {
|
|
1380
|
-
// Treeless rows stay on the any-change subscription. If bulkReplace
|
|
1381
|
-
// below clone-renders trees instead, the next new-side eval re-records
|
|
1382
|
-
// real deps (self-healing).
|
|
1383
931
|
markAlways(trackSub);
|
|
1384
|
-
// affected.js conservatively flags a treeless (batch-rendered) iteration on
|
|
1385
|
-
// ANY state change, since it can't walk per-row trees to see which bindings
|
|
1386
|
-
// actually depend on what changed. Before tearing down and recreating every
|
|
1387
|
-
// row, re-run the batch: if it yields identical HTML, the rows don't depend
|
|
1388
|
-
// on what changed, so keep the existing DOM nodes — preserving their event
|
|
1389
|
-
// listeners (e.g. tooltip mouseleave) and any in-progress click on a row
|
|
1390
|
-
// control. DOM-property writes (value/checked/etc.) aren't reflected in the
|
|
1391
|
-
// HTML string, so they're re-applied against the kept rows — property
|
|
1392
|
-
// assignment also wins over a user-dirtied checkbox, which an attribute
|
|
1393
|
-
// rewrite wouldn't.
|
|
1394
932
|
const rt = iterationNode.runtime;
|
|
1395
933
|
if (rt.batchFn && rt.lastBatchHtml !== undefined) {
|
|
1396
934
|
const stateValues = rt.stateKeys.map((k) => newState[k]);
|
|
1397
935
|
const newHtml = rt.batchFn(newArray, ...stateValues, newState);
|
|
1398
936
|
if (newHtml === rt.lastBatchHtml) {
|
|
1399
937
|
if (rt.domPropertyWrites?.length) {
|
|
1400
|
-
// Identical HTML implies identical row count — refresh item refs so
|
|
1401
|
-
// $scope handlers and property writes read the live array.
|
|
1402
938
|
for (let i = 0; i < instances.length; i++) instances[i].item = newArray[i];
|
|
1403
939
|
applyDomPropertyWrites(
|
|
1404
940
|
instances,
|
|
@@ -1423,25 +959,18 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
1423
959
|
getItemKey(item, i, evalKeyForItem(iterationNode, item, i, newState, parentScope)),
|
|
1424
960
|
);
|
|
1425
961
|
|
|
1426
|
-
// Detect index-coupled fallback keys (debug-mode warning only).
|
|
1427
962
|
if (!iterationNode.meta.keyExpr && newKeys.some(k => k.startsWith('hash_') || k.startsWith('val_'))) {
|
|
1428
963
|
warnIndexCoupledKey(iterationNode);
|
|
1429
964
|
}
|
|
1430
965
|
|
|
1431
|
-
// O(n) check: if no keys are shared, do bulk replacement instead of O(n²) LCS
|
|
1432
966
|
const oldKeySet = new Set(oldKeys);
|
|
1433
967
|
if (!newKeys.some(k => oldKeySet.has(k))) {
|
|
1434
968
|
bulkReplace(iterationNode, newArray, newState, manifest, parentScope);
|
|
1435
969
|
return;
|
|
1436
970
|
}
|
|
1437
971
|
|
|
1438
|
-
// Standard diff-based updates (arrays share some common items)
|
|
1439
972
|
const operations = computeDiff(oldKeys, newKeys, oldArray, newArray);
|
|
1440
973
|
|
|
1441
|
-
// Apply array-shape operations and capture which instances were
|
|
1442
|
-
// bindings-refreshed by an op (ADD built a fresh tree with newState;
|
|
1443
|
-
// UPDATE patched bindings in place). Captured by reference, not index,
|
|
1444
|
-
// because indices shift during the loop.
|
|
1445
974
|
const refreshedByOps = new WeakSet();
|
|
1446
975
|
operations.forEach((op) => {
|
|
1447
976
|
switch (op.type) {
|
|
@@ -1462,10 +991,6 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
1462
991
|
}
|
|
1463
992
|
});
|
|
1464
993
|
|
|
1465
|
-
// Refresh every row the ops didn't touch: moved rows (index changed,
|
|
1466
|
-
// bindings stale) and untouched rows whose outer-scope bindings depend
|
|
1467
|
-
// on state that changed in this update cycle. Each row goes through
|
|
1468
|
-
// updateInstance exactly once — via an op or here.
|
|
1469
994
|
const retained = iterationNode.runtime.instances;
|
|
1470
995
|
for (let i = 0; i < retained.length; i++) {
|
|
1471
996
|
retained[i].index = i;
|
|
@@ -1473,27 +998,13 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
1473
998
|
updateInstance(iterationNode, i, retained[i].item, oldState, newState, manifest, parentScope);
|
|
1474
999
|
}
|
|
1475
1000
|
|
|
1476
|
-
// Re-stamp after the diff settles: moved/updated instances now carry their
|
|
1477
|
-
// current item + index, so `$scope` handlers resolve correctly post-reorder.
|
|
1478
1001
|
stampScopes(iterationNode, manifest, parentScope);
|
|
1479
1002
|
};
|
|
1480
1003
|
|
|
1481
|
-
// Prune removed iteration rows from the global manifest (dotPath -> element)
|
|
1482
|
-
// and parsed tree. Both views still reference rows that mounted globally-tracked
|
|
1483
|
-
// content (e.g. a <component src> registers its subtree in the manifest). Vibe
|
|
1484
|
-
// disconnects the page MutationObserver while it reconciles, so the removals
|
|
1485
|
-
// below are never observed — left unpruned, the entries pin detached subtrees
|
|
1486
|
-
// (memory leak) and bloat every later affected/hydrate walk (the
|
|
1487
|
-
// combat-fps-decays-per-reset bug). Scoped to exactly the removed nodes.
|
|
1488
1004
|
const releaseRemovedSubtrees = (removedRoots, manifest) => {
|
|
1489
1005
|
if (!manifest || removedRoots.length === 0) return;
|
|
1490
1006
|
const tree = manifest.__tree;
|
|
1491
1007
|
|
|
1492
|
-
// Reverse the manifest (dotPath -> element) once so each removed root resolves
|
|
1493
|
-
// to its path. An element can be registered at more than one path (a component
|
|
1494
|
-
// wrapper appears at both its own node and an inlined child slot); keep the
|
|
1495
|
-
// SHORTEST so the path scopes the whole row subtree, not an inner slot.
|
|
1496
|
-
// __live / __tree are non-enumerable, so for-in skips them.
|
|
1497
1008
|
const pathOf = new Map();
|
|
1498
1009
|
for (const key in manifest) {
|
|
1499
1010
|
const el = manifest[key];
|
|
@@ -1502,8 +1013,8 @@ const releaseRemovedSubtrees = (removedRoots, manifest) => {
|
|
|
1502
1013
|
if (existing === undefined || key.length < existing.length) pathOf.set(el, key);
|
|
1503
1014
|
}
|
|
1504
1015
|
|
|
1505
|
-
const removedEls = new Set();
|
|
1506
|
-
const removedPaths = new Set();
|
|
1016
|
+
const removedEls = new Set();
|
|
1017
|
+
const removedPaths = new Set();
|
|
1507
1018
|
for (let i = 0; i < removedRoots.length; i++) {
|
|
1508
1019
|
const root = removedRoots[i];
|
|
1509
1020
|
if (!root) continue;
|
|
@@ -1513,30 +1024,15 @@ const releaseRemovedSubtrees = (removedRoots, manifest) => {
|
|
|
1513
1024
|
}
|
|
1514
1025
|
if (removedPaths.size === 0 && removedEls.size === 0) return;
|
|
1515
1026
|
|
|
1516
|
-
// Manifest: drop every entry at or under a removed root's path. Path scope (not
|
|
1517
|
-
// element identity) is what catches content hoisted out of the DOM — an inactive
|
|
1518
|
-
// conditional branch template sits in a detached container yet stays registered
|
|
1519
|
-
// under its row's path.
|
|
1520
1027
|
if (removedPaths.size > 0) {
|
|
1521
1028
|
for (const key in manifest) {
|
|
1522
1029
|
if (pathUnderRemoved(key, removedPaths)) delete manifest[key];
|
|
1523
1030
|
}
|
|
1524
1031
|
}
|
|
1525
1032
|
|
|
1526
|
-
// Parsed tree: delete each removed root's node — its whole subtree (nested
|
|
1527
|
-
// conditionals/iterations and their branch templates) goes with it. Prune
|
|
1528
|
-
// by CONTAINMENT, not just root identity: a row's inlined component trees
|
|
1529
|
-
// are also linked under OTHER ancestors' children (the dual linkage the
|
|
1530
|
-
// affected-walk dedupe documents), keyed by inner wrapper elements that are
|
|
1531
|
-
// never themselves removed roots. Left in place, every later walk descends
|
|
1532
|
-
// those detached trees — re-hydrating dead DOM and (since subscriptions)
|
|
1533
|
-
// re-registering its subscribers after teardown pruned them, pinning the
|
|
1534
|
-
// whole removed subtree forever.
|
|
1535
1033
|
const roots = [...removedEls];
|
|
1536
1034
|
if (tree) pruneTreeNodes(tree, removedEls, roots);
|
|
1537
1035
|
|
|
1538
|
-
// The removed rows' subscribers (bindings, nested conditionals/iterations)
|
|
1539
|
-
// are anchored to nodes that just left the document.
|
|
1540
1036
|
pruneDisconnected();
|
|
1541
1037
|
};
|
|
1542
1038
|
|
|
@@ -1547,7 +1043,6 @@ const underRemovedRoot = (el, roots) => {
|
|
|
1547
1043
|
return false;
|
|
1548
1044
|
};
|
|
1549
1045
|
|
|
1550
|
-
// True when `key` is, or is a descendant of, any path in `removedPaths`.
|
|
1551
1046
|
const pathUnderRemoved = (key, removedPaths) => {
|
|
1552
1047
|
if (removedPaths.has(key)) return true;
|
|
1553
1048
|
for (let i = key.indexOf('.', 1); i !== -1; i = key.indexOf('.', i + 1)) {
|
|
@@ -1568,17 +1063,10 @@ const pruneTreeNodes = (node, removedEls, roots) => {
|
|
|
1568
1063
|
}
|
|
1569
1064
|
};
|
|
1570
1065
|
|
|
1571
|
-
// Bulk replacement: clear all DOM and re-render from scratch
|
|
1572
|
-
// Used when arrays share no common keys (avoids O(n²) LCS)
|
|
1573
1066
|
const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
1574
1067
|
const { template, startComment, endComment } = iterationNode.meta;
|
|
1575
1068
|
const parent = startComment.parentNode;
|
|
1576
1069
|
|
|
1577
|
-
// Clear all existing DOM between comments using Range (single operation),
|
|
1578
|
-
// then prune the removed subtrees from the manifest + tree. Collect the LIVE
|
|
1579
|
-
// nodes in the range rather than the instances' clonedNodes: a <component src>
|
|
1580
|
-
// row is replaced in place by component.js, so clonedNodes can point at the
|
|
1581
|
-
// stale original wrapper, not the processed content actually being removed.
|
|
1582
1070
|
if (iterationNode.runtime.instances.length > 0) {
|
|
1583
1071
|
const removedRoots = [];
|
|
1584
1072
|
for (let cur = startComment.nextSibling; cur && cur !== endComment; cur = cur.nextSibling) {
|
|
@@ -1596,8 +1084,6 @@ const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
|
1596
1084
|
return;
|
|
1597
1085
|
}
|
|
1598
1086
|
|
|
1599
|
-
// For simple templates (no nested iterations/conditionals, single root element),
|
|
1600
|
-
// use batch string rendering: one string concatenation loop + one innerHTML parse
|
|
1601
1087
|
if (canUseBatchRender(template)) {
|
|
1602
1088
|
renderBatch(iterationNode, newArray, state, parent, endComment, parentScope, manifest);
|
|
1603
1089
|
const instances = iterationNode.runtime.instances;
|
|
@@ -1607,20 +1093,9 @@ const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
|
1607
1093
|
return;
|
|
1608
1094
|
}
|
|
1609
1095
|
|
|
1610
|
-
// Complex templates: one shared clone+hydrate render loop.
|
|
1611
1096
|
renderInstances(iterationNode, newArray, state, manifest, parentScope);
|
|
1612
1097
|
};
|
|
1613
1098
|
|
|
1614
|
-
// Find an instance's canonical in-DOM anchor (the first of its cloned nodes
|
|
1615
|
-
// that still lives directly under the iteration's parent). Nested primitives
|
|
1616
|
-
// inside the iteration template — <!-- if -->, <!-- each -->, <component> —
|
|
1617
|
-
// can move/replace cloned nodes between iteration renders (inactive branches
|
|
1618
|
-
// get hoisted into template containers; component[src] wrappers get swapped
|
|
1619
|
-
// for processed wrappers). Any of those mutations make `clonedNodes[0]` a
|
|
1620
|
-
// stale reference to a node no longer under the iteration parent. Callers use
|
|
1621
|
-
// this anchor instead of trusting `clonedNodes[0]` directly, so that
|
|
1622
|
-
// insert-before / move operations always resolve against the iteration's real
|
|
1623
|
-
// DOM slot.
|
|
1624
1099
|
const findInstanceAnchor = (instance, iterationParent) => {
|
|
1625
1100
|
const nodes = instance.clonedNodes || (instance.element ? [instance.element] : []);
|
|
1626
1101
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -1629,10 +1104,6 @@ const findInstanceAnchor = (instance, iterationParent) => {
|
|
|
1629
1104
|
return null;
|
|
1630
1105
|
};
|
|
1631
1106
|
|
|
1632
|
-
// Resolve the reference node for `parent.insertBefore(..., ref)` at a given
|
|
1633
|
-
// logical iteration index. Walks later instances until it finds one with a
|
|
1634
|
-
// live anchor under the iteration parent; falls back to `endComment` when no
|
|
1635
|
-
// later instance has any node currently mounted in the iteration.
|
|
1636
1107
|
const resolveInsertBefore = (iterationNode, index, parent) => {
|
|
1637
1108
|
const { instances } = iterationNode.runtime;
|
|
1638
1109
|
for (let i = index; i < instances.length; i++) {
|
|
@@ -1642,28 +1113,15 @@ const resolveInsertBefore = (iterationNode, index, parent) => {
|
|
|
1642
1113
|
return iterationNode.meta.endComment;
|
|
1643
1114
|
};
|
|
1644
1115
|
|
|
1645
|
-
// Detach every DOM node belonging to a logical instance, including content
|
|
1646
|
-
// mounted by nested primitives (conditional branches, nested each rows,
|
|
1647
|
-
// fetched component wrappers) that isn't tracked in `instance.clonedNodes`.
|
|
1648
|
-
// Walks iteration-parent siblings from this instance's anchor up to the next
|
|
1649
|
-
// instance's anchor / endComment, so anything in between — clones, mounted
|
|
1650
|
-
// branches, swapped-in component wrappers — all gets detached. Also sweeps
|
|
1651
|
-
// any clonedNodes that were hoisted out of the iteration parent (e.g. into a
|
|
1652
|
-
// sibling conditional's template container).
|
|
1653
1116
|
const detachInstanceDom = (iterationNode, index, parent) => {
|
|
1654
1117
|
const instance = iterationNode.runtime.instances[index];
|
|
1655
1118
|
const { endComment } = iterationNode.meta;
|
|
1656
1119
|
const anchor = findInstanceAnchor(instance, parent);
|
|
1657
1120
|
const nextAnchor = resolveInsertBefore(iterationNode, index + 1, parent);
|
|
1658
1121
|
|
|
1659
|
-
// Return the live nodes actually removed so callers can prune them from the
|
|
1660
|
-
// manifest + tree (clonedNodes can be stale once a <component src> row is
|
|
1661
|
-
// replaced in place by component.js).
|
|
1662
1122
|
const removed = [];
|
|
1663
1123
|
if (anchor) {
|
|
1664
1124
|
let cur = anchor;
|
|
1665
|
-
// endComment caps the walk even if nextAnchor ordering is ever
|
|
1666
|
-
// corrupted — iteration DOM is bounded by startComment / endComment.
|
|
1667
1125
|
while (cur && cur !== nextAnchor && cur !== endComment) {
|
|
1668
1126
|
const nextSibling = cur.nextSibling;
|
|
1669
1127
|
parent.removeChild(cur);
|
|
@@ -1683,14 +1141,6 @@ const detachInstanceDom = (iterationNode, index, parent) => {
|
|
|
1683
1141
|
return removed;
|
|
1684
1142
|
};
|
|
1685
1143
|
|
|
1686
|
-
// Build a fresh instance's DOM + tree + scope from the iteration template.
|
|
1687
|
-
// Pure function — no DOM insertion, no side effects on iteration state.
|
|
1688
|
-
// Callers decide where the clones go (iteration parent, DocumentFragment).
|
|
1689
|
-
// `liveItem` (resolved by the caller via resolveLiveArray, so a derived-array
|
|
1690
|
-
// loop evaluates the expression once per render, not once per row) is the live
|
|
1691
|
-
// `$`-proxy element for this index. Building scope from it gives nested
|
|
1692
|
-
// conditional stamps and `$scope` handlers the app-visible identity; `item`
|
|
1693
|
-
// (plain snapshot) is still tracked for the diff.
|
|
1694
1144
|
const buildInstance = (iterationNode, item, index, state, parentScope, liveItem) => {
|
|
1695
1145
|
const { itemAlias, indexAlias, template, startComment } = iterationNode.meta;
|
|
1696
1146
|
const localVars = { [itemAlias]: liveItem, [indexAlias]: index };
|
|
@@ -1702,11 +1152,6 @@ const buildInstance = (iterationNode, item, index, state, parentScope, liveItem)
|
|
|
1702
1152
|
return { ...built, scopedState, localVars, liveItem };
|
|
1703
1153
|
};
|
|
1704
1154
|
|
|
1705
|
-
// After a built instance's clones are placed in the DOM (directly or via a
|
|
1706
|
-
// fragment), mark element clones as managed so the page-level MutationObserver
|
|
1707
|
-
// skips them in processMutations, then fire nested iteration/conditional
|
|
1708
|
-
// renders. Without the managed mark those clones would be re-parsed + hydrated
|
|
1709
|
-
// on top of the internal render, duplicating every nested branch.
|
|
1710
1155
|
const finalizeInstance = (built, manifest, parentScope) => {
|
|
1711
1156
|
const { clonedNodes, tree, scopedState, localVars } = built;
|
|
1712
1157
|
for (let i = 0; i < clonedNodes.length; i++) {
|
|
@@ -1719,10 +1164,6 @@ const finalizeInstance = (built, manifest, parentScope) => {
|
|
|
1719
1164
|
}
|
|
1720
1165
|
};
|
|
1721
1166
|
|
|
1722
|
-
// Build, finalize, and commit a fresh set of instances for `array` in one
|
|
1723
|
-
// batched DOM insertion, replacing iterationNode.runtime.instances. The single
|
|
1724
|
-
// clone+hydrate render loop shared by initial render (renderIteration) and full
|
|
1725
|
-
// rebuild (bulkReplace) — they differ only in their preamble, not this loop.
|
|
1726
1167
|
const renderInstances = (iterationNode, array, state, manifest, parentScope) => {
|
|
1727
1168
|
const { startComment, endComment } = iterationNode.meta;
|
|
1728
1169
|
const parent = startComment.parentNode;
|
|
@@ -1740,7 +1181,6 @@ const renderInstances = (iterationNode, array, state, manifest, parentScope) =>
|
|
|
1740
1181
|
}
|
|
1741
1182
|
parent.insertBefore(frag, endComment);
|
|
1742
1183
|
iterationNode.runtime.instances = instances;
|
|
1743
|
-
// Stamp loop scope so `$scope(this,'alias')` handlers resolve the live item.
|
|
1744
1184
|
stampScopes(iterationNode, manifest, parentScope);
|
|
1745
1185
|
};
|
|
1746
1186
|
|
|
@@ -1777,30 +1217,18 @@ const moveInstance = (iterationNode, fromIndex, toIndex) => {
|
|
|
1777
1217
|
|
|
1778
1218
|
const insertBefore = resolveInsertBefore(iterationNode, toIndex + 1, parent);
|
|
1779
1219
|
const nodes = instance.clonedNodes || [instance.element];
|
|
1780
|
-
// Re-insert only nodes currently under the iteration parent — those hoisted
|
|
1781
|
-
// into nested-conditional template containers stay there so we don't
|
|
1782
|
-
// double-count branch content.
|
|
1783
1220
|
for (let i = 0; i < nodes.length; i++) {
|
|
1784
1221
|
const n = nodes[i];
|
|
1785
1222
|
if (n?.parentNode === parent) parent.insertBefore(n, insertBefore);
|
|
1786
1223
|
}
|
|
1787
1224
|
};
|
|
1788
1225
|
|
|
1789
|
-
// Detach-and-rebuild fallback. Used when the row's parsed tree isn't
|
|
1790
|
-
// available (compiled iterations), or when the row template's top-level
|
|
1791
|
-
// shape itself depends on the item (a `<!-- if -->` directly under the
|
|
1792
|
-
// iteration with the item in its expression — flipping branches needs a
|
|
1793
|
-
// rebuild because the active root element type changes).
|
|
1794
1226
|
const updateInstanceRebuild = (iterationNode, index, newItem, state, manifest, parentScope) => {
|
|
1795
1227
|
const parent = iterationNode.meta.startComment.parentNode;
|
|
1796
1228
|
const instance = iterationNode.runtime.instances[index];
|
|
1797
1229
|
|
|
1798
1230
|
const liveItem = liveItemAt(resolveLiveArray(iterationNode, manifest, parentScope), index, newItem);
|
|
1799
1231
|
const built = buildInstance(iterationNode, newItem, index, state, parentScope, liveItem);
|
|
1800
|
-
// Prune the OLD row's manifest/tree entries before finalizing the rebuilt one,
|
|
1801
|
-
// so the replaced subtree is released (same removal contract as removeInstance).
|
|
1802
|
-
// Order matters: prune the detached old nodes before finalizeInstance registers
|
|
1803
|
-
// the new ones, so the new entries are never touched.
|
|
1804
1232
|
const removed = parent ? detachInstanceDom(iterationNode, index, parent) : [];
|
|
1805
1233
|
releaseRemovedSubtrees(removed, manifest);
|
|
1806
1234
|
const insertBefore = resolveInsertBefore(iterationNode, index + 1, parent);
|
|
@@ -1815,17 +1243,6 @@ const updateInstanceRebuild = (iterationNode, index, newItem, state, manifest, p
|
|
|
1815
1243
|
instance.scopedState = built.scopedState;
|
|
1816
1244
|
};
|
|
1817
1245
|
|
|
1818
|
-
// In-place refresh of a single row: rebuild scoped states from oldState +
|
|
1819
|
-
// newState (and possibly a new item), run affected→hydrate against the row's
|
|
1820
|
-
// parsed tree. Same pipeline as top-level state changes; the row's DOM nodes
|
|
1821
|
-
// survive, only changed bindings update.
|
|
1822
|
-
//
|
|
1823
|
-
// Called from two sites in updateIteration:
|
|
1824
|
-
// 1. UPDATE diff op — newItem differs from instance.item
|
|
1825
|
-
// 2. Post-diff outer-state propagation — newItem === instance.item
|
|
1826
|
-
// Both flow through here so iteration updates have one code path, not two.
|
|
1827
|
-
// Compiled / batch instances have no row tree to walk and fall back to a
|
|
1828
|
-
// full rebuild via updateInstanceRebuild.
|
|
1829
1246
|
const updateInstance = (iterationNode, index, newItem, oldState, newState, manifest, parentScope = {}) => {
|
|
1830
1247
|
if (index < 0 || index >= iterationNode.runtime.instances.length) return;
|
|
1831
1248
|
const instance = iterationNode.runtime.instances[index];
|
|
@@ -1837,11 +1254,6 @@ const updateInstance = (iterationNode, index, newItem, oldState, newState, manif
|
|
|
1837
1254
|
|
|
1838
1255
|
const { itemAlias, indexAlias } = iterationNode.meta;
|
|
1839
1256
|
const newLocalVars = { [itemAlias]: newItem, [indexAlias]: index };
|
|
1840
|
-
// The "old" scoped state is exactly last cycle's "new" one — same global
|
|
1841
|
-
// snapshot (this cycle's oldState) and same item/index — already stored on the
|
|
1842
|
-
// instance. Reuse it instead of allocating a second proxy per row per frame
|
|
1843
|
-
// (createScopedState is a combat hot spot). Falls back on the first update
|
|
1844
|
-
// after an add, before scopedState has been recorded.
|
|
1845
1257
|
const oldScopedState =
|
|
1846
1258
|
instance.scopedState ||
|
|
1847
1259
|
createScopedState(oldState, { [itemAlias]: instance.item, [indexAlias]: index }, parentScope);
|