Skip to content

Commit b5d19f1

Browse files
committed
perf: reduce allocations in encode, normalize, compare, and url-converter
- Reuse shared URLSearchParams instance in encodeQualifierParam instead of allocating per call - Cache compiled wildcard regexes in matchWildcard via Map - Return empty array constant instead of Object.entries({}) for undefined qualifiers - Move URL type support arrays to module-level Set constants
1 parent c8e0b5e commit b5d19f1

4 files changed

Lines changed: 72 additions & 62 deletions

File tree

src/compare.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,29 @@ function toCanonicalString(input: PurlInput): string {
2828
return input.toString()
2929
}
3030

31+
/**
32+
* Cache for compiled wildcard regexes to avoid recompilation on repeated calls.
33+
*/
34+
const wildcardRegexCache = new Map<string, RegExp>()
35+
3136
/**
3237
* Simple wildcard matcher for PURL components.
3338
* Supports * (match any chars), ? (match single char), ** (match anything including empty).
3439
* Designed for version strings and package names, not file paths.
3540
*/
3641
function matchWildcard(pattern: string, value: string): boolean {
37-
// Convert glob pattern to regex
38-
// Escape regex special chars except * and ?
39-
let regexPattern = pattern
40-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
41-
.replace(/\*/g, '.*')
42-
.replace(/\?/g, '.')
43-
44-
// Anchor to start and end
45-
regexPattern = `^${regexPattern}$`
46-
47-
const regex = new RegExp(regexPattern)
42+
let regex = wildcardRegexCache.get(pattern)
43+
if (regex === undefined) {
44+
// Convert glob pattern to regex
45+
// Escape regex special chars except * and ?
46+
const regexPattern = pattern
47+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
48+
.replace(/\*/g, '.*')
49+
.replace(/\?/g, '.')
50+
51+
regex = new RegExp(`^${regexPattern}$`)
52+
wildcardRegexCache.set(pattern, regex)
53+
}
4854
return regex.test(value)
4955
}
5056

src/encode.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Provides special handling for names, namespaces, versions, qualifiers, and subpaths.
44
*/
55
import {
6+
REUSED_SEARCH_PARAMS,
67
REUSED_SEARCH_PARAMS_KEY,
78
REUSED_SEARCH_PARAMS_OFFSET,
89
} from './constants.js'
@@ -41,13 +42,12 @@ function encodeQualifierParam(param: unknown): string {
4142
const value = prepareValueForSearchParams(param)
4243
// Use URLSearchParams#set to preserve plus signs
4344
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
44-
// Use a local instance to avoid mutation issues in concurrent scenarios
45-
const searchParams = new URLSearchParams()
46-
searchParams.set(REUSED_SEARCH_PARAMS_KEY, value)
45+
// Reuse shared instance — JS is single-threaded so no concurrent mutation issues
46+
REUSED_SEARCH_PARAMS.set(REUSED_SEARCH_PARAMS_KEY, value)
4747
// Param key and value are encoded with `percentEncodeSet` of
4848
// 'application/x-www-form-urlencoded' and `spaceAsPlus` of `true`
4949
// https://url.spec.whatwg.org/#urlencoded-serializing
50-
const search = searchParams.toString()
50+
const search = REUSED_SEARCH_PARAMS.toString()
5151
return normalizeSearchParamsEncoding(
5252
search.slice(REUSED_SEARCH_PARAMS_OFFSET),
5353
)

src/normalize.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import { isObject } from './objects.js'
66
import { isBlank } from './strings.js'
77

8+
const EMPTY_ENTRIES: Iterable<[string, string]> = []
9+
810
import type { QualifiersObject } from './purl-component.js'
911

1012
/**
@@ -146,7 +148,7 @@ function qualifiersToEntries(
146148
}
147149
return typeof rawQualifiers === 'string'
148150
? new URLSearchParams(rawQualifiers).entries()
149-
: Object.entries({})
151+
: EMPTY_ENTRIES
150152
}
151153

152154
/**

src/url-converter.ts

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,52 @@ export interface DownloadUrl {
6565
* const downloadUrl = UrlConverter.toDownloadUrl(purl)
6666
* ```
6767
*/
68+
const DOWNLOAD_URL_TYPES = new Set([
69+
'cargo',
70+
'composer',
71+
'conda',
72+
'gem',
73+
'golang',
74+
'hex',
75+
'maven',
76+
'npm',
77+
'nuget',
78+
'pub',
79+
'pypi',
80+
])
81+
82+
const REPOSITORY_URL_TYPES = new Set([
83+
'bioconductor',
84+
'bitbucket',
85+
'cargo',
86+
'chrome',
87+
'clojars',
88+
'cocoapods',
89+
'composer',
90+
'conan',
91+
'conda',
92+
'cpan',
93+
'deno',
94+
'docker',
95+
'elm',
96+
'gem',
97+
'github',
98+
'gitlab',
99+
'golang',
100+
'hackage',
101+
'hex',
102+
'homebrew',
103+
'huggingface',
104+
'luarocks',
105+
'maven',
106+
'npm',
107+
'nuget',
108+
'pub',
109+
'pypi',
110+
'swift',
111+
'vscode',
112+
])
113+
68114
export class UrlConverter {
69115
/**
70116
* Get all available URLs for a PackageURL.
@@ -89,20 +135,7 @@ export class UrlConverter {
89135
* conversion logic implemented.
90136
*/
91137
static supportsDownloadUrl(type: string): boolean {
92-
const supportedTypes = [
93-
'cargo',
94-
'composer',
95-
'conda',
96-
'gem',
97-
'golang',
98-
'hex',
99-
'maven',
100-
'npm',
101-
'nuget',
102-
'pub',
103-
'pypi',
104-
]
105-
return supportedTypes.includes(type)
138+
return DOWNLOAD_URL_TYPES.has(type)
106139
}
107140

108141
/**
@@ -112,38 +145,7 @@ export class UrlConverter {
112145
* conversion logic implemented.
113146
*/
114147
static supportsRepositoryUrl(type: string): boolean {
115-
const supportedTypes = [
116-
'bioconductor',
117-
'bitbucket',
118-
'cargo',
119-
'chrome',
120-
'clojars',
121-
'cocoapods',
122-
'composer',
123-
'conan',
124-
'conda',
125-
'cpan',
126-
'deno',
127-
'docker',
128-
'elm',
129-
'gem',
130-
'github',
131-
'gitlab',
132-
'golang',
133-
'hackage',
134-
'hex',
135-
'homebrew',
136-
'huggingface',
137-
'luarocks',
138-
'maven',
139-
'npm',
140-
'nuget',
141-
'pub',
142-
'pypi',
143-
'swift',
144-
'vscode',
145-
]
146-
return supportedTypes.includes(type)
148+
return REPOSITORY_URL_TYPES.has(type)
147149
}
148150

149151
/**

0 commit comments

Comments
 (0)