From c09810b641cf3a1cd61d9e59ab3d23c73146fb45 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:47:22 -0700 Subject: [PATCH] perf(rpc): restore compiled Zod request schemas without override (#17374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(rpc): compile Zod request schemas lazily * chore(deps): pin zod 4.5.4 and except it from the release-age gate 4.5.4 is the first release fixing isRecursiveSchema (upstream 84e416f, #6500), which compile() calls on every schema — on 4.5.0 it fired .default() factories at compile time. Verified: compile-time factory calls 0 on 4.5.4, 1 on 4.5.0. --- package.json | 2 +- pnpm-lock.yaml | 26 ++++----- pnpm-workspace.yaml | 2 + .../rpc/dispatcher-request-parsing.test.ts | 54 +++++++++++++++++++ .../runtime/rpc/dispatcher-request-parsing.ts | 15 +++++- 5 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 src/main/runtime/rpc/dispatcher-request-parsing.test.ts diff --git a/package.json b/package.json index 115095a14b9..04623e54b33 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "tweetnacl": "^1.0.3", "ws": "^8.21.3", "yaml": "^2.8.4", - "zod": "~4.4.3" + "zod": "~4.5.4" }, "devDependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aee8c1fcc45..73be03378d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,8 +191,8 @@ importers: specifier: ^2.8.4 version: 2.8.4 zod: - specifier: ~4.4.3 - version: 4.4.3 + specifier: ~4.5.4 + version: 4.5.4 devDependencies: '@dnd-kit/core': specifier: ^6.3.1 @@ -217,7 +217,7 @@ importers: version: 3.2.0 '@stablyai/playwright-test': specifier: ^2.1.14 - version: 2.1.14(@playwright/test@1.59.1)(zod@4.4.3) + version: 2.1.14(@playwright/test@1.59.1)(zod@4.5.4) '@tailwindcss/vite': specifier: ^4.2.4 version: 4.2.4(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4)) @@ -6982,8 +6982,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.5.4: + resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} zustand@5.0.14: resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} @@ -8916,27 +8916,27 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@stablyai/playwright-base@2.1.14(@playwright/test@1.59.1)(zod@4.4.3)': + '@stablyai/playwright-base@2.1.14(@playwright/test@1.59.1)(zod@4.5.4)': dependencies: '@playwright/test': 1.59.1 jpeg-js: 0.4.4 p-retry: 4.6.2 pngjs: 7.0.0 optionalDependencies: - zod: 4.4.3 + zod: 4.5.4 - '@stablyai/playwright-test@2.1.14(@playwright/test@1.59.1)(zod@4.4.3)': + '@stablyai/playwright-test@2.1.14(@playwright/test@1.59.1)(zod@4.5.4)': dependencies: '@playwright/test': 1.59.1 - '@stablyai/playwright': 2.1.14(@playwright/test@1.59.1)(zod@4.4.3) - '@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.4.3) + '@stablyai/playwright': 2.1.14(@playwright/test@1.59.1)(zod@4.5.4) + '@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.5.4) transitivePeerDependencies: - zod - '@stablyai/playwright@2.1.14(@playwright/test@1.59.1)(zod@4.4.3)': + '@stablyai/playwright@2.1.14(@playwright/test@1.59.1)(zod@4.5.4)': dependencies: '@playwright/test': 1.59.1 - '@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.4.3) + '@stablyai/playwright-base': 2.1.14(@playwright/test@1.59.1)(zod@4.5.4) transitivePeerDependencies: - zod @@ -13787,7 +13787,7 @@ snapshots: zod@3.25.76: {} - zod@4.4.3: {} + zod@4.5.4: {} zustand@5.0.14(@types/react@19.2.17)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 903588e880f..8338c47e126 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,8 +6,10 @@ packages: [] minimumReleaseAge: 4320 # 6.3.289 was published 2026-08-29T12:48Z; it clears the 3-day gate on 2026-09-01T12:48Z. +# zod 4.5.4 was published 2026-08-29T17:55Z; it clears the 3-day gate on 2026-09-01T17:55Z. minimumReleaseAgeExclude: - pdfjs-dist@6.3.289 + - zod@4.5.4 shamefullyHoist: true supportedArchitectures: diff --git a/src/main/runtime/rpc/dispatcher-request-parsing.test.ts b/src/main/runtime/rpc/dispatcher-request-parsing.test.ts new file mode 100644 index 00000000000..e3df920a39a --- /dev/null +++ b/src/main/runtime/rpc/dispatcher-request-parsing.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest' +import { z } from 'zod' +import { defineMethod, type RpcEnvelopeMeta, type RpcRequest } from './core' +import { parseRpcRequestParams } from './dispatcher-request-parsing' + +const META: RpcEnvelopeMeta = { runtimeId: 'runtime-1' } + +function request(params: unknown): RpcRequest { + return { id: 'req-1', authToken: 'token', method: 'test.parse', params } +} + +describe('parseRpcRequestParams', () => { + it('parses repeated valid requests through the compiled schema', () => { + const method = defineMethod({ + name: 'test.parse', + params: z.object({ requestId: z.string().min(1), count: z.number().int().nonnegative() }), + handler: () => null + }) + + expect(parseRpcRequestParams(request({ requestId: 'a', count: 1 }), method, META)).toEqual({ + value: { requestId: 'a', count: 1 } + }) + expect(parseRpcRequestParams(request({ requestId: 'b', count: 2 }), method, META)).toEqual({ + value: { requestId: 'b', count: 2 } + }) + }) + + it('preserves validation errors from the runtime fallback', () => { + const method = defineMethod({ + name: 'test.parse', + params: z.object({ count: z.number().int('Count must be an integer') }), + handler: () => null + }) + + expect(parseRpcRequestParams(request({ count: 1.5 }), method, META)).toMatchObject({ + error: { + ok: false, + error: { code: 'invalid_argument', message: 'Count must be an integer' } + } + }) + }) + + it('keeps transform output when compilation falls back', () => { + const method = defineMethod({ + name: 'test.parse', + params: z.object({ value: z.string().transform((value) => value.toUpperCase()) }), + handler: () => null + }) + + expect(parseRpcRequestParams(request({ value: 'ok' }), method, META)).toEqual({ + value: { value: 'OK' } + }) + }) +}) diff --git a/src/main/runtime/rpc/dispatcher-request-parsing.ts b/src/main/runtime/rpc/dispatcher-request-parsing.ts index 2aef84ed8c6..da4ec510e75 100644 --- a/src/main/runtime/rpc/dispatcher-request-parsing.ts +++ b/src/main/runtime/rpc/dispatcher-request-parsing.ts @@ -1,3 +1,4 @@ +import { compile, type ZodType } from 'zod' import { formatZodError, type RpcAnyMethod, @@ -7,6 +8,8 @@ import { } from './core' import { invalidArgumentResponse } from './dispatcher-error-response' +const compiledParams = new WeakMap() + export function parseRpcRequestParams( request: RpcRequest, method: RpcAnyMethod, @@ -15,7 +18,7 @@ export function parseRpcRequestParams( if (method.params === null) { return { value: undefined } } - const result = method.params.safeParse(request.params ?? {}) + const result = getCompiledParams(method.params).safeParse(request.params ?? {}) if (!result.success) { return { error: invalidArgumentResponse(request, meta, formatZodError(result.error)) @@ -23,3 +26,13 @@ export function parseRpcRequestParams( } return { value: result.data } } + +function getCompiledParams(schema: ZodType): ZodType { + const cached = compiledParams.get(schema) + if (cached) { + return cached + } + const compiled = compile(schema) + compiledParams.set(schema, compiled) + return compiled +}