Feathers-vuex: Question: Recommendable way to track instance status "isPatchPending, etc..."

Created on 19 Aug 2020  路  9Comments  路  Source: feathersjs-ecosystem/feathers-vuex

Expected behavior

I load a list of ToDo in some card grid, I delete one and I show "Deleting..." on that specific card while I'm waiting for the store to be updated and this card to disappear.

I imagined it would be nice to bind a reactive kind of status like "MyTodoInstance.isRemovePending" or something like that, like having some isRemovePending or isPatchPending but on the specific model instance that you used to call "MyTodoInstance.remove() or .save() etc..."

I was thinking of extending the current BaseModel but then I might endup with something that will break apart on your next library update so I thought asking you first haha. If that is something you are interested in I don't mind contributing.

Thx!

Most helpful comment

Here's my lovely COVID quarantine project :-)

The business logic related to this issue is all in this file https://gitlab.com/hamiltoes/preserve/-/blob/master/src/feathers-client.ts (among a lot of other stuff!)

It doesn't do exactly what @barnacker is asking, but as @marshallswain said, maybe it will give a boost in getting this implemented in FeathersVuex itself.

The main difference is that in my app, I was tracking instance statuses for create and patch under one status: is this item saving. I was not tracking update or remove, but that can extend pretty naturally from here.

State

The extra state that I included is typed here

interface AutoSaveServiceState {
  /** Track IDs that are currently debouncing a call to save() */
  saveDebouncingById: { [key in IdType]: boolean }
  /** Track IDs that are currently saving */
  saveWaitingIds: IdType[]
}

and implemented here.

In my app I auto-save as the user inputs data so I debounce saves. Hence the saveDebouncingById object. I decided to use a plain object for saveDebouncingById because if the user triggers multiple saves on the same instance, due to debouncing the saves, there is only ever one is this instance debouncing a save status.

But for saveWaitingIds, I decided to use an array because there could be multiple saves triggered on the same object in overlapping times (if there is any sort of substantial lag between saves). So for the actual are we waiting on the server status, I push the instance's ID onto this array when a save starts, and then remove one instance of the ID from the array when the save finishes (when the server responds). That way if four saves are triggered in an overlapping fashion, then the instances save status is not done pending until the fourth save finishes.

If you don't need to account for the possibility of overlapping saves, then a simple object will work fine!

Getters

The getters for retrieving the instance pending status are implemented here,

    getters: {
      getSaveDebouncing({ saveDebouncingById }) {
        for (const key in saveDebouncingById) {
          if (Object.prototype.hasOwnProperty.call(saveDebouncingById, key)) {
            return true;
          }
        }
        return false;
      },
      getSaveDebouncingById({ saveDebouncingById }) {
        return (id: IdType) => saveDebouncingById[id] === true;
      },
      getSavePending(_, { getSaveDebouncing, getSaveWaiting }): boolean {
        return getSaveDebouncing || getSaveWaiting;
      },
      getSaveWaiting({ saveWaitingIds }) {
        return saveWaitingIds.length > 0;
      },
      getSaveWaitingById({ saveWaitingIds }) {
        return (id: IdType) => saveWaitingIds.includes(id);
      },
    },

and the mutations are here

    mutations: {
      setIdDebouncing({ saveDebouncingById }, id) {
        Vue.set(saveDebouncingById, id, true);
      },
      setIdSaving({ saveWaitingIds }, id) {
        saveWaitingIds.push(id);
      },
      unsetIdDebouncing({ saveDebouncingById }, id) {
        Vue.delete(saveDebouncingById, id);
      },
      unsetIdSaving({ saveWaitingIds }, id) {
        const idx = saveWaitingIds.indexOf(id);
        if (idx >= 0) {
          Vue.delete(saveWaitingIds, idx);
        }
      }
    },

Actions

I override the default create action here and the patch action here so I can call my mutations.

BaseModel convenience getters

Then my derived BaseModel (called AutoSaveBaseModel) has getters for retrieving the instance status via the Vuex getters and those are implemented here

/** this is an excerpt of my AutoSaveBaseModel implementation **/
  /**
   * Is this Model currently debouncing a call to save?
   */
  get isSaveDebouncing() {
    const { _getters } = this.constructor as typeof BaseModel;
    return _getters.call(this.constructor, "getSaveDebouncingById", this.__id || this.id)
  }

  /**
   * Is this Model currently debouncing OR waiting for save response?
   */
  get isSavePending() {
    return this.isSaveDebouncing || this.isSaveWaiting;
  }

  /**
   * Is this Model currently being saved and waiting for API response?
   */
  get isSaveWaiting() {
    const { _getters } = this.constructor as typeof BaseModel;
    return _getters.call(this.constructor, "getSaveWaitingById", this.__id || this.id)
  }

Summary

This is is probably overkill. Unless you need debouncing and handling overlapping saves, this can be greatly simplified. Can also be expanded for each feathers method if desired.

I hope this helps someone :-)

All 9 comments

This is a great idea. I could use this everywhere in my apps. I would prefer to find a way to store it that doesn't end up saving to the database. Putting the extra attributes on individual model records will make for a nice API, as you've demonstrated in your comment.

Just wanted to throw in my 2c cuz I've done this in an app I was working on.

It ended up working better for me to store the individual pending status in the Vuex store on a dictionary for each feathers method. The key to each dictionary being the tempId or id of each instance. Then in my extended base model, I just had a getter, (e.g. isPatchPending) that looked up its own id in that table. This way any instance clones would also show status.

But I agree this a great feature that more people would probably use if it was included out of the box!

Ah. I'm grateful for your input @hamiltoes. That's a really smart solution. I hadn't thought about the clones. If you still have access to that code it might give us a boost in getting it implemented. If not, no worries. I'm feeling like that's the way to built it.

Yes we discussed using that approach internally, the main reason being that it shouldn't break much in the upcoming updates of this package. But just in case, before coding anything, I thought of posting it here since it could be included out of the box like you mentioned. I was imagining it done automatically when registering the feather "ToDo" service in the store.

Like you just proposed, it would automatically have an extra list like "instancesStates" the base model object would contain those getters out of the box. The .save() would set the values automatically True/False etc...

Something like that. And if the owners/contributors of this library are opened to that, I would be willing to share the final code in hopes that it be included in a version of the official library as a new feature.

Here's my lovely COVID quarantine project :-)

The business logic related to this issue is all in this file https://gitlab.com/hamiltoes/preserve/-/blob/master/src/feathers-client.ts (among a lot of other stuff!)

It doesn't do exactly what @barnacker is asking, but as @marshallswain said, maybe it will give a boost in getting this implemented in FeathersVuex itself.

The main difference is that in my app, I was tracking instance statuses for create and patch under one status: is this item saving. I was not tracking update or remove, but that can extend pretty naturally from here.

State

The extra state that I included is typed here

interface AutoSaveServiceState {
  /** Track IDs that are currently debouncing a call to save() */
  saveDebouncingById: { [key in IdType]: boolean }
  /** Track IDs that are currently saving */
  saveWaitingIds: IdType[]
}

and implemented here.

In my app I auto-save as the user inputs data so I debounce saves. Hence the saveDebouncingById object. I decided to use a plain object for saveDebouncingById because if the user triggers multiple saves on the same instance, due to debouncing the saves, there is only ever one is this instance debouncing a save status.

But for saveWaitingIds, I decided to use an array because there could be multiple saves triggered on the same object in overlapping times (if there is any sort of substantial lag between saves). So for the actual are we waiting on the server status, I push the instance's ID onto this array when a save starts, and then remove one instance of the ID from the array when the save finishes (when the server responds). That way if four saves are triggered in an overlapping fashion, then the instances save status is not done pending until the fourth save finishes.

If you don't need to account for the possibility of overlapping saves, then a simple object will work fine!

Getters

The getters for retrieving the instance pending status are implemented here,

    getters: {
      getSaveDebouncing({ saveDebouncingById }) {
        for (const key in saveDebouncingById) {
          if (Object.prototype.hasOwnProperty.call(saveDebouncingById, key)) {
            return true;
          }
        }
        return false;
      },
      getSaveDebouncingById({ saveDebouncingById }) {
        return (id: IdType) => saveDebouncingById[id] === true;
      },
      getSavePending(_, { getSaveDebouncing, getSaveWaiting }): boolean {
        return getSaveDebouncing || getSaveWaiting;
      },
      getSaveWaiting({ saveWaitingIds }) {
        return saveWaitingIds.length > 0;
      },
      getSaveWaitingById({ saveWaitingIds }) {
        return (id: IdType) => saveWaitingIds.includes(id);
      },
    },

and the mutations are here

    mutations: {
      setIdDebouncing({ saveDebouncingById }, id) {
        Vue.set(saveDebouncingById, id, true);
      },
      setIdSaving({ saveWaitingIds }, id) {
        saveWaitingIds.push(id);
      },
      unsetIdDebouncing({ saveDebouncingById }, id) {
        Vue.delete(saveDebouncingById, id);
      },
      unsetIdSaving({ saveWaitingIds }, id) {
        const idx = saveWaitingIds.indexOf(id);
        if (idx >= 0) {
          Vue.delete(saveWaitingIds, idx);
        }
      }
    },

Actions

I override the default create action here and the patch action here so I can call my mutations.

BaseModel convenience getters

Then my derived BaseModel (called AutoSaveBaseModel) has getters for retrieving the instance status via the Vuex getters and those are implemented here

/** this is an excerpt of my AutoSaveBaseModel implementation **/
  /**
   * Is this Model currently debouncing a call to save?
   */
  get isSaveDebouncing() {
    const { _getters } = this.constructor as typeof BaseModel;
    return _getters.call(this.constructor, "getSaveDebouncingById", this.__id || this.id)
  }

  /**
   * Is this Model currently debouncing OR waiting for save response?
   */
  get isSavePending() {
    return this.isSaveDebouncing || this.isSaveWaiting;
  }

  /**
   * Is this Model currently being saved and waiting for API response?
   */
  get isSaveWaiting() {
    const { _getters } = this.constructor as typeof BaseModel;
    return _getters.call(this.constructor, "getSaveWaitingById", this.__id || this.id)
  }

Summary

This is is probably overkill. Unless you need debouncing and handling overlapping saves, this can be greatly simplified. Can also be expanded for each feathers method if desired.

I hope this helps someone :-)

Wow that's very interesting, I will take some time aside to look into it because I'm convinced that this can help doing what I want! :)

Also: Great formatting and documentation of the code!! 馃憤

This is now available in [email protected]. Thanks, everybody!

Wow! Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gustojs picture gustojs  路  7Comments

kaizenseed picture kaizenseed  路  7Comments

daenuprobst picture daenuprobst  路  5Comments

tguelcan picture tguelcan  路  6Comments

apmcodes picture apmcodes  路  6Comments