2 posts tagged with "lodash"

View All Tags

Каналы в JS - это когда "тебе" не только кнопочки "рисовать"

Timofey Aksenov

Timofey Aksenov

Front End Developer

Работал я как-то свою работу на работе: "рисовал кнопочки на реакте" и вдруг бах! Тимлид мне говорит:

Тимофейчик, нам нужно срочно в проект закатить поддержку web sockets с возможностью одновременного подключения нескольких разных web socket серверов и с возможностью подключаться к множеству различных каналов внутри web socket сервера, а так же нужно как-то активными каналами управлять, gracefull stopping им, независимо друг от друга, делать и следить за их количеством и т.д. и т.п. И да, вот мы тут развернули сервак на технологии centrifuge. Сделай красиво, в общем.

Делать нечего, нужно сделать.

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

Разработка веб-сервисов на Go - основы языка / Результат 100%

Разработка веб-сервисов на Golang, часть 2 / Результат 100%

В общем на этих курсах изучая, как работают горутины и пишутся микросервисы, я узнал про каналы, успешно реализовал их в ДЗ и запомнил, что каналы вещь нужная и полезная.

И вот в процессе первоначального мозгового штурма, я вспомнил, что в документации к redux-saga читал про eventChannels и там был пример создания канала для web sockets на основе socket.io client. О да, если бы не курсы по гошечке, то я бы эту часть документации пропустил и пошел дальше экшенами в стор пуляться... ой "рисовать кнопочки на реакте" конечно же.

Изучив пример из документации redux-saga, я понял, что eventChannels это то что мне нужно. Но нужно доработать свой код, чтобы учесть технологические требования:

  1. Подключение к различным физическим серверам web sockets (оборачивание каждого в отдельный канал)
  2. Подключение к различным каналам внутри одного физического сервера web sockets (оборачивание каждого в отдельный канал)
  3. Независимый запуск каналов
  4. Независимая остановка каналов
  5. Возможность провести инвентаризацию активных каналов

Реализация задуманного:

В файле chanSaga.ts я создал 5 функций, которые обладают следущей функциональностью:

/**
* Создает канал событий для указанного подключения к сокет серверу
*
* @param {{ data: any; socket: any }} { data, socket }
* @returns
*/
export function createSocketChannel({
data,
socket,
outerCallback,
}: {
data: any;
socket: any;
outerCallback: (params: any) => void;
}) {
const { token, channel } = data;
return eventChannel((emit: any) => {
socket.setToken(token);
// NB: Обратите внимание на строку кода `outerCallback({ message, emit });` будет бонус
const publishHandlerFunction = (message: any) => {
outerCallback({ message, emit });
};
const subscribeErrorHandlerFunction = (errorEvent: any) => {
emit(new Error(errorEvent.reason));
};
const subscription = socket.subscribe(channel);
subscription.on('publish', publishHandlerFunction);
subscription.on('error', subscribeErrorHandlerFunction);
socket.connect();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('disconnect', (_context: any) => {
emit(END);
});
// unsubscribe
return () => {
socket.disconnect();
};
});
}
/**
* Получает инициализационные данные о сокет сервере из АПИ,
* для последующего создания соединения с сокет сервером
*
* @export
* @param {IPayload} payload
* @returns {(Generator<CallEffect<Promise<AxiosResponse<any>> | null>, IInitData, IRes>)}
*/
export function* initSocketConnection(
payload: IPayload,
): Generator<CallEffect<Promise<AxiosResponse<any>> | null>, IInitData, IRes> {
const { userToken: token, path } = payload;
const socketInitData: IRes = yield call(socketAPI.postDataInitSocket, {
token,
path,
});
const { data } = socketInitData;
const contractStatus = socketInitDataContract.decode(data);
if (E.isLeft(contractStatus)) {
console.log('Socket init: contract error, path:', path);
return { status: InitStatusEnum.ERROR };
}
return { status: InitStatusEnum.SUCCESS, data };
}
/**
* Создать подключение к сокет серверу.
*
* Клиент сокет сервера: Centrifuge
*
* @export
* @param {IData} data
* @returns
*/
export function createSocketConnection(data: IData) {
const { socket } = data;
const { proto, host, port } = socket;
const suffix = environment.WEB_SOCKET_SUFFIX;
const centrifuge = new Centrifuge(`${proto}${host}:${port}${suffix}`);
return centrifuge;
}
/**
* Отключиться от сокет сервера, а так же закрыть канал `eventChannel`
*
* @export
* @param {string} socketChanName
*/
export function* terminateSocketChan(socketChanName: string) {
const scktSelector = yield select(socketSelector);
const centrifuge =
get(scktSelector, `${socketChanName}`, 'NO_CHAN') || 'NO_CHAN';
if (centrifuge === 'NO_CHAN') {
return null;
}
centrifuge.disconnect();
const tmpScktData = omit(scktSelector, `${socketChanName}`);
yield put({
type: socketSaveSocket.toString(),
payload: {
...tmpScktData,
},
});
}
/**
* Вотчер.
*
* Следит за каналом принимая сообщения от сокет сервера или команду останова.
*
* Прерывается через вызов функции terminateSocketChan из других саг:
* `yield call(terminateSocketChan, `${channelName}`);`
*
* @export
* @param {} chanName
*/
export function* watchSocketChan(chanName: any) {
while (true) {
try {
const action = yield take(chanName);
yield put(action);
yield fork(terminateSocketChan, '');
} catch (err) {
console.log('socket error:', err);
chanName.close();
}
}
}

Практическая реализация задуманного:

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

// Выберем нужный сокет канал
const scktSelector: any = yield select(socketSelector);
// Прервем его, если он вдруг остался запущен,
yield call(terminateSocketChan, SUPPORT_MESSAGES_CHAN);
yield put({
type: socketSaveSocket.toString(),
payload: {},
});
// Создадим сокет
const socket = yield call(createSocketConnection, data);
// Создадим канал для сокета
// NB: Обратите внимание на строку кода `outerCallback: chatListSupportSocketCallback,` будет бонус
const channel = yield call(createSocketChannel, {
data,
socket,
outerCallback: chatListSupportSocketCallback,
});
yield put({
type: socketSaveSocket.toString(),
payload: {
...scktSelector,
[SUPPORT_MESSAGES_CHAN]: socket,
},
});
// Запустим вотчер, принимающий сообщения в канал от внешнего web socket сервера
yield call(watchSocketChan, channel);

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

Бонус про зубодробительный пример с замыканием:

/**
* Коллбек, который помещается в event канал с сокетами,
* где при событии от сокет сервера `publish`
* "вытягивает" emit (эмиттер) в замыкание.
* Эмиттер (emit) это аргумент коллбека из функции `eventChannel` пакета redux-saga:
*
* `eventChannel((emit: any) => {...}`
*
* Эмиттер нужен, чтобы потом "дернуть" экшен и передать в его `payload`
* данные полученные от сокет сервера
*
* @param {IParams} params
* @param message any;
* @param emit (input: any) => void;
*/
export const socketCallback = (params: IParams) => {
const { message } = params;
const { data } = message;
checkContract({
contract: soketServerMsgContract,
data,
direction: DIRECTION.B_F,
});
params.emit({
type: socketSupportClearBeforeRes.toString(),
});
params.emit({
type: chatListSupportResMsgFromSocket.toString(),
payload: data,
});
};

Котики любят каналы, все любят каналы 🐱

Когда функциональное программирование это хорошо, правильно и нужно

Timofey Aksenov

Timofey Aksenov

Front End Developer

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

В процессе написания, мне очень хорошо запомнился один момент, а именно: установка бейджика со статусом для коробки, паллеты, партии с готовой продукцией. Один и тот же объект в бизнес-логике складского учета может иметь различные статусы, которые отображаются в таблице с итоговыми выборками. С бекенда я получал только статусы в виде строк IN_PARTY и номера этих самых партий, которые преобразовывал в человекочитаемый вид. Например: В коробке, В партии, В партии: №2, В паллете, В паллете: №6.

Код первоначальной версии:

const {
...
logisticStatus,
partyNumber = -1,
} = item;
...
const statusForHuman = getLogisticStatus(logisticStatus);
const storeTwoPalletStatusNumber =
logisticStatus === 'IN_PARTY' ? partyNumber : -1;
...
return (
...
<Table.Cell>
{statusForHuman}
{storeTwoPalletStatusNumber !== -1
? `: №${storeTwoPalletStatusNumber}`
: null}
</Table.Cell>
...
)

Когда я прототипировал и готовил первоначальную версию кода, меня совершенно не смутила вот эта часть кода:

<Table.Cell>
{statusForHuman}
{storeTwoPalletStatusNumber !== -1
? `: №${storeTwoPalletStatusNumber}`
: null}
</Table.Cell>

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

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

И о чудо! Решение нашлось в самом неожиданном месте: на досуге я смотрел доклад по ЯП elixir и обратил внимание на оператор пайпа в данном языке |>. Это натолкнуло меня на мысль, что мне для решения моей проблемы с размазанной по шаблонам логикой поможет функциональное программирование.

Сказано, сделано. Я открыл гайд по ФП в js , Lodash FP Guide и быстро выяснил, что для решения моей проблемы подойдет lodash метод _.flow([funcs]), который по сути реализует пайп или выполнение функций слева на право

Код итоговой версии:

...
const {
...
logisticStatus,
partyNumber = -1,
} = item;
// Один раз передаем данные на вход потоку функций
const statusForHuman = setPalletStatusForHuman(
logisticStatus,
partyNumber,
);
...
return (
...
{/* Пользуемся удобными бейджиками во множестве мест компонента */}
<Table.Cell>{statusForHuman}</Table.Cell>
<Table.Cell>{statusForHuman}</Table.Cell>
<Table.Cell>{statusForHuman}</Table.Cell>
...
)

Код реализации хелперов, устанавливающих человекочитаемые статусы

import flow from 'lodash/flow';
/**
* Интерфейс с описанием типов статуса
*
* @interface IStatus
*/
interface IStatus {
logisticStatus: string;
correctNumber?: number;
}
/**
* Установка номера паллеты или номера партии для полного статуса
*
* @param {string} logisticStatus
* @param {number} palletNumber
* @param {number} partyNumber
* @returns {object} {
* {string} logisticStatus,
* {number | undefined | -1} correctNumber,
* }
*/
const storeTwoPackStatusNumber: (s: string, n: number, p: number) => IStatus = (
logisticStatus: string,
palletNumber: number,
partyNumber: number,
) => {
return logisticStatus === 'IN_PALLET'
? { logisticStatus, correctNumber: palletNumber }
: { logisticStatus, correctNumber: partyNumber };
};
/**
* Установка номера паллеты для полного статуса
*
* @param {string} logisticStatus
* @param {number} partyNumber
* @returns{object} {
* {string} logisticStatus,
* {number | undefined | -1} correctNumber,
* }
*/
const storeTwoPalletStatusNumber: (s: string, p: number) => IStatus = (
logisticStatus: string,
partyNumber: number,
) => {
return logisticStatus === 'IN_PARTY'
? { logisticStatus, correctNumber: partyNumber }
: { logisticStatus, correctNumber: -1 };
};
/**
* Преобразование кода статуса, полученного с бекенда в человекочитаемый статус
*
* @param {*} {
* logisticStatus,
* correctNumber,
* }
* @returns {object} {
* {string} logisticStatus,
* {number | undefined | -1} correctNumber,
* }
*/
const getHumanLogisticStatus: (data: IStatus) => IStatus = ({
logisticStatus,
correctNumber,
}) => {
let statusForHuman = logisticStatus;
let temp = -1;
if (correctNumber) {
temp = correctNumber;
}
switch (logisticStatus) {
case 'DISASSEMBLED':
statusForHuman = 'Разобрано';
break;
case 'ASSEMBLED':
statusForHuman = 'Собрано';
break;
case 'IN_PALLET':
statusForHuman = 'В паллете';
break;
case 'IN_PARTY':
statusForHuman = 'В партии';
break;
case 'SHIPPING':
statusForHuman = 'Отгружено';
break;
default:
statusForHuman = logisticStatus;
break;
}
return { logisticStatus: statusForHuman, correctNumber: temp };
};
/**
* Получение статуса в человекочитаемом формате, с указанием номера (при необходимости)
*
* @param {*} {
* logisticStatus,
* correctNumber,
* }
* @returns {string} statusForHuman
*/
const getLogisticStatus: (data: IStatus) => string = ({
logisticStatus,
correctNumber,
}) => {
let statusForHuman = logisticStatus;
switch (logisticStatus) {
case 'В паллете':
statusForHuman = `В паллете${
correctNumber !== -1 ? `: №${correctNumber}` : ''
}`;
break;
case 'В партии':
statusForHuman = `В партии${
correctNumber !== -1 ? `: №${correctNumber}` : ''
}`;
break;
default:
statusForHuman = logisticStatus;
break;
}
return statusForHuman;
};
/**
* Pipe для установки отформатированного статуса паллеты в компоненте
*
* @param {string} logisticStatus
* @param {number} partyNumber
*
* @returns {string} statusForHuman
*/
export const setPalletStatusForHuman = flow([
storeTwoPalletStatusNumber,
getHumanLogisticStatus,
getLogisticStatus,
]);
/**
* Pipe для установки отформатированного статуса коробки в компоненте
*
* @param {string} logisticStatus
* @param {number} palletNumber
* @param {number} partyNumber
*
* @returns {string} statusForHuman
*/
export const setPackStatusForHuman = flow([
storeTwoPackStatusNumber,
getHumanLogisticStatus,
getLogisticStatus,
]);
/**
* Pipe для установки отформатированного статуса для общего случая в компоненте
*
* @param {*} {
* {string} logisticStatus - код статуса с бекенда,
* {number | undefined | -1} correctNumber - номер объекта,
* }
*
* @returns {string} statusForHuman
*/
export const setCommonStatusForHuman = flow([
getHumanLogisticStatus,
getLogisticStatus,
]);

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

И программисты рады, и котики рады! 🐱