# @tozd/vue-observer-utils

This NPM package exposes [Vue observer](https://vuejs.org/v2/guide/reactivity.html) internals (code which provides
reactivity in Vue). Moreover, it extends the observer with additional API.

## Installation

This is a NPM package. You can install it using NPM:

```bash
$ npm install @tozd/vue-observer-utils
```

It requires `vue` peer dependency:

```bash
$ npm install vue
```

## Usage

First, you have to register the package as a Vue plugin:

```js
import Vue from 'vue';
import VueObserverUtils from '@tozd/vue-observer-utils';

Vue.use(VueObserverUtils);
```

After that, you can access:

* [`Dep`](https://github.com/vuejs/vue/blob/dev/src/core/observer/dep.js) at `Vue.util.Dep`
* [`Watcher`](https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js) at `Vue.util.Watcher`

For example, to know if you code is being executed in a reactive context you can now
check if `Vue.util.Dep.target` is set. Moreover, `Vue.util.Dep.target` gives you access to
the current reactive context, a `Watcher` instance.

## API

The following are additional API functions available.

### `Vue.util.nonreactive(f)`

Calls function `f` outside of a reactive context and returns its returned value.

### `Vue.util.onInvalidate(callback)`

Registers `callback` to run when current reactive context is next invalidated,
or runs it immediately if the reactive context is already invalidated.
The callback is run exactly once and not upon future invalidations unless
`onInvalidate` is called again after the reactive context becomes valid again.

Registered `callback` is run also when the reactive context is teared down.

### `Vue.util.onTeardown(callback)`

Registers `callback` to run when current reactive context is teared down,
or runs it immediately if the reactive context is already teared down.
The callback is run after any `onInvalidate` callbacks.

### `vm.$wait(condition, effect)`

Runs `condition` function repeatedly inside a reactive context until it returns
a truthy value, after which `effect` function is called once provided that value.
`effect` is run exactly once unless `$wait` is called again.

`$wait` returns `unwait` function which you can call to stop waiting.
If `condition` is already satisfied when `$wait` is called, then `effect`
function is called immediately even before `$wait` returns. Moreover, `$wait`
returns `null` in this case.

### `vm.$await(promise, [options])`

Returns `undefined` until the `promise` is resolved, after that
it returns the resolved value of the `promise`. It is reactive
so the value changes once the `promise` gets resolved.

Important: `$await` is meant to be used inside a reactive context
which can rerun multiple times. Make sure that you are not creating
a new (different) `promise` for every rerun by mistake, but reuse
an existing one.

Available `options`:

* `key`, default `promise`: an `Object` to identify the promise by,
  this is used to remember and retrieve resolved value in when
  reactive context is rerun
* `forgetRejected`, default `false`: if `promise` is rejected,
  forget that it has been run so that if reactive context is rerun,
  a new attempt at resolving will be made (instead of returning
  `undefined`)
* `invalidateRejected`, default `false`: if `promise` is rejected,
  invalidate the reactive context to force rerun

## Examples

See [`@tozd/vue-format-time`](https://gitlab.com/tozd/vue-format-time) for an example
how you can use `Vue.util.onInvalidate` to implement an efficient time-based reactive
transformations.

### Alert queue

```js
const component = {
  data() {
    return {
      queue: [],
    };
  },

  created() {
    this.unwait = null;
    this.showNextAlert();
  },
  
  methods: {
    showNextAlert() {
      // Wait for the first next message to be available.
      this.unwait = this.$wait(function () {
        // Messages are enqueued from oldest to newest and "find" searches array elements in
        // same order as well, so the first one which matches is also the oldest one.
        return this.queue.find((element) => element.shown === false);
      }, function (message) {
        this.unwait = null;
        
        message.shown = true;
        
        alert(message.text);
        
        this.showNextAlert();
      });
    },
    
    addAlert(text) {
      this.queue.push({
        text,
        shown: false,
      })
    },
  },
};
```

For a more feature-full implementation of a notification queue,
see [`@tozd/vue-snackbar-queue`](https://gitlab.com/tozd/vue-snackbar-queue).

### Reactive fetch

In this example we maintain a simple cache of promises for all
`fetch` requests we made. We assume the mapping does not change:
once user's ID is resolved to an username that never changes.
In your case you might want to have cache invalidation as well.
Moreover, this cache can grow indefinitely even if some values
are not used anymore (for example, a particular user is not
displayed anymore so we do not need their username). You can use
`onInvalidate` to track when a particular promise is not being used
anymore and if it is not used again in a later rerun, you could
remove if from the cache (maybe after some expiration time).

```js
// This is shared between all component instances.
const idToUsername = new Map();

const component = {
  data() {
    return {
      users: [
        893,
        991,
        140,
      ],
    };
  },
  
  computed: {
    usernames() {
      // We want to fetch username for each user in a reactive manner.
      return this.users.map((user) => {
        return this.getUsername(user);
      });
    },
  },

  methods: {
    // This method checks if we have already fetched (or are in process
    // of fetching) a username for this user. We use a simple Map in this
    // example to cache fetches made.
    getUsername(id) {
      if (!idToUsername.has(id)) {
        idToUsername.set(id, this.fetchUsername(id));
      }
      
      // It is important that we "$await" same promise so we use a Map
      // as a cache for that. In a way we create a new cache if an argument
      // to the "async" function changes.
      return this.$await(idToUsername.get(id));
    },
    
    // A regular "async" function without any reactivity.
    async fetchUsername(id) {
      const response = await fetch(`https://example.com/api/user/${id}`, {
        mode: 'cors',
        credentials: 'omit',
      });
    
      if (response.status !== 200) {
        throw new Error(`Fetch error: ${response.statusText}`);
      }
    
      const result = await response.json();
      
      return result.username;
    },
  },
};
```
