Repository: vazco/uniforms (ts)

Issue:
AutoForm with autosave validates stale model
[Reproduction playground](https://uniforms.tools/playground/#?N4IgDgTgpgzlAuIBcIDqUA2BjA9gWygEIQAacCHMGZUAQwFd4cZaA3KZeCeqMhpluwAimWgE9kABjIATAJYsARhigzkAM1oY4ZDLUWZO3XuD1YoACxwYZUCBq06QMLBah5ayEADsoAdwACAGU5PDAVINd3WgAmACEIORkAcygACgAdbwCA30CQsIiojzTgLJycmDkALygkALLsipz4MTA64K45b2SScuaArQwcP1UANS0eGHqAbQByAA8YOZIAueXVubwVtYwdxb2AXT6mnIBffrOASiyr0mcrPwBJbwxuqABRCAp7JE1tEy0GBibxYADy3iC9EUeDkiD-jkBwNBEImbxktHgHARALOZHgbgIXno3hgrRUajOQA). Workaround: set `autosaveDelay` to 1.

Pull request description:
Asynchronized autosave (fixes #832).
This change fixes #832 by asynchronizing autosave. This has one positive side effect - autosave is now batched.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35abba57..7f8c09ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## Next
+
+- **Breaking:** Due to synchronization of `AutoForm` `model`, autosave is now **always** asynchronous. [\#832](https://github.com/vazco/uniforms/issues/832)
+
 ## [v3.0.0-rc.7](https://github.com/vazco/uniforms/tree/v3.0.0-rc.7) (2020-11-30)
 
 - **Breaking:** Synchronous `return` and `throw` in `onSubmit` are no longer allowed. To return an error or some result, return `Promise` instead. [\#819](https://github.com/vazco/uniforms/issues/819)
diff --git a/packages/uniforms/src/BaseForm.tsx b/packages/uniforms/src/BaseForm.tsx
index 2436cee6..4b1eea16 100644
--- a/packages/uniforms/src/BaseForm.tsx
+++ b/packages/uniforms/src/BaseForm.tsx
@@ -207,13 +207,17 @@ export class BaseForm<
         this.delayId = clearTimeout(this.delayId);
       }
 
-      if (this.props.autosaveDelay > 0) {
-        this.delayId = setTimeout(() => {
-          this.onSubmit();
-        }, this.props.autosaveDelay);
-      } else {
-        this.onSubmit();
-      }
+      // Delay autosave by `autosaveDelay` milliseconds...
+      this.delayId = setTimeout(() => {
+        // ...and wait for all scheduled `setState`s to commit. This is required
+        // for AutoForm to validate correct model, waiting in `onChange`.
+        this.setState(
+          () => null,
+          () => {
+            this.onSubmit();
+          },
+        );
+      }, this.props.autosaveDelay);
     }
   }
 


Test patch:
diff --git a/packages/uniforms/__tests__/AutoForm.tsx b/packages/uniforms/__tests__/AutoForm.tsx
index bddd1e27..1645d502 100644
--- a/packages/uniforms/__tests__/AutoForm.tsx
+++ b/packages/uniforms/__tests__/AutoForm.tsx
@@ -115,10 +115,12 @@ describe('AutoForm', () => {
       expect(onSubmit).not.toBeCalled();
       wrapper.instance().getContext().onChange('a', 1);
 
-      await new Promise(resolve => process.nextTick(resolve));
+      await new Promise(resolve => setTimeout(resolve));
 
       expect(onSubmit).toHaveBeenCalledTimes(1);
       expect(onSubmit).toHaveBeenLastCalledWith({ a: 1 });
+      expect(validator).toHaveBeenCalledTimes(1);
+      expect(validator).toHaveBeenLastCalledWith({ a: 1 });
     });
   });
 
diff --git a/packages/uniforms/__tests__/BaseForm.tsx b/packages/uniforms/__tests__/BaseForm.tsx
index b69202a4..a884bace 100644
--- a/packages/uniforms/__tests__/BaseForm.tsx
+++ b/packages/uniforms/__tests__/BaseForm.tsx
@@ -135,28 +135,31 @@ describe('BaseForm', () => {
       expect(context2.changedMap.$?.[1]).toBeTruthy();
     });
 
-    it('autosaves correctly (`autosave` = true)', () => {
+    it('autosaves correctly (`autosave` = true)', async () => {
       wrapper.setProps({ autosave: true });
       wrapper.instance().getContext().onChange('a', 1);
+      await new Promise(resolve => setTimeout(resolve));
 
       expect(onSubmit).toHaveBeenCalledTimes(1);
       expect(onSubmit).toHaveBeenLastCalledWith(model);
     });
 
-    it('autosaves are not delayed', () => {
+    it('autosaves are not delayed', async () => {
       wrapper.instance().getContext().onChange('a', 1);
-      wrapper.instance().getContext().onChange('a', 2);
-      wrapper.instance().getContext().onChange('a', 3);
+      await new Promise(resolve => setTimeout(resolve));
 
-      expect(onSubmit).toHaveBeenCalledTimes(3);
+      expect(onSubmit).toHaveBeenCalledTimes(1);
       expect(onSubmit).toHaveBeenLastCalledWith(model);
     });
 
     it('autosaves can be delayed', async () => {
-      wrapper.setProps({ autosaveDelay: 10 });
+      wrapper.setProps({ autosaveDelay: 25 });
       wrapper.instance().getContext().onChange('a', 1);
       wrapper.instance().getContext().onChange('a', 2);
       wrapper.instance().getContext().onChange('a', 3);
+      await new Promise(resolve => setTimeout(resolve));
+
+      expect(onSubmit).not.toHaveBeenCalled();
 
       await new Promise(resolve => setTimeout(resolve, 25));