cx 26.7.0 → 26.7.1
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 +5 -0
- package/build/ui/Cx.d.ts.map +1 -1
- package/build/ui/Cx.js +34 -3
- package/build/ui/Restate.d.ts +6 -0
- package/build/ui/Restate.d.ts.map +1 -1
- package/build/ui/Restate.js +2 -1
- package/build/widgets/DocumentTitle.d.ts.map +1 -1
- package/build/widgets/DocumentTitle.js +2 -0
- package/build/widgets/grid/TreeNode.d.ts +2 -0
- package/build/widgets/grid/TreeNode.d.ts.map +1 -1
- package/dist/manifest.js +846 -846
- package/dist/ui.js +36 -3
- package/dist/widgets.js +1 -0
- package/package.json +1 -1
- package/src/ui/Cx.tsx +44 -3
- package/src/ui/DataProxy.ts +55 -55
- package/src/ui/Rescope.ts +50 -50
- package/src/ui/Restate.tsx +9 -0
- package/src/ui/exprHelpers.ts +96 -96
- package/src/widgets/DocumentTitle.ts +2 -0
- package/src/widgets/Sandbox.ts +104 -104
- package/src/widgets/form/ColorField.scss +112 -112
- package/src/widgets/form/DateTimeField.scss +111 -111
- package/src/widgets/form/LookupField.maps.scss +26 -26
- package/src/widgets/form/MonthField.scss +113 -113
- package/src/widgets/form/Select.scss +104 -104
- package/src/widgets/form/TextField.scss +66 -66
- package/src/widgets/grid/TreeNode.tsx +3 -0
- package/src/widgets/nav/MenuItem.tsx +525 -525
package/dist/ui.js
CHANGED
|
@@ -2094,6 +2094,30 @@ 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
|
+
}
|
|
2097
2121
|
class Cx extends VDOM.Component {
|
|
2098
2122
|
widget;
|
|
2099
2123
|
store;
|
|
@@ -2192,7 +2216,11 @@ class Cx extends VDOM.Component {
|
|
|
2192
2216
|
let data = this.store.getData();
|
|
2193
2217
|
debug(appDataFlag, data);
|
|
2194
2218
|
if (this.flags.preparing) this.flags.dirty = true;
|
|
2195
|
-
|
|
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()))) {
|
|
2196
2224
|
notifyBatchedUpdateStarting();
|
|
2197
2225
|
this.setState(
|
|
2198
2226
|
{
|
|
@@ -2201,14 +2229,17 @@ class Cx extends VDOM.Component {
|
|
|
2201
2229
|
notifyBatchedUpdateCompleted,
|
|
2202
2230
|
);
|
|
2203
2231
|
} else {
|
|
2204
|
-
//in standard mode sequential store commands are batched
|
|
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
|
|
2205
2234
|
if (!this.pendingUpdateTimer) {
|
|
2206
2235
|
notifyBatchedUpdateStarting();
|
|
2207
2236
|
this.pendingUpdateTimer = setTimeout(() => {
|
|
2208
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
|
|
2209
2240
|
this.setState(
|
|
2210
2241
|
{
|
|
2211
|
-
data: data,
|
|
2242
|
+
data: this.props.limitSyncBursts ? this.store.getData() : data,
|
|
2212
2243
|
},
|
|
2213
2244
|
notifyBatchedUpdateCompleted,
|
|
2214
2245
|
);
|
|
@@ -2662,6 +2693,7 @@ class Restate extends PureContainerBase {
|
|
|
2662
2693
|
deferredUntilIdle: instance.data.deferredUntilIdle,
|
|
2663
2694
|
idleTimeout: instance.data.idleTimeout,
|
|
2664
2695
|
immediate: this.immediate,
|
|
2696
|
+
limitSyncBursts: this.limitSyncBursts,
|
|
2665
2697
|
cultureInfo: instance.cultureInfo,
|
|
2666
2698
|
},
|
|
2667
2699
|
key,
|
|
@@ -2671,6 +2703,7 @@ class Restate extends PureContainerBase {
|
|
|
2671
2703
|
Restate.prototype.detached = false;
|
|
2672
2704
|
Restate.prototype.waitForIdle = false;
|
|
2673
2705
|
Restate.prototype.immediate = false;
|
|
2706
|
+
Restate.prototype.limitSyncBursts = false;
|
|
2674
2707
|
Restate.prototype.culture = null;
|
|
2675
2708
|
const PrivateStore = Restate;
|
|
2676
2709
|
class RestateStore extends Store {
|
package/dist/widgets.js
CHANGED
|
@@ -1124,6 +1124,7 @@ class DocumentTitle extends Widget {
|
|
|
1124
1124
|
super.explore(context, instance);
|
|
1125
1125
|
}
|
|
1126
1126
|
prepare(context, instance) {
|
|
1127
|
+
if (typeof document == "undefined") return;
|
|
1127
1128
|
if (context.documentTitle.activeInstance == instance) document.title = context.documentTitle.title;
|
|
1128
1129
|
}
|
|
1129
1130
|
render() {
|
package/package.json
CHANGED
package/src/ui/Cx.tsx
CHANGED
|
@@ -21,6 +21,11 @@ export interface CxProps {
|
|
|
21
21
|
parentInstance?: Instance;
|
|
22
22
|
subscribe?: boolean;
|
|
23
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;
|
|
24
29
|
deferredUntilIdle?: boolean;
|
|
25
30
|
idleTimeout?: number;
|
|
26
31
|
options?: any;
|
|
@@ -35,6 +40,32 @@ export interface CxState {
|
|
|
35
40
|
data?: any;
|
|
36
41
|
}
|
|
37
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
|
+
|
|
38
69
|
export class Cx extends VDOM.Component<CxProps, CxState> {
|
|
39
70
|
widget: Widget;
|
|
40
71
|
store: View;
|
|
@@ -160,16 +191,26 @@ export class Cx extends VDOM.Component<CxProps, CxState> {
|
|
|
160
191
|
let data = this.store.getData();
|
|
161
192
|
debug(appDataFlag, data);
|
|
162
193
|
if (this.flags.preparing) this.flags.dirty = true;
|
|
163
|
-
|
|
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()))) {
|
|
164
199
|
notifyBatchedUpdateStarting();
|
|
165
200
|
this.setState({ data: data }, notifyBatchedUpdateCompleted);
|
|
166
201
|
} else {
|
|
167
|
-
//in standard mode sequential store commands are batched
|
|
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
|
|
168
204
|
if (!this.pendingUpdateTimer) {
|
|
169
205
|
notifyBatchedUpdateStarting();
|
|
170
206
|
this.pendingUpdateTimer = setTimeout(() => {
|
|
171
207
|
delete this.pendingUpdateTimer;
|
|
172
|
-
|
|
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
|
+
);
|
|
173
214
|
}, 0);
|
|
174
215
|
}
|
|
175
216
|
}
|
package/src/ui/DataProxy.ts
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
import { AccessorChain } from "../data";
|
|
2
|
-
import { NestedDataView } from "../data/NestedDataView";
|
|
3
|
-
import { StructuredProp, WritableProp } from "./Prop";
|
|
4
|
-
import { PureContainerBase, PureContainerConfig } from "./PureContainer";
|
|
5
|
-
import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
|
|
6
|
-
|
|
7
|
-
export interface DataProxyConfig extends PureContainerConfig {
|
|
8
|
-
/** Data object with computed values to be exposed in the local store. */
|
|
9
|
-
data?: StructuredProp;
|
|
10
|
-
|
|
11
|
-
/** Binding to a value to be exposed under the `alias` name. */
|
|
12
|
-
value?: WritableProp<any>;
|
|
13
|
-
|
|
14
|
-
/** Alias name under which `value` is exposed in the local store. */
|
|
15
|
-
alias?: string | AccessorChain<any>;
|
|
16
|
-
|
|
17
|
-
/** Indicate that parent store data should not be mutated. */
|
|
18
|
-
immutable?: boolean;
|
|
19
|
-
|
|
20
|
-
/** Indicate that local store data should not be mutated. */
|
|
21
|
-
sealed?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class DataProxy extends PureContainerBase<DataProxyConfig> {
|
|
25
|
-
declare data?: any;
|
|
26
|
-
declare alias?: string;
|
|
27
|
-
declare value?: any;
|
|
28
|
-
declare immutable: boolean;
|
|
29
|
-
declare sealed: boolean;
|
|
30
|
-
|
|
31
|
-
init() {
|
|
32
|
-
if (!this.data) this.data = {};
|
|
33
|
-
|
|
34
|
-
if (this.alias) this.data[this.alias] = this.value;
|
|
35
|
-
|
|
36
|
-
super.init();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
initInstance(context: any, instance: any) {
|
|
40
|
-
instance.store = new NestedDataView({
|
|
41
|
-
store: instance.parentStore,
|
|
42
|
-
nestedData: new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true }),
|
|
43
|
-
immutable: this.immutable,
|
|
44
|
-
sealed: this.sealed,
|
|
45
|
-
});
|
|
46
|
-
super.initInstance(context, instance);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
applyParentStore(instance: any) {
|
|
50
|
-
instance.store.setStore(instance.parentStore);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
DataProxy.prototype.immutable = false;
|
|
55
|
-
DataProxy.prototype.sealed = false;
|
|
1
|
+
import { AccessorChain } from "../data";
|
|
2
|
+
import { NestedDataView } from "../data/NestedDataView";
|
|
3
|
+
import { StructuredProp, WritableProp } from "./Prop";
|
|
4
|
+
import { PureContainerBase, PureContainerConfig } from "./PureContainer";
|
|
5
|
+
import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
|
|
6
|
+
|
|
7
|
+
export interface DataProxyConfig extends PureContainerConfig {
|
|
8
|
+
/** Data object with computed values to be exposed in the local store. */
|
|
9
|
+
data?: StructuredProp;
|
|
10
|
+
|
|
11
|
+
/** Binding to a value to be exposed under the `alias` name. */
|
|
12
|
+
value?: WritableProp<any>;
|
|
13
|
+
|
|
14
|
+
/** Alias name under which `value` is exposed in the local store. */
|
|
15
|
+
alias?: string | AccessorChain<any>;
|
|
16
|
+
|
|
17
|
+
/** Indicate that parent store data should not be mutated. */
|
|
18
|
+
immutable?: boolean;
|
|
19
|
+
|
|
20
|
+
/** Indicate that local store data should not be mutated. */
|
|
21
|
+
sealed?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class DataProxy extends PureContainerBase<DataProxyConfig> {
|
|
25
|
+
declare data?: any;
|
|
26
|
+
declare alias?: string;
|
|
27
|
+
declare value?: any;
|
|
28
|
+
declare immutable: boolean;
|
|
29
|
+
declare sealed: boolean;
|
|
30
|
+
|
|
31
|
+
init() {
|
|
32
|
+
if (!this.data) this.data = {};
|
|
33
|
+
|
|
34
|
+
if (this.alias) this.data[this.alias] = this.value;
|
|
35
|
+
|
|
36
|
+
super.init();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
initInstance(context: any, instance: any) {
|
|
40
|
+
instance.store = new NestedDataView({
|
|
41
|
+
store: instance.parentStore,
|
|
42
|
+
nestedData: new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true }),
|
|
43
|
+
immutable: this.immutable,
|
|
44
|
+
sealed: this.sealed,
|
|
45
|
+
});
|
|
46
|
+
super.initInstance(context, instance);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
applyParentStore(instance: any) {
|
|
50
|
+
instance.store.setStore(instance.parentStore);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
DataProxy.prototype.immutable = false;
|
|
55
|
+
DataProxy.prototype.sealed = false;
|
package/src/ui/Rescope.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { Widget } from "./Widget";
|
|
2
|
-
import { PureContainerBase, PureContainerConfig } from "./PureContainer";
|
|
3
|
-
import { Binding } from "../data/Binding";
|
|
4
|
-
import { ZoomIntoPropertyView } from "../data/ZoomIntoPropertyView";
|
|
5
|
-
import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
|
|
6
|
-
import { isObject } from "../util/isObject";
|
|
7
|
-
import { StructuredProp } from "./Prop";
|
|
8
|
-
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
9
|
-
|
|
10
|
-
export interface RescopeConfig extends PureContainerConfig {
|
|
11
|
-
bind: string | AccessorChain<any>;
|
|
12
|
-
rootName?: string | AccessorChain<any>;
|
|
13
|
-
rootAlias?: string | AccessorChain<any>;
|
|
14
|
-
data?: StructuredProp;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class Rescope extends PureContainerBase<RescopeConfig> {
|
|
18
|
-
declare bind: string;
|
|
19
|
-
declare binding: any;
|
|
20
|
-
declare rootAlias?: string;
|
|
21
|
-
declare rootName: string;
|
|
22
|
-
declare data?: any;
|
|
23
|
-
|
|
24
|
-
init() {
|
|
25
|
-
this.binding = Binding.get(this.bind);
|
|
26
|
-
if (this.rootAlias) this.rootName = this.rootAlias;
|
|
27
|
-
super.init();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
initInstance(context: any, instance: any) {
|
|
31
|
-
instance.store = new ZoomIntoPropertyView({
|
|
32
|
-
store: instance.parentStore,
|
|
33
|
-
binding: this.binding,
|
|
34
|
-
rootName: this.rootName,
|
|
35
|
-
nestedData: isObject(this.data)
|
|
36
|
-
? new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true })
|
|
37
|
-
: undefined,
|
|
38
|
-
});
|
|
39
|
-
super.initInstance(context, instance);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
applyParentStore(instance: any) {
|
|
43
|
-
instance.store.setStore(instance.parentStore);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
Rescope.prototype.bind = "$page";
|
|
48
|
-
Rescope.prototype.rootName = "$root";
|
|
49
|
-
|
|
50
|
-
Widget.alias("rescope", Rescope);
|
|
1
|
+
import { Widget } from "./Widget";
|
|
2
|
+
import { PureContainerBase, PureContainerConfig } from "./PureContainer";
|
|
3
|
+
import { Binding } from "../data/Binding";
|
|
4
|
+
import { ZoomIntoPropertyView } from "../data/ZoomIntoPropertyView";
|
|
5
|
+
import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
|
|
6
|
+
import { isObject } from "../util/isObject";
|
|
7
|
+
import { StructuredProp } from "./Prop";
|
|
8
|
+
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
9
|
+
|
|
10
|
+
export interface RescopeConfig extends PureContainerConfig {
|
|
11
|
+
bind: string | AccessorChain<any>;
|
|
12
|
+
rootName?: string | AccessorChain<any>;
|
|
13
|
+
rootAlias?: string | AccessorChain<any>;
|
|
14
|
+
data?: StructuredProp;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Rescope extends PureContainerBase<RescopeConfig> {
|
|
18
|
+
declare bind: string;
|
|
19
|
+
declare binding: any;
|
|
20
|
+
declare rootAlias?: string;
|
|
21
|
+
declare rootName: string;
|
|
22
|
+
declare data?: any;
|
|
23
|
+
|
|
24
|
+
init() {
|
|
25
|
+
this.binding = Binding.get(this.bind);
|
|
26
|
+
if (this.rootAlias) this.rootName = this.rootAlias;
|
|
27
|
+
super.init();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
initInstance(context: any, instance: any) {
|
|
31
|
+
instance.store = new ZoomIntoPropertyView({
|
|
32
|
+
store: instance.parentStore,
|
|
33
|
+
binding: this.binding,
|
|
34
|
+
rootName: this.rootName,
|
|
35
|
+
nestedData: isObject(this.data)
|
|
36
|
+
? new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true })
|
|
37
|
+
: undefined,
|
|
38
|
+
});
|
|
39
|
+
super.initInstance(context, instance);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
applyParentStore(instance: any) {
|
|
43
|
+
instance.store.setStore(instance.parentStore);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Rescope.prototype.bind = "$page";
|
|
48
|
+
Rescope.prototype.rootName = "$root";
|
|
49
|
+
|
|
50
|
+
Widget.alias("rescope", Rescope);
|
package/src/ui/Restate.tsx
CHANGED
|
@@ -33,6 +33,12 @@ export interface RestateConfig extends PureContainerConfig {
|
|
|
33
33
|
/** Set to `true` to disable batching of updates. */
|
|
34
34
|
immediate?: boolean;
|
|
35
35
|
|
|
36
|
+
/** Cap re-entrant synchronous updates on the detached `Cx` this Restate renders: once a burst of
|
|
37
|
+
* back-to-back updates gets too deep, defer the excess to the coalesced branch instead of updating
|
|
38
|
+
* synchronously. Avoids React's "Maximum update depth exceeded" on very large synchronous render bursts.
|
|
39
|
+
* Only applies in `detached` mode (a non-detached Restate has no `Cx` of its own). Off by default. */
|
|
40
|
+
limitSyncBursts?: boolean;
|
|
41
|
+
|
|
36
42
|
/** Error handler callback. */
|
|
37
43
|
onError?: (error: Error, instance: Instance) => void;
|
|
38
44
|
|
|
@@ -53,6 +59,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
|
|
|
53
59
|
declare onError?: (error: Error, instance: Instance) => void;
|
|
54
60
|
declare waitForIdle: boolean;
|
|
55
61
|
declare immediate: boolean;
|
|
62
|
+
declare limitSyncBursts: boolean;
|
|
56
63
|
|
|
57
64
|
declareData(...args: any[]) {
|
|
58
65
|
return super.declareData(...args, {
|
|
@@ -138,6 +145,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
|
|
|
138
145
|
deferredUntilIdle={instance.data.deferredUntilIdle}
|
|
139
146
|
idleTimeout={instance.data.idleTimeout}
|
|
140
147
|
immediate={this.immediate}
|
|
148
|
+
limitSyncBursts={this.limitSyncBursts}
|
|
141
149
|
cultureInfo={instance.cultureInfo}
|
|
142
150
|
/>
|
|
143
151
|
);
|
|
@@ -147,6 +155,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
|
|
|
147
155
|
Restate.prototype.detached = false;
|
|
148
156
|
Restate.prototype.waitForIdle = false;
|
|
149
157
|
Restate.prototype.immediate = false;
|
|
158
|
+
Restate.prototype.limitSyncBursts = false;
|
|
150
159
|
Restate.prototype.culture = null;
|
|
151
160
|
|
|
152
161
|
export const PrivateStore = Restate;
|
package/src/ui/exprHelpers.ts
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import { computable } from "../data/computable";
|
|
2
|
-
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
3
|
-
import { Selector } from "../data/Selector";
|
|
4
|
-
import { Format } from "../util/Format";
|
|
5
|
-
import { expr } from "./expr";
|
|
6
|
-
|
|
7
|
-
/** Returns a selector that converts the value to boolean using !! */
|
|
8
|
-
export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
9
|
-
return expr(arg, (x) => !!x);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Returns a selector that checks if the value is falsy using ! */
|
|
13
|
-
export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
14
|
-
return expr(arg, (x) => !x);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Returns a selector that checks if the value is strictly true (=== true) */
|
|
18
|
-
export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
|
|
19
|
-
return expr(arg, (x) => x === true);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Returns a selector that checks if the value is strictly false (=== false) */
|
|
23
|
-
export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
|
|
24
|
-
return expr(arg, (x) => x === false);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Returns a selector that checks if the value is not null or undefined (x != null) */
|
|
28
|
-
export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
29
|
-
return expr(arg, (x) => x != null);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
|
|
33
|
-
export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
34
|
-
return expr(arg, (x) => x == null || x.length === 0);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
|
|
38
|
-
export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
39
|
-
return expr(arg, (x) => x != null && x.length > 0);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Returns a selector that checks if the value is less than the given value */
|
|
43
|
-
export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
44
|
-
return expr(arg, (x) => x < value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Returns a selector that checks if the value is less than or equal to the given value */
|
|
48
|
-
export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
49
|
-
return expr(arg, (x) => x <= value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Returns a selector that checks if the value is greater than the given value */
|
|
53
|
-
export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
54
|
-
return expr(arg, (x) => x > value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Returns a selector that checks if the value is greater than or equal to the given value */
|
|
58
|
-
export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
59
|
-
return expr(arg, (x) => x >= value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Returns a selector that checks if the value equals the given value using == */
|
|
63
|
-
export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
64
|
-
return expr(arg, (x) => x == value);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Returns a selector that checks if the value does not equal the given value using != */
|
|
68
|
-
export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
69
|
-
return expr(arg, (x) => x != value);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Returns a selector that checks if the value strictly equals the given value using === */
|
|
73
|
-
export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
74
|
-
return expr(arg, (x) => x === value);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Returns a selector that checks if the value strictly does not equal the given value using !== */
|
|
78
|
-
export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
79
|
-
return expr(arg, (x) => x !== value);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Returns a selector that formats the value using the specified format string.
|
|
83
|
-
* Format strings use semicolon-separated syntax: "formatType;param1;param2"
|
|
84
|
-
* @param arg - The accessor chain to the value
|
|
85
|
-
* @param fmt - The format string
|
|
86
|
-
* @param nullText - Optional text to display for null/undefined values
|
|
87
|
-
* @example
|
|
88
|
-
* format(m.price, "n;2") // formats as number with 2 decimal places
|
|
89
|
-
* format(m.date, "d") // formats as date
|
|
90
|
-
* format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
|
|
91
|
-
* format(m.value, "n;2", "N/A") // shows "N/A" for null values
|
|
92
|
-
*/
|
|
93
|
-
export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
|
|
94
|
-
let f = nullText != null ? `${fmt}|${nullText}` : fmt;
|
|
95
|
-
return computable(arg, (x) => Format.value(x, f));
|
|
96
|
-
}
|
|
1
|
+
import { computable } from "../data/computable";
|
|
2
|
+
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
3
|
+
import { Selector } from "../data/Selector";
|
|
4
|
+
import { Format } from "../util/Format";
|
|
5
|
+
import { expr } from "./expr";
|
|
6
|
+
|
|
7
|
+
/** Returns a selector that converts the value to boolean using !! */
|
|
8
|
+
export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
9
|
+
return expr(arg, (x) => !!x);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Returns a selector that checks if the value is falsy using ! */
|
|
13
|
+
export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
14
|
+
return expr(arg, (x) => !x);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Returns a selector that checks if the value is strictly true (=== true) */
|
|
18
|
+
export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
|
|
19
|
+
return expr(arg, (x) => x === true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Returns a selector that checks if the value is strictly false (=== false) */
|
|
23
|
+
export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
|
|
24
|
+
return expr(arg, (x) => x === false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns a selector that checks if the value is not null or undefined (x != null) */
|
|
28
|
+
export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
29
|
+
return expr(arg, (x) => x != null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
|
|
33
|
+
export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
34
|
+
return expr(arg, (x) => x == null || x.length === 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
|
|
38
|
+
export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
39
|
+
return expr(arg, (x) => x != null && x.length > 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returns a selector that checks if the value is less than the given value */
|
|
43
|
+
export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
44
|
+
return expr(arg, (x) => x < value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Returns a selector that checks if the value is less than or equal to the given value */
|
|
48
|
+
export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
49
|
+
return expr(arg, (x) => x <= value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns a selector that checks if the value is greater than the given value */
|
|
53
|
+
export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
54
|
+
return expr(arg, (x) => x > value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns a selector that checks if the value is greater than or equal to the given value */
|
|
58
|
+
export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
59
|
+
return expr(arg, (x) => x >= value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Returns a selector that checks if the value equals the given value using == */
|
|
63
|
+
export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
64
|
+
return expr(arg, (x) => x == value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Returns a selector that checks if the value does not equal the given value using != */
|
|
68
|
+
export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
69
|
+
return expr(arg, (x) => x != value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Returns a selector that checks if the value strictly equals the given value using === */
|
|
73
|
+
export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
74
|
+
return expr(arg, (x) => x === value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns a selector that checks if the value strictly does not equal the given value using !== */
|
|
78
|
+
export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
79
|
+
return expr(arg, (x) => x !== value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Returns a selector that formats the value using the specified format string.
|
|
83
|
+
* Format strings use semicolon-separated syntax: "formatType;param1;param2"
|
|
84
|
+
* @param arg - The accessor chain to the value
|
|
85
|
+
* @param fmt - The format string
|
|
86
|
+
* @param nullText - Optional text to display for null/undefined values
|
|
87
|
+
* @example
|
|
88
|
+
* format(m.price, "n;2") // formats as number with 2 decimal places
|
|
89
|
+
* format(m.date, "d") // formats as date
|
|
90
|
+
* format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
|
|
91
|
+
* format(m.value, "n;2", "N/A") // shows "N/A" for null values
|
|
92
|
+
*/
|
|
93
|
+
export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
|
|
94
|
+
let f = nullText != null ? `${fmt}|${nullText}` : fmt;
|
|
95
|
+
return computable(arg, (x) => Format.value(x, f));
|
|
96
|
+
}
|
|
@@ -80,6 +80,8 @@ export class DocumentTitle extends Widget<DocumentTitleConfig> {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
prepare(context: RenderingContext, instance: Instance): void {
|
|
83
|
+
if (typeof document == "undefined") return;
|
|
84
|
+
|
|
83
85
|
if ((context as any).documentTitle.activeInstance == instance)
|
|
84
86
|
document.title = (context as any).documentTitle.title;
|
|
85
87
|
}
|