Page 1 of 1

Async operations and destroyed instances

Posted: Thu Dec 01, 2022 11:24 am
by Josh Argent

Hi,

We've come across a scenario that is causing problems for us and I'd like your advise on how you would expect this situation to be handled.

We have a number of async processes (such as loading data) that interact with the grid and store instances once the promise resolves. However, if the grid or store has been destroyed while that promise is waiting, once it resolves we get errors when it tries to interact with the grids properties or methods. As far as I can tell, when something is destroyed it removes all properties from that instance, which is why we see "Cannot read properties of null" errors.

What do you suggest we do in these scenarios? Should we be checking that the grid is not destroyed every time a something resolves?

I have put together an example using the Tree Grid demo. There are two buttons in the toolbar "Async Thing" and "Destroy". If you press "Async Thing" it triggers a setTimeout for 5 seconds which will eventually call a method on the grid. If you press "Destroy" before the timeout resolves, you end up getting an error in the console.

import { TreeGrid, GridRowModel } from '../../build/grid.module.js?463787';
import shared from '../_shared/shared.module.js?463787';

class Gate extends GridRowModel {
    static get fields() {
        return [{
            name : 'capacity',
            type : 'number'
        }, 'domestic', 'airline', 'manager'];
    }
}

// Transform a parent node to a leaf node when all its children are removed
Gate.convertEmptyParentToLeaf = true;

const tree = new TreeGrid({

appendTo : 'container',

features : {
    cellEdit   : true,
    filter     : true,
    rowReorder : true,
    stripe     : true
},

loadMask : 'Loading tree data...',

columns : [
    { text : 'Id', field : 'id', width : 40, editor : false },
    { text : 'ParentIndex', field : 'parentIndex', width : 40, hidden : true },
    {
        text        : 'Name',
        field       : 'name',
        flex        : 3,
        type        : 'tree',
        touchConfig : { editor : false }
        // You can customize expand/collapse icons
        // expandIconCls   : 'b-fa b-fa-plus-square',
        // collapseIconCls : 'b-fa b-fa-minus-square'
    },
    { type : 'aggregate', text : 'Capacity', field : 'capacity', flex : 1 },
    { text : 'Domestic', field : 'domestic', flex : 1 },
    { text : 'Airline', field : 'airline', flex : 1 },
    { text : 'Responsible<br/>Manager', field : 'manager', width : 100, htmlEncodeHeaderText : false }
],

store : {
    modelClass : Gate,
    readUrl    : 'data/kastrup-airport.json',
    autoLoad   : true
},

handleAsyncActionComplete: () => {
    console.log('async action complete!');
},

tbar : [
    {
        type        : 'button',
        ref         : 'customButton',
        icon        : 'b-fa-folder-open',
        pressedIcon : 'b-fa-plane',
        text        : 'Use custom tree icons',
        toggleable  : true,
        onToggle({ pressed }) {
            tree.store.readUrl = 'data/' + (pressed ? 'ohare-airport.json' : 'kastrup-airport.json');
            tree.element.classList[pressed ? 'add' : 'remove']('ohare-airport');
            tree.store.load();
        }
    },
    {
        type     : 'button',
        ref      : 'expandAllButton',
        icon     : 'b-fa b-fa-angle-double-down',
        text     : 'Expand all',
        onAction : () => tree.expandAll()
    },
    {
        type     : 'button',
        ref      : 'collapseAllButton',
        icon     : 'b-fa b-fa-angle-double-up',
        text     : 'Collapse all',
        onAction : () => tree.collapseAll()
    },
    {
        type     : 'button',
        text     : 'Async Thing',
        onAction : () => setTimeout(() => grid.handleAsyncActionComplete(), 5000)
    },
    {
        type     : 'button',
        text     : 'Destroy',
        onAction : () => tree.destroy()
    }
]
});

I would appreciate any suggestions you have for how to handle these situations :)

Thanks.


Re: Async operations and destroyed instances

Posted: Thu Dec 01, 2022 12:21 pm
by Animal

Use grid.setTimeout

Grid is a Widget and all Widgets are Delayables, and its own timers are all cancelled when it is destroyed.


Re: Async operations and destroyed instances

Posted: Thu Dec 01, 2022 12:40 pm
by Josh Argent

That's handy, I didn't know about grid.setTimeout!

What if it's a promise that we're waiting to resolve? Something like a call to an API to load data? We see the same problem with that.


Re: Async operations and destroyed instances

Posted: Thu Dec 01, 2022 1:42 pm
by Animal

It's just timers that are cancelled, but if the Promise is resolved by the firing of a timer, then if that timer is cancelled, the promise won't be resolved.


Re: Async operations and destroyed instances

Posted: Thu Dec 01, 2022 2:03 pm
by Josh Argent

So if the promise isn't timer based (let's say its a fetch call to an API somewhere), we have to check that the grid hasn't been destroyed before calling anything on it?


Re: Async operations and destroyed instances

Posted: Thu Dec 01, 2022 5:40 pm
by Animal

Yes, if you embark on a long (In computer terms) operation like a network request , you need to be ready to deal with conditions that apply at the time the operation finishes.

I'm not sure how components get destroyed during a running application. They should all persist until the page is exited, but if you are destroying components, then you have to code for the consequences of that.


Re: Async operations and destroyed instances

Posted: Fri Dec 02, 2022 10:47 am
by Josh Argent

I guess you wouldn't normally expect a component to be destroyed while it's running. To add some context, this is happening inside Salesforce Lightning App Builder, whenever the LWC is moved around the page it destroys and recreates the grid.

One workaround that seems to work is to add this to all of our affected components:

doDestroy() {
    this.removeAllListeners();
}

Maybe removing all event listeners when it is destroyed should be part of the events mixin?


Re: Async operations and destroyed instances

Posted: Fri Dec 02, 2022 12:00 pm
by Animal

All event listeners are removed upon destroy. See the doDestroy method in the Events mixin.

Screenshot 2022-12-02 at 11.01.22.png
Screenshot 2022-12-02 at 11.01.22.png (421.69 KiB) Viewed 822 times