cx 26.7.3 → 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/build/ui/Cx.d.ts +4 -1
- package/build/ui/Cx.d.ts.map +1 -1
- package/build/ui/Cx.js +91 -29
- package/build/ui/batchUpdates.d.ts +1 -0
- package/build/ui/batchUpdates.d.ts.map +1 -1
- package/build/ui/batchUpdates.js +6 -0
- package/dist/manifest.js +821 -812
- package/dist/ui.js +107 -36
- package/package.json +1 -1
- package/src/ui/Cx.spec.tsx +58 -0
- package/src/ui/Cx.tsx +492 -422
- package/src/ui/Restate.tsx +217 -217
- package/src/ui/batchUpdates.ts +7 -0
package/src/ui/Cx.tsx
CHANGED
|
@@ -1,422 +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 {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (this.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.setState({ data:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 {
|
|
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
|
+
}
|