@ape-egg/vibe 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +10 -15
- package/llms.txt +8 -6
- package/package.json +19 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +312 -99
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +251 -111
- package/runtime/index.js +180 -71
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +69 -5
- package/runtime/pre-compiled-iterations.js +19 -6
- package/runtime/pre-compiled-manifest.js +13 -4
- package/runtime/staging.js +153 -0
- package/runtime/state.js +31 -0
- package/runtime/tracking.js +173 -0
- package/runtime/utils.js +155 -78
- package/spa.js +77 -14
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1196
- package/ROADMAP.md +0 -397
- package/compiler/bin/vibe-compile.js +0 -121
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +0 -2023
- package/compiler/src/Cargo.toml +0 -38
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
- package/compiler/src/compiler/binding_case.rs +0 -88
- package/compiler/src/compiler/compile.rs +0 -2880
- package/compiler/src/compiler/component_tagger.rs +0 -469
- package/compiler/src/compiler/iteration_optimizer.rs +0 -455
- package/compiler/src/compiler/js_analyzer.rs +0 -715
- package/compiler/src/compiler/manifest_builder.rs +0 -693
- package/compiler/src/compiler/mod.rs +0 -16
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/spa.rs +0 -477
- package/compiler/src/compiler/state_extractor.rs +0 -263
- package/compiler/src/compiler/value_stamper.rs +0 -921
- package/compiler/src/compiler/watcher.rs +0 -1278
- package/compiler/src/config.rs +0 -279
- package/compiler/src/main.rs +0 -358
- package/compiler/src/parser/element.rs +0 -96
- package/compiler/src/parser/html.rs +0 -1004
- package/compiler/src/parser/mod.rs +0 -8
- package/runtime/pre-compiled-manifest.test.mjs +0 -58
- package/runtime/scope.js +0 -50
- package/test-results/.last-run.json +0 -4
package/runtime/conditionals.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import parse from './parse.js';
|
|
2
|
-
import affected from './affected.js';
|
|
2
|
+
import affected, { nodeSubscriberOf } from './affected.js';
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
|
+
import { beginTracking, endTracking, pruneDisconnected } from './tracking.js';
|
|
5
|
+
import { overlayKeysOf } from './utils.js';
|
|
6
|
+
import { setManifestEntry, manifestPathOf } from './manifest.js';
|
|
4
7
|
import { createScopedState, renderAllIterations, initializeBlock, resolveIterationComponentProps } from './iterate.js';
|
|
5
8
|
import { evalInScope } from './utils.js';
|
|
6
9
|
import { collectComponentIds, releaseOrphanedComponentState, executeCompiledComponentScriptsIn } from './component.js';
|
|
10
|
+
import { isOutgoing } from './staging.js';
|
|
7
11
|
|
|
8
12
|
// Registry of DOM nodes owned by conditional branches.
|
|
9
13
|
// Maps a DOM node to { nodes: array_ref, index: number } so that
|
|
@@ -17,22 +21,19 @@ export const branchNodeRegistry = new WeakMap();
|
|
|
17
21
|
export const managedNodes = new WeakSet();
|
|
18
22
|
|
|
19
23
|
// Find the dot path of a conditional node in the manifest.
|
|
20
|
-
//
|
|
24
|
+
// Resolves the parent element through the reverse index, then appends the
|
|
25
|
+
// conditional's key.
|
|
21
26
|
const findConditionalPath = (node, manifest) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
if (!parentEntry) return null;
|
|
27
|
+
const parentPath = manifestPathOf(manifest, node.meta.startComment.parentNode);
|
|
28
|
+
if (parentPath === null) return null;
|
|
25
29
|
|
|
26
|
-
// Find which key this conditional has in the parent's children
|
|
27
|
-
// by matching the startComment reference
|
|
28
|
-
const [parentPath] = parentEntry;
|
|
29
30
|
return `${parentPath}.${node._key}`;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
// Register branch tree nodes in the manifest (recursive)
|
|
33
34
|
const addBranchToManifest = (tree, manifest, basePath) => {
|
|
34
35
|
if (tree.element) {
|
|
35
|
-
manifest
|
|
36
|
+
setManifestEntry(manifest, basePath, tree.element);
|
|
36
37
|
}
|
|
37
38
|
if (tree.children) {
|
|
38
39
|
for (const key in tree.children) {
|
|
@@ -71,6 +72,7 @@ export const extractDependencies = (expression) => {
|
|
|
71
72
|
return [...new Set(matches)]; // Unique values
|
|
72
73
|
};
|
|
73
74
|
|
|
75
|
+
|
|
74
76
|
// Render all conditionals in the parsed tree
|
|
75
77
|
export const renderAllConditionals = (tree, state, manifest, parentScope = {}) => {
|
|
76
78
|
let count = 0;
|
|
@@ -81,8 +83,9 @@ export const renderAllConditionals = (tree, state, manifest, parentScope = {}) =
|
|
|
81
83
|
return 1;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
// Recursively render conditionals in child nodes
|
|
85
|
-
|
|
86
|
+
// Recursively render conditionals in child nodes — except under an
|
|
87
|
+
// outgoing wrapper, whose content the pending swap will replace wholesale.
|
|
88
|
+
if (tree.children && !(tree.element && isOutgoing(tree.element))) {
|
|
86
89
|
Object.keys(tree.children).forEach((key) => {
|
|
87
90
|
const child = tree.children[key];
|
|
88
91
|
if (typeof child === 'object' && child !== null) {
|
|
@@ -94,6 +97,42 @@ export const renderAllConditionals = (tree, state, manifest, parentScope = {}) =
|
|
|
94
97
|
return count;
|
|
95
98
|
};
|
|
96
99
|
|
|
100
|
+
// Re-settle every ALREADY-RENDERED conditional against the current world.
|
|
101
|
+
// The boot's initial pass can run before module scripts have evaluated
|
|
102
|
+
// (compiled shell: boot.js rides the vibe-module chain), so a gate on a
|
|
103
|
+
// module-provided global — `<!-- if window.isDev -->` — first evaluates
|
|
104
|
+
// against the pre-module world and carries no reactive dependency that would
|
|
105
|
+
// ever re-check it. Walks like renderAllConditionals but routes through
|
|
106
|
+
// updateConditional: value-unchanged branches are a no-op, changed ones
|
|
107
|
+
// mount/unmount through the normal branch machinery.
|
|
108
|
+
export const settleConditionals = (tree, state, manifest, parentScope = {}) => {
|
|
109
|
+
if (tree.type === 'conditional') {
|
|
110
|
+
updateConditional(tree, state, state, manifest, parentScope);
|
|
111
|
+
// A COMPILED pre-rendered conditional keeps its live content in
|
|
112
|
+
// `children` (never mountBranch'd) — nested conditionals there need
|
|
113
|
+
// settling too. Connected-root check skips a runtime conditional's
|
|
114
|
+
// detached template nodes.
|
|
115
|
+
if (tree.children) {
|
|
116
|
+
Object.keys(tree.children).forEach((key) => {
|
|
117
|
+
const child = tree.children[key];
|
|
118
|
+
if (!child || typeof child !== 'object') return;
|
|
119
|
+
const root = child.element ?? child.textNode;
|
|
120
|
+
if (!root || !root.isConnected) return;
|
|
121
|
+
settleConditionals(child, state, manifest, parentScope);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (tree.children && !(tree.element && isOutgoing(tree.element))) {
|
|
127
|
+
Object.keys(tree.children).forEach((key) => {
|
|
128
|
+
const child = tree.children[key];
|
|
129
|
+
if (typeof child === 'object' && child !== null) {
|
|
130
|
+
settleConditionals(child, state, manifest, parentScope);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
97
136
|
// Initial render of a conditional block
|
|
98
137
|
export const renderConditional = (node, state, manifest, parentScope = {}) => {
|
|
99
138
|
const { expression, startComment, endComment, branches } = node.meta;
|
|
@@ -141,8 +180,13 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
|
|
|
141
180
|
// @ts-ignore - adding custom property to comment node
|
|
142
181
|
startComment.__vibeRendered = true;
|
|
143
182
|
|
|
144
|
-
// Evaluate condition with current state
|
|
183
|
+
// Evaluate condition with current state — inside a tracking window, so the
|
|
184
|
+
// conditional subscribes to what its expression read from first render on.
|
|
185
|
+
const trackSub = nodeSubscriberOf(node, 'conditional');
|
|
186
|
+
trackSub.lastScope = state;
|
|
187
|
+
beginTracking(trackSub, overlayKeysOf(state));
|
|
145
188
|
const conditionResult = evaluateCondition(expression, state, startComment.parentElement);
|
|
189
|
+
endTracking();
|
|
146
190
|
|
|
147
191
|
// Determine which branch to mount
|
|
148
192
|
const branchToMount = conditionResult ? branches.if : branches.else;
|
|
@@ -243,7 +287,7 @@ const mountBranch = (node, branchData, state, manifest, parentScope) => {
|
|
|
243
287
|
// mounts (or re-mounts after unmount) must run its own scripts here to
|
|
244
288
|
// re-register component-local state. Must happen BEFORE rendering nested
|
|
245
289
|
// iterations/conditionals so `<!-- each _cN.x -->` sees the registered state.
|
|
246
|
-
executeCompiledComponentScriptsIn(clonedNodes);
|
|
290
|
+
const scriptsPending = executeCompiledComponentScriptsIn(clonedNodes);
|
|
247
291
|
|
|
248
292
|
// Recursively render any nested iterations and conditionals
|
|
249
293
|
if (branchTree) {
|
|
@@ -257,6 +301,31 @@ const mountBranch = (node, branchData, state, manifest, parentScope) => {
|
|
|
257
301
|
nodes: clonedNodes,
|
|
258
302
|
parsedTree: branchTree,
|
|
259
303
|
};
|
|
304
|
+
|
|
305
|
+
// Import-bearing branch scripts run async through the script chain, so the
|
|
306
|
+
// eager render above may evaluate expressions before the window helpers those
|
|
307
|
+
// scripts define exist — and defining a global is not a state write, so no
|
|
308
|
+
// flush ever re-dispatches them (the fetch path never has this gap: it mounts
|
|
309
|
+
// DOM only after its scripts ran). Finish the mount when the chain settles:
|
|
310
|
+
// re-render this branch's directives against the fully-scripted world.
|
|
311
|
+
// Idempotent — rendered iterations early-return, value-unchanged conditionals
|
|
312
|
+
// are a no-op — and skipped when a remount superseded this instance.
|
|
313
|
+
//
|
|
314
|
+
// The settle evaluates CURRENT state (the live `$`), never the mount-time
|
|
315
|
+
// snapshot: state that legitimately changed while the imports were pending
|
|
316
|
+
// (a nested gate flipping true off a socket flush) already dispatched and
|
|
317
|
+
// mounted — settling against the stale snapshot would unmount it again.
|
|
318
|
+
// Loop-scoped branches keep their mount scope (the row's aliases aren't
|
|
319
|
+
// reachable from the root proxy); a row update supersedes activeInstance
|
|
320
|
+
// and the guard skips the settle entirely.
|
|
321
|
+
if (scriptsPending && branchTree) {
|
|
322
|
+
scriptsPending.then(() => {
|
|
323
|
+
if (node.runtime.activeInstance?.nodes !== clonedNodes) return;
|
|
324
|
+
const healState = node.meta.scopeAliases?.length ? scopedState : (window.$ ?? scopedState);
|
|
325
|
+
renderAllIterations(branchTree, healState, manifest, parentScope);
|
|
326
|
+
settleConditionals(branchTree, healState, manifest, parentScope);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
260
329
|
};
|
|
261
330
|
|
|
262
331
|
// Unmount currently active branch
|
|
@@ -299,11 +368,34 @@ const unmountBranch = (node, manifest) => {
|
|
|
299
368
|
|
|
300
369
|
// CLEANUP OF CURRENT STATE
|
|
301
370
|
releaseOrphanedComponentState(ids);
|
|
371
|
+
// The swept range's binding subscribers are dead — their anchors just left
|
|
372
|
+
// the document. (The conditional's own subscriber survives: its anchor is
|
|
373
|
+
// the start comment, which stays.)
|
|
374
|
+
pruneDisconnected();
|
|
302
375
|
|
|
303
376
|
// Clear active instance
|
|
304
377
|
node.runtime.activeInstance = null;
|
|
305
378
|
};
|
|
306
379
|
|
|
380
|
+
// Subscription dispatch: flip the branch if the expression's value moved,
|
|
381
|
+
// nothing else. Branch CONTENT updates arrive through the content's own
|
|
382
|
+
// subscribers — re-walking the branch here (updateConditional's else path)
|
|
383
|
+
// would reintroduce a partial walk per dirty conditional. A mounting branch
|
|
384
|
+
// registers its fresh subscribers through initializeBlock's hydrate pass.
|
|
385
|
+
export const dispatchConditional = (node, newState, manifest, parentScope = {}) => {
|
|
386
|
+
if (!node.runtime.templateRemoved) return;
|
|
387
|
+
const sub = nodeSubscriberOf(node, 'conditional');
|
|
388
|
+
sub.lastScope = newState;
|
|
389
|
+
beginTracking(sub, overlayKeysOf(newState));
|
|
390
|
+
const result = evaluateCondition(node.meta.expression, newState, node.meta.startComment?.parentElement);
|
|
391
|
+
endTracking();
|
|
392
|
+
const newBranchData = result ? node.meta.branches.if : node.meta.branches.else;
|
|
393
|
+
if (node.runtime.activeBranch !== newBranchData) {
|
|
394
|
+
mountBranch(node, newBranchData, newState, manifest, parentScope);
|
|
395
|
+
node.runtime.activeBranch = newBranchData;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
307
399
|
// Update conditional when dependencies change
|
|
308
400
|
export const updateConditional = (node, newState, oldState, manifest, parentScope = {}) => {
|
|
309
401
|
const { expression, branches, startComment } = node.meta;
|
|
@@ -313,8 +405,13 @@ export const updateConditional = (node, newState, oldState, manifest, parentScop
|
|
|
313
405
|
return;
|
|
314
406
|
}
|
|
315
407
|
|
|
316
|
-
// Evaluate expression with new state
|
|
408
|
+
// Evaluate expression with new state — re-records the subscription every
|
|
409
|
+
// update, so branch-dependent reads self-heal.
|
|
410
|
+
const trackSub = nodeSubscriberOf(node, 'conditional');
|
|
411
|
+
trackSub.lastScope = newState;
|
|
412
|
+
beginTracking(trackSub, overlayKeysOf(newState));
|
|
317
413
|
const newConditionResult = evaluateCondition(expression, newState, startComment?.parentElement);
|
|
414
|
+
endTracking();
|
|
318
415
|
const newBranchData = newConditionResult ? branches.if : branches.else;
|
|
319
416
|
|
|
320
417
|
// Check if branch changed (compare references)
|
package/runtime/debug.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { DEBUGGER_NAME } from './constants.js';
|
|
2
2
|
|
|
3
|
+
// Loud-in-debug evaluation failures. A typo'd binding must not fail silently
|
|
4
|
+
// while developing — "let it crash in development" — while production keeps
|
|
5
|
+
// the silent-undefined contract (a broken binding renders nothing, the page
|
|
6
|
+
// survives). Once per expression: a broken binding re-evaluates every flush
|
|
7
|
+
// and would otherwise firehose the console.
|
|
8
|
+
// The affected-walk deliberately evaluates expressions in speculative
|
|
9
|
+
// contexts (row templates against global state, conservative "can't prove
|
|
10
|
+
// unaffected" probes) where throwing is expected and self-recovering. Those
|
|
11
|
+
// are engine business — only RENDER-path failures are author business.
|
|
12
|
+
let speculativeDepth = 0;
|
|
13
|
+
export const beginSpeculative = () => {
|
|
14
|
+
speculativeDepth++;
|
|
15
|
+
};
|
|
16
|
+
export const endSpeculative = () => {
|
|
17
|
+
speculativeDepth--;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const reportedEvalErrors = new Set();
|
|
21
|
+
export const reportEvalError = (expression, element, error) => {
|
|
22
|
+
if (!globalThis.__vibe?.debug || speculativeDepth > 0 || reportedEvalErrors.has(expression)) return;
|
|
23
|
+
reportedEvalErrors.add(expression);
|
|
24
|
+
console.error(`[vibe] Binding evaluation failed: @[${expression}]`, error, element ?? '');
|
|
25
|
+
};
|
|
26
|
+
|
|
3
27
|
// Phase colors - synced with index.css debug phase colors
|
|
4
28
|
// Muted palette based on comment green (#6a9955)
|
|
5
29
|
const PHASE_COLORS = {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Subscription dispatch — subscribe mode's flush (phase 2 of auto-tracked
|
|
2
|
+
// subscriptions). Where the walk answers "what changed?" by evaluating the
|
|
3
|
+
// world, dispatch already knows: the flush's changed keys select their
|
|
4
|
+
// subscribers from the reverse index, each re-evaluates in document order
|
|
5
|
+
// through the SAME patch machinery the walk uses (hydrate's entry
|
|
6
|
+
// dispatcher, mountBranch, updateIteration) and re-records its dependencies
|
|
7
|
+
// while doing so. The walk itself retires to mount duty — first hydration,
|
|
8
|
+
// processMutations, branch mounts — where it keeps registering fresh
|
|
9
|
+
// subscribers.
|
|
10
|
+
import hydrate from './hydrate.js';
|
|
11
|
+
import { updateIteration } from './iterate.js';
|
|
12
|
+
import { dispatchConditional } from './conditionals.js';
|
|
13
|
+
import { beginTracking, endTracking, unsubscribe } from './tracking.js';
|
|
14
|
+
import { activeOutgoingRoots, outgoingRootOf, isRemountTrigger } from './staging.js';
|
|
15
|
+
import {
|
|
16
|
+
scopedOverlayOf,
|
|
17
|
+
rememberOverlay,
|
|
18
|
+
overlayKeysOf,
|
|
19
|
+
evalInScope,
|
|
20
|
+
resolveThisPath,
|
|
21
|
+
} from './utils.js';
|
|
22
|
+
import { resolvePath } from './iteration-utils.js';
|
|
23
|
+
|
|
24
|
+
// Rebuild a row subscriber's evaluation scope against the CURRENT globals.
|
|
25
|
+
// The scope it last evaluated in is a per-flush merged snapshot (stale
|
|
26
|
+
// globals); only its alias overlay is durable — item/index identity is owned
|
|
27
|
+
// by the iteration's diff, which rebuilds rows whenever it changes.
|
|
28
|
+
export const scopeFor = (sub, base) => {
|
|
29
|
+
const overlay = sub.lastScope ? scopedOverlayOf(sub.lastScope) : null;
|
|
30
|
+
if (!overlay) return base;
|
|
31
|
+
const merged = { ...base, ...overlay };
|
|
32
|
+
rememberOverlay(merged, overlay);
|
|
33
|
+
return merged;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// The plan's iteration shallow-compare, generalized from the walk's
|
|
37
|
+
// CALL_EXPR fresh-ref fix: same length + same item refs as the rendered
|
|
38
|
+
// instances ⇒ the rows are already right (outer-state bindings inside rows
|
|
39
|
+
// are their own subscribers). Ref inequality — including a reassigned array
|
|
40
|
+
// after in-place mutation — falls through to the full diff.
|
|
41
|
+
export const rowsShallowEqual = (instances, newArray) =>
|
|
42
|
+
instances.length === newArray.length &&
|
|
43
|
+
instances.every((inst, i) => inst.item === newArray[i]);
|
|
44
|
+
|
|
45
|
+
// A text node's interpolation group renders all-or-nothing (hydrate
|
|
46
|
+
// re-interpolates the whole span), so one dirty binding expands to entries
|
|
47
|
+
// for every binding in its group.
|
|
48
|
+
export const entriesForText = (sub, scope) => {
|
|
49
|
+
const e = sub.entry;
|
|
50
|
+
const group = e.binding?._group ?? [e.binding];
|
|
51
|
+
return group.map((m) => ({
|
|
52
|
+
matchOuter: m.outer,
|
|
53
|
+
matchInner: m.inner,
|
|
54
|
+
input: m.input,
|
|
55
|
+
element: e.element,
|
|
56
|
+
textNode: e.textNode,
|
|
57
|
+
scopedState: scope,
|
|
58
|
+
binding: m,
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Document-order sort over subscriber anchors — the walk is document-ordered
|
|
63
|
+
// and the corpus expects that stability. Bit 4 = FOLLOWING, 2 = PRECEDING.
|
|
64
|
+
const docOrder = (a, b) => {
|
|
65
|
+
const x = a.anchor;
|
|
66
|
+
const y = b.anchor;
|
|
67
|
+
if (x === y || !x?.compareDocumentPosition || !y) return 0;
|
|
68
|
+
const pos = x.compareDocumentPosition(y);
|
|
69
|
+
if (pos & 4) return -1;
|
|
70
|
+
if (pos & 2) return 1;
|
|
71
|
+
return 0;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const dispatchIteration = (sub, currentState, previousState, manifest) => {
|
|
75
|
+
const node = sub.node;
|
|
76
|
+
// Not yet rendered — the mount path owns first render.
|
|
77
|
+
if (!node.runtime.instances || !node.runtime.templateRemoved) return;
|
|
78
|
+
const scope = scopeFor(sub, currentState);
|
|
79
|
+
const parentEl = node.meta.startComment?.parentElement;
|
|
80
|
+
const resolvedExpr = resolveThisPath(node.meta.arrayPath, parentEl);
|
|
81
|
+
beginTracking(sub, overlayKeysOf(scope));
|
|
82
|
+
const newArray =
|
|
83
|
+
evalInScope(resolvedExpr, scope, parentEl) ?? resolvePath(scope, resolvedExpr);
|
|
84
|
+
endTracking();
|
|
85
|
+
const instances = node.runtime.instances;
|
|
86
|
+
const treeless = instances.length > 0 && !instances[0].tree;
|
|
87
|
+
// Treeless (batch/compiled) rows skip the shallow guard — their HTML may
|
|
88
|
+
// read non-array state; updateIteration's identical-HTML check absorbs
|
|
89
|
+
// no-ops there.
|
|
90
|
+
if (!treeless && Array.isArray(newArray) && rowsShallowEqual(instances, newArray)) return;
|
|
91
|
+
updateIteration(node, scope, scopeFor(sub, previousState), manifest);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// The subscribe-mode flush: document-ordered dirty set → patch. Binding
|
|
95
|
+
// entries batch through single hydrate calls between structural ops so
|
|
96
|
+
// relative order is preserved (and hydrate's same-element input chaining
|
|
97
|
+
// keeps working).
|
|
98
|
+
export default (dirty, currentState, previousState, manifest) => {
|
|
99
|
+
const subs = [...dirty].sort(docOrder);
|
|
100
|
+
// Remount triggers (a wrapper's own src/key binding) hydrate FLUSH-WIDE
|
|
101
|
+
// FIRST: they register the outgoing roots every later entry's freeze-skip
|
|
102
|
+
// and parking depend on. The walk got this ordering from hydrate's
|
|
103
|
+
// per-call two-pass; dispatch splits into multiple hydrate calls at
|
|
104
|
+
// structural boundaries, so the hoist must span the whole flush.
|
|
105
|
+
const triggers = [];
|
|
106
|
+
const rest = [];
|
|
107
|
+
for (const sub of subs) {
|
|
108
|
+
if (sub.anchor && !sub.anchor.isConnected) {
|
|
109
|
+
unsubscribe(sub);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Already-frozen subtree (a staging window spanning flushes): skip.
|
|
113
|
+
if (activeOutgoingRoots.size && outgoingRootOf(sub.anchor)) continue;
|
|
114
|
+
(sub.entry && isRemountTrigger(sub.entry) ? triggers : rest).push(sub);
|
|
115
|
+
}
|
|
116
|
+
if (triggers.length > 0) {
|
|
117
|
+
hydrate(
|
|
118
|
+
triggers.map((s) => ({ ...s.entry, scopedState: scopeFor(s, currentState) })),
|
|
119
|
+
currentState,
|
|
120
|
+
manifest,
|
|
121
|
+
previousState,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const seenGroups = new Set();
|
|
125
|
+
let batch = [];
|
|
126
|
+
const flushBatch = () => {
|
|
127
|
+
if (batch.length > 0) {
|
|
128
|
+
hydrate(batch, currentState, manifest, previousState);
|
|
129
|
+
batch = [];
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
for (const sub of rest) {
|
|
133
|
+
// Died mid-flush: an earlier structural op (iteration diff, branch flip)
|
|
134
|
+
// tore this subscriber's DOM down after beginFlush selected it. The lazy
|
|
135
|
+
// self-prune, at dispatch time.
|
|
136
|
+
if (sub.anchor && !sub.anchor.isConnected) {
|
|
137
|
+
unsubscribe(sub);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Frozen mid-flush: a trigger above marked this subscriber's page
|
|
141
|
+
// outgoing — its DOM is replaced wholesale at the commit, so rendering
|
|
142
|
+
// it against post-navigation state is the mid-navigation collapse the
|
|
143
|
+
// freeze exists to prevent. Subscriptions stay; the swap's removal
|
|
144
|
+
// prunes them lazily via isConnected. (Ancestor styling bindings park
|
|
145
|
+
// inside hydrate — same seam the walk uses.)
|
|
146
|
+
if (activeOutgoingRoots.size && outgoingRootOf(sub.anchor)) continue;
|
|
147
|
+
if (sub.kind === 'conditional') {
|
|
148
|
+
flushBatch();
|
|
149
|
+
dispatchConditional(sub.node, scopeFor(sub, currentState), manifest);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (sub.kind === 'iteration') {
|
|
153
|
+
flushBatch();
|
|
154
|
+
dispatchIteration(sub, currentState, previousState, manifest);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const e = sub.entry;
|
|
158
|
+
if (!e) continue;
|
|
159
|
+
const scope = scopeFor(sub, currentState);
|
|
160
|
+
if (e.type === 'attribute' || e.type === 'nameBinding') {
|
|
161
|
+
batch.push({ ...e, scopedState: scope });
|
|
162
|
+
} else {
|
|
163
|
+
const group = e.binding?._group;
|
|
164
|
+
if (group) {
|
|
165
|
+
if (seenGroups.has(group)) continue;
|
|
166
|
+
seenGroups.add(group);
|
|
167
|
+
}
|
|
168
|
+
batch.push(...entriesForText(sub, scope));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
flushBatch();
|
|
172
|
+
};
|