-
Notifications
You must be signed in to change notification settings - Fork 331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Atomic updates to Properties #85
Comments
I got this idea yesterday. There might, after all, be a quite simple way to achieve atomic updates. The main idea would be to hold all Property updates to external observers until the orinal event has been fully dispatched. All internal updates would work as before, but only the very last update originating from the same input event would be sent to any external observer. That's it! Now we just need to identify external and internal observers and the entry point of the original event so that we can do this. The external and internal observers can be distinguished using a The entrance point for events can also be determined quite easily: in a single-threaded environment we can simply use global state to keep track of whether we are already "inside the system". In The idea can be taken a step further too: We can easily provide Please (in)validate this idea! |
I don't see any holes in the logic. Nice work! I don't know much about browser event architecture, but: For example, I have a 'tick' stream that fires on each requestAnimationFrame: tick = new Bacon.EventStream (sink) ->
cb = (time) ->
sink(new Bacon.Next time)
window.requestAnimationFrame cb
window.requestAnimationFrame cb
_.noop These should probably count as new entrance points. If there's no way to auto-detect this situation, we might need to add a way to manually tell Bacon that something is an entrance point. Or, perhaps we could only support atomic property updates when using Bacon.later, Bacon.interval, and perhaps a built-in Bacon.frame observable I don't understand browser or bacon internals enough to be confident at all here-- I might just be making up imaginary problems :) |
Ted, Thanks for helping me validating this idea! It happens to be so, that all EventStreams route their events through a Dispatcher, which means that we can in fact capture all entrance points in Dispatcher.push. Something like iAmRoot = false
try
if not Bacon.inside
iAmRoot = true
Bacon.inside = true
doTheUsualDispatchingStuff()
finally
if iAmRoot
sendPendingExternalUpdatesNow()
Bacon.inside = false |
If the update would be async (e.g. for a server fetch), I think there's still a problem. This solution relies on not giving up the cpu. It might allow, however, putting the async stream into some kind of pending state (None?). E.g. suppose b requires a server fetch. When d updates, a1->a2, b1->undefined, and C filters on !undefined. When the fetch completes, b goes undefined->b2, and C gets a new value. I'm not sure I understand how the internal/external thing would work. Suppose C is doing a join on two arrays by passing a function to .combine(). Is that function internal or external? If it's internal, the function will be called with inconsistent data, which might lead to the join failing. |
You're correct in that the proposed solution only guarantees atomicity in case there's a single entry point. In case A,B require an async fetch, you can keep the D -> C update atomic by using flatMap+combine:
Here the |
Atomica update will be a feature of next releases of bacon.js? |
Maybe. I'm still not sure about the implementation details. Also, I've got limited time to spend on this. Also, I don't need this myself right now. Will accept a well-tested-and-documented Pull Request of course :) |
I'm wondering how important this is and to how many people. The problem is that I don't think it will be possible to totally guarantee atomicity using the current (simple) internal design. Bacon.js uses internally the exact same mechanism as it provides for external subscribers, so it does not have, for instance, a dependency graph of the whole event network. This limits what we can do with regard to atomic updates. I'm planning to try something like the proposition above, but that will only guarantee atomicity in a graph comprised of Properties. So it would guarantee atomic updates in cases like the one in the description of this issue, but it wouldn't guarantee atomicity in case there are EventStreams in the graph. So, a half-ass implementation might be doable with moderate effort, but a kick-ass one would require a grand re-implementation. I've got a gut feeling that even the half-ass one would satisfy most needs. What do you think? |
I'm not familiar with the internals, but: Might it be possible to infer dependent EventStreams without drastic changes, using a global context? e.g., onValue: (f, args...) ->
f = makeFunction(f, args)
@subscribe (event) =>
Bacon._parent = this
f event.value() if event.hasValue()
Bacon._parent = null Bacon._addDependency = (child) ->
# we now know that `child` depends on the current value of `Bacon._parent` Contrived setup: bus = new Bacon.Bus()
stream = new Bacon.sequentially(100, [1,2,3,4,5])
stream.onValue (val) -> bus.push(val+1) Somewhere in EventStream or EventStream's dispatcher, we could call |
I think the critical issue here is that FRP implies a consistent view of time. You can get an inconsistent view of time just using callbacks. FRP is different than callbacks in just this way. If bacon isn't going to provide that, it's not FRP, it's something else. Flapjax and rxjs might provide examples. Flapjax uses a counter as timestamp to prevent pushing inconsistent data. |
It might, in fact, make sense to dig into flapjax implementation to see how they've solved this. My impression on RxJs, though, is that it has the most inconsistent view of time of all the FRP libs, as it's based on the Observable abstraction only. Or does it have atomic updates? Should try... |
Whoa! Finally got something done. First, ugly draft exists in the atomic-updates branch. I'd be happy to have more tests for this. |
Anyone interested in Atomic Updates? I'd be glad to get some feedback on the first implementation. It passes all automatic tests in the branch, but does it pass yours? |
I hope to get back to this soon. I'm in server code for a couple more days, at least. |
No problemo. I'd be very glad to get some feedback on this before going to master. And as I said, this is a very rough first draft, and I'll do some cleanup if it seems to work. Have fun with servers! |
I re-implemented sampledBy combinator too to fix #161 in the atomic-updates branch. Would like to go to master with atomic updates. Still would like some external validation on this one. Would you like to try the atomic-updates version? |
I can't comment on the implementation (I haven't had a chance to learn bacon's internals yet), but I just dropped atomic-updates into a decently-sized project that depends heavily on bacon, and nothing broke! 👍 |
Thanks Ted! I really appreciate that. Exactly what was needed. I'll try it on my "work" project too and if that works, i think I'll merge. |
The way Elm does this is:
Elm currently prohibits cycles in the event graph. |
I just tried it against my unit test again, but it's still failing. I won't swear I updated correctly. I did a git pull, checked-out out atomic-updates, and ran "coffee -c src/Bacon.coffee". I'm testing that there are no inconsistent states, where a combined stream has out-of-sync data from its dependencies. Here's the test. The third assert fails because the combined stream gets three updates when only two updates have happened. 'test no glitches': function() {
var a = new Bacon.Bus();
var b = a.map(function (v) { return "b" + v; });
var c = Bacon.combineAsArray(a, b);
var cb = sinon.stub();
c.onValue(cb);
a.push(1);
assertTrue(cb.calledOnce);
assertEquals([[[1, "b1"]]], cb.args);
a.push(2);
assertEquals(2, cb.callCount);
assertEquals([[[1, "b1"]], [[2, "b2"]]], cb.args);
}, |
@acthp the problem in your case is that If you modify it to this, it will work (coffee/jasmine):
|
@des1303 that approach sound good, but not suitable for Bacon.js, where there's no concept of a Node. To apply the Elm approach, a complete internal redesign would be needed. Also, it might prove hard because in Bacon.js you can create new streams "on the fly" instead of defining the statically at start. Isn't this how you set up the "event network" in Elm? |
Atomic Updates, with limitations, are now in |
Assume you have properties A and B and property C = A + B. Assume that both A and B depend on D, so that when D changes, both A and B will change too.
When D changes
d1 -> d2
, the value of Aa1 -> a2
and B changesb1 -> b2
simultaneously, you'd like C to update atomically so that it would go directlya1+b1 -> a2+b2
. Unfortunately, Bacon.js does not recognize simultaneous events/updates and C will have a transitional state likea1+b1 -> a2+b1 -> a2+b2
The text was updated successfully, but these errors were encountered: