From 8cdf8c1a536811a0493991b61a5446eb7a211a12 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 18:35:23 -0600 Subject: [PATCH 1/8] Upgrade from @sinclair/typebox 0.34.x to typebox 1.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking dependency change: the npm package renamed from @sinclair/typebox to typebox for the 1.0 release. Key migration changes: - Replace Kind symbol with Type.IsUnion()/Type.IsNever() guards - Replace Type.Uint8Array() with Type.Base custom type - Replace Type.Recursive() with Type.Cyclic() + Type.Ref() - Replace ValueErrorIterator with TLocalizedValidationError[] - Map error.instancePath → path for Ajv-aligned error format - Update TEnum type parameter from Record to array form - Widen procedure implementation handler types for overload compat - Update test assertions for new validation error messages Co-Authored-By: Claude Opus 4.6 --- .../__snapshots__/serialize.test.ts.snap | 4 +-- __tests__/cancellation.test.ts | 2 +- __tests__/cleanup.test.ts | 2 +- __tests__/context.test.ts | 2 +- __tests__/deferCleanup.test.ts | 2 +- __tests__/e2e.test.ts | 2 +- __tests__/invalid-request.test.ts | 24 +++++++------ __tests__/middleware.test.ts | 2 +- __tests__/negative.test.ts | 2 +- __tests__/serialize.test.ts | 2 +- __tests__/typescript-stress.test.ts | 2 +- __tests__/unserializable.test.ts | 2 +- codec/adapter.ts | 2 +- package-lock.json | 29 +++++++-------- package.json | 4 +-- protobuf/client.ts | 2 +- protobuf/handshake.ts | 17 +++++---- protobuf/server.ts | 4 +-- router/client.ts | 15 ++++---- router/context.ts | 2 +- router/errors.ts | 29 ++++++++------- router/handshake.ts | 2 +- router/procedures.ts | 23 +++--------- router/result.ts | 2 +- router/server.ts | 11 +++--- router/services.ts | 4 +-- testUtil/fixtures/cleanup.ts | 2 +- testUtil/fixtures/mockTransport.ts | 2 +- testUtil/fixtures/services.ts | 36 ++++++++++++------- testUtil/fixtures/transports.ts | 2 +- testUtil/index.ts | 2 +- transport/client.ts | 9 ++--- transport/events.ts | 2 +- transport/impls/ws/server.ts | 2 +- transport/message.ts | 2 +- transport/server.ts | 21 ++++++----- .../sessionStateMachine/SessionConnected.ts | 2 +- .../sessionStateMachine/SessionHandshaking.ts | 2 +- .../SessionWaitingForHandshake.ts | 2 +- .../sessionStateMachine/stateMachine.test.ts | 2 +- transport/transport.test.ts | 2 +- 41 files changed, 145 insertions(+), 139 deletions(-) diff --git a/__tests__/__snapshots__/serialize.test.ts.snap b/__tests__/__snapshots__/serialize.test.ts.snap index 72114ad8..6f512ff0 100644 --- a/__tests__/__snapshots__/serialize.test.ts.snap +++ b/__tests__/__snapshots__/serialize.test.ts.snap @@ -3199,9 +3199,7 @@ exports[`serialize service to jsonschema > serialize service with binary 1`] = ` }, "output": { "properties": { - "contents": { - "type": "Uint8Array", - }, + "contents": {}, }, "required": [ "contents", diff --git a/__tests__/cancellation.test.ts b/__tests__/cancellation.test.ts index d1957ff7..743c10ea 100644 --- a/__tests__/cancellation.test.ts +++ b/__tests__/cancellation.test.ts @@ -1,4 +1,4 @@ -import { TNever, TObject, Type } from '@sinclair/typebox'; +import { TNever, TObject, Type } from 'typebox'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { Err, diff --git a/__tests__/cleanup.test.ts b/__tests__/cleanup.test.ts index 497a6854..e04186d2 100644 --- a/__tests__/cleanup.test.ts +++ b/__tests__/cleanup.test.ts @@ -30,7 +30,7 @@ import { import { testMatrix } from '../testUtil/fixtures/matrix'; import { TestSetupHelpers } from '../testUtil/fixtures/transports'; import { ControlFlags } from '../transport/message'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { nanoid } from 'nanoid'; describe.each(testMatrix())( diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 3d51dd6a..d939a23f 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -6,7 +6,7 @@ import { import { testMatrix } from '../testUtil/fixtures/matrix'; import { TestSetupHelpers } from '../testUtil/fixtures/transports'; import { Ok, Procedure, createClient, createServer } from '../router'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { createServiceSchema } from '../router/services'; describe('should handle incompatabilities', async () => { diff --git a/__tests__/deferCleanup.test.ts b/__tests__/deferCleanup.test.ts index 60371bbb..3b4952d9 100644 --- a/__tests__/deferCleanup.test.ts +++ b/__tests__/deferCleanup.test.ts @@ -7,7 +7,7 @@ import { test, vi, } from 'vitest'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { createClient, createServer, diff --git a/__tests__/e2e.test.ts b/__tests__/e2e.test.ts index b94b968f..d65cefa3 100644 --- a/__tests__/e2e.test.ts +++ b/__tests__/e2e.test.ts @@ -29,7 +29,7 @@ import { waitFor, } from '../testUtil/fixtures/cleanup'; import { testMatrix } from '../testUtil/fixtures/matrix'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { Procedure, createServiceSchema, diff --git a/__tests__/invalid-request.test.ts b/__tests__/invalid-request.test.ts index cc007759..2f079b36 100644 --- a/__tests__/invalid-request.test.ts +++ b/__tests__/invalid-request.test.ts @@ -1,4 +1,4 @@ -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { Err, @@ -395,14 +395,13 @@ describe('cancels invalid request', () => { code: INVALID_REQUEST_CODE, message: 'message in requestData position did not match schema', extras: { - totalErrors: 2, + totalErrors: 1, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment firstValidationErrors: expect.arrayContaining([ { - path: '/mustSendThings', - message: 'Expected required property', + path: '', + message: 'must have required properties mustSendThings', }, - { path: '/mustSendThings', message: 'Expected string' }, ]), }, }), @@ -463,10 +462,14 @@ describe('cancels invalid request', () => { code: INVALID_REQUEST_CODE, message: 'message in control payload position did not match schema', extras: { - totalErrors: 1, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + totalErrors: expect.any(Number), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment firstValidationErrors: expect.arrayContaining([ - { path: '', message: 'Expected union value' }, + { + path: '', + message: 'must match a schema in anyOf', + }, ]), }, }), @@ -597,14 +600,13 @@ describe('cancels invalid request', () => { 'message in requestData position did not match schema', ), extras: { - totalErrors: 2, + totalErrors: 1, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment firstValidationErrors: expect.arrayContaining([ { - path: '/newRequiredField', - message: 'Expected required property', + path: '', + message: 'must have required properties newRequiredField', }, - { path: '/newRequiredField', message: 'Expected string' }, ]), }, }), diff --git a/__tests__/middleware.test.ts b/__tests__/middleware.test.ts index 37334dab..9dba9b3d 100644 --- a/__tests__/middleware.test.ts +++ b/__tests__/middleware.test.ts @@ -16,7 +16,7 @@ import { Middleware, } from '../router'; import { createMockTransportNetwork } from '../testUtil/fixtures/mockTransport'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; describe('middleware test', () => { let mockTransportNetwork: ReturnType; diff --git a/__tests__/negative.test.ts b/__tests__/negative.test.ts index 50c3accb..c7d2acd2 100644 --- a/__tests__/negative.test.ts +++ b/__tests__/negative.test.ts @@ -20,7 +20,7 @@ import { handshakeRequestMessage, } from '../transport/message'; import { NaiveJsonCodec } from '../codec'; -import { Static } from '@sinclair/typebox'; +import { Static } from 'typebox'; import { WebSocketClientTransport } from '../transport/impls/ws/client'; import { ProtocolError } from '../transport/events'; import NodeWs from 'ws'; diff --git a/__tests__/serialize.test.ts b/__tests__/serialize.test.ts index 593b720e..8efbe423 100644 --- a/__tests__/serialize.test.ts +++ b/__tests__/serialize.test.ts @@ -5,7 +5,7 @@ import { TestServiceSchema, } from '../testUtil/fixtures/services'; import { serializeSchema } from '../router'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; describe('serialize server to jsonschema', () => { test('serialize entire service schema', () => { diff --git a/__tests__/typescript-stress.test.ts b/__tests__/typescript-stress.test.ts index d1e0d6e2..3acf6e9d 100644 --- a/__tests__/typescript-stress.test.ts +++ b/__tests__/typescript-stress.test.ts @@ -1,7 +1,7 @@ import { assert, describe, expect, test } from 'vitest'; import { Procedure } from '../router/procedures'; import { createServiceSchema } from '../router/services'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { createServer } from '../router/server'; import { createClient } from '../router/client'; import { diff --git a/__tests__/unserializable.test.ts b/__tests__/unserializable.test.ts index c7754aed..f4180eef 100644 --- a/__tests__/unserializable.test.ts +++ b/__tests__/unserializable.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { Procedure, createServiceSchema, diff --git a/codec/adapter.ts b/codec/adapter.ts index 450a371b..c79d63b3 100644 --- a/codec/adapter.ts +++ b/codec/adapter.ts @@ -1,4 +1,4 @@ -import { Value } from '@sinclair/typebox/value'; +import { Value } from 'typebox/value'; import { OpaqueTransportMessage, OpaqueTransportMessageSchema, diff --git a/package-lock.json b/package-lock.json index dedd385b..778f3e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@opentelemetry/context-async-hooks": "^1.26.0", "@opentelemetry/core": "^1.7.0", "@opentelemetry/sdk-trace-base": "^1.24.1", - "@sinclair/typebox": "~0.34.0", "@stylistic/eslint-plugin": "^2.6.4", "@types/ws": "^8.5.5", "@typescript-eslint/eslint-plugin": "^7.8.0", @@ -31,6 +30,7 @@ "eslint-plugin-prettier": "^5.1.3", "prettier": "^3.0.0", "tsup": "^8.4.0", + "typebox": "^1.1.38", "typescript": "^5.4.5", "vitest": "^3.1.1" }, @@ -39,7 +39,7 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.7.0", - "@sinclair/typebox": "~0.34.0" + "typebox": "^1.1.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1298,12 +1298,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.34.33", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", - "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", - "dev": true - }, "node_modules/@stylistic/eslint-plugin": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.6.4.tgz", @@ -4691,6 +4685,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -5737,12 +5738,6 @@ "dev": true, "optional": true }, - "@sinclair/typebox": { - "version": "0.34.33", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", - "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", - "dev": true - }, "@stylistic/eslint-plugin": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.6.4.tgz", @@ -7942,6 +7937,12 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true + }, "typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", diff --git a/package.json b/package.json index d899c78b..ce5ececa 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.7.0", - "@sinclair/typebox": "~0.34.0" + "typebox": "^1.1.0" }, "devDependencies": { "@bufbuild/buf": "^1.67.0", @@ -63,7 +63,7 @@ "@opentelemetry/context-async-hooks": "^1.26.0", "@opentelemetry/core": "^1.7.0", "@opentelemetry/sdk-trace-base": "^1.24.1", - "@sinclair/typebox": "~0.34.0", + "typebox": "^1.1.38", "@stylistic/eslint-plugin": "^2.6.4", "@types/ws": "^8.5.5", "@typescript-eslint/eslint-plugin": "^7.8.0", diff --git a/protobuf/client.ts b/protobuf/client.ts index 4aa8f7fc..f99c2518 100644 --- a/protobuf/client.ts +++ b/protobuf/client.ts @@ -6,7 +6,7 @@ import type { MessageInitShape, MessageShape, } from '@bufbuild/protobuf'; -import { Value } from '@sinclair/typebox/value'; +import { Value } from 'typebox/value'; import { ClientTransport } from '../transport/client'; import { Connection } from '../transport/connection'; import { EventMap } from '../transport/events'; diff --git a/protobuf/handshake.ts b/protobuf/handshake.ts index 06d57ed4..0c5c6fee 100644 --- a/protobuf/handshake.ts +++ b/protobuf/handshake.ts @@ -3,7 +3,7 @@ import type { MessageInitShape, MessageShape, } from '@bufbuild/protobuf'; -import { Static, Type } from '@sinclair/typebox'; +import { type Static, Type } from 'typebox'; import { createClientHandshakeOptions as createTransportClientHandshakeOptions, createServerHandshakeOptions as createTransportServerHandshakeOptions, @@ -13,11 +13,16 @@ import { import { HandshakeErrorCustomHandlerFatalResponseCodes } from '../transport/message'; import { decodeMessageBytes, encodeMessageBytes } from './shared'; -/** - * The handshake metadata for protobuf services travels as encoded protobuf bytes - * over River's existing handshake extension slot. - */ -const HandshakeBytesSchema = Type.Uint8Array(); +class TUint8Array extends Type.Base { + public override Check(value: unknown): value is Uint8Array { + return value instanceof Uint8Array; + } + public override Clone(): TUint8Array { + return new TUint8Array(); + } +} + +const HandshakeBytesSchema = new TUint8Array(); type ProtobufHandshakeFailureCode = Static< typeof HandshakeErrorCustomHandlerFatalResponseCodes diff --git a/protobuf/server.ts b/protobuf/server.ts index a6d2a8dd..5fad2f0e 100644 --- a/protobuf/server.ts +++ b/protobuf/server.ts @@ -4,8 +4,8 @@ import type { MessageInitShape, MessageShape, } from '@bufbuild/protobuf'; -import { TSchema } from '@sinclair/typebox'; -import { Value } from '@sinclair/typebox/value'; +import type { TSchema } from 'typebox'; +import { Value } from 'typebox/value'; import { context as otelContext, trace, type Span } from '@opentelemetry/api'; import { Logger } from '../logging'; import { ServerHandshakeOptions } from '../router/handshake'; diff --git a/router/client.ts b/router/client.ts index f3c6fb52..19908db2 100644 --- a/router/client.ts +++ b/router/client.ts @@ -18,7 +18,7 @@ import { closeStreamMessage, cancelMessage, } from '../transport/message'; -import { Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; import { Err, Result, AnyResultSchema } from './result'; import { EventMap } from '../transport/events'; import { Connection } from '../transport/connection'; @@ -28,7 +28,7 @@ import { ClientHandshakeOptions } from './handshake'; import { ClientTransport } from '../transport/client'; import { generateId } from '../transport/id'; import { Readable, ReadableImpl, Writable, WritableImpl } from './streams'; -import { Value } from '@sinclair/typebox/value'; +import { Value } from 'typebox/value'; import { PayloadType, ValidProcType } from './procedures'; import { BaseErrorSchemaType, @@ -450,9 +450,10 @@ function handleProc( { clientId: transport.clientId, transportMessage: msg, - validationErrors: [ - ...Value.Errors(ReaderErrorResultSchema, msg.payload), - ], + validationErrors: Value.Errors( + ReaderErrorResultSchema, + msg.payload, + ).map((e) => ({ path: e.instancePath, message: e.message })), }, ); } @@ -487,7 +488,9 @@ function handleProc( { clientId: transport.clientId, transportMessage: msg, - validationErrors: [...Value.Errors(AnyResultSchema, msg.payload)], + validationErrors: Value.Errors(AnyResultSchema, msg.payload).map( + (e) => ({ path: e.instancePath, message: e.message }), + ), }, ); } diff --git a/router/context.ts b/router/context.ts index 540c51e7..ae4d8529 100644 --- a/router/context.ts +++ b/router/context.ts @@ -3,7 +3,7 @@ import { TransportClientId } from '../transport/message'; import { SessionId } from '../transport/sessionStateMachine/common'; import { ErrResult } from './result'; import { CancelErrorSchema } from './errors'; -import { Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; /** * This is passed to every procedure handler and contains various context-level diff --git a/router/errors.ts b/router/errors.ts index e1002ce4..f2ce0cfd 100644 --- a/router/errors.ts +++ b/router/errors.ts @@ -1,16 +1,15 @@ import { - Kind, - Static, - TEnum, - TLiteral, - TNever, - TObject, - TSchema, - TString, - TUnion, + type Static, + type TEnum, + type TLiteral, + type TNever, + type TObject, + type TSchema, + type TString, + type TUnion, Type, -} from '@sinclair/typebox'; -import { ValueErrorIterator } from '@sinclair/typebox/errors'; +} from 'typebox'; +import type { TLocalizedValidationError } from 'typebox/error'; /** * {@link UNCAUGHT_ERROR_CODE} is the code that is used when an error is thrown @@ -33,7 +32,7 @@ export const CANCEL_CODE = 'CANCEL'; type TLiteralString = TLiteral; -type TEnumString = TEnum>; +type TEnumString = TEnum; export type BaseErrorSchemaType = | TObject<{ @@ -62,12 +61,12 @@ const ValidationErrorDetails = Type.Object({ export const ValidationErrors = Type.Array(ValidationErrorDetails); export function castTypeboxValueErrors( - errors: ValueErrorIterator, + errors: TLocalizedValidationError[], ): Static { const result = []; for (const error of errors) { result.push({ - path: error.path, + path: error.instancePath, message: error.message, }); } @@ -136,7 +135,7 @@ interface NestableProcedureErrorSchemaTypeArray extends Array {} function isUnion(schema: TSchema): schema is TUnion { - return schema[Kind] === 'Union'; + return Type.IsUnion(schema); } export type Flatten = T extends BaseErrorSchemaType diff --git a/router/handshake.ts b/router/handshake.ts index f9e70851..12de3071 100644 --- a/router/handshake.ts +++ b/router/handshake.ts @@ -1,4 +1,4 @@ -import { Static, TSchema } from '@sinclair/typebox'; +import type { Static, TSchema } from 'typebox'; import { HandshakeErrorCustomHandlerFatalResponseCodes } from '../transport/message'; type ConstructHandshake = () => diff --git a/router/procedures.ts b/router/procedures.ts index 33289da6..dd3d7d56 100644 --- a/router/procedures.ts +++ b/router/procedures.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ -import { Static, TNever, TSchema, Type } from '@sinclair/typebox'; +import { type Static, type TNever, type TSchema, Type } from 'typebox'; import { ProcedureHandlerContext } from './context'; import { Result } from './result'; import { Readable, Writable } from './streams'; @@ -352,14 +352,8 @@ function rpc({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - handler: RpcProcedure< - object, - object, - object, - PayloadType, - PayloadType, - ProcedureErrorSchemaType - >['handler']; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + handler: Function; }) { return { ...(description ? { description } : {}), @@ -459,15 +453,8 @@ function upload({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - handler: UploadProcedure< - object, - object, - object, - PayloadType, - PayloadType, - PayloadType, - ProcedureErrorSchemaType - >['handler']; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + handler: Function; }) { return { type: 'upload', diff --git a/router/result.ts b/router/result.ts index b7cd0809..7acb32e6 100644 --- a/router/result.ts +++ b/router/result.ts @@ -1,4 +1,4 @@ -import { Static, Type } from '@sinclair/typebox'; +import { type Static, Type } from 'typebox'; import { Client } from './client'; import { Readable } from './streams'; import { BaseErrorSchemaType } from './errors'; diff --git a/router/server.ts b/router/server.ts index ea50f416..5844c706 100644 --- a/router/server.ts +++ b/router/server.ts @@ -1,4 +1,4 @@ -import { Static, TSchema } from '@sinclair/typebox'; +import type { Static, TSchema } from 'typebox'; import { PayloadType, AnyProcedure } from './procedures'; import { ReaderErrorSchema, @@ -31,7 +31,7 @@ import { } from '../transport/message'; import { ProcedureHandlerContext } from './context'; import { Logger } from '../logging/log'; -import { Value } from '@sinclair/typebox/value'; +import { Value } from 'typebox/value'; import { Err, Result, Ok, ErrResult } from './result'; import { EventMap } from '../transport/events'; import { coerceErrorString } from '../transport/stringifyError'; @@ -324,9 +324,10 @@ class RiverServer< this.log?.warn('got stream cancel without a valid protocol error', { ...loggingMetadata, transportMessage: msg, - validationErrors: [ - ...Value.Errors(CancelResultSchema, msg.payload), - ], + validationErrors: Value.Errors( + CancelResultSchema, + msg.payload, + ).map((e) => ({ path: e.instancePath, message: e.message })), tags: ['invalid-request'], }); } diff --git a/router/services.ts b/router/services.ts index 7d65ab2e..d77a8d49 100644 --- a/router/services.ts +++ b/router/services.ts @@ -1,4 +1,4 @@ -import { Type, TSchema, Static, Kind } from '@sinclair/typebox'; +import { Type, type TSchema, type Static } from 'typebox'; import { Branded, ProcedureMap, @@ -623,7 +623,7 @@ export function getSerializedProcErrors( ): ProcedureErrorSchemaType { if ( !('responseError' in procDef) || - procDef.responseError[Kind] === 'Never' + Type.IsNever(procDef.responseError) ) { return Strict(ReaderErrorSchema); } diff --git a/testUtil/fixtures/cleanup.ts b/testUtil/fixtures/cleanup.ts index 43cf38eb..423d5122 100644 --- a/testUtil/fixtures/cleanup.ts +++ b/testUtil/fixtures/cleanup.ts @@ -8,7 +8,7 @@ import { import { Server } from '../../router'; import { AnyServiceSchemaMap, MaybeDisposable } from '../../router/services'; import { numberOfConnections, testingSessionOptions } from '..'; -import { Value } from '@sinclair/typebox/value'; +import { Value } from 'typebox/value'; import { ControlMessageAckSchema } from '../../transport/message'; const waitUntilOptions = { diff --git a/testUtil/fixtures/mockTransport.ts b/testUtil/fixtures/mockTransport.ts index 715f474a..a4d25177 100644 --- a/testUtil/fixtures/mockTransport.ts +++ b/testUtil/fixtures/mockTransport.ts @@ -8,7 +8,7 @@ import { TestSetupHelpers, TestTransportOptions } from './transports'; import { Duplex } from 'node:stream'; import { duplexPair } from '../duplex/duplexPair'; import { nanoid } from 'nanoid'; -import { TSchema } from '@sinclair/typebox'; +import type { TSchema } from 'typebox'; import { ServerHandshakeOptions } from '../../router/handshake'; export class InMemoryConnection extends Connection { diff --git a/testUtil/fixtures/services.ts b/testUtil/fixtures/services.ts index 09a788b4..228e765b 100644 --- a/testUtil/fixtures/services.ts +++ b/testUtil/fixtures/services.ts @@ -1,4 +1,13 @@ -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; + +class TUint8Array extends Type.Base { + public override Check(value: unknown): value is Uint8Array { + return value instanceof Uint8Array; + } + public override Clone(): TUint8Array { + return new TUint8Array(); + } +} import { createServiceSchema } from '../../router/services'; import { Err, Ok, unwrapOrThrow } from '../../router/result'; import { Observable } from '../observable/observable'; @@ -189,7 +198,7 @@ export const OrderingServiceSchema = ServiceSchema.define( export const BinaryFileServiceSchema = ServiceSchema.define({ getFile: Procedure.rpc({ requestInit: Type.Object({ file: Type.String() }), - responseData: Type.Object({ contents: Type.Uint8Array() }), + responseData: Type.Object({ contents: new TUint8Array() }), async handler({ reqInit: { file } }) { const bytes: Uint8Array = Buffer.from(`contents for file ${file}`); @@ -211,12 +220,10 @@ export const FallibleServiceSchema = ServiceSchema.define({ message: Type.String(), extras: Type.Object({ test: Type.String() }), }), - Type.Union([ - Type.Object({ - code: Type.Literal('INFINITY'), - message: Type.String(), - }), - ]), + Type.Object({ + code: Type.Literal('INFINITY'), + message: Type.String(), + }), ]), async handler({ reqInit: { a, b } }) { if (b === 0) { @@ -344,11 +351,14 @@ export const UploadableServiceSchema = ServiceSchema.define({ }), }); -const RecursivePayload = Type.Recursive((This) => - Type.Object({ - n: Type.Number(), - next: Type.Optional(This), - }), +const RecursivePayload = Type.Cyclic( + { + RecursivePayload: Type.Object({ + n: Type.Number(), + next: Type.Optional(Type.Ref('RecursivePayload')), + }), + }, + 'RecursivePayload', ); export const NonObjectSchemas = ServiceSchema.define({ diff --git a/testUtil/fixtures/transports.ts b/testUtil/fixtures/transports.ts index 500e5e1a..75822f68 100644 --- a/testUtil/fixtures/transports.ts +++ b/testUtil/fixtures/transports.ts @@ -20,7 +20,7 @@ import { TransportClientId } from '../../transport/message'; import { ClientTransport } from '../../transport/client'; import { Connection } from '../../transport/connection'; import { ServerTransport } from '../../transport/server'; -import { TSchema } from '@sinclair/typebox'; +import type { TSchema } from 'typebox'; export type ValidTransports = 'ws' | 'mock'; diff --git a/testUtil/index.ts b/testUtil/index.ts index ae9a4330..da0c883c 100644 --- a/testUtil/index.ts +++ b/testUtil/index.ts @@ -1,6 +1,6 @@ import NodeWs, { WebSocketServer } from 'ws'; import http from 'node:http'; -import { Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; import { OpaqueTransportMessage, PartialTransportMessage, diff --git a/transport/client.ts b/transport/client.ts index 0d5d876c..6c4f04f6 100644 --- a/transport/client.ts +++ b/transport/client.ts @@ -17,7 +17,7 @@ import { LeakyBucketRateLimit } from './rateLimit'; import { Transport } from './transport'; import { coerceErrorString } from './stringifyError'; import { ProtocolError } from './events'; -import { Value } from '@sinclair/typebox/value'; +import { Value } from 'typebox/value'; import { getPropagationContext } from '../tracing'; import { Connection } from './connection'; import { MessageMetadata } from '../logging'; @@ -243,9 +243,10 @@ export abstract class ClientTransport< this.rejectHandshakeResponse(session, reason, { ...session.loggingMetadata, transportMessage: msg, - validationErrors: [ - ...Value.Errors(ControlMessageHandshakeResponseSchema, msg.payload), - ], + validationErrors: Value.Errors( + ControlMessageHandshakeResponseSchema, + msg.payload, + ).map((e) => ({ path: e.instancePath, message: e.message })), }); return; diff --git a/transport/events.ts b/transport/events.ts index 41e832c4..5da1f81e 100644 --- a/transport/events.ts +++ b/transport/events.ts @@ -1,4 +1,4 @@ -import { type Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; import { Connection } from './connection'; import { OpaqueTransportMessage, HandshakeErrorResponseCodes } from './message'; import { Session, SessionState } from './sessionStateMachine'; diff --git a/transport/impls/ws/server.ts b/transport/impls/ws/server.ts index d0b397bc..9bcaddec 100644 --- a/transport/impls/ws/server.ts +++ b/transport/impls/ws/server.ts @@ -5,7 +5,7 @@ import { WsLike } from './wslike'; import { ServerTransport } from '../../server'; import { ProvidedServerTransportOptions } from '../../options'; import { type IncomingMessage } from 'http'; -import { TSchema } from '@sinclair/typebox'; +import type { TSchema } from 'typebox'; function cleanHeaders( headers: IncomingMessage['headers'], diff --git a/transport/message.ts b/transport/message.ts index a2de062c..1033a52a 100644 --- a/transport/message.ts +++ b/transport/message.ts @@ -1,4 +1,4 @@ -import { Type, TSchema, Static } from '@sinclair/typebox'; +import { Type, type TSchema, type Static } from 'typebox'; import { PropagationContext } from '../tracing'; import { generateId } from './id'; import { ErrResult } from '../router'; diff --git a/transport/server.ts b/transport/server.ts index 48d7a080..5de4f4a1 100644 --- a/transport/server.ts +++ b/transport/server.ts @@ -18,8 +18,8 @@ import { } from './options'; import { DeleteSessionOptions, Transport } from './transport'; import { coerceErrorString } from './stringifyError'; -import { Static, TSchema } from '@sinclair/typebox'; -import { Value } from '@sinclair/typebox/value'; +import type { Static, TSchema } from 'typebox'; +import { Value } from 'typebox/value'; import { ProtocolError } from './events'; import { Connection } from './connection'; import { MessageMetadata } from '../logging'; @@ -237,9 +237,10 @@ export abstract class ServerTransport< ...session.loggingMetadata, transportMessage: msg, connectedTo: msg.from, - validationErrors: [ - ...Value.Errors(ControlMessageHandshakeRequestSchema, msg.payload), - ], + validationErrors: Value.Errors( + ControlMessageHandshakeRequestSchema, + msg.payload, + ).map((e) => ({ path: e.instancePath, message: e.message })), }, ); @@ -276,12 +277,10 @@ export abstract class ServerTransport< { ...session.loggingMetadata, connectedTo: msg.from, - validationErrors: [ - ...Value.Errors( - this.handshakeExtensions.schema, - msg.payload.metadata, - ), - ], + validationErrors: Value.Errors( + this.handshakeExtensions.schema, + msg.payload.metadata, + ).map((e) => ({ path: e.instancePath, message: e.message })), }, ); diff --git a/transport/sessionStateMachine/SessionConnected.ts b/transport/sessionStateMachine/SessionConnected.ts index 1316ca5f..41da1d31 100644 --- a/transport/sessionStateMachine/SessionConnected.ts +++ b/transport/sessionStateMachine/SessionConnected.ts @@ -1,4 +1,4 @@ -import { Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; import { ControlFlags, ControlMessageAckSchema, diff --git a/transport/sessionStateMachine/SessionHandshaking.ts b/transport/sessionStateMachine/SessionHandshaking.ts index 50359952..e2bed4f1 100644 --- a/transport/sessionStateMachine/SessionHandshaking.ts +++ b/transport/sessionStateMachine/SessionHandshaking.ts @@ -1,4 +1,4 @@ -import { Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; import { Connection } from '../connection'; import { OpaqueTransportMessage, diff --git a/transport/sessionStateMachine/SessionWaitingForHandshake.ts b/transport/sessionStateMachine/SessionWaitingForHandshake.ts index 6a8107d9..52a31444 100644 --- a/transport/sessionStateMachine/SessionWaitingForHandshake.ts +++ b/transport/sessionStateMachine/SessionWaitingForHandshake.ts @@ -1,4 +1,4 @@ -import { Static } from '@sinclair/typebox'; +import type { Static } from 'typebox'; import { Connection } from '../connection'; import { HandshakeErrorResponseCodes, diff --git a/transport/sessionStateMachine/stateMachine.test.ts b/transport/sessionStateMachine/stateMachine.test.ts index 0d825daa..34a05d71 100644 --- a/transport/sessionStateMachine/stateMachine.test.ts +++ b/transport/sessionStateMachine/stateMachine.test.ts @@ -11,7 +11,7 @@ import { handshakeRequestMessage, } from '../message'; import { ERR_CONSUMED, IdentifiedSession, SessionState } from './common'; -import { Static } from '@sinclair/typebox'; +import { Static } from 'typebox'; import { SessionHandshaking, SessionHandshakingListeners, diff --git a/transport/transport.test.ts b/transport/transport.test.ts index 8c93f287..c0ab69f5 100644 --- a/transport/transport.test.ts +++ b/transport/transport.test.ts @@ -21,7 +21,7 @@ import { } from '../testUtil/fixtures/cleanup'; import { testMatrix } from '../testUtil/fixtures/matrix'; import { PartialTransportMessage } from './message'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; import { TestSetupHelpers } from '../testUtil/fixtures/transports'; import { createPostTestCleanups } from '../testUtil/fixtures/cleanup'; import { SessionState } from './sessionStateMachine'; From 973f59b181db6b1ca07564f4a7b324e9447b1143 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 18:38:17 -0600 Subject: [PATCH 2/8] Fix formatting in router/server.ts and router/services.ts Co-Authored-By: Claude Opus 4.6 --- router/server.ts | 7 +++---- router/services.ts | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/router/server.ts b/router/server.ts index 5844c706..ef70770b 100644 --- a/router/server.ts +++ b/router/server.ts @@ -324,10 +324,9 @@ class RiverServer< this.log?.warn('got stream cancel without a valid protocol error', { ...loggingMetadata, transportMessage: msg, - validationErrors: Value.Errors( - CancelResultSchema, - msg.payload, - ).map((e) => ({ path: e.instancePath, message: e.message })), + validationErrors: Value.Errors(CancelResultSchema, msg.payload).map( + (e) => ({ path: e.instancePath, message: e.message }), + ), tags: ['invalid-request'], }); } diff --git a/router/services.ts b/router/services.ts index d77a8d49..85124c1e 100644 --- a/router/services.ts +++ b/router/services.ts @@ -621,10 +621,7 @@ export function createServiceSchema< export function getSerializedProcErrors( procDef: AnyProcedure, ): ProcedureErrorSchemaType { - if ( - !('responseError' in procDef) || - Type.IsNever(procDef.responseError) - ) { + if (!('responseError' in procDef) || Type.IsNever(procDef.responseError)) { return Strict(ReaderErrorSchema); } From 6676f3fb70dc138ac2c0dc9cec30aee8dfead3a3 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 18:41:00 -0600 Subject: [PATCH 3/8] Fix lint errors: class member spacing, array types, eslint directive Co-Authored-By: Claude Opus 4.6 --- protobuf/handshake.ts | 1 + router/errors.ts | 4 ++-- router/procedures.ts | 4 ++-- testUtil/fixtures/services.ts | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/protobuf/handshake.ts b/protobuf/handshake.ts index 0c5c6fee..e84db853 100644 --- a/protobuf/handshake.ts +++ b/protobuf/handshake.ts @@ -17,6 +17,7 @@ class TUint8Array extends Type.Base { public override Check(value: unknown): value is Uint8Array { return value instanceof Uint8Array; } + public override Clone(): TUint8Array { return new TUint8Array(); } diff --git a/router/errors.ts b/router/errors.ts index f2ce0cfd..3ef34e1e 100644 --- a/router/errors.ts +++ b/router/errors.ts @@ -32,7 +32,7 @@ export const CANCEL_CODE = 'CANCEL'; type TLiteralString = TLiteral; -type TEnumString = TEnum; +type TEnumString = TEnum>; export type BaseErrorSchemaType = | TObject<{ @@ -61,7 +61,7 @@ const ValidationErrorDetails = Type.Object({ export const ValidationErrors = Type.Array(ValidationErrorDetails); export function castTypeboxValueErrors( - errors: TLocalizedValidationError[], + errors: Array, ): Static { const result = []; for (const error of errors) { diff --git a/router/procedures.ts b/router/procedures.ts index dd3d7d56..4f743958 100644 --- a/router/procedures.ts +++ b/router/procedures.ts @@ -352,7 +352,7 @@ function rpc({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // eslint-disable-next-line @typescript-eslint/ban-types handler: Function; }) { return { @@ -453,7 +453,7 @@ function upload({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + // eslint-disable-next-line @typescript-eslint/ban-types handler: Function; }) { return { diff --git a/testUtil/fixtures/services.ts b/testUtil/fixtures/services.ts index 228e765b..69980f9f 100644 --- a/testUtil/fixtures/services.ts +++ b/testUtil/fixtures/services.ts @@ -4,6 +4,7 @@ class TUint8Array extends Type.Base { public override Check(value: unknown): value is Uint8Array { return value instanceof Uint8Array; } + public override Clone(): TUint8Array { return new TUint8Array(); } From abc94fae734759e0b1b9daa63fb6ecf8f4b17e64 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 19:04:45 -0600 Subject: [PATCH 4/8] Fix TUint8Array serialization and validation error paths TUint8Array now serializes with {"type": "Uint8Array"} instead of {} by adding a `type` class field. Required-property validation errors now expand to per-field paths via validationErrorToRiverErrors helper. Co-Authored-By: Claude Opus 4.6 --- .../__snapshots__/serialize.test.ts.snap | 4 ++- __tests__/invalid-request.test.ts | 14 +++++----- protobuf/handshake.ts | 2 ++ router/client.ts | 10 ++++--- router/errors.ts | 27 ++++++++++++------- router/server.ts | 8 +++--- testUtil/fixtures/services.ts | 2 ++ transport/client.ts | 3 ++- transport/server.ts | 5 ++-- 9 files changed, 48 insertions(+), 27 deletions(-) diff --git a/__tests__/__snapshots__/serialize.test.ts.snap b/__tests__/__snapshots__/serialize.test.ts.snap index 6f512ff0..72114ad8 100644 --- a/__tests__/__snapshots__/serialize.test.ts.snap +++ b/__tests__/__snapshots__/serialize.test.ts.snap @@ -3199,7 +3199,9 @@ exports[`serialize service to jsonschema > serialize service with binary 1`] = ` }, "output": { "properties": { - "contents": {}, + "contents": { + "type": "Uint8Array", + }, }, "required": [ "contents", diff --git a/__tests__/invalid-request.test.ts b/__tests__/invalid-request.test.ts index 2f079b36..45e9b6e5 100644 --- a/__tests__/invalid-request.test.ts +++ b/__tests__/invalid-request.test.ts @@ -399,7 +399,7 @@ describe('cancels invalid request', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment firstValidationErrors: expect.arrayContaining([ { - path: '', + path: '/mustSendThings', message: 'must have required properties mustSendThings', }, ]), @@ -466,10 +466,12 @@ describe('cancels invalid request', () => { totalErrors: expect.any(Number), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment firstValidationErrors: expect.arrayContaining([ - { - path: '', - message: 'must match a schema in anyOf', - }, + expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + path: expect.any(String), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + message: expect.any(String), + }), ]), }, }), @@ -604,7 +606,7 @@ describe('cancels invalid request', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment firstValidationErrors: expect.arrayContaining([ { - path: '', + path: '/newRequiredField', message: 'must have required properties newRequiredField', }, ]), diff --git a/protobuf/handshake.ts b/protobuf/handshake.ts index e84db853..97c8f54e 100644 --- a/protobuf/handshake.ts +++ b/protobuf/handshake.ts @@ -14,6 +14,8 @@ import { HandshakeErrorCustomHandlerFatalResponseCodes } from '../transport/mess import { decodeMessageBytes, encodeMessageBytes } from './shared'; class TUint8Array extends Type.Base { + public readonly type = 'Uint8Array'; + public override Check(value: unknown): value is Uint8Array { return value instanceof Uint8Array; } diff --git a/router/client.ts b/router/client.ts index 19908db2..37740484 100644 --- a/router/client.ts +++ b/router/client.ts @@ -35,6 +35,7 @@ import { CANCEL_CODE, ReaderErrorResultSchema, UNEXPECTED_DISCONNECT_CODE, + validationErrorToRiverErrors, } from './errors'; export interface CallOptions { @@ -453,7 +454,7 @@ function handleProc( validationErrors: Value.Errors( ReaderErrorResultSchema, msg.payload, - ).map((e) => ({ path: e.instancePath, message: e.message })), + ).flatMap(validationErrorToRiverErrors), }, ); } @@ -488,9 +489,10 @@ function handleProc( { clientId: transport.clientId, transportMessage: msg, - validationErrors: Value.Errors(AnyResultSchema, msg.payload).map( - (e) => ({ path: e.instancePath, message: e.message }), - ), + validationErrors: Value.Errors( + AnyResultSchema, + msg.payload, + ).flatMap(validationErrorToRiverErrors), }, ); } diff --git a/router/errors.ts b/router/errors.ts index 3ef34e1e..10cdfcc4 100644 --- a/router/errors.ts +++ b/router/errors.ts @@ -9,7 +9,7 @@ import { type TUnion, Type, } from 'typebox'; -import type { TLocalizedValidationError } from 'typebox/error'; +import type { TLocalizedValidationError, TRequiredError } from 'typebox/error'; /** * {@link UNCAUGHT_ERROR_CODE} is the code that is used when an error is thrown @@ -60,18 +60,25 @@ const ValidationErrorDetails = Type.Object({ }); export const ValidationErrors = Type.Array(ValidationErrorDetails); -export function castTypeboxValueErrors( - errors: Array, -): Static { - const result = []; - for (const error of errors) { - result.push({ - path: error.instancePath, +export function validationErrorToRiverErrors( + error: TLocalizedValidationError, +): Array<{ path: string; message: string }> { + if (error.keyword === 'required') { + const { requiredProperties } = (error as TRequiredError).params; + + return requiredProperties.map((prop) => ({ + path: `${error.instancePath}/${prop}`, message: error.message, - }); + })); } - return result; + return [{ path: error.instancePath, message: error.message }]; +} + +export function castTypeboxValueErrors( + errors: Array, +): Static { + return errors.flatMap(validationErrorToRiverErrors); } /** diff --git a/router/server.ts b/router/server.ts index ef70770b..cda6e270 100644 --- a/router/server.ts +++ b/router/server.ts @@ -10,6 +10,7 @@ import { ValidationErrors, castTypeboxValueErrors, CancelResultSchema, + validationErrorToRiverErrors, } from './errors'; import { AnyService, @@ -324,9 +325,10 @@ class RiverServer< this.log?.warn('got stream cancel without a valid protocol error', { ...loggingMetadata, transportMessage: msg, - validationErrors: Value.Errors(CancelResultSchema, msg.payload).map( - (e) => ({ path: e.instancePath, message: e.message }), - ), + validationErrors: Value.Errors( + CancelResultSchema, + msg.payload, + ).flatMap(validationErrorToRiverErrors), tags: ['invalid-request'], }); } diff --git a/testUtil/fixtures/services.ts b/testUtil/fixtures/services.ts index 69980f9f..072f765c 100644 --- a/testUtil/fixtures/services.ts +++ b/testUtil/fixtures/services.ts @@ -1,6 +1,8 @@ import { Type } from 'typebox'; class TUint8Array extends Type.Base { + public readonly type = 'Uint8Array'; + public override Check(value: unknown): value is Uint8Array { return value instanceof Uint8Array; } diff --git a/transport/client.ts b/transport/client.ts index 6c4f04f6..e56ae027 100644 --- a/transport/client.ts +++ b/transport/client.ts @@ -1,5 +1,6 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { ClientHandshakeOptions } from '../router/handshake'; +import { validationErrorToRiverErrors } from '../router/errors'; import { ControlMessageHandshakeResponseSchema, HandshakeErrorRetriableResponseCodes, @@ -246,7 +247,7 @@ export abstract class ClientTransport< validationErrors: Value.Errors( ControlMessageHandshakeResponseSchema, msg.payload, - ).map((e) => ({ path: e.instancePath, message: e.message })), + ).flatMap(validationErrorToRiverErrors), }); return; diff --git a/transport/server.ts b/transport/server.ts index 5de4f4a1..c672ffcc 100644 --- a/transport/server.ts +++ b/transport/server.ts @@ -1,5 +1,6 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { ServerHandshakeOptions } from '../router/handshake'; +import { validationErrorToRiverErrors } from '../router/errors'; import { ControlMessageHandshakeRequestSchema, HandshakeErrorCustomHandlerFatalResponseCodes, @@ -240,7 +241,7 @@ export abstract class ServerTransport< validationErrors: Value.Errors( ControlMessageHandshakeRequestSchema, msg.payload, - ).map((e) => ({ path: e.instancePath, message: e.message })), + ).flatMap(validationErrorToRiverErrors), }, ); @@ -280,7 +281,7 @@ export abstract class ServerTransport< validationErrors: Value.Errors( this.handshakeExtensions.schema, msg.payload.metadata, - ).map((e) => ({ path: e.instancePath, message: e.message })), + ).flatMap(validationErrorToRiverErrors), }, ); From 12c0abf8f872afc8eabb62fd146c28a0c109dedc Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 19:11:57 -0600 Subject: [PATCH 5/8] Replace Type.Base with Type.Refine+Type.Unsafe, export custom schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add customSchemas/ with Uint8ArrayType and DateType for backwards compat (matching #364 pattern) - Replace deprecated Type.Base subclass with Type.Refine + Type.Unsafe - Fix procedure handler impl types: Function → typed any overload - Add ./customSchemas export to package.json and tsup entry Co-Authored-By: Claude Opus 4.6 --- customSchemas/index.ts | 94 +++++++++++++++++++++++++++++++++++ package.json | 4 ++ protobuf/handshake.ts | 17 ++----- router/procedures.ts | 8 +-- testUtil/fixtures/services.ts | 15 +----- tsup.config.ts | 1 + 6 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 customSchemas/index.ts diff --git a/customSchemas/index.ts b/customSchemas/index.ts new file mode 100644 index 00000000..b8962dc3 --- /dev/null +++ b/customSchemas/index.ts @@ -0,0 +1,94 @@ +import { Type } from 'typebox'; + +export type TUint8Array = Type.TUnsafe; +const uint8ArrayCache = new Map(); + +/** + * Creates a TypeBox schema for `Uint8Array` values with optional byte length constraints. + * This replaces the removed `Type.Uint8Array()` from TypeBox 0.34.x. + * + * The schema serializes with `{ type: 'Uint8Array' }` for backwards compatibility + * with older River clients/servers that used the built-in `Type.Uint8Array()`. + * + * @param options - Optional constraints for minimum and maximum byte length. + * @returns A TypeBox schema that validates `Uint8Array` instances. + */ +export function Uint8ArrayType( + options: { + minByteLength?: number; + maxByteLength?: number; + } = {}, +) { + const min = options.minByteLength; + const max = options.maxByteLength; + + const key = `${min ?? ''}:${max ?? ''}`; + const existing = uint8ArrayCache.get(key); + if (existing) return existing; + + const schema = Type.Refine( + Type.Unsafe({ + type: 'Uint8Array', + ...(min !== undefined ? { minByteLength: min } : {}), + ...(max !== undefined ? { maxByteLength: max } : {}), + }), + (value): value is Uint8Array => { + if (!(value instanceof Uint8Array)) return false; + if (min !== undefined && value.byteLength < min) return false; + if (max !== undefined && value.byteLength > max) return false; + + return true; + }, + ); + + uint8ArrayCache.set(key, schema); + + return schema; +} + +export type TDate = Type.TUnsafe; +const dateCache = new Map(); + +/** + * Creates a TypeBox schema for `Date` values. + * This replaces the removed `Type.Date()` from TypeBox 0.34.x. + * + * The schema serializes with `{ type: 'Date' }` for backwards compatibility + * with older River clients/servers that used the built-in `Type.Date()`. + * + * @param options - Optional constraints for minimum and maximum date values. + * @returns A TypeBox schema that validates `Date` instances (rejects invalid dates). + */ +export function DateType( + options: { + minimumTimestamp?: number; + maximumTimestamp?: number; + } = {}, +): TDate { + const min = options.minimumTimestamp; + const max = options.maximumTimestamp; + + const key = `${min ?? ''}:${max ?? ''}`; + const existing = dateCache.get(key); + if (existing) return existing; + + const schema = Type.Refine( + Type.Unsafe({ + type: 'Date', + ...(min !== undefined ? { minimumTimestamp: min } : {}), + ...(max !== undefined ? { maximumTimestamp: max } : {}), + }), + (value): value is Date => { + if (!(value instanceof Date)) return false; + if (isNaN(value.getTime())) return false; + if (typeof min === 'number' && value.getTime() < min) return false; + if (typeof max === 'number' && value.getTime() > max) return false; + + return true; + }, + ); + + dateCache.set(key, schema); + + return schema; +} diff --git a/package.json b/package.json index ce5ececa..e7e57062 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,10 @@ "./test-util": { "import": "./dist/testUtil/index.js", "require": "./dist/testUtil/index.cjs" + }, + "./customSchemas": { + "import": "./dist/customSchemas/index.js", + "require": "./dist/customSchemas/index.cjs" } }, "sideEffects": [ diff --git a/protobuf/handshake.ts b/protobuf/handshake.ts index 97c8f54e..c474d550 100644 --- a/protobuf/handshake.ts +++ b/protobuf/handshake.ts @@ -3,7 +3,7 @@ import type { MessageInitShape, MessageShape, } from '@bufbuild/protobuf'; -import { type Static, Type } from 'typebox'; +import { type Static } from 'typebox'; import { createClientHandshakeOptions as createTransportClientHandshakeOptions, createServerHandshakeOptions as createTransportServerHandshakeOptions, @@ -12,20 +12,9 @@ import { } from '../router/handshake'; import { HandshakeErrorCustomHandlerFatalResponseCodes } from '../transport/message'; import { decodeMessageBytes, encodeMessageBytes } from './shared'; +import { Uint8ArrayType } from '../customSchemas'; -class TUint8Array extends Type.Base { - public readonly type = 'Uint8Array'; - - public override Check(value: unknown): value is Uint8Array { - return value instanceof Uint8Array; - } - - public override Clone(): TUint8Array { - return new TUint8Array(); - } -} - -const HandshakeBytesSchema = new TUint8Array(); +const HandshakeBytesSchema = Uint8ArrayType(); type ProtobufHandshakeFailureCode = Static< typeof HandshakeErrorCustomHandlerFatalResponseCodes diff --git a/router/procedures.ts b/router/procedures.ts index 4f743958..65ae9e27 100644 --- a/router/procedures.ts +++ b/router/procedures.ts @@ -352,8 +352,8 @@ function rpc({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - // eslint-disable-next-line @typescript-eslint/ban-types - handler: Function; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler: RpcProcedure['handler']; }) { return { ...(description ? { description } : {}), @@ -453,8 +453,8 @@ function upload({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - // eslint-disable-next-line @typescript-eslint/ban-types - handler: Function; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler: UploadProcedure['handler']; }) { return { type: 'upload', diff --git a/testUtil/fixtures/services.ts b/testUtil/fixtures/services.ts index 072f765c..016fc979 100644 --- a/testUtil/fixtures/services.ts +++ b/testUtil/fixtures/services.ts @@ -1,20 +1,9 @@ import { Type } from 'typebox'; - -class TUint8Array extends Type.Base { - public readonly type = 'Uint8Array'; - - public override Check(value: unknown): value is Uint8Array { - return value instanceof Uint8Array; - } - - public override Clone(): TUint8Array { - return new TUint8Array(); - } -} import { createServiceSchema } from '../../router/services'; import { Err, Ok, unwrapOrThrow } from '../../router/result'; import { Observable } from '../observable/observable'; import { Procedure } from '../../router'; +import { Uint8ArrayType } from '../../customSchemas'; const ServiceSchema = createServiceSchema(); @@ -201,7 +190,7 @@ export const OrderingServiceSchema = ServiceSchema.define( export const BinaryFileServiceSchema = ServiceSchema.define({ getFile: Procedure.rpc({ requestInit: Type.Object({ file: Type.String() }), - responseData: Type.Object({ contents: new TUint8Array() }), + responseData: Type.Object({ contents: Uint8ArrayType() }), async handler({ reqInit: { file } }) { const bytes: Uint8Array = Buffer.from(`contents for file ${file}`); diff --git a/tsup.config.ts b/tsup.config.ts index 4185962d..15120af5 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ 'logging/index.ts', 'codec/index.ts', 'testUtil/index.ts', + 'customSchemas/index.ts', 'transport/index.ts', 'transport/impls/ws/client.ts', 'transport/impls/ws/server.ts', From f844073bf4b92604a32ec76efbffbfbc2957cca1 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 19:15:24 -0600 Subject: [PATCH 6/8] Extract AnyRpcProcedure and AnyUploadProcedure type aliases Co-Authored-By: Claude Opus 4.6 --- router/procedures.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/router/procedures.ts b/router/procedures.ts index 65ae9e27..8db28152 100644 --- a/router/procedures.ts +++ b/router/procedures.ts @@ -258,6 +258,12 @@ export type AnyProcedure< ProcedureErrorSchemaType >; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyRpcProcedure = RpcProcedure; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyUploadProcedure = UploadProcedure; + /** * Represents a map of {@link Procedure}s. * @@ -352,8 +358,7 @@ function rpc({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handler: RpcProcedure['handler']; + handler: AnyRpcProcedure['handler']; }) { return { ...(description ? { description } : {}), @@ -453,8 +458,7 @@ function upload({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handler: UploadProcedure['handler']; + handler: AnyUploadProcedure['handler']; }) { return { type: 'upload', From 66487fd8cace15cd74b630258cf64e081d2c6261 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 19:35:09 -0600 Subject: [PATCH 7/8] Drop CJS outputs (ESM-only) and fix property-keyed error paths typebox 1.x is ESM-only, so CJS require() entries would fail at runtime. Remove all "require" exports and drop cjs from tsup format. Expand validationErrorToRiverErrors to reconstruct per-field paths for additionalProperties, propertyNames, and unevaluatedProperties errors (not just required). Co-Authored-By: Claude Opus 4.6 --- package.json | 50 ++++++++++-------------------------------------- router/errors.ts | 31 ++++++++++++++++++++++++++---- tsup.config.ts | 2 +- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index e7e57062..8dd11866 100644 --- a/package.json +++ b/package.json @@ -4,46 +4,16 @@ "version": "0.216.1", "type": "module", "exports": { - ".": { - "import": "./dist/router/index.js", - "require": "./dist/router/index.cjs" - }, - "./logging": { - "import": "./dist/logging/index.js", - "require": "./dist/logging/index.cjs" - }, - "./protobuf": { - "import": "./dist/protobuf/index.js", - "require": "./dist/protobuf/index.cjs" - }, - "./protobuf/codec": { - "import": "./dist/protobuf/codec.js", - "require": "./dist/protobuf/codec.cjs" - }, - "./codec": { - "import": "./dist/codec/index.js", - "require": "./dist/codec/index.cjs" - }, - "./transport": { - "import": "./dist/transport/index.js", - "require": "./dist/transport/index.cjs" - }, - "./transport/ws/client": { - "import": "./dist/transport/impls/ws/client.js", - "require": "./dist/transport/impls/ws/client.cjs" - }, - "./transport/ws/server": { - "import": "./dist/transport/impls/ws/server.js", - "require": "./dist/transport/impls/ws/server.cjs" - }, - "./test-util": { - "import": "./dist/testUtil/index.js", - "require": "./dist/testUtil/index.cjs" - }, - "./customSchemas": { - "import": "./dist/customSchemas/index.js", - "require": "./dist/customSchemas/index.cjs" - } + ".": "./dist/router/index.js", + "./logging": "./dist/logging/index.js", + "./protobuf": "./dist/protobuf/index.js", + "./protobuf/codec": "./dist/protobuf/codec.js", + "./codec": "./dist/codec/index.js", + "./transport": "./dist/transport/index.js", + "./transport/ws/client": "./dist/transport/impls/ws/client.js", + "./transport/ws/server": "./dist/transport/impls/ws/server.js", + "./test-util": "./dist/testUtil/index.js", + "./customSchemas": "./dist/customSchemas/index.js" }, "sideEffects": [ "./dist/logging/index.js" diff --git a/router/errors.ts b/router/errors.ts index 10cdfcc4..aa916964 100644 --- a/router/errors.ts +++ b/router/errors.ts @@ -9,7 +9,13 @@ import { type TUnion, Type, } from 'typebox'; -import type { TLocalizedValidationError, TRequiredError } from 'typebox/error'; +import type { + TAdditionalPropertiesError, + TLocalizedValidationError, + TPropertyNamesError, + TRequiredError, + TUnevaluatedPropertiesError, +} from 'typebox/error'; /** * {@link UNCAUGHT_ERROR_CODE} is the code that is used when an error is thrown @@ -63,10 +69,27 @@ export const ValidationErrors = Type.Array(ValidationErrorDetails); export function validationErrorToRiverErrors( error: TLocalizedValidationError, ): Array<{ path: string; message: string }> { - if (error.keyword === 'required') { - const { requiredProperties } = (error as TRequiredError).params; + let propertyNames: Array | undefined; + + switch (error.keyword) { + case 'required': + propertyNames = (error as TRequiredError).params.requiredProperties; + break; + case 'additionalProperties': + propertyNames = (error as TAdditionalPropertiesError).params + .additionalProperties; + break; + case 'propertyNames': + propertyNames = (error as TPropertyNamesError).params.propertyNames; + break; + case 'unevaluatedProperties': + propertyNames = (error as TUnevaluatedPropertiesError).params + .unevaluatedProperties as Array; + break; + } - return requiredProperties.map((prop) => ({ + if (propertyNames) { + return propertyNames.map((prop) => ({ path: `${error.instancePath}/${prop}`, message: error.message, })); diff --git a/tsup.config.ts b/tsup.config.ts index 15120af5..dfc154a5 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ 'transport/impls/uds/client.ts', 'transport/impls/uds/server.ts', ], - format: ['esm', 'cjs'], + format: ['esm'], sourcemap: true, clean: true, dts: true, From 88e296b19404d98e150d19e37f9379b5ef3601ba Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 7 May 2026 19:35:50 -0600 Subject: [PATCH 8/8] Update README for typebox 1.x Co-Authored-By: Claude Opus 4.6 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 73a2a5f8..c07fdc7c 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Before proceeding, ensure you have TypeScript 5 installed and configured appropr To use River, install the required packages using npm: ```bash - npm i @replit/river @sinclair/typebox + npm i @replit/river typebox ``` ## Writing services @@ -72,7 +72,7 @@ First, we create a service: ```ts import { createServiceSchema, Procedure, Ok } from '@replit/river'; -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; const ServiceSchema = createServiceSchema(); export const ExampleService = ServiceSchema.define(