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/dist/ui.js CHANGED
@@ -2094,30 +2094,6 @@ class LinkedListsNode {
2094
2094
  }
2095
2095
  }
2096
2096
 
2097
- // Optional guard (opt in per-instance via the `limitSyncBursts` prop) against React's "Maximum update depth
2098
- // exceeded" during very large synchronous render bursts. On the initial render of a large document the store
2099
- // can be mutated thousands of times within a single React commit: every Instance.setState wraps store.notify()
2100
- // in batchUpdates(), so isBatchingUpdates() stays true and a subscribed Cx takes its *synchronous* setState
2101
- // branch on each notification. React counts those as nested render-phase updates and aborts past ~50
2102
- // (surfacing as the recoverable "error during concurrent rendering"). When a Cx opts in, once back-to-back
2103
- // updates exceed the limit it falls back to the coalesced setTimeout branch, which renders the latest store
2104
- // data on the next tick. The counter is global (it mirrors React's global nested-update counter) and resets
2105
- // after any idle gap. It is advanced on every update() of an opted-in Cx -- counting only the synchronous
2106
- // branch would let the window reset whenever a deferred update lands between two synchronous ones, so the
2107
- // counter would never reach the limit. performance.now() gives sub-ms resolution (Date.now() is the fallback;
2108
- // Timing.now() can't be used here because it returns 0 in production).
2109
- const SYNC_UPDATE_BURST_LIMIT = 30;
2110
- const SYNC_UPDATE_BURST_RESET_MS = 10;
2111
- const syncUpdateBurstNow =
2112
- typeof performance !== "undefined" && performance.now ? () => performance.now() : () => Date.now();
2113
- let syncUpdateBurstCount = 0;
2114
- let syncUpdateBurstLastAt = 0;
2115
- function trackSyncUpdateBurst() {
2116
- let t = syncUpdateBurstNow();
2117
- if (t - syncUpdateBurstLastAt > SYNC_UPDATE_BURST_RESET_MS) syncUpdateBurstCount = 0;
2118
- syncUpdateBurstLastAt = t;
2119
- return ++syncUpdateBurstCount > SYNC_UPDATE_BURST_LIMIT;
2120
- }
2121
2097
  class Cx extends VDOM.Component {
2122
2098
  widget;
2123
2099
  store;
@@ -2131,6 +2107,8 @@ class Cx extends VDOM.Component {
2131
2107
  deferCounter;
2132
2108
  pendingUpdateTimer;
2133
2109
  unsubscribeIdleRequest;
2110
+ // 0 when no synchronous setState is in flight; >0 while one is pending (re-entrancy guard for update())
2111
+ stateUpdateDepth = 0;
2134
2112
  constructor(props) {
2135
2113
  super(props);
2136
2114
  if (props.instance) {
@@ -2151,11 +2129,12 @@ class Cx extends VDOM.Component {
2151
2129
  }
2152
2130
  this.state = {
2153
2131
  deferToken: 0,
2132
+ data: props.subscribe ? this.store.getData() : null,
2154
2133
  };
2155
2134
  if (props.subscribe) {
2156
2135
  this.unsubscribe = this.store.subscribe(this.update.bind(this));
2157
- this.state.data = this.store.getData();
2158
2136
  }
2137
+ this.onStateUpdateCompleted = this.onStateUpdateCompleted.bind(this);
2159
2138
  this.flags = {};
2160
2139
  this.renderCount = 0;
2161
2140
  if (props.onError) this.componentDidCatch = this.componentDidCatchHandler.bind(this);
@@ -2187,7 +2166,7 @@ class Cx extends VDOM.Component {
2187
2166
  return this.instance;
2188
2167
  }
2189
2168
  if (this.widget && this.parentInstance)
2190
- return (this.instance = this.parentInstance.getDetachedChild(this.widget, 0, this.store));
2169
+ return (this.instance = this.parentInstance.getDetachedChild(this.widget, "0", this.store));
2191
2170
  throw new Error("Could not resolve a widget instance in the Cx component.");
2192
2171
  }
2193
2172
  render() {
@@ -2216,35 +2195,63 @@ class Cx extends VDOM.Component {
2216
2195
  let data = this.store.getData();
2217
2196
  debug(appDataFlag, data);
2218
2197
  if (this.flags.preparing) this.flags.dirty = true;
2219
- // `immediate` always renders synchronously (its contract). Otherwise, while batching, a Cx that opts in
2220
- // via `limitSyncBursts` defers once a synchronous burst gets too deep (avoids React's max-update-depth).
2221
- // Without the flag the condition is unchanged -- trackSyncUpdateBurst() is never even called, so the
2222
- // guard has zero effect on instances that don't opt in.
2223
- else if (this.props.immediate || (isBatchingUpdates() && !(this.props.limitSyncBursts && trackSyncUpdateBurst()))) {
2224
- notifyBatchedUpdateStarting();
2225
- this.setState(
2226
- {
2227
- data: data,
2228
- },
2229
- notifyBatchedUpdateCompleted,
2230
- );
2231
- } else {
2232
- //in standard mode sequential store commands are batched -- as is any update that exceeds the
2233
- //synchronous burst limit above, which falls through here to avoid React's max-update-depth abort
2234
- if (!this.pendingUpdateTimer) {
2198
+ // Synchronous path (while batching, or for `immediate` instances). At most one setState may be in flight
2199
+ // per Cx: if one is already pending (stateUpdateDepth != 0) skip issuing another. Re-entrant setStates
2200
+ // nested deep -- e.g. a large initial render that writes to the store while it renders -- are what trip
2201
+ // React's "Maximum update depth exceeded"; coalescing them into a single in-flight update avoids that.
2202
+ // The pending update's completion callback re-reads the store and renders again if it changed
2203
+ // (see onStateUpdateCompleted), so no change is dropped.
2204
+ else if (this.props.immediate || isBatchingUpdates()) {
2205
+ if (this.stateUpdateDepth == 0) {
2206
+ this.stateUpdateDepth = 1;
2235
2207
  notifyBatchedUpdateStarting();
2236
- this.pendingUpdateTimer = setTimeout(() => {
2237
- delete this.pendingUpdateTimer;
2238
- //for opted-in instances read fresh data at fire time so a deferred (coalesced) update isn't
2239
- //left stale after a burst; otherwise keep the original captured-data behavior unchanged
2240
- this.setState(
2241
- {
2242
- data: this.props.limitSyncBursts ? this.store.getData() : data,
2243
- },
2244
- notifyBatchedUpdateCompleted,
2245
- );
2246
- }, 0);
2208
+ this.setState(
2209
+ {
2210
+ data: data,
2211
+ },
2212
+ this.onStateUpdateCompleted,
2213
+ );
2247
2214
  }
2215
+ } else {
2216
+ // standard mode: coalesce sequential store commands into a single deferred update
2217
+ this.scheduleStateUpdate();
2218
+ }
2219
+ }
2220
+ scheduleStateUpdate() {
2221
+ if (!this.pendingUpdateTimer) {
2222
+ notifyBatchedUpdateStarting();
2223
+ this.pendingUpdateTimer = setTimeout(() => {
2224
+ delete this.pendingUpdateTimer;
2225
+ // read fresh data at fire time so the coalesced update renders the latest store state
2226
+ this.setState(
2227
+ {
2228
+ data: this.store.getData(),
2229
+ },
2230
+ notifyBatchedUpdateCompleted,
2231
+ );
2232
+ }, 0);
2233
+ }
2234
+ }
2235
+ // Completion callback for the synchronous setState above. If the store changed while that update was
2236
+ // rendering -- i.e. the render itself wrote to the store, as happens throughout a large initial render --
2237
+ // render once more with the latest data, keeping this callback armed; otherwise the burst has settled, so
2238
+ // clear the in-flight flag and report completion. Re-arming coalesces until the store reaches a fixpoint;
2239
+ // a genuinely non-converging update loop is still caught by React's own max-update-depth guard.
2240
+ onStateUpdateCompleted() {
2241
+ let latestData = this.store.getData();
2242
+ notifyBatchedUpdateCompleted();
2243
+ if (this.state.data === latestData) {
2244
+ this.stateUpdateDepth = 0;
2245
+ } else {
2246
+ this.stateUpdateDepth++;
2247
+ if (this.stateUpdateDepth < 20 && isBatchingUpdates())
2248
+ this.setState(
2249
+ {
2250
+ data: latestData,
2251
+ },
2252
+ this.onStateUpdateCompleted,
2253
+ );
2254
+ else this.scheduleStateUpdate();
2248
2255
  }
2249
2256
  }
2250
2257
  waitForIdle() {
@@ -2253,9 +2260,12 @@ class Cx extends VDOM.Component {
2253
2260
  let token = ++this.deferCounter;
2254
2261
  this.unsubscribeIdleRequest = onIdleCallback(
2255
2262
  () => {
2256
- this.setState({
2257
- deferToken: token,
2258
- });
2263
+ this.setState(
2264
+ {
2265
+ deferToken: token,
2266
+ },
2267
+ this.onStateUpdateCompleted,
2268
+ );
2259
2269
  },
2260
2270
  {
2261
2271
  timeout: this.props.idleTimeout || 30000,
@@ -2693,7 +2703,6 @@ class Restate extends PureContainerBase {
2693
2703
  deferredUntilIdle: instance.data.deferredUntilIdle,
2694
2704
  idleTimeout: instance.data.idleTimeout,
2695
2705
  immediate: this.immediate,
2696
- limitSyncBursts: this.limitSyncBursts,
2697
2706
  cultureInfo: instance.cultureInfo,
2698
2707
  },
2699
2708
  key,
@@ -2703,7 +2712,6 @@ class Restate extends PureContainerBase {
2703
2712
  Restate.prototype.detached = false;
2704
2713
  Restate.prototype.waitForIdle = false;
2705
2714
  Restate.prototype.immediate = false;
2706
- Restate.prototype.limitSyncBursts = false;
2707
2715
  Restate.prototype.culture = null;
2708
2716
  const PrivateStore = Restate;
2709
2717
  class RestateStore extends Store {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cx",
3
- "version": "26.7.1",
3
+ "version": "26.7.3",
4
4
  "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.",
5
5
  "exports": {
6
6
  "./data": {