UNPKG

@nimel/directorr-router

Version:
338 lines (327 loc) 13.5 kB
/* (c) Nikita Melnikov - MIT Licensed 2020 - now */ import { pathToRegexp, compile } from 'path-to-regexp'; import qs from 'query-string'; import { EMPTY_OBJECT, EMPTY_STRING, createActionAndEffect, action, effect, composePropertyDecorators, whenState, whenPayload, EMPTY_FUNC, whenDestroy } from '@nimel/directorr'; import { createBrowserHistory } from 'history'; const ANY_PATH = '(.*)'; const ROOT_URL = '/'; class Cache { constructor(cacheSize = 10000) { this.store = new Map(); this.cacheSize = cacheSize; } set(key, value) { if (this.store.size > this.cacheSize) { this.store.delete(this.store.keys().next().value); } return this.store.set(key, value); } get(key) { return this.store.get(key); } has(key) { return this.store.has(key); } get size() { return this.store.size; } } const CACHE_PATTERNS = new Map(); const CACHE_PATHS = new Cache(); function compileRGXP(pattern, end, strict) { const cacheKey = EMPTY_STRING + end + strict; if (!CACHE_PATTERNS.has(cacheKey)) CACHE_PATTERNS.set(cacheKey, new Cache()); const patternCache = CACHE_PATTERNS.get(cacheKey); if (patternCache.has(pattern)) return patternCache.get(pattern); const keys = []; const reg = pathToRegexp(pattern, keys, { end, strict }); const compiledRGXP = { reg, keys }; patternCache.set(pattern, compiledRGXP); return compiledRGXP; } function matchPath(pathname, urlPattern, exact = true, strict = false) { const { reg, keys } = compileRGXP(urlPattern, exact, strict); const patterns = reg.exec(pathname); if (patterns) { return { patterns, keys, }; } } function calcParams(patterns, keys) { const params = {}; for (let i = 0, l = keys.length, name, value; i < l; ++i) { name = keys[i].name; value = patterns[i + 1]; if (name) { params[name] = value; } } return params; } function calcPath(path, queryObject) { if (queryObject) { const query = qs.stringify(queryObject); return query ? `${path}?${query}` : path; } return path; } function compilePath(path) { if (CACHE_PATHS.has(path)) return CACHE_PATHS.get(path); const genPath = compile(path); CACHE_PATHS.set(path, genPath); return genPath; } function generatePath(path = ROOT_URL, params = EMPTY_OBJECT) { return path === ROOT_URL ? path : compilePath(path)(params); } function reloadWindow() { window.location.reload(); } function returnTrue() { return true; } function addStoreToPayload(payload, store) { return Object.assign(Object.assign({}, payload), { store }); } function pickSameStore(payload, store) { return store === payload.store; } const [actionRouterPush, effectRouterPush] = createActionAndEffect('ROUTER.PUSH'); const [actionRouterReplace, effectRouterReplace] = createActionAndEffect('ROUTER.REPLACE'); const [actionRouterBack, effectRouterBack] = createActionAndEffect('ROUTER.BACK'); const [actionRouterForward, effectRouterForward] = createActionAndEffect('ROUTER.FORWARD'); const [actionRouterGoTo, effectRouterGoTo] = createActionAndEffect('ROUTER.GOTO'); const [actionRouterReload, effectRouterReload] = createActionAndEffect('ROUTER.RELOAD'); const [actionRouterBlock, effectRouterBlock] = createActionAndEffect('ROUTER.BLOCK'); const [actionRouterCancelBlock, effectRouterCancelBlock] = createActionAndEffect('ROUTER.CANCEL_BLOCK'); const [actionRouterState, effectRouterState] = createActionAndEffect('ROUTER.STATE'); const actionRouterIsPattern = action('ROUTER.IS_PATTERN', addStoreToPayload); const effectRouterIsPattern = effect(actionRouterIsPattern.type); const actionRouterIsPatternSuccess = action('ROUTER.IS_PATTERN_SUCCESS'); const effectRouterIsPatternSuccess = composePropertyDecorators([effect(actionRouterIsPatternSuccess.type), whenState(pickSameStore)]); const [actionHistoryPop, effectHistoryPop] = createActionAndEffect('HISTORY.POP'); const [actionHistoryPush, effectHistoryPush] = createActionAndEffect('HISTORY.PUSH'); const [actionHistoryReplace, effectHistoryReplace] = createActionAndEffect('HISTORY.REPLACE'); const DEFAULT_OPTIONS = { exact: true, strict: true, }; function historyChange(urlPattern, { exact, strict } = DEFAULT_OPTIONS) { return composePropertyDecorators([ effectHistoryPop, effectHistoryPush, effectHistoryReplace, whenPayload(returnTrue, (payload) => { const match = matchPath(payload.path, urlPattern, exact, strict); return Object.assign(Object.assign({}, payload), { match: !!match, queryObject: Object.assign(Object.assign({}, payload.queryObject), ((match === null || match === void 0 ? void 0 : match.keys.length) && { params: calcParams(match.patterns, match.keys) })) }); }), ]); } const ACTION = { POP: 'POP', PUSH: 'PUSH', REPLACE: 'REPLACE', }; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __metadata(metadataKey, metadataValue) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); } class HistoryStore { constructor(history = createBrowserHistory()) { this.unsubHistory = EMPTY_FUNC; this.handlersStack = []; this.blockState = [EMPTY_FUNC, EMPTY_FUNC]; this.subscribe = (handler) => { this.handlersStack = [...this.handlersStack, handler]; }; this.unsubscribe = (handler) => { this.handlersStack = this.handlersStack.filter(h => h !== handler); }; this.toSetState = ({ path, queryObject, state }) => { this.path = path; this.queryObject = queryObject; this.state = state; }; this.toDestroy = () => { this.unsubHistory(); }; this.toHistoryPush = (payload) => payload; this.toHistoryPop = (payload) => payload; this.toHistoryReplace = (payload) => payload; this.toPop = ({ path, queryObject, state, action }) => { this.path = path; this.queryObject = queryObject; this.state = state; this.action = action; }; this.push = (path, queryObject = EMPTY_OBJECT, state) => ({ path, queryObject, state, }); this.replace = (path, queryObject = EMPTY_OBJECT, state) => ({ path, queryObject, state, }); this.back = EMPTY_FUNC; this.reload = EMPTY_FUNC; this.toReload = () => reloadWindow(); this.toPush = ({ path, queryObject, state }) => this.history.push(calcPath(path, queryObject), state); this.toReplace = ({ path, queryObject, state }) => this.history.replace(calcPath(path, queryObject), state); this.toBack = () => this.history.back(); this.goTo = (index) => ({ index }); this.toGoTo = ({ index }) => this.history.go(index); this.forward = EMPTY_FUNC; this.toForward = () => this.history.forward(); this.block = (blocker) => ({ blocker }); this.toBlock = ({ blocker }) => { this.blockState = [blocker, this.history.block(blocker)]; }; this.cancelBlock = (blocker) => ({ blocker }); this.toCancelBlock = ({ blocker }) => { const [lastBlocker, handler] = this.blockState; if (blocker === lastBlocker) { handler(); } }; this.dispatchAction = (task) => { const { location, action } = task; const routerPayload = Object.assign(Object.assign({ path: location.pathname, queryObject: qs.parse(location.search) }, (location.state && { state: location.state })), { action }); switch (action) { case ACTION.POP: this.toHistoryPop(routerPayload); break; case ACTION.REPLACE: this.toHistoryReplace(routerPayload); break; default: this.toHistoryPush(routerPayload); } for (const handler of this.handlersStack) { handler(task); } }; this.history = history; const { location: { pathname, search, state }, action, } = this.history; this.unsubHistory = this.history.listen(this.dispatchAction); this.path = pathname; this.queryObject = qs.parse(search); this.state = state; this.action = action; } toJSON() { } } __decorate([ effectRouterState, __metadata("design:type", Object) ], HistoryStore.prototype, "toSetState", void 0); __decorate([ whenDestroy, __metadata("design:type", Object) ], HistoryStore.prototype, "toDestroy", void 0); __decorate([ actionHistoryPush, __metadata("design:type", Object) ], HistoryStore.prototype, "toHistoryPush", void 0); __decorate([ actionHistoryPop, __metadata("design:type", Object) ], HistoryStore.prototype, "toHistoryPop", void 0); __decorate([ actionHistoryReplace, __metadata("design:type", Object) ], HistoryStore.prototype, "toHistoryReplace", void 0); __decorate([ effectHistoryPop, effectHistoryPush, effectHistoryReplace, __metadata("design:type", Object) ], HistoryStore.prototype, "toPop", void 0); __decorate([ actionRouterPush, __metadata("design:type", Object) ], HistoryStore.prototype, "push", void 0); __decorate([ actionRouterReplace, __metadata("design:type", Object) ], HistoryStore.prototype, "replace", void 0); __decorate([ actionRouterBack, __metadata("design:type", Object) ], HistoryStore.prototype, "back", void 0); __decorate([ actionRouterReload, __metadata("design:type", Object) ], HistoryStore.prototype, "reload", void 0); __decorate([ effectRouterReload, __metadata("design:type", Object) ], HistoryStore.prototype, "toReload", void 0); __decorate([ effectRouterPush, __metadata("design:type", Object) ], HistoryStore.prototype, "toPush", void 0); __decorate([ effectRouterReplace, __metadata("design:type", Object) ], HistoryStore.prototype, "toReplace", void 0); __decorate([ effectRouterBack, __metadata("design:type", Object) ], HistoryStore.prototype, "toBack", void 0); __decorate([ actionRouterGoTo, __metadata("design:type", Object) ], HistoryStore.prototype, "goTo", void 0); __decorate([ effectRouterGoTo, __metadata("design:type", Object) ], HistoryStore.prototype, "toGoTo", void 0); __decorate([ actionRouterForward, __metadata("design:type", Object) ], HistoryStore.prototype, "forward", void 0); __decorate([ effectRouterForward, __metadata("design:type", Object) ], HistoryStore.prototype, "toForward", void 0); __decorate([ actionRouterBlock, __metadata("design:type", Object) ], HistoryStore.prototype, "block", void 0); __decorate([ effectRouterBlock, __metadata("design:type", Object) ], HistoryStore.prototype, "toBlock", void 0); __decorate([ actionRouterCancelBlock, __metadata("design:type", Object) ], HistoryStore.prototype, "cancelBlock", void 0); __decorate([ effectRouterCancelBlock, __metadata("design:type", Object) ], HistoryStore.prototype, "toCancelBlock", void 0); export { ACTION, ANY_PATH, HistoryStore, actionHistoryPop, actionHistoryPush, actionHistoryReplace, actionRouterBack, actionRouterBlock, actionRouterCancelBlock, actionRouterForward, actionRouterGoTo, actionRouterIsPattern, actionRouterIsPatternSuccess, actionRouterPush, actionRouterReload, actionRouterReplace, actionRouterState, addStoreToPayload, calcPath, effectHistoryPop, effectHistoryPush, effectHistoryReplace, effectRouterBack, effectRouterBlock, effectRouterCancelBlock, effectRouterForward, effectRouterGoTo, effectRouterIsPattern, effectRouterIsPatternSuccess, effectRouterPush, effectRouterReload, effectRouterReplace, effectRouterState, generatePath, historyChange, matchPath, pickSameStore };