From 5e79c54a278f69e3b59ddc8c556b931824ae7a25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:46:55 +0000 Subject: [PATCH 1/2] Initial plan From 2c48cf620239df03df262d6d437985dc776ac4ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:58:47 +0000 Subject: [PATCH 2/2] revert marginal optimizations that hurt readability while keeping significant performance wins Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- src/core/_internal.ts | 7 ++-- src/query.ts | 11 +++---- src/util/_hash.ts | 10 +++--- src/util/_internal.ts | 75 +++++++++---------------------------------- 4 files changed, 27 insertions(+), 76 deletions(-) diff --git a/src/core/_internal.ts b/src/core/_internal.ts index 0c245b59..4ca85154 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -203,8 +203,7 @@ export class Context { ): Context { const ctx = new Context(); // ensure all operator types are initialized - for (const type of Object.keys(ops)) { - const operators = (ops as Record>)[type]; + for (const [type, operators] of Object.entries(ops)) { if (ctx.#operators[type] && operators) { // direct assignment since operators are empty at init time ctx.#operators[type] = { ...operators }; @@ -302,7 +301,7 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { // we check and process them in that order. // // if expr begins only a single "$", then it is a path to a field on the object. - if (isString(expr) && expr.length > 0 && expr.charCodeAt(0) === 0x24 /*$*/) { + if (isString(expr) && expr.length > 0 && expr[0] === "$") { // we return redact variables as literals if (expr === "$$KEEP" || expr === "$$PRUNE" || expr === "$$DESCEND") return expr; @@ -330,7 +329,7 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { break; } expr = dotIdx === -1 ? "" : expr.substring(dotIdx + 1); - } else if (prefix.length >= 2 && prefix.charCodeAt(1) === 0x24 /*$*/) { + } else if (prefix.length >= 2 && prefix[1] === "$") { // handle user-defined variables ctx = Object.assign( {}, diff --git a/src/query.ts b/src/query.ts index 4e5e4eab..51d8ab74 100644 --- a/src/query.ts +++ b/src/query.ts @@ -52,10 +52,8 @@ export class Query { ); const whereOperator: { field?: string; expr?: Any } = {}; - const conditions = Object.keys(this.#condition); - for (let ci = 0; ci < conditions.length; ci++) { - const field = conditions[ci]; - const expr = (this.#condition as AnyObject)[field]; + const conditions = Object.entries(this.#condition); + for (const [field, expr] of conditions) { if ("$where" === field) { assert( this.#options.scriptEnabled, @@ -68,9 +66,8 @@ export class Query { // normalize expression assert(!isOperator(field), `unknown top level operator: ${field}`); const normalized = normalize(expr) as AnyObject; - const normKeys = Object.keys(normalized); - for (let ni = 0; ni < normKeys.length; ni++) { - this.processOperator(field, normKeys[ni], normalized[normKeys[ni]]); + for (const [operator, val] of Object.entries(normalized)) { + this.processOperator(field, operator, val); } } diff --git a/src/util/_hash.ts b/src/util/_hash.ts index 43917ffe..1c7f8ec9 100644 --- a/src/util/_hash.ts +++ b/src/util/_hash.ts @@ -131,13 +131,11 @@ function hashObject( if (seen.has(obj)) return Tag.Cycle; seen.add(obj); - const keys = Object.keys(obj); - // sort in-place to avoid extra allocation (keys array is already a fresh copy) - keys.sort(); + const keys = Object.keys(obj).sort(); let h = hashString(obj?.constructor?.name); - for (let i = 0; i < keys.length; i++) { - h = mix(h, hashString(keys[i])); - h = mix(h, internalHash(obj[keys[i]], seen)); + for (const k of keys) { + h = mix(h, hashString(k)); + h = mix(h, internalHash(obj[k], seen)); } seen.delete(obj); diff --git a/src/util/_internal.ts b/src/util/_internal.ts index 1353e03b..fada5c5e 100644 --- a/src/util/_internal.ts +++ b/src/util/_internal.ts @@ -21,15 +21,9 @@ export const isPrimitive = (v: Any): boolean => /** Scalar types provided by the JS runtime. Includes primitives, RegExp, and Date */ const isScalar = (v: Any) => isPrimitive(v) || isDate(v) || isRegExp(v); -/** Check if a string contains only digits (equivalent to /^\d+$/ but faster). */ -const isDigitStr = (s: string): boolean => { - if (s.length === 0) return false; - for (let i = 0; i < s.length; i++) { - const c = s.charCodeAt(i); - if (c < 48 || c > 57) return false; - } - return true; -}; +/** Check if a string contains only digits. */ +const DIGIT_RE = /^\d+$/; +const isDigitStr = (s: string): boolean => DIGIT_RE.test(s); /** MongoDB sort comparison order. https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order */ const SORT_ORDER: Record = { @@ -323,14 +317,9 @@ export function assert(condition: Any, msg: string): void { export function typeOf(v: Any): string { if (v === null) return "null"; const t = typeof v; - // fast path for primitives - avoid hash lookup - if (t === "number") return "number"; - if (t === "string") return "string"; - if (t === "boolean") return "boolean"; - if (t === "function") return "function"; - if (t === "symbol") return "symbol"; - if (t === "undefined") return "undefined"; - // object types + // primitives + if (t !== "object" && SORT_ORDER[t]) return t; + // fast path for common types if (isArray(v)) return "array"; if (isDate(v)) return "date"; if (isRegExp(v)) return "regexp"; @@ -366,13 +355,8 @@ export const isEmpty = (x: Any): boolean => /** ensure a value is an array or wrapped within one. */ export const ensureArray = (x: T | T[]): T[] => (isArray(x) ? x : [x]); -export const has = (obj: object, ...props: string[]): boolean => { - if (!obj) return false; - for (let i = 0; i < props.length; i++) { - if (!Object.prototype.hasOwnProperty.call(obj, props[i])) return false; - } - return true; -}; +export const has = (obj: object, ...props: string[]): boolean => + !!obj && props.every(p => Object.prototype.hasOwnProperty.call(obj, p)); const isTypedArray = (v: Any): v is ArrayBuffer => typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(v); @@ -714,15 +698,15 @@ export function walk( fn: (_o: AnyObject, _k: string) => void, options?: WalkOptions ): void { - const sep = selector.indexOf("."); - const key = sep === -1 ? selector : selector.substring(0, sep); + const names = selector.split("."); + const key = names[0]; + const next = names.slice(1).join("."); - if (sep === -1) { + if (names.length === 1) { if (isObject(obj) || (isArray(obj) && isDigitStr(key))) { fn(obj, key); } } else { - const next = selector.substring(sep + 1); // force the rest of the graph while traversing if (options?.buildGraph && isNil(obj[key])) { obj[key] = {}; @@ -733,9 +717,7 @@ export function walk( // nothing more to do if (!item) return; // we peek to see if next key is an array index. - const nextSep = next.indexOf("."); - const nextKey = nextSep === -1 ? next : next.substring(0, nextSep); - const isNextArrayIndex = isDigitStr(nextKey); + const isNextArrayIndex = !!(names.length > 1 && isDigitStr(names[1])); // if we have an array value but the next key is not an index and the 'descendArray' option is set, // we walk each item in the array separately. This allows for handling traversing keys for objects // nested within an array. @@ -797,25 +779,8 @@ export function removeValue( * This is cheap and safe to do since keys beginning with '$' should be reserved for internal use. * @param {String} name */ -export const isOperator = (name: string): boolean => { - if (!name || name.length < 2 || name.charCodeAt(0) !== 0x24 /*$*/) { - return false; - } - for (let i = 1; i < name.length; i++) { - const c = name.charCodeAt(i); - if ( - !( - (c >= 97 && c <= 122) || // a-z - (c >= 65 && c <= 90) || // A-Z - (c >= 48 && c <= 57) || // 0-9 - c === 95 // _ - ) - ) { - return false; - } - } - return true; -}; +export const isOperator = (name: string): boolean => + !!name && name[0] === "$" && /^\$[a-zA-Z0-9_]+$/.test(name); /** * Simplify expression for easy evaluation with query operators map @@ -830,15 +795,7 @@ export function normalize(expr: Any): Any { // normalize object expression. using ObjectLike handles custom types if (isObjectLike(expr)) { // no valid query operator found, so we do simple comparison - let hasOp = false; - const keys = Object.keys(expr as AnyObject); - for (let i = 0; i < keys.length; i++) { - if (isOperator(keys[i])) { - hasOp = true; - break; - } - } - if (!hasOp) return { $eq: expr }; + if (!Object.keys(expr as AnyObject).some(isOperator)) return { $eq: expr }; // ensure valid regex if (isObject(expr) && has(expr, "$regex")) { const newExpr = { ...expr };