public async Task ExampleUsage2() { var t = Log.MethodEntered("DataStoreExample2.ExampleUsage2"); // Add a thunk middleware to allow dispatching async actions: var thunkMiddleware = Middlewares.NewThunkMiddleware <MyAppState1>(); // aDD A logging middleware to log all dispatched actions: var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>(); // Add a recorder middleware to enable hot reload by replaying previously recorded actions: var recorder = new ReplayRecorder <MyAppState1>(); var recMiddleware = recorder.CreateMiddleware(); var undoable = new UndoRedoReducer <MyAppState1>(); // To allow undo redo on the full store wrap the main reducer with the undo reducer: var undoReducer = undoable.Wrap(MyReducers1.ReduceMyAppState1); var data = new MyAppState1(null, null, 0); // the initial immutable state var store = new DataStore <MyAppState1>(undoReducer, data, loggingMiddleware, recMiddleware, thunkMiddleware); store.storeName = "Store 1"; TestNormalDispatchOfActions(store); TestUndoAndRedo(store); await TestAsyncActions(store); await TestReplayRecorder(recorder, store); TestSkippableUndoActions(store); Log.MethodDone(t); }
private static DataStore <MyModel> NewDataStore() { MyModel model = new MyModel(null, ImmutableList <MyCircle> .Empty); Middleware <MyModel> exampleMiddleware = Middlewares.NewLoggingMiddleware <MyModel>(); UndoRedoReducer <MyModel> undoLogic = new UndoRedoReducer <MyModel>(); return(new DataStore <MyModel>(undoLogic.Wrap(MyReducer), model, exampleMiddleware)); }
public static async Task ShowIn(ViewStack viewStack) { MyModel model = new MyModel(null, ImmutableList <MyUser> .Empty); Middleware <MyModel> exampleMiddleware = Middlewares.NewLoggingMiddleware <MyModel>(); UndoRedoReducer <MyModel> undoLogic = new UndoRedoReducer <MyModel>(); DataStore <MyModel> store = new DataStore <MyModel>(undoLogic.Wrap(MyReducer), model, exampleMiddleware); MyPresenter presenter = new MyPresenter(); presenter.targetView = viewStack.ShowView("7GUIs_Task5_CRUD"); await presenter.LoadModelIntoView(store); }
public override IEnumerator RunTest() { // Create an immutable datastore that will contain the data model in this example: var log = Middlewares.NewLoggingMiddleware <MyDataModel3>(); IDataStore <MyDataModel3> store = new DataStore <MyDataModel3>(MainReducer, new MyDataModel3(), log); IoC.inject.SetSingleton(store); // Create a presenter that connectes the model with the view (the Unity UI): var currentUserPresenter = new MyUserUi3(); // Set the target view by loading it from a prefab and setting the root GO: currentUserPresenter.targetView = ViewStackHelper.MainViewStack().ShowView("MyUserUi1"); // Connect the model changes with the presenter: currentUserPresenter.ListenToStoreUpdates(store, state => state.currentUser); // Dispatch a first setUser action to update the UI: store.Dispatch(new ActionSetNewUser() { newUser = new MyUser3("Carl", 99) }); // Delay needed since the UI update simulates a delay too: yield return(new WaitForSeconds(0.5f)); // Check that the UI was automatically updated: AssertV2.AreEqual("Carl", currentUserPresenter.NameUi().text); AssertV2.AreEqual("99", currentUserPresenter.AgeUi().text); // Simulate that the user changed the model via the UI: store.Dispatch(new ActionUpdateUser() { target = store.GetState().currentUser, newValues = new MyUser3("Paul", 0) }); // Delay needed since the UI update simulates a delay too: yield return(new WaitForSeconds(2f)); // Check that the UI was automatically updated: AssertV2.AreEqual("Paul", currentUserPresenter.NameUi().text); AssertV2.AreEqual("0", currentUserPresenter.AgeUi().text); }
private async Task TestReplayRecorderOnNewStore(ReplayRecorder <MyAppState1> recorder, MyAppState1 finalStateOfFirstStore) { var t = Log.MethodEntered("TestReplayRecorderOnNewStore"); // Connect the recorder to the new store: var recMiddleware = recorder.CreateMiddleware(); var undoable = new UndoRedoReducer <MyAppState1>(); var logging = Middlewares.NewLoggingMiddleware <MyAppState1>(); var data2 = new MyAppState1(null, null, 0); var store2 = new DataStore <MyAppState1>(undoable.Wrap(MyReducers1.ReduceMyAppState1), data2, logging, recMiddleware); store2.storeName = "Store 2"; // Replaying the recorder will now fill the second store with the same actions: await recorder.ReplayStore(); AssertEqualJson(finalStateOfFirstStore, store2.GetState()); Log.MethodDone(t); }
public static async Task ShowIn(ViewStack viewStack) { // Call model unit tests manually before the UI is shown: CellsModelTests.TestFromAndToRowName(); CellsModelTests.TestDataStoreTransitiveChanges(); CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); Middleware <CellsModel> logging = Middlewares.NewLoggingMiddleware <CellsModel>(); UndoRedoReducer <CellsModel> undoLogic = new UndoRedoReducer <CellsModel>(); DataStore <CellsModel> store = new DataStore <CellsModel>(undoLogic.Wrap(CellsReducers.MainReducer), model, logging); MyPresenter presenter = new MyPresenter(); presenter.targetView = viewStack.ShowView("7GUIs_Task7_Cells"); await presenter.LoadModelIntoView(store); await TaskV2.Delay(2000); Toast.Show("Now simulating some table model changes.."); // Simulate changes in the model to check if the UI updates correctly: CellsModelTests.SimulateSomeChangesInModel(store); }
public void ExampleUsage1() { var t = Log.MethodEntered("DataStoreExample3.ExampleUsage1"); // A middleware that will allow to use mutable data in the data store: var mutableMiddleware = Middlewares.NewMutableDataSupport <MyAppState1>(); // Add A logging middleware to log all dispatched actions: var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>(); MyUser1 user = new MyUser1() { name = "Carl" }; var model = new MyAppState1() { user = user }; var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, model, loggingMiddleware, mutableMiddleware); IoC.inject.SetSingleton(store); store.storeName = "Store 4"; // Setup 3 listeners that react when the name of the user or his contacts change: var userChangedCounter = 0; var userNameChangedCounter = 0; var contact1NameChangedCounter = 0; var contact2NameChangedCounter = 0; store.AddStateChangeListener(s => s.user, (MyUser1 theChangedUser) => { userChangedCounter++; }); store.AddStateChangeListener(s => s.user?.name, (string theChangedName) => { userNameChangedCounter++; }); store.AddStateChangeListener(s => s.user?.contacts?.FirstOrDefault()?.contactData.name, (_) => { contact1NameChangedCounter++; }); store.AddStateChangeListener(s => s.user?.contacts?.Skip(1).FirstOrDefault()?.contactData.name, (_) => { contact2NameChangedCounter++; }); var contact1 = new MyUser1() { name = "Tom" }; { // Add a first contact to the user: Assert.Null(user.contacts); store.Dispatch(new ActionAddContact() { targetUserId = user.id, newContact = new MyContact1() { contactData = contact1 } }); Assert.Same(contact1, user.contacts.First().contactData); Assert.True(user.WasModifiedInLastDispatch()); // Now that there is a contact 1 the listener was triggered: Assert.Equal(1, userChangedCounter); Assert.Equal(0, userNameChangedCounter); Assert.Equal(1, contact1NameChangedCounter); Assert.Equal(0, contact2NameChangedCounter); } var contact2 = new MyUser1() { name = "Bill" }; { // Add a second contact to the user which should not affect contact 1: store.Dispatch(new ActionAddContact() { targetUserId = user.id, newContact = new MyContact1() { contactData = contact2 } }); Assert.Same(contact2, user.contacts.Last().contactData); Assert.True(user.WasModifiedInLastDispatch()); Assert.False(contact1.WasModifiedInLastDispatch()); Assert.Equal(2, userChangedCounter); Assert.Equal(0, userNameChangedCounter); Assert.Equal(1, contact1NameChangedCounter); Assert.Equal(1, contact2NameChangedCounter); } { // Change the name of contact 1 which should not affect contact 2: var newName1 = "Toooom"; store.Dispatch(new ActionChangeUserName() { targetUserId = contact1.id, newName = newName1 }); Assert.True(user.WasModifiedInLastDispatch()); Assert.True(contact1.WasModifiedInLastDispatch()); Assert.False(contact2.WasModifiedInLastDispatch()); Assert.Equal(3, userChangedCounter); Assert.Equal(0, userNameChangedCounter); Assert.Equal(2, contact1NameChangedCounter); Assert.Equal(1, contact2NameChangedCounter); } { // Change the name of the user which should not affect the 2 contacts: var newName = "Caaaarl"; Assert.NotEqual(newName, user.name); Assert.Equal(user.name, store.GetState().user.name); var tBeforeDispatch = user.LastMutation; store.Dispatch(new ActionChangeUserName() { targetUserId = user.id, newName = newName }); Assert.Equal(newName, store.GetState().user.name); Assert.Equal(newName, user.name); Assert.Same(model, store.GetState()); Assert.NotEqual(tBeforeDispatch, user.LastMutation); Assert.True(user.WasModifiedInLastDispatch()); Assert.False(contact1.WasModifiedInLastDispatch()); Assert.False(contact2.WasModifiedInLastDispatch()); Assert.Equal(4, userChangedCounter); Assert.Equal(1, userNameChangedCounter); Assert.Equal(2, contact1NameChangedCounter); Assert.Equal(1, contact2NameChangedCounter); } { // Marking an object mutated while not dispatching will throw an exception: Assert.Throws <InvalidOperationException>(() => { user.name = "Cooorl"; user.MarkMutated(); }); Assert.Equal(4, userChangedCounter); // Count should not have changed Assert.Equal(1, userNameChangedCounter); } }
public async Task ExampleUsage1() { var t = Log.MethodEntered("DataStoreExample3.ExampleUsage1"); // Add a thunk middleware to allow dispatching async actions: var thunkMiddleware = Middlewares.NewThunkMiddleware <MyAppState1>(); // aDD A logging middleware to log all dispatched actions: var loggingMiddleware = Middlewares.NewLoggingMiddleware <MyAppState1>(); var serverOutboxHandler = new ServerOutboxHandler <MyAppState1>(); // To allow undo redo on the full store wrap the main reducer with the undo reducer: var outboxReducer = serverOutboxHandler.Wrap(MyReducers1.ReduceMyAppState1); var initialState = new MyAppState1(); // the initial immutable state var store = new DataStore <MyAppState1>(outboxReducer, initialState, loggingMiddleware, thunkMiddleware); IoC.inject.SetSingleton(store); store.storeName = "Store 3"; { // Do a login which is an async server action that cant be cached optimistically and wont work offline: Func <Task> asyncLoginTask = async() => { await TaskV2.Delay(100); store.Dispatch(new ActionUserLoggedIn() { newLoggedInUser = new MyUser1("*****@*****.**") }); }; await(store.Dispatch(asyncLoginTask) as Task); } { // Change the email a first time: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }; store.Dispatch(a); Assert.Equal(a, store.GetState().serverOutbox.serverActions.First()); Assert.False(store.GetState().user.emailConfirmed); } { // Change the email a second time: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }; store.Dispatch(a); Assert.Equal(a, store.GetState().serverOutbox.serverActions.Last()); } Assert.Equal(2, store.GetState().serverOutbox.serverActions.Count); await store.SyncWithServer(store.GetState().serverOutbox.serverActions.First()); Assert.Single(store.GetState().serverOutbox.serverActions); await store.SyncWithServer(store.GetState().serverOutbox.serverActions.First()); Assert.Empty(store.GetState().serverOutbox.serverActions); Assert.True(store.GetState().user.emailConfirmed); { // Simulate a server task that has a timeout: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**", simulateOneTimeout = true }; store.Dispatch(a); Assert.Single(store.GetState().serverOutbox.serverActions); Assert.False(store.GetState().user.emailConfirmed); await store.SyncWithServer(a); Assert.Empty(store.GetState().serverOutbox.serverActions); Assert.Equal(2, a.sentToServerCounter); Assert.True(store.GetState().user.emailConfirmed); } { // Simulate the server rejecting an email change: var a = new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**", simulateError = true }; store.Dispatch(a); await store.SyncWithServer(a); Assert.Empty(store.GetState().serverOutbox.serverActions); Assert.Equal("*****@*****.**", store.GetState().user.email); Assert.True(store.GetState().user.emailConfirmed); } { // Test persisting and restoring the full store and continue with the pending server requests: store.Dispatch(new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }); store.Dispatch(new ActionOnUser.ChangeEmail() { targetEmail = "*****@*****.**", newEmail = "*****@*****.**" }); Assert.Equal(2, store.GetState().serverOutbox.serverActions.Count); Assert.False(store.GetState().user.emailConfirmed); Assert.Equal("*****@*****.**", store.GetState().user.email); // Simulate persisiting the store to disk and back into memory: string persistedStateJson = TypedJsonHelper.NewTypedJsonWriter().Write(store.GetState()); store.Destroy(); // Destroy the old store before loading the state again into an new store var data2 = TypedJsonHelper.NewTypedJsonReader().Read <MyAppState1>(persistedStateJson); var store2 = new DataStore <MyAppState1>(outboxReducer, data2, loggingMiddleware, thunkMiddleware); IoC.inject.SetSingleton(store2, overrideExisting: true); store2.storeName = "Store 3 (2)"; Assert.Equal(2, store2.GetState().serverOutbox.serverActions.Count); Assert.False(store2.GetState().user.emailConfirmed); Assert.Equal("*****@*****.**", store2.GetState().user.email); // Sync the pending server tasks one after another: foreach (var serverAction in store2.GetState().serverOutbox.serverActions) { await store2.SyncWithServer(serverAction); } Assert.True(store2.GetState().user.emailConfirmed); Assert.NotNull(store2.GetState().serverOutbox); Assert.Empty(store2.GetState().serverOutbox.serverActions); } }
public async Task RunTestTask() { MyUser1 initialState = null; // Initially no user is logged in var store = new DataStore <MyUser1>(MyReducers1.ReduceUser, initialState, Middlewares.NewLoggingMiddleware <MyUser1>()); var links = gameObject.GetLinkMap(); var loginButton = links.Get <Button>("LoginBtn"); var loginButtonWasClicked = loginButton.SetOnClickAction(async delegate { loginButton.gameObject.Destroy(); await TaskV2.Run(async() => { await TaskV2.Delay(1000); store.Dispatch(new ActionLoginUser() { newLoggedInUser = new MyUser1("Karl") }); }).LogOnError(); Assert.IsNotNull(store.GetState()); }); // Register a listener that is attached to the UI button to demonstrate that its no longer triggered once the button is destroyed: var userNameChangedCounter = 0; var subStateListener = store.NewSubStateListenerForUnity(loginButton, user => user); subStateListener.AddStateChangeListener(x => x?.name, newName => { userNameChangedCounter++; Toast.Show("User name changed to " + newName); Assert.IsFalse(loginButton.IsDestroyed()); }, triggerInstantToInit: false); var userInfoText1 = links.Get <InputField>("UserNameInput1"); ConnectInputFieldUiToModel(userInfoText1, store); var userInfoText2 = links.Get <InputField>("UserNameInput2"); ConnectInputFieldUiToModel(userInfoText2, store); var oldCounterValue = userNameChangedCounter; SimulateButtonClickOn("LoginBtn"); await loginButtonWasClicked; Assert.IsTrue(loginButton.IsDestroyed()); // Since the button was destroyed, the counter should not change anymore: Assert.AreEqual(oldCounterValue, userNameChangedCounter); Toast.Show("Changing user name from background thread..."); await Task.Delay(2000); // When NewSubStateListener instead of NewSubStateListenerForUnity is used, the // event will arrive on the thread where it was dispatched: var wasCalledOnMainThread = true; store.NewSubStateListener(user => user).AddStateChangeListener(x => x.name, newName => { wasCalledOnMainThread = MainThread.isMainThread; }, triggerInstantToInit: false); await TaskV2.Run(async() => { store.Dispatch(new ChangeName() { newName = "Caaarl" }); }); Assert.AreEqual("Caaarl", store.GetState().name); Assert.IsFalse(wasCalledOnMainThread); }
public static void TestDataStoreTransitiveChanges() { CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); var store = new DataStore <CellsModel>(CellsReducers.MainReducer, model, Middlewares.NewLoggingMiddleware <CellsModel>()); store.Dispatch(new MyActions.SetCell("A", 1, "1 + 1")); var cells = store.SelectElement(s => s.cells); Cell a1 = cells()[new CellPos("A", 1)]; Assert.Equal(2, a1.value); // B1 will depend on A1: store.Dispatch(new MyActions.SetCell("B", 1, "3 * A1")); Cell b1 = cells()[new CellPos("B", 1)]; Assert.Equal(6, b1.value); // C1 will depend on A1 and B1: store.Dispatch(new MyActions.SetCell("C", 1, "B1 + 3 - A1")); Cell c1 = cells()[new CellPos("C", 1)]; Assert.Equal(7, c1.value); // D1 will depend on C1 (so transitivly also on A1 and B1): store.Dispatch(new MyActions.SetCell("D", 1, "2 * C1")); Cell d1 = cells()[new CellPos("D", 1)]; Assert.Equal(14, d1.value); // Now changing A1 must have affects to B1, C1, D1 as well: store.Dispatch(new MyActions.SetCell("A", 1, "4 + 1")); c1 = cells()[new CellPos("C", 1)]; Assert.Equal(13, c1.value); d1 = cells()[new CellPos("D", 1)]; Assert.Equal(26, d1.value); // Select cell C1: store.Dispatch(new MyActions.SelectCell(c1.pos)); Assert.True(cells()[new CellPos("C", 1)].isSelected); // Select cell D1: store.Dispatch(new MyActions.SelectCell(d1.pos)); Assert.True(cells()[new CellPos("D", 1)].isSelected); Assert.False(cells()[new CellPos("C", 1)].isSelected); store.Dispatch(new MyActions.SetCell("A", 3, "1")); store.Dispatch(new MyActions.SetCell("A", 2, "A3 + 1")); Assert.Throws <MyActions.SetCell.SelfRefException>(() => { store.Dispatch(new MyActions.SetCell("A", 3, "A2 + 1")); }); }
public static async Task TestLoggingOverhead() { StopwatchV2 t1, t2; { t1 = Log.MethodEntered("SimulateManyChangesInModel without logging"); CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); var store = new DataStore <CellsModel>(CellsReducers.MainReducer, model); await SimulateManyChangesInModel(store); Log.MethodDone(t1); } { t2 = Log.MethodEntered("SimulateManyChangesInModel without logging"); CellsModel model = new CellsModel(ImmutableDictionary <CellPos, Cell> .Empty); var store = new DataStore <CellsModel>(CellsReducers.MainReducer, model, Middlewares.NewLoggingMiddleware <CellsModel>()); await SimulateManyChangesInModel(store); Log.MethodDone(t2); } // Logging makes mutating the model at least double as slow: Assert.True(t1.ElapsedMilliseconds * 2 < t2.ElapsedMilliseconds, $"t1={t1}, t2={t2}"); }