UNPKG

cs-element

Version:

Advanced reactive data management library with state machines, blueprints, persistence, compression, networking, and multithreading support

1,491 lines (1,480 loc) 838 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var eventemitter3 = require('eventemitter3'); var JSPath = require('jspath'); var Joi = require('joi'); var events = require('events'); var Ajv = require('ajv'); var addFormats = require('ajv-formats'); var react = require('react'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var JSPath__namespace = /*#__PURE__*/_interopNamespaceDefault(JSPath); var Joi__namespace = /*#__PURE__*/_interopNamespaceDefault(Joi); /** * Основные типы и интерфейсы для библиотеки CSElement */ /** * Типы событий элемента */ exports.ElementEventType = void 0; (function (ElementEventType) { ElementEventType["ElementAdded"] = "element:added"; ElementEventType["ElementRemoved"] = "element:removed"; ElementEventType["DataChanged"] = "data:changed"; ElementEventType["OwnerChanged"] = "owner:changed"; ElementEventType["Locked"] = "locked"; ElementEventType["Unlocked"] = "unlocked"; })(exports.ElementEventType || (exports.ElementEventType = {})); /** * Генерация уникального идентификатора */ function generateId() { return `cs_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Глубокое клонирование объекта */ function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { const cloneArr = []; for (let i = 0; i < obj.length; i++) { cloneArr[i] = deepClone(obj[i]); } return cloneArr; } if (obj instanceof Map) { const cloneMap = new Map(); obj.forEach((value, key) => { cloneMap.set(key, deepClone(value)); }); return cloneMap; } if (obj instanceof Set) { const cloneSet = new Set(); obj.forEach((value) => { cloneSet.add(deepClone(value)); }); return cloneSet; } const cloneObj = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key]); } } return cloneObj; } /** * Задержка выполнения */ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Универсальный движок запросов для CSElement * Поддерживает CSS-селекторы, XPath, объектные селекторы, индексирование и оптимизацию запросов */ /** * Типы селекторов */ var SelectorType; (function (SelectorType) { SelectorType["CSS"] = "css"; SelectorType["XPATH"] = "xpath"; SelectorType["OBJECT"] = "object"; })(SelectorType || (SelectorType = {})); /** * Расширенный движок запросов */ class QueryEngine { /** * Выполняет поиск элементов по универсальному селектору */ static query(root, selector) { const startTime = performance.now(); const selectorString = this.getSelectorString(selector); // Создаем ключ кэша, который включает ID корневого элемента const cacheKey = `${root.id}:${selectorString}`; // Проверяем кэш const cached = this.getCachedResult(cacheKey); if (cached) { this.stats.cacheHits++; this.stats.totalQueries++; // Кэшированные запросы тоже считаются как запросы // Обновляем частоту селекторов для кэшированных запросов const currentCount = this.selectorFrequency.get(selectorString) || 0; this.selectorFrequency.set(selectorString, currentCount + 1); // Обновляем топ селекторов this.stats.mostFrequentSelectors = Array.from(this.selectorFrequency.entries()) .map(([sel, count]) => ({ selector: sel, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); return cached; } let result; if (typeof selector === 'string') { // Автоматически определяем тип селектора if (selector.startsWith('//') || selector.startsWith('/')) { result = this.queryXPath(root, selector); } else { result = this.queryCSS(root, selector); } } else { switch (selector.type) { case SelectorType.CSS: result = this.queryCSS(root, selector.selector); break; case SelectorType.XPATH: result = this.queryXPath(root, selector.expression); break; case SelectorType.OBJECT: result = this.queryObject(root, selector.selector); break; default: throw new Error(`Неподдерживаемый тип селектора: ${selector.type}`); } } // Обновляем статистику const executionTime = performance.now() - startTime; this.updateStats(selectorString, executionTime); // Кэшируем результат с учетом корневого элемента this.cacheResult(cacheKey, result); return result; } /** * Находит первый элемент по селектору */ static queryOne(root, selector) { const results = this.query(root, selector); return results.length > 0 ? results[0] : null; } /** * Выполняет поиск по CSS-селектору */ static queryCSS(root, selector) { const parsed = this.parseCSS(selector); return this.executeCSS(root, parsed); } /** * Выполняет поиск по XPath */ static queryXPath(root, expression) { const context = this.createXPathContext(root); return this.evaluateXPath(context, expression); } /** * Выполняет поиск по объектному селектору */ static queryObject(root, selector) { return this.queryBySelector(root, selector); } /** * Включить/выключить индексирование */ static setIndexingEnabled(enabled) { // В данной реализации индексирование всегда включено // Этот метод оставлен для совместимости API console.log(`Индексирование ${enabled ? 'включено' : 'выключено'}`); } /** * Создает или обновляет индекс для элемента */ static buildIndex(root) { const index = { byName: new Map(), byId: new Map(), byClass: new Map(), byAttribute: new Map(), byDepth: new Map(), all: new Set() }; this.traverseAndIndex(root, index, 0); this.indices.set(root, index); } /** * Получает индекс для элемента */ static getIndex(root) { let index = this.indices.get(root); if (!index) { this.buildIndex(root); index = this.indices.get(root); } return index; } /** * Очищает кэш запросов */ static clearCache() { this.queryCache.clear(); } /** * Получает статистику запросов */ static getStats() { return { ...this.stats }; } /** * Сбрасывает статистику */ static resetStats() { this.stats = { totalQueries: 0, cacheHits: 0, averageExecutionTime: 0, slowestQuery: { selector: '', time: 0 }, mostFrequentSelectors: [] }; this.selectorFrequency.clear(); } /** * Приватные методы */ static getSelectorString(selector) { if (typeof selector === 'string') { return selector; } switch (selector.type) { case SelectorType.CSS: return `css:${selector.selector}`; case SelectorType.XPATH: return `xpath:${selector.expression}`; case SelectorType.OBJECT: return `object:${JSON.stringify(selector.selector)}`; default: return 'unknown'; } } static getCachedResult(selector) { const cached = this.queryCache.get(selector); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.result; } if (cached) { this.queryCache.delete(selector); } return null; } static cacheResult(selector, result) { this.queryCache.set(selector, { result: [...result], timestamp: Date.now() }); } static updateStats(selector, executionTime) { this.stats.totalQueries++; // Обновляем среднее время выполнения this.stats.averageExecutionTime = (this.stats.averageExecutionTime * (this.stats.totalQueries - 1) + executionTime) / this.stats.totalQueries; // Обновляем самый медленный запрос if (executionTime > this.stats.slowestQuery.time) { this.stats.slowestQuery = { selector, time: executionTime }; } // Обновляем частоту селекторов const currentCount = this.selectorFrequency.get(selector) || 0; this.selectorFrequency.set(selector, currentCount + 1); // Обновляем топ селекторов this.stats.mostFrequentSelectors = Array.from(this.selectorFrequency.entries()) .map(([sel, count]) => ({ selector: sel, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); } static parseCSS(selector) { // Улучшенный CSS парсер const result = {}; // Убираем лишние пробелы и разбиваем по комбинаторам const trimmed = selector.trim(); // Измененный regex, чтобы поддерживать селекторы, начинающиеся с комбинатора const combinatorMatch = trimmed.match(/^([>+~])?\s*(.*)$/); if (!combinatorMatch) { this.parseSelectorPart(trimmed, result); return result; } const [, combinator, rest] = combinatorMatch; if (combinator) { result.combinator = combinator; const nextParts = rest.split(/\s*([>+~])\s*/); result.next = this.parseCSS(nextParts.join(' ')); return result; } // Старая логика для селекторов, не начинающихся с комбинатора const simpleMatch = trimmed.match(/^([^>+~\s]+)(?:\s*([>+~]|\s+)\s*(.+))?$/); if (!simpleMatch) { this.parseSelectorPart(trimmed, result); return result; } const [, currentPart, simpleCombinator, simpleRest] = simpleMatch; this.parseSelectorPart(currentPart, result); if (simpleCombinator && simpleRest) { result.combinator = simpleCombinator.trim() === '' ? ' ' : simpleCombinator.trim(); result.next = this.parseCSS(simpleRest); } return result; } static parseSelectorPart(part, result) { let remaining = part; // Парсим элемент, ID, классы, атрибуты и псевдо-классы while (remaining) { if (remaining.startsWith('#')) { // ID селектор const match = remaining.match(/^#([^.:\[]+)/); if (match) { result.id = match[1]; remaining = remaining.substring(match[0].length); } else { break; } } else if (remaining.startsWith('.')) { // Class селектор const match = remaining.match(/^\.([^.:\[#]+)/); if (match) { if (!result.classes) result.classes = []; result.classes.push(match[1]); remaining = remaining.substring(match[0].length); } else { break; } } else if (remaining.startsWith('[')) { // Атрибутный селектор const match = remaining.match(/^\[([^=\]]+)(?:(=|~=|\|=|\^=|\$=|\*=)"?([^"\]]+)"?)?\]/); if (match) { if (!result.attributes) result.attributes = []; result.attributes.push({ name: match[1], operator: match[2], value: match[3] }); remaining = remaining.substring(match[0].length); } else { break; } } else if (remaining.startsWith(':')) { // Псевдо-класс const match = remaining.match(/^:([^:(]+)(?:\(([^)]+)\))?/); if (match) { if (!result.pseudoClasses) result.pseudoClasses = []; result.pseudoClasses.push({ name: match[1], argument: match[2] }); remaining = remaining.substring(match[0].length); } else { break; } } else { // Имя элемента (должно быть в начале) const match = remaining.match(/^([a-zA-Z][a-zA-Z0-9-_]*)/); if (match && !result.element && !result.id && !result.classes && !result.attributes && !result.pseudoClasses) { result.element = match[1]; remaining = remaining.substring(match[0].length); } else { break; } } } } static executeCSS(root, parsed) { // Phase 1: Find elements matching the first part of the selector. // If the selector part is empty (e.g., '> .foo'), the initial context is just the root. const isPartEmpty = !parsed.element && !parsed.id && !parsed.classes && !parsed.attributes && !parsed.pseudoClasses; const initialMatches = isPartEmpty ? [root] : this.findDescendantsAndSelf(root, parsed); // If there's no combinator, we're done. if (!parsed.combinator || !parsed.next) { return initialMatches; } // Phase 2: Apply combinator to find the next set of elements. const finalMatches = new Set(); for (const element of initialMatches) { switch (parsed.combinator) { case '>': const children = element.getAllElements(); for (const child of children) { if (this.matchesParsedSelector(child, parsed.next)) { finalMatches.add(child); } } break; case ' ': const descendants = this.findDescendantsAndSelf(element, parsed.next); // Exclude the element itself from its descendants descendants.forEach(d => { if (d.id !== element.id) finalMatches.add(d); }); break; case '+': const parentPlus = element.mainOwner; if (parentPlus) { const siblings = parentPlus.getAllElements(); const elementIndex = siblings.findIndex(el => el.id === element.id); if (elementIndex > -1 && elementIndex + 1 < siblings.length) { const nextSibling = siblings[elementIndex + 1]; if (this.matchesParsedSelector(nextSibling, parsed.next)) { finalMatches.add(nextSibling); } } } break; case '~': const parentTilde = element.mainOwner; if (parentTilde) { const siblings = parentTilde.getAllElements(); const elementIndex = siblings.findIndex(el => el.id === element.id); if (elementIndex > -1) { for (let i = elementIndex + 1; i < siblings.length; i++) { const nextSibling = siblings[i]; if (this.matchesParsedSelector(nextSibling, parsed.next)) { finalMatches.add(nextSibling); } } } } break; } } return Array.from(finalMatches); } // Helper to find all descendants (and self) that match a simple selector part static findDescendantsAndSelf(root, parsed) { const results = []; // Проверяем сам корневой элемент if (this.matchesParsedSelector(root, parsed)) { results.push(root); } // Рекурсивно ищем в потомках const searchInChildren = (element) => { const children = element.getAllElements(); for (const child of children) { if (this.matchesParsedSelector(child, parsed)) { results.push(child); } // Рекурсивно ищем в потомках searchInChildren(child); } }; searchInChildren(root); return results; } static findDescendantsByParsedSelector(root, parsed) { const results = []; // Рекурсивно ищем среди потомков, не включая сам root const searchInChildren = (element) => { const children = element.getAllElements(); for (const child of children) { if (this.matchesParsedSelector(child, parsed)) { results.push(child); } // Рекурсивно ищем в потомках searchInChildren(child); } }; searchInChildren(root); return results; } static matchesParsedSelector(element, parsed) { // Проверяем имя элемента if (parsed.element && element.name !== parsed.element) { return false; } // Проверяем ID if (parsed.id && element.getData('id') !== parsed.id) { return false; } // Проверяем классы if (parsed.classes) { const elementClasses = element.getData('class'); if (!elementClasses) return false; const classList = typeof elementClasses === 'string' ? elementClasses.split(' ') : []; for (const className of parsed.classes) { if (!classList.includes(className)) { return false; } } } // Проверяем атрибуты if (parsed.attributes) { for (const attr of parsed.attributes) { const value = element.getData(attr.name); // Если нет оператора, просто проверяем наличие атрибута if (!attr.operator) { if (value === undefined || value === null) { return false; } continue; } // Если есть оператор, проверяем значение if (!value) return false; if (attr.operator && attr.value) { switch (attr.operator) { case '=': if (value !== attr.value) return false; break; case '~=': if (!value.toString().split(' ').includes(attr.value)) return false; break; case '|=': if (!value.toString().startsWith(attr.value + '-') && value !== attr.value) return false; break; case '^=': if (!value.toString().startsWith(attr.value)) return false; break; case '$=': if (!value.toString().endsWith(attr.value)) return false; break; case '*=': if (!value.toString().includes(attr.value)) return false; break; } } } } // Проверяем псевдо-классы if (parsed.pseudoClasses) { for (const pseudo of parsed.pseudoClasses) { if (!this.matchesPseudoClass(element, pseudo)) { return false; } } } return true; } static matchesPseudoClass(element, pseudo) { switch (pseudo.name) { case 'first-child': return element.index === 0; case 'last-child': const parent = element.mainOwner; return parent ? element.index === parent.elementsCount() - 1 : true; case 'nth-child': if (pseudo.argument) { const n = parseInt(pseudo.argument); return element.index === n - 1; // CSS использует 1-based индексы } return false; case 'empty': return element.elementsCount() === 0; case 'not': // Реализация :not() селектора if (pseudo.argument) { const notParsed = this.parseCSS(pseudo.argument); return !this.matchesParsedSelector(element, notParsed); } return false; case 'has': // Реализация :has() селектора if (pseudo.argument) { const hasParsed = this.parseCSS(pseudo.argument); // Ищем только среди потомков, не включая сам элемент const descendants = this.findDescendantsByParsedSelector(element, hasParsed); return descendants.length > 0; } return false; default: return false; } } static createXPathContext(root) { // Этот метод больше не нужен, так как мы используем JSPath // который работает с JSON-объектами напрямую. return { root }; } static evaluateXPath(context, expression) { const rootElement = context.root; // 1. Создаем карту всех элементов для быстрого доступа по ID const elementMap = new Map(); const traverseAndMap = (element) => { elementMap.set(element.id, element); element.getAllElements().forEach(child => traverseAndMap(child)); }; traverseAndMap(rootElement); // 2. Преобразуем структуру в JSON const jsonObject = rootElement.toJSPathObject(); // 3. Применяем JSPath let results = JSPath__namespace.apply(expression, jsonObject); if (results === undefined) { results = []; } else if (!Array.isArray(results)) { results = [results]; } // 4. Сопоставляем результаты с реальными элементами CSElement const finalElements = []; const seenIds = new Set(); for (const res of results) { if (res && typeof res === 'object' && res.___id) { const elementId = res.___id; if (!seenIds.has(elementId)) { const element = elementMap.get(elementId); if (element) { finalElements.push(element); seenIds.add(elementId); } } } } return finalElements; } static traverseAndIndex(element, index, depth) { // Добавляем в общий индекс index.all.add(element); // Индексируем по имени if (element.name) { if (!index.byName.has(element.name)) { index.byName.set(element.name, new Set()); } index.byName.get(element.name).add(element); } // Индексируем по ID const id = element.getData('id'); if (id) { index.byId.set(id, element); } // Индексируем по классам const classes = element.getData('class'); if (classes) { const classList = typeof classes === 'string' ? classes.split(' ') : [classes]; for (const className of classList) { if (!index.byClass.has(className)) { index.byClass.set(className, new Set()); } index.byClass.get(className).add(element); } } // Индексируем по атрибутам const data = element.data; for (const [key, value] of data) { if (!index.byAttribute.has(key)) { index.byAttribute.set(key, new Map()); } const attrMap = index.byAttribute.get(key); if (!attrMap.has(value)) { attrMap.set(value, new Set()); } attrMap.get(value).add(element); } // Индексируем по глубине if (!index.byDepth.has(depth)) { index.byDepth.set(depth, new Set()); } index.byDepth.get(depth).add(element); // Рекурсивно обрабатываем дочерние элементы const children = element.getAllElements(); for (const child of children) { this.traverseAndIndex(child, index, depth + 1); } } // ===== Methods from old QueryEngine ===== /** * Выполняет поиск по объекту селектора */ static queryBySelector(root, selector) { const results = []; this.traverseAndMatchObject(root, selector, results); return results; } /** * Рекурсивно обходит дерево и собирает подходящие элементы для объектного селектора */ static traverseAndMatchObject(element, selector, results) { if (this.matchesObjectSelector(element, selector)) { results.push(element); } const children = element.getAllElements(); for (const child of children) { this.traverseAndMatchObject(child, selector, results); } } /** * Проверяет, соответствует ли элемент объектному селектору */ static matchesObjectSelector(element, selector) { if (selector.name !== undefined && element.name !== selector.name) { return false; } if (selector.index !== undefined && element.index !== selector.index) { return false; } if (selector.hasData) { for (const key of selector.hasData) { if (!element.getData(key)) { return false; } } } if (selector.depth !== undefined) { const depth = this.getElementDepth(element); if (typeof selector.depth === 'number') { if (depth !== selector.depth) return false; } else { const { min, max } = selector.depth; if (min !== undefined && depth < min) return false; if (max !== undefined && depth > max) return false; } } if (selector.custom && !selector.custom(element)) { return false; } return true; } /** * Вычисляет глубину элемента в дереве */ static getElementDepth(element) { let depth = 0; let current = element.mainOwner; while (current) { depth++; current = current.mainOwner; } return depth; } /** * Создает комплексный селектор для поиска элементов */ static createSelector() { return new SelectorBuilder(); } } QueryEngine.indices = new WeakMap(); QueryEngine.queryCache = new Map(); QueryEngine.cacheTimeout = 5000; // 5 секунд QueryEngine.stats = { totalQueries: 0, cacheHits: 0, averageExecutionTime: 0, slowestQuery: { selector: '', time: 0 }, mostFrequentSelectors: [] }; QueryEngine.selectorFrequency = new Map(); /** * Builder для создания сложных селекторов */ class SelectorBuilder { constructor() { this.selector = {}; } withName(name) { this.selector.name = name; return this; } withIndex(index) { this.selector.index = index; return this; } withData(...keys) { this.selector.hasData = [...(this.selector.hasData || []), ...keys]; return this; } withDepth(depth) { this.selector.depth = depth; return this; } withCustom(predicate) { const existing = this.selector.custom; if (existing) { this.selector.custom = (element) => existing(element) && predicate(element); } else { this.selector.custom = predicate; } return this; } build() { return { ...this.selector }; } } /** * Предопределенные селекторы */ const CommonSelectors = { byName: (name) => ({ name }), byIndex: (index) => ({ index }), leaves: () => ({ custom: (element) => element.elementsCount() === 0 }), roots: () => ({ custom: (element) => element.mainOwner === null }), withChildrenCount: (count) => ({ custom: (element) => element.elementsCount() === count }), atDepth: (depth) => ({ depth }), withDataType: (key, type) => ({ custom: (element) => typeof element.getData(key) === type }) }; /** * Реализация менеджера Live queries (реактивных запросов) */ class LiveQueryManagerImpl extends eventemitter3.EventEmitter { constructor() { super(); this.queries = new Map(); this.subscriptions = new Map(); this.debounceTimers = new Map(); this.updateBatches = new Map(); this.indexes = new Map(); this.CSElementClass = null; this.stats = this.createEmptyStats(); } setCSElementClass(cls) { this.CSElementClass = cls; } createLiveQuery(selector, options = {}, config = {}) { const id = generateId(); const query = { id, selector, options, config: { autoStart: true, debounce: 100, cache: true, cacheTTL: 5000, deep: false, batch: true, ...config }, results: [], active: false, lastUpdated: 0, updateCount: 0, watchedElements: new Set(), filters: [], transforms: [], sort: undefined, onResults: undefined, onError: undefined }; this.queries.set(id, query); this.subscriptions.set(id, new Map()); this.stats.totalQueries++; // Обновляем статистику селекторов this.updateSelectorStats(selector); // Автоматический запуск если включен if (query.config.autoStart) { this.start(id); } this.emit('query-created', { queryId: id, query }); this.emitEvent({ type: 'query-created', queryId: id, data: { selector, options, config }, timestamp: Date.now() }); return query; } start(queryId) { const query = this.queries.get(queryId); if (!query || query.active) { return; } query.active = true; this.stats.activeQueries++; // Выполняем первоначальный запрос this.executeQuery(queryId); // Настраиваем наблюдение за изменениями this.setupQueryWatching(query); this.emitEvent({ type: 'query-started', queryId, timestamp: Date.now() }); } stop(queryId) { const query = this.queries.get(queryId); if (!query || !query.active) { return; } query.active = false; this.stats.activeQueries--; // Останавливаем наблюдение this.stopQueryWatching(query); // Очищаем таймеры debounce const timer = this.debounceTimers.get(queryId); if (timer) { clearTimeout(timer); this.debounceTimers.delete(queryId); } this.emitEvent({ type: 'query-stopped', queryId, timestamp: Date.now() }); } getLiveQuery(queryId) { return this.queries.get(queryId); } getAllLiveQueries() { return Array.from(this.queries.values()); } removeLiveQuery(queryId) { const query = this.queries.get(queryId); if (!query) { return false; } // Останавливаем запрос if (query.active) { this.stop(queryId); } // Удаляем все подписки this.subscriptions.delete(queryId); // Удаляем запрос this.queries.delete(queryId); this.stats.totalQueries--; this.emitEvent({ type: 'query-removed', queryId, timestamp: Date.now() }); return true; } updateQuery(queryId) { const query = this.queries.get(queryId); if (!query || !query.active) { return; } if (query.config.debounce && query.config.debounce > 0) { // Используем debounce const existingTimer = this.debounceTimers.get(queryId); if (existingTimer) { clearTimeout(existingTimer); } const timer = setTimeout(() => { this.executeQuery(queryId); this.debounceTimers.delete(queryId); }, query.config.debounce); this.debounceTimers.set(queryId, timer); } else { // Немедленное обновление this.executeQuery(queryId); } } updateAllQueries() { for (const [queryId, query] of this.queries) { if (query.active) { this.updateQuery(queryId); } } } addFilter(queryId, filter) { const query = this.queries.get(queryId); if (query) { query.filters.push(filter); this.updateQuery(queryId); } } removeFilter(queryId, filterIndex) { const query = this.queries.get(queryId); if (query && filterIndex >= 0 && filterIndex < query.filters.length) { query.filters.splice(filterIndex, 1); this.updateQuery(queryId); } } addTransform(queryId, transform) { const query = this.queries.get(queryId); if (query) { query.transforms.push(transform); this.updateQuery(queryId); } } setSort(queryId, sort) { const query = this.queries.get(queryId); if (query) { query.sort = sort; this.updateQuery(queryId); } } subscribe(queryId, callback) { const subscriptionId = generateId(); const subscriptions = this.subscriptions.get(queryId); if (subscriptions) { const subscription = { id: subscriptionId, queryId, callback, active: true, createdAt: Date.now() }; subscriptions.set(subscriptionId, subscription); } return subscriptionId; } unsubscribe(queryId, subscriptionId) { const subscriptions = this.subscriptions.get(queryId); if (subscriptions) { return subscriptions.delete(subscriptionId); } return false; } getStats() { // Обновляем актуальную статистику this.updateStats(); return { ...this.stats }; } clear() { // Останавливаем все активные запросы for (const [queryId, query] of this.queries) { if (query.active) { this.stop(queryId); } } // Очищаем все данные this.queries.clear(); this.subscriptions.clear(); this.debounceTimers.clear(); this.updateBatches.clear(); this.indexes.clear(); // Сбрасываем статистику this.stats = this.createEmptyStats(); } notifyElementChange(element, _changeType) { // Находим все запросы, которые могут быть затронуты этим изменением const affectedQueries = this.findAffectedQueries(element); for (const queryId of affectedQueries) { this.updateQuery(queryId); } } notifyDataChange(element, key, _newValue, _oldValue) { // Находим запросы, которые используют этот ключ данных const affectedQueries = this.findQueriesUsingDataKey(element, key); for (const queryId of affectedQueries) { this.updateQuery(queryId); } } // Приватные методы async executeQuery(queryId) { const query = this.queries.get(queryId); if (!query) { return; } const startTime = Date.now(); try { // Выполняем базовый запрос let results = await this.performQuery(query); // Применяем фильтры for (const filter of query.filters) { results = results.filter(filter); } // Применяем трансформации for (const transform of query.transforms) { results = results.map(transform); } // Применяем сортировку if (query.sort) { results.sort(query.sort); } // Применяем лимит if (query.config.limit && query.config.limit > 0) { results = results.slice(0, query.config.limit); } // Обновляем результаты const oldResults = query.results; query.results = results; query.lastUpdated = Date.now(); query.updateCount++; // Обновляем статистику this.stats.totalUpdates++; const executionTime = Date.now() - startTime; this.updateExecutionTimeStats(executionTime); // Уведомляем подписчиков this.notifySubscribers(queryId, results, query); // Вызываем callback если установлен if (query.onResults) { query.onResults(results, query); } this.emitEvent({ type: 'query-updated', queryId, data: { results, oldResults, executionTime, changeCount: this.calculateChangeCount(oldResults, results) }, timestamp: Date.now() }); this.emitEvent({ type: 'results-changed', queryId, data: { results, executionTime }, timestamp: Date.now() }); } catch (error) { // Обработка ошибок if (query.onError) { query.onError(error, query); } this.emit('query-error', { queryId, error }); console.error(`Live query ${queryId} error:`, error); } } async performQuery(query) { if (!this.CSElementClass) { throw new Error('CSElementClass has not been set on LiveQueryManager.'); } try { const root = query.options?.root; if (!root) { // Fallback to searching all elements if no root is provided. const allElements = this.CSElementClass.getAllElements(); const results = []; allElements.forEach((el) => { results.push(...QueryEngine.query(el, query.selector)); }); return Array.from(new Set(results)); // Remove duplicates } // Если есть корневой элемент, ищем только в его контексте return QueryEngine.query(root, query.selector); } catch (error) { console.warn('Error executing query:', error); return []; } } setupQueryWatching(_query) { // Эта логика теперь полностью управляется через // вызовы notifyElementChange и notifyDataChange, // которые инициируют updateQuery. // Использование computed здесь было бы некорректным, // так как система реактивности не отслеживает // добавление/удаление дочерних элементов напрямую. } stopQueryWatching(_query) { // Останавливаем наблюдение за элементами // Это будет реализовано при интеграции с ReactivityManager } findAffectedQueries(element) { const affected = []; for (const [queryId, query] of this.queries) { if (query.active && this.queryAffectedByElement(query, element)) { affected.push(queryId); } } return affected; } findQueriesUsingDataKey(_element, key) { const affected = []; for (const [queryId, query] of this.queries) { if (query.active && this.queryUsesDataKey(query, key)) { affected.push(queryId); } } return affected; } queryAffectedByElement(query, element) { // Проверяем, может ли изменение элемента повлиять на результаты запроса // Это упрощенная логика, в реальности будет более сложная return query.watchedElements.has(element.id) || query.selector.includes(element.name) || query.selector.includes('*'); } queryUsesDataKey(query, key) { // Проверяем, использует ли запрос определенный ключ данных return query.selector.includes(`[${key}]`) || query.selector.includes(`data-${key}`) || query.selector.includes(`@${key}`); } notifySubscribers(queryId, results, query) { const subscriptions = this.subscriptions.get(queryId); if (!subscriptions) { return; } for (const subscription of subscriptions.values()) { if (subscription.active) { try { subscription.callback(results, query); } catch (error) { console.error(`Subscription callback error for query ${queryId}:`, error); } } } } calculateChangeCount(oldResults, newResults) { // Простой подсчет изменений if (oldResults.length !== newResults.length) { return Math.abs(oldResults.length - newResults.length); } let changes = 0; for (let i = 0; i < oldResults.length; i++) { if (oldResults[i] !== newResults[i]) { changes++; } } return changes; } updateSelectorStats(selector) { let selectorStats = this.stats.selectorStats.get(selector); if (!selectorStats) { selectorStats = { selector, queryCount: 0, averageResults: 0, lastUsed: Date.now(), totalExecutionTime: 0 }; this.stats.selectorStats.set(selector, selectorStats); } selectorStats.queryCount++; selectorStats.lastUsed = Date.now(); } updateExecutionTimeStats(executionTime) { const currentAverage = this.stats.averageQueryTime; const totalQueries = this.stats.totalUpdates; this.stats.averageQueryTime = (currentAverage * (totalQueries - 1) + executionTime) / totalQueries; } updateStats() { this.stats.watchedElements = 0; this.stats.memoryUsage = this.calculateMemoryUsage(); for (const query of this.queries.values()) { this.stats.watchedElements += query.watchedElements.size; } } calculateMemoryUsage() { // Упрощенный расчет использования памяти let usage = 0; for (const query of this.queries.values()) { usage += JSON.stringify(query.results).length; usage += query.selector.length; usage += query.watchedElements.size * 50; // Примерный размер ID элемента } return usage; } createEmptyStats() { return { totalQueries: 0, activeQueries: 0, totalUpdates: 0, averageQueryTime: 0, memoryUsage: 0, watchedElements: 0, selectorStats: new Map() }; } emitEvent(event) { this.emit('event', event); this.emit(event.type, event); } } // Builder для удобного создания Live queries class LiveQueryBuilderImpl { constructor(manager) { this._selector = ''; this._options = {}; this._config = {}; this._filters = []; this._transforms = []; this.manager = manager; } selector(selector) { this._selector = selector; return this; } options(options) { this._options = { ...this._options, ...options }; return this; } config(config) { this._config = { ...this._config, ...config }; return this; } filter(filter) { this._filters.push(filter); return this; } transform(transform) { this._transforms.push(transform); return this; } sort(sort) { this._sort = sort; return this; } limit(limit) { this._config.limit = limit; return this; } debounce(ms) { this._config.debounce = ms; return this; } cache(ttl) { this._config.cache = true; if (ttl !== undefined) { this._config.cacheTTL = ttl; } return this; } subscribe(callback) { this._onResults = callback; return this; } onError(callback) { this._onError = callback; return this; } start() { const query = this.build(); this.manager.start(query.id); return query; } build() { const query = this.manager.createLiveQuery(this._selector, this._options, { ...this._config, autoStart: false }); // Применяем фильтры, трансформации и сортировку for (const filter of this._filters) { this.manager.addFilter(query.id, filter); } for (const transform of this._transforms) { this.manager.addTransform(query.id, transform); } if (this._sort) { this.manager.setSort(query.id, this._sort); } // Устанавливаем callbacks if (this._onResults) { query.onResults = this._onResults; } if (this._onError) { query.onError = this._onError; } return query; } } /** * Асинхронная блокировка для обеспечения потокобезопасности */ class AsyncLock { constructor() { this._queue = []; this._locked = false; } /** * Получить блокировку */ async acquire() { return new Promise((resolve) => { if (!this._locked) { this._locked = true; resolve(); } else { this._queue.push(resolve); } }); } /** * Освободить блокировку */ release() { if (!this._locked) { throw new Error('Cannot release an unlocked lock'); } const next = this._queue.shift(); if (next) { next(); } else { this._locked = false; } } /** * Проверить заблокирована ли блокировка */ isLocked() { return this._locked; } /** * Выполнить функцию с блокировкой */ async withLock(fn) { await this.acquire(); try { return await fn(); } finally { this.release(); } } } /** * Расширенные типы валидации */ exports.ValidationType = void 0; (function (ValidationType) { ValidationType["REQUIRED"] = "required"; ValidationType["TYPE"] = "type"; ValidationType["RANGE"] = "range"; ValidationType["CUSTOM"] = "custom"; ValidationType["SCHEMA"] = "schema"; ValidationType["ASYNC"] = "async"; ValidationType["PATTERN"] = "pattern"; ValidationType["ENUM"] = "enum"; ValidationType["ARRAY"] = "array"; ValidationType["OBJECT"] = "object"; })(exports.ValidationType || (exports.ValidationType = {})); /** * Кэш схем валидации */ class ValidationSchemaCache { constructor() { this.cache = new Map(); this.maxSize = 100; } get(key) { return this.cache.get(key); } set(key, schema) { if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey) { this.cache.delete(firstKey);