@astral/services
v1.6.0
Published
Shared js-сервисы.
Downloads
300
Readme
@astral/services
Shared js-сервисы.
UserAgentDetector
UserAgentDetector — сервис парсинга строки User-Agent браузера для получения информацию о текущей ОС, браузере и его версии, а также устройстве пользователя.
Использование
import {
Browser,
Device,
OperatingSystem,
type UserAgentDetector,
userAgentDetector as userAgentDetectorInstance,
} from '@astral/services';
export class UIStore {
constructor(private readonly userAgentDetector: UserAgentDetector) {
makeAutoObservable(this, {}, { autoBind: true });
}
public get isOsSupported() {
return this.userAgentDetector.os === OperatingSystem.Windows;
}
public get browserName() {
return this.userAgentDetector.browser; // Browser.Chrome === 'Chrome'
}
public get browserVersion() {
return this.userAgentDetector.browserVersion; // 91.0.4472.124
}
public get isMobile() {
return this.userAgentDetector.device === Device.Mobile
|| this.userAgentDetector.device === Device.Tablet
}
}
export const createUIStore = () => new UIStore(userAgentDetectorInstance);Перечисления
OperatingSystem:
Windows= 'Windows',MacOS= 'macOS',Unix= 'Unix', // В т.ч. Linux, CentOS, Debian, Mint, RedHat, Ubuntu, SUSE, Unix, Solaris, AIX, FreeBSDiOS= 'iOS',Android= 'Android',Unknown= 'Unknown',
Browser:
Chrome= 'Chrome',Yandex= 'YaBrowser',Opera= 'Opera',Firefox= 'Firefox',Safari= 'Safari',Edge= 'Edge',IE= 'IE',Unknown= 'Unknown',
Device:
Mobile= 'Mobile',Tablet= 'Tablet',Other= 'Other',
FeatureFlags
FeatureFlagsStore — позволяет включать или выключать определенные части функциональности приложения
Может применяться в 2-х случаях:
- для сокрытия функциональности, находящейся на ранних стадиях разработки (Boolean)
- для A/B/n тестирования (String)
Использование
- Создать модуль featureToggle
├── index.ts
└── domain/
├── constants.ts # Конфиги флагов
└── stores/
├── FeatureToggleStore.ts # Фасад для взаимодействия со стором
└── index.ts- Зарегистрировать флаги
Для A/B/n тестирования (StringFeatureFlags) обязательно наличие целевого события для вычисления конверсии
Записываем дефолтные значения для флагов In-memory на случай, если источник флагов не отвечает
export const DEFAULT_STRING_VALUE = 'NA' as const;
export const BooleanFeatureFlags: BooleanFeatureFlagsMap<FeatureFlagsRepositoryDTO.KeyProductionReady> =
{
NewFeature: {
flagKey: 'NewFeature',
defaultValue: false,
},
};
export const StringFeatureFlags: StringFeatureFlagsMap<
FeatureFlagsRepositoryDTO.KeyForExperiment,
FeatureFlagsRepositoryDTO.EventType
> = {
FeatureExperiment: {
flagKey: 'FeatureExperiment',
defaultValue: 'one',
variants: {
one: 'one',
two: 'two',
},
eventType: 'FeatureExperimentEvent',
},
};Создать репозиторий с методами getBooleanFlagList и getStringFlagList, которые получают из remote источника состояния флагов
Создать фасад для взаимодействия с сервисом, где при инициализации передать коллбэк для обновления данных о состоянии флагов
export class FeatureToggleStore {
constructor(
private readonly flagsStore: FeatureFlagsStore<
FeatureFlagsRepositoryDTO.KeyProductionReady,
FeatureFlagsRepositoryDTO.KeyForExperiment,
FeatureFlagsRepositoryDTO.EventType
>,
private readonly router: Router
) {
makeAutoObservable(this);
}
public init = () => {
this.flagsStore.init(this.router.onNavigate);
};
public get productionReady() {
return this.flagsStore.productionReady;
}
public get experiments() {
return this.flagsStore.experiments;
}
}
const featureFlagsStore = createFeatureFlagsStore(
{
booleanFeatureFlags: BooleanFeatureFlags,
stringFeatureFlags: StringFeatureFlags,
defaultStringValue: DEFAULT_STRING_VALUE,
},
featureFlagsRepository,
);
export const featureToggleStore = new FeatureToggleStore(
featureFlagsStore,
routerService
);- Инициализировать featureToggleStore
featureToggleStore.init();- Применить во View-компоненте
export const Main = observer(() => {
const featureProductionReady = featureToggleStore.productionReady;
return (
<Main>
{featureProductionReady.NewFeature && (
<FeatureInDevelop />
)}
</Main>
);
});export const Main = observer(() => {
const { flags, track } = featureToggleStore.experiments;
const handleClick = () => {
track('FeatureExperimentEvent');
};
return (
{flags?.FeatureExperiment === 'two' ? (
<VariantTwo onClick={handleClick} />
) : (
<VariantOne onClick={handleClick} />
)}
);
});Как работает
При первой загрузке приложения сразу происходит получение данных о состоянии флагов из двух запросов.
При каждом переходе на новую страницу происходит перезапрос и данные о состоянии флагов обновляются сразу, не дожидаясь монтирования компонента, в котором требуется флаг. Поэтому при инициализации необходимо передать коллбэк, который срабатывает при смене URL.
При медленной сети запрос может длиться долго, и данные могут прийти после того, как смонтировался компонент. Поэтому флаги обладают реактивным свойством и могут обновить состояние компонента после монтирования.
LocalStorageService
LocalStorageService — сервис для взаимодействия с localStorage.
Сервис поддерживает:
- безопасный доступ/модификацию свойств localStorage с возможностью подавления ошибок (по умолчания подавляются);
- установку свойств с учетом TTL (time-to-live/Время жизни) - возможностью управления временем жизни;
- совместимость с legacy-форматами значений в localStorage;
- восстановление после временных ошибок;
Единый формат хранения
Сервис устанавливает единый формат хранения данных в localStorage:
type LocalStorageItem = {
value: string;
expiredAt: number | null; // Время истечения значения в мс. Если null - бессрочно
};Поддерживается обратная совместимость со значениями, установленными в localStorage его нативными методами
Сервис корректно обработает:
//Примитивные значения, установленные нативным методом setItem
localStorage.setItem('x','value')
//Сериализованные в JSON объекты, установленные нативным методом setItem
localStorage.setItem('x', JSON.stringify({ x : 'value' }))
//Значения, типа { value: string; expiredAt: number }, установленные другим сервисом
localStorage.setItem('x', JSON.stringify({ value : 'x', expiredAt : 100 }))Использование
Базовый пример:
Передайте сервис параметром в нужный store через DI:
export class UIStore {
constructor(
private readonly _localStorageService: LocalStorageService,
) {
makeAutoObservable(this);
}
public setValueToLocalStorage = () => {
this._localStorageService.setItem('x', 'value');
//Остальная логика...
};
}
export const createUIStore = () => new UIStore(createLocalStorageService())Установка значений:
//Запись значения 'value' по ключу 'x'
localStorageService.setItem('x', 'value');
//Запись значения с временем жизни 100 мс.
localStorageService.setItem('x', 'value', { ttl: 100 });
Чтение значений:
/** Получение значения по ключу 'x'.
* При попытке чтения значения с истекшим временем жизни вернется null */
const value = localStorageService.getItem('x');Удаление значений:
//Удаление значения по ключу 'x'
await localStorageService.removeItem('x');Очистка всех значений:
await localStorageService.clear();Обработка ошибок.
Взаимодействие с localStorage чаще всего может провоцировать следующие ошибки при:
- Инициализации в окружениях
- серверный рендеринг (SSR: Next.js, Node.js)
- WebView с ограничениями
- Отключении пользователем localStorage
- через настройки браузера
- расширения (AdBlock и др.)
- Переполнении хранилища (лимит зависит среды выполнения)
- при попытках установить значение (QuotaExceededError)
Сервис предоставляет следующие способы наблюдения за ошибками:
//Через отслеживание параметра error
export class UIStore {
constructor(private readonly _localStorageService: LocalStorageService) {}
public checkIsSidebarOpen = () => {
if(!this._localStorageService.error) {
this._localStorageService.getItem('isSidebarOpen')
} else {
//Логика обработки ошибки
}
}
}
//Через коллбэк при инициализации
const handleLocalStorageError = (error) => {
if (
error.name === 'QuotaExceededError'
) {
notifyService.warning(
'Хранилище заполнено. Очистите историю браузера и повторите попытку.',
);
}
}
export const localStorageService = createLocalStorageService({ onError : handleLocalStorageError})Тестирование
Для мока localStorageService используйте createLocalStorageServiceMock.
Базовый пример
import { createLocalStorageServiceMock } from "@astral/services/LocalStorageService/_mocks";
it('Alert не показывается пользователю в течении недели, при его скрытии', () => {
const weekInMilliseconds = 7 * 24 * 60 * 60 * 1000;
const { localStorageService : localStorageServiceMock, localStorageValues } = createLocalStorageServiceMock(
{ initialValuesList: [{ key: 'isAlertHide', value : 'IvanDivan', options : { ttl : weekInMilliseconds } }] }
);
//localStorageValues
//{ isAlertHide: '{"value":"IvalDivan","expiredAt":1736294400000}' }
const sut = new UIStore(localStorageServiceMock);
sut.hideAlert()
expect(sut.isAlertVisible).toBe(false);
});