npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@avv/ucd-ibus

v1.1.0

Published

Post messaging processor.

Readme

@avv/ucd-ibus

Установка

npm i @avv/ucd-ibus

Описание postMessage обработчика (v1.0.0)

Модуль служит задаче обеспечения коммуникации через postMessage web API. Модуль реализует некоторый протокол обмена сообщениями, поэтому предполагается, что он будет установлен на обеих сторонах коммуникации. Однако, это не обязательно, если следовать описанному ниже протоколу.

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

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

Для начала работы, необходимо инциализировать message обработку. Для этого необходимо импортировать initialize функцию из пакета и вызвать её с коллекцией функций и опциональным объектом дополнительных параметров:

import { initialize } from 'ucd_post-messaging'

const postMessageActions = {
    someMethod: () => {},
    groupMethods: {
        someMethod2: (payload, sender, additionalParams) => {},
        someMethod3: async (payload, sender, additionalParams) => {}
    }
}
const additionalParams = {
    // некоторые дополнительные данные
}

const {
    open,
    close,
    allow,
    forbid,
    setPermissions,
    getSender
} = initialize(postMessageActions, additionalParams)

Это необходимо сделать при запуске всего приложения в самом начале. Возможно, до монитирования в DOM дерево.

Коллекция функций postMessageActions может быть древовидной, таким образом функции можно группировать по смыслу. Функция принимает:

  • payload - данные из объекта сообщения;
  • sender - имя отправителя;
  • additionalParams - объект дополнительных параметров, переданных при инициализациии.

Функция может быть синхронной и асинхронной. Её результат ложится в payload объект ответного сообщения.

На этапе инициализации приложения, в корневом компоненте, необходимо вызвать метод open, а при размонтировании - метод close:

const RootAppComponent = () => {
    // некоторая логика...
    useEffect(() => {
        // ...
        open();
        return () => {
            // ...
            close();
            // ...
        };
    }, []);
    // дальнейшая логика
}

Методы open и close создают и навешивают/уничтожают соответственно callback обработчик message события на объекте Window.

Для того, чтобы обмен сообщениями стал возможен, необходимо на обеих сторонах вызвать allow метод с параметрами соединения. Для закрытия соединения, можно вызвать forbid метод с именем соединения.

allow('Pparent', 'http://dashboard.host.com/', { 'someMethod': false, 'anotherMethod': true }, null, window.parent);
// ...
forbid('Parent');

Пример компонента, создающего iframe:

const Iframe = ({
    name,
    origin,
    url,
    permissions = true,
    height = 100,
    onSourceChange = null,
    source = null
}) => {
    useEffect(() => {
        allow(name, origin, permissions, onSourceChange, source);
        return () => {
            forbid(name);
        }
    }, []);

    // render <iframe .../> элемента...
};

Интерфейс метода allow:

  • name - имя соединения;
  • origin - protocol://host:port соединения (желательно знать заранее);
  • permissions - настройки доступа к методам текущего окна (подробно рассмотрено ниже);
  • onSourceChange - callback, с помощью которого можно подписаться на событие смены ссылки на объект Window соседнего фрейма.
  • source - ссылка на объект Window соседнего фрейма (может быть равным null, в таком случае будет проинициализирован при обработке @INIT сообщении).

По объекту permissions стоит заметить, что если нет необходимости регулировать доступ к функциям, и нужно разрешить полный доступ - то можно просто передать true значение вместо объекта. Принцип объекта permissions описан ниже.

Здесь есть рекомендация: инициировать коммуникацию удобнее всего из дочернего фрейма, так как в нём точно можно поймать момент, когда он готов к коммуникации. Поэтому обычно именно дочерний фрейм инициирует коммуникацию и первым запрашивает нужные данные. Однако, если нужно, чтобы первым данные отправил родительский фрейм - это можно осуществить через onSourceChange callback. Он будет вызван, как только прилетит @INIT сообщение и будет получена ссылка на объект Window дочернего фрейма. В этот момент можно начинать отправлять сообщения в дочерний фрейм.

Чтобы отправить сообщение, нужно получить callback sender путём вызова getSender метода:

const sender = getSender('Parent');
// ...
const answer = await sender(new ActionMessage('someMethod', { someParam: 'someValue' }))

Callback sender возвращает Promise объект. Он резолвится с ответом, и реджектится с ошибкой. Конструктор объекта сообщения первым параметром принимает имя метода, вторым - объект данных в произвольном формате. Второй параметр опциональный.

Если нам не нужно получать ответа на наш запрос, можно в sender вторым параметром передать false:

sender(new ActionMessage('groupMethods/someMethod2'), false); // ответ не ожидается к обработке.

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

Протокол и общая логика работы

Структура сообщения

Общем случае, структура сообщений реализует интерфейс:

interface IMessage<T> {
    messageId: number;
    provider: string;
    receiver: string;
    sender?: string;
    message: IMessageBody<T>;
}

Поля имеют следующие значения:

  • messageId - идентификатор сообщения, обязательное поле; идентификатор сообщение генерируется автоматически внутри модуля при отправке сообщения;
  • provider - строковый идентификатор модуля, обязательное поле; поле нужно для того, чтобы была возможность отфильтровывать сообщения от других подсистем, отправляющих и получающих сообщения через postMessage API;
  • receiver - имя получателя, имя получатея назначается отправителем; поле обрабатывается автоматически;
  • sender - имя отправителя, поля может не быть в init сообщении, имя назначается получателем; поле обрабатывается автоматически;
  • message - тело сообщения, его интерфейс разбирается ниже.
interface IMessageBody<T> {
    type: MESSAGE_TYPE,
    action?: string,
    status?: RESPONSE_STATUS,
    payload?: IMessagePayload<T>
}

type IMessagePayload<T> = T | IErrorPayload | undefined

interface IErrorPayload {
    code: ERROR_CODES,
    error: string
}

enum MESSAGE_TYPE {
    INIT = '@INIT',
    REQUEST = '@REQUEST',
    RESPONSE = '@RESPONSE',
    END = '@END'
}

enum RESPONSE_STATUS {
    SUCCESS = 'SUCCESS',
    ERROR = 'ERROR'
}

Поля тела сообщения имеют следующие значения:

  • type - тип сообщения, обязательное поле, поле может содержать одно из четырёх значений из MESSAGE_TYPE списка:
    • @INIT - начальное инициирующее сообщение, обрабатывается полностью автоматически модулем;
    • @REQUEST - запрос, данных или каких-либо действий;
    • @RESPONSE - ответ, возвращает результат работы метода, к которому обращались;
    • @END - завершающее сообщение, закрывает соединение;
  • action - поле обязательно для сообщения-запроса (type = @REQUEST), содержит имя метода, который нужно вызвать и вернуть его результат;
  • status - поле обязательно для сообщения-ответа (type = @RESPONSE), может содержать одно из двух значений из RESPONSE_TYPE:
    • SUCCESS - запрос успешно обработан, и возможно содержит ответ в payload;
    • ERROR - обработка запроса завершилась с ошибкой, в payload содержится объект ошибки;
  • payload - полезная нагрузка сообщения; может передаваться в запросе и в ответном сообщении; содержит данные при успешной обработке запроса и при возникновении ошибок (интерфейс IErrorPayload выше); може быть пустым; в запросе именно объект payload передаётся на вход вызываемого метода в системе-получателе; имеет произвольную структуру; логика обработки содержимого этого поля определяется разработчиком.

Установление соединения

Для того, чтобы два экземпляра модуля могли установить друг с другом связь, необходимо эту коммуникацию разрешить. Разрешение на коммуникацию с выдаётся путём вызова allow метода с передачей:

  • имени "собеседника",
  • origin "собеседника",
  • настроек прав доступа "собеседника",
  • обработчика смены ссылки на объект Window "собеседника" (если необходимо),
  • ссылки на объект Window "собеседника" (если есть).
// в дочернем фрейме разрешена коммуникация с родительским
allow('Parent', '*', true, null, window.parent)
// в родительском фрейме разрешена коммуникация с дочерним
allow('Porter', 'https://porter.com/', true, null, null)

Интерфейс метода allow будет рассмотрен подробнее ниже.

Имя "собеседника" необходимо передать в обязательном порядке, и оно должно быть уникальным. Крайне желательно, на момент разрешения, точно знать origin "собеседника" - это гарантирует безопасность коммуникации. Однако, если origin неизвестен, то можно передать * - при обработке ответа на @INIT сообщение будет проставлен реальный origin отправителя ответа.

Для отправки @INIT сообщения, один из участников коммуникации должен иметь ссылку на объект Window получателя. Чаще всего, это дочерний iframe. Соответственно, именно дочернему iframe удобнее всего первым отправить @INIT сообщение. Родительский фрейм может изначально не иметь ссылки на объект Window дочернего, но он получит эту ссылку при обработке @INIT запроса и сохранит у себя.

Начальное сообщение:

{
    "messageId": 1,
    "sender": null,
    "receiver": "Parent",
    "provider": "POST-MESSAGING-PACKAGE-V1",
    "message": {
        "type": "@INIT"
    }
}

Ответ:

{
    "messageId": 1,
    "sender": "Parent",
    "receiver": "Porter",
    "provider": "POST-MESSAGING-PACKAGE-V1",
    "message": {
        "type": "@RESPONSE",
        "payload": {
          "inited": true
        }
    }
}

С момента получения ответа на @INIT сообщение, возможна полноценная коммуникация в обе стороны.

Обмен сообщениями

Запрос-ответ связываются друг с другом внутри модуля через идентификатор запроса: ответное сообщение имеет тот же идентификатор.

Сообщение-запрос имеет тип type = @REQUEST, содержит имя вызываемого метода в поле action = method/name, опционально может содержать payload с произвольным содержимым. Содержимо поля payload будет передано в функцию method/name в принимающей системе.

Сообщение-ответ имеет тип type = @RESPONSE, содержит статус ответа в поле status, и в поле payload (так же опциональном) произвольные данные. С этими данными резолвится промис, возвращаемый при отправке сообщения. Состояние промиса можно обработать произвольным образом. Содержимое payload рвно тому, что вернется в результате вызова method/name метода.

Пример сообщения-запроса:

{
    "messageId": 2,
    "sender": "Porter",
    "receiver": "Parent",
    "provider": "POST-MESSAGING-PACKAGE-V1",
    "message": {
        "type": "@REQUEST",
        "action": "dataRequest/getClient"
    }
}

Пример сообщения-ответа:

{
    "messageId": 2,
    "sender": "Parent",
    "receiver": "Porter",
    "provider": "POST-MESSAGING-PACKAGE-V1",
    "message": {
        "type": "@RESPONSE",
        "status": "SUCCESS",
        "payload": {
            "base": {
                "guid": "sdglk234234j2l34jk234l2j"
            }
        }
    }
}

Для отправки сообщения, необходимо вызвать getSender с именем получателя (зарегистрированном при вызове allow) получить ссылку на callback, и вызвать этот его с экземпляром объекта сообщения:

const sender = getSender('Parent');
sender(new ActionMessage('dataRequest/getSomeData', { param: 'value' }))
    .then((answer) => {
        // логика обработки ответа
    });

// или 

const answer = await sender(new ActionMessage('dataRequest/getSomeData', { param: 'value' }));

Экземпляр объекта сообщения первым параметром получает имя вызываемого метода, вторым - данные для этого метода в произвольном формате. Второй параметр не обязательный.

Callback sender возвращает Promise объект.

Закрытие коммуникации

Коммуникация между двумя окнами закрывается при отправке @END сообщения. При этом на обеих сторонах уничтожается информация друг о друге. Ответ на это сообщение не отправляется.

Пример завершающего сообщения:

{
    "messageId": 1224,
    "sender": "Porter",
    "receiver": "Parent",
    "provider": "POST-MESSAGING-PACKAGE-V1",
    "message": {
        "type": "@END"
    }
}

Отправить завершающее сообщение можно через sender callback. Вызов forbid метода не отправляет никаких сообщений, но закрывает коммуникацию с другим фреймом молча.

const sender = getSender('Parent');
sender(new EndMessage());

// или 

forbid('Parent');

Callback sender, как обычно вернет Promise объект. Он зарезолвится как только сообщение физически будет отправлено, а данные соединения будут уничтожены. Происходит это синхронно, поэтому обычно нет надобности подписываться на событие закрытия соединения. Метод forbid ничего не возвращает.

API модуля

Функцияinitialize

Модуль экспортирует одну функцию initialize. Она при вызове принимает объект коллекции функций и объект произвольных дополнительных параметров (опциональный), который впоследствии будет передан в вызываемые при обработке запросов методы из коллекции функций.

Вызов функции инициализирует модуль, но ещё не создаёт обработчик message события.

const {
    open,
    close,
    allow,
    forbid,
    setPermissions,
    getSender
} = initialize(postMessageActions, additionalParams)

Возвращает объект со вспомогательными методами.

Метод open

Создает обработчик события message и инициализирует все внутренние механизмы модуля. Повторный вызов метода от одного экземпляра initialize проблем не создаст: повторной регистрации обработчика message и инициализации модуля - не случится. Никаких параметров не принимает. Ничего не возвращает.

Метод close

Уничтожает обработчик message события для данного экземпляра модуля и останавливает все внутренние процессы в модуле, уничтожает все данные. Никаких параметров не принимает и ничего не возвращает.

Метод allow

Метод разрешающий коммуникацию с фреймом с указанными параметрами. Принимает параметры (уже было описано выше):

  • name - имя соединения;
  • origin - protocol://host:port соединения;
  • permissions - настройки доступа к методам текущего окна;
  • onSourceChange - callback, с помощью которого можно подписаться на событие смены ссылки на объект Window соседнего фрейма.
  • source - ссылка на объект Window соседнего фрейма.

Метод ничего не возвращает.

Важно отметить, что желательно чтобы origin и source были известны на момент вызова. Однако не всегда это доступно или удобно. Поэтому здесь допустимы следующие ситуации:

  • origin неизвестен, source известен

    В таком случае в качестве origin можно передать * - то есть он может быть любым. Фрейм, который находится в таком положении должен первым отправить сообщение @INIT. В ответном сообщении будет origin отправителя, и именно он будет в итоге записан вместо *.

  • origin известен, source неизвестен

    Фрейм, находящийся в таком состоянии вынужден ждать @INIT сообщения от фрейма с указанным origin. Именно указанный origin становится идентификатором, позволяющий точно понять к какому соединению относится данный запрос. Внутри модуля из запроса извлекается source и записывается в параметры данного соединения.

  • оба параметры известны

    Формально, они могут друг-другу одновременно отправить @INIT сообщение. В таком случае финальным состоянием соединения будет состояние по итогам обработки второго @INIT сообщения. Но лучше принять решение, о том какая сторона инициирует коммуникацию первой.

Ситуация, когда оба параметра неизвестны - недопустима. В таком случае, объективно, нет возможности однозначно и точно понять источник запроса @INIT и корректно проинициализировать параметры соединения. Не говоря уже о том, что неизвестный origin создаёт потенциальные проблемы безопасности.

Метод forbid

Закрывает соединение с указанным именем, принимает только строку. Имя должно совпадать с именем переданным в allow первым параметром. Ничего не возвращает. Соедиение будет закрыто "молча", сообщение @END отправлено не будет.

Метод setPermissions

Метод принимает имя соединения (или origin) и объект (или boolean значение) permissions. Принцип работы настроек доступа описан ниже. Метод ничего не возвращает.

Метод getSender

Метод принимает имя или origin соединения и возвращает функцию отправки сообщения.

Возвращённый callback принимает экземляр объекта сообщения, созданного конструкторами ActionMessage, InitMessage, EndMessage. Вторым параметром принимает boolean значение, по-умолчанию равно true: ожидается ли ответ на запрос. Если ожидается ответ, то будет возвращён Promise объект, который резолвится с ответом на запрос. Если ответ не ожидается, что callback ничего не вернёт.

Конструкторы сообщений

Модуль экспортирует три конструктора сообщений и один конструктор параметров ошибки в payload ответного сообщения:

  • InitMessage
  • EndMessage
  • ActionMessage
  • ErrorPayload

Для передачи @INIT или @END сообщений, достаточно в sender callback передать соответствующие экземпляры:

sender(new InitMessage());
sender(new EndMessage());

Конструктор ActionMessage принимает два параметра: имя вызываемого метода и payload сообщения. Второй параметр опциональный. Объект payload может быть произвольным (может быть и скалярным значением).

Политика безопасности

Параметр permissions в методе allow может принимать либо булево значение, либо передаваться в виде объекта (плоский HashMap). Во втором случае ключами являются имена методов, значениями статус доступа true|false.

Если передаётся permissions = true, это значает что данному источнику сообщений предоставляется полный доступ ко всем имеющимся методам. Передача permissions = false рвносильно полному запрету всякого доступа. Может использоваться для временной полной блокировки.

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

const functions = {
    client: {
        getClient: async () => {},
        setClientName: async (payload, sender, additionalParams) => {},
        getMoney: async (payload, sender, additionalParams) => {}
    },
    settings: {
        getSettings: () => {},
        setParam: (payload, sender, additionalParams) => {}
    }
}

В какой-то момент мы вынуждены работать с фреймом, которому доступ к данным клиента предоставлять нельзя, но можно выполнять разные операции с настройками системы. Мы можем передать в permissions следующие настройки:

const permissions = {
    'client': false,
    'settings': true
}

Или, фрейм может обращаться ко всем методам, кроме getMoney:

const permissions = {
    'client': true,
    'client/getMoney': false,
    'settings': true
}

Или, фрейм может обращаться ко всем методам настроек системы, и может запрашивать данные клиента, но ему запрещено модифицировать данные клиента и выполнять манипуляции с деньгами:

const permissions = {
    'client/getClient': true,
    'settings': true
}

Объект настроек доступа работает по следующим правилам:

  • если permissions = true разрешается доступ ко всему;
  • если permissions = false ко всему доступ запрещён;
  • если передан объект, следует принципу "все запрещено, если не указано иное":
    • если в объекте не указано имя конкретного метода или группы методов - доступ к методу и группе методов запрещён (если объект пустой - доступ ко всему запрещён);
    • если в объекте указано имя конкретного метода или группы методов, и в нём содержится true, то доступ к этому методу или группе методов разрешен, ко всему остальному доступ запрещён;
    • если в объекте указано имя конкретного метода или группы методов, и в нём содержится false, то доступ к этому методу или группе методов запрещён, даже если есть имя надгруппы и там содержится значение true;

Дальнейшие доработки и развитие модуля

  • адаптировать модуль к postMessage взаимодействию с Worker объектом (один на один);
  • предоставить возможность разработчку самому создавать обработчик входящих сообщений (передавать генератор в стиле саги) - может быть это не нужно;
  • ввиду схожести механизма WebSocket коммуникации, возможно есть смысл его адаптировать и под такую возможность.