rask-ui 0.28.3 → 0.29.0
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/README.md +1 -1
- package/dist/component.d.ts +4 -3
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +37 -57
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/render.js +2 -2
- package/dist/scheduler.d.ts +2 -3
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +31 -104
- package/dist/tests/batch.test.js +202 -12
- package/dist/tests/createContext.test.js +50 -37
- package/dist/tests/error.test.js +25 -12
- package/dist/tests/renderCount.test.d.ts +2 -0
- package/dist/tests/renderCount.test.d.ts.map +1 -0
- package/dist/tests/renderCount.test.js +95 -0
- package/dist/tests/scopeEnforcement.test.d.ts +2 -0
- package/dist/tests/scopeEnforcement.test.d.ts.map +1 -0
- package/dist/tests/scopeEnforcement.test.js +157 -0
- package/dist/tests/useAction.test.d.ts +2 -0
- package/dist/tests/useAction.test.d.ts.map +1 -0
- package/dist/tests/useAction.test.js +132 -0
- package/dist/tests/useAsync.test.d.ts +2 -0
- package/dist/tests/useAsync.test.d.ts.map +1 -0
- package/dist/tests/useAsync.test.js +499 -0
- package/dist/tests/useDerived.test.d.ts +2 -0
- package/dist/tests/useDerived.test.d.ts.map +1 -0
- package/dist/tests/useDerived.test.js +407 -0
- package/dist/tests/useEffect.test.d.ts +2 -0
- package/dist/tests/useEffect.test.d.ts.map +1 -0
- package/dist/tests/useEffect.test.js +600 -0
- package/dist/tests/useLookup.test.d.ts +2 -0
- package/dist/tests/useLookup.test.d.ts.map +1 -0
- package/dist/tests/useLookup.test.js +299 -0
- package/dist/tests/useRef.test.d.ts +2 -0
- package/dist/tests/useRef.test.d.ts.map +1 -0
- package/dist/tests/useRef.test.js +189 -0
- package/dist/tests/useState.test.d.ts +2 -0
- package/dist/tests/useState.test.d.ts.map +1 -0
- package/dist/tests/useState.test.js +178 -0
- package/dist/tests/useSuspend.test.d.ts +2 -0
- package/dist/tests/useSuspend.test.d.ts.map +1 -0
- package/dist/tests/useSuspend.test.js +752 -0
- package/dist/tests/useView.test.d.ts +2 -0
- package/dist/tests/useView.test.d.ts.map +1 -0
- package/dist/tests/useView.test.js +305 -0
- package/dist/useAsync.d.ts.map +1 -1
- package/dist/useAsync.js +12 -11
- package/dist/useDerived.d.ts +1 -1
- package/dist/useDerived.d.ts.map +1 -1
- package/dist/useDerived.js +9 -63
- package/dist/useEffect.d.ts.map +1 -1
- package/dist/useEffect.js +4 -19
- package/dist/useLookup.d.ts.map +1 -1
- package/dist/useLookup.js +9 -14
- package/dist/useRef.d.ts.map +1 -1
- package/dist/useRef.js +4 -8
- package/dist/useRouter.d.ts.map +1 -1
- package/dist/useRouter.js +4 -8
- package/dist/useState.d.ts +0 -1
- package/dist/useState.d.ts.map +1 -1
- package/dist/useState.js +2 -100
- package/dist/useSuspend.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<img src="https://raw.githubusercontent.com/christianalfoni/rask-ui/main/logo.png" alt="Logo" width="200">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
A lightweight reactive component library
|
|
7
|
+
A lightweight reactive component library built on battle-tested technologies: **Inferno's** highly optimized reconciler and **MobX's** proven reactive state management.
|
|
8
8
|
|
|
9
9
|
**[Visit rask-ui.io for full documentation](https://rask-ui.io)**
|
|
10
10
|
|
package/dist/component.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VNode, Component, Props, InfernoNode } from "inferno";
|
|
2
|
-
import {
|
|
2
|
+
import { IObservableValue } from "mobx";
|
|
3
3
|
export declare function getCurrentComponent(): RaskComponent<any> | undefined;
|
|
4
4
|
export declare function useMountEffect(cb: () => void): void;
|
|
5
5
|
export declare function useCleanup(cb: () => void): void;
|
|
@@ -7,12 +7,13 @@ export type RaskStatelessFunctionComponent<P extends Props<any>> = (() => VNode)
|
|
|
7
7
|
export type RaskStatefulFunctionComponent<P extends Props<any>> = (() => () => VNode) | ((props: P) => () => VNode);
|
|
8
8
|
export declare class RaskComponent<P extends Props<any>> extends Component<P> {
|
|
9
9
|
renderFn: RaskStatelessFunctionComponent<P>;
|
|
10
|
-
propsSignals: Record<string,
|
|
10
|
+
propsSignals: Record<string, IObservableValue<any>>;
|
|
11
11
|
private reactiveProps;
|
|
12
12
|
private isNotified;
|
|
13
13
|
private isReconciling;
|
|
14
14
|
private hasChangedComponent;
|
|
15
|
-
|
|
15
|
+
private createReaction;
|
|
16
|
+
private reaction;
|
|
16
17
|
isRendering: boolean;
|
|
17
18
|
effects: Array<{
|
|
18
19
|
isDirty: boolean;
|
package/dist/component.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAwB,MAAM,MAAM,CAAC;AAM9D,wBAAgB,mBAAmB,mCAElC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,QAM5C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,QAMxC;AAED,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC3D,CAAC,MAAM,KAAK,CAAC,GACb,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;AAE1B,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC1D,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;IACpD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAM;IACzD,OAAO,CAAC,aAAa,CAAc;IAwBnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,QAAQ,CAAmC;IAEnD,WAAW,UAAS;IACpB,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,UAAU,CAAC,OAAO,EAAE,OAAO;IAS3B,eAAe;IAMf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IAEnC,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAI5B,yBAAyB,CACvB,SAAS,EAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,CAAC,CAAC,GAClD,IAAI;IAQP,qBAAqB,IAAI,OAAO;IAMhC,MAAM;CAoEP"}
|
package/dist/component.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Component } from "inferno";
|
|
2
|
-
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
3
|
-
import { syncBatch } from "./batch";
|
|
4
2
|
import { CatchErrorContext } from "./useCatchError";
|
|
3
|
+
import { Reaction, observable } from "mobx";
|
|
4
|
+
import { transaction } from "./scheduler";
|
|
5
|
+
import { assignState } from "./useState";
|
|
5
6
|
let currentComponent;
|
|
6
7
|
export function getCurrentComponent() {
|
|
7
8
|
return currentComponent;
|
|
@@ -46,13 +47,16 @@ export class RaskComponent extends Component {
|
|
|
46
47
|
isNotified = false;
|
|
47
48
|
isReconciling = false;
|
|
48
49
|
hasChangedComponent = true;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
createReaction() {
|
|
51
|
+
return new Reaction("ComponentRender", () => {
|
|
52
|
+
if (this.isReconciling) {
|
|
53
|
+
this.isNotified = true;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.forceUpdate();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
reaction = this.createReaction();
|
|
56
60
|
// Flag to prevent props from tracking in render scope (We use props reconciliation)
|
|
57
61
|
isRendering = false;
|
|
58
62
|
effects = [];
|
|
@@ -80,17 +84,10 @@ export class RaskComponent extends Component {
|
|
|
80
84
|
}
|
|
81
85
|
componentWillReceiveProps(nextProps) {
|
|
82
86
|
this.isReconciling = true;
|
|
83
|
-
const prevProps = this.props;
|
|
84
|
-
this.props = nextProps;
|
|
85
87
|
this.hasChangedComponent =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (prevProps[prop] === nextProps[prop]) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
this.propsSignals[prop].notify();
|
|
93
|
-
}
|
|
88
|
+
nextProps.__component !== this.props.__component;
|
|
89
|
+
transaction(() => {
|
|
90
|
+
assignState(this.reactiveProps, nextProps);
|
|
94
91
|
});
|
|
95
92
|
}
|
|
96
93
|
shouldComponentUpdate() {
|
|
@@ -101,17 +98,17 @@ export class RaskComponent extends Component {
|
|
|
101
98
|
}
|
|
102
99
|
render() {
|
|
103
100
|
currentComponent = this;
|
|
104
|
-
const stopObserving = this.observer.observe();
|
|
105
101
|
try {
|
|
106
102
|
if (this.hasChangedComponent) {
|
|
107
103
|
this.hasChangedComponent = false;
|
|
108
104
|
this.componentWillUnmount();
|
|
109
|
-
this.reactiveProps =
|
|
105
|
+
this.reactiveProps = observable(this.props);
|
|
110
106
|
const component = this.props.__component;
|
|
111
107
|
const renderFn = component(this.reactiveProps);
|
|
112
108
|
if (typeof renderFn === "function") {
|
|
113
109
|
// Since we ran a setup function we need to clear any signals accessed
|
|
114
|
-
this.
|
|
110
|
+
this.reaction.dispose();
|
|
111
|
+
this.reaction = this.createReaction();
|
|
115
112
|
this.renderFn = renderFn;
|
|
116
113
|
}
|
|
117
114
|
else {
|
|
@@ -121,7 +118,24 @@ export class RaskComponent extends Component {
|
|
|
121
118
|
}
|
|
122
119
|
let result = null;
|
|
123
120
|
this.isRendering = true;
|
|
124
|
-
|
|
121
|
+
this.reaction.track(() => {
|
|
122
|
+
try {
|
|
123
|
+
result = this.renderFn(this.reactiveProps);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
const notifyError = CatchErrorContext.use();
|
|
128
|
+
if (typeof notifyError !== "function") {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
notifyError(error);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
125
139
|
this.isRendering = false;
|
|
126
140
|
return result;
|
|
127
141
|
}
|
|
@@ -139,41 +153,7 @@ export class RaskComponent extends Component {
|
|
|
139
153
|
}
|
|
140
154
|
}
|
|
141
155
|
finally {
|
|
142
|
-
stopObserving();
|
|
143
156
|
currentComponent = undefined;
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
}
|
|
147
|
-
function createReactiveProps(comp) {
|
|
148
|
-
const props = new Proxy({}, {
|
|
149
|
-
ownKeys() {
|
|
150
|
-
return Object.getOwnPropertyNames(comp.props);
|
|
151
|
-
},
|
|
152
|
-
getOwnPropertyDescriptor(_, prop) {
|
|
153
|
-
return {
|
|
154
|
-
configurable: true,
|
|
155
|
-
enumerable: true,
|
|
156
|
-
value: comp.props[prop],
|
|
157
|
-
writable: false,
|
|
158
|
-
};
|
|
159
|
-
},
|
|
160
|
-
get(_, prop) {
|
|
161
|
-
// Skip known non-reactive props
|
|
162
|
-
if (prop === "key" || prop === "ref") {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const observer = getCurrentObserver();
|
|
166
|
-
if (observer) {
|
|
167
|
-
// Lazy create signal only when accessed in reactive context
|
|
168
|
-
let signal = comp.propsSignals[prop];
|
|
169
|
-
if (!signal) {
|
|
170
|
-
signal = new Signal();
|
|
171
|
-
comp.propsSignals[prop] = signal;
|
|
172
|
-
}
|
|
173
|
-
observer.subscribeSignal(signal);
|
|
174
|
-
}
|
|
175
|
-
return comp.props[prop];
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
return props;
|
|
179
|
-
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,6 @@ export { useRef, assignRef, Ref } from "./useRef";
|
|
|
11
11
|
export { useView } from "./useView";
|
|
12
12
|
export { useEffect } from "./useEffect";
|
|
13
13
|
export { useDerived, Derived } from "./useDerived";
|
|
14
|
-
export { syncBatch } from "./batch";
|
|
15
14
|
export { inspect } from "./inspect";
|
|
16
15
|
export { Router, useRouter } from "./useRouter";
|
|
17
16
|
export { useLookup } from "./useLookup";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,CAAC;AAEjB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,CAAC;AAEjB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,6 @@ export { useRef, assignRef } from "./useRef";
|
|
|
12
12
|
export { useView } from "./useView";
|
|
13
13
|
export { useEffect } from "./useEffect";
|
|
14
14
|
export { useDerived } from "./useDerived";
|
|
15
|
-
export { syncBatch } from "./batch";
|
|
16
15
|
export { inspect } from "./inspect";
|
|
17
16
|
export { useRouter } from "./useRouter";
|
|
18
17
|
export { useLookup } from "./useLookup";
|
package/dist/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { render as infernoRender } from "inferno";
|
|
2
|
-
import {
|
|
2
|
+
import { transaction } from "./scheduler";
|
|
3
3
|
/**
|
|
4
4
|
* Renders a component with automatic event batching.
|
|
5
5
|
* Temporarily patches document.addEventListener to wrap
|
|
@@ -42,7 +42,7 @@ export function render(...params) {
|
|
|
42
42
|
!patchedEvents.has(type)) {
|
|
43
43
|
patchedEvents.add(type);
|
|
44
44
|
const wrappedListener = function (event) {
|
|
45
|
-
|
|
45
|
+
transaction(() => {
|
|
46
46
|
listener.call(this, event);
|
|
47
47
|
});
|
|
48
48
|
};
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export declare function
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function installGlobalBatching(target?: EventTarget): () => void;
|
|
1
|
+
export declare function transaction(cb: () => void): void;
|
|
2
|
+
export declare function autorun(cb: () => void): import("mobx").IReactionDisposer;
|
|
4
3
|
//# sourceMappingURL=scheduler.d.ts.map
|
package/dist/scheduler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAQA,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,IAAI,QAKzC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,oCAOrC"}
|
package/dist/scheduler.js
CHANGED
|
@@ -1,107 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// TODO: call your Inferno render/commit once.
|
|
9
|
-
// infernoRender(vnode, container);
|
|
10
|
-
}
|
|
11
|
-
function flushNow() {
|
|
12
|
-
scheduled = false;
|
|
13
|
-
if (!dirty)
|
|
14
|
-
return;
|
|
15
|
-
dirty = false;
|
|
16
|
-
performWork();
|
|
17
|
-
}
|
|
18
|
-
// Called by setters after enqueueing their state change
|
|
19
|
-
export function enqueueUpdateFromSetter() {
|
|
20
|
-
dirty = true;
|
|
21
|
-
if (depth > 0) {
|
|
22
|
-
// We're inside a batched input event; we'll flush on exit (same frame).
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
if (!scheduled) {
|
|
26
|
-
scheduled = true;
|
|
27
|
-
queueMicrotask(flushNow); // one flush per task
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Batch-scope control used by the global capture listeners
|
|
31
|
-
function enter() {
|
|
32
|
-
depth++;
|
|
33
|
-
}
|
|
34
|
-
function exit() {
|
|
35
|
-
if (--depth === 0) {
|
|
36
|
-
// End of the event propagation; commit now (before next paint).
|
|
37
|
-
flushNow();
|
|
38
|
-
}
|
|
1
|
+
import { configure, transaction as mobxTransaction, autorun as mobxAutorun, } from "mobx";
|
|
2
|
+
let isSync = false;
|
|
3
|
+
export function transaction(cb) {
|
|
4
|
+
isSync = true;
|
|
5
|
+
mobxTransaction(() => {
|
|
6
|
+
cb();
|
|
7
|
+
});
|
|
39
8
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"dblclick",
|
|
45
|
-
"contextmenu",
|
|
46
|
-
"mousedown",
|
|
47
|
-
"mouseup",
|
|
48
|
-
"mousemove",
|
|
49
|
-
"pointerdown",
|
|
50
|
-
"pointerup",
|
|
51
|
-
"pointermove",
|
|
52
|
-
"touchstart",
|
|
53
|
-
"touchmove",
|
|
54
|
-
"touchend",
|
|
55
|
-
"touchcancel",
|
|
56
|
-
"dragstart",
|
|
57
|
-
"drag",
|
|
58
|
-
"dragend",
|
|
59
|
-
"dragenter",
|
|
60
|
-
"dragleave",
|
|
61
|
-
"dragover",
|
|
62
|
-
"drop",
|
|
63
|
-
"wheel",
|
|
64
|
-
// Keyboard
|
|
65
|
-
"keydown",
|
|
66
|
-
"keypress",
|
|
67
|
-
"keyup",
|
|
68
|
-
// Focus & input
|
|
69
|
-
"focus",
|
|
70
|
-
"blur",
|
|
71
|
-
"focusin",
|
|
72
|
-
"focusout",
|
|
73
|
-
"input",
|
|
74
|
-
"beforeinput",
|
|
75
|
-
"change",
|
|
76
|
-
"compositionstart",
|
|
77
|
-
"compositionupdate",
|
|
78
|
-
"compositionend",
|
|
79
|
-
// Forms
|
|
80
|
-
"submit",
|
|
81
|
-
"reset",
|
|
82
|
-
// Selection / clipboard
|
|
83
|
-
"select",
|
|
84
|
-
"selectionchange",
|
|
85
|
-
"copy",
|
|
86
|
-
"cut",
|
|
87
|
-
"paste",
|
|
88
|
-
];
|
|
89
|
-
export function installGlobalBatching(target = window) {
|
|
90
|
-
const handlers = [];
|
|
91
|
-
INTERACTIVE_EVENTS.forEach((type) => {
|
|
92
|
-
const onCapture = () => {
|
|
93
|
-
enter();
|
|
94
|
-
// Close the scope after all handlers (capture→target→bubble) have run.
|
|
95
|
-
queueMicrotask(exit);
|
|
96
|
-
};
|
|
97
|
-
target.addEventListener(type, onCapture, { capture: true });
|
|
98
|
-
handlers.push([onCapture, { capture: true }]);
|
|
9
|
+
export function autorun(cb) {
|
|
10
|
+
isSync = true;
|
|
11
|
+
const disposer = mobxAutorun(() => {
|
|
12
|
+
cb();
|
|
99
13
|
});
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
INTERACTIVE_EVENTS.forEach((type, i) => {
|
|
103
|
-
const [fn, opts] = handlers[i];
|
|
104
|
-
target.removeEventListener(type, fn, opts);
|
|
105
|
-
});
|
|
106
|
-
};
|
|
14
|
+
return disposer;
|
|
107
15
|
}
|
|
16
|
+
let hasQueuedFlush = false;
|
|
17
|
+
configure({
|
|
18
|
+
enforceActions: "never",
|
|
19
|
+
reactionScheduler: (f) => {
|
|
20
|
+
if (isSync) {
|
|
21
|
+
f(); // Flush immediately at the end the transaction
|
|
22
|
+
hasQueuedFlush = false;
|
|
23
|
+
isSync = false;
|
|
24
|
+
}
|
|
25
|
+
else if (!hasQueuedFlush) {
|
|
26
|
+
hasQueuedFlush = true;
|
|
27
|
+
queueMicrotask(() => {
|
|
28
|
+
isSync = true;
|
|
29
|
+
f();
|
|
30
|
+
hasQueuedFlush = false;
|
|
31
|
+
}); // Defer "loose" user changes
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
package/dist/tests/batch.test.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { syncBatch } from "../batch";
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "../useState";
|
|
4
4
|
import { Observer } from "../observation";
|
|
5
5
|
describe("syncBatch", () => {
|
|
6
6
|
it("should batch multiple state changes into a single notification", () => {
|
|
7
|
-
const state =
|
|
7
|
+
const state = useState({ count: 0, name: "Alice" });
|
|
8
8
|
let notifyCount = 0;
|
|
9
9
|
const observer = new Observer(() => {
|
|
10
10
|
notifyCount++;
|
|
@@ -26,7 +26,7 @@ describe("syncBatch", () => {
|
|
|
26
26
|
observer.dispose();
|
|
27
27
|
});
|
|
28
28
|
it("should handle nested batches correctly", () => {
|
|
29
|
-
const state =
|
|
29
|
+
const state = useState({ count: 0 });
|
|
30
30
|
let notifyCount = 0;
|
|
31
31
|
const observer = new Observer(() => {
|
|
32
32
|
notifyCount++;
|
|
@@ -47,7 +47,7 @@ describe("syncBatch", () => {
|
|
|
47
47
|
observer.dispose();
|
|
48
48
|
});
|
|
49
49
|
it("should handle multiple observers with syncBatch", () => {
|
|
50
|
-
const state =
|
|
50
|
+
const state = useState({ count: 0 });
|
|
51
51
|
let notifyCount1 = 0;
|
|
52
52
|
let notifyCount2 = 0;
|
|
53
53
|
const observer1 = new Observer(() => {
|
|
@@ -74,7 +74,7 @@ describe("syncBatch", () => {
|
|
|
74
74
|
observer2.dispose();
|
|
75
75
|
});
|
|
76
76
|
it("should maintain correct state values after syncBatch", () => {
|
|
77
|
-
const state =
|
|
77
|
+
const state = useState({
|
|
78
78
|
count: 0,
|
|
79
79
|
name: "Alice",
|
|
80
80
|
items: [1, 2, 3],
|
|
@@ -90,7 +90,7 @@ describe("syncBatch", () => {
|
|
|
90
90
|
expect(state.items).toEqual([100, 2, 3, 4]);
|
|
91
91
|
});
|
|
92
92
|
it("should not flush if exception thrown within syncBatch", () => {
|
|
93
|
-
const state =
|
|
93
|
+
const state = useState({ count: 0 });
|
|
94
94
|
let notifyCount = 0;
|
|
95
95
|
const observer = new Observer(() => {
|
|
96
96
|
notifyCount++;
|
|
@@ -114,7 +114,7 @@ describe("syncBatch", () => {
|
|
|
114
114
|
observer.dispose();
|
|
115
115
|
});
|
|
116
116
|
it("should deduplicate notifications for the same observer", () => {
|
|
117
|
-
const state =
|
|
117
|
+
const state = useState({ count: 0, name: "Alice" });
|
|
118
118
|
let notifyCount = 0;
|
|
119
119
|
const observer = new Observer(() => {
|
|
120
120
|
notifyCount++;
|
|
@@ -135,7 +135,7 @@ describe("syncBatch", () => {
|
|
|
135
135
|
});
|
|
136
136
|
describe("queue (async batching)", () => {
|
|
137
137
|
it("should queue updates and flush on microtask", async () => {
|
|
138
|
-
const state =
|
|
138
|
+
const state = useState({ count: 0 });
|
|
139
139
|
let notifyCount = 0;
|
|
140
140
|
const observer = new Observer(() => {
|
|
141
141
|
notifyCount++;
|
|
@@ -157,7 +157,7 @@ describe("queue (async batching)", () => {
|
|
|
157
157
|
observer.dispose();
|
|
158
158
|
});
|
|
159
159
|
it("should batch multiple async updates into one notification", async () => {
|
|
160
|
-
const state =
|
|
160
|
+
const state = useState({ count: 0, name: "Alice" });
|
|
161
161
|
let notifyCount = 0;
|
|
162
162
|
const observer = new Observer(() => {
|
|
163
163
|
notifyCount++;
|
|
@@ -175,7 +175,7 @@ describe("queue (async batching)", () => {
|
|
|
175
175
|
observer.dispose();
|
|
176
176
|
});
|
|
177
177
|
it("should handle separate async batches", async () => {
|
|
178
|
-
const state =
|
|
178
|
+
const state = useState({ count: 0 });
|
|
179
179
|
let notifyCount = 0;
|
|
180
180
|
const observer = new Observer(() => {
|
|
181
181
|
notifyCount++;
|
|
@@ -196,7 +196,7 @@ describe("queue (async batching)", () => {
|
|
|
196
196
|
});
|
|
197
197
|
describe("syncBatch with nested async updates", () => {
|
|
198
198
|
it("should handle syncBatch inside async context", async () => {
|
|
199
|
-
const state =
|
|
199
|
+
const state = useState({ count: 0 });
|
|
200
200
|
let notifyCount = 0;
|
|
201
201
|
const observer = new Observer(() => {
|
|
202
202
|
notifyCount++;
|
|
@@ -217,7 +217,7 @@ describe("syncBatch with nested async updates", () => {
|
|
|
217
217
|
observer.dispose();
|
|
218
218
|
});
|
|
219
219
|
it("should handle async updates inside syncBatch callback", async () => {
|
|
220
|
-
const state =
|
|
220
|
+
const state = useState({ count: 0 });
|
|
221
221
|
let notifyCount = 0;
|
|
222
222
|
const observer = new Observer(() => {
|
|
223
223
|
notifyCount++;
|
|
@@ -242,3 +242,193 @@ describe("syncBatch with nested async updates", () => {
|
|
|
242
242
|
observer.dispose();
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
|
+
describe("syncBatch with cascading updates", () => {
|
|
246
|
+
it("should handle cascading observer notifications within the same batch", () => {
|
|
247
|
+
const state = useState({ count: 0 });
|
|
248
|
+
const derived = useState({ doubled: 0 });
|
|
249
|
+
let stateNotifyCount = 0;
|
|
250
|
+
let derivedNotifyCount = 0;
|
|
251
|
+
let componentNotifyCount = 0;
|
|
252
|
+
// Observer 1: Watches state, updates derived (simulates useDerived)
|
|
253
|
+
const derivedObserver = new Observer(() => {
|
|
254
|
+
stateNotifyCount++;
|
|
255
|
+
// When state changes, update derived synchronously
|
|
256
|
+
derived.doubled = state.count * 2;
|
|
257
|
+
});
|
|
258
|
+
const dispose1 = derivedObserver.observe();
|
|
259
|
+
state.count; // Track state
|
|
260
|
+
dispose1();
|
|
261
|
+
// Observer 2: Watches derived (simulates component)
|
|
262
|
+
const componentObserver = new Observer(() => {
|
|
263
|
+
derivedNotifyCount++;
|
|
264
|
+
});
|
|
265
|
+
const dispose2 = componentObserver.observe();
|
|
266
|
+
derived.doubled; // Track derived
|
|
267
|
+
dispose2();
|
|
268
|
+
// Observer 3: Also watches derived (another component)
|
|
269
|
+
const component2Observer = new Observer(() => {
|
|
270
|
+
componentNotifyCount++;
|
|
271
|
+
});
|
|
272
|
+
const dispose3 = component2Observer.observe();
|
|
273
|
+
derived.doubled; // Track derived
|
|
274
|
+
dispose3();
|
|
275
|
+
// Make a change in a batch
|
|
276
|
+
syncBatch(() => {
|
|
277
|
+
state.count = 5;
|
|
278
|
+
});
|
|
279
|
+
// All observers should have been notified exactly once
|
|
280
|
+
expect(stateNotifyCount).toBe(1);
|
|
281
|
+
expect(derivedNotifyCount).toBe(1);
|
|
282
|
+
expect(componentNotifyCount).toBe(1);
|
|
283
|
+
expect(state.count).toBe(5);
|
|
284
|
+
expect(derived.doubled).toBe(10);
|
|
285
|
+
derivedObserver.dispose();
|
|
286
|
+
componentObserver.dispose();
|
|
287
|
+
component2Observer.dispose();
|
|
288
|
+
});
|
|
289
|
+
it("should handle multi-level cascading updates", () => {
|
|
290
|
+
const state = useState({ value: 0 });
|
|
291
|
+
const derived1 = useState({ level1: 0 });
|
|
292
|
+
const derived2 = useState({ level2: 0 });
|
|
293
|
+
const derived3 = useState({ level3: 0 });
|
|
294
|
+
const notifyCounts = [0, 0, 0, 0];
|
|
295
|
+
// Level 1: state -> derived1
|
|
296
|
+
const observer1 = new Observer(() => {
|
|
297
|
+
notifyCounts[0]++;
|
|
298
|
+
derived1.level1 = state.value + 1;
|
|
299
|
+
});
|
|
300
|
+
const dispose1 = observer1.observe();
|
|
301
|
+
state.value;
|
|
302
|
+
dispose1();
|
|
303
|
+
// Level 2: derived1 -> derived2
|
|
304
|
+
const observer2 = new Observer(() => {
|
|
305
|
+
notifyCounts[1]++;
|
|
306
|
+
derived2.level2 = derived1.level1 + 1;
|
|
307
|
+
});
|
|
308
|
+
const dispose2 = observer2.observe();
|
|
309
|
+
derived1.level1;
|
|
310
|
+
dispose2();
|
|
311
|
+
// Level 3: derived2 -> derived3
|
|
312
|
+
const observer3 = new Observer(() => {
|
|
313
|
+
notifyCounts[2]++;
|
|
314
|
+
derived3.level3 = derived2.level2 + 1;
|
|
315
|
+
});
|
|
316
|
+
const dispose3 = observer3.observe();
|
|
317
|
+
derived2.level2;
|
|
318
|
+
dispose3();
|
|
319
|
+
// Final observer: watches derived3
|
|
320
|
+
const observer4 = new Observer(() => {
|
|
321
|
+
notifyCounts[3]++;
|
|
322
|
+
});
|
|
323
|
+
const dispose4 = observer4.observe();
|
|
324
|
+
derived3.level3;
|
|
325
|
+
dispose4();
|
|
326
|
+
// Update state in a batch
|
|
327
|
+
syncBatch(() => {
|
|
328
|
+
state.value = 10;
|
|
329
|
+
});
|
|
330
|
+
// All levels should have cascaded and each observer notified exactly once
|
|
331
|
+
expect(notifyCounts).toEqual([1, 1, 1, 1]);
|
|
332
|
+
expect(state.value).toBe(10);
|
|
333
|
+
expect(derived1.level1).toBe(11);
|
|
334
|
+
expect(derived2.level2).toBe(12);
|
|
335
|
+
expect(derived3.level3).toBe(13);
|
|
336
|
+
observer1.dispose();
|
|
337
|
+
observer2.dispose();
|
|
338
|
+
observer3.dispose();
|
|
339
|
+
observer4.dispose();
|
|
340
|
+
});
|
|
341
|
+
it("should handle diamond dependency pattern", () => {
|
|
342
|
+
// Diamond: state -> [derived1, derived2] -> derived3
|
|
343
|
+
const state = useState({ value: 0 });
|
|
344
|
+
const derived1 = useState({ path1: 0 });
|
|
345
|
+
const derived2 = useState({ path2: 0 });
|
|
346
|
+
const derived3 = useState({ combined: 0 });
|
|
347
|
+
let derived3NotifyCount = 0;
|
|
348
|
+
// State -> derived1
|
|
349
|
+
const obs1 = new Observer(() => {
|
|
350
|
+
derived1.path1 = state.value * 2;
|
|
351
|
+
});
|
|
352
|
+
const d1 = obs1.observe();
|
|
353
|
+
state.value;
|
|
354
|
+
d1();
|
|
355
|
+
// State -> derived2
|
|
356
|
+
const obs2 = new Observer(() => {
|
|
357
|
+
derived2.path2 = state.value * 3;
|
|
358
|
+
});
|
|
359
|
+
const d2 = obs2.observe();
|
|
360
|
+
state.value;
|
|
361
|
+
d2();
|
|
362
|
+
// [derived1, derived2] -> derived3
|
|
363
|
+
const obs3 = new Observer(() => {
|
|
364
|
+
derived3.combined = derived1.path1 + derived2.path2;
|
|
365
|
+
});
|
|
366
|
+
const d3 = obs3.observe();
|
|
367
|
+
derived1.path1;
|
|
368
|
+
derived2.path2;
|
|
369
|
+
d3();
|
|
370
|
+
// Watch derived3
|
|
371
|
+
const obs4 = new Observer(() => {
|
|
372
|
+
derived3NotifyCount++;
|
|
373
|
+
});
|
|
374
|
+
const d4 = obs4.observe();
|
|
375
|
+
derived3.combined;
|
|
376
|
+
d4();
|
|
377
|
+
syncBatch(() => {
|
|
378
|
+
state.value = 5;
|
|
379
|
+
});
|
|
380
|
+
// derived3 should only be notified once despite two paths updating
|
|
381
|
+
expect(derived3NotifyCount).toBe(1);
|
|
382
|
+
expect(derived1.path1).toBe(10);
|
|
383
|
+
expect(derived2.path2).toBe(15);
|
|
384
|
+
expect(derived3.combined).toBe(25);
|
|
385
|
+
obs1.dispose();
|
|
386
|
+
obs2.dispose();
|
|
387
|
+
obs3.dispose();
|
|
388
|
+
obs4.dispose();
|
|
389
|
+
});
|
|
390
|
+
it("should not create infinite loops with circular dependencies", () => {
|
|
391
|
+
const state1 = useState({ value: 0 });
|
|
392
|
+
const state2 = useState({ value: 0 });
|
|
393
|
+
let notify1Count = 0;
|
|
394
|
+
let notify2Count = 0;
|
|
395
|
+
// Observer 1: watches state1, updates state2
|
|
396
|
+
const obs1 = new Observer(() => {
|
|
397
|
+
notify1Count++;
|
|
398
|
+
if (notify1Count > 10) {
|
|
399
|
+
throw new Error("Infinite loop detected");
|
|
400
|
+
}
|
|
401
|
+
// Only update if different to break the cycle
|
|
402
|
+
if (state2.value !== state1.value + 1) {
|
|
403
|
+
state2.value = state1.value + 1;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const d1 = obs1.observe();
|
|
407
|
+
state1.value;
|
|
408
|
+
d1();
|
|
409
|
+
// Observer 2: watches state2, updates state1
|
|
410
|
+
const obs2 = new Observer(() => {
|
|
411
|
+
notify2Count++;
|
|
412
|
+
if (notify2Count > 10) {
|
|
413
|
+
throw new Error("Infinite loop detected");
|
|
414
|
+
}
|
|
415
|
+
// Only update if different to break the cycle
|
|
416
|
+
if (state1.value !== state2.value - 1) {
|
|
417
|
+
state1.value = state2.value - 1;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
const d2 = obs2.observe();
|
|
421
|
+
state2.value;
|
|
422
|
+
d2();
|
|
423
|
+
syncBatch(() => {
|
|
424
|
+
state1.value = 5;
|
|
425
|
+
});
|
|
426
|
+
// Should stabilize without infinite loop
|
|
427
|
+
expect(notify1Count).toBeLessThan(10);
|
|
428
|
+
expect(notify2Count).toBeLessThan(10);
|
|
429
|
+
expect(state1.value).toBe(5);
|
|
430
|
+
expect(state2.value).toBe(6);
|
|
431
|
+
obs1.dispose();
|
|
432
|
+
obs2.dispose();
|
|
433
|
+
});
|
|
434
|
+
});
|