Асинхронный Typescript в Rich Internet Application и декораторы для борьбы с ним

    С момента появления async/await в Typescript вышло много статей, превозносящих этот подход в разработке (hackernoon, blog.bitsrc.io, habr.com). Мы используем их с самого начала на стороне клиента (когда ES6 Generators поддерживало меньше 50% браузеров). И сейчас хочется поделиться опытом, потому что параллельное выполнение — это еще не все, что хорошо бы знать на этом пути.


    Мне не очень нравится итоговая статья: что-то может быть непонятным. Частично из-за того, что проприетарный код не могу предоставить — только обрисовать общий подход. Поэтому:


    • не стесняясь, закрывайте вкладку не дочитав
    • если все-таки осилите — спрашивайте неясные детали
    • от самых стойких и докопавшихся до сути с радостью приму советы и критику.

    Список основных технологий:


    • Проект написан в основном на Typescript с использованием нескольких Javascript библиотек. Основная библиотека — ExtJS. По модности она уступает React, но для Enterprise продукта с богатым интерфейсом подходит лучше всего: много готовых компонентов, хорошо проработанные таблицы из коробки, богатая экосистема сопутствующих продутов для упрощения разработки.
    • Асинхронный многопоточный сервер.
    • В качестве транспорта между клиентом и сервером используется RPC через Websocket. Реализация похожа на .NET WCF.
      • Любой объект является сервисом.
      • Любой объект может передаться как по значению так и по ссылке.
    • Интерфейс запроса данных напоминает GraphQL от Facebook, только на Typescript.
    • Связь двустороняя: инициализация обновления данных может быть запущена как с клиента, так и с севрера.
    • Асинхронный код пишется последовательно — через использование async/await функций Typesrcipt'а.
    • API сервера генерируется на Typescript: если оно изменяется, билд в случае ошибки сразу покажет ее.

    Что на выходе


    Расскажу, как с этим работаем и что сделали для безопасного неконкурентного выполнения асинхронного кода: свои декораторы Typesrcipt, реализующие функционал очередей. От самых основ до решения race condition и других сложностей, которые возникают в процессе разработки.


    Как структурированы данные, получаемые с сервера


    Сервер возвращает родительский объект, который содержит в своих свойствах данные (другие объекты, коллекции объектов, строки etc.) в виде графа. Это обусловлено в том числе самим приложением:


    • оно у нас делает анализ данных/ML направленным графом узлов-обработчиков.
    • каждый узел в свою очередь может содержать свой вложенный граф
    • графы имеют зависимости: узлы могут быть "отнаследованы", и по их "классу" созданы новые узлы.

    Но структура запроса в виде графа может быть применена почти в любом приложении и GraphQL, насколько мне известно, также об этом упоминает в своей спецификации.


    Пример структуры данных:


    // Родительский объект
    interface IParent {
        ServerId: string;
        Nodes: INodes; // INodes - коллекция узлов обработки данных INode
    }
    
    // Интерфейс коллекции узлов графа
    interface INodes<TNode extends INode> extends ICollection {
        IndexOf(item: TNode): number;
        Item(index: number): TNode;
        // ... Другие стандартные методы коллекции
    }
    
    // Интерфейс узла графа
    interface INode extends IItem {
        Guid: string;
        Name: string;
        DisplayName: string;
        Links: ILinks; // ILinks - коллекция связей узла
        Info: INodeInfo; // Вложенный объект с какой-то информацией
    }
    
    // Интерфейс связи между узлами графа
    interface ILink {
        Guid: string;
        DisplayName: string;
        SourceNode: INode; // Ссылка на узел-источник данных
        TargetNode: INode; // Ссылка на узел, получающий данные
    }
    
    interface INodeInfo {
        Component: IComponent;
        ConfigData: IData;
    }
    

    Как клиент получает данные


    Все просто: при запросе какого-либо свойства объекта нескалярного типа RPC возвращает Promise:


    
    let Nodes = Parent.Nodes; // Nodes -> Promise<INodes>
    

    Асинхронность без "Callback Hell".


    Для организации "последовательного" асинхронного кода используются async/await функционал Typescript:


    
    async function ShowNodes(parent: IParent): Promise<void> {
        // Получаем коллекцию узлов
        let Nodes = await parent.Nodes;
    
        // Пробегаемся по всем и отображаем их
        await Nodes.forEachParallel(async function(node): Promise<void> {
            await RenderNode(node); // Вызываем асинхронную функцию получения данных узла и его отрисовки
        });
    }
    

    Нет смысла подробно останавливаться на нем, на хабре уже есть достаточно подробный материал. Они появились в Typescript еще в 2016 году. Мы используем этот подход с тех пор, как он появился в feature ветке репозитория Typescript, поэтому давно набили шишек и теперь работаем с удовольствием. С некоторых пор уже и в production.


    Коротко суть для тех, кто еще не знаком с предметом:


    Как только вы добавляете к функции ключевое слово async, она автоматически будет возвращать Promise<Возвращаемый_тип>. Особенности таких функций:


    • Выражения внутри async функций с await (которые возвращают Promise) будут останавливать выполнение функции и продолжать после разрешения ожидаемых Promise.
    • При возникновении исключения в async функции, возвращаемый Promise будет отклонен с этим исключением.
    • При компиляции в Javascript коде будут генераторы для стандарта ES6 (функции function* вместо async function и yield вместо await) или же страшный код со switch для ES5 (конечный автомат). await — это ключевое слово, которое дожидается результата промиса. В момент встречи в ходе выполнения кода функция ShowNodes останавливается, и во время ожидания данных Javascript может выполнять какой-то другой код.

    В коде выше у коллекции есть метод forEachParallel, который параллельно вызывает асинхронный коллбек для каждого узла. При этом await перед Nodes.forEachParallel дождется всех коллбеков. Внутри реализации — Promise.all:


    
    /**
     * Вызвать функцию для каждого элемента списка не дожидаясь завершения предыдующей иетерации
     * @param items Список
     * @param callbackfn Функция
     * @param [thisArg] Ссылка на объект, который будет передан в качестве this в callbackfn
     */
    export async function forEachParallel<T>(items: IItemArray<T>, callbackfn: (value: T, index: int, items: IItemArray<T>) => Promise<void | any>, thisArg?: any): Promise<void> {
        let xCount = items ? await items.Count : 0;
        if (!xCount)
            return;
        let xActions = new Array<Promise<void | any>>(xCount);
        for (let i = 0; i < xCount; i++) {
            let xItem = items.Item(i);
            xActions[i] = ExecuteCallback(xItem, callbackfn, i, items, thisArg);
        }
        await Promise.all(xActions);
    }
    
    /** Получить асинхронно item и выполнить callbackfn */
    async function ExecuteCallback<T>(item: Promise<T> | T, callbackfn: (value: T, index: int, items: IItemArray<T>) => Promise<void | any>, index: int, items: IItemArray<T>, thisArg?: any): Promise<void> {
        let xItem = await item;
        await callbackfn.call(thisArg, xItem, index, items);
    }
    

    Это синтаксический сахар: подобные методы стоит использовать не только для своих коллекций, но и определить для стандартных массивов Javascript'а.


    Функция ShowNodes выглядит крайне неоптимальной: при запросе очередной сущности мы ее каждый раз дожидаемся. Удобство состоит в том, что подобный код можно писать быстро, поэтому такой подход хорош при быстром прототипировании. В финальном варианте нужно использовать язык запросов для сокращения количества обращений к серверу.


    Язык запросов


    Есть несколько функций, которые используются для "сборки" запроса данных от сервера. Они "говорят" серверу, какие узлы графа данных нужно вернуть в ответе:


    
    /**
     * Функция получает в качестве параметра объект item и возвращает в Promise его же,
     * забирая только необходимые переданные свойства properties
     */
    selectAsync<T extends IItem>(item: T, properties: () => any[]): Promise<T>;
    
    /**
     * Получает коллекцию items, забирая у каждого ее элемента свойства properties
     */
    selectAsyncAll<T extends ICollection>(items: T[], properties: () => any[]): Promise<T[]>;
    
    /** Функция используется в selectAsync для получения вложенных объектов */
    select<T>(item: T, properties: () => any[]): T;
    
    /** Функция используется в selectAsync для получения вложенных объектов */
    selectAll<T>(items: T[], properties: () => any[]): T[];
    

    Теперь посмотрим на применение этих функций для запроса нужных вложенных данных одним обращением на сервер:


    
    async function ShowNodes(parentPoint: IParent): Promise<void> {
        // Запрашиваем одно свойство у объекта типа IParent - коллекцию узлов через selectAsync (возвращает
        // Promise, дожидаемся его).
        let Parent = await selectAsync(parentPoint, parent => [
            // Для каждого узла из коллекции запрашиваем только требуемые нам данные
            selectAll(parent.Nodes, nodes => [node.Name, node.DisplayName]) // [node.Name, node.DisplayName] - список запрашиваемых у очередного узла колекции свойств
        ]);
    
        // Далее синхронно пробегаемся и отрисовываем Parent.Nodes
        ...
    }
    

    Пример чуть более сложного запроса с получением глубоко вложенной информации:


    
        // Можно запросить сразу коллекцию parent.Nodes через selectAsyncAll, чтобы сократить код
        let Parent = await selectAsyncAll(parent.Nodes, nodes => [
            // У каждого узла кэшируем:
            select(node, node => [
                node.Name,
                node.DisplayName,
                selectAll(node.Links, link => [
                    link.Guid,
                    link.DisplayName,
                    select(link.TargetNode, targetNode => [targetNode.Guid])
                ]),
                select(node.Info, info => [info.Component]) // Забираем из интерфейса IInfo просто ссылку на IComponent, данные которого сейчас, например не требуются, но потом будут нужны для другого запроса
            ])
        ]);
    

    Язык запросов помогает избежать лишних запросов на сервер. Но код никогда не бывает идеальным, и в нем непременно будут места нескольких конкурентных запросов и, как следствие, race condition.


    Race Condition и пути решения


    Так как мы подписываемся на события сервера и пишем код с большим количеством асинхронных запросов, то может возникать состояние гонки, когда async функция FuncOne прерывается, ожидая Promise. В это время может придти событие сервера (или от следующего действия пользователя) и, выполнившись конкурентно, изменить модель на клиенте. Тогда FuncOne после резолвинга промиса может обратиться, например, к уже удаленным ресурсам.


    Представим себе такую упрощенную ситуацию: у объекта IParent есть серверный делегат Parent.OnSynchronize.


    /** Синхронизация узлов */
    Parent.OnSynchronize.AddListener(async function(): Promise<void> {
        //  Добавляем новые. Пробегаемся по узлам, удаляем уничтоженные.
    });
    

    Он вызывается при обновлении списка узлов INodes на сервере. Тогда при следующем сценарии возможно состояние гонки:


    1. Вызываем асинхронное удаление узла с клиента, дожидаясь завершения для удаления клиентского объекта
      async function OnClickRemoveNode(node: INode): Promise<void> {
      let removedOnServer: boolean = await Parent.RemoveNode(node);
      // Код удаления клиенского объекта
      if (removedOnServer) ....
      }
    2. Через Parent.OnSynchronize приходит событие обновления списка узлов.
    3. Parent.OnSynchronize обрабатывается и удаляет клиентский объект.
    4. async OnClickRemoveNode() продолжает выполняться после первого await и происходит попытка удалить уже удаленный клиентский объект.

    Можно сделать в OnClickRemoveNode проверку существования клиентского объекта. Это упрощенный пример и в нем подобная проверка — нормальна. Но что если цепочка вызовов сложнее? Поэтому использование подобного подхода после каждого await — плохая практика:


    • Раздутый таким образом код сложен для поддержки и расширения.
    • Код работает не так, как задумывалось: инициируется удаление в OnClickRemoveNode, а фактическое удаление клиентского объекта происходит в другом месте. Нарушения определенной разработчиком последовательности не должно быть, иначе будут регрессионные ошибки.
    • Это недостаточно надежно: если забыть где-то сделать проверку, то будет ошибка. Опасность прежде всего в том, что забытая проверка может не привести к ошибке локально и в тестовом окружении, а у пользователей при большей сетевой задержке — будет возникать.
    • А если контроллер, к которому принадлежат эти обработчики, может быть уничтожен? После каждого await проверять его уничтоженность?

    Возникает еще один вопрос: что, если подобных конкурентных методов много? Представьте, что есть еще:


    • Добавление узла
    • Обновление узла
    • Добавление/удаление связей
    • Метод преобразования нескольких узлов
    • Сложное поведение приложения: изменяем состояние одного узла и сервер запускает обновление зависимых от него узлов.

    Требуется архитектурная реализация, которая в принципе исключает возможность ошибок из-за race condition, параллельных действий пользователя и т.п. Правильное решение для устранения одновременного изменения модели с клиента или сервера — реализация критической секции с очередью вызовов. Здесь будут полезны декораторы Typescript для декларативной пометки таких конкурентных асинхронных функций контроллера.


    Обозначим требования и ключевые особенности таких декораторов:


    1. Внутри должна быть реализована очередь вызовов асинхронных функций. В зависимости от типа декоратора вызов функции может быть поставлен в очередь или отклонен при наличии в ней других вызовов.
    2. У помечаемых функций потребуется контекст выполнения для привязки к очереди. Нужно либо явно создавать очередь, либо делать это автоматически на основе View, к которому принадлежит контроллер.
    3. Необходима информация об уничтоженности экземпляра контроллера (например, свойство IsDestroyed). Чтобы декораторы запрещали выполнение вставших в очередь вызовов после уничтожения контроллера.
    4. Для View контроллера добавим функционал накладывания полупрозрачной маски для исключения действий в момент выполнения очереди и визуального обозначения выполняющейся обработки.
    5. Все декораторы должны завершаться вызовом Promise.done(). В этом методе нужно реализовать handler необработанных исключений. Весьма полезная вещь:
      • исключения, возникшие в Promise-ах, не отлавливаются стандартным обработчиком ошибок (который, например, отображает окошко с текстом и stak trace'ом), поэтому их можно не заметить (если не мониторить консоль все время при разработке). А пользователь их вообще не увидит — это затруднит поддержку. Примечание: есть возможность подписаться для обработки на событие unhandledrejection, но все равно его пока что поддерживают только Chrome и Edge:

        window.addEventListener('unhandledrejection', function(event) { 
        // handling... 
        });
      • так как декораторами мы помечаем самую верхнюю async функцию-обработчик события, то получаем весь stack trace ошибки.

    Теперь приведем примерный список таких декораторов с описанием и затем покажем, как они могут быть применены.


    
    /**
     * Поведение:
     * 1. Помеченная функция будет выполняться эксклюзивно
     * 2. Если в очереди есть другие функции, вызов будет отклонен.
     * 
     * Помечать действия, инициированные пользователем: нажатия на кнопки, обработчики действий горячих клавиш и другой пользовательский ввод
     */
    @Lock
    
    /**
     * Поведение:
     * Помеченная функция ставится в очередь, вызов никогда не будет отклонен.
     * 
     * Помечать функции, которые всегда должны быть выполнены: обработчики серверных событий, обновление и др.
     */
    @LockQueue
    
    /**
     * Аналогичное LockQueue поведение. Отличие - будет выполнен только последний вставший в очередь вызов
     * 
     * Удобно помечать функции, для которых имеет значение только последний вызов. Например, обработчик полной синхронизации.
     */
    @LockBetween
    
    /**
     * Поведение:  
     * Помеченная функция будет выполнена только один раз, спустя заданный интервал. 
     * Удобно для дискретных действий обновления. Например: пользователь вводит в поле текст, и мы раз в 300 мсек. делаем фильтрацию контента на основе введенного текста. 
     */
    @LockDeferred(300)
    
    // Интерфейс, который должны поддерживать объекты, чьи обработчики помечаются этими декораторами:
    
    interface ILockTarget {
    
        /**
         * Функция, которая вернет View, являющийся контекстом очереди. Для тех случаев, когда есть несколько конкурентных обработчиков у разных контроллеров, но привязанных к одному отображению, которое и является контекстом их общей очереди
         */
        GetControllerView?(): IView;
        /** Равен true после уничтожения экземпляра контроллера  */
        IsDestroyed: boolean;
    
    }
    

    Описания довольно абстрактные, но как только вы увидите пример использования с пояснениями, все станет понятнее:


    
    class GraphController implements ILockTarget {
    
        /** Отображение, которое будет маскировано при выполнении очереди. Оно же будет контекстом очереди */
        private View: IView;
    
        public GetControllerView(): IView {
            return this.View;
        }
    
        /** Обработка удаления узла по клику пользователя. */
        @Lock
        private async OnClickRemoveNode(): Promise<void> {
            ...
        }
    
        /** Удаление связи по клику мыши. */
        @Lock
        private async OnClickRemoveLink(): Promise<void> {
            ...
        }
    
        /** Добавление пользователем нового узла */
        @Lock
        private async OnClickAddNewNode(): Promise<void> {
            ...
        }
    
        /** Обработчик события сервера "Обновление узла" */
        @LockQueue
        private async OnServerUpdateNode(): Promise<void> {
            ...
        }
    
        /** Обработчик события сервера "Добавление связи" */
        @LockQueue
        private async OnServerAddLink(): Promise<void> {
            ...
        }
    
        /** Обработчик события сервера "Добавление узла" */
        @LockQueue
        private async OnServerAddNode(): Promise<void> {
            ...
        }
    
        /** Обработчик события сервера - удаление узла */
        @LockQueue
        private async OnServerRemoveNode(): Promise<void> {
            ...
        }
    
        /** Обработчик события сервера - полная синхронизация всех узлов и связей */
        @LockBetween
        private async OnServerSynchronize(): Promise<void> {
            ...
        }
    
        /** Обработчик события сервера - обновление статуса узла (выполнен/warning/error/...) */
        @LockQueue
        private async OnServerUpdateNodeStatus(): Promise<void> {
            ...
        }
    
        /** Фильтрация  данных с обращением на сервер */
        @LockDeferred(300)
        private async OnSearchFieldChange(): Promise<void> {
            ...
        }
    
    }
    

    Теперь разберем пару типичных сценариев возможных ошибок и их устранения декораторами:


    1. Пользователь инициирует какое-либо действие: OnClickRemoveNode, OnClickRemoveLink. Для правильной обработки необходимо, чтобы в очереди не было других выполняющихся обработчиков (будь то клиентских или серверных). Иначе возможна, например, такая ошибка:
      • Модель на клиенте еще обновляется до актуального серверного состояния
      • Инициируем удаление объекта до завершения обновления (в очереди есть выполняющийся обработчик OnServerSynchronize). Но этого объекта на самом деле уже нет — просто полная синхронизация еще не завершилась и он еще отображается на клиенте.
        Поэтому все действия, инициированные пользователем, декоратор Lock должен отклонить при наличии в очереди других обработчиков с тем же контекстом очереди. С учетом того, что сервер асинхронный, это особенно важно. Да, Websocket шлет запросы последовательно, но если клиент нарушит последовательность, получим ошибку на сервере.
    2. Инициируем добавление узла: OnClickAddNewNode. От сервера приходят события OnServerSynchronize, OnServerAddNode.
      • OnClickAddNewNode занял очередь (если бы в ней что-то было, декоратор Lock этого метода отклонил бы вызов)
      • OnServerSynchronize, OnServerAddNode встали в очередь, выполнились последовательно после OnClickAddNewNode, не конкурируя с ним.
    3. В очереди есть вызовы OnServerSynchronize и OnServerUpdateNode. Допустим, что в процессе выполнения первого пользователь закрывает GraphController. Тогда второй вызов OnServerUpdateNode автоматически не должен быть выполнен, чтобы не совершить действия на уничтоженном контроллере, что гарантированно приведет к ошибке. Для этого в интерфейсе ILockTarget есть IsDestroyed — декоратор проверяет флаг, не выполняя следующий обработчик из очереди.
      Profit: не нужно после каждого await писать if (!this.IsDestroyed()).
    4. Запускается изменение нескольких узлов. От сервера приходят события OnServerSynchronize, OnServerUpdateNode. Конкурентное их выполнение приведет к невоспроизводимым ошибкам. Но т.к. они помечены декораторами постановки в очередь LockQueue и LockBetween, то выполнятся последовательно.
    5. Представьте, что узлы могут иметь внутри себя вложенные графы узлов. Есть GraphController #1, а внутри одного из его узлов — вложенный граф GraphController #2. Причем, GraphController-ы не уничтожаются при закрытии, а скрываются (так быстрее — пользователю не нужно каждый раз при переключении ждать загрузки), т.е. продолжают получать все события обновления. Мы их:
      • Запоминаем
      • Не выполняем сразу
      • При отображении скрытого GraphController #2, у которого пришли эти события, выполняем их в очереди.
    6. OnSearchFieldChange вызывается каждый раз, когда пользователь вводит букву в поле фильтрации. Эта функция делает запрос на сервер и показывает какие-то отфильтрованные данные. Декоратор @LockDeferred(300) откладывает ее выполнение на 300 мс: сколько бы раз за это время не вводи букв, запрос будет сделан не чаще, чем раз в 300 мс. Частый кейс, но реализация удобнее с таким декларативным подходом. Дополнительные бонусы:
      • Если откладываем дольше, например на 500 мс, то пользователь может успеть закрыть контроллер с этой фильтрацией. Но ошибки не будет — декоратор в этом случае просто не выполнит OnSearchFieldChange, как и другие приведенные выше.
      • На время откладывания и выполнения OnSearchFieldChange очередь будет занята — другие действия с объектами не приведут к ошибкам, аналогично описанным выше.

    Что нужно знать при использовании декораторов


    1. Возможен Deadlock: если из асинхронного обработчика Handler1, который выполняется в очереди, вызывать с await другой обработчик Handler2, помеченный LockQueue, получим бесконечное ожидание Handler2Handler1 никогда не завершит выполнение.
    2. Есть ситуации, в которых на одном View нельзя выполнять все в одной очереди. Пример: методы изменения модели и обработчики изменения статуса должны быть в разных очередях, если вторые происходят слишком часто — иначе они займут почти все время и приложение будет постоянно закрываться маской и создавать ощущение тормозов.

    Профилирование запросов к серверу


    У нас есть декораторы, которые помечают все асинхронные методы, причем начиная с самых верхних синхронных обработчиков событий. Что ж:


    • Добавим сохранение выполнения каждого метода в хэш-таблице <Class>.<Method> => <Time> (в отладочной версии).
    • Расчитываем среднее и общее времена выполнения методов.
    • Получаем удобное профилирование скорости всех асинхронных запросов приложения.

    Десерт


    Отлично, у нас есть декораторы, которые не будут давать выполняться обработчикам из очереди на удаленных контроллерах, к которым принадлежат обработчики. Но что делать с текущим выполняемым асинхронным методом? Что если во время его выполнения контроллер будет уничтожен? Пример кода:


    
    class GraphController implements ILockTarget {
    
        private View: IView;
    
        public GetControllerView(): IView {
            return this.View;
        }
    
        /** Запуск обработки очень сложных вычислений. */
        @Lock
        private async RunBigDataCalculations(): Promise<void> {
            await Start();
            await UpdateSmth();
            await End();
            await CleanUp();
        }
    
        /** Изменение состояния узла. */
        @LockQueue
        private async OnChangeNodeState(node: INode): Promise<void> {
            await GetNodeData(node);
            await UpdateNode(node);
        }
    
    }
    

    Вот возможная ситуация:


    1. Запускаем RunBigDataCalculations.
    2. Выполняем await Start();
    3. Пользователь закрывает контроллер/переходит на другой(с закрытием текущего)
    4. Закончили выполнять await Start();, пытаемся выполнить await UpdateSmth(); на уничтоженном контроллере и получаем ошибку.

    Или:


    1. Запускаем RunBigDataCalculations.
    2. Приходит серверное событие OnChangeNodeState, которое выполняется в другой очереди и не блокирует интерфейс (т.к. частое событие).
    3. Начинаем выполнять await GetNodeData(node);
    4. Пользователь закрывает контроллер/переходит на другой(с закрытием текущего)
    5. Закончили выполнять await GetNodeData(node);, пытаемся выполнить await UpdateNode(node); на уничтоженном контроллере и получаем ошибку.

    И с этим тоже надо как-то жить. Нам потребуется:


    • Поддержка отложенного уничтожения:

    /**
     * Интерфейс контроллера с незавершенной очередью асинхронных вызовов, который поддерживает уничтожение своих ресурсов
     */
    export interface IQueuedDisposableLockTarget extends ILockTarget {
        /** Объект находится в состоянии уничтожения. Lock декораторы также не должны позволять выполняться методам контроллера при IsDisposing() === true */
        IsDisposing(): boolean;
        SetDisposing(): void;
    }

    • Функция отложенного уничтожения при наличии в очереди обработчика:

    
    function QueuedDispose(controller: IQueuedDisposableLockTarget): void {
        // Получаем очередь из контекста контроллера
        let xQueue = GetQueue(controller);
        // 1. Смотрим, есть ли в ней что-то, если нет - уничтожаем сразу
        if (xQueue.Empty) {
            controller.Dispose();
            return;
        }
        // 2. Если есть, то пометим контроллер флагом "будет уничтожен", чтобы не выполнять асинхронные методы, поставленные в очередь.  
        controller.SetDisposing();
        // 3. И в finally уничтожим его
        xQueue.finally(() => {
            debug.assert(!IsDisposed(controller), "Кто-то уничтожил контроллер до его отложенного уничтожения, возможна ошибка");
            controller.Dispose();
        });
    }
    

    Таким образом, помеченные функции выполнятся до конца и не приведут к ошибкам. Но при использовании QueuedDispose обязательно нужно иметь в виду:


    • Вызывающий уничтожение код не должен требовать немедленного уничтожения контроллера. Либо он должен работать с учетом этого.
    • Перед вызовом QueuedDispose вы скорее всего скроете controller. В таком случае библиотека должна работать без ошибок — у ExtJS с этим иногда есть проблемы.

    Исходники


    К сожалению, прямо сейчас исходный код декораторов критической секции не могу предоставить, т.к. он проприетарный и в нем много лишнего нашего кода. Возможно, вам вообще не нужен этот велосипед? Но вдруг я ошибаюсь, добавлю голосовалку.


    Если сотня другая наберется, то подчищу код и выложу тут:


    Ваш покорный слуга в vk.com
    Ваш покорный слуга в Telegram

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Выложить исходный код декораторов критической секции?

    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 3

      +2
      await Nodes.forEachParallel(async function(node): Promise<void> {
              await RenderNode(node); // Вызываем асинхронную функцию получения данных узла и его отрисовки
       });
      
      Я видимо переутомился к пятнице, но почему не просто
       await Promise.all(Nodes.map(RenderNode));
      
        +1

        TLDR: чтобы наши асинхронные функции на клиенте не конфликтовали, мы рассовали их выполнение по очередям в зависимости от вьюхи.


        Вроде, остальная часть поста — вода в декораторах.

          0
          От спасибо мил человек! Сразу стало понятнее о чём статья

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое