Commit 628f5576 by Andrey Golubov

Добавил общие фукции работы приложения

Signed-off-by: Golubov Andrey <a.golubov@taxi-master.com>
parent 5e305893
# Common Application
Данный модуль предоставляет пользовательскому приложению следущие базовые работы с приложением
import { Async, DecoderError } from '~/common/application/error';
import * as cr from '~/common/config-reader';
import * as eff from '~/common/eff';
import * as either from '~/common/either';
import { configDecoder, infoDecoder, presetKeyDecoder } from '~/config';
export type Config = typeof configDecoder['_A'];
export type Info = typeof infoDecoder['_A'];
export type PresetKey = typeof presetKeyDecoder['_A'];
export { configDecoder, infoDecoder, presetKeyDecoder };
/**
* Контекст конфигурации проекта
*/
export type Ctx = Config & { bundle: Info };
// Сгенирированные через Webpack.DefinePlugin занчения
declare const __CONFIG__: any;
declare const __INFO__: any;
/**
* Считывает конфигурации собранные в переменной `__CONFIG__` и информацию о сборке из переменной `__INFO__`
* Позволяет переопределить значения `__CONFIG__` через `window.location.search`
*/
export function init(): Async<Ctx> {
return either.ap(
configDecoder.validateConfigs(
cr.json(__CONFIG__),
cr.queryString(window.location.search),
),
infoDecoder.validate(__INFO__),
(config, bundle) => ({ ...config, bundle }),
).fold(problem => eff.failure(DecoderError(problem)), eff.success);
}
import { Problem } from '../decoder';
import { Eff } from '../eff';
import { Err as LocalStorageErr } from '../persistent';
import { literal } from '../types';
/**
* Data конструктор `UnexpectedError` ошибок
* @param error предположительно `Error`
*/
export function UnexpectedError(error: any) {
return Object.freeze({ tag: literal('UnexpectedError'), error });
}
/**
* Data конструктор `DecoderError` ошибок
* @param error проблема из `decoder.validate`
*/
export function DecoderError(error: Problem) {
return Object.freeze({ tag: literal('ValidationError'), error });
}
/**
* Ошибка загрузки переводов для `gettext`
* @param locale локаль при загрузке которой возникла ошибка
*/
export function TranslationLoadError(locale: string) {
return Object.freeze({ tag: literal('TranslatonLoadError'), locale });
}
/**
* Data-конструкто для `LocalStorageError` ошибок
* @param error возможные ошибки при работе с `window.localStorage`
*/
export function LocalStorageError(error: LocalStorageErr) {
return Object.freeze({ tag: literal('LocalStorageError'), error });
}
/**
* Перечесление возможных ошибок приложения
*/
export type Err =
| ReturnType<typeof UnexpectedError>
| ReturnType<typeof DecoderError>
| ReturnType<typeof TranslationLoadError>
| ReturnType<typeof LocalStorageError>
;
/**
* `Eff<Err, T>`, где `T` generic параметр `Async`
*/
export type Async<T> = Eff<Err, T>;
import * as eff from '~/common/eff';
import { Ctx as TranslationCtx, Translations } from '~/common/gettext';
import { Cache as LocalStorage } from '~/common/persistent';
import * as bcp47 from '~/common/utils/bcp47';
import { defaultLocale, localeCacheKey, localeDecoder, localePath, locales } from '~/config';
import { Async, LocalStorageError, TranslationLoadError } from '../error';
export type Locale = typeof localeDecoder['_A'];
export type Locales = Locale[];
export const cache = new LocalStorage<Locale>(localeCacheKey, localeDecoder);
/**
* Возвращает в случаи успеха `Translations`. Необходим для работы с `gettext`
* @param locale вариант перевода из `Locales`
*/
export function loadTranslations(locale: Locale): Async<Translations> {
const translations: Translations | undefined = require(`${localePath('~')}${locale}.po`);
return translations ? eff.success(translations) : eff.failure(TranslationLoadError(locale));
}
/**
* Возвращает подходящую локаль из `window.navigator.languages` или `defaultLocale`
*/
export function BrowserLocale(): Locale {
return (window.navigator.languages ? window.navigator.languages.slice() : [])
.map((locale) => bcp47.lookupLocale(locale, locales)) // Приводит к BCP47, если это возможно
.filter(browser => locales.find(locale => locale === browser))[0]
|| defaultLocale;
}
/**
* Контекст для Locale
*/
export type Ctx = TranslationCtx & { locale: Locale };
/**
* Инициализация локализации приложения по следущей логике:
* Если в `LocalStorage` присутсвует установленная локаль, подгружает ее.
* В случаи если `LocalStorage` значение отсуствует, выбор предоставляется из `локали браузера`
* @param locale вариант перевода из `Locales`
*/
export function init(): Async<Ctx> {
return cache.read().onError(e => {
switch (e.tag) {
case 'NoItem': return eff.success(BrowserLocale());
default: return eff.failure(LocalStorageError(e));
}
}).chain((locale) => loadTranslations(locale)
.map(translations => ({ translations, locale })),
);
}
import * as Rx from 'rxjs';
import { defaultRoute, parser } from '~/config';
import * as eff from '../../eff';
import { IO } from '../../types';
export type RouteOut = typeof parser._O;
export type RouteIn = typeof parser._I;
/**
* Возвращает хлебные крошки в виде `RouteOut[]`
* @param location
*/
export function parseLocation(location: Location): RouteOut[] {
if (!location.hash) { return [defaultRoute]; }
const breadcrumbs = parser.parseAll(location.hash.substr(1));
return breadcrumbs.length === 0 ? [defaultRoute] : breadcrumbs;
}
/**
* Печатает `RouteOut` в строку
* @param route роут
*/
export function print(route: RouteIn): string {
return `#${parser.print(route)}`;
}
/**
* Подписка на изменения `#` роута
*/
export const routeStream = new Rx.Subject<RouteOut[]>();
window.onpopstate = (() => { routeStream.next(parseLocation(window.location)); });
export function routePush(route: RouteIn, title: string = ''): IO<void> {
window.history.pushState(null, title, print(route));
routeStream.next(parseLocation(window.location));
}
export const routePush$ = (route: RouteIn, title: string = '') => eff.forget(routePush, route, title);
export function routeReplace(route: RouteIn, title: string = ''): IO<void> {
window.history.replaceState(null, title, print(route));
routeStream.next(parseLocation(window.location));
}
export const routeReplace$ = (route: RouteIn, title: string = '') => eff.forget(routePush, route, title);
/** For better type inference */ /** For better type inference */
export type Expr = boolean|null|undefined|number|string|{}|any[]|[any,any]|[any,any,any]|Function export type Expr = boolean|null|undefined|number|string|{}|any[]|[any,any]|[any,any,any]|Function
/** Non pure function wrapper type */
export type IO<T> = T;
/** Don't coerce string literals to `string` type */ /** Don't coerce string literals to `string` type */
export function literal<A extends string>(x: A): A { export function literal<A extends string>(x: A): A {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment