cx 26.7.4 → 26.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/ui/Cx.d.ts CHANGED
@@ -27,6 +27,7 @@ export interface CxProps {
27
27
  export interface CxState {
28
28
  deferToken: number;
29
29
  data?: any;
30
+ error?: boolean;
30
31
  }
31
32
  export declare class Cx extends VDOM.Component<CxProps, CxState> {
32
33
  widget: Widget;
@@ -40,12 +41,13 @@ export declare class Cx extends VDOM.Component<CxProps, CxState> {
40
41
  };
41
42
  renderCount: number;
42
43
  unsubscribe?: () => void;
43
- componentDidCatch?: (error: Error, info: any) => void;
44
44
  forceUpdateCallback: () => void;
45
45
  deferCounter: number;
46
46
  pendingUpdateTimer?: NodeJS.Timeout;
47
47
  unsubscribeIdleRequest?: () => void;
48
- stateUpdateInFlight: boolean;
48
+ owedNotifications: Set<number>;
49
+ mounted: boolean;
50
+ burstRoundCounted: boolean;
49
51
  constructor(props: CxProps);
50
52
  UNSAFE_componentWillReceiveProps(props: CxProps): void;
51
53
  getInstance(): Instance;
@@ -53,12 +55,13 @@ export declare class Cx extends VDOM.Component<CxProps, CxState> {
53
55
  componentDidMount(): void;
54
56
  componentDidUpdate(): void;
55
57
  update(): void;
56
- issueSyncSetState(): void;
58
+ issueStateUpdate(seq: number): void;
59
+ completeNotification(seq: number): void;
57
60
  scheduleStateUpdate(): void;
58
- onStateUpdateCompleted(): void;
59
61
  waitForIdle(): void;
60
62
  componentWillUnmount(): void;
61
63
  shouldComponentUpdate(props: CxProps, state: CxState): boolean;
62
- componentDidCatchHandler(error: Error, info: any): void;
64
+ static getDerivedStateFromError(): Partial<CxState>;
65
+ componentDidCatch(error: Error, info: any): void;
63
66
  }
64
67
  //# sourceMappingURL=Cx.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Cx.d.ts","sourceRoot":"","sources":["../../src/ui/Cx.tsx"],"names":[],"mappings":"AAAA,6BAA6B;AAE7B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAc,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAatC,OAAO,EAA2D,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACzG,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAgBhC,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAG/D;AACD,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AASD,MAAM,WAAW,OAAO;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,cAAc,CAAC,EAAE,QAAQ,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAChE,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,GAAG,CAAA;KAAE,KAAK,GAAG,CAAC;IACnD,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAED,MAAM,WAAW,OAAO;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,GAAG,CAAC;CACb;AAED,qBAAa,EAAG,SAAQ,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,IAAI,CAAC;IACZ,cAAc,CAAC,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrE,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IACtD,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAGpC,mBAAmB,EAAE,OAAO,CAAS;gBAEzB,KAAK,EAAE,OAAO;IA0C1B,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAsBtD,WAAW,IAAI,QAAQ;IAcvB,MAAM;IAkBN,iBAAiB,IAAI,IAAI;IAOzB,kBAAkB,IAAI,IAAI;IAM1B,MAAM,IAAI,IAAI;IAmCd,iBAAiB,IAAI,IAAI;IAezB,mBAAmB;IAgBnB,sBAAsB;IAatB,WAAW,IAAI,IAAI;IAgBnB,oBAAoB,IAAI,IAAI;IAc5B,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO;IAe9D,wBAAwB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,IAAI;CAIzD"}
1
+ {"version":3,"file":"Cx.d.ts","sourceRoot":"","sources":["../../src/ui/Cx.tsx"],"names":[],"mappings":"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"}
package/build/ui/Cx.js CHANGED
@@ -5,37 +5,41 @@ import { Instance } from "./Instance";
5
5
  import { RenderingContext } from "./RenderingContext";
6
6
  import { debug, appDataFlag } from "../util/Debug";
7
7
  import { Timing, now, appLoopFlag, vdomRenderFlag } from "../util/Timing";
8
- import { isBatchingUpdates, notifyBatchedUpdateStarting, notifyBatchedUpdateCompleted, hasBatchedUpdateSubscribers, } from "./batchUpdates";
8
+ import { isBatchingUpdates, notifyBatchedUpdateStarting, notifyBatchedUpdateCompleted } from "./batchUpdates";
9
9
  import { shallowEquals } from "../util/shallowEquals";
10
10
  import { PureContainer } from "./PureContainer";
11
11
  import { onIdleCallback } from "../util/onIdleCallback";
12
12
  import { getCurrentCulture, pushCulture, popCulture } from "./Culture";
13
- // On by default. Cx coalesces re-entrant synchronous updates and, once a burst grows deep, yields to a
14
- // microtask so React's global per-root nested-update counter resets before continuing -- preventing
15
- // "Maximum update depth exceeded" on large renders that write to the store while they render (e.g. a
16
- // several-hundred-page report). For updates that settle in a single render -- virtually all of them --
17
- // this is equivalent to the previous behavior; it only diverges under a deep re-entrant render burst.
18
- // If you suspect it causes trouble, opt out at app startup with disableSyncUpdateCoalescing() -- and
19
- // please report the issue so it can be fixed.
20
- let coalesceSyncUpdates = true;
21
- // Consecutive commit-phase re-render rounds allowed before Cx yields to a microtask. Kept under React's
22
- // ~50 nested-update limit (which is global to the root, not per component); the default trades a little
23
- // initial-render time for a safety margin. Override via enableSyncUpdateCoalescing(limit) if needed.
24
- let syncBurstLimit = 35;
13
+ // On by default. Once a synchronous update burst grows deep, Cx starts issuing updates from microtasks
14
+ // so React's global nested-update counter resets before continuing -- preventing "Maximum update depth
15
+ // exceeded" on large renders that write to the store while they render (e.g. a several-hundred-page
16
+ // report). For updates that settle in a single render -- virtually all of them -- this is equivalent to
17
+ // rendering synchronously; it only diverges under a deep re-entrant render burst. If you suspect it
18
+ // causes trouble, opt out at app startup with disableSyncUpdateCoalescing() -- and please report the
19
+ // issue so it can be fixed.
20
+ // Synchronous updates allowed within one burst before Cx switches to issuing updates from a microtask.
21
+ // Kept under React's ~50 nested-update limit (which is global to the root, not per component); the
22
+ // default trades a little initial-render time for a safety margin. Override via
23
+ // enableSyncUpdateCoalescing(limit) if needed.
24
+ const defaultSyncBurstLimit = 35;
25
+ let syncBurstLimit = defaultSyncBurstLimit;
26
+ // Microtask-issued updates allowed within one burst before Cx escalates to setTimeout. Deep enough that
27
+ // only a store that never converges reaches it; timeouts let the event loop turn, so the page stays
28
+ // responsive and batchUpdatesAndNotify fallback timers can fire instead of the tab hanging.
29
+ let microtaskBurstLimit = 1000;
25
30
  export function enableSyncUpdateCoalescing(limit) {
26
- coalesceSyncUpdates = true;
27
- if (limit != null)
28
- syncBurstLimit = limit;
31
+ syncBurstLimit = limit ?? defaultSyncBurstLimit;
29
32
  }
30
33
  export function disableSyncUpdateCoalescing() {
31
- coalesceSyncUpdates = false;
34
+ syncBurstLimit = Infinity;
32
35
  }
33
- // Module-global because React's nested-update limit is global to the root, not per component. A large
34
- // initial render can re-render the root Cx and every detached page Restate hundreds of times in one
35
- // synchronous burst; once the burst grows past syncBurstLimit we yield so React's commit finishes
36
- // without a synchronously-scheduled follow-up (which resets its counter) before we render again.
37
- let activeSyncUpdates = 0; // Cx instances with a synchronous setState in flight
38
- let syncBurstRounds = 0; // commit-phase re-render rounds issued since the last yield / burst start
36
+ // Module-global because React's nested-update limit is global to the root, not per component. The burst
37
+ // counter resets only when all notifications settle (see completeNotification) -- tying the burst
38
+ // window to unsettled work makes it immune to how React schedules the flushes. Legacy React chains
39
+ // re-entrant updates synchronously within one task, but React 19 may run each round from its own
40
+ // microtask, so no task/microtask boundary is a reliable reset point.
41
+ let syncBurstRounds = 0; // updates issued in the current burst
42
+ let outstandingNotifications = 0; // reported notifications not yet rendered, across all Cx instances
39
43
  export class Cx extends VDOM.Component {
40
44
  widget;
41
45
  store;
@@ -44,14 +48,18 @@ export class Cx extends VDOM.Component {
44
48
  flags;
45
49
  renderCount;
46
50
  unsubscribe;
47
- componentDidCatch;
48
51
  forceUpdateCallback;
49
52
  deferCounter;
50
53
  pendingUpdateTimer;
51
54
  unsubscribeIdleRequest;
52
- // true while a coalesced synchronous setState is in flight for this Cx (re-entrancy guard for update());
53
- // only used when coalesceSyncUpdates is enabled
54
- stateUpdateInFlight = false;
55
+ // store notifications reported to batchUpdatesAndNotify subscribers but not yet rendered and completed
56
+ owedNotifications = new Set();
57
+ // setState is not allowed before the component mounts; pre-mount notifications set flags.dirty instead
58
+ // and componentDidMount picks them up (see update())
59
+ mounted = false;
60
+ // true once this instance has contributed to syncBurstRounds for the current render round; cleared on
61
+ // render, so bursts are counted per round rather than per notification (see update())
62
+ burstRoundCounted = false;
55
63
  constructor(props) {
56
64
  super(props);
57
65
  if (props.instance) {
@@ -78,14 +86,11 @@ export class Cx extends VDOM.Component {
78
86
  if (props.subscribe) {
79
87
  this.unsubscribe = this.store.subscribe(this.update.bind(this));
80
88
  }
81
- this.onStateUpdateCompleted = this.onStateUpdateCompleted.bind(this);
82
89
  this.flags = {};
83
90
  this.renderCount = 0;
84
- if (props.onError)
85
- this.componentDidCatch = this.componentDidCatchHandler.bind(this);
86
91
  this.forceUpdateCallback = this.forceUpdate.bind(this);
87
- this.deferCounter = 0;
88
- this.waitForIdle();
92
+ // deferredUntilIdle content stays hidden until the idle callback scheduled on mount bumps the token
93
+ this.deferCounter = props.deferredUntilIdle ? 1 : 0;
89
94
  }
90
95
  UNSAFE_componentWillReceiveProps(props) {
91
96
  let newStore = props.instance
@@ -121,12 +126,21 @@ export class Cx extends VDOM.Component {
121
126
  throw new Error("Could not resolve a widget instance in the Cx component.");
122
127
  }
123
128
  render() {
129
+ this.burstRoundCounted = false;
130
+ // an error was captured and is being dispatched to the onError callback (see componentDidCatch);
131
+ // render nothing until the callback repairs the state
132
+ if (this.state.error)
133
+ return null;
124
134
  if (this.props.deferredUntilIdle && this.state.deferToken < this.deferCounter)
125
135
  return null;
126
136
  let cultureInfo = this.props.cultureInfo ?? getCurrentCulture();
127
137
  return (_jsx(CxContext, { instance: this.getInstance(), flags: this.flags, options: this.props.options, buster: ++this.renderCount, contentFactory: this.props.contentFactory, forceUpdate: this.forceUpdateCallback, cultureInfo: cultureInfo }));
128
138
  }
129
139
  componentDidMount() {
140
+ this.mounted = true;
141
+ // schedule the deferredUntilIdle reveal here rather than in the constructor -- the idle callback
142
+ // could otherwise fire before the component mounts and issue a setState React does not allow yet
143
+ this.waitForIdle();
130
144
  this.componentDidUpdate();
131
145
  if (this.props.options && this.props.options.onPipeUpdate)
132
146
  this.props.options.onPipeUpdate(this.update.bind(this));
@@ -139,83 +153,76 @@ export class Cx extends VDOM.Component {
139
153
  update() {
140
154
  let data = this.store.getData();
141
155
  debug(appDataFlag, data);
142
- if (this.flags.preparing)
156
+ if (this.flags.preparing || !this.mounted)
143
157
  this.flags.dirty = true;
144
158
  // Synchronous path: while batching (incl. batchUpdatesAndNotify, which page-breaking relies on) or for
145
159
  // `immediate` instances.
146
160
  else if (this.props.immediate || isBatchingUpdates()) {
147
- if (!coalesceSyncUpdates) {
148
- // Opt-out path (disableSyncUpdateCoalescing()): the original behavior -- render synchronously
149
- // for every update, no coalescing.
150
- notifyBatchedUpdateStarting();
151
- this.setState({ data: data }, notifyBatchedUpdateCompleted);
152
- return;
161
+ // Every notification is reported to batchUpdatesAndNotify subscribers up front and completed only
162
+ // once the setState that renders it commits, so notify callbacks never fire before the DOM reflects
163
+ // the change. Each notification always gets its own setState; only its timing escalates as the
164
+ // burst grows: synchronous at first, then from a microtask (lets React's commit finish so its
165
+ // global nested-update counter resets instead of tripping "Maximum update depth exceeded"), and
166
+ // finally from a timeout (lets the event loop turn, so a store that never converges degrades to a
167
+ // responsive page instead of a frozen tab).
168
+ let seq = notifyBatchedUpdateStarting();
169
+ this.owedNotifications.add(seq);
170
+ outstandingNotifications++;
171
+ // Count render rounds rather than notifications: only the first notification an instance
172
+ // receives per render round bumps the shared depth (the flag clears on render). This tracks
173
+ // React's nested-commit count -- the thing the escalation must stay under -- so a round that
174
+ // writes many values doesn't burn through the budget in one go.
175
+ if (!this.burstRoundCounted) {
176
+ this.burstRoundCounted = true;
177
+ ++syncBurstRounds;
178
+ if (process.env.NODE_ENV !== "production" && syncBurstRounds === microtaskBurstLimit + 1)
179
+ console.error("Cx: store updates are not converging after " +
180
+ microtaskBurstLimit +
181
+ " render rounds -- possible update loop. Updates are now issued from timeouts to keep the page responsive. Look for code that writes a new value to the store on every render.");
153
182
  }
154
- // Coalescing enabled: at most one setState may be in flight per Cx. Re-entrant update() calls (the
155
- // render itself writing to the store, as happens throughout a large initial render) are skipped --
156
- // the in-flight round re-reads the store on completion (see onStateUpdateCompleted), so nothing is
157
- // dropped. Nesting these setStates deep is what trips React's "Maximum update depth exceeded".
158
- if (this.stateUpdateInFlight)
159
- return;
160
- if (activeSyncUpdates === 0)
161
- syncBurstRounds = 0; // fresh burst -> reset the shared round counter
162
- activeSyncUpdates++;
163
- this.stateUpdateInFlight = true;
164
- notifyBatchedUpdateStarting();
165
- this.issueSyncSetState();
183
+ if (syncBurstRounds <= syncBurstLimit)
184
+ this.issueStateUpdate(seq);
185
+ else if (syncBurstRounds <= microtaskBurstLimit)
186
+ queueMicrotask(() => this.issueStateUpdate(seq));
187
+ else
188
+ setTimeout(() => this.issueStateUpdate(seq), 0);
166
189
  }
167
190
  else {
168
191
  // standard mode: coalesce sequential store commands into a single deferred update
169
192
  this.scheduleStateUpdate();
170
193
  }
171
194
  }
172
- // Issue the next synchronous render round (coalescing path). Once a burst grows past SYNC_BURST_LIMIT we
173
- // render from a microtask instead, so React's commit finishes without a synchronously-scheduled follow-up
174
- // and its global nested-update counter resets before we continue. The yield is suppressed while a
175
- // batchUpdatesAndNotify is in flight: page-breaking convergence is shallow, so staying fully synchronous
176
- // keeps its notify callback firing right after the change commits (and the counter never gets near 50).
177
- issueSyncSetState() {
178
- if (hasBatchedUpdateSubscribers() || ++syncBurstRounds <= syncBurstLimit) {
179
- this.setState({ data: this.store.getData() }, this.onStateUpdateCompleted);
180
- }
181
- else {
182
- queueMicrotask(() => {
183
- // The event loop has turned, so React's global nested-update counter has reset; realign ours.
184
- // Resetting here (not before scheduling) keeps the counter high through the rest of the current
185
- // commit, so any other Cx re-arming in the same commit also yields instead of extending the chain.
186
- syncBurstRounds = 0;
187
- if (this.stateUpdateInFlight)
188
- this.setState({ data: this.store.getData() }, this.onStateUpdateCompleted);
189
- });
190
- }
195
+ // Render the latest store data and report the notification completed once the commit is done, so
196
+ // batchUpdatesAndNotify resolves only when the DOM reflects the store.
197
+ issueStateUpdate(seq) {
198
+ // skip notifications no longer owed -- unmount may have already released them
199
+ if (!this.owedNotifications.has(seq))
200
+ return;
201
+ this.setState({ data: this.store.getData() }, () => this.completeNotification(seq));
202
+ }
203
+ completeNotification(seq) {
204
+ // skip notifications no longer owed -- unmount may have already released them
205
+ if (!this.owedNotifications.delete(seq))
206
+ return;
207
+ outstandingNotifications--;
208
+ notifyBatchedUpdateCompleted(seq);
209
+ // everything settled -- the next burst starts fresh, and so does React's nested-update counter
210
+ // (the commit that settled the last notification ends without scheduling further synchronous work)
211
+ if (outstandingNotifications === 0)
212
+ syncBurstRounds = 0;
191
213
  }
192
214
  scheduleStateUpdate() {
193
215
  if (!this.pendingUpdateTimer) {
194
- notifyBatchedUpdateStarting();
216
+ let seq = notifyBatchedUpdateStarting();
217
+ this.owedNotifications.add(seq);
218
+ outstandingNotifications++;
195
219
  this.pendingUpdateTimer = setTimeout(() => {
196
220
  delete this.pendingUpdateTimer;
197
221
  // read fresh data at fire time so the coalesced update renders the latest store state
198
- this.setState({ data: this.store.getData() }, notifyBatchedUpdateCompleted);
222
+ this.setState({ data: this.store.getData() }, () => this.completeNotification(seq));
199
223
  }, 0);
200
224
  }
201
225
  }
202
- // Completion callback for the coalescing path's setState. React runs it after the commit, so the DOM
203
- // already reflects this render. If the render wrote to the store, run another round -- keeping the
204
- // batched-update accounting balanced (open the next round before closing this one) so `finished` never
205
- // catches `pending` mid-convergence and batchUpdatesAndNotify resolves only at the store fixpoint.
206
- // Otherwise the burst has settled: clear the in-flight flag and report completion.
207
- onStateUpdateCompleted() {
208
- if (this.state.data === this.store.getData()) {
209
- // Converged: the store didn't change while this round rendered, so the DOM is up to date.
210
- this.stateUpdateInFlight = false;
211
- activeSyncUpdates--;
212
- notifyBatchedUpdateCompleted();
213
- return;
214
- }
215
- notifyBatchedUpdateStarting(); // open the next round
216
- notifyBatchedUpdateCompleted(); // close this one
217
- this.issueSyncSetState();
218
- }
219
226
  waitForIdle() {
220
227
  if (!this.props.deferredUntilIdle)
221
228
  return;
@@ -229,13 +236,10 @@ export class Cx extends VDOM.Component {
229
236
  });
230
237
  }
231
238
  componentWillUnmount() {
232
- if (this.stateUpdateInFlight) {
233
- // Release the open pending round so a waiting batchUpdatesAndNotify can settle instead of waiting
234
- // out its fallback timeout, and keep the shared in-flight refcount balanced.
235
- this.stateUpdateInFlight = false;
236
- activeSyncUpdates--;
237
- notifyBatchedUpdateCompleted();
238
- }
239
+ // Release notifications that will never render so a waiting batchUpdatesAndNotify can settle instead
240
+ // of waiting out its fallback timeout.
241
+ for (let seq of this.owedNotifications)
242
+ this.completeNotification(seq);
239
243
  if (this.pendingUpdateTimer)
240
244
  clearTimeout(this.pendingUpdateTimer);
241
245
  if (this.unsubscribeIdleRequest)
@@ -257,9 +261,22 @@ export class Cx extends VDOM.Component {
257
261
  props.parentInstance !== this.props.parentInstance ||
258
262
  props.cultureInfo !== this.props.cultureInfo);
259
263
  }
260
- componentDidCatchHandler(error, info) {
264
+ // Render null for the failed subtree while the error is dispatched to componentDidCatch below --
265
+ // returning a state update here is what React expects of an error boundary (rendering the broken
266
+ // children again would just rethrow).
267
+ static getDerivedStateFromError() {
268
+ return { error: true };
269
+ }
270
+ componentDidCatch(error, info) {
261
271
  this.flags.preparing = false;
272
+ // without an onError callback this instance is not an error boundary -- rethrow so the error
273
+ // reaches the nearest ancestor boundary, matching the behavior before getDerivedStateFromError
274
+ // was introduced
275
+ if (!this.props.onError)
276
+ throw error;
262
277
  this.props.onError(error, this.getInstance(), info);
278
+ // the callback had a chance to repair the state (e.g. replace the failing content) -- resume rendering
279
+ this.setState({ error: false });
263
280
  }
264
281
  }
265
282
  class CxContext extends VDOM.Component {
@@ -1,7 +1,6 @@
1
1
  export declare function batchUpdates(callback: () => void): void;
2
2
  export declare function isBatchingUpdates(): boolean;
3
- export declare function hasBatchedUpdateSubscribers(): boolean;
4
- export declare function notifyBatchedUpdateStarting(): void;
5
- export declare function notifyBatchedUpdateCompleted(): void;
3
+ export declare function notifyBatchedUpdateStarting(): number;
4
+ export declare function notifyBatchedUpdateCompleted(seq: number): void;
6
5
  export declare function batchUpdatesAndNotify(callback: () => void, notifyCallback: (success: boolean) => void, timeout?: number): void;
7
6
  //# sourceMappingURL=batchUpdates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"batchUpdates.d.ts","sourceRoot":"","sources":["../../src/ui/batchUpdates.ts"],"names":[],"mappings":"AAYA,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAWvD;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAKD,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAIlD;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAMnD;AAED,wBAAgB,qBAAqB,CAClC,QAAQ,EAAE,MAAM,IAAI,EACpB,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EAC1C,OAAO,GAAE,MAAa,GACtB,IAAI,CAwBN"}
1
+ {"version":3,"file":"batchUpdates.d.ts","sourceRoot":"","sources":["../../src/ui/batchUpdates.ts"],"names":[],"mappings":"AAcA,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAWvD;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAID,wBAAgB,2BAA2B,IAAI,MAAM,CAMpD;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAS9D;AAED,wBAAgB,qBAAqB,CAClC,QAAQ,EAAE,MAAM,IAAI,EACpB,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EAC1C,OAAO,GAAE,MAAa,GACtB,IAAI,CAyBN"}
@@ -1,6 +1,7 @@
1
1
  import { VDOM } from "./VDOM";
2
2
  import { SubscriberList } from "../util/SubscriberList";
3
3
  let isBatching = 0;
4
+ let updateSequence = 0;
4
5
  let promiseSubscribers = new SubscriberList();
5
6
  export function batchUpdates(callback) {
6
7
  if (VDOM.DOM.unstable_batchedUpdates)
@@ -19,20 +20,22 @@ export function batchUpdates(callback) {
19
20
  export function isBatchingUpdates() {
20
21
  return isBatching > 0;
21
22
  }
22
- // True while a batchUpdatesAndNotify accumulator is subscribed, i.e. some caller is waiting for the
23
- // current batch's renders to finish. Cx uses this to stay fully synchronous during page-breaking so the
24
- // notify callback still fires right after the change commits.
25
- export function hasBatchedUpdateSubscribers() {
26
- return !promiseSubscribers.isEmpty();
27
- }
23
+ // Returns a sequence number identifying the update; pass it to notifyBatchedUpdateCompleted once the
24
+ // update is rendered.
28
25
  export function notifyBatchedUpdateStarting() {
26
+ let seq = ++updateSequence;
29
27
  promiseSubscribers.execute((x) => {
30
28
  x.pending++;
31
29
  });
30
+ return seq;
32
31
  }
33
- export function notifyBatchedUpdateCompleted() {
32
+ export function notifyBatchedUpdateCompleted(seq) {
34
33
  promiseSubscribers.execute((x) => {
35
34
  let cb = x;
35
+ // ignore updates that started before this subscriber attached -- counting them would let the notify
36
+ // callback fire before the updates the subscriber is actually waiting on are rendered
37
+ if (seq <= cb.watermark)
38
+ return;
36
39
  cb.finished++;
37
40
  if (cb.finished >= cb.pending)
38
41
  cb.complete(true);
@@ -45,6 +48,7 @@ export function batchUpdatesAndNotify(callback, notifyCallback, timeout = 1000)
45
48
  const update = {
46
49
  pending: 0,
47
50
  finished: 0,
51
+ watermark: updateSequence,
48
52
  complete: (success) => {
49
53
  if (!done) {
50
54
  done = true;