mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
perf(rpc): reuse required primitive schemas by message (#35072)
Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user