cx 26.7.2 → 26.7.4

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/src/ui/Cx.tsx CHANGED
@@ -1,416 +1,492 @@
1
- /** @jsxImportSource react */
2
-
3
- import { Widget, VDOM, getContent } from "./Widget";
4
- import { Instance } from "./Instance";
5
- import { RenderingContext } from "./RenderingContext";
6
- import { debug, appDataFlag } from "../util/Debug";
7
- import { Timing, now, appLoopFlag, vdomRenderFlag } from "../util/Timing";
8
- import { isBatchingUpdates, notifyBatchedUpdateStarting, notifyBatchedUpdateCompleted } from "./batchUpdates";
9
- import { shallowEquals } from "../util/shallowEquals";
10
- import { PureContainer } from "./PureContainer";
11
- import { onIdleCallback } from "../util/onIdleCallback";
12
- import { getCurrentCulture, pushCulture, popCulture, CultureInfo, ResolvedCultureInfo } from "./Culture";
13
- import { View } from "../data/View";
14
- import { Config } from "./Prop";
15
-
16
- export interface CxProps {
17
- widget?: Config;
18
- items?: Config;
19
- store?: View;
20
- instance?: Instance;
21
- parentInstance?: Instance;
22
- subscribe?: boolean;
23
- immediate?: boolean;
24
- deferredUntilIdle?: boolean;
25
- idleTimeout?: number;
26
- options?: any;
27
- onError?: (error: Error, instance: Instance, info: any) => void;
28
- params?: any;
29
- contentFactory?: (props: { children: any }) => any;
30
- cultureInfo?: ResolvedCultureInfo;
31
- }
32
-
33
- export interface CxState {
34
- deferToken: number;
35
- data?: any;
36
- }
37
-
38
- export class Cx extends VDOM.Component<CxProps, CxState> {
39
- widget: Widget;
40
- store: View;
41
- parentInstance?: Instance;
42
- instance?: Instance;
43
- flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
44
- renderCount: number;
45
- unsubscribe?: () => void;
46
- componentDidCatch?: (error: Error, info: any) => void;
47
- forceUpdateCallback: () => void;
48
- deferCounter: number;
49
- pendingUpdateTimer?: NodeJS.Timeout;
50
- unsubscribeIdleRequest?: () => void;
51
- // 0 when no synchronous setState is in flight; >0 while one is pending (re-entrancy guard for update())
52
- stateUpdateDepth: number = 0;
53
-
54
- constructor(props: CxProps) {
55
- super(props);
56
-
57
- if (props.instance) {
58
- this.widget = (props.instance as any).widget;
59
- this.store = (props.instance as any).store;
60
- } else {
61
- this.widget = PureContainer.create({ items: props.widget || props.items });
62
-
63
- if (props.parentInstance) {
64
- this.parentInstance = props.parentInstance;
65
- this.store = props.store || (this.parentInstance as any).store;
66
- } else {
67
- this.parentInstance = new Instance(this.widget, "0", undefined, props.store);
68
- this.store = props.store!;
69
- }
70
-
71
- if (!this.store) throw new Error("Cx component requires a store.");
72
- }
73
-
74
- this.state = {
75
- deferToken: 0,
76
- };
77
-
78
- if (props.subscribe) {
79
- this.unsubscribe = this.store.subscribe(this.update.bind(this));
80
- (this.state as any).data = this.store.getData();
81
- }
82
-
83
- this.onStateUpdateCompleted = this.onStateUpdateCompleted.bind(this);
84
-
85
- this.flags = {};
86
- this.renderCount = 0;
87
-
88
- if (props.onError) this.componentDidCatch = this.componentDidCatchHandler.bind(this);
89
-
90
- this.forceUpdateCallback = this.forceUpdate.bind(this);
91
-
92
- this.deferCounter = 0;
93
- this.waitForIdle();
94
- }
95
-
96
- UNSAFE_componentWillReceiveProps(props: CxProps): void {
97
- let newStore = props.instance
98
- ? (props.instance as any).store
99
- : props.store
100
- ? props.store
101
- : (props.parentInstance as any).store;
102
-
103
- if (newStore != this.store) {
104
- this.store = newStore;
105
- if (this.unsubscribe) this.unsubscribe();
106
- if (props.subscribe) this.unsubscribe = this.store.subscribe(this.update.bind(this));
107
- }
108
-
109
- if (props.subscribe) {
110
- let data = this.store.getData();
111
- if (data !== this.state.data) {
112
- this.waitForIdle();
113
- this.setState({ data });
114
- }
115
- }
116
- }
117
-
118
- getInstance(): Instance {
119
- if (this.props.instance) return this.props.instance;
120
-
121
- if (this.instance && this.instance.widget === this.widget) {
122
- if (this.instance.parentStore != this.store) this.instance.setParentStore(this.store);
123
- return this.instance;
124
- }
125
-
126
- if (this.widget && this.parentInstance)
127
- return (this.instance = this.parentInstance.getDetachedChild(this.widget, "0", this.store));
128
-
129
- throw new Error("Could not resolve a widget instance in the Cx component.");
130
- }
131
-
132
- render() {
133
- if (this.props.deferredUntilIdle && this.state.deferToken < this.deferCounter) return null;
134
-
135
- let cultureInfo = this.props.cultureInfo ?? getCurrentCulture();
136
-
137
- return (
138
- <CxContext
139
- instance={this.getInstance()}
140
- flags={this.flags}
141
- options={this.props.options}
142
- buster={++this.renderCount}
143
- contentFactory={this.props.contentFactory}
144
- forceUpdate={this.forceUpdateCallback}
145
- cultureInfo={cultureInfo}
146
- />
147
- );
148
- }
149
-
150
- componentDidMount(): void {
151
- this.componentDidUpdate();
152
-
153
- if (this.props.options && this.props.options.onPipeUpdate)
154
- this.props.options.onPipeUpdate(this.update.bind(this));
155
- }
156
-
157
- componentDidUpdate(): void {
158
- if (this.flags.dirty) {
159
- this.update();
160
- }
161
- }
162
-
163
- update(): void {
164
- let data = this.store.getData();
165
- debug(appDataFlag, data);
166
- if (this.flags.preparing) this.flags.dirty = true;
167
- // Synchronous path (while batching, or for `immediate` instances). At most one setState may be in flight
168
- // per Cx: if one is already pending (stateUpdateDepth != 0) skip issuing another. Re-entrant setStates
169
- // nested deep -- e.g. a large initial render that writes to the store while it renders -- are what trip
170
- // React's "Maximum update depth exceeded"; coalescing them into a single in-flight update avoids that.
171
- // The pending update's completion callback re-reads the store and renders again if it changed
172
- // (see onStateUpdateCompleted), so no change is dropped.
173
- else if (this.props.immediate || isBatchingUpdates()) {
174
- if (this.stateUpdateDepth == 0) {
175
- this.stateUpdateDepth = 1;
176
- notifyBatchedUpdateStarting();
177
- this.setState({ data: data }, this.onStateUpdateCompleted);
178
- }
179
- } else {
180
- // standard mode: coalesce sequential store commands into a single deferred update
181
- if (!this.pendingUpdateTimer) {
182
- notifyBatchedUpdateStarting();
183
- this.pendingUpdateTimer = setTimeout(() => {
184
- delete this.pendingUpdateTimer;
185
- // read fresh data at fire time so the coalesced update renders the latest store state
186
- this.setState({ data: this.store.getData() }, notifyBatchedUpdateCompleted);
187
- }, 0);
188
- }
189
- }
190
- }
191
-
192
- // Completion callback for the synchronous setState above. If the store changed while that update was
193
- // rendering -- i.e. the render itself wrote to the store, as happens throughout a large initial render --
194
- // render once more with the latest data, keeping this callback armed; otherwise the burst has settled, so
195
- // clear the in-flight flag and report completion. Re-arming coalesces until the store reaches a fixpoint;
196
- // a genuinely non-converging update loop is still caught by React's own max-update-depth guard.
197
- onStateUpdateCompleted() {
198
- let latestData = this.store.getData();
199
- if (this.state.data === latestData) {
200
- this.stateUpdateDepth = 0;
201
- notifyBatchedUpdateCompleted();
202
- } else {
203
- this.stateUpdateDepth++;
204
- this.setState({ data: latestData }, this.onStateUpdateCompleted);
205
- }
206
- }
207
-
208
- waitForIdle(): void {
209
- if (!this.props.deferredUntilIdle) return;
210
-
211
- if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
212
-
213
- let token = ++this.deferCounter;
214
- this.unsubscribeIdleRequest = onIdleCallback(
215
- () => {
216
- this.setState({ deferToken: token }, this.onStateUpdateCompleted);
217
- },
218
- {
219
- timeout: this.props.idleTimeout || 30000,
220
- },
221
- );
222
- }
223
-
224
- componentWillUnmount(): void {
225
- if (this.pendingUpdateTimer) clearTimeout(this.pendingUpdateTimer);
226
- if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
227
- if (this.unsubscribe) this.unsubscribe();
228
- if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(null);
229
- }
230
-
231
- shouldComponentUpdate(props: CxProps, state: CxState): boolean {
232
- if (props.deferredUntilIdle && state.deferToken != this.deferCounter) return false;
233
-
234
- return (
235
- state !== this.state ||
236
- !props.params ||
237
- !shallowEquals(props.params, this.props.params) ||
238
- props.instance !== this.props.instance ||
239
- props.widget !== this.props.widget ||
240
- props.store !== this.props.store ||
241
- props.parentInstance !== this.props.parentInstance ||
242
- props.cultureInfo !== this.props.cultureInfo
243
- );
244
- }
245
-
246
- componentDidCatchHandler(error: Error, info: any): void {
247
- this.flags.preparing = false;
248
- this.props.onError!(error, this.getInstance(), info);
249
- }
250
- }
251
-
252
- interface CxContextProps {
253
- instance: Instance;
254
- flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
255
- options?: any;
256
- buster: number;
257
- contentFactory?: (props: { children: any }) => any;
258
- forceUpdate: () => void;
259
- cultureInfo?: ResolvedCultureInfo;
260
- }
261
-
262
- class CxContext extends VDOM.Component<CxContextProps, {}> {
263
- renderCount: number;
264
- timings: any;
265
- content: any;
266
- renderingContext?: RenderingContext;
267
-
268
- constructor(props: CxContextProps) {
269
- super(props);
270
- this.renderCount = 0;
271
- this.UNSAFE_componentWillReceiveProps(props);
272
- }
273
-
274
- UNSAFE_componentWillReceiveProps(props: CxContextProps): void {
275
- this.timings = {
276
- start: now(),
277
- };
278
-
279
- let { instance, options, contentFactory } = props;
280
- let count = 0,
281
- visible,
282
- context,
283
- forceContinue;
284
-
285
- //should not be tracked by parents for destroy
286
- if (!(instance as any).detached)
287
- throw new Error("The instance passed to a Cx component should be detached from its parent.");
288
-
289
- if (this.props.instance !== instance && (this.props.instance as any).destroyTracked)
290
- (this.props.instance as any).destroy();
291
-
292
- this.props.flags.preparing = true;
293
-
294
- if (this.props.cultureInfo) pushCulture(this.props.cultureInfo);
295
-
296
- try {
297
- do {
298
- count++;
299
- forceContinue = false;
300
- context = new RenderingContext(options);
301
- (context as any).forceUpdate = this.props.forceUpdate;
302
- this.props.flags.dirty = false;
303
- (instance as any).assignedRenderList = (context as any).getRootRenderList();
304
- visible = (instance as any).scheduleExploreIfVisible(context);
305
- if (visible) {
306
- while (!(context as any).exploreStack.empty()) {
307
- let inst = (context as any).exploreStack.pop();
308
- //console.log("EXPLORE", inst.widget.constructor.name, inst.widget.tag, inst.widget.widgetId);
309
- (inst as any).explore(context);
310
- }
311
- } else if ((instance as any).destroyTracked) {
312
- (instance as any).destroy();
313
- }
314
-
315
- if (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8) {
316
- forceContinue = true;
317
- continue;
318
- }
319
-
320
- if (visible) {
321
- this.timings.afterExplore = now();
322
-
323
- for (let i = 0; i < (context as any).prepareList.length; i++)
324
- (context as any).prepareList[i].prepare(context);
325
- this.timings.afterPrepare = now();
326
- }
327
- } while (
328
- forceContinue ||
329
- (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8)
330
- );
331
-
332
- if (visible) {
333
- //walk in reverse order so children get rendered first
334
- let renderList = (context as any).getRootRenderList();
335
- while (renderList) {
336
- for (let i = renderList.data.length - 1; i >= 0; i--) {
337
- renderList.data[i].render(context);
338
- }
339
- renderList = renderList.right;
340
- }
341
-
342
- this.content = getContent((instance as any).vdom);
343
- if (contentFactory) this.content = contentFactory({ children: this.content });
344
- this.timings.afterRender = now();
345
- for (let i = 0; i < (context as any).cleanupList.length; i++)
346
- (context as any).cleanupList[i].cleanup(context);
347
- } else {
348
- this.content = null;
349
- this.timings.afterExplore = this.timings.afterPrepare = this.timings.afterRender = now();
350
- }
351
- } finally {
352
- if (this.props.cultureInfo) popCulture(this.props.cultureInfo);
353
- }
354
-
355
- this.timings.beforeVDOMRender = now();
356
- this.props.flags.preparing = false;
357
- this.props.flags.rendering = true;
358
- this.renderingContext = context;
359
- }
360
-
361
- render() {
362
- return this.content;
363
- }
364
-
365
- componentDidMount(): void {
366
- this.componentDidUpdate();
367
- }
368
-
369
- componentDidUpdate(): void {
370
- this.props.flags.rendering = false;
371
- this.timings.afterVDOMRender = now();
372
-
373
- //let {instance} = this.props;
374
- //instance.cleanup(this.renderingContext);
375
-
376
- this.timings.afterCleanup = now();
377
- this.renderCount++;
378
-
379
- if (process.env.NODE_ENV !== "production") {
380
- let { start, beforeVDOMRender, afterVDOMRender, afterPrepare, afterExplore, afterRender, afterCleanup } =
381
- this.timings;
382
-
383
- Timing.log(
384
- vdomRenderFlag,
385
- this.renderCount,
386
- "cx",
387
- (beforeVDOMRender - start + afterCleanup - afterVDOMRender).toFixed(2) + "ms",
388
- "vdom",
389
- (afterVDOMRender - beforeVDOMRender).toFixed(2) + "ms",
390
- );
391
-
392
- Timing.log(
393
- appLoopFlag,
394
- this.renderCount,
395
- (this.renderingContext as any).options.name || "main",
396
- "total",
397
- (afterCleanup - start).toFixed(1) + "ms",
398
- "explore",
399
- (afterExplore - start).toFixed(1) + "ms",
400
- "prepare",
401
- (afterPrepare - afterExplore).toFixed(1),
402
- "render",
403
- (afterRender - afterPrepare).toFixed(1),
404
- "vdom",
405
- (afterVDOMRender - beforeVDOMRender).toFixed(1),
406
- "cleanup",
407
- (afterCleanup - afterVDOMRender).toFixed(1),
408
- );
409
- }
410
- }
411
-
412
- componentWillUnmount(): void {
413
- let { instance } = this.props;
414
- if ((instance as any).destroyTracked) (instance as any).destroy();
415
- }
416
- }
1
+ /** @jsxImportSource react */
2
+
3
+ import { Widget, VDOM, getContent } from "./Widget";
4
+ import { Instance } from "./Instance";
5
+ import { RenderingContext } from "./RenderingContext";
6
+ import { debug, appDataFlag } from "../util/Debug";
7
+ import { Timing, now, appLoopFlag, vdomRenderFlag } from "../util/Timing";
8
+ import {
9
+ isBatchingUpdates,
10
+ notifyBatchedUpdateStarting,
11
+ notifyBatchedUpdateCompleted,
12
+ hasBatchedUpdateSubscribers,
13
+ } from "./batchUpdates";
14
+ import { shallowEquals } from "../util/shallowEquals";
15
+ import { PureContainer } from "./PureContainer";
16
+ import { onIdleCallback } from "../util/onIdleCallback";
17
+ import { getCurrentCulture, pushCulture, popCulture, CultureInfo, ResolvedCultureInfo } from "./Culture";
18
+ import { View } from "../data/View";
19
+ import { Config } from "./Prop";
20
+
21
+ // On by default. Cx coalesces re-entrant synchronous updates and, once a burst grows deep, yields to a
22
+ // microtask so React's global per-root nested-update counter resets before continuing -- preventing
23
+ // "Maximum update depth exceeded" on large renders that write to the store while they render (e.g. a
24
+ // several-hundred-page report). For updates that settle in a single render -- virtually all of them --
25
+ // this is equivalent to the previous behavior; it only diverges under a deep re-entrant render burst.
26
+ // If you suspect it causes trouble, opt out at app startup with disableSyncUpdateCoalescing() -- and
27
+ // please report the issue so it can be fixed.
28
+ let coalesceSyncUpdates = true;
29
+
30
+ // Consecutive commit-phase re-render rounds allowed before Cx yields to a microtask. Kept under React's
31
+ // ~50 nested-update limit (which is global to the root, not per component); the default trades a little
32
+ // initial-render time for a safety margin. Override via enableSyncUpdateCoalescing(limit) if needed.
33
+ let syncBurstLimit = 35;
34
+
35
+ export function enableSyncUpdateCoalescing(limit?: number): void {
36
+ coalesceSyncUpdates = true;
37
+ if (limit != null) syncBurstLimit = limit;
38
+ }
39
+ export function disableSyncUpdateCoalescing(): void {
40
+ coalesceSyncUpdates = false;
41
+ }
42
+
43
+ // Module-global because React's nested-update limit is global to the root, not per component. A large
44
+ // initial render can re-render the root Cx and every detached page Restate hundreds of times in one
45
+ // synchronous burst; once the burst grows past syncBurstLimit we yield so React's commit finishes
46
+ // without a synchronously-scheduled follow-up (which resets its counter) before we render again.
47
+ let activeSyncUpdates = 0; // Cx instances with a synchronous setState in flight
48
+ let syncBurstRounds = 0; // commit-phase re-render rounds issued since the last yield / burst start
49
+
50
+ export interface CxProps {
51
+ widget?: Config;
52
+ items?: Config;
53
+ store?: View;
54
+ instance?: Instance;
55
+ parentInstance?: Instance;
56
+ subscribe?: boolean;
57
+ immediate?: boolean;
58
+ deferredUntilIdle?: boolean;
59
+ idleTimeout?: number;
60
+ options?: any;
61
+ onError?: (error: Error, instance: Instance, info: any) => void;
62
+ params?: any;
63
+ contentFactory?: (props: { children: any }) => any;
64
+ cultureInfo?: ResolvedCultureInfo;
65
+ }
66
+
67
+ export interface CxState {
68
+ deferToken: number;
69
+ data?: any;
70
+ }
71
+
72
+ export class Cx extends VDOM.Component<CxProps, CxState> {
73
+ widget: Widget;
74
+ store: View;
75
+ parentInstance?: Instance;
76
+ instance?: Instance;
77
+ flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
78
+ renderCount: number;
79
+ unsubscribe?: () => void;
80
+ componentDidCatch?: (error: Error, info: any) => void;
81
+ forceUpdateCallback: () => void;
82
+ deferCounter: number;
83
+ pendingUpdateTimer?: NodeJS.Timeout;
84
+ unsubscribeIdleRequest?: () => void;
85
+ // true while a coalesced synchronous setState is in flight for this Cx (re-entrancy guard for update());
86
+ // only used when coalesceSyncUpdates is enabled
87
+ stateUpdateInFlight: boolean = false;
88
+
89
+ constructor(props: CxProps) {
90
+ super(props);
91
+
92
+ if (props.instance) {
93
+ this.widget = (props.instance as any).widget;
94
+ this.store = (props.instance as any).store;
95
+ } else {
96
+ this.widget = PureContainer.create({ items: props.widget || props.items });
97
+
98
+ if (props.parentInstance) {
99
+ this.parentInstance = props.parentInstance;
100
+ this.store = props.store || (this.parentInstance as any).store;
101
+ } else {
102
+ this.parentInstance = new Instance(this.widget, "0", undefined, props.store);
103
+ this.store = props.store!;
104
+ }
105
+
106
+ if (!this.store) throw new Error("Cx component requires a store.");
107
+ }
108
+
109
+ this.state = {
110
+ deferToken: 0,
111
+ data: props.subscribe ? this.store.getData() : null,
112
+ };
113
+
114
+ if (props.subscribe) {
115
+ this.unsubscribe = this.store.subscribe(this.update.bind(this));
116
+ }
117
+
118
+ this.onStateUpdateCompleted = this.onStateUpdateCompleted.bind(this);
119
+
120
+ this.flags = {};
121
+ this.renderCount = 0;
122
+
123
+ if (props.onError) this.componentDidCatch = this.componentDidCatchHandler.bind(this);
124
+
125
+ this.forceUpdateCallback = this.forceUpdate.bind(this);
126
+
127
+ this.deferCounter = 0;
128
+ this.waitForIdle();
129
+ }
130
+
131
+ UNSAFE_componentWillReceiveProps(props: CxProps): void {
132
+ let newStore = props.instance
133
+ ? (props.instance as any).store
134
+ : props.store
135
+ ? props.store
136
+ : (props.parentInstance as any).store;
137
+
138
+ if (newStore != this.store) {
139
+ this.store = newStore;
140
+ if (this.unsubscribe) this.unsubscribe();
141
+ if (props.subscribe) this.unsubscribe = this.store.subscribe(this.update.bind(this));
142
+ }
143
+
144
+ if (props.subscribe) {
145
+ let data = this.store.getData();
146
+ if (data !== this.state.data) {
147
+ this.waitForIdle();
148
+ this.setState({ data });
149
+ }
150
+ }
151
+ }
152
+
153
+ getInstance(): Instance {
154
+ if (this.props.instance) return this.props.instance;
155
+
156
+ if (this.instance && this.instance.widget === this.widget) {
157
+ if (this.instance.parentStore != this.store) this.instance.setParentStore(this.store);
158
+ return this.instance;
159
+ }
160
+
161
+ if (this.widget && this.parentInstance)
162
+ return (this.instance = this.parentInstance.getDetachedChild(this.widget, "0", this.store));
163
+
164
+ throw new Error("Could not resolve a widget instance in the Cx component.");
165
+ }
166
+
167
+ render() {
168
+ if (this.props.deferredUntilIdle && this.state.deferToken < this.deferCounter) return null;
169
+
170
+ let cultureInfo = this.props.cultureInfo ?? getCurrentCulture();
171
+
172
+ return (
173
+ <CxContext
174
+ instance={this.getInstance()}
175
+ flags={this.flags}
176
+ options={this.props.options}
177
+ buster={++this.renderCount}
178
+ contentFactory={this.props.contentFactory}
179
+ forceUpdate={this.forceUpdateCallback}
180
+ cultureInfo={cultureInfo}
181
+ />
182
+ );
183
+ }
184
+
185
+ componentDidMount(): void {
186
+ this.componentDidUpdate();
187
+
188
+ if (this.props.options && this.props.options.onPipeUpdate)
189
+ this.props.options.onPipeUpdate(this.update.bind(this));
190
+ }
191
+
192
+ componentDidUpdate(): void {
193
+ if (this.flags.dirty) {
194
+ this.update();
195
+ }
196
+ }
197
+
198
+ update(): void {
199
+ let data = this.store.getData();
200
+ debug(appDataFlag, data);
201
+ if (this.flags.preparing) this.flags.dirty = true;
202
+ // Synchronous path: while batching (incl. batchUpdatesAndNotify, which page-breaking relies on) or for
203
+ // `immediate` instances.
204
+ else if (this.props.immediate || isBatchingUpdates()) {
205
+ if (!coalesceSyncUpdates) {
206
+ // Opt-out path (disableSyncUpdateCoalescing()): the original behavior -- render synchronously
207
+ // for every update, no coalescing.
208
+ notifyBatchedUpdateStarting();
209
+ this.setState({ data: data }, notifyBatchedUpdateCompleted);
210
+ return;
211
+ }
212
+ // Coalescing enabled: at most one setState may be in flight per Cx. Re-entrant update() calls (the
213
+ // render itself writing to the store, as happens throughout a large initial render) are skipped --
214
+ // the in-flight round re-reads the store on completion (see onStateUpdateCompleted), so nothing is
215
+ // dropped. Nesting these setStates deep is what trips React's "Maximum update depth exceeded".
216
+ if (this.stateUpdateInFlight) return;
217
+ if (activeSyncUpdates === 0) syncBurstRounds = 0; // fresh burst -> reset the shared round counter
218
+ activeSyncUpdates++;
219
+ this.stateUpdateInFlight = true;
220
+ notifyBatchedUpdateStarting();
221
+ this.issueSyncSetState();
222
+ } else {
223
+ // standard mode: coalesce sequential store commands into a single deferred update
224
+ this.scheduleStateUpdate();
225
+ }
226
+ }
227
+
228
+ // Issue the next synchronous render round (coalescing path). Once a burst grows past SYNC_BURST_LIMIT we
229
+ // render from a microtask instead, so React's commit finishes without a synchronously-scheduled follow-up
230
+ // and its global nested-update counter resets before we continue. The yield is suppressed while a
231
+ // batchUpdatesAndNotify is in flight: page-breaking convergence is shallow, so staying fully synchronous
232
+ // keeps its notify callback firing right after the change commits (and the counter never gets near 50).
233
+ issueSyncSetState(): void {
234
+ if (hasBatchedUpdateSubscribers() || ++syncBurstRounds <= syncBurstLimit) {
235
+ this.setState({ data: this.store.getData() }, this.onStateUpdateCompleted);
236
+ } else {
237
+ queueMicrotask(() => {
238
+ // The event loop has turned, so React's global nested-update counter has reset; realign ours.
239
+ // Resetting here (not before scheduling) keeps the counter high through the rest of the current
240
+ // commit, so any other Cx re-arming in the same commit also yields instead of extending the chain.
241
+ syncBurstRounds = 0;
242
+ if (this.stateUpdateInFlight)
243
+ this.setState({ data: this.store.getData() }, this.onStateUpdateCompleted);
244
+ });
245
+ }
246
+ }
247
+
248
+ scheduleStateUpdate() {
249
+ if (!this.pendingUpdateTimer) {
250
+ notifyBatchedUpdateStarting();
251
+ this.pendingUpdateTimer = setTimeout(() => {
252
+ delete this.pendingUpdateTimer;
253
+ // read fresh data at fire time so the coalesced update renders the latest store state
254
+ this.setState({ data: this.store.getData() }, notifyBatchedUpdateCompleted);
255
+ }, 0);
256
+ }
257
+ }
258
+
259
+ // Completion callback for the coalescing path's setState. React runs it after the commit, so the DOM
260
+ // already reflects this render. If the render wrote to the store, run another round -- keeping the
261
+ // batched-update accounting balanced (open the next round before closing this one) so `finished` never
262
+ // catches `pending` mid-convergence and batchUpdatesAndNotify resolves only at the store fixpoint.
263
+ // Otherwise the burst has settled: clear the in-flight flag and report completion.
264
+ onStateUpdateCompleted() {
265
+ if (this.state.data === this.store.getData()) {
266
+ // Converged: the store didn't change while this round rendered, so the DOM is up to date.
267
+ this.stateUpdateInFlight = false;
268
+ activeSyncUpdates--;
269
+ notifyBatchedUpdateCompleted();
270
+ return;
271
+ }
272
+ notifyBatchedUpdateStarting(); // open the next round
273
+ notifyBatchedUpdateCompleted(); // close this one
274
+ this.issueSyncSetState();
275
+ }
276
+
277
+ waitForIdle(): void {
278
+ if (!this.props.deferredUntilIdle) return;
279
+
280
+ if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
281
+
282
+ let token = ++this.deferCounter;
283
+ this.unsubscribeIdleRequest = onIdleCallback(
284
+ () => {
285
+ this.setState({ deferToken: token });
286
+ },
287
+ {
288
+ timeout: this.props.idleTimeout || 30000,
289
+ },
290
+ );
291
+ }
292
+
293
+ componentWillUnmount(): void {
294
+ if (this.stateUpdateInFlight) {
295
+ // Release the open pending round so a waiting batchUpdatesAndNotify can settle instead of waiting
296
+ // out its fallback timeout, and keep the shared in-flight refcount balanced.
297
+ this.stateUpdateInFlight = false;
298
+ activeSyncUpdates--;
299
+ notifyBatchedUpdateCompleted();
300
+ }
301
+ if (this.pendingUpdateTimer) clearTimeout(this.pendingUpdateTimer);
302
+ if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
303
+ if (this.unsubscribe) this.unsubscribe();
304
+ if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(null);
305
+ }
306
+
307
+ shouldComponentUpdate(props: CxProps, state: CxState): boolean {
308
+ if (props.deferredUntilIdle && state.deferToken != this.deferCounter) return false;
309
+
310
+ return (
311
+ state !== this.state ||
312
+ !props.params ||
313
+ !shallowEquals(props.params, this.props.params) ||
314
+ props.instance !== this.props.instance ||
315
+ props.widget !== this.props.widget ||
316
+ props.store !== this.props.store ||
317
+ props.parentInstance !== this.props.parentInstance ||
318
+ props.cultureInfo !== this.props.cultureInfo
319
+ );
320
+ }
321
+
322
+ componentDidCatchHandler(error: Error, info: any): void {
323
+ this.flags.preparing = false;
324
+ this.props.onError!(error, this.getInstance(), info);
325
+ }
326
+ }
327
+
328
+ interface CxContextProps {
329
+ instance: Instance;
330
+ flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
331
+ options?: any;
332
+ buster: number;
333
+ contentFactory?: (props: { children: any }) => any;
334
+ forceUpdate: () => void;
335
+ cultureInfo?: ResolvedCultureInfo;
336
+ }
337
+
338
+ class CxContext extends VDOM.Component<CxContextProps, {}> {
339
+ renderCount: number;
340
+ timings: any;
341
+ content: any;
342
+ renderingContext?: RenderingContext;
343
+
344
+ constructor(props: CxContextProps) {
345
+ super(props);
346
+ this.renderCount = 0;
347
+ this.UNSAFE_componentWillReceiveProps(props);
348
+ }
349
+
350
+ UNSAFE_componentWillReceiveProps(props: CxContextProps): void {
351
+ this.timings = {
352
+ start: now(),
353
+ };
354
+
355
+ let { instance, options, contentFactory } = props;
356
+ let count = 0,
357
+ visible,
358
+ context,
359
+ forceContinue;
360
+
361
+ //should not be tracked by parents for destroy
362
+ if (!(instance as any).detached)
363
+ throw new Error("The instance passed to a Cx component should be detached from its parent.");
364
+
365
+ if (this.props.instance !== instance && (this.props.instance as any).destroyTracked)
366
+ (this.props.instance as any).destroy();
367
+
368
+ this.props.flags.preparing = true;
369
+
370
+ if (this.props.cultureInfo) pushCulture(this.props.cultureInfo);
371
+
372
+ try {
373
+ do {
374
+ count++;
375
+ forceContinue = false;
376
+ context = new RenderingContext(options);
377
+ (context as any).forceUpdate = this.props.forceUpdate;
378
+ this.props.flags.dirty = false;
379
+ (instance as any).assignedRenderList = (context as any).getRootRenderList();
380
+ visible = (instance as any).scheduleExploreIfVisible(context);
381
+ if (visible) {
382
+ while (!(context as any).exploreStack.empty()) {
383
+ let inst = (context as any).exploreStack.pop();
384
+ //console.log("EXPLORE", inst.widget.constructor.name, inst.widget.tag, inst.widget.widgetId);
385
+ (inst as any).explore(context);
386
+ }
387
+ } else if ((instance as any).destroyTracked) {
388
+ (instance as any).destroy();
389
+ }
390
+
391
+ if (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8) {
392
+ forceContinue = true;
393
+ continue;
394
+ }
395
+
396
+ if (visible) {
397
+ this.timings.afterExplore = now();
398
+
399
+ for (let i = 0; i < (context as any).prepareList.length; i++)
400
+ (context as any).prepareList[i].prepare(context);
401
+ this.timings.afterPrepare = now();
402
+ }
403
+ } while (
404
+ forceContinue ||
405
+ (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8)
406
+ );
407
+
408
+ if (visible) {
409
+ //walk in reverse order so children get rendered first
410
+ let renderList = (context as any).getRootRenderList();
411
+ while (renderList) {
412
+ for (let i = renderList.data.length - 1; i >= 0; i--) {
413
+ renderList.data[i].render(context);
414
+ }
415
+ renderList = renderList.right;
416
+ }
417
+
418
+ this.content = getContent((instance as any).vdom);
419
+ if (contentFactory) this.content = contentFactory({ children: this.content });
420
+ this.timings.afterRender = now();
421
+ for (let i = 0; i < (context as any).cleanupList.length; i++)
422
+ (context as any).cleanupList[i].cleanup(context);
423
+ } else {
424
+ this.content = null;
425
+ this.timings.afterExplore = this.timings.afterPrepare = this.timings.afterRender = now();
426
+ }
427
+ } finally {
428
+ if (this.props.cultureInfo) popCulture(this.props.cultureInfo);
429
+ }
430
+
431
+ this.timings.beforeVDOMRender = now();
432
+ this.props.flags.preparing = false;
433
+ this.props.flags.rendering = true;
434
+ this.renderingContext = context;
435
+ }
436
+
437
+ render() {
438
+ return this.content;
439
+ }
440
+
441
+ componentDidMount(): void {
442
+ this.componentDidUpdate();
443
+ }
444
+
445
+ componentDidUpdate(): void {
446
+ this.props.flags.rendering = false;
447
+ this.timings.afterVDOMRender = now();
448
+
449
+ //let {instance} = this.props;
450
+ //instance.cleanup(this.renderingContext);
451
+
452
+ this.timings.afterCleanup = now();
453
+ this.renderCount++;
454
+
455
+ if (process.env.NODE_ENV !== "production") {
456
+ let { start, beforeVDOMRender, afterVDOMRender, afterPrepare, afterExplore, afterRender, afterCleanup } =
457
+ this.timings;
458
+
459
+ Timing.log(
460
+ vdomRenderFlag,
461
+ this.renderCount,
462
+ "cx",
463
+ (beforeVDOMRender - start + afterCleanup - afterVDOMRender).toFixed(2) + "ms",
464
+ "vdom",
465
+ (afterVDOMRender - beforeVDOMRender).toFixed(2) + "ms",
466
+ );
467
+
468
+ Timing.log(
469
+ appLoopFlag,
470
+ this.renderCount,
471
+ (this.renderingContext as any).options.name || "main",
472
+ "total",
473
+ (afterCleanup - start).toFixed(1) + "ms",
474
+ "explore",
475
+ (afterExplore - start).toFixed(1) + "ms",
476
+ "prepare",
477
+ (afterPrepare - afterExplore).toFixed(1),
478
+ "render",
479
+ (afterRender - afterPrepare).toFixed(1),
480
+ "vdom",
481
+ (afterVDOMRender - beforeVDOMRender).toFixed(1),
482
+ "cleanup",
483
+ (afterCleanup - afterVDOMRender).toFixed(1),
484
+ );
485
+ }
486
+ }
487
+
488
+ componentWillUnmount(): void {
489
+ let { instance } = this.props;
490
+ if ((instance as any).destroyTracked) (instance as any).destroy();
491
+ }
492
+ }