From aa31c4d18b507978f874dc3b51b1bfb3aaa50716 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:03:30 +0000 Subject: [PATCH 1/7] Initial plan From 7b0c2e0c5f647c181e95d0078a8c66d7cff02970 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:12:49 +0000 Subject: [PATCH 2/7] perf: optimize hot paths in expression evaluation, query compilation, resolve, hash, and utilities - Replace regex in isOperator() with charCode-based validation - Use Set for SYSTEM_VARS lookup instead of Array.includes() - Avoid unnecessary string splits in computeExpression() - Use Object.keys() instead of Object.entries() in computeExpression - Optimize query compilation with Set-based top-level op check - Add fast path for 2-segment resolve (e.g. "address.city") - Optimize walk() to use indexOf+substring instead of split+slice+join - Replace for-of loops with indexed for loops in isEqual and hashObject - Optimize has() to use for-loop instead of Array.every - Optimize typeOf() with direct string comparisons - Optimize Context constructor to use literal object instead of Object.fromEntries - Cache OpType values array to avoid repeated Object.values() calls Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- src/core/_internal.ts | 69 +++++++++++++++++++-------------- src/query.ts | 18 +++++---- src/util/_hash.ts | 10 +++-- src/util/_internal.ts | 88 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 127 insertions(+), 58 deletions(-) diff --git a/src/core/_internal.ts b/src/core/_internal.ts index 064b4d39..262d1555 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -169,17 +169,27 @@ export enum OpType { WINDOW = "window" } +// cached values to avoid repeated Object.values() calls +const OP_TYPE_VALUES: OpType[] = Object.values(OpType); + /** * The `Context` class is a utility for managing and organizing operators of various types. * It provides methods to initialize, merge, and retrieve operators, as well as add specific * types of operators to the context. */ export class Context { - #operators: Record> = Object.fromEntries( - Object.values(OpType).map(k => [k, {}]) - ); - - private constructor() {} + #operators: Record>; + + private constructor() { + this.#operators = { + [OpType.ACCUMULATOR]: {}, + [OpType.EXPRESSION]: {}, + [OpType.PIPELINE]: {}, + [OpType.PROJECTION]: {}, + [OpType.QUERY]: {}, + [OpType.WINDOW]: {} + }; + } static init( ops: { @@ -193,7 +203,8 @@ export class Context { ): Context { const ctx = new Context(); // ensure all operator types are initialized - for (const [type, operators] of Object.entries(ops)) { + for (const type of Object.keys(ops)) { + const operators = (ops as Record>)[type]; if (ctx.#operators[type] && operators) { // direct assignment since operators are empty at init time ctx.#operators[type] = { ...operators }; @@ -208,7 +219,7 @@ export class Context { if (ctx.length === 1) return ctx[0]; const newCtx = new Context(); for (const context of ctx) { - for (const type of Object.values(OpType)) { + for (const type of OP_TYPE_VALUES) { newCtx.addOps(type, context.#operators[type]); } } @@ -282,8 +293,7 @@ export function evalExpr(obj: Any, expr: Any, options: Options): Any { return computeExpression(obj, expr, copts); } -const SYSTEM_VARS = ["$$ROOT", "$$CURRENT", "$$REMOVE", "$$NOW"] as const; -type SystemVar = (typeof SYSTEM_VARS)[number]; +const SYSTEM_VARS = new Set(["$$ROOT", "$$CURRENT", "$$REMOVE", "$$NOW"]); /** Computes the value of the expr given for the object. */ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { @@ -292,7 +302,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[0] === "$") { + if (isString(expr) && expr.length > 0 && expr.charCodeAt(0) === 0x24 /*$*/) { // we return redact variables as literals if (expr === "$$KEEP" || expr === "$$PRUNE" || expr === "$$DESCEND") return expr; @@ -301,11 +311,12 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { let ctx = options.local.root; // handle selectors with explicit prefix - const arr = expr.split("."); - if (SYSTEM_VARS.includes(arr[0] as SystemVar)) { + const dotIdx = expr.indexOf("."); + const prefix = dotIdx === -1 ? expr : expr.substring(0, dotIdx); + if (SYSTEM_VARS.has(prefix)) { // set 'root' only the first time it is required to be used for all subsequent calls // if it already available on the options, it will be used - switch (arr[0] as SystemVar) { + switch (prefix) { case "$$ROOT": break; case "$$CURRENT": @@ -318,8 +329,8 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { ctx = new Date(options.now); break; } - expr = expr.slice(arr[0].length + 1); // +1 for '.' - } else if (arr[0].slice(0, 2) === "$$") { + expr = dotIdx === -1 ? "" : expr.substring(dotIdx + 1); + } else if (prefix.length >= 2 && prefix.charCodeAt(1) === 0x24 /*$*/) { // handle user-defined variables ctx = Object.assign( {}, @@ -331,12 +342,12 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { options?.local?.variables ); // the variable name - const name = arr[0].slice(2); + const name = prefix.substring(2); assert(has(ctx as AnyObject, name), `Use of undefined variable: ${name}`); - expr = expr.slice(2); + expr = expr.substring(2); } else { // 'expr' is a path to a field on the object. - expr = expr.slice(1); + expr = expr.substring(1); } return expr === "" ? ctx : resolve(ctx as ArrayOrObject, expr as string); @@ -348,18 +359,18 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { } if (isObject(expr)) { + const keys = Object.keys(expr); + // if object represents an operator expression, there should only be a single key + if (keys.length > 0 && isOperator(keys[0])) { + assert( + keys.length === 1, + `Expression must contain a single operator. got [${keys.join(",")}]` + ); + return computeOperator(obj, expr[keys[0]], keys[0], options); + } const result: AnyObject = {}; - const elems = Object.entries(expr); - for (const [key, val] of elems) { - // if object represents an operator expression, there should only be a single key - if (isOperator(key)) { - assert( - elems.length === 1, - `Expression must contain a single operator. got [${Object.keys(expr).join(",")}]` - ); - return computeOperator(obj, val, key, options); - } - result[key] = computeExpression(obj, val, options); + for (let i = 0; i < keys.length; i++) { + result[keys[i]] = computeExpression(obj, expr[keys[i]], options); } return result; } diff --git a/src/query.ts b/src/query.ts index 5b5f7765..4e5e4eab 100644 --- a/src/query.ts +++ b/src/query.ts @@ -12,7 +12,7 @@ import type { } from "./types"; import { assert, cloneDeep, isObject, isOperator, normalize } from "./util"; -const TOP_LEVEL_RE = /^\$(and|or|nor|expr|jsonSchema)$/; +const TOP_LEVEL_OPS = new Set(["$and", "$or", "$nor", "$expr", "$jsonSchema"]); /** * Represents a query object used to filter and match documents based on specified criteria. @@ -52,23 +52,25 @@ export class Query { ); const whereOperator: { field?: string; expr?: Any } = {}; - const conditions = Object.entries(this.#condition); - for (const [field, expr] of conditions) { + 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]; if ("$where" === field) { assert( this.#options.scriptEnabled, "$where operator requires 'scriptEnabled' option to be true." ); Object.assign(whereOperator, { field: field, expr: expr }); - } else if (TOP_LEVEL_RE.test(field)) { + } else if (TOP_LEVEL_OPS.has(field)) { this.processOperator(field, field, expr); } else { // normalize expression assert(!isOperator(field), `unknown top level operator: ${field}`); - for (const [operator, val] of Object.entries( - normalize(expr) as AnyObject - )) { - this.processOperator(field, operator, val); + 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]]); } } diff --git a/src/util/_hash.ts b/src/util/_hash.ts index 1c7f8ec9..43917ffe 100644 --- a/src/util/_hash.ts +++ b/src/util/_hash.ts @@ -131,11 +131,13 @@ function hashObject( if (seen.has(obj)) return Tag.Cycle; seen.add(obj); - const keys = Object.keys(obj).sort(); + const keys = Object.keys(obj); + // sort in-place to avoid extra allocation (keys array is already a fresh copy) + keys.sort(); let h = hashString(obj?.constructor?.name); - for (const k of keys) { - h = mix(h, hashString(k)); - h = mix(h, internalHash(obj[k], seen)); + for (let i = 0; i < keys.length; i++) { + h = mix(h, hashString(keys[i])); + h = mix(h, internalHash(obj[keys[i]], seen)); } seen.delete(obj); diff --git a/src/util/_internal.ts b/src/util/_internal.ts index 3d248d59..9ae87369 100644 --- a/src/util/_internal.ts +++ b/src/util/_internal.ts @@ -184,7 +184,11 @@ export function isEqual(a: Any, b: Any): boolean { if (isRegExp(a)) return isRegExp(b) && a.source === b.source && a.flags === b.flags; if (isArray(a) && isArray(b)) { - return a.length === b.length && a.every((v, i) => isEqual(v, b[i])); + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!isEqual(a[i], b[i])) return false; + } + return true; } if (a?.constructor !== Object && hasCustomString(a)) { return (a as Str)?.toString() === (b as Str)?.toString(); @@ -195,7 +199,15 @@ export function isEqual(a: Any, b: Any): boolean { const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) return false; - return keysA.every(k => has(objB, k) && isEqual(objA[k], objB[k])); + for (let i = 0; i < keysA.length; i++) { + const k = keysA[i]; + if ( + !Object.prototype.hasOwnProperty.call(objB, k) || + !isEqual(objA[k], objB[k]) + ) + return false; + } + return true; } /** @@ -311,9 +323,14 @@ export function assert(condition: Any, msg: string): void { export function typeOf(v: Any): string { if (v === null) return "null"; const t = typeof v; - // primitives - if (t !== "object" && SORT_ORDER[t]) return t; - // fast path for common types + // 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 if (isArray(v)) return "array"; if (isDate(v)) return "date"; if (isRegExp(v)) return "regexp"; @@ -349,8 +366,13 @@ 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 => - !!obj && props.every(p => Object.prototype.hasOwnProperty.call(obj, p)); +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; +}; const isTypedArray = (v: Any): v is ArrayBuffer => typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(v); @@ -527,12 +549,23 @@ export function resolve( ): Any { if (isScalar(obj)) return obj; - // fast path for simple single-segment selectors on non-array objects (e.g., "active", "age") const path = pathArray || selector.split("."); + + // fast path for simple single-segment selectors on non-array objects (e.g., "active", "age") if (path.length === 1 && !isArray(obj)) { return getValue(obj, path[0]); } + // fast path for 2-segment selectors on plain objects (e.g., "address.city") + if (path.length === 2 && !isArray(obj)) { + const first = getValue(obj, path[0]); + if (first === undefined) return undefined; + if (!isArray(first)) { + return getValue(first as ArrayOrObject, path[1]); + } + // first is an array; fall through to general case + } + let depth = 0; function resolve2(o: ArrayOrObject, path: string[]): Any { let value: Any = o; @@ -667,15 +700,15 @@ export function walk( fn: (_o: AnyObject, _k: string) => void, options?: WalkOptions ): void { - const names = selector.split("."); - const key = names[0]; - const next = names.slice(1).join("."); + const sep = selector.indexOf("."); + const key = sep === -1 ? selector : selector.substring(0, sep); - if (names.length === 1) { + if (sep === -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] = {}; @@ -686,7 +719,9 @@ export function walk( // nothing more to do if (!item) return; // we peek to see if next key is an array index. - const isNextArrayIndex = !!(names.length > 1 && isDigitStr(names[1])); + const nextSep = next.indexOf("."); + const nextKey = nextSep === -1 ? next : next.substring(0, nextSep); + const isNextArrayIndex = isDigitStr(nextKey); // 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. @@ -748,8 +783,26 @@ 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 => - !!name && name[0] === "$" && /^\$[a-zA-Z0-9_]+$/.test(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); + // a-z: 97-122, A-Z: 65-90, 0-9: 48-57, _: 95 + if ( + !( + (c >= 97 && c <= 122) || + (c >= 65 && c <= 90) || + (c >= 48 && c <= 57) || + c === 95 + ) + ) { + return false; + } + } + return true; +}; /** * Simplify expression for easy evaluation with query operators map @@ -765,8 +818,9 @@ export function normalize(expr: Any): Any { if (isObjectLike(expr)) { // no valid query operator found, so we do simple comparison let hasOp = false; - for (const k in expr as AnyObject) { - if (Object.prototype.hasOwnProperty.call(expr, k) && isOperator(k)) { + const keys = Object.keys(expr as AnyObject); + for (let i = 0; i < keys.length; i++) { + if (isOperator(keys[i])) { hasOp = true; break; } From b97bfc5273b3050ea987ff4bff198853ad2caca3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:14:45 +0000 Subject: [PATCH 3/7] fix: address code review feedback - handle null in resolve fast path, remove redundant check Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- src/core/_internal.ts | 2 +- src/util/_internal.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/_internal.ts b/src/core/_internal.ts index 262d1555..0c245b59 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -361,7 +361,7 @@ function computeExpression(obj: Any, expr: Any, options: ComputeOptions): Any { if (isObject(expr)) { const keys = Object.keys(expr); // if object represents an operator expression, there should only be a single key - if (keys.length > 0 && isOperator(keys[0])) { + if (isOperator(keys[0])) { assert( keys.length === 1, `Expression must contain a single operator. got [${keys.join(",")}]` diff --git a/src/util/_internal.ts b/src/util/_internal.ts index 9ae87369..849e8cdc 100644 --- a/src/util/_internal.ts +++ b/src/util/_internal.ts @@ -559,7 +559,7 @@ export function resolve( // fast path for 2-segment selectors on plain objects (e.g., "address.city") if (path.length === 2 && !isArray(obj)) { const first = getValue(obj, path[0]); - if (first === undefined) return undefined; + if (first == null) return undefined; if (!isArray(first)) { return getValue(first as ArrayOrObject, path[1]); } From 58881896b31ab8519a8909f57049809de2c3b651 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:07:10 +0000 Subject: [PATCH 4/7] perf: use native Set methods (intersection, difference, isSubsetOf) for primitive-only arrays Add dual-path optimizations to set operations: native Set methods for primitive arrays (O(1) lookup) with HashMap fallback for complex objects. Applies to intersection(), unique(), $setDifference, $setIsSubset, $setEquals. Add esnext.collection to tsconfig lib for Set method type support. Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- src/operators/expression/set/setDifference.ts | 8 +++++++- src/operators/expression/set/setEquals.ts | 12 +++++++++++- src/operators/expression/set/setIsSubset.ts | 8 +++++++- src/util/_internal.ts | 14 ++++++++++++++ src/util/index.ts | 1 + tsconfig.json | 1 + 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/operators/expression/set/setDifference.ts b/src/operators/expression/set/setDifference.ts index 96f3fc9f..15d9943d 100644 --- a/src/operators/expression/set/setDifference.ts +++ b/src/operators/expression/set/setDifference.ts @@ -1,6 +1,6 @@ import { evalExpr } from "../../../core/_internal"; import { Any, AnyObject, Options } from "../../../types"; -import { assert, HashMap, isArray, isNil } from "../../../util"; +import { assert, HashMap, isArray, isNil, isPrimitive } from "../../../util"; import { errExpectArray } from "../_internal"; const OP = "$setDifference"; @@ -23,6 +23,12 @@ export const $setDifference = ( ok &&= isArray(v); } if (!ok) return errExpectArray(foe, `${OP} arguments`); + + // fast path: use native Set.difference() when all elements are primitives + if (args[0].every(isPrimitive) && args[1].every(isPrimitive)) { + return Array.from(new Set(args[0]).difference(new Set(args[1]))); + } + const m = HashMap.init(); args[0].forEach(v => m.set(v, true)); args[1].forEach(v => m.delete(v)); diff --git a/src/operators/expression/set/setEquals.ts b/src/operators/expression/set/setEquals.ts index 9aee125f..260db0c1 100644 --- a/src/operators/expression/set/setEquals.ts +++ b/src/operators/expression/set/setEquals.ts @@ -1,6 +1,6 @@ import { evalExpr } from "../../../core/_internal"; import { Any, AnyObject, Options } from "../../../types"; -import { assert, HashMap, isArray } from "../../../util"; +import { assert, HashMap, isArray, isPrimitive } from "../../../util"; import { errExpectArray } from "../_internal"; /** @@ -17,6 +17,16 @@ export const $setEquals = ( // all args must be arrays (not null/undefined) if (!args.every(isArray)) return errExpectArray(foe, "$setEquals arguments"); + // fast path: use native Set when all elements across all arrays are primitives + if (args.every(arr => arr.every(isPrimitive))) { + const first = new Set(args[0]); + for (let i = 1; i < args.length; i++) { + const other = new Set(args[i]); + if (first.size !== other.size || !first.isSubsetOf(other)) return false; + } + return true; + } + // store a unique number for each unique item. repeated values may be overriden but that's okay. const map = HashMap.init(); args[0].every((v, i) => map.set(v, i)); diff --git a/src/operators/expression/set/setIsSubset.ts b/src/operators/expression/set/setIsSubset.ts index a63c3b8e..4d3f0ba7 100644 --- a/src/operators/expression/set/setIsSubset.ts +++ b/src/operators/expression/set/setIsSubset.ts @@ -1,6 +1,6 @@ import { evalExpr } from "../../../core/_internal"; import { Any, AnyObject, Options } from "../../../types"; -import { assert, HashMap, isArray } from "../../../util"; +import { assert, HashMap, isArray, isPrimitive } from "../../../util"; import { errExpectArray } from "../_internal"; const OP = "$setIsSubset"; @@ -20,6 +20,12 @@ export const $setIsSubset = ( return errExpectArray(options.failOnError, `${OP} arguments`); const [first, second] = args; + + // fast path: use native Set.isSubsetOf() when all elements are primitives + if (first.every(isPrimitive) && second.every(isPrimitive)) { + return new Set(first).isSubsetOf(new Set(second)); + } + const map = HashMap.init(); const set = new Set(); diff --git a/src/util/_internal.ts b/src/util/_internal.ts index 849e8cdc..8a51f941 100644 --- a/src/util/_internal.ts +++ b/src/util/_internal.ts @@ -421,6 +421,16 @@ export function intersection(input: T[][]): T[] { if (input.some(arr => arr.length === 0)) return []; if (input.length === 1) return input[0].slice(); + // fast path: use native Set.intersection() when all elements are primitives + if (input.every(arr => arr.every(isPrimitive))) { + let result = new Set(input[0]); + for (let i = 1; i < input.length; i++) { + result = result.intersection(new Set(input[i])); + if (result.size === 0) return []; + } + return Array.from(result) as T[]; + } + const vmaps = [HashMap.init(), HashMap.init()]; // start with last array to ensure stableness. input[input.length - 1].forEach(v => vmaps[0].set(v, true)); @@ -465,6 +475,10 @@ export function flatten(xs: Any[], depth = 1): Any[] { * @return {Array} */ export function unique(input: T[]): T[] { + // fast path: use native Set for primitives + if (input.every(isPrimitive)) { + return Array.from(new Set(input)); + } const m = HashMap.init(); input.forEach(v => m.set(v, true)); return Array.from(m.keys()); diff --git a/src/util/index.ts b/src/util/index.ts index 3d42b14a..dc302331 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -22,6 +22,7 @@ export { isObject, isObjectLike, isOperator, + isPrimitive, isRegExp, isString, isSymbol, diff --git a/tsconfig.json b/tsconfig.json index ff356d67..ee4c4ab4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es2020", + "lib": ["es2020", "esnext.collection"], "noEmit": true, "module": "preserve", "moduleResolution": "bundler", From 68510250584e9031c058e39903dc32234f612698 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:11:41 +0000 Subject: [PATCH 5/7] docs: add inline comments explaining charCode ranges in isOperator() Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- src/util/_internal.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/util/_internal.ts b/src/util/_internal.ts index 8a51f941..1353e03b 100644 --- a/src/util/_internal.ts +++ b/src/util/_internal.ts @@ -803,13 +803,12 @@ export const isOperator = (name: string): boolean => { } for (let i = 1; i < name.length; i++) { const c = name.charCodeAt(i); - // a-z: 97-122, A-Z: 65-90, 0-9: 48-57, _: 95 if ( !( - (c >= 97 && c <= 122) || - (c >= 65 && c <= 90) || - (c >= 48 && c <= 57) || - c === 95 + (c >= 97 && c <= 122) || // a-z + (c >= 65 && c <= 90) || // A-Z + (c >= 48 && c <= 57) || // 0-9 + c === 95 // _ ) ) { return false; From ec717cd97e0aedf09940585f978aac0aaeda5599 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:47:16 +0000 Subject: [PATCH 6/7] ci: remove Node.js v18/v20 from CI matrix, use v22+ for native Set method support Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4873539..6b7d363b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - node-version: [18.x, 20.x] + node-version: [22.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: From edad84285be7f7b50a57faf6e249d47434559d39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:31:26 +0000 Subject: [PATCH 7/7] refactor: internalize Set method types - remove esnext.collection from tsconfig lib Co-authored-by: pubkey <8926560+pubkey@users.noreply.github.com> --- src/set-methods.d.ts | 16 ++++++++++++++++ tsconfig.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/set-methods.d.ts diff --git a/src/set-methods.d.ts b/src/set-methods.d.ts new file mode 100644 index 00000000..a9f78ea1 --- /dev/null +++ b/src/set-methods.d.ts @@ -0,0 +1,16 @@ +/** + * Type declarations for native Set methods (ES2025). + * Internalized here so consumers of the library do not need to add + * "esnext.collection" to their own tsconfig lib. + */ +interface Set { + intersection(other: ReadonlySetLike): Set; + difference(other: ReadonlySetLike): Set; + isSubsetOf(other: ReadonlySetLike): boolean; +} + +interface ReadonlySetLike { + has(value: T): boolean; + keys(): IterableIterator; + readonly size: number; +} diff --git a/tsconfig.json b/tsconfig.json index ee4c4ab4..4991c550 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2020", - "lib": ["es2020", "esnext.collection"], + "lib": ["es2020"], "noEmit": true, "module": "preserve", "moduleResolution": "bundler",