@torpor/view 0.4.18 → 1.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 +41 -16
- package/dist/Cleanup-D0wvGW5d.d.cts +9 -0
- package/dist/Cleanup-D0wvGW5d.d.cts.map +1 -0
- package/dist/Cleanup-D0wvGW5d.d.mts +9 -0
- package/dist/Cleanup-D0wvGW5d.d.mts.map +1 -0
- package/dist/Component-0MH0zd-m.d.cts +11 -0
- package/dist/Component-0MH0zd-m.d.cts.map +1 -0
- package/dist/Component-0MH0zd-m.d.mts +11 -0
- package/dist/Component-0MH0zd-m.d.mts.map +1 -0
- package/dist/Effect-BTKurOpr.d.mts +381 -0
- package/dist/Effect-BTKurOpr.d.mts.map +1 -0
- package/dist/Effect-COl3aRJV.d.cts +381 -0
- package/dist/Effect-COl3aRJV.d.cts.map +1 -0
- package/dist/compile.cjs +1727 -739
- package/dist/compile.d.cts +85 -50
- package/dist/compile.d.cts.map +1 -1
- package/dist/compile.d.mts +85 -50
- package/dist/compile.d.mts.map +1 -1
- package/dist/compile.mjs +1730 -741
- package/dist/compile.mjs.map +1 -1
- package/dist/dev.cjs +62 -63
- package/dist/dev.d.cts +4 -3
- package/dist/dev.d.cts.map +1 -1
- package/dist/dev.d.mts +4 -3
- package/dist/dev.d.mts.map +1 -1
- package/dist/dev.mjs +61 -62
- package/dist/dev.mjs.map +1 -1
- package/dist/{devContext-B3yskfhA.cjs → devContext-BulTn3U5.cjs} +6 -9
- package/dist/{devContext-CQ8RhBX-.mjs → devContext-ZNsPyOgB.mjs} +3 -4
- package/dist/devContext-ZNsPyOgB.mjs.map +1 -0
- package/dist/fromWebSocket-1QOjcrHE.cjs +182 -0
- package/dist/fromWebSocket-B52kN3-W.d.mts +91 -0
- package/dist/fromWebSocket-B52kN3-W.d.mts.map +1 -0
- package/dist/fromWebSocket-DeB35Q4w.d.cts +91 -0
- package/dist/fromWebSocket-DeB35Q4w.d.cts.map +1 -0
- package/dist/fromWebSocket-Dxc_uwm1.mjs +137 -0
- package/dist/fromWebSocket-Dxc_uwm1.mjs.map +1 -0
- package/dist/index.cjs +2073 -494
- package/dist/index.d.cts +600 -112
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +600 -112
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2054 -491
- package/dist/index.mjs.map +1 -1
- package/dist/ssr.cjs +78 -17
- package/dist/ssr.d.cts +64 -11
- package/dist/ssr.d.cts.map +1 -1
- package/dist/ssr.d.mts +64 -11
- package/dist/ssr.d.mts.map +1 -1
- package/dist/ssr.mjs +67 -14
- package/dist/ssr.mjs.map +1 -1
- package/package.json +21 -19
- package/dist/Cleanup-CYsytTEc.d.cts +0 -9
- package/dist/Cleanup-CYsytTEc.d.cts.map +0 -1
- package/dist/Cleanup-CgjyN0lW.d.mts +0 -9
- package/dist/Cleanup-CgjyN0lW.d.mts.map +0 -1
- package/dist/Component-DmWGMgak.d.mts +0 -11
- package/dist/Component-DmWGMgak.d.mts.map +0 -1
- package/dist/Component-kGLRFYg2.d.cts +0 -11
- package/dist/Component-kGLRFYg2.d.cts.map +0 -1
- package/dist/Region-Bb6HAb-F.d.cts +0 -201
- package/dist/Region-Bb6HAb-F.d.cts.map +0 -1
- package/dist/Region-CTchywkr.d.mts +0 -201
- package/dist/Region-CTchywkr.d.mts.map +0 -1
- package/dist/devContext-CQ8RhBX-.mjs.map +0 -1
- package/dist/formatText-BNP3P38F.d.cts +0 -17
- package/dist/formatText-BNP3P38F.d.cts.map +0 -1
- package/dist/formatText-D6Ov8Ub0.d.mts +0 -17
- package/dist/formatText-D6Ov8Ub0.d.mts.map +0 -1
- package/dist/formatText-DTIRmc3X.mjs +0 -61
- package/dist/formatText-DTIRmc3X.mjs.map +0 -1
- package/dist/formatText-Dhl2O0Ee.cjs +0 -96
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_devContext = require("./devContext-BulTn3U5.cjs");
|
|
3
|
+
const require_fromWebSocket = require("./fromWebSocket-1QOjcrHE.cjs");
|
|
4
4
|
//#region src/render/context.ts
|
|
5
5
|
const CONTEXT_SYMBOL = Symbol.for("torp.Context");
|
|
6
6
|
const context = globalThis[CONTEXT_SYMBOL] ??= {
|
|
@@ -10,55 +10,41 @@ const context = globalThis[CONTEXT_SYMBOL] ??= {
|
|
|
10
10
|
batch: 0,
|
|
11
11
|
batchOperation: 0,
|
|
12
12
|
registerComputed: null,
|
|
13
|
-
|
|
13
|
+
effectsToRun: [],
|
|
14
14
|
firstSignalToUpdate: null,
|
|
15
|
+
lastSignalToUpdate: null,
|
|
15
16
|
activeRegion: null,
|
|
16
17
|
previousRegion: null,
|
|
17
18
|
rootRegion: null,
|
|
19
|
+
awaitBoundary: null,
|
|
20
|
+
suspendPeek: false,
|
|
21
|
+
suspendPeekHit: false,
|
|
22
|
+
refreshSignals: null,
|
|
23
|
+
refreshInitialized: null,
|
|
18
24
|
mountEffects: [],
|
|
19
25
|
stashedEvents: [],
|
|
20
26
|
stashedAnimations: [],
|
|
21
27
|
hydrationNode: null
|
|
22
28
|
};
|
|
23
|
-
var context_default = context;
|
|
24
|
-
|
|
25
29
|
//#endregion
|
|
26
30
|
//#region src/render/addAnimation.ts
|
|
27
31
|
function addAnimation(el, entry, exit) {
|
|
28
|
-
|
|
29
|
-
region:
|
|
32
|
+
context.stashedAnimations.push({
|
|
33
|
+
region: context.activeRegion,
|
|
30
34
|
el,
|
|
31
35
|
in: entry,
|
|
32
36
|
out: exit
|
|
33
37
|
});
|
|
34
38
|
}
|
|
35
|
-
|
|
36
39
|
//#endregion
|
|
37
40
|
//#region src/render/addEvent.ts
|
|
38
41
|
function addEvent(el, type, listener) {
|
|
39
|
-
if (listener !== void 0 && listener !== null)
|
|
40
|
-
region: context_default.activeRegion,
|
|
42
|
+
if (listener !== void 0 && listener !== null) context.stashedEvents.push({
|
|
41
43
|
el,
|
|
42
44
|
type,
|
|
43
45
|
listener
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
|
-
//#region src/types/constants.ts
|
|
49
|
-
/**
|
|
50
|
-
* The type of a Signal object.
|
|
51
|
-
*/
|
|
52
|
-
const SIGNAL_TYPE = 0;
|
|
53
|
-
/**
|
|
54
|
-
* The type of a Computed object.
|
|
55
|
-
*/
|
|
56
|
-
const COMPUTED_TYPE = 1;
|
|
57
|
-
/**
|
|
58
|
-
* The type of an Effect object.
|
|
59
|
-
*/
|
|
60
|
-
const EFFECT_TYPE = 2;
|
|
61
|
-
|
|
62
48
|
//#endregion
|
|
63
49
|
//#region src/watch/clearSources.ts
|
|
64
50
|
/**
|
|
@@ -77,7 +63,6 @@ function clearSources(target) {
|
|
|
77
63
|
previousSub = clearSub;
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
|
-
|
|
81
66
|
//#endregion
|
|
82
67
|
//#region src/watch/deactivateSources.ts
|
|
83
68
|
/**
|
|
@@ -86,7 +71,6 @@ function clearSources(target) {
|
|
|
86
71
|
function deactivateSources(target) {
|
|
87
72
|
for (let sourceSub = target.firstSource; sourceSub !== null; sourceSub = sourceSub.nextSource) sourceSub.active = false;
|
|
88
73
|
}
|
|
89
|
-
|
|
90
74
|
//#endregion
|
|
91
75
|
//#region src/watch/trackSignal.ts
|
|
92
76
|
/**
|
|
@@ -94,60 +78,48 @@ function deactivateSources(target) {
|
|
|
94
78
|
* run when the signal changes.
|
|
95
79
|
*/
|
|
96
80
|
function trackSignal(signal) {
|
|
97
|
-
const target =
|
|
81
|
+
const target = context.activeTarget;
|
|
98
82
|
if (target !== null) {
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
lastSub = nextSub;
|
|
103
|
-
if (nextSub.target === target) {
|
|
104
|
-
sub = nextSub;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
83
|
+
for (let existingSub = target.firstSource; existingSub !== null; existingSub = existingSub.nextSource) if (existingSub.source === signal) {
|
|
84
|
+
existingSub.active = true;
|
|
85
|
+
return;
|
|
107
86
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else {
|
|
122
|
-
let lastEffectSub = target.firstSource;
|
|
123
|
-
for (let nextSub = lastEffectSub.nextSource; nextSub !== null; nextSub = nextSub.nextSource) lastEffectSub = nextSub;
|
|
124
|
-
lastEffectSub.nextSource = sub;
|
|
125
|
-
}
|
|
126
|
-
} else sub.active = true;
|
|
87
|
+
const oldFirstTarget = signal.firstTarget;
|
|
88
|
+
const sub = {
|
|
89
|
+
source: signal,
|
|
90
|
+
target,
|
|
91
|
+
previousTarget: null,
|
|
92
|
+
nextTarget: oldFirstTarget,
|
|
93
|
+
nextSource: target.firstSource,
|
|
94
|
+
active: true,
|
|
95
|
+
recalc: false
|
|
96
|
+
};
|
|
97
|
+
if (oldFirstTarget !== null) oldFirstTarget.previousTarget = sub;
|
|
98
|
+
signal.firstTarget = sub;
|
|
99
|
+
target.firstSource = sub;
|
|
127
100
|
}
|
|
128
101
|
}
|
|
129
|
-
|
|
130
102
|
//#endregion
|
|
131
103
|
//#region src/watch/runComputed.ts
|
|
132
104
|
function runComputed(computed) {
|
|
133
|
-
const oldActiveTarget =
|
|
105
|
+
const oldActiveTarget = context.activeTarget;
|
|
134
106
|
computed.running = true;
|
|
135
107
|
computed.didError = false;
|
|
108
|
+
computed.didSuspend = false;
|
|
136
109
|
try {
|
|
137
|
-
|
|
110
|
+
context.activeTarget = computed;
|
|
138
111
|
computed.value = computed.run();
|
|
139
112
|
} catch (err) {
|
|
140
113
|
computed.didError = true;
|
|
141
114
|
computed.value = err;
|
|
142
115
|
throw err;
|
|
143
116
|
} finally {
|
|
144
|
-
|
|
117
|
+
context.activeTarget = oldActiveTarget;
|
|
145
118
|
computed.running = false;
|
|
146
119
|
}
|
|
147
120
|
trackSignal(computed);
|
|
148
121
|
return computed.value;
|
|
149
122
|
}
|
|
150
|
-
|
|
151
123
|
//#endregion
|
|
152
124
|
//#region src/watch/checkComputed.ts
|
|
153
125
|
/**
|
|
@@ -171,7 +143,6 @@ function checkComputed(computed) {
|
|
|
171
143
|
}
|
|
172
144
|
return changed;
|
|
173
145
|
}
|
|
174
|
-
|
|
175
146
|
//#endregion
|
|
176
147
|
//#region src/watch/checkSources.ts
|
|
177
148
|
/**
|
|
@@ -180,21 +151,20 @@ function checkComputed(computed) {
|
|
|
180
151
|
*/
|
|
181
152
|
function checkSources(target) {
|
|
182
153
|
let haveComputed = false;
|
|
183
|
-
for (let sourceSub = target.firstSource; sourceSub !== null; sourceSub = sourceSub.nextSource) if (sourceSub.recalc) if (sourceSub.source.type ===
|
|
154
|
+
for (let sourceSub = target.firstSource; sourceSub !== null; sourceSub = sourceSub.nextSource) if (sourceSub.recalc) if (sourceSub.source.type === 0) return true;
|
|
184
155
|
else if (sourceSub.source.recalc) haveComputed = true;
|
|
185
156
|
else return true;
|
|
186
157
|
if (haveComputed) {
|
|
187
|
-
for (let sourceSub = target.firstSource; sourceSub !== null; sourceSub = sourceSub.nextSource) if (sourceSub.recalc && sourceSub.source.type ===
|
|
188
|
-
const oldActiveTarget =
|
|
189
|
-
|
|
158
|
+
for (let sourceSub = target.firstSource; sourceSub !== null; sourceSub = sourceSub.nextSource) if (sourceSub.recalc && sourceSub.source.type === 1 && sourceSub.source.recalc) {
|
|
159
|
+
const oldActiveTarget = context.activeTarget;
|
|
160
|
+
context.activeTarget = target;
|
|
190
161
|
const recalc = checkComputed(sourceSub.source);
|
|
191
|
-
|
|
162
|
+
context.activeTarget = oldActiveTarget;
|
|
192
163
|
if (recalc) return true;
|
|
193
164
|
}
|
|
194
165
|
}
|
|
195
166
|
return false;
|
|
196
167
|
}
|
|
197
|
-
|
|
198
168
|
//#endregion
|
|
199
169
|
//#region src/watch/runCleanups.ts
|
|
200
170
|
/**
|
|
@@ -203,9 +173,10 @@ function checkSources(target) {
|
|
|
203
173
|
function runCleanups(effect) {
|
|
204
174
|
let effectToClean = effect;
|
|
205
175
|
for (let i = 0; i < effect.extent; i++) {
|
|
176
|
+
if (effectToClean === null) break;
|
|
206
177
|
if (typeof effectToClean.cleanup === "function") {
|
|
207
|
-
const oldActiveTarget =
|
|
208
|
-
|
|
178
|
+
const oldActiveTarget = context.activeTarget;
|
|
179
|
+
context.activeTarget = null;
|
|
209
180
|
try {
|
|
210
181
|
effectToClean.cleanup();
|
|
211
182
|
} catch (err) {
|
|
@@ -214,18 +185,12 @@ function runCleanups(effect) {
|
|
|
214
185
|
throw err;
|
|
215
186
|
} finally {
|
|
216
187
|
effectToClean.cleanup = void 0;
|
|
217
|
-
|
|
188
|
+
context.activeTarget = oldActiveTarget;
|
|
218
189
|
}
|
|
219
190
|
}
|
|
220
|
-
|
|
221
|
-
if (i !== 0 && effect.nextEffectToRun === effectToClean) {
|
|
222
|
-
effect.nextEffectToRun = effectToClean.nextEffectToRun;
|
|
223
|
-
effectToClean.nextEffect = effectToClean.nextEffectToRun = null;
|
|
224
|
-
}
|
|
225
|
-
effectToClean = nextEffect;
|
|
191
|
+
effectToClean = effectToClean.nextEffect;
|
|
226
192
|
}
|
|
227
193
|
}
|
|
228
|
-
|
|
229
194
|
//#endregion
|
|
230
195
|
//#region src/watch/checkEffect.ts
|
|
231
196
|
/**
|
|
@@ -240,130 +205,245 @@ function checkEffect(effect) {
|
|
|
240
205
|
clearSources(effect);
|
|
241
206
|
}
|
|
242
207
|
}
|
|
243
|
-
|
|
244
208
|
//#endregion
|
|
245
|
-
//#region src/watch/
|
|
209
|
+
//#region src/watch/routeEffectError.ts
|
|
246
210
|
/**
|
|
247
|
-
*
|
|
211
|
+
* Routes an error thrown by an effect re-run to the nearest enclosing error
|
|
212
|
+
* boundary (`@try`/`@catch` or top-level `@error`), and forces the boundary's
|
|
213
|
+
* control effect to re-run so it renders its catch branch. Returns true if a
|
|
214
|
+
* boundary handled the error.
|
|
215
|
+
*
|
|
216
|
+
* The boundary is found by walking the flattened region chain from the
|
|
217
|
+
* effect's owning region (`Effect.region`, set by `$run`) through its
|
|
218
|
+
* ancestors (regions with strictly decreasing depth on the `previousRegion`
|
|
219
|
+
* chain — the same shape `clearRegion` walks).
|
|
220
|
+
*
|
|
221
|
+
* For recovery, the boundary holds the failing effect's source signals
|
|
222
|
+
* (captured by `runEffect` before it cleared them, `Effect.errorSources`)
|
|
223
|
+
* together with the boundary effect's own sources: reads wrapped in nested
|
|
224
|
+
* `$run` effects (text/attribute interpolation) are tracked by those
|
|
225
|
+
* effects, not the boundary, so without holding them the catch branch would
|
|
226
|
+
* never re-attempt the try branch. `runTry` re-subscribes the boundary
|
|
227
|
+
* effect to the held signals on every run that shows the catch branch.
|
|
228
|
+
*
|
|
229
|
+
* Called from `triggerEffects`' per-effect catch. If forcing the boundary
|
|
230
|
+
* re-run itself throws (e.g. the catch branch content errors), the error
|
|
231
|
+
* propagates to the caller, which reports it as unhandled — matching the
|
|
232
|
+
* compiled form, where a throwing catch branch escapes the boundary.
|
|
248
233
|
*/
|
|
249
|
-
function
|
|
250
|
-
let
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
234
|
+
function routeEffectError(effect, error) {
|
|
235
|
+
let region = effect.region ?? null;
|
|
236
|
+
while (region !== null) {
|
|
237
|
+
const boundary = region.errorBoundary;
|
|
238
|
+
if (boundary !== void 0 && boundary.effect !== null) {
|
|
239
|
+
const boundaryEffect = boundary.effect;
|
|
240
|
+
const held = [];
|
|
241
|
+
for (let sub = boundaryEffect.firstSource; sub !== null; sub = sub.nextSource) held.push(sub.source);
|
|
242
|
+
for (const signal of effect.errorSources ?? []) if (!held.includes(signal)) held.push(signal);
|
|
243
|
+
boundary.error = error;
|
|
244
|
+
boundary.hasError = true;
|
|
245
|
+
boundary.heldSignals = held;
|
|
246
|
+
runCleanups(boundaryEffect);
|
|
247
|
+
deactivateSources(boundaryEffect);
|
|
248
|
+
runEffect(boundaryEffect);
|
|
249
|
+
clearSources(boundaryEffect);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
const depth = region.depth;
|
|
253
|
+
let previous = region.previousRegion;
|
|
254
|
+
while (previous !== null && previous.depth >= depth) previous = previous.previousRegion;
|
|
255
|
+
region = previous;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
254
258
|
}
|
|
255
|
-
|
|
256
259
|
//#endregion
|
|
257
260
|
//#region src/watch/triggerEffects.ts
|
|
258
261
|
/**
|
|
262
|
+
* Re-subscribes a failed effect to the suspended computeds its run had read,
|
|
263
|
+
* after `runEffect`'s catch detached them via `clearSources`. Called when no
|
|
264
|
+
* error boundary handled the crash: the error still surfaces (see below),
|
|
265
|
+
* but the promise's resolve re-runs the effect, so a crash caused by a
|
|
266
|
+
* pending read's `undefined` heals instead of leaving a dead effect.
|
|
267
|
+
*/
|
|
268
|
+
function resubscribeSuspendSources(effect) {
|
|
269
|
+
const sources = effect.suspendSources;
|
|
270
|
+
if (!sources || sources.size === 0) return;
|
|
271
|
+
const oldActive = context.activeTarget;
|
|
272
|
+
context.activeTarget = effect;
|
|
273
|
+
for (const signal of sources) trackSignal(signal);
|
|
274
|
+
context.activeTarget = oldActive;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
259
277
|
* Runs the effects that have been collected during the batch.
|
|
260
278
|
*/
|
|
261
279
|
function triggerEffects() {
|
|
262
|
-
let
|
|
263
|
-
|
|
264
|
-
let
|
|
265
|
-
|
|
266
|
-
|
|
280
|
+
let unhandledError = null;
|
|
281
|
+
const queue = context.effectsToRun;
|
|
282
|
+
for (let i = 0; i < queue.length; i++) {
|
|
283
|
+
const effect = queue[i];
|
|
284
|
+
context.batchOperation++;
|
|
285
|
+
effect.queued = false;
|
|
267
286
|
try {
|
|
268
287
|
checkEffect(effect);
|
|
269
288
|
} catch (err) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
let handled = false;
|
|
290
|
+
try {
|
|
291
|
+
handled = routeEffectError(effect, err);
|
|
292
|
+
} catch (routingError) {
|
|
293
|
+
if (unhandledError === null) unhandledError = { error: routingError };
|
|
294
|
+
}
|
|
295
|
+
if (!handled) {
|
|
296
|
+
resubscribeSuspendSources(effect);
|
|
297
|
+
if (unhandledError === null) unhandledError = { error: err };
|
|
273
298
|
}
|
|
274
299
|
}
|
|
275
|
-
effect = effect.nextEffectToRun;
|
|
276
|
-
}
|
|
277
|
-
effect = context_default.firstEffectToRun;
|
|
278
|
-
context_default.firstEffectToRun = null;
|
|
279
|
-
while (effect !== null) {
|
|
280
|
-
let nextEffect = effect.nextEffectToRun;
|
|
281
|
-
effect.nextEffectToRun = null;
|
|
282
|
-
effect = nextEffect;
|
|
283
300
|
}
|
|
284
|
-
let
|
|
285
|
-
|
|
301
|
+
for (let effect of queue) effect.queued = false;
|
|
302
|
+
context.effectsToRun = [];
|
|
303
|
+
let signal = context.firstSignalToUpdate;
|
|
304
|
+
context.firstSignalToUpdate = null;
|
|
305
|
+
context.lastSignalToUpdate = null;
|
|
286
306
|
while (signal !== null) {
|
|
287
|
-
clearTargets(signal);
|
|
288
307
|
const nextSignal = signal.nextSignalToUpdate;
|
|
289
308
|
signal.nextSignalToUpdate = null;
|
|
290
309
|
signal = nextSignal;
|
|
291
310
|
}
|
|
292
|
-
if (
|
|
311
|
+
if (unhandledError !== null) throw unhandledError.error;
|
|
293
312
|
}
|
|
294
|
-
|
|
295
313
|
//#endregion
|
|
296
314
|
//#region src/watch/batchEnd.ts
|
|
297
315
|
function batchEnd() {
|
|
298
|
-
if (
|
|
299
|
-
|
|
316
|
+
if (context.batch > 1) {
|
|
317
|
+
context.batch--;
|
|
300
318
|
return;
|
|
301
319
|
}
|
|
302
320
|
try {
|
|
303
321
|
triggerEffects();
|
|
304
322
|
} finally {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
323
|
+
context.batch = 0;
|
|
324
|
+
context.batchOperation = 0;
|
|
325
|
+
context.previousEffect = null;
|
|
308
326
|
}
|
|
309
327
|
}
|
|
310
|
-
|
|
311
328
|
//#endregion
|
|
312
329
|
//#region src/watch/batchStart.ts
|
|
313
330
|
function batchStart() {
|
|
314
|
-
|
|
331
|
+
context.batch++;
|
|
315
332
|
}
|
|
316
|
-
|
|
317
333
|
//#endregion
|
|
318
334
|
//#region src/watch/runEffect.ts
|
|
319
335
|
function runEffect(effect) {
|
|
320
|
-
require_devContext.
|
|
321
|
-
const oldActiveTarget =
|
|
322
|
-
const oldExtent =
|
|
336
|
+
require_devContext.devContext.effectRun(effect);
|
|
337
|
+
const oldActiveTarget = context.activeTarget;
|
|
338
|
+
const oldExtent = context.extent;
|
|
323
339
|
effect.didError = false;
|
|
340
|
+
effect.errorSources = null;
|
|
341
|
+
effect.didSuspend = false;
|
|
342
|
+
effect.suspendSources = null;
|
|
324
343
|
batchStart();
|
|
325
344
|
try {
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
if (context.previousEffect !== null) context.previousEffect.nextEffect = effect;
|
|
346
|
+
context.activeTarget = effect;
|
|
347
|
+
context.previousEffect = effect;
|
|
348
|
+
context.extent = 1;
|
|
330
349
|
effect.cleanup = effect.run();
|
|
331
|
-
effect.extent =
|
|
350
|
+
effect.extent = context.extent;
|
|
332
351
|
} catch (err) {
|
|
333
352
|
effect.didError = true;
|
|
353
|
+
let sources = null;
|
|
354
|
+
for (let sub = effect.firstSource; sub !== null; sub = sub.nextSource) (sources ??= []).push(sub.source);
|
|
355
|
+
effect.errorSources = sources;
|
|
334
356
|
clearSources(effect);
|
|
335
357
|
throw err;
|
|
336
358
|
} finally {
|
|
337
|
-
|
|
338
|
-
|
|
359
|
+
context.activeTarget = oldActiveTarget;
|
|
360
|
+
context.extent += oldExtent - 1;
|
|
339
361
|
batchEnd();
|
|
340
362
|
}
|
|
341
363
|
}
|
|
342
|
-
|
|
343
364
|
//#endregion
|
|
344
365
|
//#region src/watch/$run.ts
|
|
345
366
|
/**
|
|
346
367
|
* Runs and re-runs a function that depends on a watched object
|
|
347
368
|
*
|
|
348
369
|
* @param fn The function to run, which may return a cleanup function.
|
|
370
|
+
* @param options Internal fields to set on the constructed Effect (e.g.
|
|
371
|
+
* `isMountEffect`, `forVarDeps`). Not part of the public API.
|
|
349
372
|
*/
|
|
350
|
-
function $run(fn, name) {
|
|
373
|
+
function $run(fn, name, options) {
|
|
351
374
|
let effect = {
|
|
352
|
-
type:
|
|
375
|
+
type: 2,
|
|
353
376
|
run: fn,
|
|
354
377
|
cleanup: void 0,
|
|
355
378
|
firstSource: null,
|
|
356
379
|
nextEffect: null,
|
|
357
380
|
extent: 0,
|
|
358
|
-
|
|
381
|
+
queued: false,
|
|
359
382
|
didError: false,
|
|
360
|
-
|
|
383
|
+
didSuspend: false,
|
|
384
|
+
region: null,
|
|
385
|
+
errorSources: null,
|
|
386
|
+
name,
|
|
387
|
+
isMountEffect: options?.isMountEffect,
|
|
388
|
+
forVarMask: options?.forVarMask
|
|
361
389
|
};
|
|
362
|
-
|
|
390
|
+
const region = context.activeRegion;
|
|
391
|
+
if (region !== null) {
|
|
392
|
+
region.effects.push(effect);
|
|
393
|
+
effect.region = region;
|
|
394
|
+
}
|
|
395
|
+
require_devContext.devContext.onRun(effect);
|
|
363
396
|
runEffect(effect);
|
|
364
397
|
return effect;
|
|
365
398
|
}
|
|
366
|
-
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/watch/$peek.ts
|
|
401
|
+
/**
|
|
402
|
+
* Runs a function without tracking the reactive values it reads: `$watch`'d
|
|
403
|
+
* state accessed inside `fn` does not become a dependency of the currently
|
|
404
|
+
* running effect or computed. Useful for reading state inside an effect
|
|
405
|
+
* without re-running it when that state changes.
|
|
406
|
+
*
|
|
407
|
+
* @param fn The function to run untracked.
|
|
408
|
+
*/
|
|
409
|
+
function $peek(fn) {
|
|
410
|
+
const oldActiveTarget = context.activeTarget;
|
|
411
|
+
context.activeTarget = null;
|
|
412
|
+
const result = fn();
|
|
413
|
+
context.activeTarget = oldActiveTarget;
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/watch/flushMountEffects.ts
|
|
418
|
+
/**
|
|
419
|
+
* Runs the collected `$onmount`/`onmount` callbacks — once, after the
|
|
420
|
+
* component's markup has been added to the DOM (or, when hydrating, after
|
|
421
|
+
* hydration is complete). Multiple callbacks run in the order they were
|
|
422
|
+
* added.
|
|
423
|
+
*
|
|
424
|
+
* Each callback becomes an effect with `isMountEffect` set, so the keyed-list
|
|
425
|
+
* reconciler's force-rerun path (`rerunRegionEffects`) skips it. Its body
|
|
426
|
+
* also runs untracked: mount callbacks are once-only, so reactive reads
|
|
427
|
+
* inside them do not subscribe. In dev builds a tracked pass warns when a
|
|
428
|
+
* callback reads reactive state, since that is almost always a mistake — the
|
|
429
|
+
* reactive part should be wrapped in `$run`.
|
|
430
|
+
*/
|
|
431
|
+
function flushMountEffects() {
|
|
432
|
+
if (context.mountEffects.length === 0) return;
|
|
433
|
+
for (let fn of context.mountEffects) $run(() => runMountCallback(fn), void 0, { isMountEffect: true });
|
|
434
|
+
context.mountEffects.length = 0;
|
|
435
|
+
}
|
|
436
|
+
function runMountCallback(fn) {
|
|
437
|
+
if (!require_devContext.devContext.enabled) return $peek(fn);
|
|
438
|
+
const result = fn();
|
|
439
|
+
const effect = context.activeTarget;
|
|
440
|
+
if (effect !== null && effect.type === 2 && effect.firstSource !== null) {
|
|
441
|
+
console.warn("[torpor] A $onmount/onmount callback read reactive state, but mount callbacks run only once and will not re-run. Wrap the reactive part in $run(), e.g. $onmount(() => { $run(() => ...); });");
|
|
442
|
+
deactivateSources(effect);
|
|
443
|
+
clearSources(effect);
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
367
447
|
//#endregion
|
|
368
448
|
//#region src/render/animate.ts
|
|
369
449
|
/**
|
|
@@ -384,76 +464,279 @@ async function animate(el, enter, keyframes, options) {
|
|
|
384
464
|
setTimeout(() => {
|
|
385
465
|
animation.timeline = el.ownerDocument.timeline;
|
|
386
466
|
}, 1);
|
|
387
|
-
const activeRegion =
|
|
467
|
+
const activeRegion = context.activeRegion;
|
|
388
468
|
activeRegion.animations ??= /* @__PURE__ */ new Set();
|
|
389
469
|
activeRegion.animations.add(animation);
|
|
390
470
|
await animation.finished;
|
|
391
471
|
}
|
|
392
|
-
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/render/delegatedEvents.ts
|
|
474
|
+
/**
|
|
475
|
+
* Event delegation.
|
|
476
|
+
*
|
|
477
|
+
* For event types in `DELEGATED_EVENT_TYPES` (all natively bubbling), we
|
|
478
|
+
* register a single listener on `document` per type and dispatch by walking
|
|
479
|
+
* up from `event.target` to find the closest ancestor with a stashed handler.
|
|
480
|
+
* This replaces per-element `addEventListener` for delegated types.
|
|
481
|
+
*
|
|
482
|
+
* Why: in the js-framework-bench row template, every `<tr>` clones two `<a>`
|
|
483
|
+
* elements each with a `click` handler. With per-element `addEventListener`,
|
|
484
|
+
* `runlots` (10k rows) issues 20000 `addEventListener` calls and the browser
|
|
485
|
+
* allocates 20000 `Listener` objects. With delegation it's 2 calls total
|
|
486
|
+
* (one per delegated type, registered lazily on first use) and zero
|
|
487
|
+
* per-element `Listener` allocations — the handler is stored as a property
|
|
488
|
+
* on the element.
|
|
489
|
+
*
|
|
490
|
+
* Model: last-write-wins per element per type (same as Solid's
|
|
491
|
+
* `delegateEvents`). Repeated `t_event(el, "click", h)` calls with the same
|
|
492
|
+
* element reassign the property; calling with `null`/`undefined` clears it.
|
|
493
|
+
* Torpor's compiler emits `t_event(el, type, h)` exactly once per element
|
|
494
|
+
* lifetime (outside any tracked effect), so this is the natural shape.
|
|
495
|
+
*
|
|
496
|
+
* Non-bubbling events (`focus`, `blur`, `scroll`, `load`, `error`, `abort`,
|
|
497
|
+
* `unload`, `resize`, etc.) bypass delegation and use direct
|
|
498
|
+
* `addEventListener` on the element, preserving native semantics.
|
|
499
|
+
*/
|
|
500
|
+
const DELEGATED_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
501
|
+
"auxclick",
|
|
502
|
+
"beforeinput",
|
|
503
|
+
"change",
|
|
504
|
+
"click",
|
|
505
|
+
"compositionend",
|
|
506
|
+
"compositionstart",
|
|
507
|
+
"compositionupdate",
|
|
508
|
+
"contextmenu",
|
|
509
|
+
"dblclick",
|
|
510
|
+
"drag",
|
|
511
|
+
"dragend",
|
|
512
|
+
"dragenter",
|
|
513
|
+
"dragleave",
|
|
514
|
+
"dragover",
|
|
515
|
+
"dragstart",
|
|
516
|
+
"drop",
|
|
517
|
+
"focusin",
|
|
518
|
+
"focusout",
|
|
519
|
+
"input",
|
|
520
|
+
"keydown",
|
|
521
|
+
"keypress",
|
|
522
|
+
"keyup",
|
|
523
|
+
"mousedown",
|
|
524
|
+
"mousemove",
|
|
525
|
+
"mouseout",
|
|
526
|
+
"mouseover",
|
|
527
|
+
"mouseup",
|
|
528
|
+
"pointerdown",
|
|
529
|
+
"pointermove",
|
|
530
|
+
"pointerout",
|
|
531
|
+
"pointerover",
|
|
532
|
+
"pointerup",
|
|
533
|
+
"reset",
|
|
534
|
+
"submit",
|
|
535
|
+
"touchend",
|
|
536
|
+
"touchmove",
|
|
537
|
+
"touchstart",
|
|
538
|
+
"wheel"
|
|
539
|
+
]);
|
|
540
|
+
function isDelegatedEventType(type) {
|
|
541
|
+
return DELEGATED_EVENT_TYPES.has(type);
|
|
542
|
+
}
|
|
543
|
+
const DELEGATE_PREFIX = "torp.$$";
|
|
544
|
+
const DELEGATE_REGISTERED = Symbol.for("torp.delegated.registered");
|
|
545
|
+
function delegatedHandler(event) {
|
|
546
|
+
let node = event.target;
|
|
547
|
+
const stopAt = event.currentTarget;
|
|
548
|
+
const key = DELEGATE_PREFIX + event.type;
|
|
549
|
+
while (node != null && node !== stopAt) {
|
|
550
|
+
const handler = node[key];
|
|
551
|
+
if (handler !== void 0) {
|
|
552
|
+
Object.defineProperty(event, "currentTarget", {
|
|
553
|
+
configurable: true,
|
|
554
|
+
value: node,
|
|
555
|
+
writable: false
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
handler.call(node, event);
|
|
559
|
+
} finally {
|
|
560
|
+
delete event.currentTarget;
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
node = node.parentNode;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function ensureDelegatedListener(type) {
|
|
568
|
+
if (typeof document === "undefined") return;
|
|
569
|
+
let registered = document[DELEGATE_REGISTERED];
|
|
570
|
+
if (!registered) {
|
|
571
|
+
registered = /* @__PURE__ */ new Set();
|
|
572
|
+
document[DELEGATE_REGISTERED] = registered;
|
|
573
|
+
}
|
|
574
|
+
if (registered.has(type)) return;
|
|
575
|
+
registered.add(type);
|
|
576
|
+
document.addEventListener(type, delegatedHandler);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Attach a delegated event handler to an element.
|
|
580
|
+
*
|
|
581
|
+
* Stores the handler as a property on the element and ensures a single
|
|
582
|
+
* document-level listener exists for the type. Idempotent for the
|
|
583
|
+
* document-level listener: the first call for a given type registers it,
|
|
584
|
+
* subsequent calls are a Set-has check.
|
|
585
|
+
*
|
|
586
|
+
* Passing `null` or `undefined` as the listener clears any previously-set
|
|
587
|
+
* handler for that type on the element (mirroring `addEvent`'s existing
|
|
588
|
+
* null-skipping semantics, extended to support explicit removal).
|
|
589
|
+
*/
|
|
590
|
+
function attachDelegatedEvent(el, type, listener) {
|
|
591
|
+
const key = DELEGATE_PREFIX + type;
|
|
592
|
+
if (listener === void 0 || listener === null) {
|
|
593
|
+
el[key] = void 0;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
ensureDelegatedListener(type);
|
|
597
|
+
el[key] = listener;
|
|
598
|
+
}
|
|
599
|
+
const DIRECT_LISTENERS = "torp.$directListeners";
|
|
600
|
+
/**
|
|
601
|
+
* Records a direct (non-delegated) listener on the element at attach time,
|
|
602
|
+
* so `copyEventListeners` can re-attach it after a dynamic-element tag
|
|
603
|
+
* swap. `addEventListener` listeners can't be discovered after the fact.
|
|
604
|
+
*/
|
|
605
|
+
function recordDirectListener(el, type, listener) {
|
|
606
|
+
let list = el[DIRECT_LISTENERS];
|
|
607
|
+
if (!list) {
|
|
608
|
+
list = [];
|
|
609
|
+
el[DIRECT_LISTENERS] = list;
|
|
610
|
+
}
|
|
611
|
+
list.push({
|
|
612
|
+
type,
|
|
613
|
+
listener
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Copies the event listeners attached to one element onto another — used by
|
|
618
|
+
* `t_dynamic` so a dynamic element keeps its listeners when its tag changes.
|
|
619
|
+
* Delegated handlers are `torp.$$<type>` properties, copied per known
|
|
620
|
+
* delegated type (swaps are rare, so the fixed set of checks is fine);
|
|
621
|
+
* direct listeners are re-registered from the list `recordDirectListener`
|
|
622
|
+
* built at attach time.
|
|
623
|
+
*/
|
|
624
|
+
function copyEventListeners(from, to) {
|
|
625
|
+
for (const type of DELEGATED_EVENT_TYPES) {
|
|
626
|
+
const key = DELEGATE_PREFIX + type;
|
|
627
|
+
const handler = from[key];
|
|
628
|
+
if (handler !== void 0) to[key] = handler;
|
|
629
|
+
}
|
|
630
|
+
const direct = from[DIRECT_LISTENERS];
|
|
631
|
+
if (direct) {
|
|
632
|
+
to[DIRECT_LISTENERS] = direct;
|
|
633
|
+
for (const { type, listener } of direct) to.addEventListener(type, listener);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
393
636
|
//#endregion
|
|
394
637
|
//#region src/render/isFragmentNode.ts
|
|
395
638
|
function isFragmentNode(node) {
|
|
396
639
|
return node.nodeType === 11;
|
|
397
640
|
}
|
|
398
|
-
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/render/runMountSideEffects.ts
|
|
643
|
+
/**
|
|
644
|
+
* Runs the mount-time side effects that both `addFragment` and `addElement`
|
|
645
|
+
* share: `$onmount` effects (only when not hydrating), stashed event
|
|
646
|
+
* listeners, and stashed animations. No-op when `parent` is itself a
|
|
647
|
+
* `DocumentFragment` (we're being inserted into a detached tree, so
|
|
648
|
+
* `$onmount` and event hookup would be premature — they'll run when the
|
|
649
|
+
* outer fragment attaches).
|
|
650
|
+
*
|
|
651
|
+
* Extracted so the `DocumentFragment` path (`addFragment`) and the
|
|
652
|
+
* single-element path (`addElement`) don't duplicate this logic.
|
|
653
|
+
*/
|
|
654
|
+
function runMountSideEffects(parent, activeRegion, hydrationNode) {
|
|
655
|
+
if (isFragmentNode(parent)) return;
|
|
656
|
+
if (hydrationNode === null) flushMountEffects();
|
|
657
|
+
if (context.stashedEvents.length > 0) {
|
|
658
|
+
for (let event of context.stashedEvents) if (isDelegatedEventType(event.type)) attachDelegatedEvent(event.el, event.type, event.listener);
|
|
659
|
+
else {
|
|
660
|
+
event.el.addEventListener(event.type, event.listener);
|
|
661
|
+
recordDirectListener(event.el, event.type, event.listener);
|
|
662
|
+
}
|
|
663
|
+
context.stashedEvents.length = 0;
|
|
664
|
+
}
|
|
665
|
+
for (let animation of context.stashedAnimations) {
|
|
666
|
+
context.activeRegion = animation.region;
|
|
667
|
+
$run(function runMountSideEffectsAnimation() {
|
|
668
|
+
if (animation.in !== void 0) animate(animation.el, true, animation.in.keyframes, animation.in.options);
|
|
669
|
+
if (animation.out !== void 0) return () => {
|
|
670
|
+
animate(animation.el, false, animation.out.keyframes, animation.out.options);
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
context.stashedAnimations.length = 0;
|
|
675
|
+
context.activeRegion = activeRegion;
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/render/addElement.ts
|
|
679
|
+
/**
|
|
680
|
+
* Companion to `addFragment` for the single-root-element codegen path. Wires
|
|
681
|
+
* a cloned root element (produced by `getElementFragment`) into the live DOM
|
|
682
|
+
* tree, sets the active region's start/end nodes, and runs the same
|
|
683
|
+
* mount-time side effects as `addFragment` (`$onmount` effects, stashed event
|
|
684
|
+
* listeners, stashed animations).
|
|
685
|
+
*
|
|
686
|
+
* Differences from `addFragment`:
|
|
687
|
+
* - `node` is both the start and end of the region (no
|
|
688
|
+
* `fragment.firstChild`/`fragment.lastChild` indirection).
|
|
689
|
+
* - When hydrating, the existing DOM node is reused (the cloned `node` is
|
|
690
|
+
* discarded); `endNode` is still `node` because `nodeRootElement` returns
|
|
691
|
+
* the hydration cursor's node in that case, not the cloned one.
|
|
692
|
+
*
|
|
693
|
+
* @param node The root element (cloned by `getElementFragment`, or the
|
|
694
|
+
* hydration cursor's node when hydrating).
|
|
695
|
+
* @param parent The intended parent element.
|
|
696
|
+
* @param before The sibling to insert before (or `null` to append).
|
|
697
|
+
*/
|
|
698
|
+
function addElement(node, parent, before) {
|
|
699
|
+
const activeRegion = context.activeRegion;
|
|
700
|
+
const hydrationNode = context.hydrationNode;
|
|
701
|
+
activeRegion.startNode = node;
|
|
702
|
+
activeRegion.endNode = node;
|
|
703
|
+
parent = before?.parentNode ?? parent;
|
|
704
|
+
if (hydrationNode === null) parent.insertBefore(node, before);
|
|
705
|
+
runMountSideEffects(parent, activeRegion, hydrationNode);
|
|
706
|
+
}
|
|
399
707
|
//#endregion
|
|
400
708
|
//#region src/render/addFragment.ts
|
|
401
|
-
function addFragment(fragment, parent, before, endNode) {
|
|
402
|
-
const activeRegion =
|
|
403
|
-
const hydrationNode =
|
|
404
|
-
if (hydrationNode !== null)
|
|
405
|
-
|
|
709
|
+
function addFragment(fragment, parent, before, endNode, startNode) {
|
|
710
|
+
const activeRegion = context.activeRegion;
|
|
711
|
+
const hydrationNode = context.hydrationNode;
|
|
712
|
+
if (hydrationNode !== null) {
|
|
713
|
+
if (startNode !== void 0 && startNode.parentNode !== null) activeRegion.startNode = startNode;
|
|
714
|
+
activeRegion.endNode = endNode ?? hydrationNode;
|
|
715
|
+
} else {
|
|
406
716
|
activeRegion.startNode = fragment.firstChild;
|
|
407
717
|
activeRegion.endNode = fragment.lastChild;
|
|
408
718
|
}
|
|
409
719
|
parent = before?.parentNode ?? parent;
|
|
410
720
|
if (hydrationNode === null) parent.insertBefore(fragment, before);
|
|
411
|
-
|
|
412
|
-
if (hydrationNode === null) {
|
|
413
|
-
for (let effect of context_default.mountEffects) $run(effect);
|
|
414
|
-
context_default.mountEffects.length = 0;
|
|
415
|
-
}
|
|
416
|
-
if (context_default.stashedEvents.length > 0) {
|
|
417
|
-
const events = [...context_default.stashedEvents];
|
|
418
|
-
$run(function addFragmentEvents() {
|
|
419
|
-
for (let event of events) event.el.addEventListener(event.type, event.listener);
|
|
420
|
-
return () => {
|
|
421
|
-
for (let event of events) event.el.removeEventListener(event.type, event.listener);
|
|
422
|
-
};
|
|
423
|
-
});
|
|
424
|
-
context_default.stashedEvents.length = 0;
|
|
425
|
-
}
|
|
426
|
-
for (let animation of context_default.stashedAnimations) {
|
|
427
|
-
context_default.activeRegion = animation.region;
|
|
428
|
-
$run(function addFragmentAnimation() {
|
|
429
|
-
if (animation.in !== void 0) animate(animation.el, true, animation.in.keyframes, animation.in.options);
|
|
430
|
-
if (animation.out !== void 0) return () => {
|
|
431
|
-
animate(animation.el, false, animation.out.keyframes, animation.out.options);
|
|
432
|
-
};
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
context_default.stashedAnimations.length = 0;
|
|
436
|
-
context_default.activeRegion = activeRegion;
|
|
437
|
-
}
|
|
721
|
+
runMountSideEffects(parent, activeRegion, hydrationNode);
|
|
438
722
|
}
|
|
439
|
-
|
|
440
723
|
//#endregion
|
|
441
724
|
//#region src/render/applyProps.ts
|
|
442
725
|
function applyProps(el, props, propNamesUsed) {
|
|
443
726
|
if (props !== void 0) {
|
|
444
727
|
for (let [name, value] of Object.entries(props)) if (!propNamesUsed.includes(name)) if (name.startsWith("on")) {
|
|
445
728
|
const eventName = name.substring(2);
|
|
446
|
-
|
|
447
|
-
|
|
729
|
+
if (isDelegatedEventType(eventName)) attachDelegatedEvent(el, eventName, value);
|
|
730
|
+
else el.addEventListener(eventName, value);
|
|
731
|
+
} else $run(function setAttribute() {
|
|
448
732
|
el.setAttribute(name, value);
|
|
449
733
|
});
|
|
450
734
|
}
|
|
451
735
|
}
|
|
452
|
-
|
|
453
736
|
//#endregion
|
|
454
737
|
//#region src/render/clearRegion.ts
|
|
455
738
|
function clearRegion(region) {
|
|
456
|
-
require_devContext.
|
|
739
|
+
require_devContext.devContext.onRegionCleared(region);
|
|
457
740
|
let animations = region.animations !== null ? Array.from(region.animations) : void 0;
|
|
458
741
|
let childRegion = region.nextRegion;
|
|
459
742
|
while (childRegion !== null && childRegion.depth > region.depth) {
|
|
@@ -461,9 +744,10 @@ function clearRegion(region) {
|
|
|
461
744
|
animations ??= [];
|
|
462
745
|
animations.push(...childRegion.animations);
|
|
463
746
|
}
|
|
747
|
+
const nextChild = childRegion.nextRegion;
|
|
464
748
|
releaseRegion(childRegion);
|
|
465
749
|
childRegion.depth = -2;
|
|
466
|
-
childRegion =
|
|
750
|
+
childRegion = nextChild;
|
|
467
751
|
}
|
|
468
752
|
region.nextRegion = childRegion;
|
|
469
753
|
if (region.previousRegion !== null) region.previousRegion.nextRegion = region.nextRegion;
|
|
@@ -478,31 +762,83 @@ function clearRegion(region) {
|
|
|
478
762
|
}
|
|
479
763
|
function clearNodes(region) {
|
|
480
764
|
if (region.startNode !== null && region.endNode !== null) {
|
|
765
|
+
const after = region.endNode.nextSibling;
|
|
766
|
+
let stop = region.startNode;
|
|
767
|
+
const startDetached = region.startNode.parentNode === null;
|
|
768
|
+
let fallback = false;
|
|
769
|
+
if (startDetached) {
|
|
770
|
+
let prev = region.previousRegion;
|
|
771
|
+
while (prev !== null && prev.depth > region.depth) prev = prev.previousRegion;
|
|
772
|
+
if (prev !== null && prev.endNode !== null && prev.endNode.parentNode !== null) {
|
|
773
|
+
stop = prev.endNode;
|
|
774
|
+
fallback = true;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
481
777
|
let currentNode = region.endNode;
|
|
482
|
-
while (currentNode !==
|
|
778
|
+
while (currentNode !== null && currentNode !== stop) {
|
|
483
779
|
let previousNode = currentNode.previousSibling;
|
|
484
780
|
currentNode.remove();
|
|
781
|
+
if (previousNode === null) break;
|
|
485
782
|
currentNode = previousNode;
|
|
486
783
|
}
|
|
487
|
-
currentNode.remove();
|
|
784
|
+
if (!fallback && currentNode === region.startNode) currentNode.remove();
|
|
785
|
+
const firstRemaining = fallback ? stop : after;
|
|
786
|
+
if (firstRemaining !== null) {
|
|
787
|
+
let ancestor = region.previousRegion;
|
|
788
|
+
let ancestorDepth = region.depth;
|
|
789
|
+
while (ancestor !== null && ancestor.depth < ancestorDepth) {
|
|
790
|
+
if (ancestor.startNode !== null && ancestor.startNode.parentNode === null) ancestor.startNode = firstRemaining;
|
|
791
|
+
ancestorDepth = ancestor.depth;
|
|
792
|
+
ancestor = ancestor.previousRegion;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
488
795
|
}
|
|
489
796
|
releaseRegion(region);
|
|
490
797
|
}
|
|
491
798
|
function releaseRegion(region) {
|
|
799
|
+
region.generation = -1;
|
|
492
800
|
region.startNode = null;
|
|
493
801
|
region.endNode = null;
|
|
494
802
|
region.previousRegion = null;
|
|
495
803
|
region.nextRegion = null;
|
|
496
804
|
region.animations = null;
|
|
805
|
+
region.errorBoundary = void 0;
|
|
806
|
+
for (const effect of region.effects) {
|
|
807
|
+
if (typeof effect.cleanup === "function") {
|
|
808
|
+
effect.cleanup();
|
|
809
|
+
effect.cleanup = void 0;
|
|
810
|
+
}
|
|
811
|
+
let sub = effect.firstSource;
|
|
812
|
+
effect.firstSource = null;
|
|
813
|
+
while (sub !== null) {
|
|
814
|
+
const nextSource = sub.nextSource;
|
|
815
|
+
const prev = sub.previousTarget;
|
|
816
|
+
const next = sub.nextTarget;
|
|
817
|
+
if (prev === null) {
|
|
818
|
+
if (sub.source.firstTarget === sub) sub.source.firstTarget = next;
|
|
819
|
+
} else prev.nextTarget = next;
|
|
820
|
+
if (next !== null) next.previousTarget = prev;
|
|
821
|
+
sub.previousTarget = null;
|
|
822
|
+
sub.nextTarget = null;
|
|
823
|
+
sub.nextSource = null;
|
|
824
|
+
sub = nextSource;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
region.effects.length = 0;
|
|
497
828
|
}
|
|
498
|
-
|
|
499
829
|
//#endregion
|
|
500
830
|
//#region src/render/clearLayoutSlot.ts
|
|
831
|
+
/**
|
|
832
|
+
* Clears a region that was created by `fillLayoutSlot`: tears down the
|
|
833
|
+
* rendered content and its effects, leaving the surrounding layout intact
|
|
834
|
+
* so the slot can be refilled.
|
|
835
|
+
*
|
|
836
|
+
* @param region The region to clear
|
|
837
|
+
*/
|
|
501
838
|
function clearLayoutSlot(region) {
|
|
502
|
-
|
|
839
|
+
context.previousRegion = region.previousRegion;
|
|
503
840
|
clearRegion(region);
|
|
504
841
|
}
|
|
505
|
-
|
|
506
842
|
//#endregion
|
|
507
843
|
//#region src/render/newRegion.ts
|
|
508
844
|
function newRegion(name) {
|
|
@@ -513,45 +849,58 @@ function newRegion(name) {
|
|
|
513
849
|
nextRegion: null,
|
|
514
850
|
depth: -1,
|
|
515
851
|
animations: null,
|
|
516
|
-
name
|
|
852
|
+
name,
|
|
853
|
+
effects: []
|
|
517
854
|
};
|
|
518
855
|
}
|
|
519
|
-
|
|
520
856
|
//#endregion
|
|
521
857
|
//#region src/render/popRegion.ts
|
|
522
858
|
function popRegion(oldRegion) {
|
|
523
|
-
|
|
524
|
-
require_devContext.
|
|
859
|
+
context.activeRegion = oldRegion;
|
|
860
|
+
require_devContext.devContext.onRegionPopped();
|
|
525
861
|
}
|
|
526
|
-
|
|
527
862
|
//#endregion
|
|
528
863
|
//#region src/render/pushRegion.ts
|
|
529
864
|
function pushRegion(region, toParent = false) {
|
|
530
|
-
const activeRegion =
|
|
865
|
+
const activeRegion = context.activeRegion;
|
|
531
866
|
if (toParent) {
|
|
532
|
-
const previousRegion =
|
|
867
|
+
const previousRegion = context.previousRegion;
|
|
533
868
|
region.previousRegion = previousRegion;
|
|
534
869
|
const nextRegion = previousRegion.nextRegion;
|
|
535
870
|
previousRegion.nextRegion = region;
|
|
536
871
|
region.nextRegion = nextRegion;
|
|
872
|
+
if (nextRegion !== null) nextRegion.previousRegion = region;
|
|
537
873
|
region.depth = activeRegion.depth + 1;
|
|
538
874
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
require_devContext.
|
|
875
|
+
context.activeRegion = region;
|
|
876
|
+
context.previousRegion = region;
|
|
877
|
+
require_devContext.devContext.onRegionPushed(region, toParent);
|
|
542
878
|
return activeRegion;
|
|
543
879
|
}
|
|
544
|
-
|
|
545
880
|
//#endregion
|
|
546
881
|
//#region src/render/fillLayoutSlot.ts
|
|
882
|
+
/**
|
|
883
|
+
* Renders a component into a persistent layout slot, and returns the region
|
|
884
|
+
* it was rendered into. Used by the layout engine during navigation: the
|
|
885
|
+
* component is created inside `parent` at `anchor`, with `slot` as its child
|
|
886
|
+
* slot render, and the returned region can later be passed to
|
|
887
|
+
* `clearLayoutSlot` to tear the page down — leaving the layout itself
|
|
888
|
+
* intact — before refilling the slot.
|
|
889
|
+
*
|
|
890
|
+
* @param component The component to render into the slot
|
|
891
|
+
* @param slot The slot render to pass to the component
|
|
892
|
+
* @param parent The parent node to render into
|
|
893
|
+
* @param anchor The node to insert content before, or null to append
|
|
894
|
+
* @param $props Optional props to pass to the component
|
|
895
|
+
* @param $context Optional context to pass to the component
|
|
896
|
+
*/
|
|
547
897
|
function fillLayoutSlot(component, slot, parent, anchor, $props, $context) {
|
|
548
|
-
const region = newRegion(require_devContext.
|
|
898
|
+
const region = newRegion(require_devContext.devContext.enabled ? "Layout slot" : void 0);
|
|
549
899
|
const oldRegion = pushRegion(region, true);
|
|
550
900
|
component(parent, anchor, $props, $context, { _: slot });
|
|
551
901
|
popRegion(oldRegion);
|
|
552
902
|
return region;
|
|
553
903
|
}
|
|
554
|
-
|
|
555
904
|
//#endregion
|
|
556
905
|
//#region src/render/getFragment.ts
|
|
557
906
|
function getFragment(document, array, index, html, ns) {
|
|
@@ -568,7 +917,44 @@ function getFragment(document, array, index, html, ns) {
|
|
|
568
917
|
}
|
|
569
918
|
return array[index].cloneNode(true);
|
|
570
919
|
}
|
|
571
|
-
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/render/getElementFragment.ts
|
|
922
|
+
/**
|
|
923
|
+
* Returns a clone of the cached template's single root element, bypassing the
|
|
924
|
+
* `DocumentFragment` wrapper that `getFragment` produces.
|
|
925
|
+
*
|
|
926
|
+
* Used by the compiler-emitted `createListItem` for `@for` bodies (and other
|
|
927
|
+
* fragments) whose template has exactly one rendering root child — e.g.
|
|
928
|
+
* `<tr><td>...</td>...</tr>`. Cloning `template.content.firstElementChild`
|
|
929
|
+
* directly saves the per-row `DocumentFragment` allocation that `getFragment`'s
|
|
930
|
+
* `cloneNode(true)` on the cached template's content would otherwise produce,
|
|
931
|
+
* which dominates the cost of bulk-row creates (`run`/`add`/`runlots`).
|
|
932
|
+
*
|
|
933
|
+
* Contract: the compiler only emits `t_fragment_el` for fragment indices whose
|
|
934
|
+
* template has exactly one rendering root, and that root is an `Element`. The
|
|
935
|
+
* cache array (`t_fragment_els`) is separate from `t_fragments` so each helper
|
|
936
|
+
* only ever sees its own cached type at a given index.
|
|
937
|
+
*
|
|
938
|
+
* @param document The owner document (used to build the cached `<template>`).
|
|
939
|
+
* @param array The per-component `t_fragment_els` cache.
|
|
940
|
+
* @param index The fragment's index in the cache.
|
|
941
|
+
* @param html The fragment's HTML source, with `#` placeholders for reactive
|
|
942
|
+
* text and `<!>` for anchor comments.
|
|
943
|
+
* @param ns When true, the template is created in the SVG namespace (its
|
|
944
|
+
* `firstElementChild` is then an SVG element).
|
|
945
|
+
*/
|
|
946
|
+
function getElementFragment(document, array, index, html, ns) {
|
|
947
|
+
if (array[index] === void 0) if (ns === true) {
|
|
948
|
+
const template = document.createElementNS("http://www.w3.org/2000/svg", "template");
|
|
949
|
+
template.innerHTML = html;
|
|
950
|
+
array[index] = template.firstElementChild;
|
|
951
|
+
} else {
|
|
952
|
+
const template = document.createElement("template");
|
|
953
|
+
template.innerHTML = html;
|
|
954
|
+
array[index] = template.content.firstElementChild;
|
|
955
|
+
}
|
|
956
|
+
return array[index].cloneNode(true);
|
|
957
|
+
}
|
|
572
958
|
//#endregion
|
|
573
959
|
//#region src/render/hydrate.ts
|
|
574
960
|
/**
|
|
@@ -578,16 +964,14 @@ function getFragment(document, array, index, html, ns) {
|
|
|
578
964
|
* @param props An object containing component props
|
|
579
965
|
*/
|
|
580
966
|
function hydrate(parent, component, props, slots) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
pushRegion(
|
|
967
|
+
context.hydrationNode = parent.firstChild;
|
|
968
|
+
context.rootRegion = newRegion(require_devContext.devContext.enabled ? "Root" : void 0);
|
|
969
|
+
context.previousRegion = context.rootRegion;
|
|
970
|
+
pushRegion(context.rootRegion);
|
|
585
971
|
component(parent, null, props, void 0, slots);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
context_default.mountEffects.length = 0;
|
|
972
|
+
context.hydrationNode = null;
|
|
973
|
+
flushMountEffects();
|
|
589
974
|
}
|
|
590
|
-
|
|
591
975
|
//#endregion
|
|
592
976
|
//#region src/render/mount.ts
|
|
593
977
|
/**
|
|
@@ -600,14 +984,36 @@ function hydrate(parent, component, props, slots) {
|
|
|
600
984
|
function mount(parent, component, props, slots) {
|
|
601
985
|
if (parent.childElementCount > 0) throw new Error("The parent node must have no child elements");
|
|
602
986
|
while (parent.firstChild !== null) parent.firstChild.remove();
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
pushRegion(
|
|
987
|
+
if (context.rootRegion === null) {
|
|
988
|
+
context.rootRegion = newRegion(require_devContext.devContext.enabled ? "Root" : void 0);
|
|
989
|
+
context.previousRegion = context.rootRegion;
|
|
990
|
+
pushRegion(context.rootRegion);
|
|
607
991
|
}
|
|
608
992
|
component(parent, null, props, void 0, slots);
|
|
609
993
|
}
|
|
610
|
-
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/render/unmount.ts
|
|
996
|
+
/**
|
|
997
|
+
* Tears down the current UI: disposes the root region tree (running effect
|
|
998
|
+
* cleanups and detaching subscriptions), resets the render context, and
|
|
999
|
+
* removes any remaining child nodes from `parent`.
|
|
1000
|
+
*
|
|
1001
|
+
* Call this before `mount`ing a fresh component tree into the same container
|
|
1002
|
+
* (e.g. when client-side navigating to a route whose layout chain differs
|
|
1003
|
+
* from the previous one). `mount` refuses to mount into a non-empty parent
|
|
1004
|
+
* and reuses an existing root region, so both must be cleared first.
|
|
1005
|
+
*
|
|
1006
|
+
* @param parent The container that the previous UI was mounted into
|
|
1007
|
+
*/
|
|
1008
|
+
function unmount(parent) {
|
|
1009
|
+
if (context.rootRegion !== null) {
|
|
1010
|
+
clearRegion(context.rootRegion);
|
|
1011
|
+
context.rootRegion = null;
|
|
1012
|
+
context.previousRegion = null;
|
|
1013
|
+
context.activeRegion = null;
|
|
1014
|
+
}
|
|
1015
|
+
while (parent.firstChild !== null) parent.firstChild.remove();
|
|
1016
|
+
}
|
|
611
1017
|
//#endregion
|
|
612
1018
|
//#region src/render/newListItem.ts
|
|
613
1019
|
function newListItem(data, key, name) {
|
|
@@ -620,28 +1026,20 @@ function newListItem(data, key, name) {
|
|
|
620
1026
|
animations: null,
|
|
621
1027
|
name,
|
|
622
1028
|
data,
|
|
623
|
-
key
|
|
1029
|
+
key,
|
|
1030
|
+
effects: []
|
|
624
1031
|
};
|
|
625
1032
|
}
|
|
626
|
-
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region src/render/hydrationMarkers.ts
|
|
629
|
-
const HYDRATION_START = "[";
|
|
630
|
-
const HYDRATION_END = "]";
|
|
631
|
-
const HYDRATION_BREAK = "^";
|
|
632
|
-
|
|
633
1033
|
//#endregion
|
|
634
1034
|
//#region src/render/isCommentNode.ts
|
|
635
1035
|
function isCommentNode(node) {
|
|
636
1036
|
return node.nodeType === 8;
|
|
637
1037
|
}
|
|
638
|
-
|
|
639
1038
|
//#endregion
|
|
640
1039
|
//#region src/render/isTextNode.ts
|
|
641
1040
|
function isTextNode(node) {
|
|
642
1041
|
return node.nodeType === 3;
|
|
643
1042
|
}
|
|
644
|
-
|
|
645
1043
|
//#endregion
|
|
646
1044
|
//#region src/render/nodeCheckHydrationBreak.ts
|
|
647
1045
|
/**
|
|
@@ -653,17 +1051,14 @@ function isTextNode(node) {
|
|
|
653
1051
|
* @returns The next node.
|
|
654
1052
|
*/
|
|
655
1053
|
function nodeCheckHydrationBreak(node) {
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
comment.remove();
|
|
661
|
-
}
|
|
662
|
-
context_default.hydrationNode = node;
|
|
1054
|
+
if (node !== null && isCommentNode(node) && node.data === "^") {
|
|
1055
|
+
let comment = node;
|
|
1056
|
+
node = node.nextSibling;
|
|
1057
|
+
comment.remove();
|
|
663
1058
|
}
|
|
1059
|
+
context.hydrationNode = node;
|
|
664
1060
|
return node;
|
|
665
1061
|
}
|
|
666
|
-
|
|
667
1062
|
//#endregion
|
|
668
1063
|
//#region src/render/nodeNext.ts
|
|
669
1064
|
/**
|
|
@@ -675,10 +1070,18 @@ function nodeCheckHydrationBreak(node) {
|
|
|
675
1070
|
* @param text Whether we require a text node.
|
|
676
1071
|
*/
|
|
677
1072
|
function nodeNext(node, text = false) {
|
|
678
|
-
if (
|
|
679
|
-
|
|
1073
|
+
if (context.hydrationNode !== null) {
|
|
1074
|
+
if (text && isTextNode(node)) return node;
|
|
1075
|
+
let next = nodeCheckHydrationBreak(node.nextSibling);
|
|
1076
|
+
while (!text && next !== null && isTextNode(next) && next.textContent?.trim() === "") next = nodeCheckHydrationBreak(next.nextSibling);
|
|
1077
|
+
if (!text && next !== null && next.nodeName === "TBODY" && next.firstChild && isTextNode(node)) {
|
|
1078
|
+
next = next.firstChild;
|
|
1079
|
+
context.hydrationNode = next;
|
|
1080
|
+
}
|
|
1081
|
+
return next;
|
|
1082
|
+
}
|
|
1083
|
+
return node.nextSibling;
|
|
680
1084
|
}
|
|
681
|
-
|
|
682
1085
|
//#endregion
|
|
683
1086
|
//#region src/render/nodeAnchor.ts
|
|
684
1087
|
/**
|
|
@@ -696,32 +1099,42 @@ function nodeNext(node, text = false) {
|
|
|
696
1099
|
* @returns The anchor node.
|
|
697
1100
|
*/
|
|
698
1101
|
function nodeAnchor(node) {
|
|
699
|
-
if (
|
|
700
|
-
if (isCommentNode(node) && node.data ===
|
|
1102
|
+
if (context.hydrationNode !== null) {
|
|
1103
|
+
if (isCommentNode(node) && node.data === "[") {
|
|
1104
|
+
const startParent = node.parentNode;
|
|
701
1105
|
let currentNode = nodeNext(node);
|
|
1106
|
+
const firstInside = currentNode;
|
|
702
1107
|
node.remove();
|
|
703
1108
|
let level = 1;
|
|
704
1109
|
while (currentNode !== null) {
|
|
705
1110
|
if (isCommentNode(currentNode)) {
|
|
706
|
-
if (currentNode.data ===
|
|
707
|
-
else if (currentNode.data ===
|
|
1111
|
+
if (currentNode.data === "[") level += 1;
|
|
1112
|
+
else if (currentNode.data === "]") {
|
|
708
1113
|
level -= 1;
|
|
709
1114
|
if (level === 0) {
|
|
710
1115
|
const endNode = currentNode.nextSibling;
|
|
711
1116
|
currentNode.remove();
|
|
712
|
-
if (
|
|
1117
|
+
if (context.hydrationNode === currentNode) context.hydrationNode = endNode;
|
|
1118
|
+
const region = context.activeRegion;
|
|
1119
|
+
if (region.startNode === null && firstInside !== null) region.startNode = firstInside;
|
|
713
1120
|
return endNode;
|
|
714
1121
|
}
|
|
715
1122
|
}
|
|
716
1123
|
}
|
|
717
|
-
currentNode = currentNode.
|
|
1124
|
+
if (currentNode.firstChild) currentNode = currentNode.firstChild;
|
|
1125
|
+
else if (currentNode.nextSibling) currentNode = currentNode.nextSibling;
|
|
1126
|
+
else {
|
|
1127
|
+
let parent = currentNode.parentNode;
|
|
1128
|
+
while (parent !== null && parent.nextSibling === null && parent !== startParent) parent = parent.parentNode;
|
|
1129
|
+
if (parent === null || parent === startParent) currentNode = null;
|
|
1130
|
+
else currentNode = parent.nextSibling;
|
|
1131
|
+
}
|
|
718
1132
|
}
|
|
719
1133
|
if (currentNode === null) throw new Error("End hydration comment not found");
|
|
720
1134
|
}
|
|
721
1135
|
}
|
|
722
1136
|
return node;
|
|
723
1137
|
}
|
|
724
|
-
|
|
725
1138
|
//#endregion
|
|
726
1139
|
//#region src/render/nodeChild.ts
|
|
727
1140
|
/**
|
|
@@ -733,40 +1146,104 @@ function nodeAnchor(node) {
|
|
|
733
1146
|
*/
|
|
734
1147
|
function nodeChild(parent) {
|
|
735
1148
|
let childNode = parent.firstChild;
|
|
736
|
-
if (
|
|
1149
|
+
if (context.hydrationNode !== null) {
|
|
737
1150
|
if (childNode === null) childNode = parent.appendChild(parent.ownerDocument.createTextNode(""));
|
|
738
|
-
|
|
1151
|
+
context.hydrationNode = childNode;
|
|
739
1152
|
}
|
|
740
1153
|
return childNode;
|
|
741
1154
|
}
|
|
742
|
-
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/render/walkHydrationRoot.ts
|
|
1157
|
+
/**
|
|
1158
|
+
* Walks the hydration cursor past skippable leading nodes (whitespace text,
|
|
1159
|
+
* branch-break markers, auto-inserted `<tbody>`) and lands on the first real
|
|
1160
|
+
* content node — the same walk `nodeRoot` performs for the non-text case.
|
|
1161
|
+
*
|
|
1162
|
+
* Sets `region.startNode` to the landed node (unless it's a control-start
|
|
1163
|
+
* marker, which `nodeAnchor` consumes next). Returns the landed node and
|
|
1164
|
+
* advances `context.hydrationNode` to it.
|
|
1165
|
+
*
|
|
1166
|
+
* Extracted so `nodeRoot` and `nodeRootElement` share one implementation of
|
|
1167
|
+
* the cursor walk.
|
|
1168
|
+
*/
|
|
1169
|
+
function walkHydrationRoot() {
|
|
1170
|
+
let rootNode = context.hydrationNode;
|
|
1171
|
+
while (rootNode !== null && isSkippable(rootNode)) {
|
|
1172
|
+
const next = rootNode.nextSibling;
|
|
1173
|
+
if (isCommentNode(rootNode) && rootNode.data === "^") rootNode.remove();
|
|
1174
|
+
rootNode = next;
|
|
1175
|
+
context.hydrationNode = rootNode;
|
|
1176
|
+
}
|
|
1177
|
+
while (rootNode !== null && rootNode.nodeName === "TBODY" && rootNode.firstChild !== null) {
|
|
1178
|
+
rootNode = rootNode.firstChild;
|
|
1179
|
+
context.hydrationNode = rootNode;
|
|
1180
|
+
}
|
|
1181
|
+
const region = context.activeRegion;
|
|
1182
|
+
const isControlStart = rootNode !== null && isCommentNode(rootNode) && rootNode.data === "[";
|
|
1183
|
+
if (region.startNode === null && !isControlStart) region.startNode = rootNode;
|
|
1184
|
+
return rootNode;
|
|
1185
|
+
}
|
|
1186
|
+
function isSkippable(node) {
|
|
1187
|
+
if (isTextNode(node) && (node.textContent ?? "").trim() === "") return true;
|
|
1188
|
+
if (isCommentNode(node)) return node.data === "^" || node.data === "";
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/render/nodeRootElement.ts
|
|
1193
|
+
/**
|
|
1194
|
+
* Gets the root element of a single-root fragment built via `getElementFragment`.
|
|
1195
|
+
*
|
|
1196
|
+
* Companion to `nodeRoot`, for the codegen path where the compiler has emitted
|
|
1197
|
+
* `t_fragment_el` (cloning the cached template's `firstElementChild` directly)
|
|
1198
|
+
* instead of `t_fragment` (cloning into a `DocumentFragment`). In the
|
|
1199
|
+
* non-hydrating case the cloned element is already the root, so this is a
|
|
1200
|
+
* no-op pass-through. In the hydrating case we perform the same cursor walk as
|
|
1201
|
+
* `nodeRoot`'s non-text branch so the region's `startNode` lands on the
|
|
1202
|
+
* existing DOM node and the hydration cursor advances past any leading
|
|
1203
|
+
* whitespace, branch-break markers, and auto-inserted `<tbody>` wrappers.
|
|
1204
|
+
*
|
|
1205
|
+
* @param node The cloned root element (ignored when hydrating).
|
|
1206
|
+
*/
|
|
1207
|
+
function nodeRootElement(node) {
|
|
1208
|
+
if (context.hydrationNode !== null) return walkHydrationRoot();
|
|
1209
|
+
return node;
|
|
1210
|
+
}
|
|
743
1211
|
//#endregion
|
|
744
1212
|
//#region src/render/nodeRoot.ts
|
|
745
1213
|
/**
|
|
746
1214
|
* Gets the first child in a fragment.
|
|
747
1215
|
*
|
|
748
|
-
* When hydrating, also sets the active
|
|
1216
|
+
* When hydrating, also sets the active region's start node, while we have it.
|
|
1217
|
+
*
|
|
1218
|
+
* If the hydration cursor is on a control-start marker (`<![>`), the node is
|
|
1219
|
+
* left in place for `nodeAnchor` (called next as `t_anchor(t_root(...))`) to
|
|
1220
|
+
* walk — it consumes the matched `<![>...<!]>` pair, removes the markers and
|
|
1221
|
+
* sets the region's start node to the first node inside the block.
|
|
1222
|
+
*
|
|
1223
|
+
* For non-text fragments, leading branch-break markers (`<!^>`), empty anchor
|
|
1224
|
+
* comments left by preceding siblings, and residual whitespace-only text nodes
|
|
1225
|
+
* are skipped (break markers removed) so the region's start node lands on real
|
|
1226
|
+
* content. This replaces the per-sibling `t_next` calls that the codegen used
|
|
1227
|
+
* to emit when leading whitespace separated siblings; with whitespace trimmed
|
|
1228
|
+
* those calls are gone, so the advancement happens here.
|
|
749
1229
|
*
|
|
750
1230
|
* @param parent The parent of the fragment.
|
|
751
1231
|
* @param text Whether we require a text node.
|
|
752
1232
|
*/
|
|
753
1233
|
function nodeRoot(parent, text = false) {
|
|
754
|
-
if (
|
|
755
|
-
let rootNode =
|
|
1234
|
+
if (context.hydrationNode !== null) {
|
|
1235
|
+
let rootNode = context.hydrationNode;
|
|
756
1236
|
if (text && !isTextNode(rootNode) && rootNode.previousSibling !== null && isTextNode(rootNode.previousSibling)) {
|
|
757
1237
|
rootNode = rootNode.previousSibling;
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
if (isCommentNode(rootNode) && rootNode.data === HYDRATION_START) {
|
|
761
|
-
rootNode = rootNode.nextSibling;
|
|
762
|
-
context_default.hydrationNode = rootNode;
|
|
1238
|
+
context.hydrationNode = rootNode;
|
|
763
1239
|
}
|
|
764
|
-
|
|
765
|
-
|
|
1240
|
+
if (!text) return walkHydrationRoot();
|
|
1241
|
+
const region = context.activeRegion;
|
|
1242
|
+
const isControlStart = rootNode !== null && isCommentNode(rootNode) && rootNode.data === "[";
|
|
1243
|
+
if (region.startNode === null && !isControlStart) region.startNode = rootNode;
|
|
766
1244
|
return rootNode;
|
|
767
1245
|
} else return parent.firstChild;
|
|
768
1246
|
}
|
|
769
|
-
|
|
770
1247
|
//#endregion
|
|
771
1248
|
//#region src/render/nodeSkip.ts
|
|
772
1249
|
/**
|
|
@@ -779,10 +1256,108 @@ function nodeRoot(parent, text = false) {
|
|
|
779
1256
|
*/
|
|
780
1257
|
function nodeSkip(node, count) {
|
|
781
1258
|
let nextNode = node;
|
|
782
|
-
for (let i = 0; i < count; i++) nextNode = nodeCheckHydrationBreak(nextNode.nextSibling);
|
|
1259
|
+
if (context.hydrationNode !== null) for (let i = 0; i < count; i++) nextNode = nodeCheckHydrationBreak(nextNode.nextSibling);
|
|
1260
|
+
else for (let i = 0; i < count; i++) nextNode = nextNode.nextSibling;
|
|
783
1261
|
return nextNode;
|
|
784
1262
|
}
|
|
785
|
-
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/render/rerunRegionEffects.ts
|
|
1265
|
+
/**
|
|
1266
|
+
* Force re-runs every effect owned by `region` and its descendant regions
|
|
1267
|
+
* that depends on at least one of the for-vars set in `changedMask`.
|
|
1268
|
+
*
|
|
1269
|
+
* Used by keyed-list `updateListItem` callbacks emitted by the compiler when
|
|
1270
|
+
* the `@for` body has been classified "no-proxy safe" — i.e. the per-item
|
|
1271
|
+
* data bag is *not* wrapped in a shallow `$watch` Proxy. Without that Proxy,
|
|
1272
|
+
* a write such as `t_old_item.data.row = newRow` doesn't propagate through a
|
|
1273
|
+
* signal, so any effects that read `data.row` must be re-run manually.
|
|
1274
|
+
*
|
|
1275
|
+
* `changedMask` is a bitmask of the for-var positions whose references
|
|
1276
|
+
* actually changed in this update. Each effect carries `forVarMask`
|
|
1277
|
+
* (computed at compile time) listing the for-vars its body reads; effects
|
|
1278
|
+
* whose `forVarMask & changedMask === 0` are skipped. Mount callbacks
|
|
1279
|
+
* (`isMountEffect` — `$onmount`/`onmount`) are always skipped: they run
|
|
1280
|
+
* once per DOM mount and their bodies are untracked.
|
|
1281
|
+
*
|
|
1282
|
+
* Equivalent to what `checkEffect` does when a signal has propagated, but
|
|
1283
|
+
* unconditional for the effects that do match: cleanup → deactivate sources
|
|
1284
|
+
* → run → clear unused sources, for every matching effect on the region and
|
|
1285
|
+
* on every descendant region (`depth > region.depth`, the same chain shape
|
|
1286
|
+
* `clearRegion` walks).
|
|
1287
|
+
*/
|
|
1288
|
+
function rerunRegionEffects(region, changedMask) {
|
|
1289
|
+
rerunEffectsOnRegion(region, changedMask);
|
|
1290
|
+
let child = region.nextRegion;
|
|
1291
|
+
while (child !== null && child.depth > region.depth) {
|
|
1292
|
+
rerunEffectsOnRegion(child, changedMask);
|
|
1293
|
+
child = child.nextRegion;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function rerunEffectsOnRegion(region, changedMask) {
|
|
1297
|
+
for (let effect of region.effects) {
|
|
1298
|
+
if (effect.isMountEffect) continue;
|
|
1299
|
+
if (effect.forVarMask !== void 0 && (effect.forVarMask & changedMask) === 0) continue;
|
|
1300
|
+
runCleanups(effect);
|
|
1301
|
+
deactivateSources(effect);
|
|
1302
|
+
runEffect(effect);
|
|
1303
|
+
clearSources(effect);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/render/restoreHydration.ts
|
|
1308
|
+
/**
|
|
1309
|
+
* Restores the hydration cursor to a previously-snapshotted node. Used by
|
|
1310
|
+
* `@try`/`@catch`: if the try branch throws partway through its hydration
|
|
1311
|
+
* walk, the catch branch must resume hydrating from where the try group
|
|
1312
|
+
* started, not from wherever the failed branch left the cursor.
|
|
1313
|
+
*
|
|
1314
|
+
* The cursor is set unconditionally: a failed try-branch build can walk the
|
|
1315
|
+
* cursor past the last node (setting it to `null` via `nodeNext`), and the
|
|
1316
|
+
* rewind must still restore it — otherwise the catch branch would think it
|
|
1317
|
+
* isn't hydrating and insert fresh nodes alongside the server's. When not
|
|
1318
|
+
* hydrating at all, the snapshot itself is `null`, so this is a no-op.
|
|
1319
|
+
*
|
|
1320
|
+
* @param node The snapshotted cursor node (from `saveHydration`).
|
|
1321
|
+
*/
|
|
1322
|
+
function restoreHydration(node) {
|
|
1323
|
+
context.hydrationNode = node;
|
|
1324
|
+
}
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/render/widenAncestorsAtAnchor.ts
|
|
1327
|
+
/**
|
|
1328
|
+
* Widens ancestor region node windows after a control re-run rendered
|
|
1329
|
+
* content at its anchor.
|
|
1330
|
+
*
|
|
1331
|
+
* A region that owns only a control's anchor comment captures
|
|
1332
|
+
* `startNode = endNode = anchor` when it mounts (e.g. a `@for` row whose
|
|
1333
|
+
* body is a single `@if`, or an outer `@if` whose body is a single control).
|
|
1334
|
+
* Content rendered by a LATER re-run of that control is inserted into the
|
|
1335
|
+
* live DOM just BEFORE the anchor — outside that window — so clearing the
|
|
1336
|
+
* owning region would remove only the anchor and orphan the branch content.
|
|
1337
|
+
* At mount time this can't happen (content is added to the region's detached
|
|
1338
|
+
* fragment first, so its `startNode` already covers it), which is why only
|
|
1339
|
+
* re-runs need this fix.
|
|
1340
|
+
*
|
|
1341
|
+
* The first node of the newly rendered content is the first DESCENDANT region
|
|
1342
|
+
* of the control that owns nodes (control regions themselves have null
|
|
1343
|
+
* `startNode`). Ancestors are the regions with strictly decreasing depth on
|
|
1344
|
+
* the `previousRegion` chain; only those whose window starts exactly at the
|
|
1345
|
+
* anchor are widened.
|
|
1346
|
+
*/
|
|
1347
|
+
function widenAncestorsAtAnchor(region, anchor) {
|
|
1348
|
+
if (anchor === null) return;
|
|
1349
|
+
let child = region.nextRegion;
|
|
1350
|
+
while (child !== null && child.depth > region.depth && child.startNode === null) child = child.nextRegion;
|
|
1351
|
+
if (child === null || child.depth <= region.depth || child.startNode === anchor) return;
|
|
1352
|
+
const firstNode = child.startNode;
|
|
1353
|
+
let ancestor = region.previousRegion;
|
|
1354
|
+
let ancestorDepth = region.depth;
|
|
1355
|
+
while (ancestor !== null && ancestor.depth < ancestorDepth) {
|
|
1356
|
+
if (ancestor.startNode === anchor) ancestor.startNode = firstNode;
|
|
1357
|
+
ancestorDepth = ancestor.depth;
|
|
1358
|
+
ancestor = ancestor.previousRegion;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
786
1361
|
//#endregion
|
|
787
1362
|
//#region src/render/runControl.ts
|
|
788
1363
|
/**
|
|
@@ -791,27 +1366,106 @@ function nodeSkip(node, count) {
|
|
|
791
1366
|
*/
|
|
792
1367
|
function runControl(region, anchor, create, name) {
|
|
793
1368
|
let first = true;
|
|
794
|
-
|
|
1369
|
+
const gen = region.generation = (region.generation ?? 0) + 1;
|
|
1370
|
+
$run(function runControl() {
|
|
1371
|
+
if (region.generation !== gen) return;
|
|
1372
|
+
if (region.depth === -2) region.recreate = true;
|
|
795
1373
|
const oldRegion = pushRegion(region, first);
|
|
796
1374
|
first = false;
|
|
797
1375
|
create(anchor);
|
|
798
1376
|
popRegion(oldRegion);
|
|
1377
|
+
widenAncestorsAtAnchor(region, anchor);
|
|
1378
|
+
if (context.hydrationNode !== null && anchor !== null) context.hydrationNode = anchor;
|
|
799
1379
|
}, name);
|
|
800
|
-
if (context_default.hydrationNode) anchor = context_default.hydrationNode.nextSibling;
|
|
801
1380
|
}
|
|
802
|
-
|
|
803
1381
|
//#endregion
|
|
804
1382
|
//#region src/render/runControlBranch.ts
|
|
805
1383
|
function runControlBranch(region, oldIndex, index) {
|
|
806
|
-
|
|
807
|
-
if (region.
|
|
808
|
-
|
|
1384
|
+
const recreate = region.recreate;
|
|
1385
|
+
if (recreate) region.recreate = false;
|
|
1386
|
+
const firstRender = region.hasRenderedBranch !== true;
|
|
1387
|
+
region.hasRenderedBranch = true;
|
|
1388
|
+
if (oldIndex === index) {
|
|
1389
|
+
if (recreate) {
|
|
1390
|
+
if (region.nextRegion !== null && region.nextRegion.depth > region.depth) clearRegion(region.nextRegion);
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
if (!firstRender && region.nextRegion !== null && region.nextRegion.depth > region.depth) clearRegion(region.nextRegion);
|
|
809
1396
|
return true;
|
|
810
1397
|
}
|
|
811
|
-
|
|
1398
|
+
//#endregion
|
|
1399
|
+
//#region src/watch/deepWrap.ts
|
|
1400
|
+
/**
|
|
1401
|
+
* Wraps a value with `$watch` if it is a plain object (or Date/Map/Set) that
|
|
1402
|
+
* is not already watched and is not a promise. Returns the value unchanged
|
|
1403
|
+
* otherwise.
|
|
1404
|
+
*
|
|
1405
|
+
* Used by the `get` trap (wrap values on read) and the `set` trap (wrap
|
|
1406
|
+
* values on write), so a value stored in watched state is reactive whether
|
|
1407
|
+
* it's been read yet or not.
|
|
1408
|
+
*/
|
|
1409
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
1410
|
+
function deepWrap(value) {
|
|
1411
|
+
if (value === void 0 || value === null || typeof value !== "object") return value;
|
|
1412
|
+
if (value[require_fromWebSocket.proxyDataSymbol] !== void 0) return proxyCache.get(value) ?? value;
|
|
1413
|
+
if (value.then !== void 0) return value;
|
|
1414
|
+
if (value.nodeType !== void 0) return value;
|
|
1415
|
+
const cached = proxyCache.get(value);
|
|
1416
|
+
if (cached !== void 0) return cached;
|
|
1417
|
+
const proxy = $watch(value);
|
|
1418
|
+
proxyCache.set(value, proxy);
|
|
1419
|
+
return proxy;
|
|
1420
|
+
}
|
|
812
1421
|
//#endregion
|
|
813
1422
|
//#region src/watch/propagateSignal.ts
|
|
814
1423
|
/**
|
|
1424
|
+
* Walk the target graph from a signal source (ProxySignal or Computed),
|
|
1425
|
+
* marking each dependent as needing recalculation and queuing effects for
|
|
1426
|
+
* re-run. Shared between `propagateSignal` (for proxy writes) and `$async`'s
|
|
1427
|
+
* promise-resolve handler (for async value resolution).
|
|
1428
|
+
*
|
|
1429
|
+
* Must be called inside `batchStart`/`batchEnd` — callers own the batch.
|
|
1430
|
+
*/
|
|
1431
|
+
function propagateFromSignal(signal) {
|
|
1432
|
+
const first = signal.firstTarget;
|
|
1433
|
+
if (first === null) return;
|
|
1434
|
+
let targetSub = first;
|
|
1435
|
+
let computed;
|
|
1436
|
+
outer: while (true) {
|
|
1437
|
+
targetSub.active = false;
|
|
1438
|
+
targetSub.recalc = true;
|
|
1439
|
+
const target = targetSub.target;
|
|
1440
|
+
if (target.type === 2) {
|
|
1441
|
+
if (!target.queued) {
|
|
1442
|
+
target.queued = true;
|
|
1443
|
+
context.effectsToRun.push(target);
|
|
1444
|
+
}
|
|
1445
|
+
} else if (!target.recalc) {
|
|
1446
|
+
target.recalc = true;
|
|
1447
|
+
if (target.firstTarget !== null) {
|
|
1448
|
+
computed = target;
|
|
1449
|
+
computed.rollback = targetSub;
|
|
1450
|
+
targetSub = target.firstTarget;
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
while (targetSub.nextTarget === null && computed !== void 0) {
|
|
1455
|
+
const rollback = computed.rollback;
|
|
1456
|
+
if (rollback === null) break;
|
|
1457
|
+
targetSub = rollback;
|
|
1458
|
+
if (targetSub.source.type !== 1) {
|
|
1459
|
+
computed = void 0;
|
|
1460
|
+
continue outer;
|
|
1461
|
+
}
|
|
1462
|
+
computed = targetSub.source;
|
|
1463
|
+
}
|
|
1464
|
+
if (targetSub.nextTarget === null) break;
|
|
1465
|
+
targetSub = targetSub.nextTarget;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
815
1469
|
* When a signal is changed, we need to
|
|
816
1470
|
* - Set the signal's subscriptions to recalc
|
|
817
1471
|
* - Set any dependent computed values to recalc
|
|
@@ -822,61 +1476,26 @@ function propagateSignal(proxy, key) {
|
|
|
822
1476
|
if (signal !== void 0 && signal.firstTarget !== null) {
|
|
823
1477
|
batchStart();
|
|
824
1478
|
try {
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
let lastEffect = context_default.firstEffectToRun;
|
|
832
|
-
if (lastEffect !== null) while (lastEffect.nextEffectToRun !== null) lastEffect = lastEffect.nextEffectToRun;
|
|
833
|
-
let targetSub = signal.firstTarget;
|
|
834
|
-
let computed;
|
|
835
|
-
outer: while (true) {
|
|
836
|
-
targetSub.active = false;
|
|
837
|
-
targetSub.recalc = true;
|
|
838
|
-
const target = targetSub.target;
|
|
839
|
-
if (target.type === EFFECT_TYPE) {
|
|
840
|
-
if (lastEffect === null) {
|
|
841
|
-
context_default.firstEffectToRun = target;
|
|
842
|
-
lastEffect = target;
|
|
843
|
-
} else if (target.nextEffectToRun === null && lastEffect !== target) {
|
|
844
|
-
lastEffect.nextEffectToRun = target;
|
|
845
|
-
lastEffect = target;
|
|
846
|
-
}
|
|
847
|
-
} else if (!target.recalc) {
|
|
848
|
-
target.recalc = true;
|
|
849
|
-
if (target.firstTarget !== null) {
|
|
850
|
-
computed = target;
|
|
851
|
-
computed.rollback = targetSub;
|
|
852
|
-
targetSub = target.firstTarget;
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
while (targetSub.nextTarget === null && computed !== void 0) {
|
|
857
|
-
targetSub = computed.rollback;
|
|
858
|
-
if (targetSub.source.type !== COMPUTED_TYPE) {
|
|
859
|
-
computed = void 0;
|
|
860
|
-
continue outer;
|
|
861
|
-
}
|
|
862
|
-
computed = targetSub.source;
|
|
863
|
-
}
|
|
864
|
-
if (targetSub.nextTarget === null) break;
|
|
865
|
-
targetSub = targetSub.nextTarget;
|
|
1479
|
+
if (context.lastSignalToUpdate === null) {
|
|
1480
|
+
context.firstSignalToUpdate = signal;
|
|
1481
|
+
context.lastSignalToUpdate = signal;
|
|
1482
|
+
} else if (signal.nextSignalToUpdate === null) {
|
|
1483
|
+
context.lastSignalToUpdate.nextSignalToUpdate = signal;
|
|
1484
|
+
context.lastSignalToUpdate = signal;
|
|
866
1485
|
}
|
|
1486
|
+
propagateFromSignal(signal);
|
|
867
1487
|
} finally {
|
|
868
1488
|
batchEnd();
|
|
869
1489
|
}
|
|
870
1490
|
}
|
|
871
1491
|
}
|
|
872
|
-
|
|
873
1492
|
//#endregion
|
|
874
1493
|
//#region src/watch/trackProxySignal.ts
|
|
875
1494
|
function trackProxySignal(data, key) {
|
|
876
1495
|
let signal = data.signals.get(key);
|
|
877
1496
|
if (signal === void 0) {
|
|
878
1497
|
signal = {
|
|
879
|
-
type:
|
|
1498
|
+
type: 0,
|
|
880
1499
|
firstTarget: null,
|
|
881
1500
|
nextSignalToUpdate: null
|
|
882
1501
|
};
|
|
@@ -884,12 +1503,53 @@ function trackProxySignal(data, key) {
|
|
|
884
1503
|
}
|
|
885
1504
|
trackSignal(signal);
|
|
886
1505
|
}
|
|
887
|
-
|
|
888
1506
|
//#endregion
|
|
889
1507
|
//#region src/watch/proxyGet.ts
|
|
1508
|
+
/**
|
|
1509
|
+
* Called when a read hits a `didSuspend` computed. Subscribes the reader (so
|
|
1510
|
+
* resolve re-runs it through the normal reactive graph), propagates the
|
|
1511
|
+
* suspend up the cache chain by tainting the active reader, and notifies the
|
|
1512
|
+
* nearest `@await` boundary.
|
|
1513
|
+
*
|
|
1514
|
+
* Return value (stale-while-revalidate, ASYNC.md §6.2): on a refresh suspend
|
|
1515
|
+
* (the computed has resolved before) returns the previously resolved value
|
|
1516
|
+
* held in `staleValue`, so readers keep displaying the old content instead of
|
|
1517
|
+
* a placeholder — an `@await` boundary thus keeps its content mounted
|
|
1518
|
+
* rather than flashing the `with` branch. On a first load (`staleValue` is
|
|
1519
|
+
* undefined) returns `undefined`, which the boundary replaces with its
|
|
1520
|
+
* `with` branch. In peek mode (used by `$pending`) always returns `undefined`
|
|
1521
|
+
* — the value isn't needed, only the subscription.
|
|
1522
|
+
*/
|
|
1523
|
+
function suspendRead(signal) {
|
|
1524
|
+
trackSignal(signal);
|
|
1525
|
+
if (context.suspendPeek) {
|
|
1526
|
+
if (!signal.suspendQuiet) context.suspendPeekHit = true;
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (context.activeTarget !== null) {
|
|
1530
|
+
context.activeTarget.didSuspend = true;
|
|
1531
|
+
if (context.activeTarget.type === 2) {
|
|
1532
|
+
const effect = context.activeTarget;
|
|
1533
|
+
(effect.suspendSources ??= /* @__PURE__ */ new Set()).add(signal);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (context.awaitBoundary !== null) {
|
|
1537
|
+
context.awaitBoundary.suspended = true;
|
|
1538
|
+
context.awaitBoundary.pending.add(signal);
|
|
1539
|
+
const boundaryEffect = context.awaitBoundary.effect;
|
|
1540
|
+
if (boundaryEffect !== null && boundaryEffect !== context.activeTarget) {
|
|
1541
|
+
const oldActive = context.activeTarget;
|
|
1542
|
+
context.activeTarget = boundaryEffect;
|
|
1543
|
+
trackSignal(signal);
|
|
1544
|
+
context.activeTarget = oldActive;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return signal.staleValue;
|
|
1548
|
+
}
|
|
890
1549
|
function proxyGet(target, key, receiver) {
|
|
891
|
-
let data = target[
|
|
892
|
-
if (key ===
|
|
1550
|
+
let data = target[require_fromWebSocket.proxyDataSymbol];
|
|
1551
|
+
if (key === require_fromWebSocket.proxyDataSymbol) return data;
|
|
1552
|
+
if ((data.isDate || data.isMap || data.isSet) && !Object.prototype.hasOwnProperty.call(target, key)) return collectionGet(data, target, key, receiver);
|
|
893
1553
|
let signal = data.signals.get(key);
|
|
894
1554
|
if (signal === void 0) {
|
|
895
1555
|
const propDescriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
@@ -897,38 +1557,92 @@ function proxyGet(target, key, receiver) {
|
|
|
897
1557
|
if (propDescriptor.writable) {
|
|
898
1558
|
if (data.shallow !== true) {
|
|
899
1559
|
const value = target[key];
|
|
900
|
-
|
|
1560
|
+
const wrapped = deepWrap(value);
|
|
1561
|
+
if (wrapped !== value) target[key] = wrapped;
|
|
901
1562
|
}
|
|
902
1563
|
trackProxySignal(data, key);
|
|
903
1564
|
} else if (propDescriptor.get) {
|
|
904
|
-
const oldRegisterComputed =
|
|
1565
|
+
const oldRegisterComputed = context.registerComputed;
|
|
905
1566
|
try {
|
|
906
|
-
|
|
1567
|
+
context.registerComputed = (computed) => {
|
|
907
1568
|
data.signals.set(key, computed);
|
|
908
1569
|
};
|
|
909
|
-
|
|
1570
|
+
const result = Reflect.get(target, key, receiver);
|
|
1571
|
+
const registered = data.signals.get(key);
|
|
1572
|
+
if (context.refreshSignals !== null && registered !== void 0 && registered.type === 1 && registered.isAsync) {
|
|
1573
|
+
context.refreshSignals.push(registered);
|
|
1574
|
+
context.refreshInitialized?.add(registered);
|
|
1575
|
+
}
|
|
1576
|
+
if (registered !== void 0 && registered.type === 1 && registered.didSuspend) return suspendRead(registered);
|
|
1577
|
+
return result;
|
|
910
1578
|
} finally {
|
|
911
|
-
|
|
1579
|
+
context.registerComputed = oldRegisterComputed;
|
|
912
1580
|
}
|
|
913
1581
|
}
|
|
914
1582
|
} else if (data.isArray) {
|
|
915
1583
|
if (arrayWrapper[key] !== void 0) return arrayWrapper[key](data, target, key);
|
|
916
1584
|
} else trackProxySignal(data, key);
|
|
917
|
-
} else if (signal.type ===
|
|
918
|
-
|
|
1585
|
+
} else if (signal.type === 0) {
|
|
1586
|
+
if (data.shallow !== true) {
|
|
1587
|
+
const value = target[key];
|
|
1588
|
+
if (typeof value === "object" && value !== null) {
|
|
1589
|
+
const propDescriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
1590
|
+
if (propDescriptor !== void 0 && propDescriptor.writable) {
|
|
1591
|
+
const wrapped = deepWrap(value);
|
|
1592
|
+
if (wrapped !== value) target[key] = wrapped;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
trackProxySignal(data, key);
|
|
1597
|
+
} else if (signal.type === 1) {
|
|
1598
|
+
if (context.refreshSignals !== null && signal.isAsync) {
|
|
1599
|
+
context.refreshSignals.push(signal);
|
|
1600
|
+
return signal.value;
|
|
1601
|
+
}
|
|
919
1602
|
if (signal.running) throw new Error("Cycle detected");
|
|
920
1603
|
else if (signal.recalc) checkComputed(signal);
|
|
921
|
-
|
|
922
|
-
|
|
1604
|
+
if (signal.didError) {
|
|
1605
|
+
trackSignal(signal);
|
|
1606
|
+
throw signal.value;
|
|
1607
|
+
}
|
|
1608
|
+
if (signal.didSuspend) return suspendRead(signal);
|
|
1609
|
+
trackSignal(signal);
|
|
923
1610
|
return signal.value;
|
|
924
1611
|
}
|
|
925
|
-
return
|
|
1612
|
+
return target[key];
|
|
926
1613
|
}
|
|
927
1614
|
const arrayWrapper = {
|
|
928
|
-
[Symbol.iterator]: function(data, target,
|
|
1615
|
+
[Symbol.iterator]: function(data, target, _key) {
|
|
929
1616
|
trackProxySignal(data, "length");
|
|
930
|
-
|
|
931
|
-
return
|
|
1617
|
+
context.activeTarget = null;
|
|
1618
|
+
return function arrayIterator() {
|
|
1619
|
+
let index = 0;
|
|
1620
|
+
return {
|
|
1621
|
+
next() {
|
|
1622
|
+
if (index >= target.length) return {
|
|
1623
|
+
value: void 0,
|
|
1624
|
+
done: true
|
|
1625
|
+
};
|
|
1626
|
+
let value = target[index];
|
|
1627
|
+
if (data.shallow !== true) {
|
|
1628
|
+
const wrapped = deepWrap(value);
|
|
1629
|
+
if (wrapped !== value) {
|
|
1630
|
+
value = wrapped;
|
|
1631
|
+
target[index] = value;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
trackProxySignal(data, String(index));
|
|
1635
|
+
index++;
|
|
1636
|
+
return {
|
|
1637
|
+
value,
|
|
1638
|
+
done: false
|
|
1639
|
+
};
|
|
1640
|
+
},
|
|
1641
|
+
[Symbol.iterator]() {
|
|
1642
|
+
return this;
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
};
|
|
932
1646
|
},
|
|
933
1647
|
pop: arrayHandle,
|
|
934
1648
|
push: arrayHandle,
|
|
@@ -936,8 +1650,35 @@ const arrayWrapper = {
|
|
|
936
1650
|
shift: arrayHandle,
|
|
937
1651
|
sort: arrayHandle,
|
|
938
1652
|
splice: arrayHandle,
|
|
939
|
-
unshift: arrayHandle
|
|
1653
|
+
unshift: arrayHandle,
|
|
1654
|
+
slice: readHandle,
|
|
1655
|
+
concat: readHandle,
|
|
1656
|
+
filter: readHandle,
|
|
1657
|
+
map: readHandle,
|
|
1658
|
+
toReversed: readHandle,
|
|
1659
|
+
toSorted: readHandle,
|
|
1660
|
+
toSpliced: readHandle,
|
|
1661
|
+
indexOf: readHandle,
|
|
1662
|
+
lastIndexOf: readHandle,
|
|
1663
|
+
includes: readHandle,
|
|
1664
|
+
find: readHandle,
|
|
1665
|
+
findIndex: readHandle,
|
|
1666
|
+
some: readHandle,
|
|
1667
|
+
every: readHandle,
|
|
1668
|
+
join: readHandle,
|
|
1669
|
+
forEach: readHandle,
|
|
1670
|
+
reduce: readHandle,
|
|
1671
|
+
reduceRight: readHandle,
|
|
1672
|
+
flat: readHandle,
|
|
1673
|
+
flatMap: readHandle,
|
|
1674
|
+
at: readHandle
|
|
940
1675
|
};
|
|
1676
|
+
function readHandle(data, target, key) {
|
|
1677
|
+
return function(...args) {
|
|
1678
|
+
trackProxySignal(data, "length");
|
|
1679
|
+
return target[key].apply(target, args);
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
941
1682
|
function arrayHandle(data, target, key) {
|
|
942
1683
|
return function(...args) {
|
|
943
1684
|
const result = target[key].apply(target, args);
|
|
@@ -945,48 +1686,331 @@ function arrayHandle(data, target, key) {
|
|
|
945
1686
|
return result;
|
|
946
1687
|
};
|
|
947
1688
|
}
|
|
948
|
-
|
|
1689
|
+
const COLLECTION_KEY = Symbol("torp.collection");
|
|
1690
|
+
const TIME_KEY = "#time";
|
|
1691
|
+
/**
|
|
1692
|
+
* Routes a `get` on a watched Date/Map/Set to the right wrapper handler.
|
|
1693
|
+
*
|
|
1694
|
+
* Note the two levels of indirection for methods: the handler runs at get-trap
|
|
1695
|
+
* time (returning the method to call), and the returned function runs when the
|
|
1696
|
+
* method is invoked. `size` is the exception -- it returns its value at
|
|
1697
|
+
* get-trap time, because it's a readonly property, not a method.
|
|
1698
|
+
*/
|
|
1699
|
+
function collectionGet(data, target, key, receiver) {
|
|
1700
|
+
if (data.isDate) return dateGet(data, target, key);
|
|
1701
|
+
const handle = (data.isMap ? mapWrapper : setWrapper)[key];
|
|
1702
|
+
if (handle !== void 0) return handle(data, target, key, receiver);
|
|
1703
|
+
return target[key];
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Returns the signal key for a Map/Set operation: primitives get a per-key
|
|
1707
|
+
* signal, so reading one key doesn't re-run effects when an unrelated key
|
|
1708
|
+
* changes. Anything else shares the collection-wide signal.
|
|
1709
|
+
*/
|
|
1710
|
+
function collectionKey(key) {
|
|
1711
|
+
const type = typeof key;
|
|
1712
|
+
return type === "string" || type === "number" || type === "symbol" ? key : COLLECTION_KEY;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Propagates a Map/Set mutation: the per-key signal (when the key is a
|
|
1716
|
+
* primitive) plus the collection-wide signal. The collection signal is always
|
|
1717
|
+
* enough for object keys, and propagating the same signal twice would re-run
|
|
1718
|
+
* effects twice (they re-subscribe between flushes).
|
|
1719
|
+
*/
|
|
1720
|
+
function propagateCollection(data, key) {
|
|
1721
|
+
if (key !== COLLECTION_KEY) propagateSignal(data, key);
|
|
1722
|
+
propagateSignal(data, COLLECTION_KEY);
|
|
1723
|
+
}
|
|
1724
|
+
function dateGet(data, target, key) {
|
|
1725
|
+
if (typeof key === "symbol") {
|
|
1726
|
+
if (key === Symbol.toPrimitive) return function(hint) {
|
|
1727
|
+
const result = target[Symbol.toPrimitive](hint);
|
|
1728
|
+
trackProxySignal(data, TIME_KEY);
|
|
1729
|
+
return result;
|
|
1730
|
+
};
|
|
1731
|
+
return target[key];
|
|
1732
|
+
}
|
|
1733
|
+
if (typeof key !== "string") return target[key];
|
|
1734
|
+
const value = target[key];
|
|
1735
|
+
if (typeof value !== "function") return value;
|
|
1736
|
+
if (key.startsWith("set")) return function(...args) {
|
|
1737
|
+
const before = target.getTime();
|
|
1738
|
+
const result = value.apply(target, args);
|
|
1739
|
+
if (target.getTime() !== before) propagateSignal(data, TIME_KEY);
|
|
1740
|
+
return result;
|
|
1741
|
+
};
|
|
1742
|
+
if (key.startsWith("get") || key.startsWith("to") || key === "valueOf") return function(...args) {
|
|
1743
|
+
const result = value.apply(target, args);
|
|
1744
|
+
trackProxySignal(data, TIME_KEY);
|
|
1745
|
+
return result;
|
|
1746
|
+
};
|
|
1747
|
+
return value;
|
|
1748
|
+
}
|
|
1749
|
+
const mapWrapper = {
|
|
1750
|
+
size: function(data, target) {
|
|
1751
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1752
|
+
return target.size;
|
|
1753
|
+
},
|
|
1754
|
+
get: function(data, target) {
|
|
1755
|
+
return function(key) {
|
|
1756
|
+
key = require_fromWebSocket.$unwrap(key);
|
|
1757
|
+
trackProxySignal(data, collectionKey(key));
|
|
1758
|
+
const value = target.get(key);
|
|
1759
|
+
if (data.shallow !== true) {
|
|
1760
|
+
const wrapped = deepWrap(value);
|
|
1761
|
+
if (wrapped !== value) target.set(key, wrapped);
|
|
1762
|
+
return wrapped;
|
|
1763
|
+
}
|
|
1764
|
+
return value;
|
|
1765
|
+
};
|
|
1766
|
+
},
|
|
1767
|
+
has: function(data, target) {
|
|
1768
|
+
return function(key) {
|
|
1769
|
+
key = require_fromWebSocket.$unwrap(key);
|
|
1770
|
+
trackProxySignal(data, collectionKey(key));
|
|
1771
|
+
return target.has(key);
|
|
1772
|
+
};
|
|
1773
|
+
},
|
|
1774
|
+
set: function(data, target, _key, receiver) {
|
|
1775
|
+
return function(key, value) {
|
|
1776
|
+
key = require_fromWebSocket.$unwrap(key);
|
|
1777
|
+
if (target.has(key) && require_fromWebSocket.$unwrap(target.get(key)) === require_fromWebSocket.$unwrap(value)) return receiver ?? target;
|
|
1778
|
+
const result = target.set(key, value);
|
|
1779
|
+
propagateCollection(data, collectionKey(key));
|
|
1780
|
+
return receiver ?? result;
|
|
1781
|
+
};
|
|
1782
|
+
},
|
|
1783
|
+
delete: function(data, target) {
|
|
1784
|
+
return function(key) {
|
|
1785
|
+
key = require_fromWebSocket.$unwrap(key);
|
|
1786
|
+
const result = target.delete(key);
|
|
1787
|
+
propagateCollection(data, collectionKey(key));
|
|
1788
|
+
return result;
|
|
1789
|
+
};
|
|
1790
|
+
},
|
|
1791
|
+
clear: function(data, target) {
|
|
1792
|
+
return function() {
|
|
1793
|
+
const result = target.clear();
|
|
1794
|
+
batchStart();
|
|
1795
|
+
try {
|
|
1796
|
+
for (const key of data.signals.keys()) propagateSignal(data, key);
|
|
1797
|
+
} finally {
|
|
1798
|
+
batchEnd();
|
|
1799
|
+
}
|
|
1800
|
+
return result;
|
|
1801
|
+
};
|
|
1802
|
+
},
|
|
1803
|
+
keys: function(data, target) {
|
|
1804
|
+
return function() {
|
|
1805
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1806
|
+
return [...target.keys()][Symbol.iterator]();
|
|
1807
|
+
};
|
|
1808
|
+
},
|
|
1809
|
+
values: function(data, target) {
|
|
1810
|
+
return function() {
|
|
1811
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1812
|
+
let values = [...target.values()];
|
|
1813
|
+
if (data.shallow !== true) values = values.map(deepWrap);
|
|
1814
|
+
return values[Symbol.iterator]();
|
|
1815
|
+
};
|
|
1816
|
+
},
|
|
1817
|
+
entries: mapEntries,
|
|
1818
|
+
[Symbol.iterator]: mapEntries,
|
|
1819
|
+
forEach: function(data, target, _key, receiver) {
|
|
1820
|
+
return function(callback, thisArg) {
|
|
1821
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1822
|
+
const wrap = data.shallow !== true;
|
|
1823
|
+
target.forEach((value, key) => {
|
|
1824
|
+
callback.call(thisArg, wrap ? deepWrap(value) : value, key, receiver ?? target);
|
|
1825
|
+
});
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
function mapEntries(data, target) {
|
|
1830
|
+
return function() {
|
|
1831
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1832
|
+
const wrap = data.shallow !== true;
|
|
1833
|
+
return [...target.entries()].map((entry) => [entry[0], wrap ? deepWrap(entry[1]) : entry[1]])[Symbol.iterator]();
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
const setWrapper = {
|
|
1837
|
+
size: function(data, target) {
|
|
1838
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1839
|
+
return target.size;
|
|
1840
|
+
},
|
|
1841
|
+
has: function(data, target) {
|
|
1842
|
+
return function(value) {
|
|
1843
|
+
value = require_fromWebSocket.$unwrap(value);
|
|
1844
|
+
trackProxySignal(data, collectionKey(value));
|
|
1845
|
+
return target.has(value);
|
|
1846
|
+
};
|
|
1847
|
+
},
|
|
1848
|
+
add: function(data, target, _key, receiver) {
|
|
1849
|
+
return function(value) {
|
|
1850
|
+
value = require_fromWebSocket.$unwrap(value);
|
|
1851
|
+
if (target.has(value)) return receiver ?? target;
|
|
1852
|
+
const result = target.add(value);
|
|
1853
|
+
propagateCollection(data, collectionKey(value));
|
|
1854
|
+
return receiver ?? result;
|
|
1855
|
+
};
|
|
1856
|
+
},
|
|
1857
|
+
delete: function(data, target) {
|
|
1858
|
+
return function(value) {
|
|
1859
|
+
value = require_fromWebSocket.$unwrap(value);
|
|
1860
|
+
const result = target.delete(value);
|
|
1861
|
+
propagateCollection(data, collectionKey(value));
|
|
1862
|
+
return result;
|
|
1863
|
+
};
|
|
1864
|
+
},
|
|
1865
|
+
clear: function(data, target) {
|
|
1866
|
+
return function() {
|
|
1867
|
+
const result = target.clear();
|
|
1868
|
+
batchStart();
|
|
1869
|
+
try {
|
|
1870
|
+
for (const key of data.signals.keys()) propagateSignal(data, key);
|
|
1871
|
+
} finally {
|
|
1872
|
+
batchEnd();
|
|
1873
|
+
}
|
|
1874
|
+
return result;
|
|
1875
|
+
};
|
|
1876
|
+
},
|
|
1877
|
+
keys: setValues,
|
|
1878
|
+
values: setValues,
|
|
1879
|
+
[Symbol.iterator]: setValues,
|
|
1880
|
+
entries: setEntries,
|
|
1881
|
+
forEach: function(data, target, _key, receiver) {
|
|
1882
|
+
return function(callback, thisArg) {
|
|
1883
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1884
|
+
const wrap = data.shallow !== true;
|
|
1885
|
+
target.forEach((value) => {
|
|
1886
|
+
const wrapped = wrap ? deepWrap(value) : value;
|
|
1887
|
+
callback.call(thisArg, wrapped, wrapped, receiver ?? target);
|
|
1888
|
+
});
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
function setValues(data, target) {
|
|
1893
|
+
return function() {
|
|
1894
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1895
|
+
let values = [...target.values()];
|
|
1896
|
+
if (data.shallow !== true) values = values.map(deepWrap);
|
|
1897
|
+
return values[Symbol.iterator]();
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function setEntries(data, target) {
|
|
1901
|
+
return function() {
|
|
1902
|
+
trackProxySignal(data, COLLECTION_KEY);
|
|
1903
|
+
const wrap = data.shallow !== true;
|
|
1904
|
+
return [...target.values()].map((value) => {
|
|
1905
|
+
const wrapped = wrap ? deepWrap(value) : value;
|
|
1906
|
+
return [wrapped, wrapped];
|
|
1907
|
+
})[Symbol.iterator]();
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
949
1910
|
//#endregion
|
|
950
1911
|
//#region src/watch/proxySet.ts
|
|
951
1912
|
function proxySet(target, key, value, receiver) {
|
|
952
1913
|
const oldValue = target[key];
|
|
953
1914
|
if (value !== oldValue) {
|
|
954
|
-
if (
|
|
955
|
-
let data = target[
|
|
956
|
-
require_devContext.
|
|
957
|
-
if (
|
|
1915
|
+
if (context.batchOperation > 100) throw new Error("Cycle detected");
|
|
1916
|
+
let data = target[require_fromWebSocket.proxyDataSymbol];
|
|
1917
|
+
require_devContext.devContext.signalSet(data, key);
|
|
1918
|
+
if (data.shallow !== true) value = deepWrap(value);
|
|
958
1919
|
Reflect.set(target, key, value, receiver);
|
|
959
1920
|
propagateSignal(data, key);
|
|
960
|
-
if (data.isArray && !isNaN(+
|
|
1921
|
+
if (data.isArray && typeof key !== "symbol" && !isNaN(+key)) propagateSignal(data, "length");
|
|
961
1922
|
}
|
|
962
1923
|
return true;
|
|
963
1924
|
}
|
|
964
|
-
|
|
965
1925
|
//#endregion
|
|
966
1926
|
//#region src/watch/$watch.ts
|
|
1927
|
+
const sharedHandler = {
|
|
1928
|
+
get: proxyGet,
|
|
1929
|
+
set: proxySet
|
|
1930
|
+
};
|
|
967
1931
|
/**
|
|
968
|
-
* Watches an object and runs effects when its properties are changed
|
|
1932
|
+
* Watches an object and runs effects when its properties are changed.
|
|
1933
|
+
*
|
|
1934
|
+
* Dates, Maps and Sets are supported too: their methods are reactive (reads
|
|
1935
|
+
* track, writes notify), so `new Date()`, `new Map()` and `new Set()` can be
|
|
1936
|
+
* used in watched state directly.
|
|
969
1937
|
*
|
|
970
1938
|
* @param object The object to watch
|
|
971
1939
|
*/
|
|
972
1940
|
function $watch(object, options) {
|
|
973
|
-
if (object === void 0 || object === null || object[
|
|
1941
|
+
if (object === void 0 || object === null || object[require_fromWebSocket.proxyDataSymbol] !== void 0) return object;
|
|
974
1942
|
const data = {
|
|
975
1943
|
target: object,
|
|
976
1944
|
isArray: Array.isArray(object),
|
|
1945
|
+
isDate: object instanceof Date,
|
|
1946
|
+
isMap: object instanceof Map,
|
|
1947
|
+
isSet: object instanceof Set,
|
|
977
1948
|
shallow: options?.shallow === true,
|
|
978
1949
|
signals: /* @__PURE__ */ new Map()
|
|
979
1950
|
};
|
|
980
|
-
object
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1951
|
+
Object.defineProperty(object, require_fromWebSocket.proxyDataSymbol, {
|
|
1952
|
+
value: data,
|
|
1953
|
+
writable: true,
|
|
1954
|
+
enumerable: false,
|
|
1955
|
+
configurable: true
|
|
1956
|
+
});
|
|
1957
|
+
const proxy = new Proxy(object, sharedHandler);
|
|
1958
|
+
require_devContext.devContext.onWatch(data);
|
|
987
1959
|
return proxy;
|
|
988
1960
|
}
|
|
989
|
-
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region src/render/getSequence.ts
|
|
1963
|
+
/**
|
|
1964
|
+
* Computes the indices (into `arr`) of a longest strictly-increasing
|
|
1965
|
+
* subsequence, skipping zero entries. Adapted from Vue 3's `getSequence`.
|
|
1966
|
+
*
|
|
1967
|
+
* Used by the keyed-list reconciler (`runListItems`): `arr` is the
|
|
1968
|
+
* `newIndexToOldIndex` map where `arr[i] = oldIndex + 1` (so `0` marks a
|
|
1969
|
+
* new/unmounted slot, which is skipped). The returned indices identify the
|
|
1970
|
+
* items that are already in correct relative order and therefore don't need
|
|
1971
|
+
* to be moved — everything else is moved or mounted.
|
|
1972
|
+
*
|
|
1973
|
+
* Runs in O(n log n).
|
|
1974
|
+
*/
|
|
1975
|
+
function getSequence(arr) {
|
|
1976
|
+
const p = arr.slice();
|
|
1977
|
+
const result = [0];
|
|
1978
|
+
let i;
|
|
1979
|
+
let j;
|
|
1980
|
+
let u;
|
|
1981
|
+
let v;
|
|
1982
|
+
let c;
|
|
1983
|
+
const len = arr.length;
|
|
1984
|
+
for (i = 0; i < len; i++) {
|
|
1985
|
+
const arrI = arr[i];
|
|
1986
|
+
if (arrI !== 0) {
|
|
1987
|
+
j = result[result.length - 1];
|
|
1988
|
+
if (arr[j] < arrI) {
|
|
1989
|
+
p[i] = j;
|
|
1990
|
+
result.push(i);
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
u = 0;
|
|
1994
|
+
v = result.length - 1;
|
|
1995
|
+
while (u < v) {
|
|
1996
|
+
c = u + v >> 1;
|
|
1997
|
+
if (arr[result[c]] < arrI) u = c + 1;
|
|
1998
|
+
else v = c;
|
|
1999
|
+
}
|
|
2000
|
+
if (arrI < arr[result[u]]) {
|
|
2001
|
+
if (u > 0) p[i] = result[u - 1];
|
|
2002
|
+
result[u] = i;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
u = result.length;
|
|
2007
|
+
v = result[u - 1];
|
|
2008
|
+
while (u-- > 0) {
|
|
2009
|
+
result[u] = v;
|
|
2010
|
+
v = p[v];
|
|
2011
|
+
}
|
|
2012
|
+
return result;
|
|
2013
|
+
}
|
|
990
2014
|
//#endregion
|
|
991
2015
|
//#region src/render/moveRegion.ts
|
|
992
2016
|
function moveRegion(parent, region, before) {
|
|
@@ -1000,145 +2024,416 @@ function moveRegion(parent, region, before) {
|
|
|
1000
2024
|
currentNode = nextNode;
|
|
1001
2025
|
}
|
|
1002
2026
|
}
|
|
1003
|
-
|
|
1004
2027
|
//#endregion
|
|
1005
2028
|
//#region src/render/runListItems.ts
|
|
2029
|
+
const SHALLOW_WATCH_OPTIONS = { shallow: true };
|
|
1006
2030
|
/**
|
|
2031
|
+
* Mounts a new list item from a spec: allocates the ListItem, establishes it
|
|
2032
|
+
* as the active region (so the compiler-emitted `create` callback attaches
|
|
2033
|
+
* effects/events to the right region), sets its depth, optionally wraps its
|
|
2034
|
+
* data in a shallow watch, then runs `create`. Returns the mounted item so the
|
|
2035
|
+
* caller can slot it into the live list. The sibling chain is left for the
|
|
2036
|
+
* end-of-pass relink.
|
|
2037
|
+
*/
|
|
2038
|
+
function mountSpec(spec, region, before, create, noWatch) {
|
|
2039
|
+
const item = newListItem(spec.data, spec.key);
|
|
2040
|
+
const savedActiveRegion = context.activeRegion;
|
|
2041
|
+
item.depth = region.depth + 1;
|
|
2042
|
+
context.activeRegion = item;
|
|
2043
|
+
if (noWatch !== true) item.data = $watch(item.data, SHALLOW_WATCH_OPTIONS);
|
|
2044
|
+
create(item, before);
|
|
2045
|
+
context.activeRegion = savedActiveRegion;
|
|
2046
|
+
return item;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Reconciles an old keyed list against a new keyed list, reusing old
|
|
2050
|
+
* ListItems for survivors and producing the minimal set of DOM moves /
|
|
2051
|
+
* mounts / clears.
|
|
2052
|
+
*
|
|
2053
|
+
* @returns The new live list of ListItems (reused old items + freshly
|
|
2054
|
+
* mounted ones), which the caller keeps for the next reconciliation.
|
|
2055
|
+
*
|
|
1007
2056
|
* @param region The list's region
|
|
1008
2057
|
* @param parent The parent DOM element
|
|
1009
|
-
* @param anchor The DOM
|
|
1010
|
-
* @param oldItems The list of current items
|
|
1011
|
-
* @param
|
|
2058
|
+
* @param anchor The DOM node to create new items before (the list's trailing anchor)
|
|
2059
|
+
* @param oldItems The list of current (live) items from the previous run
|
|
2060
|
+
* @param newSpecs The lightweight {key, data} specs for the new data
|
|
1012
2061
|
* @param create A function that creates the DOM elements for a new item
|
|
2062
|
+
* @param update A function that syncs a matched item's data from its spec
|
|
2063
|
+
* (re-runs effects if a loop variable's reference actually changed)
|
|
2064
|
+
* @param noWatch When true, skip the per-item shallow `$watch` wrap. The
|
|
2065
|
+
* compiler guarantees the `@for` body never writes to its loop variables,
|
|
2066
|
+
* and emits an `update` callback that re-runs item effects manually when a
|
|
2067
|
+
* loop variable's reference actually changes (via `t_rerun_region_effects`).
|
|
1013
2068
|
*/
|
|
1014
|
-
function runListItems(region, parent, anchor, oldItems,
|
|
1015
|
-
|
|
1016
|
-
let
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
let
|
|
1023
|
-
let
|
|
1024
|
-
let
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
} else if (
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
} else {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2069
|
+
function runListItems(region, parent, anchor, oldItems, newSpecs, create, update, noWatch) {
|
|
2070
|
+
const newItems = Array.from({ length: newSpecs.length });
|
|
2071
|
+
let nextSibling;
|
|
2072
|
+
if (oldItems.length > 0) {
|
|
2073
|
+
const lastOld = oldItems[oldItems.length - 1];
|
|
2074
|
+
nextSibling = lastOld.nextRegion;
|
|
2075
|
+
while (nextSibling !== null && nextSibling.depth > lastOld.depth) nextSibling = nextSibling.nextRegion;
|
|
2076
|
+
} else nextSibling = region.nextRegion;
|
|
2077
|
+
let oldStart = 0;
|
|
2078
|
+
let oldEnd = oldItems.length - 1;
|
|
2079
|
+
let newStart = 0;
|
|
2080
|
+
let newEnd = newSpecs.length - 1;
|
|
2081
|
+
let chainNeedsRelink = false;
|
|
2082
|
+
while (oldStart <= oldEnd && newStart <= newEnd) if (oldItems[oldStart].key === newSpecs[newStart].key) {
|
|
2083
|
+
const item = oldItems[oldStart];
|
|
2084
|
+
update(item, newSpecs[newStart]);
|
|
2085
|
+
newItems[newStart] = item;
|
|
2086
|
+
oldStart++;
|
|
2087
|
+
newStart++;
|
|
2088
|
+
} else if (oldItems[oldEnd].key === newSpecs[newEnd].key) {
|
|
2089
|
+
const item = oldItems[oldEnd];
|
|
2090
|
+
update(item, newSpecs[newEnd]);
|
|
2091
|
+
newItems[newEnd] = item;
|
|
2092
|
+
oldEnd--;
|
|
2093
|
+
newEnd--;
|
|
2094
|
+
} else if (oldItems[oldStart].key === newSpecs[newEnd].key) {
|
|
2095
|
+
const item = oldItems[oldStart];
|
|
2096
|
+
moveRegion(parent, item, oldItems[oldEnd].endNode?.nextSibling ?? anchor);
|
|
2097
|
+
chainNeedsRelink = true;
|
|
2098
|
+
update(item, newSpecs[newEnd]);
|
|
2099
|
+
newItems[newEnd] = item;
|
|
2100
|
+
oldStart++;
|
|
2101
|
+
newEnd--;
|
|
2102
|
+
} else if (oldItems[oldEnd].key === newSpecs[newStart].key) {
|
|
2103
|
+
const item = oldItems[oldEnd];
|
|
2104
|
+
moveRegion(parent, item, oldItems[oldStart].startNode);
|
|
2105
|
+
chainNeedsRelink = true;
|
|
2106
|
+
update(item, newSpecs[newStart]);
|
|
2107
|
+
newItems[newStart] = item;
|
|
2108
|
+
oldEnd--;
|
|
2109
|
+
newStart++;
|
|
2110
|
+
} else break;
|
|
2111
|
+
if (oldStart > oldEnd && newStart <= newEnd) {
|
|
2112
|
+
const before = newEnd + 1 < newSpecs.length ? newItems[newEnd + 1].startNode : anchor;
|
|
2113
|
+
for (let i = newStart; i <= newEnd; i++) newItems[i] = mountSpec(newSpecs[i], region, before, create, noWatch);
|
|
2114
|
+
chainNeedsRelink = true;
|
|
2115
|
+
} else if (newStart > newEnd) for (let i = oldStart; i <= oldEnd; i++) clearRegion(oldItems[i]);
|
|
2116
|
+
else {
|
|
2117
|
+
const newKeyToIndex = /* @__PURE__ */ new Map();
|
|
2118
|
+
for (let i = newStart; i <= newEnd; i++) newKeyToIndex.set(newSpecs[i].key, i);
|
|
2119
|
+
let anyOverlap = false;
|
|
2120
|
+
for (let i = oldStart; i <= oldEnd; i++) if (newKeyToIndex.has(oldItems[i].key)) {
|
|
2121
|
+
anyOverlap = true;
|
|
2122
|
+
break;
|
|
1053
2123
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
newStartItem.nextRegion = oldStartItem;
|
|
1063
|
-
oldStartItem.previousRegion = newStartItem;
|
|
1064
|
-
clearRegion(oldStartItem);
|
|
1065
|
-
oldStartItem = oldItems[++oldStartIndex];
|
|
1066
|
-
newStartItem = newItems[++newStartIndex];
|
|
1067
|
-
} else if (oldIndex === void 0) {
|
|
1068
|
-
const oldRegion = pushRegion(newStartItem, true);
|
|
1069
|
-
newStartItem.data = $watch(newStartItem.data, { shallow: true });
|
|
1070
|
-
create(newStartItem, oldStartItem.startNode);
|
|
1071
|
-
popRegion(oldRegion);
|
|
1072
|
-
newStartItem = newItems[++newStartIndex];
|
|
1073
|
-
} else if (newIndex === void 0) {
|
|
1074
|
-
clearRegion(oldStartItem);
|
|
1075
|
-
oldStartItem = oldItems[++oldStartIndex];
|
|
2124
|
+
if (!anyOverlap) {
|
|
2125
|
+
let before = oldItems[oldEnd]?.endNode?.nextSibling ?? anchor;
|
|
2126
|
+
for (let i = oldEnd; i >= oldStart; i--) clearRegion(oldItems[i]);
|
|
2127
|
+
for (let i = newStart; i <= newEnd; i++) {
|
|
2128
|
+
newItems[i] = mountSpec(newSpecs[i], region, before, create, noWatch);
|
|
2129
|
+
before = newItems[i].endNode.nextSibling;
|
|
2130
|
+
}
|
|
2131
|
+
chainNeedsRelink = true;
|
|
1076
2132
|
} else {
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
2133
|
+
const newMidLen = newEnd - newStart + 1;
|
|
2134
|
+
const newIndexToOld = Array.from({ length: newMidLen }).fill(0);
|
|
2135
|
+
for (let i = oldStart; i <= oldEnd; i++) {
|
|
2136
|
+
const oldItem = oldItems[i];
|
|
2137
|
+
const newIdx = newKeyToIndex.get(oldItem.key);
|
|
2138
|
+
if (newIdx !== void 0) {
|
|
2139
|
+
newIndexToOld[newIdx - newStart] = i + 1;
|
|
2140
|
+
update(oldItem, newSpecs[newIdx]);
|
|
2141
|
+
newItems[newIdx] = oldItem;
|
|
2142
|
+
} else clearRegion(oldItem);
|
|
2143
|
+
}
|
|
2144
|
+
const seq = getSequence(newIndexToOld);
|
|
2145
|
+
let j = seq.length - 1;
|
|
2146
|
+
for (let i = newMidLen - 1; i >= 0; i--) {
|
|
2147
|
+
const newIdx = newStart + i;
|
|
2148
|
+
const before = newIdx + 1 < newSpecs.length ? newItems[newIdx + 1].startNode : anchor;
|
|
2149
|
+
if (newIndexToOld[i] === 0) {
|
|
2150
|
+
newItems[newIdx] = mountSpec(newSpecs[newIdx], region, before, create, noWatch);
|
|
2151
|
+
chainNeedsRelink = true;
|
|
2152
|
+
} else if (j < 0 || i !== seq[j]) {
|
|
2153
|
+
moveRegion(parent, newItems[newIdx], before);
|
|
2154
|
+
chainNeedsRelink = true;
|
|
2155
|
+
} else j--;
|
|
2156
|
+
}
|
|
1082
2157
|
}
|
|
1083
2158
|
}
|
|
1084
|
-
if (
|
|
1085
|
-
let
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
popRegion(oldRegion);
|
|
1091
|
-
before = newStartItem.endNode.nextSibling;
|
|
2159
|
+
if (!chainNeedsRelink && newItems.length > 0) {
|
|
2160
|
+
let lastRegion = newItems[newItems.length - 1];
|
|
2161
|
+
let next = lastRegion.nextRegion;
|
|
2162
|
+
while (next !== null && next.depth > lastRegion.depth) {
|
|
2163
|
+
lastRegion = next;
|
|
2164
|
+
next = next.nextRegion;
|
|
1092
2165
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
2166
|
+
context.previousRegion = lastRegion;
|
|
2167
|
+
return newItems;
|
|
2168
|
+
}
|
|
2169
|
+
if (newItems.length > 0) {
|
|
2170
|
+
let prevTail = region;
|
|
2171
|
+
for (let i = 0; i < newItems.length; i++) {
|
|
2172
|
+
const item = newItems[i];
|
|
2173
|
+
item.previousRegion = prevTail;
|
|
2174
|
+
prevTail.nextRegion = item;
|
|
2175
|
+
let last = item;
|
|
2176
|
+
let next = item.nextRegion;
|
|
2177
|
+
while (next !== null && next.depth > item.depth) {
|
|
2178
|
+
last = next;
|
|
2179
|
+
next = next.nextRegion;
|
|
2180
|
+
}
|
|
2181
|
+
prevTail = last;
|
|
2182
|
+
}
|
|
2183
|
+
prevTail.nextRegion = nextSibling;
|
|
2184
|
+
if (nextSibling !== null) nextSibling.previousRegion = prevTail;
|
|
2185
|
+
region.nextRegion = newItems[0];
|
|
2186
|
+
context.previousRegion = prevTail;
|
|
2187
|
+
} else {
|
|
2188
|
+
region.nextRegion = nextSibling;
|
|
2189
|
+
if (nextSibling !== null) nextSibling.previousRegion = region;
|
|
2190
|
+
context.previousRegion = region;
|
|
2191
|
+
}
|
|
2192
|
+
return newItems;
|
|
1103
2193
|
}
|
|
1104
|
-
|
|
1105
2194
|
//#endregion
|
|
1106
2195
|
//#region src/render/runList.ts
|
|
1107
2196
|
/**
|
|
1108
2197
|
* Runs a `for` control statement
|
|
2198
|
+
* @param buildItems A function that returns the current list of lightweight
|
|
2199
|
+
* `{key, data}` specs (one per row of the source data)
|
|
1109
2200
|
* @param create A function that creates the control statement's branches
|
|
2201
|
+
* @param noWatch When true, the compiler has determined the `@for` body never
|
|
2202
|
+
* writes to its loop variables, so each item's `data` bag can be left
|
|
2203
|
+
* unwrapped (skipping the per-item shallow `$watch` Proxy + ProxyData +
|
|
2204
|
+
* signals Map allocations). The compiler-emitted `update` callback is then
|
|
2205
|
+
* responsible for re-running item effects when a loop variable's reference
|
|
2206
|
+
* actually changes (via `t_rerun_region_effects`).
|
|
1110
2207
|
*/
|
|
1111
|
-
function runList(region, parent, anchor, buildItems, create, update) {
|
|
2208
|
+
function runList(region, parent, anchor, buildItems, create, update, noWatch) {
|
|
1112
2209
|
let first = true;
|
|
1113
2210
|
let listItems = [];
|
|
1114
|
-
$run(function runList
|
|
2211
|
+
$run(function runList() {
|
|
2212
|
+
if (region.depth === -2) return;
|
|
1115
2213
|
const oldRegion = pushRegion(region, first);
|
|
1116
2214
|
first = false;
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
runListItems(region, parent, anchor, listItems,
|
|
1120
|
-
listItems = newItems;
|
|
2215
|
+
const newSpecs = buildItems();
|
|
2216
|
+
context.activeTarget = null;
|
|
2217
|
+
listItems = runListItems(region, parent, anchor, listItems, newSpecs, create, update, noWatch);
|
|
1121
2218
|
popRegion(oldRegion);
|
|
1122
2219
|
});
|
|
1123
|
-
if (context_default.hydrationNode !== null) anchor = context_default.hydrationNode.nextSibling;
|
|
1124
2220
|
}
|
|
1125
|
-
|
|
2221
|
+
//#endregion
|
|
2222
|
+
//#region src/render/runAwait.ts
|
|
2223
|
+
/**
|
|
2224
|
+
* Renders an `@await` boundary. On the first run, content is rendered
|
|
2225
|
+
* speculatively; if any read inside suspends (`didSuspend`), the boundary
|
|
2226
|
+
* discards the partial render and shows the `with` branch instead.
|
|
2227
|
+
*
|
|
2228
|
+
* Fine-grained updates: on subsequent runs the boundary only decides whether
|
|
2229
|
+
* to SWITCH branches. It does so from its `pending` set — the suspended
|
|
2230
|
+
* computeds recorded by `suspendRead` — dropping entries that resolved and
|
|
2231
|
+
* re-subscribing the still-suspended ones (a re-run deactivates all of the
|
|
2232
|
+
* effect's source subscriptions, so they must be re-tracked to survive
|
|
2233
|
+
* `clearSources`). That check is O(pending reads), not O(all sources), and
|
|
2234
|
+
* non-suspend dependency changes inside content are left to the child
|
|
2235
|
+
* effects that read them — the boundary isn't re-run by them at all.
|
|
2236
|
+
*
|
|
2237
|
+
* Stale-while-revalidate (ASYNC.md §6.2): once content has been produced
|
|
2238
|
+
* (`hasContent`), a subsequent suspend during a refresh does NOT switch to
|
|
2239
|
+
* the `with` branch — the boundary keeps the stale content mounted. `$async`
|
|
2240
|
+
* retains the previous resolved value (`Computed.staleValue`) and
|
|
2241
|
+
* `suspendRead` returns it, so child effects keep displaying the old value
|
|
2242
|
+
* until the new promise resolves and updates them in place. The `with` branch
|
|
2243
|
+
* is shown only on the first load, before content has ever rendered
|
|
2244
|
+
* successfully.
|
|
2245
|
+
*
|
|
2246
|
+
* @param renderContent Builds the content children (may read $async getters).
|
|
2247
|
+
* @param renderWith Builds the `with`-branch children, or null for empty.
|
|
2248
|
+
*/
|
|
2249
|
+
function runAwait(region, anchor, renderContent, renderWith, name) {
|
|
2250
|
+
const boundary = {
|
|
2251
|
+
suspended: false,
|
|
2252
|
+
pending: /* @__PURE__ */ new Set(),
|
|
2253
|
+
effect: null
|
|
2254
|
+
};
|
|
2255
|
+
let index = -1;
|
|
2256
|
+
let first = true;
|
|
2257
|
+
let hasContent = false;
|
|
2258
|
+
let theEffect = null;
|
|
2259
|
+
const gen = region.generation = (region.generation ?? 0) + 1;
|
|
2260
|
+
$run(function runAwait() {
|
|
2261
|
+
if (region.generation !== gen) return;
|
|
2262
|
+
theEffect = context.activeTarget;
|
|
2263
|
+
boundary.effect = theEffect;
|
|
2264
|
+
const oldRegion = pushRegion(region, first);
|
|
2265
|
+
first = false;
|
|
2266
|
+
const renderBranch = (targetIndex, fn) => {
|
|
2267
|
+
runControlBranch(region, index, targetIndex);
|
|
2268
|
+
context.previousRegion = region;
|
|
2269
|
+
const oldR = pushRegion(newRegion(targetIndex === 0 ? "await_content" : "await_with"), true);
|
|
2270
|
+
fn(anchor);
|
|
2271
|
+
popRegion(oldR);
|
|
2272
|
+
index = targetIndex;
|
|
2273
|
+
};
|
|
2274
|
+
const attemptContent = () => {
|
|
2275
|
+
const oldBoundary = context.awaitBoundary;
|
|
2276
|
+
context.awaitBoundary = boundary;
|
|
2277
|
+
boundary.suspended = false;
|
|
2278
|
+
const savedHydration = context.hydrationNode;
|
|
2279
|
+
if (savedHydration !== null) context.hydrationNode = null;
|
|
2280
|
+
renderBranch(0, renderContent);
|
|
2281
|
+
if (savedHydration !== null) context.hydrationNode = savedHydration;
|
|
2282
|
+
context.awaitBoundary = oldBoundary;
|
|
2283
|
+
if (boundary.suspended && renderWith !== null) renderBranch(1, renderWith);
|
|
2284
|
+
else if (!boundary.suspended) hasContent = true;
|
|
2285
|
+
};
|
|
2286
|
+
if (theEffect === null) attemptContent();
|
|
2287
|
+
else {
|
|
2288
|
+
let suspended = false;
|
|
2289
|
+
for (const signal of boundary.pending) if (signal.didSuspend) {
|
|
2290
|
+
suspended = true;
|
|
2291
|
+
trackSignal(signal);
|
|
2292
|
+
} else boundary.pending.delete(signal);
|
|
2293
|
+
const targetIndex = suspended ? 1 : 0;
|
|
2294
|
+
if (targetIndex !== index) {
|
|
2295
|
+
if (targetIndex === 0) attemptContent();
|
|
2296
|
+
else if (!hasContent) if (renderWith !== null) renderBranch(1, renderWith);
|
|
2297
|
+
else {
|
|
2298
|
+
runControlBranch(region, index, 1);
|
|
2299
|
+
index = 1;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
popRegion(oldRegion);
|
|
2304
|
+
widenAncestorsAtAnchor(region, anchor);
|
|
2305
|
+
}, name);
|
|
2306
|
+
}
|
|
2307
|
+
//#endregion
|
|
2308
|
+
//#region src/render/saveHydration.ts
|
|
2309
|
+
/**
|
|
2310
|
+
* Snapshots the hydration cursor so a `@try` branch's partial hydration walk
|
|
2311
|
+
* can be rewound if the branch throws and the `@catch` branch renders instead.
|
|
2312
|
+
*
|
|
2313
|
+
* @returns The current hydration cursor node, or `null` when not hydrating.
|
|
2314
|
+
*/
|
|
2315
|
+
function saveHydration() {
|
|
2316
|
+
return context.hydrationNode;
|
|
2317
|
+
}
|
|
2318
|
+
//#endregion
|
|
2319
|
+
//#region src/render/runTry.ts
|
|
2320
|
+
/**
|
|
2321
|
+
* Renders a `@try`/`@catch` group, or a top-level `@error` block (which the
|
|
2322
|
+
* compiler wraps around the whole render).
|
|
2323
|
+
*
|
|
2324
|
+
* Like `runAwait`, the boundary is a control effect that renders one branch
|
|
2325
|
+
* at a time into a fresh child region (`runControlBranch` clears the old
|
|
2326
|
+
* branch on switch). Unlike the old compiled form, the try/catch lives in
|
|
2327
|
+
* this runtime, which gives it three capabilities the compiled form lacked:
|
|
2328
|
+
*
|
|
2329
|
+
* - **Effect-rerun error routing.** The boundary registers itself on its
|
|
2330
|
+
* region (`region.errorBoundary`); when an effect inside the try content
|
|
2331
|
+
* throws on a later re-run, `triggerEffects` → `routeEffectError` stores
|
|
2332
|
+
* the error here and force re-runs the boundary effect, which renders the
|
|
2333
|
+
* catch branch with it.
|
|
2334
|
+
* - **Recovery outside direct reads.** Reactive reads wrapped in nested
|
|
2335
|
+
* `$run` effects (text/attribute interpolation) are tracked by those
|
|
2336
|
+
* effects, not the boundary. When an error is routed, the boundary holds
|
|
2337
|
+
* the erroring effect's source signals (`heldSignals`) and re-subscribes
|
|
2338
|
+
* to them on every run that shows the catch branch, so a later change
|
|
2339
|
+
* re-attempts the try branch.
|
|
2340
|
+
* - **Top-level `@error` recovery.** Because the whole render is a
|
|
2341
|
+
* re-runnable branch, a later re-render error (from a nested control
|
|
2342
|
+
* re-running after a prop change) is caught by the same routing, and a
|
|
2343
|
+
* recovery re-render clears the error content and restores the normal
|
|
2344
|
+
* content.
|
|
2345
|
+
*
|
|
2346
|
+
* Semantics preserved from the compiled form:
|
|
2347
|
+
*
|
|
2348
|
+
* - Synchronous build errors in the try content render the catch branch
|
|
2349
|
+
* (rewinding the hydration cursor and restoring the control region, which
|
|
2350
|
+
* the failed partial build left active).
|
|
2351
|
+
* - While the try branch is showing, a boundary re-run with no routed error
|
|
2352
|
+
* does NOT rebuild it (`runControlBranch` same-index skip) — exactly like
|
|
2353
|
+
* `@if`/`@switch` branches.
|
|
2354
|
+
* - While the catch branch is showing, any boundary re-run re-attempts the
|
|
2355
|
+
* try branch (the recovery path for errors read directly by the boundary
|
|
2356
|
+
* effect, e.g. via `@const`).
|
|
2357
|
+
* - With no catch branch, errors propagate up to the nearest outer boundary
|
|
2358
|
+
* and no boundary is registered on the region.
|
|
2359
|
+
*
|
|
2360
|
+
* @param renderTry Builds the try content. May read reactive state.
|
|
2361
|
+
* @param renderCatch Builds the catch content, or null for `@try` without
|
|
2362
|
+
* `@catch`. Receives the error (the `@catch (err)` variable).
|
|
2363
|
+
*/
|
|
2364
|
+
function runTry(region, anchor, renderTry, renderCatch, name) {
|
|
2365
|
+
const boundary = {
|
|
2366
|
+
error: void 0,
|
|
2367
|
+
hasError: false,
|
|
2368
|
+
effect: null,
|
|
2369
|
+
heldSignals: null
|
|
2370
|
+
};
|
|
2371
|
+
if (renderCatch !== null) region.errorBoundary = boundary;
|
|
2372
|
+
let index = -1;
|
|
2373
|
+
let first = true;
|
|
2374
|
+
const gen = region.generation = (region.generation ?? 0) + 1;
|
|
2375
|
+
$run(function runTry() {
|
|
2376
|
+
if (region.generation !== gen) return;
|
|
2377
|
+
const theEffect = context.activeTarget;
|
|
2378
|
+
boundary.effect = theEffect;
|
|
2379
|
+
const oldRegion = pushRegion(region, first);
|
|
2380
|
+
first = false;
|
|
2381
|
+
const renderBranch = (targetIndex, fn) => {
|
|
2382
|
+
runControlBranch(region, index, targetIndex);
|
|
2383
|
+
context.previousRegion = region;
|
|
2384
|
+
const oldR = pushRegion(newRegion(targetIndex === 0 ? "try" : "catch"), true);
|
|
2385
|
+
fn(anchor);
|
|
2386
|
+
popRegion(oldR);
|
|
2387
|
+
index = targetIndex;
|
|
2388
|
+
};
|
|
2389
|
+
const holdSignals = () => {
|
|
2390
|
+
if (boundary.heldSignals === null || theEffect === null) return;
|
|
2391
|
+
for (const signal of boundary.heldSignals) trackSignal(signal);
|
|
2392
|
+
};
|
|
2393
|
+
const hasRoutedError = boundary.hasError;
|
|
2394
|
+
const routedError = boundary.error;
|
|
2395
|
+
boundary.hasError = false;
|
|
2396
|
+
if (hasRoutedError && renderCatch !== null) {
|
|
2397
|
+
index = -1;
|
|
2398
|
+
renderBranch(1, (a) => renderCatch(a, routedError));
|
|
2399
|
+
holdSignals();
|
|
2400
|
+
} else if (index !== 0) {
|
|
2401
|
+
const snapshot = saveHydration();
|
|
2402
|
+
try {
|
|
2403
|
+
renderBranch(0, renderTry);
|
|
2404
|
+
boundary.heldSignals = null;
|
|
2405
|
+
} catch (err) {
|
|
2406
|
+
pushRegion(region);
|
|
2407
|
+
restoreHydration(snapshot);
|
|
2408
|
+
if (renderCatch !== null) {
|
|
2409
|
+
index = -1;
|
|
2410
|
+
renderBranch(1, (a) => renderCatch(a, err));
|
|
2411
|
+
holdSignals();
|
|
2412
|
+
} else throw err;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
popRegion(oldRegion);
|
|
2416
|
+
widenAncestorsAtAnchor(region, anchor);
|
|
2417
|
+
if (context.hydrationNode !== null && anchor !== null) context.hydrationNode = anchor;
|
|
2418
|
+
}, name);
|
|
2419
|
+
}
|
|
1126
2420
|
//#endregion
|
|
1127
2421
|
//#region src/render/setAttribute.ts
|
|
1128
2422
|
function setAttribute(el, name, value) {
|
|
1129
2423
|
if (value === false || value === void 0 || value === null) el.removeAttribute(name);
|
|
1130
2424
|
else el.setAttribute(name, value);
|
|
1131
2425
|
}
|
|
1132
|
-
|
|
1133
2426
|
//#endregion
|
|
1134
2427
|
//#region src/render/setDynamicElement.ts
|
|
1135
2428
|
function setDynamicElement(el, tag) {
|
|
1136
|
-
if (
|
|
2429
|
+
if (context.hydrationNode !== null) return el;
|
|
1137
2430
|
let newElement = el.ownerDocument.createElement(tag);
|
|
2431
|
+
for (const attr of el.attributes) newElement.setAttributeNS(null, attr.name, attr.value);
|
|
2432
|
+
copyEventListeners(el, newElement);
|
|
2433
|
+
newElement.replaceChildren(...el.childNodes.values());
|
|
1138
2434
|
el.replaceWith(newElement);
|
|
1139
2435
|
return newElement;
|
|
1140
2436
|
}
|
|
1141
|
-
|
|
1142
2437
|
//#endregion
|
|
1143
2438
|
//#region src/watch/$batch.ts
|
|
1144
2439
|
/**
|
|
@@ -1148,7 +2443,7 @@ function setDynamicElement(el, tag) {
|
|
|
1148
2443
|
* @param fn The function containing the batched updates to run.
|
|
1149
2444
|
*/
|
|
1150
2445
|
function $batch(fn) {
|
|
1151
|
-
if (
|
|
2446
|
+
if (context.batch > 0) return fn();
|
|
1152
2447
|
batchStart();
|
|
1153
2448
|
try {
|
|
1154
2449
|
return fn();
|
|
@@ -1156,7 +2451,127 @@ function $batch(fn) {
|
|
|
1156
2451
|
batchEnd();
|
|
1157
2452
|
}
|
|
1158
2453
|
}
|
|
1159
|
-
|
|
2454
|
+
//#endregion
|
|
2455
|
+
//#region src/watch/$async.ts
|
|
2456
|
+
/**
|
|
2457
|
+
* Caches an async computed value from a thunk that returns a Promise. A peer
|
|
2458
|
+
* of `$cache` for async getters: the returned Computed carries a `didSuspend`
|
|
2459
|
+
* indicator that the proxy get trap reads to suspend callers until the promise
|
|
2460
|
+
* resolves.
|
|
2461
|
+
*
|
|
2462
|
+
* The thenable check and `.then` wiring live inside `computed.run` (a closure
|
|
2463
|
+
* over the computed object), so they fire on both the initial run and every
|
|
2464
|
+
* recalculation — `runComputed` and `checkComputed` are unchanged.
|
|
2465
|
+
*
|
|
2466
|
+
* @param fn A thunk returning the Promise to await. Signal reads inside `fn`
|
|
2467
|
+
* are tracked, so the fetch re-runs when dependencies change.
|
|
2468
|
+
*/
|
|
2469
|
+
function $async(fn) {
|
|
2470
|
+
if (context.registerComputed === null) throw new Error("$async must be used in a getter");
|
|
2471
|
+
const computed = {
|
|
2472
|
+
type: 1,
|
|
2473
|
+
isAsync: true,
|
|
2474
|
+
value: null,
|
|
2475
|
+
run: null,
|
|
2476
|
+
firstSource: null,
|
|
2477
|
+
firstTarget: null,
|
|
2478
|
+
recalc: false,
|
|
2479
|
+
running: false,
|
|
2480
|
+
didError: false,
|
|
2481
|
+
didSuspend: false,
|
|
2482
|
+
generation: 0,
|
|
2483
|
+
hasResolved: false,
|
|
2484
|
+
lastErrored: false,
|
|
2485
|
+
suspendQuiet: false,
|
|
2486
|
+
staleValue: void 0,
|
|
2487
|
+
rollback: null
|
|
2488
|
+
};
|
|
2489
|
+
computed.run = () => {
|
|
2490
|
+
const value = fn();
|
|
2491
|
+
if (value !== null && value !== void 0 && typeof value.then === "function") {
|
|
2492
|
+
const gen = ++computed.generation;
|
|
2493
|
+
computed.didSuspend = true;
|
|
2494
|
+
computed.suspendQuiet = computed.hasResolved && !computed.recalc;
|
|
2495
|
+
value.then((v) => {
|
|
2496
|
+
if (computed.generation !== gen) return;
|
|
2497
|
+
computed.value = v;
|
|
2498
|
+
computed.didSuspend = false;
|
|
2499
|
+
computed.hasResolved = true;
|
|
2500
|
+
computed.lastErrored = false;
|
|
2501
|
+
computed.staleValue = v;
|
|
2502
|
+
batchStart();
|
|
2503
|
+
propagateFromSignal(computed);
|
|
2504
|
+
batchEnd();
|
|
2505
|
+
}, (e) => {
|
|
2506
|
+
if (computed.generation !== gen) return;
|
|
2507
|
+
computed.value = e;
|
|
2508
|
+
computed.didError = true;
|
|
2509
|
+
computed.didSuspend = false;
|
|
2510
|
+
computed.hasResolved = true;
|
|
2511
|
+
computed.lastErrored = true;
|
|
2512
|
+
computed.staleValue = void 0;
|
|
2513
|
+
batchStart();
|
|
2514
|
+
propagateFromSignal(computed);
|
|
2515
|
+
batchEnd();
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
return value;
|
|
2519
|
+
};
|
|
2520
|
+
context.registerComputed(computed);
|
|
2521
|
+
runComputed(computed);
|
|
2522
|
+
return computed.value;
|
|
2523
|
+
}
|
|
2524
|
+
//#endregion
|
|
2525
|
+
//#region src/watch/$bind.ts
|
|
2526
|
+
/**
|
|
2527
|
+
* Establishes two-way reactive sync between matching keys on a state object
|
|
2528
|
+
* and a props object.
|
|
2529
|
+
*
|
|
2530
|
+
* For each key, `$bind` creates:
|
|
2531
|
+
* - **Forward sync** (`props → state`): when the parent pushes a new value via
|
|
2532
|
+
* `$props[key]`, it flows into `$state[key]`. Skips `undefined` values so
|
|
2533
|
+
* that defaults set in the initial `$watch(...)` are preserved when the
|
|
2534
|
+
* parent doesn't provide a value.
|
|
2535
|
+
* - **Backward sync** (`state → props`): when the component mutates
|
|
2536
|
+
* `$state[key]`, the new value is written back to `$props[key]`, which the
|
|
2537
|
+
* call site's `&key={...}` binding picks up and propagates to the parent.
|
|
2538
|
+
*
|
|
2539
|
+
* Same-value writes are no-ops (handled by the proxy layer), so two-way sync
|
|
2540
|
+
* stabilises without infinite loops.
|
|
2541
|
+
*
|
|
2542
|
+
* `$bind` syncs same-named keys, so the key must exist on the state object.
|
|
2543
|
+
* A state key named differently from the prop (e.g. `values` against a
|
|
2544
|
+
* `value` prop) would silently do nothing -- the binding reads and writes
|
|
2545
|
+
* keys that nothing else touches -- which is almost impossible to debug. The
|
|
2546
|
+
* key is therefore required to exist on the state object, and `$bind` throws
|
|
2547
|
+
* at setup otherwise.
|
|
2548
|
+
*
|
|
2549
|
+
* The prop does *not* need to exist on `props` yet: when the parent doesn't
|
|
2550
|
+
* pass an optional prop, the compiled props object has no key for it, and
|
|
2551
|
+
* the binding is simply inert (uncontrolled mode) until a value is pushed.
|
|
2552
|
+
*
|
|
2553
|
+
* @param state The component's reactive state (created via `$watch`).
|
|
2554
|
+
* @param props The component's `$props` proxy. When `undefined` (component
|
|
2555
|
+
* called with no props), `$bind` does nothing.
|
|
2556
|
+
* @param keys Keys to sync. Each key must exist on `state`.
|
|
2557
|
+
* @throws When a key does not exist on the state object.
|
|
2558
|
+
*/
|
|
2559
|
+
function $bind(state, props, ...keys) {
|
|
2560
|
+
if (props === void 0) return;
|
|
2561
|
+
for (let key of keys.flat()) {
|
|
2562
|
+
if (!(key in state)) throw new Error(`$bind: the key "${key}" does not exist on the state object. $bind syncs same-named keys, so the state object must declare it (e.g. in the initial $watch).`);
|
|
2563
|
+
$run(() => {
|
|
2564
|
+
const v = props[key];
|
|
2565
|
+
if (v !== void 0) state[key] = v;
|
|
2566
|
+
}, `bind:forward:${key}`);
|
|
2567
|
+
let firstBackward = true;
|
|
2568
|
+
$run(() => {
|
|
2569
|
+
const value = state[key];
|
|
2570
|
+
if (!firstBackward) props[key] = value;
|
|
2571
|
+
firstBackward = false;
|
|
2572
|
+
}, `bind:backward:${key}`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
1160
2575
|
//#endregion
|
|
1161
2576
|
//#region src/watch/$cache.ts
|
|
1162
2577
|
/**
|
|
@@ -1165,9 +2580,13 @@ function $batch(fn) {
|
|
|
1165
2580
|
* @param fn The function containing the signals to compute and cache.
|
|
1166
2581
|
*/
|
|
1167
2582
|
function $cache(fn) {
|
|
1168
|
-
if (
|
|
2583
|
+
if (context.registerComputed === null) {
|
|
2584
|
+
if (typeof window !== "undefined") throw new Error("$cache must be used in a getter");
|
|
2585
|
+
return fn();
|
|
2586
|
+
}
|
|
1169
2587
|
let computed = {
|
|
1170
|
-
type:
|
|
2588
|
+
type: 1,
|
|
2589
|
+
isAsync: false,
|
|
1171
2590
|
value: null,
|
|
1172
2591
|
run: fn,
|
|
1173
2592
|
firstSource: null,
|
|
@@ -1175,106 +2594,266 @@ function $cache(fn) {
|
|
|
1175
2594
|
recalc: false,
|
|
1176
2595
|
running: false,
|
|
1177
2596
|
didError: false,
|
|
2597
|
+
didSuspend: false,
|
|
2598
|
+
generation: 0,
|
|
2599
|
+
hasResolved: false,
|
|
2600
|
+
lastErrored: false,
|
|
2601
|
+
suspendQuiet: false,
|
|
2602
|
+
staleValue: void 0,
|
|
1178
2603
|
rollback: null
|
|
1179
2604
|
};
|
|
1180
|
-
|
|
2605
|
+
context.registerComputed(computed);
|
|
1181
2606
|
runComputed(computed);
|
|
2607
|
+
if (computed.value !== null && computed.value !== void 0 && typeof computed.value.then === "function") throw new Error("$cache returned a Promise — use $async for async getters");
|
|
1182
2608
|
return computed.value;
|
|
1183
2609
|
}
|
|
1184
|
-
|
|
1185
2610
|
//#endregion
|
|
1186
|
-
//#region src/watch/$
|
|
2611
|
+
//#region src/watch/$onmount.ts
|
|
1187
2612
|
/**
|
|
1188
|
-
* Runs
|
|
2613
|
+
* Runs `fn` once after the component is mounted to the DOM. May return a
|
|
2614
|
+
* cleanup function that runs on unmount / region clear.
|
|
2615
|
+
*
|
|
2616
|
+
* The callback is **not reactive**: it runs exactly once per mount, and
|
|
2617
|
+
* reactive values it reads are not tracked. To set up something that should
|
|
2618
|
+
* keep updating, wrap the reactive part in `$run`:
|
|
2619
|
+
*
|
|
2620
|
+
* ```ts
|
|
2621
|
+
* $onmount(() => {
|
|
2622
|
+
* $run(() => {
|
|
2623
|
+
* label.textContent = $state.count > 0 ? "+" : "";
|
|
2624
|
+
* });
|
|
2625
|
+
* });
|
|
2626
|
+
* ```
|
|
2627
|
+
*
|
|
2628
|
+
* The element-level equivalent is the `onmount` attribute:
|
|
2629
|
+
* `<input onmount={(el) => el.focus()} />`.
|
|
1189
2630
|
*
|
|
1190
2631
|
* @param fn The function to run, which may return a cleanup function
|
|
1191
2632
|
*/
|
|
1192
|
-
function $
|
|
1193
|
-
|
|
2633
|
+
function $onmount(fn) {
|
|
2634
|
+
context.mountEffects.push(fn);
|
|
1194
2635
|
}
|
|
1195
|
-
|
|
1196
2636
|
//#endregion
|
|
1197
|
-
//#region src/watch/$
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
2637
|
+
//#region src/watch/$pending.ts
|
|
2638
|
+
/**
|
|
2639
|
+
* Returns `true` if any value read inside `fn` is currently suspended in a
|
|
2640
|
+
* "loud" way — a `$async` getter whose promise hasn't resolved yet. A reactive
|
|
2641
|
+
* query for inline "loading…" indicators: the calling effect subscribes to the
|
|
2642
|
+
* same computeds, so `$pending` re-evaluates when they resolve.
|
|
2643
|
+
*
|
|
2644
|
+
* Uses a "peek mode" internally: reads during `fn` are tracked for
|
|
2645
|
+
* subscription but don't propagate taint or notify a `@await` boundary.
|
|
2646
|
+
* This lets `$pending` return a plain boolean without itself suspending.
|
|
2647
|
+
*
|
|
2648
|
+
* Quiet-on-refresh semantics (ASYNC.md §7.4): a suspend is *quiet* — and
|
|
2649
|
+
* therefore `$pending` returns `false` for it — when the computed has resolved
|
|
2650
|
+
* before and the re-fetch was a *silent* `$refresh(fn, { silent: true })` (a
|
|
2651
|
+
* bare refresh with no tracked dependency change, used for background
|
|
2652
|
+
* revalidation). This matches Solid's stale-while-revalidate default: quietly
|
|
2653
|
+
* re-asking the same question shouldn't ping the user. First loads and
|
|
2654
|
+
* dependency-change refreshes are loud (`true`), as is the default (loud)
|
|
2655
|
+
* `$refresh(fn)`. Quiet-ness is captured per computed at suspend time via
|
|
2656
|
+
* `suspendQuiet` (computed in `$async`'s run from `hasResolved` and `recalc`).
|
|
2657
|
+
*
|
|
2658
|
+
* @param fn A function that reads the reactive values to check.
|
|
2659
|
+
*/
|
|
2660
|
+
function $pending(fn) {
|
|
2661
|
+
const oldPeek = context.suspendPeek;
|
|
2662
|
+
context.suspendPeek = true;
|
|
2663
|
+
context.suspendPeekHit = false;
|
|
2664
|
+
try {
|
|
2665
|
+
fn();
|
|
2666
|
+
return context.suspendPeekHit;
|
|
2667
|
+
} finally {
|
|
2668
|
+
context.suspendPeek = oldPeek;
|
|
2669
|
+
}
|
|
1204
2670
|
}
|
|
1205
|
-
|
|
1206
2671
|
//#endregion
|
|
1207
|
-
//#region src/
|
|
1208
|
-
let initialized = false;
|
|
2672
|
+
//#region src/watch/$refresh.ts
|
|
1209
2673
|
/**
|
|
1210
|
-
*
|
|
2674
|
+
* Re-fetches the `$async` getters read inside `fn` without changing a
|
|
2675
|
+
* dependency. A companion to `$pending` (ASYNC.md §6.2): pull-to-refresh,
|
|
2676
|
+
* refresh buttons, refetch-on-focus, polling, retry-after-error.
|
|
2677
|
+
*
|
|
2678
|
+
* `fn` is run in a tracking context that collects every `$async` computed it
|
|
2679
|
+
* reads (`$cache` computeds are ignored). Each collected computed is then
|
|
2680
|
+
* re-run with `recalc` left true — a "bare refresh" — so the suspend is
|
|
2681
|
+
* *loud*: `$pending(fn)` returns `true` while the re-fetch is in flight and
|
|
2682
|
+
* subscribers are notified at suspend *start*, so an inline "updating…"
|
|
2683
|
+
* indicator appears immediately. Readers keep displaying the previous
|
|
2684
|
+
* resolved value (`Computed.staleValue`) until the new promise resolves and
|
|
2685
|
+
* propagates through the reactive graph; an `@await` boundary keeps its
|
|
2686
|
+
* content mounted instead of flashing fallback.
|
|
2687
|
+
*
|
|
2688
|
+
* Pass `{ silent: true }` to re-fetch quietly (stale-while-revalidate):
|
|
2689
|
+
* `$pending(fn)` stays `false` and nothing re-runs until the new promise
|
|
2690
|
+
* resolves. For background revalidation where feedback would be noise —
|
|
2691
|
+
* polling, refetch-on-focus:
|
|
2692
|
+
*
|
|
2693
|
+
* ```torp
|
|
2694
|
+
* // poll quietly every 30s
|
|
2695
|
+
* $run(() => {
|
|
2696
|
+
* const id = setInterval(() => $refresh(() => $state.data, { silent: true }), 30_000);
|
|
2697
|
+
* return () => clearInterval(id);
|
|
2698
|
+
* });
|
|
2699
|
+
* ```
|
|
2700
|
+
*
|
|
2701
|
+
* A refresh is loud regardless of `silent` when the computed has never
|
|
2702
|
+
* resolved — e.g. retrying after an error that left `didError` set on a first
|
|
2703
|
+
* load — or when the re-fetch is triggered by a dependency change, which
|
|
2704
|
+
* flows through the normal `recalc` path instead of here.
|
|
2705
|
+
*
|
|
2706
|
+
* If `fn` reads a getter the UI has never read, the collection read
|
|
2707
|
+
* initializes it — and that fetch IS the refresh: the computed is not re-run
|
|
2708
|
+
* (a second fetch would be a duplicate whose resolve the generation guard
|
|
2709
|
+
* drops). Getters already read (pending, resolved, or errored) are re-run as
|
|
2710
|
+
* usual.
|
|
2711
|
+
*
|
|
2712
|
+
* The returned value of `fn` is ignored; use `fn` to reference the getters.
|
|
1211
2713
|
*/
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
2714
|
+
function $refresh(fn, options) {
|
|
2715
|
+
const silent = options?.silent === true;
|
|
2716
|
+
const oldRefreshSignals = context.refreshSignals;
|
|
2717
|
+
const oldPeek = context.suspendPeek;
|
|
2718
|
+
const oldInitialized = context.refreshInitialized;
|
|
2719
|
+
context.refreshSignals = [];
|
|
2720
|
+
context.suspendPeek = true;
|
|
2721
|
+
context.refreshInitialized = /* @__PURE__ */ new Set();
|
|
2722
|
+
let unique;
|
|
2723
|
+
let initialized;
|
|
2724
|
+
try {
|
|
2725
|
+
fn();
|
|
2726
|
+
unique = new Set(context.refreshSignals ?? []);
|
|
2727
|
+
initialized = context.refreshInitialized;
|
|
2728
|
+
} finally {
|
|
2729
|
+
context.refreshSignals = oldRefreshSignals;
|
|
2730
|
+
context.suspendPeek = oldPeek;
|
|
2731
|
+
context.refreshInitialized = oldInitialized;
|
|
1223
2732
|
}
|
|
1224
|
-
|
|
1225
|
-
initialized
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
};
|
|
2733
|
+
for (const computed of unique) {
|
|
2734
|
+
if (initialized.has(computed)) continue;
|
|
2735
|
+
computed.recalc = !silent;
|
|
2736
|
+
deactivateSources(computed);
|
|
2737
|
+
const oldActiveTarget = context.activeTarget;
|
|
2738
|
+
context.activeTarget = null;
|
|
2739
|
+
try {
|
|
2740
|
+
runComputed(computed);
|
|
2741
|
+
} finally {
|
|
2742
|
+
context.activeTarget = oldActiveTarget;
|
|
2743
|
+
}
|
|
2744
|
+
computed.recalc = false;
|
|
2745
|
+
clearSources(computed);
|
|
2746
|
+
if (!silent) {
|
|
2747
|
+
batchStart();
|
|
2748
|
+
propagateFromSignal(computed);
|
|
2749
|
+
batchEnd();
|
|
1242
2750
|
}
|
|
1243
2751
|
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
2752
|
+
}
|
|
2753
|
+
//#endregion
|
|
2754
|
+
//#region src/watch/$stream.ts
|
|
2755
|
+
/**
|
|
2756
|
+
* Subscribes to an external source of events — server-sent events,
|
|
2757
|
+
* WebSockets, DOM events, or any custom `StreamSource` — and calls
|
|
2758
|
+
* `handler` for each event it pushes. Events are typically written into
|
|
2759
|
+
* reactive state, which keeps templates, computeds and effects updating
|
|
2760
|
+
* through the normal reactivity model:
|
|
2761
|
+
*
|
|
2762
|
+
* ```torp
|
|
2763
|
+
* let $state = $watch({ messages: [] as string[] });
|
|
2764
|
+
*
|
|
2765
|
+
* $stream(fromServer(`/sse/${$props.id}`), (e) => {
|
|
2766
|
+
* $state.messages.push(e.data);
|
|
2767
|
+
* });
|
|
2768
|
+
* ```
|
|
2769
|
+
*
|
|
2770
|
+
* The subscription is managed by the framework:
|
|
2771
|
+
*
|
|
2772
|
+
* - It starts when the component is mounted to the DOM (so `&ref`-bound
|
|
2773
|
+
* elements are available), and is unsubscribed when the component
|
|
2774
|
+
* unmounts or its region is cleared.
|
|
2775
|
+
* - Reactive state read *inside* the source is tracked: when it changes,
|
|
2776
|
+
* the source is unsubscribed and re-subscribed with fresh values (e.g.
|
|
2777
|
+
* re-opening a connection when a user id changes). A pending debounced
|
|
2778
|
+
* call is dropped on re-subscribe.
|
|
2779
|
+
* - The source is never invoked during a server render, so browser-only
|
|
2780
|
+
* APIs are safe to use.
|
|
2781
|
+
*
|
|
2782
|
+
* Errors are values like any other: a source that can fail should push an
|
|
2783
|
+
* error-shaped value and own its own reconnection (e.g. `EventSource`
|
|
2784
|
+
* reconnects automatically).
|
|
2785
|
+
*
|
|
2786
|
+
* @param source The stream source to subscribe to
|
|
2787
|
+
* @param handler Called for each event the source pushes
|
|
2788
|
+
* @param options Timing options. `debounce` delays each handler call until
|
|
2789
|
+
* the source has been quiet for that many milliseconds, resetting on
|
|
2790
|
+
* every event (only the last event in a burst is handled).
|
|
2791
|
+
*/
|
|
2792
|
+
function $stream(source, handler, options) {
|
|
2793
|
+
context.mountEffects.push(() => {
|
|
2794
|
+
let timer;
|
|
2795
|
+
$run(() => {
|
|
2796
|
+
const unsub = source((value) => {
|
|
2797
|
+
if (options?.debounce) {
|
|
2798
|
+
clearTimeout(timer);
|
|
2799
|
+
timer = setTimeout(() => handler(value), options.debounce);
|
|
2800
|
+
} else handler(value);
|
|
2801
|
+
});
|
|
2802
|
+
return () => {
|
|
2803
|
+
unsub?.();
|
|
2804
|
+
clearTimeout(timer);
|
|
2805
|
+
};
|
|
2806
|
+
});
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
1246
2809
|
//#endregion
|
|
2810
|
+
exports.$async = $async;
|
|
1247
2811
|
exports.$batch = $batch;
|
|
2812
|
+
exports.$bind = $bind;
|
|
1248
2813
|
exports.$cache = $cache;
|
|
1249
|
-
exports.$
|
|
2814
|
+
exports.$onmount = $onmount;
|
|
1250
2815
|
exports.$peek = $peek;
|
|
2816
|
+
exports.$pending = $pending;
|
|
2817
|
+
exports.$refresh = $refresh;
|
|
1251
2818
|
exports.$run = $run;
|
|
1252
|
-
exports.$
|
|
2819
|
+
exports.$stream = $stream;
|
|
2820
|
+
exports.$unwrap = require_fromWebSocket.$unwrap;
|
|
1253
2821
|
exports.$watch = $watch;
|
|
1254
|
-
exports.ReactiveDate = ReactiveDate;
|
|
1255
2822
|
exports.clearLayoutSlot = clearLayoutSlot;
|
|
1256
2823
|
exports.fillLayoutSlot = fillLayoutSlot;
|
|
2824
|
+
exports.fromElement = require_fromWebSocket.fromElement;
|
|
2825
|
+
exports.fromServer = require_fromWebSocket.fromServer;
|
|
2826
|
+
exports.fromWebSocket = require_fromWebSocket.fromWebSocket;
|
|
1257
2827
|
exports.hydrate = hydrate;
|
|
1258
2828
|
exports.mount = mount;
|
|
2829
|
+
exports.t_add_element = addElement;
|
|
1259
2830
|
exports.t_add_fragment = addFragment;
|
|
1260
2831
|
exports.t_anchor = nodeAnchor;
|
|
1261
2832
|
exports.t_animate = addAnimation;
|
|
1262
2833
|
exports.t_apply_props = applyProps;
|
|
1263
2834
|
exports.t_attribute = setAttribute;
|
|
1264
2835
|
exports.t_child = nodeChild;
|
|
1265
|
-
exports.t_class =
|
|
2836
|
+
exports.t_class = require_fromWebSocket.buildClasses;
|
|
1266
2837
|
exports.t_dynamic = setDynamicElement;
|
|
1267
2838
|
exports.t_event = addEvent;
|
|
1268
|
-
exports.t_fmt =
|
|
2839
|
+
exports.t_fmt = require_fromWebSocket.formatText;
|
|
1269
2840
|
exports.t_fragment = getFragment;
|
|
2841
|
+
exports.t_fragment_el = getElementFragment;
|
|
1270
2842
|
exports.t_list_item = newListItem;
|
|
1271
2843
|
exports.t_next = nodeNext;
|
|
1272
2844
|
exports.t_pop_region = popRegion;
|
|
1273
2845
|
exports.t_push_region = pushRegion;
|
|
1274
2846
|
exports.t_region = newRegion;
|
|
2847
|
+
exports.t_rerun_region_effects = rerunRegionEffects;
|
|
2848
|
+
exports.t_restore_hydration = restoreHydration;
|
|
1275
2849
|
exports.t_root = nodeRoot;
|
|
2850
|
+
exports.t_root_el = nodeRootElement;
|
|
2851
|
+
exports.t_run_await = runAwait;
|
|
1276
2852
|
exports.t_run_branch = runControlBranch;
|
|
1277
2853
|
exports.t_run_control = runControl;
|
|
1278
2854
|
exports.t_run_list = runList;
|
|
2855
|
+
exports.t_run_try = runTry;
|
|
2856
|
+
exports.t_save_hydration = saveHydration;
|
|
1279
2857
|
exports.t_skip = nodeSkip;
|
|
1280
|
-
exports.t_style =
|
|
2858
|
+
exports.t_style = require_fromWebSocket.buildStyles;
|
|
2859
|
+
exports.unmount = unmount;
|