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 class MyReducers1 { // The reducers to modify the immutable datamodel: // The most outer reducer is public to be passed into the store: public static MyAppState1 ReduceMyAppState1(MyAppState1 model, object action) { bool changed = false; model.MutateField(model.user, action, (_, user, a) => ReduceUser(user, a), ref changed); if (changed) { model.MarkMutated(); } return(model); }
private static class MyReducers1 { // The reducers to modify the immutable datamodel: // The most outer reducer is public to be passed into the store: public static MyAppState1 ReduceMyAppState1(MyAppState1 previousState, object action) { bool changed = false; var newUser = previousState.user.Mutate(action, ReduceUser, ref changed); if (changed) { return(new MyAppState1(newUser)); } return(previousState); }
private static class MyReducers1 { // The reducers to modify the immutable datamodel: // The most outer reducer is public to be passed into the store: public static MyAppState1 ReduceMyAppState1(MyAppState1 myState, object action) { bool changed = false; var users = myState.users.Mutate(action, ReduceUsers, ref changed); var someIds = myState.someUuids.Mutate(action, ReduceSomeIds, ref changed); if (changed) { return(new MyAppState1(users, someIds)); } return(myState); }
private MyAppState1 ReduceMyAppState1(MyAppState1 previousState, object action) { bool changed = false; var newA = previousState.substateA.Mutate(action, SubStateAReducer, ref changed); var newB = previousState.substateB.Mutate(action, SubStateBReducer, ref changed); if (changed) { return(new MyAppState1(newA, newB)); } return(previousState); }
public void ExampleUsage1() { var t = Log.MethodEntered("DataStoreExample1.ExampleUsage1"); var data = new MyAppState1(); // the initial immutable state var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, data); // Register a listener that listens to a subtree of the complete state tree: var firstContactWasModifiedCounter = 0; // Listen to state changes of the first contact of the main user: store.AddStateChangeListener(state => state.user?.contacts?.FirstOrDefault(), (firstContact) => { firstContactWasModifiedCounter++; }); Assert.Equal(1, firstContactWasModifiedCounter); store.Dispatch(new ActionLoginUser() { newLoggedInUser = new MyUser1("Karl") }); Assert.NotNull(store.GetState().user); store.Dispatch(new ActionOnUser.ChangeAge() { targetUser = "******", newAge = 99 }); Assert.Equal(99, store.GetState().user.age); store.Dispatch(new ActionOnUser.AddContact() { targetUser = "******", newContact = new MyUser1(name: "Tim") }); Assert.Equal("Tim", store.GetState().user.contacts.First().name); Assert.Equal(2, firstContactWasModifiedCounter); // Change name of Tim to Peter: store.Dispatch(new ActionOnUser.ChangeName() { targetUser = "******", newName = "Peter" }); Assert.Equal("Peter", store.GetState().user.contacts.First().name); Assert.Equal(3, firstContactWasModifiedCounter); store.Dispatch(new ActionLogoutUser()); Assert.Null(store.GetState().user); Log.MethodDone(t); }
private static class MyReducers1 { // The reducers to modify the immutable datamodel: // The most outer reducer is public to be passed into the store: public static MyAppState1 ReduceMyAppState1(MyAppState1 previousState, object action) { bool changed = false; if (action is ResetStoreAction) { return(new MyAppState1()); } var newWeather = previousState.currentWeather.Mutate(action, ReduceWeather, ref changed); var newUser = previousState.user.Mutate(action, ReduceUser, ref changed); if (changed) { return(new MyAppState1(newUser, newWeather)); } return(previousState); }
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 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 void StressTest1() { var initialState = new MyAppState1(new SubStateA("a1"), new SubStateB("b1"), NewVeryLargeList()); var store = new DataStore <MyAppState1>(ReduceMyAppState1, initialState); // In total this test will create 4 million state change listeners: int listenerCount = 100000; StopwatchV2 t1, t2, t3, t4; // The 4 measured timings of the Dispatches { // Add subListeners that are only informed by the one listener attached directly to the store: var counterA1 = 0; var subListenersA = store.NewSubStateListener(s => s.substateA); for (int i = 0; i < listenerCount; i++) { subListenersA.AddStateChangeListener(substateA => substateA.valA, newValA => counterA1++); } t1 = Log.MethodEntered("ActionChangeSubstateA"); store.Dispatch(new ActionChangeSubstateA() { newVal = "a2" }); Log.MethodDone(t1); Assert.Equal("a2", store.GetState().substateA.valA); Assert.Equal(listenerCount, counterA1); } { // Now add additional listeners to check if it makes Dispatching slower: var counterB1 = 0; var subListenersB = store.NewSubStateListener(s => s.substateB); for (int i = 0; i < listenerCount; i++) { subListenersB.AddStateChangeListener(substateB => substateB.valB, newValB => counterB1++); } t2 = Log.MethodEntered("ActionChangeSubstateB"); store.Dispatch(new ActionChangeSubstateB() { newVal = "b2" }); Log.MethodDone(t2); Assert.Equal("b2", store.GetState().substateB.valB); Assert.Equal(listenerCount, counterB1); // Make sure the additional listeners did not make the Dispatch slower: float t1t2Ratio = (float)t1.ElapsedMilliseconds / (float)t2.ElapsedMilliseconds; Log.d("t1t2Ratio=" + t1t2Ratio); Assert.True(0.3f < t1t2Ratio && t1t2Ratio < 3f, "t1t2Ratio=" + t1t2Ratio); } { // Now add the listeners directly to the store which slows down the Dispatches: var counterA2 = 0; var counterB2 = 0; for (int i = 0; i < listenerCount; i++) { store.AddStateChangeListener(s => s.substateA, newSubA => { counterA2++; }); store.AddStateChangeListener(s => s.substateB, newSubB => { counterB2++; }); } t3 = Log.MethodEntered("ActionChangeSubstateA2"); store.Dispatch(new ActionChangeSubstateA() { newVal = "a3" }); Log.MethodDone(t3); Assert.Equal("a3", store.GetState().substateA.valA); Assert.Equal(listenerCount, counterA2); t4 = Log.MethodEntered("ActionChangeSubstateB2"); store.Dispatch(new ActionChangeSubstateB() { newVal = "b3" }); Log.MethodDone(t4); Assert.Equal("b3", store.GetState().substateB.valB); Assert.Equal(listenerCount, counterB2); // Make sure the additional listeners make Dispatching much slower: float t1t3Ratio = (float)t3.ElapsedMilliseconds / (float)t1.ElapsedMilliseconds; Log.d("t1t3Ratio=" + t1t3Ratio); Assert.True(1.5f < t1t3Ratio, "t1t3Ratio=" + t1t3Ratio); float t1t4Ratio = (float)t4.ElapsedMilliseconds / (float)t1.ElapsedMilliseconds; Log.d("t1t4Ratio=" + t1t4Ratio); Assert.True(2f < t1t4Ratio, "t1t4Ratio=" + t1t4Ratio); float t3t4Ratio = (float)t3.ElapsedMilliseconds / (float)t4.ElapsedMilliseconds; Log.d("t3t4Ratio=" + t3t4Ratio); Assert.True(0.5f < t3t4Ratio && t3t4Ratio < 2f, "t3t4Ratio=" + t3t4Ratio); } TestListEntrySelector(store); }
public void ExampleUsage1() { var t = Log.MethodEntered("DataStoreExample1.ExampleUsage1"); // Some initial state of the model (eg loaded from file when the app is started) is restored and put into the store: MyUser1 carl = new MyUser1(GuidV2.NewGuid(), "Carl", 99, null, MyUser1.MyEnum.State1); var data = new MyAppState1(ImmutableDictionary <Guid, MyUser1> .Empty.Add(carl.id, carl), null); var store = new DataStore <MyAppState1>(MyReducers1.ReduceMyAppState1, data); var keepListenerAlive = true; var usersChangedCounter = 0; store.AddStateChangeListener(state => state.users, (changedUsers) => { usersChangedCounter++; return(keepListenerAlive); }, triggerInstantToInit: false); ActionAddSomeId a1 = new ActionAddSomeId() { someId = GuidV2.NewGuid() }; store.Dispatch(a1); Assert.Equal(0, usersChangedCounter); // no change happened in the users Assert.Equal(a1.someId, store.GetState().someUuids.Value.Single()); MyUser1 carlsFriend = new MyUser1(GuidV2.NewGuid(), "Carls Friend", 50, null, MyUser1.MyEnum.State1); store.Dispatch(new ActionOnUser.AddContact() { targetUser = carl.id, newContact = carlsFriend }); Assert.Equal(1, usersChangedCounter); Assert.Equal(carlsFriend, store.GetState().users[carlsFriend.id]); Assert.Contains(carlsFriend.id, store.GetState().users[carl.id].contacts); store.Dispatch(new ActionOnUser.ChangeName() { targetUser = carl.id, newName = "Karl" }); Assert.Equal(2, usersChangedCounter); Assert.Equal("Karl", store.GetState().users[carl.id].name); store.Dispatch(new ActionOnUser.ChangeAge() { targetUser = carlsFriend.id, newAge = null }); Assert.Equal(3, usersChangedCounter); Assert.Null(store.GetState().users[carlsFriend.id].age); Assert.NotEqual(MyUser1.MyEnum.State2, store.GetState().users[carl.id].myEnum); store.Dispatch(new ActionOnUser.ChangeEnumState() { targetUser = carl.id, newEnumValue = MyUser1.MyEnum.State2 }); Assert.Equal(MyUser1.MyEnum.State2, store.GetState().users[carl.id].myEnum); // Remove the listener from the store the next time an action is dispatched: keepListenerAlive = false; Assert.Equal(4, usersChangedCounter); store.Dispatch(new ActionOnUser.ChangeAge() { targetUser = carlsFriend.id, newAge = 22 }); Assert.Equal(5, usersChangedCounter); // Test that now the listener is removed and will not receive any updates anymore: store.Dispatch(new ActionOnUser.ChangeAge() { targetUser = carlsFriend.id, newAge = 33 }); Assert.Equal(5, usersChangedCounter); Log.MethodDone(t); }