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
JavaScript
'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);