UNPKG

firetruss

Version:

Advanced data sync layer for Firebase and Vue.js

113 lines (98 loc) 3.33 kB
import LruCache from './LruCache.js'; import _ from 'lodash'; const pathSegments = new LruCache(1000); const pathMatchers = {}; const maxNumPathMatchers = 1000; export function escapeKey(key) { if (!key) return key; return key.toString().replace(/[\\.$#[\]/]/g, function(char) { return '\\' + char.charCodeAt(0).toString(16); }); } export function unescapeKey(key) { if (!key) return key; return key.toString().replace(/\\[0-9a-f]{2}/gi, function(code) { return String.fromCharCode(parseInt(code.slice(1), 16)); }); } export function escapeKeys(object) { // isExtensible check avoids trying to escape references to Firetruss internals. if (!(_.isObject(object) && Object.isExtensible(object))) return object; let result = object; for (const key in object) { if (!object.hasOwnProperty(key)) continue; const value = object[key]; const escapedKey = escapeKey(key); const escapedValue = escapeKeys(value); if (escapedKey !== key || escapedValue !== value) { if (result === object) result = _.clone(object); result[escapedKey] = escapedValue; if (result[key] === value) delete result[key]; } } return result; } export function joinPath() { const segments = []; for (let segment of arguments) { if (!_.isString(segment)) segment = '' + segment; if (segment.charAt(0) === '/') segments.splice(0, segments.length); segments.push(segment); } if (segments[0] === '/') segments[0] = ''; return segments.join('/'); } export function splitPath(path, leaveSegmentsEscaped) { const key = (leaveSegmentsEscaped ? 'esc:' : '') + path; let segments = pathSegments.get(key); if (!segments) { segments = path.split('/'); if (!leaveSegmentsEscaped) segments = _.map(segments, unescapeKey); pathSegments.set(key, segments); } return segments; } class PathMatcher { constructor(pattern) { this.variables = []; const prefixMatch = _.endsWith(pattern, '/$*'); if (prefixMatch) pattern = pattern.slice(0, -3); const pathTemplate = pattern.replace(/\/\$[^/]*/g, match => { if (match.length > 1) this.variables.push(match.slice(1)); return '\u0001'; }); Object.freeze(this.variables); if (/[.$#[\]]|\\(?![0-9a-f][0-9a-f])/i.test(pathTemplate)) { throw new Error('Path pattern has unescaped keys: ' + pattern); } this._regex = new RegExp( // eslint-disable-next-line no-control-regex '^' + pathTemplate.replace(/\u0001/g, '/([^/]+)') + (prefixMatch ? '($|/)' : '$')); } match(path) { this._regex.lastIndex = 0; const match = this._regex.exec(path); if (!match) return; const bindings = {}; for (let i = 0; i < this.variables.length; i++) { bindings[this.variables[i]] = unescapeKey(match[i + 1]); } return bindings; } test(path) { return this._regex.test(path); } toString() { return this._regex.toString(); } } export function makePathMatcher(pattern) { let matcher = pathMatchers[pattern]; if (!matcher) { matcher = new PathMatcher(pattern); // Minimal pseudo-LRU behavior, since we don't expect to actually fill up the cache. if (_.size(pathMatchers) === maxNumPathMatchers) delete pathMatchers[_.keys(pathMatchers)[0]]; pathMatchers[pattern] = matcher; } return matcher; }