-
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
RangeError: Maximum call stack size exceeded #521
Comments
True that. Infinitely recursive flatMap doesn't work. There's no easy "fix" for this. Pull Requests welcome! |
I ended up working around it in my code like this: var Bacon = require('baconjs');
function looper(fn, delay) {
return Bacon.fromBinder(function(sink) {
var unsub, timer;
function sub() {
unsub = fn().subscribe(function(event) {
if (!event.isEnd()) {
sink(event);
} else {
timer = setTimeout(sub, delay);
}
});
}
sub();
return function() {
unsub();
clearTimeout(timer);
};
});
}
function step() {
return Bacon.later(1, 'beep');
}
looper(step, 1000).log(); Changing the 1000 down to 1 or 0 quickly shows that it doesn't blow up. EDIT: This function has flaws with being unsubscribed from. See my later version of it for fixes. |
For now this works: var Bacon = require('bacon.js');
function step() {
return Bacon.later(1, 'beep');
}
function fix(f) {
var bus = new Bacon.Bus();
var result = f(bus);
bus.plug(result);
return result;
}
var loop = fix(function (r) {
return step().merge(rec.delay(1));
});
loop.log(); Related to #507. Recursive |
+1 for including var animationFrames = looper(function() {
return Bacon.fromCallback(requestAnimationFrame);
}); Not sure about the |
Here how it might look without timeout: function looper(fn) {
return Bacon.fromBinder(function(sink) {
var unsub;
function sub() {
unsub = fn().subscribe(function(event) {
if (!event.isEnd()) {
sink(event);
} else {
sub();
}
});
}
sub();
return function() {
unsub();
};
});
} Edited: removed not needed manual |
The The var Bacon = require('baconjs');
var looper = require('./looper-no-timeout');
function step() {
return Bacon.later(1, 'beep');
}
looper(function() {
var s = step();
return s.merge(s.mapEnd(null).delay(1000).filter(false));
}).log(); |
This version of your last example should work fine, but it still a bit awkward I agree. function step() {
return Bacon.later(1, 'beep');
}
looper(function() {
return step().concat(Bacon.later(1000).filter(false));
}).log(); |
In my project, |
Oh, I had missed your example about using It sounds like from other bugs that synchronously-emitting streams have problems of their own (and are hopefully on the chopping block?), so worrying about them shouldn't block a timeout-less Though I just realized it's pretty trivial to check for synchronously-emitting streams (by checking if var Bacon = require('baconjs');
function looper(fn) {
return Bacon.fromBinder(function(sink) {
var stopper = new Bacon.Bus();
var stopperProp = stopper.toProperty();
// force evaluation of stopperProp so it remembers its value
stopperProp.onValue(function() {});
function sub() {
var subscribeHasReturned = false;
fn().takeUntil(stopperProp).subscribe(function(event) {
if (!event.isEnd()) {
sink(event);
} else {
if (subscribeHasReturned) {
sub();
} else {
// If the fn() stream emitted synchronously, we should loop
// asynchronously to avoid blowing up the stack.
Bacon.later(0).takeUntil(stopperProp).onValue(sub);
}
}
});
subscribeHasReturned = true;
}
sub();
return function() {
stopper.push(null);
};
});
}
// Pathological step function that needs this more defensive unsubscription logic
// used in looper in order to be unsubscribed from correctly.
var i = 0;
function step() {
if (++i == 1) {
return Bacon.later(1, 'later');
} else {
return Bacon.once(5).merge(Bacon.repeatedly(200, [6,7,8]));
}
}
looper(step).take(2).log(1); It took me quite a few edits to get the unsubscription logic fully down. (My original version had the issues too.) I tried keeping the return value of |
Yeah, in the But there is obviously some implementation issues with synchronous observables. Not sure what is desirable behavior here should be, one of the options btw is to just throw an exception saying "you can't use looper with synchronous observables". |
Throwing an error could be undesirable since the given step function might only emit synchronously occasionally at run-time depending on the application. (Might be a little odd of a case, but think of properties which may or may not have a current value.) I think the gracefully degrading behavior as implemented above where it falls back to timeouts for synchronously-ending observables is better and can be easily documented. (If you phrase it as that multiple step calls are never made in the same event loop turn, then it sounds pretty natural.) |
Here is another shot on looper function. The recursion replaced with iteration, and unsubscription done via The only problem with this implementation is: if user will do something like function looper(step) {
return Bacon.fromBinder(function(sink) {
var hasSource, inLoop, noMore = false;
function handleEvent(event) {
if (!event.isEnd()) {
if (Bacon.noMore === sink(event)) {
noMore = true;
return Bacon.noMore;
}
} else {
if (inLoop) {
hasSource = false;
} else {
iterateSubscribe();
}
}
}
function subscribe() {
hasSource = true;
step().subscribe(handleEvent);
}
function iterateSubscribe() {
hasSource = false;
inLoop = true;
while (!hasSource && !noMore) {
subscribe();
}
inLoop = false;
}
iterateSubscribe();
return function() {};
});
} |
That looper function doesn't handle unsubscription here: var i = 0;
function step() {
if (++i == 1) {
return Bacon.later(1, 'later');
} else {
return Bacon.once(5).merge(Bacon.repeatedly(200, [6,7,8]));
}
}
looper(step).take(2).log(1); The node process doesn't exit because the step stream isn't unsubscribed from. |
It does after #523 fixed in v0.7.42 ;) |
Oh good, I think I tried something like that before but I gave up trying to figure out if there was a bug there or not. |
Ok guys, there's a new implementation of Maybe we should replace |
So the new method And the tests hopefully demonstrate how it's used: https://github.com/baconjs/bacon.js/blob/master/spec/specs/fromstreamgenerator.coffee |
I was thinking about this lately too, and it's funny that I came up with same ideas, both that Also maybe it would be useful to pass iteration number to the generator — would be easier to implement I didn't understand completely how |
@pozadi you're right that it'll go recursive with syncronous streams returned from the stepper, and can thus lead to stack overflow. Your implemenation above seems to cleverly solve this issue with the flag variables. It would certainly make sense to adopt that technique :) Passing iteration number might be a good idea and simplify the implementation of some stepper functions. Also, I wish there was a way to get rid of the variables "error" and "finished" in the retry implementation. Iteration number is not quite enough for that. And then there's the issue of naming this thing: |
Oh btw it seems that Bacon=require("../dist/Bacon")
var i = 0;
function step() {
if (++i == 1) {
return Bacon.later(1, 'later');
} else {
return Bacon.once(5).merge(Bacon.repeatedly(200, [6,7,8]));
}
}
Bacon.fromStreamGenerator(step).take(2).log(); Outputs
|
How about BTW, Bacon.repeat(function() {
return Bacon.sequentially(1000, [1, 2, 3]);
}); |
Power of CoffeeScript, I guess ;) |
Noticed another problem with all implementations here. Suppose we created a steam using looper, then we subscribed to it, got some events, unsubscribed, and later subscribed again. On second subscription the generator function will be called again although the stream it returned first time maybe still alive. I think the correct behavior is that we should subscribe again to previous generated observable, if it didn't end yet instead of generating new one. |
Shouldn't unsubscribing from the looper cause the stream it returned to be
|
Yep, the problem is, we won't subscribe to it back, when the looper get a subscriber again. Instead we'll generate new one via generator function. |
Oh, I never imagined it working that way. It doesn't impact my use case at
|
End result: |
This code fails with "RangeError: Maximum call stack size exceeded" after a short time.
Seems like I independently ran into this issue, but I don't think an issue was opened here for it.
The text was updated successfully, but these errors were encountered: