question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Design problems with async stop

See original GitHub issue

Because of issues https://github.com/cyclejs/cyclejs/issues/365 and https://github.com/staltz/xstream/issues/90, I made a fix to xstream which gives special treatment to a corner case in flatten, which was released in v5.3.2. https://github.com/staltz/xstream/commit/819bc94883eb32f0debac5f29468e609a62416fa

However, (1) it doesn’t actually fix the problem, (2) it conflicts with previously desired features. It also makes new bugs appear such as #103.

(1)

Note how we added this test, which passed in v5.3.2:

    it('should restart inner stream if switching to the same inner stream', (done) => {
      const outer = fromDiagram('-A---------B----------C--------|');
      const nums = fromDiagram(  '-a-b-c-----------------------|', {
        values: {a: 1, b: 2, c: 3}
      });
      const inner = nums.fold((acc, x) => acc + x, 0);

      const stream = outer.map(() => inner).flatten();

      const expected = [0, 1, 3, 6, 0, 1, 3, 6, 0, 1, 3, 6];

      stream.addListener({
        next: (x: number) => {
          assert.equal(x, expected.shift());
        },
        error: (err: any) => done(err),
        complete: () => {
          assert.equal(expected.length, 0);
          done();
        }
      });
    });

However, this does not pass in v5.3.2:

    it('should restart inner stream if switching to the same inner stream', (done) => {
      const outer = fromDiagram('-A---------B----------C--------|');
      const nums = fromDiagram(  '-a-b-c-----------------------|', {
        values: {a: 1, b: 2, c: 3}
      });
      const inner = nums.fold((acc, x) => acc + x, 0);

-     const stream = outer.map(() => inner).flatten();
+     const stream = outer.map(() => inner.map(x => x)).flatten();

      const expected = [0, 1, 3, 6, 0, 1, 3, 6, 0, 1, 3, 6];

      stream.addListener({
        next: (x: number) => {
          assert.equal(x, expected.shift());
        },
        error: (err: any) => done(err),
        complete: () => {
          assert.equal(expected.length, 0);
          done();
        }
      });
    });

Because every time the function () => inner.map(x => x) is called, inner.map(x => x) will yield a different stream, where as () => inner would always yield inner as the same stream.

(2)

Sync start and async stop was designed to allow the inner stream to not restart if it was the same during the switch in a flatten. This was really by design, to avoid some confusion with a common pattern we had in Cycle.js, the use of RxJS connect() here: https://github.com/cyclejs/cyclejs/commit/67d176e3e8e6f96b776428fc55f5c29c81f54607#diff-32a0a3abed94d032137ef603ee4dd261L30

So “don’t restart the inner stream if it remains the same during flatten” is a feature by design.

However, to have referential transparency we want these two cases to give the same behavior:

    const inc$ = sources.DOM.select('.inc').events('click').mapTo(+1);
    const refresh$ = sources.DOM.select('.ref').events('click').startWith(0);
+   const sum$ = inc$.fold((x, y) => x + y, 0);
+   const lastSum$ = refresh$.map(_ => sum$).flatten();
-   const lastSum$ = refresh$.map(_ => inc$.fold((x, y) => x + y, 0)).flatten();
    const vdom$ = lastSum$.map(count =>
      div([

Which means we want the property “restart the inner stream if it remains the same during flatten” by design.

Which means we have a conflict, and we need to choose which of these two to do. It may mean a breaking change. I had hopes sync start and async stop would make things more intuitive but there is an obvious drawback that makes xstream less intuitive.

If you’re reading this thread, please give your friendly and thoughtful opinion on this topic. This appears to be my design mistake, but I’m just a human. What’s important is that I’m willing to look for a better way forward. My intent with sync start and async stop was to provide an “just works” experience for most cases, and together with Tylor we did a lot of predictions and bike-shedding, but a corner case slipped out of our sight.

For now, I’ll revert the bugfix that happened in v5.3.2, so that other issues don’t surface, and to keep a consistent behavior.

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Comments:15 (13 by maintainers)

github_iconTop GitHub Comments

5reactions
Hypnosphicommented, Aug 15, 2016

I liked the non-restarting behavior more. The restarting lowers the streams temperature, and referential transparency isn’t really compatible with hotness

4reactions
staltzcommented, Aug 20, 2016

After thinking about this for a few days, maybe settling with non-restarting is better, while accepting lack of referential transparency.

restarting is incompatible with non-restarting, but restarting is also incompatible with use cases of multiple listeners, see below:

const sum$ = inc$.fold((x, y) => x + y, 0);
sum$.addListener(/* ... */);
const lastSum$ = refresh$.map(_ => sum$).flatten();
// not the same as:
const lastSum$ = refresh$.map(_ => inc$.fold((x, y) => x + y, 0)).flatten();

// because the sum$ will not restart since it always 
// has the listener on the second line.
// While we keep the guarantee that each stream has only one execution.

I think the best way to resolve this is keep non-restarting behavior consistent, and then add fromObservable to allow composing streams in the cold world of RxJS or most.js, then convert to the hot world in xstream when we want to.

Please 👍 if you agree. Comment if you don’t.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Async/Await - Best Practices in Asynchronous Programming
Mixed async and blocking code can cause deadlocks, more-complex error handling and unexpected blocking of context threads. The exception to this guideline is ......
Read more >
Design with async/await - should everything be async?
It leads to deadlocks: if all ThreadPool threads are Wait ing there are no threads to complete any task. So the question is:...
Read more >
async/await is the wrong abstraction - LogRocket Blog
What we are trying to do with async/await is to ignore reality and have these async operations appear to be happening synchronously.
Read more >
Some thoughts on asynchronous API design in a post-async ...
When a connection arrives, proxy first tells main to stop listening (line ... The problem is that again, it's dest_writer.close(), not await ......
Read more >
Long Story Short: Async/Await Best Practices in .NET - Medium
You may be tempted to “stop” this by blocking in your code using Task. ... The best solution to this problem is to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found