Skip to content

Comments

service: pub-sub#141

Open
glebfomin28 wants to merge 10 commits intomasterfrom
feature/pub-sub
Open

service: pub-sub#141
glebfomin28 wants to merge 10 commits intomasterfrom
feature/pub-sub

Conversation

@glebfomin28
Copy link
Contributor

Паттерн Publish/Subscribe (Observer).

Реализовал через не строгий класс-синглтон, чтобы можно было создавать несколько экземпляров класса с уникальным ключом.

Добавил возможность типизировать инстанс класса.

@glebfomin28 glebfomin28 self-assigned this Feb 6, 2025
@tolms
Copy link
Contributor

tolms commented Feb 6, 2025

Можно пример использования publishAsync? В каких случаях целесообразно его запускать?

@tolms
Copy link
Contributor

tolms commented Feb 6, 2025

Вопросы на подумать:

  1. reset очищает все каналы. Нужно ли нам очищать один канал, но оставлять другие?
  2. нужно ли отслеживать, кто посылает сообщение (publish)? Если да, то как это лучше сделать?
  3. нужна ли одноразовая подписка subscribeOnce?
  4. нужна ли подписка для прослушивания всех событий (может быть для логирования)? Что-то наподобие Pubsub.on('*', (channelName, params) => { console.log(channelName, params) }

@tolms
Copy link
Contributor

tolms commented Feb 6, 2025

Ссылка на задачу на доске. Глеб, забирай задачу на себя
https://github.com/orgs/Byndyusoft/projects/9/views/1?pane=issue&itemId=78586865

@glebfomin28
Copy link
Contributor Author

glebfomin28 commented Feb 6, 2025

@tolms

Можно пример использования publishAsync? В каких случаях целесообразно его запускать?

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

Пример


type TPubSubInstance = {
    message: (msg: string) => void | Promise<void>;
};

const pubSubInstance = PubSub.getInstance<TPubSubInstance>();

const subscriberMessages = (listener: (msg: string) => void | Promise<void>) => {
    pubSubInstance.subscribe('message', listener);

    return () => pubSubInstance.unsubscribe('message', listener);
};

let count = 1;

const addCountToMessage = (msg: string) => {
    return new Promise<string>(res => {
        setTimeout(() => {
            res(msg + count++);
        }, 2000);
    });
};

const DemoComponent = () => {
    const [messages, setMessages] = useState<string[]>([]);

    useEffect(() => {
        const callbackAsync = async (text: string) => {
            const newMessage = await addCountToMessage(text);
            setMessages(prev => [newMessage, ...prev]);
        };

        return subscriberMessages(callbackAsync);
    }, []);

    const onSendMessageSync = () => {
        pubSubInstance.publish('message', '(Sync) Message');
        // Код ниже отработает не дожидаясь
        setMessages(prev => ['Test message', ...prev]);
        console.log('Sync');
    };

    const onSendMessageSyncAsync = async () => {
        await pubSubInstance.publishAsync('message', '(Async) Message');
        // Код ниже будет ждать пока не отработает `callbackAsync`
        setMessages(prev => ['Test message', ...prev]);
        console.log('Async');
    };

    return (
        <div>
            <button onClick={onSendMessageSync}>Send message (Sync)</button>
            <button onClick={onSendMessageSyncAsync}>Send message (Async)</button>
            <div>
                <h2>Сообщения:</h2>
                {messages.map((item, index) => (
                    <p key={index}>{item}</p>
                ))}
            </div>
        </div>
    );
};

export default DemoComponent;

@tolms
Copy link
Contributor

tolms commented Feb 6, 2025

Да, понимаю, я смотрел код.
Поначалу подумал, что нет реальных примеров использования. Но потом придумал случай возможного применения.
Допустим приложение посылает всем подписавшимся модулям сообщение "Я перезагружаюсь". И модули начинают что-то делать (например, сохранять свои данные). Приложение дождалось завершения всех операций подписантов и перезагружается.
На ум пришел еще возможный случай: модуль X, который запускает событие, может зависеть от данных Y-модуля. Тогда X-модуль ждет, когда подписанный на событие Y-модуль обновит свои данные. Тогда X-модуль подтянет обновленные данные из Y-модуля. Но тогда подобного рода действия нужно делать через другие инструменты. PubSub здесь не подходит.

В общем, опция интересная, и здорово, что она есть. Кому-то она будет полезна.
Спасибо Глеб!

/**
* Getting an instance of a class.
*/
static getInstance<ChannelsRecord extends TDefaultChannels>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Может не стоит делать сервис синглтоном? Пусть в месте, где этот сервис используется, отдельно решается быть ему синглтоном или нет.

Copy link
Contributor Author

@glebfomin28 glebfomin28 Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно убрать instances и getInstance.

Я правильно понял, что использоваться должно вот так:

const pubSub1 = new PubSub<{ message: () => void }>()
const pubSub2 = new PubSub<{ message: () => void }>()


pubSub1.subscribe('message', () => {})
pubSub1.publish('message')

pubSub2.subscribe('message', () => {})
pubSub2.publish('message')

ChannelsRecord[ChannelKey]
>[0];

export type ChannelsRecordAdapter<T> = { [K in keyof T]: T[K] };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Зачем адаптер этот нужен? Ты создаешь пакет, у него есть контракт и не надо помогать разработчику выполнять этот контракт. Это допустимо только в случае, когда ты точно знаешь, что есть какой-то существующий популярный контракт и ты его уже адаптируешь.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Чтобы типизировать инстанс сейчас нужно использовать type, а не interface. Иначе TS ругается, что интерфейс не имеет индексной сигнатуры.
Написал об этом в ридми. Добавил ChannelsRecordAdapter на случай если уж необходимо использовать interface для типизации инстанса.

@@ -0,0 +1,14 @@
export type TDefaultChannels = Record<string, (data?: any) => void>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any заменить на unknown не получилось?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Пытался, пока не придумал как заменить. Если использовать unknown, то не получится типизировать каналы через дженерик.
Будет ошибка
TS2344: Type 'TPubSubInstance' does not satisfy the constraint 'TDefaultChannels'.   Property 'message' is incompatible with index signature.     Type '(msg: string) => void' is not assignable to type '(data?: unknown) => void'.       Types of parameters 'msg' and 'data' are incompatible.         Type 'unknown' is not assignable to type 'string'.

const channelSet = this.channels.get(channel);
if (channelSet) {
for (const callback of channelSet) {
if (callback) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А на promise не надо ли проверить?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно обернуть в промис await Promise.resolve(callback(data));

@ancientbag
Copy link
Contributor

ancientbag commented Feb 12, 2025

У класса есть instances, но управлять я им не могу. Если я хочу создать новый instance, то это делается через getInstance, что, как мне кажется, не очень очевидно. Я бы либо убрал instances сделав 1 класс = 1 инстанс, либо добавил ручек, чтобы можно было управлять инстансами (например удалить инстанс если он мне больше не нужен). Но мне кажется 1 класс = 1 инстанс будет легче управляемым.
Еще я не понял для чего вообще эти инстансы нужны, если у класса каналы (если я правильно понял) общедоступные между всеми инстансами.

@ancientbag
Copy link
Contributor

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

@ancientbag
Copy link
Contributor

@sadcitizen докатит ПР

@glebfomin28
Copy link
Contributor Author

Убрал синглтон instances, и getInstance().

Поправил типы: убрал ChannelsRecordAdapter, теперь можно типизировать без ограничений.

Добавил новые методы:

  • unsubscribeAll
  • subscribeOnce
  • allSubscribes

Create a type that defines the channels and their corresponding callback signatures.

```ts
type ChannelsType = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Согласен, с глобальным экземпляром есть такая проблема. Тут можно использовать pub-sub только внутри модуля, если это возможно. Или не типизировать глобальный экземпляр и делать адаптер в каждом модуле.

@pixel-fixer
Copy link
Contributor

Надо посмотреть @sadcitizen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants