@kauko Time travel in Om is made possible by the fact that you’re using immutable state and structural sharing (see https://en.wikipedia.org/wiki/Persistent_data_structure). There is simply a vector of 100 (by default) past snapshots of app state. 99% of that state is structurally shared, so it doesn’t really consume much extra space.
state -> state2 -> state3 -> …
Ideally, rendering is a pure function of state. Therefore (render state2)
simply updates the DOM to look like it did at that state
time travel in this model is Trivial with a capital T
The “diff” that @adambros spoke of is generated by comparing two points in this sequence of snapshots. You can generate a delta between any two steps with, e.g. https://clojuredocs.org/clojure.data/diff
as for your more specific question, @kaoko, "how do I tell Om Next not to store (and replay) async things, and only store the things that actually mutate my state?"
The reason you probably didn’t get much response is this doesn’t make a lot of sense in the Om Next model. In Om next, async is meant to be hidden behind the scenes. Only the networking code need ever directly deal with it, and that is hidden behind an abstraction (transactions). There is no way to “store” an async thing…async has to do with how things are done, not what is stored.
And since you never put code in your app state, it makes little sense to talk about “replaying async things"
the state history is simply that: a history of snapshots of the application (data) state. If you wanted to store some record of “what I just did to get from state1 to state2” then you’d have to explicitly encode that (say, as data under a :last-mutation
key or something of your invention). In other words, every one of your mutations would need to be saving “I ran” in app state somewhere. Om Next does not do that.
In stock Om Next you could add this encoding into a wrapper around all mutations (e.g. a function that composes the result of a multimethod with your recording). In Untangled the easiest spot at the moment would be via the new :mutation-merge
hook when creating the client (though all you see there is the mutation name and return value from the server).
I’ve been planning to do some kind of history viewer transition recording of this sort as part of Untangled, but have not gotten to it yet. I’d like to have some top-level Untangled keys that represent the “thing that happened to go from state1 to state2” for debugging purposes, and would include the status code from the server along with a server-based timestamp if possible.
Then when you’re using the support viewer you'd have an easy time correlating production logs with user-reported errors
Thanks for the explanation @tony.kay !
I'm used to thinking about time travel as an ordered log of events, which is a different (and seems to me, a better) way of achieving time travel
Storing the states, or just how they changed, certainly works, but it doesn't show me what caused that change, right?
What I'm talking about is basically CQRS and Event Sourcing, you just need those "async commands" and "sync commands" when you're in the client. Pretty much what Elm does with Effects and Actions.
the transaction is the event, and you can save it.
CQRS is trivial and quite compatible with the entire model. In fact a talk was given last year…let me see...
Yeah, I know it's trivial 🙂 That's partly why I was surprised to find no mention of anything related to it
This is the talk that really made me understand Event Sourcing better http://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/
Yeah, the community is quite aware of how good a fit it is, but Om Next isn’t specifically targeted at CQRS. It just happens to fit nicely.
we’re aware of it as well. It adds a bit of complexity to an average or beginning app that probably isn’t worth it, so talking it up as the thing to do isn’t of much interest.
Setting up your event recording, your processing pipeline, etc etc etc is a lot of work, admin overhead, and added complexity for new devs. You have to balance that with the overall benefits I think.
But yes, Om Next is a superb fit if you want to use that model for your stack
That's good to hear 🙂 So Om Next has its own model, but if I want to have Event Sourcing, I can implement it myself. I guess since Om Next's time travel doesn't depend on the log of events, you don't really have to even care about the whole Effect/Action thing either.
I honestly just feel like you can gain so much value from Event Sourcing. Not only can the debugger be helpful during development, but getting a full history from you client in a bug report is awesome... though this you can get just by replaying the states too.
Where the log based approach wins out, is in writing tests
Maybe you can write unit tests that say "After these effects, the state must look like this".
@kauko our testing support already has direct support for that (see protocol testing)
that’s really all an Om Next mutation is, after all: an event that causes state -> state’
no need for anything to be log-based. It’s the “event” (in Om Next terminology: transaction) that matters
Hmm, sure! The log of events and a log of state transitions both represent exactly the same thing, but are not events easier for humans to read? Like you said, it is possible to store the transactions somewhere (I assume you'd give them nice names), but then you are doing Event Sourcing
Not sure why you’re differentiating. In Om Next the transactions are the abstraction that you’d refer to as an event. They are the data representation of what to do. How they are implemented is hidden. Thus, they are isomorphic to the events of event sourcing.
maybe I'm misunderstanding something then 😄 text is not the easiest medium
in Om Next, you say something like (om/transact! this ‘[(app/do-something)])
. That transaction is represented as EDN. Pure data. Thus the quoting. You’re not calling a function there (do-something)…you’re sending it off to the ether to be interpreted by the underpinnings. That bit of data is also what goes over the wire and could be stored (if you wanted) as the thing that causes the state transition in the new state itself.
so your new app state could be made to include it…it’s just a list with a symbol in it
{ :transition-reason ‘[(app/do-something)] …rest of app state...}
and since it is what goes over the wire, it is what you’d put in your CQRS log before executing it on the server
etc
Hmm, I wonder if what threw me off was when you said "no need for anything to be log-based". Do we understand the word "log" differently? What I meant by log, is just some place where transactions are stored in a time-ordered manner. New transactions go on top of the log.
I'm sorry if I'm being dumb. 🙂 Really appreciate your help!
that is exactly what I mean in the CQRS context
there is no need for log-based representation to get the benefit from testing is what I meant
of course you use a log IF you’re doing CQRS
I was just saying the two are not related in Om Next, because the transition (event/transaction as data) is a first-class citizen of the design. *If* you choose to log it and do all of the rest of the infrastructure around that, then you get CQRS as well
All of Om Next is: state ——transaction as data—> state'
thus, the unit-testability of state transitions (all operations on the UI or server) is a given…well, on the server those are usually integration tests since they side-effect
and the transaction as data does map to some specific code unit
Yeah, I think I understand what you're saying now. Kind of feel like we've been talking about the same thing this whole time. 😄
What I meant by the logs + unit tests was mostly how cool it would be to get an error report from a client (that you can replay) - fix the problem - and finally use that log of transactions in a test.
BUT let's not get stuck with this. I've another question, if you don't mind answering. If I understood you correctly, you said that Om Next does not store the transactions themselves, but it stores the state transitions. Where is that data stored? Can I access it myself somehow?
it stores the prior states, yes
see from-history
in the om next source code
next.cljc
near line 2820
it basically keeps an array of states and an index by UUID (the UUIDs are logged for debugging purposes)
Cool, thanks!
welcome
I feel like there's so much to Om Next that the docs in github don't mention at all 😕
you found docs?? 😜
which is a shame, because if non-clojurians knew what kind of ideas Om has come up with, even more people would come to Clojure!
lol yeah, the docs which STILL say they shouldn't be edited because they're WIP
well, that’s part of the objective of Untangled: Om Next leaves a lot to be defined. Make it more approachable. That said, we’re not pushing the fringe ideas very much. The core ideas are so cool that we feel like letting ppl get used to those first.
Graph database, co-located queries, transactions as data, no async code needed in the application-layer, rendering as a pure function, no in-place mutation….it’s a lot of change
Spec has a lot of potential to shake things up even more. David already mentioned he has been thinking about if queries could be represented just as specs. I don't really understand what the effect of that would be, but during the weekend I got stuck with a similar idea.. wouldn't it be cool if we could somehow define specs for when a transaction is legal? Wouldn't that mean we could use generative testing to "use" our application in crazy ways, to see if it ends up in an illegal state? 😄
Not sure how it would look like, but that would be amazing! 😄
I just fixed up the lein i18n plugin and released docs on the devguide. It covers modules to some extent as well (e.g. dynamically loading translations). Not all tested, but should be close.
http://untangled-web.github.io/untangled/guide.html#!/untangled_devguide.L_Internationalization
I left a ton of escaped quotes in the README…but you get the idea 🙂
Hello Everyone!
I’m having a hard time understanding how to implement the static initial-state method. My child components run fine and their dom elements render, but their initial state does not show up in the app state
My code is here https://github.com/BerkeleyTrue/berkeleys-first-untangled-todomvc/blob/master/src/client/todomvc/ui/root.cljs
But the main bit is
(defui ^:once Header
static uc/InitialAppState
(initial-state [this params] {:title "New Todo" :value "foo"})
static om/IQuery
(query [this] [:ui/value :title])
static om/Ident
(ident [this {:keys [title]}] [:header/by-title title])
Object
(render [this]
(let [{:keys [value] :or {value ""}} (om/props this)]
(dom/header
#js {:className "header"}
(dom/h1
#js {}
"Todo")
(dom/input
#js {
:className "new-todo"
:placeholder "What needs to be done?"
:value value
:onChange (fn [e] (m/set-string! this :value :event e))
})))))
(def ui-header (om/factory Header))
This seems to be how most tutorials online define intial state, but :title and :ui/value are both undefined with ident/render
are you composing initial-state from the root of your ui?
I believe so
ah no you are not, line 37
so initial-state needs to be composed like queries
Hmm, I’m not sure what that means.
so you need to call Header
‘s initial-state method in your Root initial-state
I see
yeah, otherwise untangled wont ever use your Header
‘s initial-state
all you’ve done is defined it
Ok sweet. I think I understand now.
the only “special” initial-state is root, and thats because you define it as your root in new-untangled-client
so untangled knows about that one
err not new-untangled-client, rather mount
Sweet! Thank worked. Thanks so much @adambros ✨
@adambros Is the same true for queries? Should they be composed together?
Yes, just found it in the untangled guide. 🙂