axiodb
Version:
The Pure JavaScript Alternative to SQLite. Embedded NoSQL database for Node.js with MongoDB-style queries, zero native dependencies, built-in InMemoryCache, and web GUI. Perfect for desktop apps, CLI tools, and embedded systems. No compilation, no platfor
379 lines • 17.5 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
const worker_threads_1 = require("worker_threads");
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const workerPath = path_1.default.resolve(__dirname, "../engine/node", "WorkerForSearch.engine.js");
class Searcher {
constructor(arr, isUpdated = false) {
this.isUpdated = false;
this.compiledQueries = null;
this.data = arr;
this.isUpdated = isUpdated;
}
/**
* Pre-compiles query operators for faster matching (regex, $in sets, etc.)
*/
compileQuery(query) {
const compiled = [];
for (const key of Object.keys(query)) {
if (key === '$or' || key === '$and')
continue;
const queryValue = query[key];
if (typeof queryValue === "object" && queryValue !== null) {
// Handle $regex - pre-compile RegExp
if ("$regex" in queryValue) {
const pattern = queryValue["$regex"];
const flags = queryValue["$options"] || "i";
compiled.push({
key,
type: 'regex',
regex: pattern instanceof RegExp ? pattern : new RegExp(pattern, flags)
});
}
// Handle $in - convert to Set for O(1) lookup
else if ("$in" in queryValue && Array.isArray(queryValue["$in"])) {
compiled.push({
key,
type: 'in_set',
inSet: new Set(queryValue["$in"])
});
}
// Handle range operators
else if ("$gt" in queryValue || "$lt" in queryValue || "$gte" in queryValue || "$lte" in queryValue) {
compiled.push({
key,
type: 'range',
rangeOps: {
$gt: queryValue["$gt"],
$lt: queryValue["$lt"],
$gte: queryValue["$gte"],
$lte: queryValue["$lte"]
}
});
}
// Handle $eq
else if ("$eq" in queryValue) {
compiled.push({
key,
type: 'eq',
eqValue: queryValue["$eq"]
});
}
}
else {
// Direct value comparison
compiled.push({
key,
type: 'direct',
directValue: queryValue
});
}
}
return compiled;
}
/**
* Fast matching using pre-compiled query.
* Note: The item passed here should already be the actual data object to compare against.
* The caller (find method) handles extracting via additionalFiled if needed.
*/
matchWithCompiled(item, compiled) {
if (!item)
return false;
for (const cq of compiled) {
const itemValue = item[cq.key];
switch (cq.type) {
case 'regex':
if (itemValue === undefined || itemValue === null)
return false;
if (!cq.regex.test(String(itemValue)))
return false;
break;
case 'in_set':
if (!cq.inSet.has(itemValue))
return false;
break;
case 'range': {
if (typeof itemValue !== 'number')
return false;
const ops = cq.rangeOps;
if (ops.$gt !== undefined && !(itemValue > ops.$gt))
return false;
if (ops.$lt !== undefined && !(itemValue < ops.$lt))
return false;
if (ops.$gte !== undefined && !(itemValue >= ops.$gte))
return false;
if (ops.$lte !== undefined && !(itemValue <= ops.$lte))
return false;
break;
}
case 'eq':
if (itemValue !== cq.eqValue)
return false;
break;
case 'direct':
if (itemValue !== cq.directValue)
return false;
break;
}
}
return true;
}
/**
* Finds items in the data array that match the given query.
* Uses optimized search strategies based on data size.
* Note: InMemoryCache at the Reader layer already handles query result caching.
*
* @param query - The query object containing conditions to match against items.
* @param additionalFiled - Optional field to extract from each item for matching.
* @param findOne - If true, stops after finding the first match (early exit)
* @param limit - Optional limit for early termination (returns when limit reached)
* @returns {Promise<any[]>} - A promise that resolves to an array of matching items.
*/
find(query_1, additionalFiled_1) {
return __awaiter(this, arguments, void 0, function* (query, additionalFiled, findOne = false, limit) {
// Pre-compile query for faster matching
const hasLogicalOps = '$or' in query || '$and' in query;
const compiled = hasLogicalOps ? null : this.compileQuery(query);
const effectiveLimit = findOne ? 1 : limit;
// For small datasets, findOne, or when limit is small - use optimized linear search
if (findOne || (effectiveLimit && effectiveLimit < 1000) || this.data.length < 10000) {
const result = [];
for (let i = 0; i < this.data.length; i++) {
const rawItem = this.data[i];
const item = additionalFiled ? rawItem[additionalFiled] : rawItem;
if (item === undefined || item === null)
continue;
// Use compiled query for faster matching when no logical operators
const matches = compiled
? this.matchWithCompiled(item, compiled)
: Searcher.matchesQuery(item, query, this.isUpdated);
if (matches) {
result.push(rawItem);
// Early termination when limit reached
if (effectiveLimit && result.length >= effectiveLimit) {
return result;
}
}
}
return result;
}
// Parallel search for large datasets with complex queries
const numWorkers = Math.min(os_1.default.cpus().length, Math.max(1, Math.ceil(this.data.length / 1000)));
const chunkSize = Math.ceil(this.data.length / numWorkers);
const tasks = [];
for (let i = 0; i < numWorkers; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, this.data.length);
const dataChunk = this.data.slice(start, end);
tasks.push(new Promise((resolve, reject) => {
const worker = new worker_threads_1.Worker(workerPath, {
workerData: {
chunk: dataChunk,
query,
isUpdated: this.isUpdated,
additionalFiled,
},
});
worker.on("message", resolve);
worker.on("error", reject);
worker.on("exit", (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with code ${code}`));
});
}));
}
const results = yield Promise.all(tasks);
return results.flat(); // Combine all matches
});
}
/**
* Matches an item against a query object.
* Supports MongoDB-like operators and logical operators ($or, $and).
*
* @param item - The item to match against the query.
* @param query - The query object containing conditions.
* @returns {boolean} - True if the item matches the query, false otherwise.
*/
static matchesQuery(item, query, isUpdated = false) {
// Handle root-level $or
if ("$or" in query && Array.isArray(query.$or)) {
const { $or } = query, rest = __rest(query, ["$or"]);
const orMatch = $or.some((sub) => this.matchesQuery(item, sub));
const restMatch = Object.keys(rest).length
? this.matchesQuery(item, rest)
: true;
return orMatch && restMatch;
}
// Handle root-level $and
if ("$and" in query && Array.isArray(query.$and)) {
const { $and } = query, rest = __rest(query, ["$and"]);
const andMatch = query.$and.every((sub) => this.matchesQuery(item, sub));
const restMatch = Object.keys(rest).length
? this.matchesQuery(item, rest)
: true;
return andMatch && restMatch;
}
// Handle root-level $nor (negated OR - none of the conditions should match)
if ("$nor" in query && Array.isArray(query.$nor)) {
const { $nor } = query, rest = __rest(query, ["$nor"]);
const norMatch = !$nor.some((sub) => this.matchesQuery(item, sub));
const restMatch = Object.keys(rest).length
? this.matchesQuery(item, rest)
: true;
return norMatch && restMatch;
}
// Two-pointer optimized query matching
const queryKeys = Object.keys(query);
const queryLength = queryKeys.length;
// Early return for empty query
if (queryLength === 0)
return true;
for (let i = 0; i < queryLength; i++) {
const key = queryKeys[i];
const queryValue = query[key];
const itemValue = isUpdated == true ? item.data[key] : item[key];
// If queryValue is an object (for operators)
if (typeof queryValue === "object" && queryValue !== null) {
// Handle MongoDB-like operators with optimized checks
if ("$regex" in queryValue) {
// Support both pre-compiled RegExp and string patterns
const pattern = queryValue["$regex"];
const regex = pattern instanceof RegExp
? pattern
: new RegExp(pattern, queryValue["$options"] || "i");
if (!regex.test(itemValue))
return false;
continue;
}
// Handle range operators - check all that are present (don't use continue to allow combined $gte + $lte)
const hasRangeOp = "$gt" in queryValue || "$lt" in queryValue || "$gte" in queryValue || "$lte" in queryValue;
if (hasRangeOp) {
if (typeof itemValue !== "number")
return false;
if ("$gt" in queryValue && !(itemValue > queryValue["$gt"]))
return false;
if ("$lt" in queryValue && !(itemValue < queryValue["$lt"]))
return false;
if ("$gte" in queryValue && !(itemValue >= queryValue["$gte"]))
return false;
if ("$lte" in queryValue && !(itemValue <= queryValue["$lte"]))
return false;
continue;
}
if ("$in" in queryValue && Array.isArray(queryValue["$in"])) {
// Use Set for O(1) lookup on large arrays
const inArray = queryValue["$in"];
if (inArray.length > 10) {
const inSet = new Set(inArray);
if (!inSet.has(itemValue))
return false;
}
else {
if (!inArray.includes(itemValue))
return false;
}
continue;
}
// $exists - Check if field exists in document
if ("$exists" in queryValue) {
const shouldExist = queryValue["$exists"];
const fieldExists = itemValue !== undefined && itemValue !== null;
if (shouldExist && !fieldExists)
return false;
if (!shouldExist && fieldExists)
return false;
continue;
}
// $elemMatch - Match array elements with nested conditions
if ("$elemMatch" in queryValue) {
if (!Array.isArray(itemValue))
return false;
const elemQuery = queryValue["$elemMatch"];
const hasMatch = itemValue.some(elem => {
return this.matchesQuery(elem, elemQuery, false);
});
if (!hasMatch)
return false;
continue;
}
// $not - Negation of query condition
if ("$not" in queryValue) {
const negatedQuery = queryValue["$not"];
const tempDoc = { [key]: itemValue };
const tempQuery = { [key]: negatedQuery };
if (this.matchesQuery(isUpdated ? { data: tempDoc } : tempDoc, tempQuery, isUpdated)) {
return false;
}
continue;
}
// $type - Check value type
if ("$type" in queryValue) {
const expectedType = queryValue["$type"];
let actualType = itemValue === null ? 'null'
: Array.isArray(itemValue) ? 'array'
: typeof itemValue;
if (actualType !== expectedType)
return false;
continue;
}
// $size - Check array length
if ("$size" in queryValue) {
if (!Array.isArray(itemValue))
return false;
if (itemValue.length !== queryValue["$size"])
return false;
continue;
}
// $all - Array must contain all specified values
if ("$all" in queryValue && Array.isArray(queryValue["$all"])) {
if (!Array.isArray(itemValue))
return false;
const requiredValues = queryValue["$all"];
const itemSet = new Set(itemValue);
const hasAll = requiredValues.every(val => itemSet.has(val));
if (!hasAll)
return false;
continue;
}
if ("$eq" in queryValue) {
if (itemValue !== queryValue["$eq"])
return false;
continue;
}
}
// Direct equality check with early failure
if (itemValue !== queryValue) {
return false;
}
}
return true;
}
}
exports.default = Searcher;
//# sourceMappingURL=Searcher.utils.js.map