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( 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..45e9b6e5 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', + message: 'must have required properties mustSendThings', }, - { path: '/mustSendThings', message: 'Expected string' }, ]), }, }), @@ -463,10 +462,16 @@ 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' }, + 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), + }), ]), }, }), @@ -597,14 +602,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', + 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/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-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..8dd11866 100644 --- a/package.json +++ b/package.json @@ -4,42 +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" - } + ".": "./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" @@ -55,7 +29,7 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.7.0", - "@sinclair/typebox": "~0.34.0" + "typebox": "^1.1.0" }, "devDependencies": { "@bufbuild/buf": "^1.67.0", @@ -63,7 +37,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..c474d550 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 } from 'typebox'; import { createClientHandshakeOptions as createTransportClientHandshakeOptions, createServerHandshakeOptions as createTransportServerHandshakeOptions, @@ -12,12 +12,9 @@ import { } from '../router/handshake'; import { HandshakeErrorCustomHandlerFatalResponseCodes } from '../transport/message'; import { decodeMessageBytes, encodeMessageBytes } from './shared'; +import { Uint8ArrayType } from '../customSchemas'; -/** - * The handshake metadata for protobuf services travels as encoded protobuf bytes - * over River's existing handshake extension slot. - */ -const HandshakeBytesSchema = Type.Uint8Array(); +const HandshakeBytesSchema = Uint8ArrayType(); 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..37740484 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,13 +28,14 @@ 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, CANCEL_CODE, ReaderErrorResultSchema, UNEXPECTED_DISCONNECT_CODE, + validationErrorToRiverErrors, } from './errors'; export interface CallOptions { @@ -450,9 +451,10 @@ function handleProc( { clientId: transport.clientId, transportMessage: msg, - validationErrors: [ - ...Value.Errors(ReaderErrorResultSchema, msg.payload), - ], + validationErrors: Value.Errors( + ReaderErrorResultSchema, + msg.payload, + ).flatMap(validationErrorToRiverErrors), }, ); } @@ -487,7 +489,10 @@ function handleProc( { clientId: transport.clientId, transportMessage: msg, - validationErrors: [...Value.Errors(AnyResultSchema, msg.payload)], + validationErrors: Value.Errors( + AnyResultSchema, + msg.payload, + ).flatMap(validationErrorToRiverErrors), }, ); } 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..aa916964 100644 --- a/router/errors.ts +++ b/router/errors.ts @@ -1,16 +1,21 @@ 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 { + TAdditionalPropertiesError, + TLocalizedValidationError, + TPropertyNamesError, + TRequiredError, + TUnevaluatedPropertiesError, +} from 'typebox/error'; /** * {@link UNCAUGHT_ERROR_CODE} is the code that is used when an error is thrown @@ -33,7 +38,7 @@ export const CANCEL_CODE = 'CANCEL'; type TLiteralString = TLiteral; -type TEnumString = TEnum>; +type TEnumString = TEnum>; export type BaseErrorSchemaType = | TObject<{ @@ -61,18 +66,42 @@ const ValidationErrorDetails = Type.Object({ }); export const ValidationErrors = Type.Array(ValidationErrorDetails); -export function castTypeboxValueErrors( - errors: ValueErrorIterator, -): Static { - const result = []; - for (const error of errors) { - result.push({ - path: error.path, +export function validationErrorToRiverErrors( + error: TLocalizedValidationError, +): Array<{ path: string; message: string }> { + 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; + } + + if (propertyNames) { + return propertyNames.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); } /** @@ -136,7 +165,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..8db28152 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'; @@ -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,14 +358,7 @@ function rpc({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - handler: RpcProcedure< - object, - object, - object, - PayloadType, - PayloadType, - ProcedureErrorSchemaType - >['handler']; + handler: AnyRpcProcedure['handler']; }) { return { ...(description ? { description } : {}), @@ -459,15 +458,7 @@ function upload({ responseData: PayloadType; responseError?: ProcedureErrorSchemaType; description?: string; - handler: UploadProcedure< - object, - object, - object, - PayloadType, - PayloadType, - PayloadType, - ProcedureErrorSchemaType - >['handler']; + handler: AnyUploadProcedure['handler']; }) { 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..cda6e270 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, @@ -10,6 +10,7 @@ import { ValidationErrors, castTypeboxValueErrors, CancelResultSchema, + validationErrorToRiverErrors, } from './errors'; import { AnyService, @@ -31,7 +32,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 +325,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, + ).flatMap(validationErrorToRiverErrors), tags: ['invalid-request'], }); } diff --git a/router/services.ts b/router/services.ts index 7d65ab2e..85124c1e 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, @@ -621,10 +621,7 @@ export function createServiceSchema< export function getSerializedProcErrors( procDef: AnyProcedure, ): ProcedureErrorSchemaType { - if ( - !('responseError' in procDef) || - procDef.responseError[Kind] === 'Never' - ) { + if (!('responseError' in procDef) || 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..016fc979 100644 --- a/testUtil/fixtures/services.ts +++ b/testUtil/fixtures/services.ts @@ -1,8 +1,9 @@ -import { Type } from '@sinclair/typebox'; +import { Type } from 'typebox'; 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(); @@ -189,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: Type.Uint8Array() }), + responseData: Type.Object({ contents: Uint8ArrayType() }), async handler({ reqInit: { file } }) { const bytes: Uint8Array = Buffer.from(`contents for file ${file}`); @@ -211,12 +212,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 +343,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..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, @@ -17,7 +18,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 +244,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, + ).flatMap(validationErrorToRiverErrors), }); 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..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, @@ -18,8 +19,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 +238,10 @@ export abstract class ServerTransport< ...session.loggingMetadata, transportMessage: msg, connectedTo: msg.from, - validationErrors: [ - ...Value.Errors(ControlMessageHandshakeRequestSchema, msg.payload), - ], + validationErrors: Value.Errors( + ControlMessageHandshakeRequestSchema, + msg.payload, + ).flatMap(validationErrorToRiverErrors), }, ); @@ -276,12 +278,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, + ).flatMap(validationErrorToRiverErrors), }, ); 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'; diff --git a/tsup.config.ts b/tsup.config.ts index 4185962d..dfc154a5 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -8,13 +8,14 @@ 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', 'transport/impls/uds/client.ts', 'transport/impls/uds/server.ts', ], - format: ['esm', 'cjs'], + format: ['esm'], sourcemap: true, clean: true, dts: true,