Skip to content

Commit

Permalink
Merge pull request #255 from core-ds/feat/client-event-bus
Browse files Browse the repository at this point in the history
feat(*): add client-event-bus package
  • Loading branch information
denis0ff authored Sep 18, 2024
2 parents 9772b62 + 4422666 commit 7874473
Show file tree
Hide file tree
Showing 20 changed files with 546 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/serious-lamps-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alfalab/client-event-bus': major
---

Добавлена библиотека для обмена данными через общую шину
2 changes: 2 additions & 0 deletions packages/client-event-bus/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
.turbo
18 changes: 18 additions & 0 deletions packages/client-event-bus/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
extends: ['custom/common'],
parserOptions: {
tsconfigRootDir: __dirname,
project: [
'./tsconfig.eslint.json',
],
},
overrides: [
{
files: ['**/__tests__/**/*.{ts,tsx}'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
}
],
};
3 changes: 3 additions & 0 deletions packages/client-event-bus/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
build
coverage
5 changes: 5 additions & 0 deletions packages/client-event-bus/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
src
build/tsconfig.tsbuildinfo
__tests__
.turbo
coverage
80 changes: 80 additions & 0 deletions packages/client-event-bus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@alfalab/client-event-bus

Библиотека позволяет обмениваться данными в приложениях с модульной архитектурой (module federation), используя событийную модель браузера.
Это особенно актуально для приложений на базе `Webpack Module Federation`, обеспечивая взаимодействие без создания жестких зависимостей.

По сути представляет собой [`EventEmitter`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) с добавлением методов для
получения последнего отправленного события уже после того, как это событие произошло.

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

`constants/event-bus.ts` - заводим ключ, по которому создается шина данных
```ts
// constants/event-bus.ts
export const BUS_KEY = 'my-first-bus';
```

`types/event-bus.ts` - определяем типы событий
```ts
// types/event-bus.ts
type EventType = 'busValueFirst' | 'busValueSecond'
type EventPayload = string | null

export type EventTypes = Record<EventType, EventPayload>;
```

`types/event-types.d.ts` - добавляем файл для типизации функций `getEventBus` и `createBus`
```ts
// types/event-types.d.ts
import type { AbstractAppEventBus } from '@alfalab/client-event-bus';

import { BUS_KEY } from '~/constants/event-bus';
import type { EventTypes } from '~types/event-bus'

export declare type EventBus = AbstractAppEventBus<EventTypes>;

declare module '@alfalab/client-event-bus' {
export declare function getEventBus(busKey: typeof BUS_KEY): EventBus;
}

declare module '@alfalab/client-event-bus' {
export declare function createBus(
key: typeof BUS_KEY,
params?: EventBusParams,
): EventBus;
}
```

## Рекомендации использования

Для именования событий предлагается следующие договоренности:

- Имя события начинается с названия вашего проекта, исключая префикс вашей системы/направления (`corp-`, `ufr-` и так далее) и суффикс `-ui`. То есть если ваш проект называется `ufr-cards-ui` событие должно начинаться с `cards_`, `corp-sales-ui` это соответственно `sales_`.
- Название события пишется в `camelCase`.

## Возвращаемое значение

Возвращает `EventBus`, со следующими методами:

- `addEventListener(eventName: string, eventHandler: (event: CustomEvent) => void, options?: AddEventListenerOptions)` - [стандартная функция](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) добавления подписки на событие
- `removeEventListener(eventName: string, eventHandler: (event: CustomEvent) => void, options?: EventListenerOptions)` - [стандартная функция](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) удаления подписки на событие
- `getLastEventDetail(eventName: string)` - Функция, которая возвращает последнее событие заданного типа. Если событие еще не происходило - возвращает `undefined`
- `addEventListenerAndGetLast(eventName: string, eventHandler: (event: CustomEvent) => void, options?: AddEventListenerOptions)` - объединяет в себе `addEventListener` и `getLastEvent`. Подписывает на событие и возвращает последнее событие этого типа

## Использование в react
Если вам нужно использовать значение из event-bus в react коде - вы можете использовать хук `useEventBusValue`:
```tsx
import { useEventBusValue } from '@alfalab/client-event-bus';

const MyComponent = () => {
const currentOrganizationId = useEventBusValue('shared_currentOrganizationId');

return (
<div>
ID текущей организации: {currentOrganizationId}
</div>
)
}
```

Хук всегда будет возвращать последнее значение из eventBus. При изменениях значения будет происходить ререндер.
13 changes: 13 additions & 0 deletions packages/client-event-bus/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
testPathIgnorePatterns: [
'/node_modules/',
'/build/',
],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
],
};
39 changes: 39 additions & 0 deletions packages/client-event-bus/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@alfalab/client-event-bus",
"version": "1.0.0",
"main": "./build/index.js",
"module": "./build/esm/index.js",
"typings": "./build/index.d.ts",
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/core-ds/arui-scripts.git"
},
"bugs": {
"url": "https://github.com/core-ds/arui-scripts/issues"
},
"homepage": "https://github.com/core-ds/arui-scripts/tree/master/packages/client-event-bus#readme",
"scripts": {
"build:commonjs": "tsc --project tsconfig.json",
"build:esm": "tsc --project tsconfig.esm.json",
"build": "yarn build:commonjs && yarn build:esm",
"test": "jest",
"lint:scripts": "eslint \"**/*.{js,jsx,ts,tsx}\" --ext .js,.jsx,.ts,.tsx",
"lint": "yarn lint:scripts",
"lint:fix": "yarn lint:scripts --fix",
"format": "prettier --write $INIT_CWD/{config,src}/**/*.{ts,tsx,js,jsx,css}"
},
"peerDependencies": {
"react": ">16.18.0"
},
"devDependencies": {
"@types/jest": "^23.3.14",
"eslint": "^8.20.0",
"eslint-config-custom": "workspace:*",
"jest": "28.1.3",
"prettier": "^2.7.1",
"react": "18.2.0",
"ts-jest": "28.0.8",
"typescript": "4.9.5"
}
}
70 changes: 70 additions & 0 deletions packages/client-event-bus/src/__tests__/implementation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { EventBus, createBus } from '../implementation';

Check warning on line 1 in packages/client-event-bus/src/__tests__/implementation.test.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Run autofix to sort these imports!

Check warning on line 1 in packages/client-event-bus/src/__tests__/implementation.test.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Run autofix to sort these imports!

type EventList = {
testEvent: { message: string };
};

describe('EventBus', () => {
let eventBus: EventBus<EventList>;

beforeEach(() => {
eventBus = new EventBus<EventList>({ debugMode: true });
});

it('should create EventBus with default parameters', () => {
expect(eventBus).toBeDefined();
expect(eventBus).toHaveProperty('targetNode');
expect(eventBus).toHaveProperty('debugMode', true);
});

it('should dispatch an event and capture it', () => {
const handler = jest.fn();

eventBus.addEventListener<'testEvent', EventList['testEvent']>('testEvent', handler);
eventBus.dispatchEvent('testEvent', { message: 'Test Message' });

expect(handler).toHaveBeenCalled();
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({ detail: { message: 'Test Message' } }),
);
expect(eventBus.getLastEventDetail('testEvent')).toEqual({ message: 'Test Message' });
});

it('should return last event detail', () => {
eventBus.dispatchEvent('testEvent', { message: 'Test Message' });

const detail = eventBus.getLastEventDetail('testEvent');

expect(detail).toEqual({ message: 'Test Message' });
});

it('should add and remove event listeners', () => {
const handler = jest.fn();

eventBus.addEventListener('testEvent', handler);
eventBus.dispatchEvent('testEvent', { message: 'Hello!' });

expect(handler).toHaveBeenCalled();

eventBus.removeEventListener('testEvent', handler);
eventBus.dispatchEvent('testEvent', { message: 'Goodbye!' });

expect(handler).not.toHaveBeenCalledTimes(2);
});
});

describe('createBus', () => {
it('should create and return the same EventBus instance for the same key', () => {
const eventBus1 = createBus('eventBus');
const eventBus2 = createBus('eventBus');

expect(eventBus1).toBe(eventBus2);
});

it('should create different EventBus instances for different keys', () => {
const eventBus1 = createBus('eventBus1');
const eventBus2 = createBus('eventBus2');

expect(eventBus1).not.toBe(eventBus2);
});
});
28 changes: 28 additions & 0 deletions packages/client-event-bus/src/custom-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// CustomEvent не поддерживается нормально в ie, поэтому полифилим его прям тут
function isNativeCustomEventAvailable() {
try {
const p = new global.CustomEvent('cat', { detail: { foo: 'bar' } });

return p.type === 'cat' && p.detail.foo === 'bar';
} catch (e) {
// just ignore it
}

return false;
}

function CustomEventPolyfill<T>(type: string, params: CustomEventInit<T>) {
const e = document.createEvent('CustomEvent');

if (params) {
e.initCustomEvent(type, Boolean(params.bubbles), Boolean(params.cancelable), params.detail);
} else {
e.initCustomEvent(type, false, false, undefined);
}

return e;
}

export const CustomEvent = (
isNativeCustomEventAvailable() ? global.CustomEvent : CustomEventPolyfill
) as unknown as typeof global.CustomEvent;
13 changes: 13 additions & 0 deletions packages/client-event-bus/src/get-event-bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Возвращает event-bus для конкретной системы. Если event-bus в текущем контексте не доступен - вернет undefined
* @param busKey ключ конкретной системы
*/
/* eslint-disable no-underscore-dangle */
export function getEventBus(busKey: string) {
if (window.__alfa_event_buses) {
return window.__alfa_event_buses[busKey];
}

return null;
}
/* eslint-enable no-underscore-dangle */
97 changes: 97 additions & 0 deletions packages/client-event-bus/src/implementation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { AbstractAppEventBus, AbstractKnownEventTypes } from './types/abstract-types';
import { CustomEvent } from './custom-event';

export class EventBus<KnownEventTypes extends AbstractKnownEventTypes>
implements AbstractAppEventBus<KnownEventTypes>
{
constructor({ targetNode = document, debugMode = false }: EventBusParams = {}) {
this.debugMode = debugMode;
this.targetNode = targetNode;
}

private targetNode: Node;

private debugMode: boolean;

private lastEventValues = {} as Record<keyof KnownEventTypes, unknown>;

dispatchEvent<
EventName extends keyof KnownEventTypes,
PayloadType extends KnownEventTypes[EventName],
>(eventName: EventName, detail?: PayloadType): void {
this.targetNode.dispatchEvent(new CustomEvent(eventName as string, { detail }));
this.lastEventValues[eventName] = detail;

if (this.debugMode) {
// eslint-disable-next-line no-console
console.debug(`Event bus, dispatchEvent: ${eventName.toString()}`, detail);
}
}

getLastEventDetail<
EventName extends keyof KnownEventTypes,
PayloadType extends KnownEventTypes[EventName],
>(eventName: EventName): PayloadType | undefined {
return this.lastEventValues[eventName] as PayloadType | undefined;
}

addEventListener<
EventName extends keyof KnownEventTypes,
PayloadType extends KnownEventTypes[EventName],
>(
eventName: EventName,
eventHandler: (event: CustomEvent<PayloadType>) => void,
options?: boolean | AddEventListenerOptions,
): void {
this.targetNode.addEventListener(
eventName as string,
eventHandler as EventListener,
options,
);
}

addEventListenerAndGetLast<
EventName extends keyof KnownEventTypes,
PayloadType extends KnownEventTypes[EventName],
>(
eventName: EventName,
eventHandler: (event: CustomEvent<PayloadType>) => void,
options?: boolean | AddEventListenerOptions,
): PayloadType | undefined {
this.addEventListener(eventName, eventHandler, options);

return this.getLastEventDetail(eventName);
}

removeEventListener<
EventName extends keyof KnownEventTypes,
PayloadType extends KnownEventTypes[EventName],
>(
eventName: EventName,
eventHandler: (event: CustomEvent<PayloadType>) => void,
options?: EventListenerOptions | boolean,
): void {
this.targetNode.removeEventListener(
eventName as string,
eventHandler as unknown as EventListener,
options,
);
}
}

/* eslint-disable no-underscore-dangle */
export function createBus(
key: string,
params: EventBusParams = {},
): EventBus<AbstractKnownEventTypes> {
if (!window.__alfa_event_buses) {
window.__alfa_event_buses = {};
}
if (!window.__alfa_event_buses[key]) {
window.__alfa_event_buses[key] = new EventBus(params);
}

return window.__alfa_event_buses[key] as EventBus<AbstractKnownEventTypes>;
}

/* eslint-enable no-underscore-dangle */
7 changes: 7 additions & 0 deletions packages/client-event-bus/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import './types/types';

export { getEventBus } from './get-event-bus';
export { createBus, EventBus } from './implementation';
export { useEventBusValue } from './use-event-bus-value';

export type { AbstractAppEventBus, AbstractKnownEventTypes } from './types/abstract-types';
Loading

0 comments on commit 7874473

Please sign in to comment.