cx 26.7.1 → 26.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui/Cx.tsx CHANGED
@@ -1,427 +1,422 @@
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
- /** Cap re-entrant synchronous updates: while batching, once a burst of back-to-back updates gets too
25
- * deep, defer the excess to the coalesced (setTimeout) branch instead of updating synchronously. Avoids
26
- * React's "Maximum update depth exceeded" on very large synchronous render bursts (e.g. the initial
27
- * render of a huge document). Off by default; has no effect on instances that don't set it. */
28
- limitSyncBursts?: boolean;
29
- deferredUntilIdle?: boolean;
30
- idleTimeout?: number;
31
- options?: any;
32
- onError?: (error: Error, instance: Instance, info: any) => void;
33
- params?: any;
34
- contentFactory?: (props: { children: any }) => any;
35
- cultureInfo?: ResolvedCultureInfo;
36
- }
37
-
38
- export interface CxState {
39
- deferToken: number;
40
- data?: any;
41
- }
42
-
43
- // Optional guard (opt in per-instance via the `limitSyncBursts` prop) against React's "Maximum update depth
44
- // exceeded" during very large synchronous render bursts. On the initial render of a large document the store
45
- // can be mutated thousands of times within a single React commit: every Instance.setState wraps store.notify()
46
- // in batchUpdates(), so isBatchingUpdates() stays true and a subscribed Cx takes its *synchronous* setState
47
- // branch on each notification. React counts those as nested render-phase updates and aborts past ~50
48
- // (surfacing as the recoverable "error during concurrent rendering"). When a Cx opts in, once back-to-back
49
- // updates exceed the limit it falls back to the coalesced setTimeout branch, which renders the latest store
50
- // data on the next tick. The counter is global (it mirrors React's global nested-update counter) and resets
51
- // after any idle gap. It is advanced on every update() of an opted-in Cx -- counting only the synchronous
52
- // branch would let the window reset whenever a deferred update lands between two synchronous ones, so the
53
- // counter would never reach the limit. performance.now() gives sub-ms resolution (Date.now() is the fallback;
54
- // Timing.now() can't be used here because it returns 0 in production).
55
- const SYNC_UPDATE_BURST_LIMIT = 30;
56
- const SYNC_UPDATE_BURST_RESET_MS = 10;
57
- const syncUpdateBurstNow: () => number =
58
- typeof performance !== "undefined" && performance.now ? () => performance.now() : () => Date.now();
59
- let syncUpdateBurstCount = 0;
60
- let syncUpdateBurstLastAt = 0;
61
-
62
- function trackSyncUpdateBurst(): boolean {
63
- let t = syncUpdateBurstNow();
64
- if (t - syncUpdateBurstLastAt > SYNC_UPDATE_BURST_RESET_MS) syncUpdateBurstCount = 0;
65
- syncUpdateBurstLastAt = t;
66
- return ++syncUpdateBurstCount > SYNC_UPDATE_BURST_LIMIT;
67
- }
68
-
69
- export class Cx extends VDOM.Component<CxProps, CxState> {
70
- widget: Widget;
71
- store: View;
72
- parentInstance?: Instance;
73
- instance?: Instance;
74
- flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
75
- renderCount: number;
76
- unsubscribe?: () => void;
77
- componentDidCatch?: (error: Error, info: any) => void;
78
- forceUpdateCallback: () => void;
79
- deferCounter: number;
80
- pendingUpdateTimer?: NodeJS.Timeout;
81
- unsubscribeIdleRequest?: () => void;
82
-
83
- constructor(props: CxProps) {
84
- super(props);
85
-
86
- if (props.instance) {
87
- this.widget = (props.instance as any).widget;
88
- this.store = (props.instance as any).store;
89
- } else {
90
- this.widget = PureContainer.create({ items: props.widget || props.items });
91
-
92
- if (props.parentInstance) {
93
- this.parentInstance = props.parentInstance;
94
- this.store = props.store || (this.parentInstance as any).store;
95
- } else {
96
- this.parentInstance = new Instance(this.widget, "0", undefined, props.store);
97
- this.store = props.store!;
98
- }
99
-
100
- if (!this.store) throw new Error("Cx component requires a store.");
101
- }
102
-
103
- this.state = {
104
- deferToken: 0,
105
- };
106
-
107
- if (props.subscribe) {
108
- this.unsubscribe = this.store.subscribe(this.update.bind(this));
109
- (this.state as any).data = this.store.getData();
110
- }
111
-
112
- this.flags = {};
113
- this.renderCount = 0;
114
-
115
- if (props.onError) this.componentDidCatch = this.componentDidCatchHandler.bind(this);
116
-
117
- this.forceUpdateCallback = this.forceUpdate.bind(this);
118
-
119
- this.deferCounter = 0;
120
- this.waitForIdle();
121
- }
122
-
123
- UNSAFE_componentWillReceiveProps(props: CxProps): void {
124
- let newStore = props.instance
125
- ? (props.instance as any).store
126
- : props.store
127
- ? props.store
128
- : (props.parentInstance as any).store;
129
-
130
- if (newStore != this.store) {
131
- this.store = newStore;
132
- if (this.unsubscribe) this.unsubscribe();
133
- if (props.subscribe) this.unsubscribe = this.store.subscribe(this.update.bind(this));
134
- }
135
-
136
- if (props.subscribe) {
137
- let data = this.store.getData();
138
- if (data !== this.state.data) {
139
- this.waitForIdle();
140
- this.setState({ data });
141
- }
142
- }
143
- }
144
-
145
- getInstance(): Instance {
146
- if (this.props.instance) return this.props.instance;
147
-
148
- if (this.instance && (this.instance as any).widget === this.widget) {
149
- if ((this.instance as any).parentStore != this.store) (this.instance as any).setParentStore(this.store);
150
- return this.instance;
151
- }
152
-
153
- if (this.widget && this.parentInstance)
154
- return (this.instance = (this.parentInstance as any).getDetachedChild(this.widget, 0, this.store));
155
-
156
- throw new Error("Could not resolve a widget instance in the Cx component.");
157
- }
158
-
159
- render() {
160
- if (this.props.deferredUntilIdle && this.state.deferToken < this.deferCounter) return null;
161
-
162
- let cultureInfo = this.props.cultureInfo ?? getCurrentCulture();
163
-
164
- return (
165
- <CxContext
166
- instance={this.getInstance()}
167
- flags={this.flags}
168
- options={this.props.options}
169
- buster={++this.renderCount}
170
- contentFactory={this.props.contentFactory}
171
- forceUpdate={this.forceUpdateCallback}
172
- cultureInfo={cultureInfo}
173
- />
174
- );
175
- }
176
-
177
- componentDidMount(): void {
178
- this.componentDidUpdate();
179
-
180
- if (this.props.options && this.props.options.onPipeUpdate)
181
- this.props.options.onPipeUpdate(this.update.bind(this));
182
- }
183
-
184
- componentDidUpdate(): void {
185
- if (this.flags.dirty) {
186
- this.update();
187
- }
188
- }
189
-
190
- update(): void {
191
- let data = this.store.getData();
192
- debug(appDataFlag, data);
193
- if (this.flags.preparing) this.flags.dirty = true;
194
- // `immediate` always renders synchronously (its contract). Otherwise, while batching, a Cx that opts in
195
- // via `limitSyncBursts` defers once a synchronous burst gets too deep (avoids React's max-update-depth).
196
- // Without the flag the condition is unchanged -- trackSyncUpdateBurst() is never even called, so the
197
- // guard has zero effect on instances that don't opt in.
198
- else if (this.props.immediate || (isBatchingUpdates() && !(this.props.limitSyncBursts && trackSyncUpdateBurst()))) {
199
- notifyBatchedUpdateStarting();
200
- this.setState({ data: data }, notifyBatchedUpdateCompleted);
201
- } else {
202
- //in standard mode sequential store commands are batched -- as is any update that exceeds the
203
- //synchronous burst limit above, which falls through here to avoid React's max-update-depth abort
204
- if (!this.pendingUpdateTimer) {
205
- notifyBatchedUpdateStarting();
206
- this.pendingUpdateTimer = setTimeout(() => {
207
- delete this.pendingUpdateTimer;
208
- //for opted-in instances read fresh data at fire time so a deferred (coalesced) update isn't
209
- //left stale after a burst; otherwise keep the original captured-data behavior unchanged
210
- this.setState(
211
- { data: this.props.limitSyncBursts ? this.store.getData() : data },
212
- notifyBatchedUpdateCompleted,
213
- );
214
- }, 0);
215
- }
216
- }
217
- }
218
-
219
- waitForIdle(): void {
220
- if (!this.props.deferredUntilIdle) return;
221
-
222
- if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
223
-
224
- let token = ++this.deferCounter;
225
- this.unsubscribeIdleRequest = onIdleCallback(
226
- () => {
227
- this.setState({ deferToken: token });
228
- },
229
- {
230
- timeout: this.props.idleTimeout || 30000,
231
- },
232
- );
233
- }
234
-
235
- componentWillUnmount(): void {
236
- if (this.pendingUpdateTimer) clearTimeout(this.pendingUpdateTimer);
237
- if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
238
- if (this.unsubscribe) this.unsubscribe();
239
- if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(null);
240
- }
241
-
242
- shouldComponentUpdate(props: CxProps, state: CxState): boolean {
243
- if (props.deferredUntilIdle && state.deferToken != this.deferCounter) return false;
244
-
245
- return (
246
- state !== this.state ||
247
- !props.params ||
248
- !shallowEquals(props.params, this.props.params) ||
249
- props.instance !== this.props.instance ||
250
- props.widget !== this.props.widget ||
251
- props.store !== this.props.store ||
252
- props.parentInstance !== this.props.parentInstance ||
253
- props.cultureInfo !== this.props.cultureInfo
254
- );
255
- }
256
-
257
- componentDidCatchHandler(error: Error, info: any): void {
258
- this.flags.preparing = false;
259
- this.props.onError!(error, this.getInstance(), info);
260
- }
261
- }
262
-
263
- interface CxContextProps {
264
- instance: Instance;
265
- flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
266
- options?: any;
267
- buster: number;
268
- contentFactory?: (props: { children: any }) => any;
269
- forceUpdate: () => void;
270
- cultureInfo?: ResolvedCultureInfo;
271
- }
272
-
273
- class CxContext extends VDOM.Component<CxContextProps, {}> {
274
- renderCount: number;
275
- timings: any;
276
- content: any;
277
- renderingContext?: RenderingContext;
278
-
279
- constructor(props: CxContextProps) {
280
- super(props);
281
- this.renderCount = 0;
282
- this.UNSAFE_componentWillReceiveProps(props);
283
- }
284
-
285
- UNSAFE_componentWillReceiveProps(props: CxContextProps): void {
286
- this.timings = {
287
- start: now(),
288
- };
289
-
290
- let { instance, options, contentFactory } = props;
291
- let count = 0,
292
- visible,
293
- context,
294
- forceContinue;
295
-
296
- //should not be tracked by parents for destroy
297
- if (!(instance as any).detached)
298
- throw new Error("The instance passed to a Cx component should be detached from its parent.");
299
-
300
- if (this.props.instance !== instance && (this.props.instance as any).destroyTracked)
301
- (this.props.instance as any).destroy();
302
-
303
- this.props.flags.preparing = true;
304
-
305
- if (this.props.cultureInfo) pushCulture(this.props.cultureInfo);
306
-
307
- try {
308
- do {
309
- count++;
310
- forceContinue = false;
311
- context = new RenderingContext(options);
312
- (context as any).forceUpdate = this.props.forceUpdate;
313
- this.props.flags.dirty = false;
314
- (instance as any).assignedRenderList = (context as any).getRootRenderList();
315
- visible = (instance as any).scheduleExploreIfVisible(context);
316
- if (visible) {
317
- while (!(context as any).exploreStack.empty()) {
318
- let inst = (context as any).exploreStack.pop();
319
- //console.log("EXPLORE", inst.widget.constructor.name, inst.widget.tag, inst.widget.widgetId);
320
- (inst as any).explore(context);
321
- }
322
- } else if ((instance as any).destroyTracked) {
323
- (instance as any).destroy();
324
- }
325
-
326
- if (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8) {
327
- forceContinue = true;
328
- continue;
329
- }
330
-
331
- if (visible) {
332
- this.timings.afterExplore = now();
333
-
334
- for (let i = 0; i < (context as any).prepareList.length; i++)
335
- (context as any).prepareList[i].prepare(context);
336
- this.timings.afterPrepare = now();
337
- }
338
- } while (
339
- forceContinue ||
340
- (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8)
341
- );
342
-
343
- if (visible) {
344
- //walk in reverse order so children get rendered first
345
- let renderList = (context as any).getRootRenderList();
346
- while (renderList) {
347
- for (let i = renderList.data.length - 1; i >= 0; i--) {
348
- renderList.data[i].render(context);
349
- }
350
- renderList = renderList.right;
351
- }
352
-
353
- this.content = getContent((instance as any).vdom);
354
- if (contentFactory) this.content = contentFactory({ children: this.content });
355
- this.timings.afterRender = now();
356
- for (let i = 0; i < (context as any).cleanupList.length; i++)
357
- (context as any).cleanupList[i].cleanup(context);
358
- } else {
359
- this.content = null;
360
- this.timings.afterExplore = this.timings.afterPrepare = this.timings.afterRender = now();
361
- }
362
- } finally {
363
- if (this.props.cultureInfo) popCulture(this.props.cultureInfo);
364
- }
365
-
366
- this.timings.beforeVDOMRender = now();
367
- this.props.flags.preparing = false;
368
- this.props.flags.rendering = true;
369
- this.renderingContext = context;
370
- }
371
-
372
- render() {
373
- return this.content;
374
- }
375
-
376
- componentDidMount(): void {
377
- this.componentDidUpdate();
378
- }
379
-
380
- componentDidUpdate(): void {
381
- this.props.flags.rendering = false;
382
- this.timings.afterVDOMRender = now();
383
-
384
- //let {instance} = this.props;
385
- //instance.cleanup(this.renderingContext);
386
-
387
- this.timings.afterCleanup = now();
388
- this.renderCount++;
389
-
390
- if (process.env.NODE_ENV !== "production") {
391
- let { start, beforeVDOMRender, afterVDOMRender, afterPrepare, afterExplore, afterRender, afterCleanup } =
392
- this.timings;
393
-
394
- Timing.log(
395
- vdomRenderFlag,
396
- this.renderCount,
397
- "cx",
398
- (beforeVDOMRender - start + afterCleanup - afterVDOMRender).toFixed(2) + "ms",
399
- "vdom",
400
- (afterVDOMRender - beforeVDOMRender).toFixed(2) + "ms",
401
- );
402
-
403
- Timing.log(
404
- appLoopFlag,
405
- this.renderCount,
406
- (this.renderingContext as any).options.name || "main",
407
- "total",
408
- (afterCleanup - start).toFixed(1) + "ms",
409
- "explore",
410
- (afterExplore - start).toFixed(1) + "ms",
411
- "prepare",
412
- (afterPrepare - afterExplore).toFixed(1),
413
- "render",
414
- (afterRender - afterPrepare).toFixed(1),
415
- "vdom",
416
- (afterVDOMRender - beforeVDOMRender).toFixed(1),
417
- "cleanup",
418
- (afterCleanup - afterVDOMRender).toFixed(1),
419
- );
420
- }
421
- }
422
-
423
- componentWillUnmount(): void {
424
- let { instance } = this.props;
425
- if ((instance as any).destroyTracked) (instance as any).destroy();
426
- }
427
- }
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
+ data: props.subscribe ? this.store.getData() : null,
77
+ };
78
+
79
+ if (props.subscribe) {
80
+ this.unsubscribe = this.store.subscribe(this.update.bind(this));
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
+ this.scheduleStateUpdate();
182
+ }
183
+ }
184
+
185
+ scheduleStateUpdate() {
186
+ if (!this.pendingUpdateTimer) {
187
+ notifyBatchedUpdateStarting();
188
+ this.pendingUpdateTimer = setTimeout(() => {
189
+ delete this.pendingUpdateTimer;
190
+ // read fresh data at fire time so the coalesced update renders the latest store state
191
+ this.setState({ data: this.store.getData() }, notifyBatchedUpdateCompleted);
192
+ }, 0);
193
+ }
194
+ }
195
+
196
+ // Completion callback for the synchronous setState above. If the store changed while that update was
197
+ // rendering -- i.e. the render itself wrote to the store, as happens throughout a large initial render --
198
+ // render once more with the latest data, keeping this callback armed; otherwise the burst has settled, so
199
+ // clear the in-flight flag and report completion. Re-arming coalesces until the store reaches a fixpoint;
200
+ // a genuinely non-converging update loop is still caught by React's own max-update-depth guard.
201
+ onStateUpdateCompleted() {
202
+ let latestData = this.store.getData();
203
+ notifyBatchedUpdateCompleted();
204
+ if (this.state.data === latestData) {
205
+ this.stateUpdateDepth = 0;
206
+ } else {
207
+ this.stateUpdateDepth++;
208
+ if (this.stateUpdateDepth < 20 && isBatchingUpdates())
209
+ this.setState({ data: latestData }, this.onStateUpdateCompleted);
210
+ else this.scheduleStateUpdate();
211
+ }
212
+ }
213
+
214
+ waitForIdle(): void {
215
+ if (!this.props.deferredUntilIdle) return;
216
+
217
+ if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
218
+
219
+ let token = ++this.deferCounter;
220
+ this.unsubscribeIdleRequest = onIdleCallback(
221
+ () => {
222
+ this.setState({ deferToken: token }, this.onStateUpdateCompleted);
223
+ },
224
+ {
225
+ timeout: this.props.idleTimeout || 30000,
226
+ },
227
+ );
228
+ }
229
+
230
+ componentWillUnmount(): void {
231
+ if (this.pendingUpdateTimer) clearTimeout(this.pendingUpdateTimer);
232
+ if (this.unsubscribeIdleRequest) this.unsubscribeIdleRequest();
233
+ if (this.unsubscribe) this.unsubscribe();
234
+ if (this.props.options && this.props.options.onPipeUpdate) this.props.options.onPipeUpdate(null);
235
+ }
236
+
237
+ shouldComponentUpdate(props: CxProps, state: CxState): boolean {
238
+ if (props.deferredUntilIdle && state.deferToken != this.deferCounter) return false;
239
+
240
+ return (
241
+ state !== this.state ||
242
+ !props.params ||
243
+ !shallowEquals(props.params, this.props.params) ||
244
+ props.instance !== this.props.instance ||
245
+ props.widget !== this.props.widget ||
246
+ props.store !== this.props.store ||
247
+ props.parentInstance !== this.props.parentInstance ||
248
+ props.cultureInfo !== this.props.cultureInfo
249
+ );
250
+ }
251
+
252
+ componentDidCatchHandler(error: Error, info: any): void {
253
+ this.flags.preparing = false;
254
+ this.props.onError!(error, this.getInstance(), info);
255
+ }
256
+ }
257
+
258
+ interface CxContextProps {
259
+ instance: Instance;
260
+ flags: { preparing?: boolean; dirty?: boolean; rendering?: boolean };
261
+ options?: any;
262
+ buster: number;
263
+ contentFactory?: (props: { children: any }) => any;
264
+ forceUpdate: () => void;
265
+ cultureInfo?: ResolvedCultureInfo;
266
+ }
267
+
268
+ class CxContext extends VDOM.Component<CxContextProps, {}> {
269
+ renderCount: number;
270
+ timings: any;
271
+ content: any;
272
+ renderingContext?: RenderingContext;
273
+
274
+ constructor(props: CxContextProps) {
275
+ super(props);
276
+ this.renderCount = 0;
277
+ this.UNSAFE_componentWillReceiveProps(props);
278
+ }
279
+
280
+ UNSAFE_componentWillReceiveProps(props: CxContextProps): void {
281
+ this.timings = {
282
+ start: now(),
283
+ };
284
+
285
+ let { instance, options, contentFactory } = props;
286
+ let count = 0,
287
+ visible,
288
+ context,
289
+ forceContinue;
290
+
291
+ //should not be tracked by parents for destroy
292
+ if (!(instance as any).detached)
293
+ throw new Error("The instance passed to a Cx component should be detached from its parent.");
294
+
295
+ if (this.props.instance !== instance && (this.props.instance as any).destroyTracked)
296
+ (this.props.instance as any).destroy();
297
+
298
+ this.props.flags.preparing = true;
299
+
300
+ if (this.props.cultureInfo) pushCulture(this.props.cultureInfo);
301
+
302
+ try {
303
+ do {
304
+ count++;
305
+ forceContinue = false;
306
+ context = new RenderingContext(options);
307
+ (context as any).forceUpdate = this.props.forceUpdate;
308
+ this.props.flags.dirty = false;
309
+ (instance as any).assignedRenderList = (context as any).getRootRenderList();
310
+ visible = (instance as any).scheduleExploreIfVisible(context);
311
+ if (visible) {
312
+ while (!(context as any).exploreStack.empty()) {
313
+ let inst = (context as any).exploreStack.pop();
314
+ //console.log("EXPLORE", inst.widget.constructor.name, inst.widget.tag, inst.widget.widgetId);
315
+ (inst as any).explore(context);
316
+ }
317
+ } else if ((instance as any).destroyTracked) {
318
+ (instance as any).destroy();
319
+ }
320
+
321
+ if (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8) {
322
+ forceContinue = true;
323
+ continue;
324
+ }
325
+
326
+ if (visible) {
327
+ this.timings.afterExplore = now();
328
+
329
+ for (let i = 0; i < (context as any).prepareList.length; i++)
330
+ (context as any).prepareList[i].prepare(context);
331
+ this.timings.afterPrepare = now();
332
+ }
333
+ } while (
334
+ forceContinue ||
335
+ (this.props.flags.dirty && count <= 3 && Widget.optimizePrepare && now() - this.timings.start < 8)
336
+ );
337
+
338
+ if (visible) {
339
+ //walk in reverse order so children get rendered first
340
+ let renderList = (context as any).getRootRenderList();
341
+ while (renderList) {
342
+ for (let i = renderList.data.length - 1; i >= 0; i--) {
343
+ renderList.data[i].render(context);
344
+ }
345
+ renderList = renderList.right;
346
+ }
347
+
348
+ this.content = getContent((instance as any).vdom);
349
+ if (contentFactory) this.content = contentFactory({ children: this.content });
350
+ this.timings.afterRender = now();
351
+ for (let i = 0; i < (context as any).cleanupList.length; i++)
352
+ (context as any).cleanupList[i].cleanup(context);
353
+ } else {
354
+ this.content = null;
355
+ this.timings.afterExplore = this.timings.afterPrepare = this.timings.afterRender = now();
356
+ }
357
+ } finally {
358
+ if (this.props.cultureInfo) popCulture(this.props.cultureInfo);
359
+ }
360
+
361
+ this.timings.beforeVDOMRender = now();
362
+ this.props.flags.preparing = false;
363
+ this.props.flags.rendering = true;
364
+ this.renderingContext = context;
365
+ }
366
+
367
+ render() {
368
+ return this.content;
369
+ }
370
+
371
+ componentDidMount(): void {
372
+ this.componentDidUpdate();
373
+ }
374
+
375
+ componentDidUpdate(): void {
376
+ this.props.flags.rendering = false;
377
+ this.timings.afterVDOMRender = now();
378
+
379
+ //let {instance} = this.props;
380
+ //instance.cleanup(this.renderingContext);
381
+
382
+ this.timings.afterCleanup = now();
383
+ this.renderCount++;
384
+
385
+ if (process.env.NODE_ENV !== "production") {
386
+ let { start, beforeVDOMRender, afterVDOMRender, afterPrepare, afterExplore, afterRender, afterCleanup } =
387
+ this.timings;
388
+
389
+ Timing.log(
390
+ vdomRenderFlag,
391
+ this.renderCount,
392
+ "cx",
393
+ (beforeVDOMRender - start + afterCleanup - afterVDOMRender).toFixed(2) + "ms",
394
+ "vdom",
395
+ (afterVDOMRender - beforeVDOMRender).toFixed(2) + "ms",
396
+ );
397
+
398
+ Timing.log(
399
+ appLoopFlag,
400
+ this.renderCount,
401
+ (this.renderingContext as any).options.name || "main",
402
+ "total",
403
+ (afterCleanup - start).toFixed(1) + "ms",
404
+ "explore",
405
+ (afterExplore - start).toFixed(1) + "ms",
406
+ "prepare",
407
+ (afterPrepare - afterExplore).toFixed(1),
408
+ "render",
409
+ (afterRender - afterPrepare).toFixed(1),
410
+ "vdom",
411
+ (afterVDOMRender - beforeVDOMRender).toFixed(1),
412
+ "cleanup",
413
+ (afterCleanup - afterVDOMRender).toFixed(1),
414
+ );
415
+ }
416
+ }
417
+
418
+ componentWillUnmount(): void {
419
+ let { instance } = this.props;
420
+ if ((instance as any).destroyTracked) (instance as any).destroy();
421
+ }
422
+ }