Skip to content
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

Track active observables #309

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Conversation

bpinto
Copy link
Contributor

@bpinto bpinto commented Dec 2, 2020

Hey all, first of all thanks for creating and managing this library.

I'd like to discuss adding support for tracking active observables so that we may support features like this on tests:

import { activeObservables, clearActiveObservables } from 'kefir'

beforeEach(() => { clearActiveObservables() })
afterEach(() => { expect(activeObservables.length).toEqual(0) })

and to eventually create a browser extension or even simpler to do a $K.activeObservables on the browser console - assuming you've bound kefir to window window.$K = Kefir - and see if there are any memory leaks on your code.

P.S.: I've pushed two commits labeled "remove me" with the intention to diff the modified dist files (see second commit), these commits are going to be removed before merging this PR.

@mAAdhaTTah
Copy link
Collaborator

Definitely going to need to ponder this a bit.

I'm not confident on my Proxy usage. Perhaps someone has another idea on how to track observables?

I don't think we need to go full Proxy. You can just monkeypatch the method. If you wanted to use Proxy, you should probably Proxy the entire Observable object and then trap the particular methods in the Proxy handlers.

@bpinto
Copy link
Contributor Author

bpinto commented Dec 3, 2020

You can just monkeypatch the method.

What is your suggestion so I could try implementing it? One problem I had before was that Kefir.atom and other classes will extend this Observable class and thus remove any implementation we set on _onActivation and _onDeactivation.

@mAAdhaTTah
Copy link
Collaborator

What is your suggestion so I could try implementing it?

Ultimately not terribly different from what you have, except instead of returning a proxy, you return a new function.

One problem I had before was that Kefir.atom and other classes will extend this Observable class and thus remove any implementation we set on _onActivation and _onDeactivation.

Not 100% sure I follow; if you have an example, that would be helpful. But if we're wrapping the method, because we still end up calling the underlying method, I don't think it'll be an issue.

@bpinto
Copy link
Contributor Author

bpinto commented Dec 3, 2020

Ultimately not terribly different from what you have, except instead of returning a proxy, you return a new function.

Ok, I'll update this PR soon.

Not 100% sure I follow; if you have an example, that would be helpful. But if we're wrapping the method, because we still end up calling the underlying method, I don't think it'll be an issue.

I misunderstood what you said earlier, I thought you had suggested we wouldn't call the original method. 👍

@bpinto
Copy link
Contributor Author

bpinto commented Dec 3, 2020

Okay, so I've pushed another code which does not use Proxy, now there is something weird happening, the following test would not unsubscribe the observable:

export const getValue = maybeObservable => {
  if (!isObservable(maybeObservable)) return maybeObservable
  let value
  const setValue = val => value = val
  const firstValue = maybeObservable.take(1)
  firstValue.onValue(setValue)
  firstValue.offValue(setValue)
  return value
}

test('returns syncronous value from observables', () => {
  expect(streams.getValue(K.constant(1))).toBe(1)
})

The following observable is still tracked by Kefir.activeObservables:

    [ AnonymousObservable {
        _dispatcher: null,
        _active: false,
        _alive: false,
        _activating: false,
        _logHandlers: null,
        _spyHandlers: null,
        _onActivation: [Function: _onActivation],
        _onDeactivation: [Function: _onDeactivation],
        _currentEvent: { type: 'value', value: 1 },
        _source: null,
        _name: 'constant.take',
        _n: 0,
        '_$handleAny': null } ]

I don't understand yet why this observable has not been removed from the array, but I'll debug it further.

P.S.: Would you rather use apply from src/utils/function ?

- [x] mocha 7.0.0: dropped support for node v6.x
- [x] mocha 8.0.0: dropped support for node v8.x
- [x] mocha 8.0.0: deprecated mocha.opts
- [x] mocha 9.0.0: dropped support for node v10.x
@bpinto
Copy link
Contributor Author

bpinto commented Aug 15, 2021

@mAAdhaTTah I've finally come back to this PR and while there are commits that I need to extract into smaller PRs (some I already have), this is ready for review.

I have a commit where I had to brute-force subscription deactivation because unless I missed it, I don't think there is another way to handle it with the existing code.

I'm not sure if it's worth exporting the onAny function on toPromise as the subscription should be deactivating automatically as a promise always ends, right?

@bpinto
Copy link
Contributor Author

bpinto commented Sep 3, 2021

@mAAdhaTTah I'm thinking that maybe we shouldn't introduce a new dev dist and just include these methods on the normal distributed files?

@mAAdhaTTah
Copy link
Collaborator

Maybe I should ask more generally: what's the purpose of this? Why do you need to track active observables?

@bpinto
Copy link
Contributor Author

bpinto commented Sep 8, 2021

I think the main purpose of this would be to track memory leaks (or observable leaks).

An usage example:

afterAll(() => {
  if (activeObservables.length !== 0) {
    fail()
  }
})

test('all observables are unsubscribed', () => {
  const {unmount} = render(<MyComponent />)
  unmount()
})

It's very easy to make a mistake and forget to unsubscribe an observable when using .observe for instance. To confirm how easy it is to introduce these memory leaks, I fixed some kefir tests that had not unsubscribed to all observables.

@mAAdhaTTah
Copy link
Collaborator

If the primary goal is testing, I think something like this might be more suited to the kefit-test-utils package. We could monkey patch the Observable.prototype in that package to track this info without expanding the base package for an uncommon use case.

@bpinto
Copy link
Contributor Author

bpinto commented Sep 8, 2021

We could, though I think that tracking observables during a test environment is not the only usage. We could for instance want to run this on our browser window and make sure that while navigating from one page to another the number of observables is corresponding to what we expect.

I was thinking about using a configurable environment variable that would default to the test environment so that users could opt in and out in any environment.

const { NODE_ENV, TRACK_OBSERVABLES }
const trackObservables = TRACK_OBSERVABLES || NODE_ENV === 'test'

P.S.: I'm happy to move it to kefir-test-utils if you think it's not worth adding it to kefir.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants