cx 26.7.3 → 26.7.5
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/build/ui/Cx.d.ts +10 -4
- package/build/ui/Cx.d.ts.map +1 -1
- package/build/ui/Cx.js +121 -42
- package/build/ui/batchUpdates.d.ts +2 -2
- package/build/ui/batchUpdates.d.ts.map +1 -1
- package/build/ui/batchUpdates.js +11 -1
- package/dist/manifest.js +820 -814
- package/dist/ui.js +137 -55
- package/package.json +1 -1
- package/src/ui/Cx.spec.tsx +85 -0
- package/src/ui/Cx.tsx +126 -43
- package/src/ui/DataProxy.ts +55 -55
- package/src/ui/Rescope.ts +50 -50
- package/src/ui/Restate.tsx +217 -217
- package/src/ui/batchUpdates.ts +12 -2
- package/src/ui/exprHelpers.ts +96 -96
- package/src/widgets/Sandbox.ts +104 -104
- package/src/widgets/form/ColorField.scss +112 -112
- package/src/widgets/form/DateTimeField.scss +111 -111
- package/src/widgets/form/LookupField.maps.scss +26 -26
- package/src/widgets/form/MonthField.scss +113 -113
- package/src/widgets/form/Select.scss +104 -104
- package/src/widgets/form/TextField.scss +66 -66
- package/src/widgets/nav/MenuItem.tsx +525 -525
package/dist/ui.js
CHANGED
|
@@ -268,6 +268,7 @@ CSS.classPrefix = "cx";
|
|
|
268
268
|
CSSHelper.alias("cx", CSS);
|
|
269
269
|
|
|
270
270
|
let isBatching = 0;
|
|
271
|
+
let updateSequence = 0;
|
|
271
272
|
let promiseSubscribers = new SubscriberList();
|
|
272
273
|
function batchUpdates(callback) {
|
|
273
274
|
if (VDOM$1.DOM.unstable_batchedUpdates)
|
|
@@ -284,14 +285,21 @@ function batchUpdates(callback) {
|
|
|
284
285
|
function isBatchingUpdates() {
|
|
285
286
|
return isBatching > 0;
|
|
286
287
|
}
|
|
288
|
+
// Returns a sequence number identifying the update; pass it to notifyBatchedUpdateCompleted once the
|
|
289
|
+
// update is rendered.
|
|
287
290
|
function notifyBatchedUpdateStarting() {
|
|
291
|
+
let seq = ++updateSequence;
|
|
288
292
|
promiseSubscribers.execute((x) => {
|
|
289
293
|
x.pending++;
|
|
290
294
|
});
|
|
295
|
+
return seq;
|
|
291
296
|
}
|
|
292
|
-
function notifyBatchedUpdateCompleted() {
|
|
297
|
+
function notifyBatchedUpdateCompleted(seq) {
|
|
293
298
|
promiseSubscribers.execute((x) => {
|
|
294
299
|
let cb = x;
|
|
300
|
+
// ignore updates that started before this subscriber attached -- counting them would let the notify
|
|
301
|
+
// callback fire before the updates the subscriber is actually waiting on are rendered
|
|
302
|
+
if (seq <= cb.watermark) return;
|
|
295
303
|
cb.finished++;
|
|
296
304
|
if (cb.finished >= cb.pending) cb.complete(true);
|
|
297
305
|
});
|
|
@@ -303,6 +311,7 @@ function batchUpdatesAndNotify(callback, notifyCallback, timeout = 1000) {
|
|
|
303
311
|
const update = {
|
|
304
312
|
pending: 0,
|
|
305
313
|
finished: 0,
|
|
314
|
+
watermark: updateSequence,
|
|
306
315
|
complete: (success) => {
|
|
307
316
|
if (!done) {
|
|
308
317
|
done = true;
|
|
@@ -2094,6 +2103,36 @@ class LinkedListsNode {
|
|
|
2094
2103
|
}
|
|
2095
2104
|
}
|
|
2096
2105
|
|
|
2106
|
+
// On by default. Once a synchronous update burst grows deep, Cx starts issuing updates from microtasks
|
|
2107
|
+
// so React's global nested-update counter resets before continuing -- preventing "Maximum update depth
|
|
2108
|
+
// exceeded" on large renders that write to the store while they render (e.g. a several-hundred-page
|
|
2109
|
+
// report). For updates that settle in a single render -- virtually all of them -- this is equivalent to
|
|
2110
|
+
// rendering synchronously; it only diverges under a deep re-entrant render burst. If you suspect it
|
|
2111
|
+
// causes trouble, opt out at app startup with disableSyncUpdateCoalescing() -- and please report the
|
|
2112
|
+
// issue so it can be fixed.
|
|
2113
|
+
// Synchronous updates allowed within one burst before Cx switches to issuing updates from a microtask.
|
|
2114
|
+
// Kept under React's ~50 nested-update limit (which is global to the root, not per component); the
|
|
2115
|
+
// default trades a little initial-render time for a safety margin. Override via
|
|
2116
|
+
// enableSyncUpdateCoalescing(limit) if needed.
|
|
2117
|
+
const defaultSyncBurstLimit = 35;
|
|
2118
|
+
let syncBurstLimit = defaultSyncBurstLimit;
|
|
2119
|
+
// Microtask-issued updates allowed within one burst before Cx escalates to setTimeout. Deep enough that
|
|
2120
|
+
// only a store that never converges reaches it; timeouts let the event loop turn, so the page stays
|
|
2121
|
+
// responsive and batchUpdatesAndNotify fallback timers can fire instead of the tab hanging.
|
|
2122
|
+
let microtaskBurstLimit = 1000;
|
|
2123
|
+
function enableSyncUpdateCoalescing(limit) {
|
|
2124
|
+
syncBurstLimit = limit ?? defaultSyncBurstLimit;
|
|
2125
|
+
}
|
|
2126
|
+
function disableSyncUpdateCoalescing() {
|
|
2127
|
+
syncBurstLimit = Infinity;
|
|
2128
|
+
}
|
|
2129
|
+
// Module-global because React's nested-update limit is global to the root, not per component. The burst
|
|
2130
|
+
// counter resets only when all notifications settle (see completeNotification) -- tying the burst
|
|
2131
|
+
// window to unsettled work makes it immune to how React schedules the flushes. Legacy React chains
|
|
2132
|
+
// re-entrant updates synchronously within one task, but React 19 may run each round from its own
|
|
2133
|
+
// microtask, so no task/microtask boundary is a reliable reset point.
|
|
2134
|
+
let syncBurstRounds = 0; // updates issued in the current burst
|
|
2135
|
+
let outstandingNotifications = 0; // reported notifications not yet rendered, across all Cx instances
|
|
2097
2136
|
class Cx extends VDOM.Component {
|
|
2098
2137
|
widget;
|
|
2099
2138
|
store;
|
|
@@ -2102,13 +2141,18 @@ class Cx extends VDOM.Component {
|
|
|
2102
2141
|
flags;
|
|
2103
2142
|
renderCount;
|
|
2104
2143
|
unsubscribe;
|
|
2105
|
-
componentDidCatch;
|
|
2106
2144
|
forceUpdateCallback;
|
|
2107
2145
|
deferCounter;
|
|
2108
2146
|
pendingUpdateTimer;
|
|
2109
2147
|
unsubscribeIdleRequest;
|
|
2110
|
-
//
|
|
2111
|
-
|
|
2148
|
+
// store notifications reported to batchUpdatesAndNotify subscribers but not yet rendered and completed
|
|
2149
|
+
owedNotifications = new Set();
|
|
2150
|
+
// setState is not allowed before the component mounts; pre-mount notifications set flags.dirty instead
|
|
2151
|
+
// and componentDidMount picks them up (see update())
|
|
2152
|
+
mounted = false;
|
|
2153
|
+
// true once this instance has contributed to syncBurstRounds for the current render round; cleared on
|
|
2154
|
+
// render, so bursts are counted per round rather than per notification (see update())
|
|
2155
|
+
burstRoundCounted = false;
|
|
2112
2156
|
constructor(props) {
|
|
2113
2157
|
super(props);
|
|
2114
2158
|
if (props.instance) {
|
|
@@ -2134,13 +2178,11 @@ class Cx extends VDOM.Component {
|
|
|
2134
2178
|
if (props.subscribe) {
|
|
2135
2179
|
this.unsubscribe = this.store.subscribe(this.update.bind(this));
|
|
2136
2180
|
}
|
|
2137
|
-
this.onStateUpdateCompleted = this.onStateUpdateCompleted.bind(this);
|
|
2138
2181
|
this.flags = {};
|
|
2139
2182
|
this.renderCount = 0;
|
|
2140
|
-
if (props.onError) this.componentDidCatch = this.componentDidCatchHandler.bind(this);
|
|
2141
2183
|
this.forceUpdateCallback = this.forceUpdate.bind(this);
|
|
2142
|
-
|
|
2143
|
-
this.
|
|
2184
|
+
// deferredUntilIdle content stays hidden until the idle callback scheduled on mount bumps the token
|
|
2185
|
+
this.deferCounter = props.deferredUntilIdle ? 1 : 0;
|
|
2144
2186
|
}
|
|
2145
2187
|
UNSAFE_componentWillReceiveProps(props) {
|
|
2146
2188
|
let newStore = props.instance ? props.instance.store : props.store ? props.store : props.parentInstance.store;
|
|
@@ -2170,6 +2212,10 @@ class Cx extends VDOM.Component {
|
|
|
2170
2212
|
throw new Error("Could not resolve a widget instance in the Cx component.");
|
|
2171
2213
|
}
|
|
2172
2214
|
render() {
|
|
2215
|
+
this.burstRoundCounted = false;
|
|
2216
|
+
// an error was captured and is being dispatched to the onError callback (see componentDidCatch);
|
|
2217
|
+
// render nothing until the callback repairs the state
|
|
2218
|
+
if (this.state.error) return null;
|
|
2173
2219
|
if (this.props.deferredUntilIdle && this.state.deferToken < this.deferCounter) return null;
|
|
2174
2220
|
let cultureInfo = this.props.cultureInfo ?? getCurrentCulture();
|
|
2175
2221
|
return jsx(CxContext, {
|
|
@@ -2183,6 +2229,10 @@ class Cx extends VDOM.Component {
|
|
|
2183
2229
|
});
|
|
2184
2230
|
}
|
|
2185
2231
|
componentDidMount() {
|
|
2232
|
+
this.mounted = true;
|
|
2233
|
+
// schedule the deferredUntilIdle reveal here rather than in the constructor -- the idle callback
|
|
2234
|
+
// could otherwise fire before the component mounts and issue a setState React does not allow yet
|
|
2235
|
+
this.waitForIdle();
|
|
2186
2236
|
this.componentDidUpdate();
|
|
2187
2237
|
if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(this.update.bind(this));
|
|
2188
2238
|
}
|
|
@@ -2194,32 +2244,68 @@ class Cx extends VDOM.Component {
|
|
|
2194
2244
|
update() {
|
|
2195
2245
|
let data = this.store.getData();
|
|
2196
2246
|
debug(appDataFlag, data);
|
|
2197
|
-
if (this.flags.preparing) this.flags.dirty = true;
|
|
2198
|
-
// Synchronous path
|
|
2199
|
-
//
|
|
2200
|
-
// nested deep -- e.g. a large initial render that writes to the store while it renders -- are what trip
|
|
2201
|
-
// React's "Maximum update depth exceeded"; coalescing them into a single in-flight update avoids that.
|
|
2202
|
-
// The pending update's completion callback re-reads the store and renders again if it changed
|
|
2203
|
-
// (see onStateUpdateCompleted), so no change is dropped.
|
|
2247
|
+
if (this.flags.preparing || !this.mounted) this.flags.dirty = true;
|
|
2248
|
+
// Synchronous path: while batching (incl. batchUpdatesAndNotify, which page-breaking relies on) or for
|
|
2249
|
+
// `immediate` instances.
|
|
2204
2250
|
else if (this.props.immediate || isBatchingUpdates()) {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2251
|
+
// Every notification is reported to batchUpdatesAndNotify subscribers up front and completed only
|
|
2252
|
+
// once the setState that renders it commits, so notify callbacks never fire before the DOM reflects
|
|
2253
|
+
// the change. Each notification always gets its own setState; only its timing escalates as the
|
|
2254
|
+
// burst grows: synchronous at first, then from a microtask (lets React's commit finish so its
|
|
2255
|
+
// global nested-update counter resets instead of tripping "Maximum update depth exceeded"), and
|
|
2256
|
+
// finally from a timeout (lets the event loop turn, so a store that never converges degrades to a
|
|
2257
|
+
// responsive page instead of a frozen tab).
|
|
2258
|
+
let seq = notifyBatchedUpdateStarting();
|
|
2259
|
+
this.owedNotifications.add(seq);
|
|
2260
|
+
outstandingNotifications++;
|
|
2261
|
+
// Count render rounds rather than notifications: only the first notification an instance
|
|
2262
|
+
// receives per render round bumps the shared depth (the flag clears on render). This tracks
|
|
2263
|
+
// React's nested-commit count -- the thing the escalation must stay under -- so a round that
|
|
2264
|
+
// writes many values doesn't burn through the budget in one go.
|
|
2265
|
+
if (!this.burstRoundCounted) {
|
|
2266
|
+
this.burstRoundCounted = true;
|
|
2267
|
+
++syncBurstRounds;
|
|
2268
|
+
if (process.env.NODE_ENV !== "production" && syncBurstRounds === microtaskBurstLimit + 1)
|
|
2269
|
+
console.error(
|
|
2270
|
+
"Cx: store updates are not converging after " +
|
|
2271
|
+
microtaskBurstLimit +
|
|
2272
|
+
" render rounds -- possible update loop. Updates are now issued from timeouts to keep the page responsive. Look for code that writes a new value to the store on every render.",
|
|
2273
|
+
);
|
|
2214
2274
|
}
|
|
2275
|
+
if (syncBurstRounds <= syncBurstLimit) this.issueStateUpdate(seq);
|
|
2276
|
+
else if (syncBurstRounds <= microtaskBurstLimit) queueMicrotask(() => this.issueStateUpdate(seq));
|
|
2277
|
+
else setTimeout(() => this.issueStateUpdate(seq), 0);
|
|
2215
2278
|
} else {
|
|
2216
2279
|
// standard mode: coalesce sequential store commands into a single deferred update
|
|
2217
2280
|
this.scheduleStateUpdate();
|
|
2218
2281
|
}
|
|
2219
2282
|
}
|
|
2283
|
+
// Render the latest store data and report the notification completed once the commit is done, so
|
|
2284
|
+
// batchUpdatesAndNotify resolves only when the DOM reflects the store.
|
|
2285
|
+
issueStateUpdate(seq) {
|
|
2286
|
+
// skip notifications no longer owed -- unmount may have already released them
|
|
2287
|
+
if (!this.owedNotifications.has(seq)) return;
|
|
2288
|
+
this.setState(
|
|
2289
|
+
{
|
|
2290
|
+
data: this.store.getData(),
|
|
2291
|
+
},
|
|
2292
|
+
() => this.completeNotification(seq),
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
completeNotification(seq) {
|
|
2296
|
+
// skip notifications no longer owed -- unmount may have already released them
|
|
2297
|
+
if (!this.owedNotifications.delete(seq)) return;
|
|
2298
|
+
outstandingNotifications--;
|
|
2299
|
+
notifyBatchedUpdateCompleted(seq);
|
|
2300
|
+
// everything settled -- the next burst starts fresh, and so does React's nested-update counter
|
|
2301
|
+
// (the commit that settled the last notification ends without scheduling further synchronous work)
|
|
2302
|
+
if (outstandingNotifications === 0) syncBurstRounds = 0;
|
|
2303
|
+
}
|
|
2220
2304
|
scheduleStateUpdate() {
|
|
2221
2305
|
if (!this.pendingUpdateTimer) {
|
|
2222
|
-
notifyBatchedUpdateStarting();
|
|
2306
|
+
let seq = notifyBatchedUpdateStarting();
|
|
2307
|
+
this.owedNotifications.add(seq);
|
|
2308
|
+
outstandingNotifications++;
|
|
2223
2309
|
this.pendingUpdateTimer = setTimeout(() => {
|
|
2224
2310
|
delete this.pendingUpdateTimer;
|
|
2225
2311
|
// read fresh data at fire time so the coalesced update renders the latest store state
|
|
@@ -2227,45 +2313,20 @@ class Cx extends VDOM.Component {
|
|
|
2227
2313
|
{
|
|
2228
2314
|
data: this.store.getData(),
|
|
2229
2315
|
},
|
|
2230
|
-
|
|
2316
|
+
() => this.completeNotification(seq),
|
|
2231
2317
|
);
|
|
2232
2318
|
}, 0);
|
|
2233
2319
|
}
|
|
2234
2320
|
}
|
|
2235
|
-
// Completion callback for the synchronous setState above. If the store changed while that update was
|
|
2236
|
-
// rendering -- i.e. the render itself wrote to the store, as happens throughout a large initial render --
|
|
2237
|
-
// render once more with the latest data, keeping this callback armed; otherwise the burst has settled, so
|
|
2238
|
-
// clear the in-flight flag and report completion. Re-arming coalesces until the store reaches a fixpoint;
|
|
2239
|
-
// a genuinely non-converging update loop is still caught by React's own max-update-depth guard.
|
|
2240
|
-
onStateUpdateCompleted() {
|
|
2241
|
-
let latestData = this.store.getData();
|
|
2242
|
-
notifyBatchedUpdateCompleted();
|
|
2243
|
-
if (this.state.data === latestData) {
|
|
2244
|
-
this.stateUpdateDepth = 0;
|
|
2245
|
-
} else {
|
|
2246
|
-
this.stateUpdateDepth++;
|
|
2247
|
-
if (this.stateUpdateDepth < 20 && isBatchingUpdates())
|
|
2248
|
-
this.setState(
|
|
2249
|
-
{
|
|
2250
|
-
data: latestData,
|
|
2251
|
-
},
|
|
2252
|
-
this.onStateUpdateCompleted,
|
|
2253
|
-
);
|
|
2254
|
-
else this.scheduleStateUpdate();
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
2321
|
waitForIdle() {
|
|
2258
2322
|
if (!this.props.deferredUntilIdle) return;
|
|
2259
2323
|
if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
|
|
2260
2324
|
let token = ++this.deferCounter;
|
|
2261
2325
|
this.unsubscribeIdleRequest = onIdleCallback(
|
|
2262
2326
|
() => {
|
|
2263
|
-
this.setState(
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
},
|
|
2267
|
-
this.onStateUpdateCompleted,
|
|
2268
|
-
);
|
|
2327
|
+
this.setState({
|
|
2328
|
+
deferToken: token,
|
|
2329
|
+
});
|
|
2269
2330
|
},
|
|
2270
2331
|
{
|
|
2271
2332
|
timeout: this.props.idleTimeout || 30000,
|
|
@@ -2273,6 +2334,9 @@ class Cx extends VDOM.Component {
|
|
|
2273
2334
|
);
|
|
2274
2335
|
}
|
|
2275
2336
|
componentWillUnmount() {
|
|
2337
|
+
// Release notifications that will never render so a waiting batchUpdatesAndNotify can settle instead
|
|
2338
|
+
// of waiting out its fallback timeout.
|
|
2339
|
+
for (let seq of this.owedNotifications) this.completeNotification(seq);
|
|
2276
2340
|
if (this.pendingUpdateTimer) clearTimeout(this.pendingUpdateTimer);
|
|
2277
2341
|
if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
|
|
2278
2342
|
if (this.unsubscribe) this.unsubscribe();
|
|
@@ -2291,9 +2355,25 @@ class Cx extends VDOM.Component {
|
|
|
2291
2355
|
props.cultureInfo !== this.props.cultureInfo
|
|
2292
2356
|
);
|
|
2293
2357
|
}
|
|
2294
|
-
|
|
2358
|
+
// Render null for the failed subtree while the error is dispatched to componentDidCatch below --
|
|
2359
|
+
// returning a state update here is what React expects of an error boundary (rendering the broken
|
|
2360
|
+
// children again would just rethrow).
|
|
2361
|
+
static getDerivedStateFromError() {
|
|
2362
|
+
return {
|
|
2363
|
+
error: true,
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
componentDidCatch(error, info) {
|
|
2295
2367
|
this.flags.preparing = false;
|
|
2368
|
+
// without an onError callback this instance is not an error boundary -- rethrow so the error
|
|
2369
|
+
// reaches the nearest ancestor boundary, matching the behavior before getDerivedStateFromError
|
|
2370
|
+
// was introduced
|
|
2371
|
+
if (!this.props.onError) throw error;
|
|
2296
2372
|
this.props.onError(error, this.getInstance(), info);
|
|
2373
|
+
// the callback had a chance to repair the state (e.g. replace the failing content) -- resume rendering
|
|
2374
|
+
this.setState({
|
|
2375
|
+
error: false,
|
|
2376
|
+
});
|
|
2297
2377
|
}
|
|
2298
2378
|
}
|
|
2299
2379
|
class CxContext extends VDOM.Component {
|
|
@@ -4388,7 +4468,9 @@ export {
|
|
|
4388
4468
|
contentAppend,
|
|
4389
4469
|
createCulture,
|
|
4390
4470
|
createFunctionalComponent,
|
|
4471
|
+
disableSyncUpdateCoalescing,
|
|
4391
4472
|
enableCultureSensitiveFormatting,
|
|
4473
|
+
enableSyncUpdateCoalescing,
|
|
4392
4474
|
equal,
|
|
4393
4475
|
executeKeyboardShortcuts,
|
|
4394
4476
|
exploreChildren,
|
package/package.json
CHANGED
package/src/ui/Cx.spec.tsx
CHANGED
|
@@ -6,6 +6,28 @@ import { bind } from "./bind";
|
|
|
6
6
|
import { createTestRenderer, createTestWidget, act } from "../util/test/createTestRenderer";
|
|
7
7
|
import assert from "assert";
|
|
8
8
|
import { HtmlElement } from "../widgets/HtmlElement";
|
|
9
|
+
import { batchUpdatesAndNotify } from "./batchUpdates";
|
|
10
|
+
|
|
11
|
+
// A widget that, each time it explores, bumps `n` by one until it reaches `target`. This reproduces the
|
|
12
|
+
// shape of a large report's initial render: the render writes to the store, which schedules another
|
|
13
|
+
// render, until the store reaches a fixpoint. It drives Cx's coalescing update path across many rounds.
|
|
14
|
+
function convergingWidget(target: number) {
|
|
15
|
+
return (
|
|
16
|
+
<cx>
|
|
17
|
+
<div
|
|
18
|
+
text={bind("n")}
|
|
19
|
+
onExplore={(context: any, instance: any) => {
|
|
20
|
+
let n = instance.store.get("n");
|
|
21
|
+
if (n < target) instance.store.set("n", n + 1);
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
</cx>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderedValue(component: any): number {
|
|
29
|
+
return Number(component.toJSON().children[0]);
|
|
30
|
+
}
|
|
9
31
|
|
|
10
32
|
describe("Cx", () => {
|
|
11
33
|
it("can render cx content", async () => {
|
|
@@ -207,4 +229,67 @@ describe("Cx", () => {
|
|
|
207
229
|
["render", "0"],
|
|
208
230
|
]);
|
|
209
231
|
});
|
|
232
|
+
|
|
233
|
+
// Coalescing of re-entrant synchronous updates is the default. These cover the two guarantees that
|
|
234
|
+
// matter for large, store-mutating-during-render trees (e.g. report page-breaking).
|
|
235
|
+
|
|
236
|
+
it("resolves batchUpdatesAndNotify only after the store reaches its fixpoint", async () => {
|
|
237
|
+
const TARGET = 6; // shallow, like page-breaking convergence -> stays fully synchronous under a notifier
|
|
238
|
+
let store = new Store({ data: { n: 0 } });
|
|
239
|
+
const component = await createTestRenderer(store, convergingWidget(TARGET));
|
|
240
|
+
assert.equal(renderedValue(component), TARGET);
|
|
241
|
+
|
|
242
|
+
let notifiedStoreValue = -1;
|
|
243
|
+
let notifiedRenderedValue = -1;
|
|
244
|
+
await act(async () => {
|
|
245
|
+
batchUpdatesAndNotify(
|
|
246
|
+
() => {
|
|
247
|
+
store.set("n", 0); // reset -> the render cascade re-converges back up to TARGET
|
|
248
|
+
},
|
|
249
|
+
() => {
|
|
250
|
+
notifiedStoreValue = store.get("n");
|
|
251
|
+
notifiedRenderedValue = renderedValue(component);
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// The notify callback is what page-breaking relies on: it must run only once the change is fully
|
|
257
|
+
// implemented -- both the store and the rendered DOM are at the final value, never an intermediate.
|
|
258
|
+
assert.equal(notifiedStoreValue, TARGET, "notify saw a pre-fixpoint store value");
|
|
259
|
+
assert.equal(notifiedRenderedValue, TARGET, "notify fired before the DOM reflected the final value");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("does not fire the notify callback early when an update is already in flight", async () => {
|
|
263
|
+
const TARGET = 50;
|
|
264
|
+
let store = new Store({ data: { n: 0 } });
|
|
265
|
+
const component = await createTestRenderer(store, convergingWidget(TARGET));
|
|
266
|
+
assert.equal(renderedValue(component), TARGET);
|
|
267
|
+
|
|
268
|
+
let notifiedStoreValue = -1;
|
|
269
|
+
let notifiedRenderedValue = -1;
|
|
270
|
+
await act(async () => {
|
|
271
|
+
store.set("n", 0); // puts an update in flight before the notifier below subscribes
|
|
272
|
+
batchUpdatesAndNotify(
|
|
273
|
+
() => {
|
|
274
|
+
store.set("n", 1);
|
|
275
|
+
},
|
|
276
|
+
() => {
|
|
277
|
+
notifiedStoreValue = store.get("n");
|
|
278
|
+
notifiedRenderedValue = renderedValue(component);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// The write issued inside batchUpdatesAndNotify must be rendered before the notify callback runs,
|
|
284
|
+
// even though it landed while a previous update was still converging.
|
|
285
|
+
assert.equal(notifiedStoreValue, TARGET, "notify saw a pre-fixpoint store value");
|
|
286
|
+
assert.equal(notifiedRenderedValue, TARGET, "notify fired before the DOM reflected the final value");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("converges a deep render burst without tripping React's max-update-depth guard", async () => {
|
|
290
|
+
const TARGET = 200; // far beyond React's ~50 nested-update limit; only survives because of the yield
|
|
291
|
+
let store = new Store({ data: { n: 0 } });
|
|
292
|
+
const component = await createTestRenderer(store, convergingWidget(TARGET));
|
|
293
|
+
assert.equal(renderedValue(component), TARGET);
|
|
294
|
+
});
|
|
210
295
|
});
|