С момента появления 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

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


  1. DarthVictor
    27.09.2019 11:49

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


  1. jehy
    27.09.2019 11:55

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


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


    1. inoyakaigor
      27.09.2019 13:41

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