perf(rpc): reuse required primitive schemas by message (#35072)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-28 22:52:51 -07:00
committed by GitHub
co-authored by m4air
parent 1c33f515de
commit fe1e179cde
4 changed files with 75 additions and 35 deletions
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { requiredNumber, requiredString, requiredStringAllowingEmpty } from './rpc-param-primitives'
describe('required RPC primitive schemas', () => {
it.each([requiredString, requiredStringAllowingEmpty, requiredNumber])(
'reuses schemas for the same message without conflating different messages',
(factory) => {
const first = factory('Missing id')
expect(factory('Missing id') === first).toBe(true)
expect(factory('Missing name') === first).toBe(false)
}
)
it('keeps each factory distinct for an identical message', () => {
const strict = requiredString('Missing value')
const empty = requiredStringAllowingEmpty('Missing value')
const number = requiredNumber('Missing value')
expect(strict.safeParse('').success).toBe(false)
expect(empty.parse('')).toBe('')
expect(empty.safeParse(1).success).toBe(false)
expect(number.parse(1)).toBe(1)
expect(number.safeParse('1').success).toBe(false)
})
})
+27 -17
View File
@@ -1,4 +1,8 @@
import { z } from 'zod'
import { memoizeStringResult } from '../string-result-cache'
// ~300 declarations repeat ~170 messages; zod schemas are immutable, so equal messages can share one instance.
const MAX_CACHED_VALIDATION_MESSAGES = 256
// Preserve legacy nonnumeric/nonfinite limits as absent. Zod v4 needs outer optional(): one hidden by pipe
// still makes object keys required.
@@ -55,22 +59,28 @@ export const BrowserTarget = z.object({
page: OptionalString
})
export function requiredString(message: string) {
return z
.unknown()
.transform((value) => (typeof value === 'string' ? value : ''))
.pipe(z.string().min(1, message))
}
export const requiredString = memoizeStringResult(
(message: string) =>
z
.unknown()
.transform((value) => (typeof value === 'string' ? value : ''))
.pipe(z.string().min(1, message)),
MAX_CACHED_VALIDATION_MESSAGES
)
export function requiredStringAllowingEmpty(message: string) {
return z.unknown().refine((value): value is string => typeof value === 'string', { message })
}
export const requiredStringAllowingEmpty = memoizeStringResult(
(message: string) =>
z.unknown().refine((value): value is string => typeof value === 'string', { message }),
MAX_CACHED_VALIDATION_MESSAGES
)
export function requiredNumber(message: string) {
return z
.unknown()
.transform((value) =>
typeof value === 'number' && Number.isFinite(value) ? value : Number.NaN
)
.pipe(z.number().refine((v) => Number.isFinite(v), { message }))
}
export const requiredNumber = memoizeStringResult(
(message: string) =>
z
.unknown()
.transform((value) =>
typeof value === 'number' && Number.isFinite(value) ? value : Number.NaN
)
.pipe(z.number().refine((v) => Number.isFinite(v), { message })),
MAX_CACHED_VALIDATION_MESSAGES
)
+22
View File
@@ -0,0 +1,22 @@
export function memoizeStringResult<T>(
compute: (key: string) => T,
maxEntries: number
): (key: string) => T {
// Box results so undefined and null are cache hits too.
const cache = new Map<string, { value: T }>()
return (key: string): T => {
const cached = cache.get(key)
if (cached) {
return cached.value
}
const value = compute(key)
if (cache.size >= maxEntries) {
const oldest = cache.keys().next()
if (!oldest.done) {
cache.delete(oldest.value)
}
}
cache.set(key, { value })
return value
}
}
@@ -1,4 +1,5 @@
/** Pure title classifiers can reuse verdicts across unrelated store writes; changed titles become new keys, so no invalidation signal is needed. */
import { memoizeStringResult } from './string-result-cache'
/** Bound growth across session length while keeping the live pane/title working set cached. */
const MAX_MEMOIZED_TITLES = 1024
@@ -6,22 +7,5 @@ const MAX_MEMOIZED_TITLES = 1024
export function memoizeTitleClassification<T>(
classify: (title: string) => T
): (title: string) => T {
// Boxed values so `undefined`/`null` verdicts are still cache hits.
const cache = new Map<string, { value: T }>()
return (title: string): T => {
const cached = cache.get(title)
if (cached) {
return cached.value
}
const value = classify(title)
// FIFO evicts superseded title frames first; they are least likely to be requested again.
if (cache.size >= MAX_MEMOIZED_TITLES) {
const oldest = cache.keys().next()
if (!oldest.done) {
cache.delete(oldest.value)
}
}
cache.set(title, { value })
return value
}
return memoizeStringResult(classify, MAX_MEMOIZED_TITLES)
}