GithubHelp home page GithubHelp logo

Destroy listeners about flyd HOT 16 CLOSED

paldepind avatar paldepind commented on July 30, 2024
Destroy listeners

from flyd.

Comments (16)

paldepind avatar paldepind commented on July 30, 2024

The destroy behavior is something that should definitely be changed! The current implementation has very limited usage. I agree with you that something should be done to a streams listeners when a stream is destroyed.

This is my thinking: Flyd streams can be divided into two types

  1. Streams that doesn't make sense without all of their dependencies. For instance streams created by flyd-sampleon and flyd-lift.
  2. Streams that works well just as long as some of their dependencies are active. For instance, streams created by flyd.merge.

This is sparsely documented, but the way you create streams of the different types depends on the third parameter to flyd.stream (it's poorly name doesNotRequireDeps in the README). That's why the implementation of flyd.merge passes true to stream. Because the created stream can work without all of it dependencies.

This is the behavior I think would be optimal: Streams that require all of their dependencies are destroyed as soon as one of their dependencies are destroyed. And, streams that just need some of their dependencies are destroyed only when all of their dependencies have been destroyed.

Does that makes sense to you? What do you think?

from flyd.

paldepind avatar paldepind commented on July 30, 2024

I also think destroy should be renamed to end.

from flyd.

iofjuupasli avatar iofjuupasli commented on July 30, 2024

doesNotRequireDeps = undefined can be implemented with doesNotRequireDeps = true

var deps = [a, b];

// all deps should has value
flyd.stream(deps, function () {
    if (deps.some(function (dep) {
            return dep() === undefined;
        })) {
        return;
    }
    return a() + b();
}, true)

So doesNotRequireDeps = true is more low-level.

I think there can be two methods. And one that implements same behavior as with doesNotRequireDeps = undefined will use more general method.

flyd.stream(fn, deps) should be most low-level method. So it calculates new value every time, even initially without values on dependencies.

flyd.combine(fn, deps) is implemented over stream and uses code like in example above. Its behavior will be same as current flyd.stream(deps, fn)

About destroy:

var a = flyd.stream();
var b = flyd.stream();

var c = flyd.stream([a, b], function () {});

c.onEnd = function (streamEnded) {
    if (a === streamEnded) {
        return;
    }
    c.end();
}

a.end();
b(1); // `c` recalculates
a(1); // throws exception
a(); // returns last value
b.end(); // c ends

For most general low-level method stream it should give user all possibilities to customize ending behavior.

onEnd called when one of dependencies ends.

Streams implemented over flyd.stream can add onEnd method: map should end when its single dependency ends, combine when one of dependency ends, and etc.

By default, I think, flyd.stream should end only when user calls .end(). It's possible for stream to emit values even when there is no one active stream in dependencies. e.g. interval.

from flyd.

paldepind avatar paldepind commented on July 30, 2024

These are interesting ideas! Thanks a lot for sharing them.

Yes, you're right that doesNotRequireDeps = undefined could be considered more low level. The implementation you've posted is pretty close to the one in Flyd.

What do you think the advantage is of having stream and merge versus just having stream with a parameter that switches behaviour? I like the elegance of having a single low level function on top of which everything can be implemented.

Another approach to ending streams: when a stream ends it triggers an update on listening streams if they have doesNotRequireDeps = true but with a flag that indicates that the stream has ended:

var a = flyd.stream();
var b = flyd.stream();

var c = flyd.stream([a, b], function (self, changed) {
  if (changed.ended) {
    // `changed` stream ended
    if (changed === b) flyd.end(self);
  }
}, true);

flyd.end(a)); // c is called with ended `a`
flyd.end(b)); // c is called with ended `b` which ends `c` as well

This design makes an explicit onEnd method unnecessary.

By default, I think, flyd.stream should end only when user calls .end(). It's possible for stream to emit values even when there is no one active stream in dependencies. e.g. interval.

Hm, interval wouldn't have any dependencies as all would it? So there would be no dependencies to end it.

from flyd.

iofjuupasli avatar iofjuupasli commented on July 30, 2024

What do you think the advantage is of having stream and merge versus just having stream with a parameter that switches behaviour?

return flyd.nameIt1([a, b], function () {
    // do something with both a and b
})

return flyd.nameIt2([a, b], function () {
    // do something with a or b (or neither)
}, true)

For first function best name, I think, combine. Same name used in almost all Rx libs. There is a difference in arguments passed to function, but combine is closest name.
Second can be named merge, but it'll be actually wrong. merge should just emit from all dependencies. With nameIt2 it's possible to do practically all known methods on stream.

Why it should be two separate methods? Because it has different intent. And nameIt1 is just shortcut for nameIt2 with checking values at beginning of body.

I think, the most flexible method should be used as basic building block.
It introduce more verbose body functions. But it's ok, I think, because reducing code duplicating is in high-level methods responsibility.

Another approach to ending streams: when a stream ends it triggers an update on listening streams if they have doesNotRequireDeps = true but with a flag that indicates that the stream has ended:

It'll be same as trigger an update with {reduced: true} and check this in 'body'. It requires every body in listeners to check this. So calling .end() will be a pain if checking was omitted in some stream.

It's better to warn user when .onEnd isn't provided, and that it should produce memory leak.

For doesNotRequireDeps = true if it means flyd.combine .onEnd can be provided in core:

function combine(body, deps) {
    var stream = flyd.stream(function () {
        if (deps.some(function (dep) {
                return !dep.hasVal;
            })) {
            return;
        }
        return body.apply(null, arguments);
    }, deps);

    stream.reduce = flyd.stream(function (self, changed) {
        if (!changed) {
            return;
        }
        // any value means reducing
        return true;
    }, deps.map(function (dep) {
        return dep.reduce
    }));
}

Hm, interval wouldn't have any dependencies as all would it? So there would be no dependencies to end it.

My js is probably better than english )

β€œTalk is cheap. Show me the code.” (c) Linus

So here is what I mean. Or why it's hard to use any default function for reduce.

var a = flyd.stream();

var intervalled = flyd.stream([a], function (self) {
    if (!a.hasValue) {
        // a is required for this stream
        return;
    }
    var value = a();
    setInterval(function () {
        self(value);
    }, 1000);
});

var c = flyd.stream([intervalled], function () {
    if (!intervalled.hasValue) {
        return;
    }
    return intervalled() + 1;
})

a(1);
a.reject();

If we reject intervalled because of all its deps rejected, it means that we should reject c-stream too. But intervalled stream looks ok, it has no deps, but produce values.
So how should looks default strategy for all streams? Or for this case it should be overrided?

from flyd.

paldepind avatar paldepind commented on July 30, 2024

For first function best name, I think, combine. Same name used in almost all Rx libs. There is a difference in arguments passed to function, but combine is closest name.
Second can be named merge, but it'll be actually wrong. merge should just emit from all dependencies. With nameIt2 it's possible to do practically all known methods on stream.

Why it should be two separate methods? Because it has different intent. And nameIt1 is just shortcut for nameIt2 with checking values at beginning of body.

combine sounds too much like merge. And as you noted merge is a different thing. I think that this naming would be confusing. And I think the reason why your names are so identical is because the two functions you're proposing does pretty much the same thing. Are more descriptive naming would be stream and streamManualDeps. But then why not just use a boolean parameter like now?

The way I see it they both have the same intent: the intent to create streams that depends on other streams. There's just a difference in how they handle dependencies and that difference, I think, can be expressed just fine with an optional parameter.

It'll be same as trigger an update with {reduced: true} and check this in 'body'. It requires every body in listeners to check this. So calling .end() will be a pain if checking was omitted in some stream.

Only functions that has doesNotRequireDeps = true will have to deal with ended streams. Other streams will just close when their dependents closes (and let me note that almost all stream are of the second type, i.e. they need all their dependencies).

I do not understand the combine function you've defined. What does the reduce function you attach to the stream do?

Your interval function is a bit odd. You read the value from the stream a only one single time! As I said before I don't think an interval function would have any dependencies. Take a look at the interval functions in Kefir og Bacon. They just take a value – not a stream.

With my proposal a delayed stream could be created like this:

var a = flyd.stream();

var delayed = flyd.stream([a], function (self, changed) {
  if (!a.hasValue) {
    // a is required for this stream
    return;
  }
  if (changed.ended) {
      setTimeout(function () {
        flyd.end(self);
    }, 1000);
  }
  var value = a();
  setTimeout(function () {
    self(value);
  }, 1000);
}, true); // Pass true to manage dependencies the low level way

var c = flyd.stream([delayed], function () {
  // This stream automatically ends 1 second after `a` ends
  return delayed() + 1;
})

a(1);
flyd.end(a);

To me the approach seems powerful enough. And you force streams that decides to manually manage their dependencies to handle ended streams. Actually, reading from an ended stream could throw an error. Then it is exposed if a dependent stream doesn't correctly handle ended streams.

from flyd.

paldepind avatar paldepind commented on July 30, 2024

c86b913 makes the following changes:

  • Adds an onEnd methods to streams exactly like you suggested (I actually misunderstood this at first, I thought you meant a function like Bacons onEnd). I am however considering renaming this to something like onDepEnd. Streams are given a default onEnd implementation that ends the stream as soon as the first dependency ends. So supplying the method is completely optional and no additional burden is placed on streams in the common case.
  • Removes flyd.destroy.
  • Adds flyd.end that calls onEnd on it all its dependents.

I am very happy with how little code had to be added and I think this approach solves the problems in a very elegant way.

I have not pushed this to npm yet though. I'd very much like to hear what you think about this @iofjuupasli.

from flyd.

iofjuupasli avatar iofjuupasli commented on July 30, 2024

@paldepind awesome work.
Now ending possible without headache.
However my last example with reduce is more flexible and powerful, I think. Probably I misnamed it.
It's stream of end events. So it possible to end stream when some another stream have some value, or some stream have emitted end.

Stream that ends after 1s after creation:

s.ending = flyd.stream([], function(self){
  setTimeout(function(){self(true)}, 1000);
}, true)

Stream s ends right as a ends:

s.ending = a.ending;

Stream s ends when a emits 0:

s.ending = flyd.stream([a], function(){
  if (a() === 0) { return true; }
})

s ends when a ends or killerStream emits any value;

s.ending = flyd.merge([a.ending, killerStream])

And so on. For me it looks really flexible and simple to understand.

After .ending emits any value, it unsubscribe streams from current stream, and also destroy .ending stream on itself.

And with .ending by default with same as current behavior

from flyd.

paldepind avatar paldepind commented on July 30, 2024

I'm sorry but I don't understand your idea. What is the ending stream you attach to streams? The stream in ending ends the stream it is attached to when it emits a value?

Why not just to this for a stream that ends after 1s:

var s = flyd.stream([someDep], function(self) {
  setTimeout(function() { flyd.end(self); }, 1000);
  return someDep();
});

And this for a stream that ends when its dependency emits 0:

var s = flyd.stream([a], function(self) {
  if (a() === 0) {
    flyd.end(self)
  } else {
    return a();
  }
});

from flyd.

iofjuupasli avatar iofjuupasli commented on July 30, 2024

The stream in ending ends the stream it is attached to when it emits a value?

Yes.
But it also make possible to subscribe to ending of any stream. And for some cases shorter.

var s = flyd.stream([b], function(){
  return a() + b();
})
s.ending = flyd.merge([a.ending, killerStream])

How you'll implement this without ending stream?

You can say that it's rare case. But it's so flexible, that there is no extra code for simple cases in your examples, but gives more possibilities. For me it looks like win-win.

Maybe you can find better name for ending, not sure that it's descriptive enough.

from flyd.

paldepind avatar paldepind commented on July 30, 2024

Ok, I get it now. I do like the idea! It is indeed very flexible and quite neat.

But does it make sense for a streams ending to depend on something that is not one of its dependencies? I'm not sure but having the flexibility seems nice!

You also have the issue that due to atomic updates the body of the ending stream will only be called once if several streams ends at the same time. Like this:

var a = stream(1);
a.ending = flyd.stream([a], function(){
  if (a() === 0) return true;
});
var b = flyd.stream([a], function(){
  return a() * 2;
});
b.ending = flyd.stream([b], function(){
  if (b() < 5) return true;
});
var s = flyd.stream([b], function(){
  return a() + b();
});
s.ending = flyd.stream([a.ending, b.ending], function(self, changed) {
  // I'm only called once
  console.log(changed + ' ended');
});

The body of s.ending might expect to be called once for every ended stream? See for instance how I've implemented flyd.merge. It counts how many streams are left based on how many times it was called. How would you implement it with your proposal?

from flyd.

iofjuupasli avatar iofjuupasli commented on July 30, 2024

But does it make sense for a streams ending to depend on something that is not one of its dependencies?

I like flyd for its flexibility so that it doesn't force you to write only idiomatic code. In flyd.stream it's possible to read value from another stream not from dependencies. But sometime it gives capability to write much better code, more readable, but with same semantic as with bacon or similar. And sometimes it's really right way to write FRP code.

You also have the issue ...

Not sure what do you mean exactly.

s.ending = flyd.merge([a.ending, b.ending])

Or maybe same as current with stream:

var deps = 2;
s.ending = flyd.stream([a.ending, b.ending], function(){
    if (--deps === 0) return true;
}, true)

from flyd.

paldepind avatar paldepind commented on July 30, 2024

I like flyd for its flexibility so that it doesn't force you to write only idiomatic code. In flyd.stream it's possible to read value from another stream not from dependencies. But sometime it gives capability to write much better code, more readable, but with same semantic as with bacon or similar. And sometimes it's really right way to write FRP code.

Yes. You are completely right! Being unrestricted, powerful and unlimiting was the design goal from the beginning. I've thought some more about this and I really like the power your proposal gives. And I can think of situations where that power would be useful!

The problem is that if a.ending and b.ending ends at the same time the body will only be called once due to atomic updates and thus the count will be incorrect.

I think a solution could be to make changed an array of all streams that has changed in the given update. That could be useful in other situations as well.

from flyd.

iofjuupasli avatar iofjuupasli commented on July 30, 2024

I think a solution could be to make changed an array of all streams that has changed in the given update. That could be useful in other situations as well.

That would be nice. Agree that it could be useful in general.

from flyd.

paldepind avatar paldepind commented on July 30, 2024

49af1e3 adds this end functionality!

You can now do stuff like this (I've renamed ended to end):

var x = stream(3);
var xEq0 = stream([x], function() {
  if (x() === 0) return true;
});
var y = stream([x], function() {
  return x();
});
flyd.endsOn(xEq0, y);
flyd.map(function() {
  console.log('y ended');
}, y.end);
x(2);
x(0); // `y` ended

By default a stream still ends as soon as one of it's dependencies end.

Implementing this was a bit tricky. Every stream has an end stream, but the every end stream would have and end-end stream. That recurses forever:

var s = flyd.stream();
s.end; // <- That is a stream
s.end.end; // <- Is that also a stream?
s.end.end.end; // <- Does this go on forever?

The current implementation breaks the chain by not giving the default end stream an end stream.

Anyway, I think the current implementation has room for some refactoring.

from flyd.

paldepind avatar paldepind commented on July 30, 2024

I've added initial documentation for how stream endings work to the readme.

Thanks a lot @iofjuupasli for sharing your great ideas!

from flyd.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.