diff --git a/apps/content/docs/integrations/effect.mdx b/apps/content/docs/integrations/effect.mdx index 436bfbaa0..9cd1c3a94 100644 --- a/apps/content/docs/integrations/effect.mdx +++ b/apps/content/docs/integrations/effect.mdx @@ -1,6 +1,6 @@ --- title: "Effect Integration" -description: "Write effectful oRPC handlers with Effect generators, provide services through context, and use Effect Schema for input and output validation." +description: "Write effectful oRPC handlers with Effect generators, call oRPC clients as effects, provide services through context, and use Effect Schema for input and output validation." sidebar: label: "Effect" --- @@ -205,6 +205,44 @@ const handled = program.pipe( All utilities support data-first `catchORPCError(program, handler)` and data-last `program.pipe(catchORPCError(handler))` styles. ::: +## Client Calls + +`createEffectClient` wraps any oRPC client, whether [server-side](/docs/client/server-side) or [client-side](/docs/client/client-side), so every procedure returns a lazy Effect instead of a promise, ready to `yield*` inside Effect generators. The output becomes the success value, and errors land in the error channel with their original types preserved, ready for utilities like `catchORPCErrorCodes`: + +```ts +import { catchORPCErrorCodes, createEffectClient } from '@orpc/experimental-effect' +import { Effect } from 'effect' + +const effectClient = createEffectClient(client) + +const program = Effect.gen(function* () { + const planet = yield* effectClient.planet.find({ id: 1 }) + return planet.name +}).pipe( + catchORPCErrorCodes({ + NOT_FOUND: error => Effect.succeed('unknown'), + }), +) +``` + +You can also combine `Effect.catchIf` with [isInferableError](/docs/client/error-handling#using-safe-and-isinferableerror) to recover from every inferable error in a typesafe way: + +```ts +import { isInferableError } from '@orpc/client' +import { Effect } from 'effect' + +const recovered = effectClient.planet.find({ id: 1 }).pipe( + Effect.catchIf(isInferableError, (error) => { + // error is fully typed here + return Effect.succeed(null) + }), +) +``` + +:::info +The effects are lazy: the client is invoked each time the effect runs, so they work naturally with `Effect.retry`. Interrupting the effect aborts the underlying call. +::: + ## Effect Schema oRPC natively supports [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec), and [Effect Schema](https://effect.website/docs/schema/introduction/) implements that spec through [Schema.toStandardSchemaV1](https://effect.website/docs/schema/standard-schema/): diff --git a/packages/effect/package.json b/packages/effect/package.json index 8468991bf..98db2b65a 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -63,6 +63,7 @@ "effect": ">=4.0.0-beta.90" }, "dependencies": { + "@orpc/client": "workspace:*", "@orpc/contract": "workspace:*", "@orpc/json-schema": "workspace:*", "@orpc/server": "workspace:*", diff --git a/packages/effect/src/client.test-d.ts b/packages/effect/src/client.test-d.ts new file mode 100644 index 000000000..ff7c453a5 --- /dev/null +++ b/packages/effect/src/client.test-d.ts @@ -0,0 +1,81 @@ +import type { Client, ClientContext, ORPCError } from '@orpc/client' +import type { EffectClient } from './client' +import { isInferableError } from '@orpc/client' +import { Effect } from 'effect' +import { createEffectClient } from './client' +import { catchORPCErrorCodes } from './error' + +describe('createEffectClient', () => { + const client = {} as { + ping: Client | ORPCError<'NOT_FOUND', { id: number }>> + optional: Client + nested: { + pong: Client<{ cache: boolean }, { id: number }, { result: string }, Error> + } + } + + const effectClient = createEffectClient(client) + + it('procedures return effects with output and error types preserved', () => { + expectTypeOf(effectClient.ping('test')).toEqualTypeOf< + Effect.Effect | ORPCError<'NOT_FOUND', { id: number }>> + >() + + expectTypeOf(effectClient.nested.pong({ id: 123 }, { context: { cache: true } })).toEqualTypeOf< + Effect.Effect<{ result: string }, Error> + >() + }) + + it('enforces input and options types', () => { + // @ts-expect-error - input must be a string + void effectClient.ping(123) + // @ts-expect-error - invalid context + void effectClient.nested.pong({ id: 123 }, { context: { cache: 'invalid' } }) + // @ts-expect-error - context is required + void effectClient.nested.pong({ id: 123 }) + }) + + it('allows omitting optional input', () => { + void effectClient.optional() + void effectClient.optional('input') + }) + + it('EffectClient type maps nested clients', () => { + expectTypeOf(effectClient).toEqualTypeOf>() + }) + + it('data is unknown when catching by code, because the error may be a hidden non-inferable ORPCError', () => { + const recovered = effectClient.ping('test').pipe( + catchORPCErrorCodes({ + BAD_GATEWAY: (error) => { + expectTypeOf(error.code).toEqualTypeOf<'BAD_GATEWAY'>() + expectTypeOf(error.data).toEqualTypeOf() + + return Effect.succeed('recovered' as const) + }, + }), + ) + + expectTypeOf(recovered).toEqualTypeOf< + Effect.Effect> + >() + }) + + it('combines isInferableError with code narrowing for typesafe error data', () => { + const recovered = effectClient.ping('test').pipe( + Effect.catchIf(isInferableError, (error) => { + expectTypeOf(error).toEqualTypeOf< + ORPCError<'BAD_GATEWAY', { val: string }> | ORPCError<'NOT_FOUND', { id: number }> + >() + + if (error.code === 'BAD_GATEWAY') { + expectTypeOf(error.data).toEqualTypeOf<{ val: string }>() + } + + return Effect.succeed('recovered' as const) + }), + ) + + expectTypeOf(recovered).toEqualTypeOf>() + }) +}) diff --git a/packages/effect/src/client.test.ts b/packages/effect/src/client.test.ts new file mode 100644 index 000000000..63a8dc92b --- /dev/null +++ b/packages/effect/src/client.test.ts @@ -0,0 +1,139 @@ +import type { Client, ClientContext, ORPCError } from '@orpc/client' +import { sleep } from '@orpc/shared' +import { Effect, Exit, Schedule } from 'effect' +import { createEffectClient } from './client' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('createEffectClient', () => { + const pingFn = vi.fn() + const pongFn = vi.fn() + + const client = { + ping: pingFn, + nested: { + pong: pongFn, + }, + invalid: 'invalid', + } as unknown as { + ping: Client> + nested: { + pong: Client + } + } + + const effectClient = createEffectClient(client) + + it('procedures return yieldable effects', async () => { + pingFn.mockResolvedValue(42) + pongFn.mockResolvedValue({ result: 'pong' }) + + const program = Effect.gen(function* () { + const ping = yield* effectClient.ping('input') + const pong = yield* effectClient.nested.pong({ id: 123 }) + return [ping, pong] + }) + + await expect(Effect.runPromise(program)).resolves.toEqual([42, { result: 'pong' }]) + + expect(pingFn).toHaveBeenCalledWith('input', { + context: {}, + signal: expect.any(AbortSignal), + }) + expect(pongFn).toHaveBeenCalledWith({ id: 123 }, { + context: {}, + signal: expect.any(AbortSignal), + }) + }) + + it('forwards context, lastEventId, and merges the signal', async () => { + pingFn.mockResolvedValue(42) + const controller = new AbortController() + + await Effect.runPromise(effectClient.ping('input', { + context: { cache: true }, + lastEventId: 'id-1', + signal: controller.signal, + })) + + const options = pingFn.mock.calls[0]![1] + expect(options.context).toEqual({ cache: true }) + expect(options.lastEventId).toBe('id-1') + expect(options.signal).toBeInstanceOf(AbortSignal) + expect(options.signal.aborted).toBe(false) + + controller.abort() + expect(options.signal.aborted).toBe(true) + }) + + it('captures rejections in the error channel', async () => { + const error = new Error('__TEST__') + pingFn.mockRejectedValue(error) + + const exit = await Effect.runPromiseExit(effectClient.ping('input')) + + expect(exit).toEqual(Exit.fail(error)) + }) + + it('procedures are lazy and re-invoke the client on retry', async () => { + pingFn + .mockRejectedValueOnce(new Error('__TEST__')) + .mockRejectedValueOnce(new Error('__TEST__')) + .mockResolvedValueOnce(42) + + const effect = effectClient.ping('input') + await sleep(0) + expect(pingFn).not.toHaveBeenCalled() + + await expect( + Effect.runPromise(effect.pipe(Effect.retry(Schedule.recurs(2)))), + ).resolves.toBe(42) + + expect(pingFn).toHaveBeenCalledTimes(3) + }) + + it('aborts the call when the effect is interrupted', async () => { + let clientSignal: AbortSignal | undefined + pingFn.mockImplementation((_input, options) => { + clientSignal = options.signal + return new Promise(() => {}) + }) + + const signal = AbortSignal.timeout(10) + const exit = await Effect.runPromiseExit(effectClient.ping('input'), { signal }) + + expect(Exit.hasInterrupts(exit)).toBe(true) + expect(clientSignal!.aborted).toBe(true) + }) + + it('returns strictly equal client on repeated access', () => { + expect(effectClient.ping).toBe(effectClient.ping) + expect(effectClient.nested).toBe(effectClient.nested) + expect(effectClient.nested.pong).toBe(effectClient.nested.pong) + expect(effectClient.nested.pong).not.toBe(effectClient.ping as unknown) + + expect(createEffectClient(client)).not.toBe(effectClient) + }) + + it('not proxy on non-object or symbol properties', () => { + expect((effectClient as any).invalid).toBe('invalid') + expect((effectClient as any)[Symbol('test')]).toEqual(undefined) + expect((effectClient.nested as any)[Symbol('test')]).toEqual(undefined) + }) + + it('not recursive on unwrap keys', async () => { + const anyClient = effectClient as any + + expect(anyClient.then).toBeUndefined() + expect(await anyClient).toBe(effectClient) + expect(anyClient.bind).toBe(anyClient.bind) + expect(anyClient.valueOf).toBe(anyClient.valueOf) + expect(anyClient.toString).toBe(anyClient.toString) + expect(anyClient.toJSON).toBeUndefined() + + expect(anyClient.nested.then).toBeUndefined() + expect(await anyClient.nested).toBe(anyClient.nested) + }) +}) diff --git a/packages/effect/src/client.ts b/packages/effect/src/client.ts new file mode 100644 index 000000000..278ad233d --- /dev/null +++ b/packages/effect/src/client.ts @@ -0,0 +1,63 @@ +import type { AnyNestedClient, Client, ClientContext, ClientRest } from '@orpc/client' +import { RECURSIVE_CLIENT_UNWRAP_KEYS, resolveClientRest } from '@orpc/client' +import { anyAbortSignal, isTypescriptObject } from '@orpc/shared' +import { Effect } from 'effect' + +function callAsEffect( + client: Client, + ...rest: ClientRest +): Effect.Effect { + const [input, options] = resolveClientRest(rest) + + return Effect.tryPromise({ + try: signal => client(input, { + ...options, + signal: anyAbortSignal([options.signal, signal]), + }), + catch: error => error as TError, + }) +} + +export type EffectClient + = T extends Client + ? (...rest: ClientRest) => Effect.Effect + : { + [K in keyof T]: T[K] extends AnyNestedClient ? EffectClient : never + } + +/** + * Creates a client whose procedures return lazy effects instead of promises, + * so you can `yield*` calls inside Effect generators. Errors are captured in + * the error channel with their original types preserved, and interrupting an + * effect aborts the underlying call. + * + * @see {@link https://orpc.dev/docs/integrations/effect#client-calls | Effect Integration - Client Calls} + */ +export function createEffectClient(client: T): EffectClient { + const cache = new Map>() + + const proxy = new Proxy((...rest: any[]) => callAsEffect(client as Client, ...rest), { + get(target, prop) { + if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + return Reflect.get(target, prop) + } + + let effectClient = cache.get(prop) + + if (effectClient === undefined) { + const value = (client as Record)[prop] + + if (!isTypescriptObject(value)) { + return value + } + + effectClient = createEffectClient(value as AnyNestedClient) + cache.set(prop, effectClient) + } + + return effectClient + }, + }) + + return proxy as EffectClient +} diff --git a/packages/effect/src/error.test-d.ts b/packages/effect/src/error.test-d.ts index dae928446..65cd32dce 100644 --- a/packages/effect/src/error.test-d.ts +++ b/packages/effect/src/error.test-d.ts @@ -1,4 +1,4 @@ -import type { ORPCError } from '@orpc/server' +import type { ORPCError, ORPCErrorCode } from '@orpc/server' import { Effect } from 'effect' import { catchORPCError, catchORPCErrorCode, catchORPCErrorCodes } from './error' @@ -12,7 +12,13 @@ class Service2 { const effect = {} as Effect.Effect< 'output', - ORPCError<'NOT_FOUND', { id: string }> | ORPCError<'CONFLICT', number> | TypeError, + ORPCError<'NOT_FOUND', { id: string }> | ORPCError<'CONFLICT', number>, + Service1 +> + +const effectWithHidden = {} as Effect.Effect< + 'output', + ORPCError<'NOT_FOUND', { id: string }> | Error, Service1 > @@ -23,7 +29,7 @@ describe('catchORPCError', () => { return Effect.succeed('recovered' as const) })) - expectTypeOf(recovered).toEqualTypeOf>() + expectTypeOf(recovered).toEqualTypeOf>() }) it('catches every ORPCError and excludes them from the error channel (data-first)', () => { @@ -32,7 +38,7 @@ describe('catchORPCError', () => { return Effect.succeed('recovered' as const) }) - expectTypeOf(recovered).toEqualTypeOf>() + expectTypeOf(recovered).toEqualTypeOf>() }) it('merges handler error and requirement channels into the result', () => { @@ -40,13 +46,41 @@ describe('catchORPCError', () => { catchORPCError(() => ({} as Effect.Effect<'recovered', RangeError, Service2>)), ) - expectTypeOf(recovered).toEqualTypeOf>() + expectTypeOf(recovered).toEqualTypeOf>() }) - it('handler receives never when the error channel contains no ORPCError', () => { - const safe = {} as Effect.Effect<'output', TypeError> + it('also receives hidden ORPCErrors, with unknown code and data', () => { + const recovered = effectWithHidden.pipe(catchORPCError((error) => { + expectTypeOf(error).toEqualTypeOf | ORPCError>() + expectTypeOf(error.data).toEqualTypeOf() + return Effect.succeed('recovered' as const) + })) + + expectTypeOf(recovered).toEqualTypeOf>() + }) + + it('treats failure types that can hold an ORPCError as possibly hiding one', () => { + const withTypeError = {} as Effect.Effect<'output', ORPCError<'NOT_FOUND', { id: string }> | TypeError> + + void withTypeError.pipe(catchORPCError((error) => { + expectTypeOf(error).toEqualTypeOf | ORPCError>() + expectTypeOf(error.data).toEqualTypeOf() + return Effect.succeed('recovered' as const) + })) + }) + + it('does not treat failure types that cannot hold an ORPCError as hidden', () => { + const withString = {} as Effect.Effect<'output', ORPCError<'NOT_FOUND', { id: string }> | 'boom'> + + void withString.pipe(catchORPCError((error) => { + expectTypeOf(error).toEqualTypeOf>() + expectTypeOf(error.data).toEqualTypeOf<{ id: string }>() + return Effect.succeed('recovered' as const) + })) + + const onlyString = {} as Effect.Effect<'output', 'boom'> - void safe.pipe(catchORPCError((error) => { + void onlyString.pipe(catchORPCError((error) => { expectTypeOf(error).toEqualTypeOf() return Effect.succeed('recovered' as const) })) @@ -60,7 +94,7 @@ describe('catchORPCErrorCode', () => { return Effect.succeed('recovered' as const) })) - expectTypeOf(recovered).toEqualTypeOf | TypeError, Service1>>() + expectTypeOf(recovered).toEqualTypeOf, Service1>>() }) it('catches ORPCErrors with a matching code and excludes them from the error channel (data-first)', () => { @@ -69,7 +103,7 @@ describe('catchORPCErrorCode', () => { return Effect.succeed('recovered' as const) }) - expectTypeOf(recovered).toEqualTypeOf | TypeError, Service1>>() + expectTypeOf(recovered).toEqualTypeOf, Service1>>() }) it('merges handler error and requirement channels into the result', () => { @@ -78,19 +112,19 @@ describe('catchORPCErrorCode', () => { ) expectTypeOf(recovered).toEqualTypeOf< - Effect.Effect<'output' | 'recovered', ORPCError<'CONFLICT', number> | RangeError | TypeError, Service1 | Service2> + Effect.Effect<'output' | 'recovered', ORPCError<'CONFLICT', number> | RangeError, Service1 | Service2> >() }) it('supports custom error codes', () => { - const custom = {} as Effect.Effect<'output', ORPCError<'__CUSTOM__', undefined> | TypeError> + const custom = {} as Effect.Effect<'output', ORPCError<'__CUSTOM__', undefined>> const recovered = custom.pipe(catchORPCErrorCode('__CUSTOM__', (error) => { expectTypeOf(error).toEqualTypeOf>() return Effect.succeed('recovered' as const) })) - expectTypeOf(recovered).toEqualTypeOf>() + expectTypeOf(recovered).toEqualTypeOf>() }) it('suggests and restricts the code to those present in the error channel', () => { @@ -103,6 +137,49 @@ describe('catchORPCErrorCode', () => { // @ts-expect-error - code must be a string void effect.pipe(catchORPCErrorCode(123, () => Effect.succeed('recovered'))) }) + + it('widens data to unknown on declared codes when the channel can hold hidden ORPCErrors', () => { + const recovered = effectWithHidden.pipe(catchORPCErrorCode('NOT_FOUND', (error) => { + expectTypeOf(error).toEqualTypeOf | ORPCError<'NOT_FOUND', unknown>>() + expectTypeOf(error.data).toEqualTypeOf() + return Effect.succeed('recovered' as const) + })) + + expectTypeOf(recovered).toEqualTypeOf>() + }) + + it('accepts undeclared codes when the channel can hold hidden ORPCErrors', () => { + const recovered = effectWithHidden.pipe(catchORPCErrorCode('CONFLICT', (error) => { + expectTypeOf(error).toEqualTypeOf>() + return Effect.succeed('recovered' as const) + })) + + expectTypeOf(recovered).toEqualTypeOf< + Effect.Effect<'output' | 'recovered', ORPCError<'NOT_FOUND', { id: string }> | Error, Service1> + >() + }) + + it('unknown error channels can also hold hidden ORPCErrors', () => { + const withUnknown = {} as Effect.Effect<'output', unknown> + + void withUnknown.pipe(catchORPCErrorCode('CONFLICT', (error) => { + expectTypeOf(error).toEqualTypeOf>() + return Effect.succeed('recovered' as const) + })) + }) + + it('keeps data typed and codes restricted when the channel cannot hold hidden ORPCErrors', () => { + const withString = {} as Effect.Effect<'output', ORPCError<'NOT_FOUND', { id: string }> | 'boom'> + + void withString.pipe(catchORPCErrorCode('NOT_FOUND', (error) => { + expectTypeOf(error).toEqualTypeOf>() + expectTypeOf(error.data).toEqualTypeOf<{ id: string }>() + return Effect.succeed('recovered' as const) + })) + + // @ts-expect-error - BAD_GATEWAY is not present and the channel cannot hold hidden ORPCErrors + void withString.pipe(catchORPCErrorCode('BAD_GATEWAY', () => Effect.succeed('recovered'))) + }) }) describe('catchORPCErrorCodes', () => { @@ -118,7 +195,7 @@ describe('catchORPCErrorCodes', () => { }, })) - expectTypeOf(recovered).toEqualTypeOf>() + expectTypeOf(recovered).toEqualTypeOf>() }) it('keeps unhandled codes in the error channel (data-first)', () => { @@ -129,7 +206,7 @@ describe('catchORPCErrorCodes', () => { }, }) - expectTypeOf(recovered).toEqualTypeOf | TypeError, Service1>>() + expectTypeOf(recovered).toEqualTypeOf, Service1>>() }) it('suggests and restricts keys to the codes present in the error channel', () => { @@ -139,4 +216,34 @@ describe('catchORPCErrorCodes', () => { // @ts-expect-error - BAD_GATEWAY is not present in the error channel (data-first) void catchORPCErrorCodes(effect, { BAD_GATEWAY: () => Effect.succeed('recovered') }) }) + + it('widens data to unknown and accepts undeclared keys when the channel can hold hidden ORPCErrors', () => { + const recovered = effectWithHidden.pipe(catchORPCErrorCodes({ + NOT_FOUND: (error) => { + expectTypeOf(error.data).toEqualTypeOf() + return Effect.succeed('nf' as const) + }, + CONFLICT: (error) => { + expectTypeOf(error).toEqualTypeOf>() + return Effect.succeed('cf' as const) + }, + })) + + expectTypeOf(recovered).toEqualTypeOf>() + }) + + it('keeps data typed and keys restricted when the channel cannot hold hidden ORPCErrors', () => { + const withString = {} as Effect.Effect<'output', ORPCError<'NOT_FOUND', { id: string }> | 'boom'> + + void withString.pipe(catchORPCErrorCodes({ + NOT_FOUND: (error) => { + expectTypeOf(error).toEqualTypeOf>() + expectTypeOf(error.data).toEqualTypeOf<{ id: string }>() + return Effect.succeed('recovered' as const) + }, + })) + + // @ts-expect-error - BAD_GATEWAY is not present and the channel cannot hold hidden ORPCErrors + void withString.pipe(catchORPCErrorCodes({ BAD_GATEWAY: () => Effect.succeed('recovered') })) + }) }) diff --git a/packages/effect/src/error.ts b/packages/effect/src/error.ts index 8e43e7689..320c49a28 100644 --- a/packages/effect/src/error.ts +++ b/packages/effect/src/error.ts @@ -5,7 +5,9 @@ import { Effect, Function } from 'effect' /** * Recovers from `ORPCError` failures in the error channel of an effect. - * Other failures re-fail with their original cause. + * Other failures re-fail with their original cause. Because failure types + * like `Error` can also hold `ORPCError` instances at runtime, the handler + * can also receive such hidden errors with unknown code and data. * * Supports both data-first `catchORPCError(effect, handler)` and data-last * `effect.pipe(catchORPCError(handler))` styles. @@ -23,11 +25,11 @@ import { Effect, Function } from 'effect' */ export const catchORPCError: { ( - f: (error: Extract) => Effect.Effect, + f: (error: E extends AnyORPCError ? E : AnyORPCError extends E ? ORPCError : never) => Effect.Effect, ): (self: Effect.Effect) => Effect.Effect, R | R2> ( self: Effect.Effect, - f: (error: Extract) => Effect.Effect, + f: (error: E extends AnyORPCError ? E : AnyORPCError extends E ? ORPCError : never) => Effect.Effect, ): Effect.Effect, R | R2> } = Function.dual( 2, @@ -42,7 +44,9 @@ export const catchORPCError: { /** * Recovers from `ORPCError` failures with a matching code in the error channel of an effect. * Other failures re-fail with their original cause. The `code` argument is - * suggested and restricted to the codes present in the error channel. + * suggested and restricted to the codes present in the error channel, and is + * unrestricted when the channel contains failure types like `Error` that can + * hold hidden `ORPCError` instances, whose data is unknown. * * Supports both data-first `catchORPCErrorCode(effect, code, handler)` and * data-last `effect.pipe(catchORPCErrorCode(code, handler))` styles. @@ -51,7 +55,7 @@ export const catchORPCError: { * ```ts * declare const program: Effect.Effect< * string, - * ORPCError<'NOT_FOUND', { id: string }> | ORPCError<'CONFLICT', undefined> | TypeError + * ORPCError<'NOT_FOUND', { id: string }> | ORPCError<'CONFLICT', undefined> * > * * const recovered = program.pipe( @@ -62,14 +66,14 @@ export const catchORPCError: { * @see {@link https://orpc.dev/docs/integrations/effect#catching-orpcerrors | Effect Integration - Catching ORPCErrors} */ export const catchORPCErrorCode: { - ? TCode : never, E, A2, E2, R2>( + ? TCode : AnyORPCError extends E ? ORPCErrorCode : never, E, A2, E2, R2>( code: TCode, - f: (error: Extract>) => Effect.Effect, + f: (error: E extends AnyORPCError ? Extract> : AnyORPCError extends E ? ORPCError : never) => Effect.Effect, ): (self: Effect.Effect) => Effect.Effect>, R | R2> - ? TCode : never, A2, E2, R2>( + ? TCode : AnyORPCError extends E ? ORPCErrorCode : never, A2, E2, R2>( self: Effect.Effect, code: TCode, - f: (error: Extract>) => Effect.Effect, + f: (error: E extends AnyORPCError ? Extract> : AnyORPCError extends E ? ORPCError : never) => Effect.Effect, ): Effect.Effect>, R | R2> } = Function.dual( 3, @@ -86,7 +90,8 @@ export const catchORPCErrorCode: { * Recovers from `ORPCError` failures using a map of codes to handlers in the * error channel of an effect. Other failures re-fail with their original * cause. The keys are suggested and restricted to the codes present in the - * error channel. + * error channel, and are unrestricted when the channel contains failure types + * like `Error` that can hold hidden `ORPCError` instances, whose data is unknown. * * Supports both data-first `catchORPCErrorCodes(effect, cases)` and data-last * `effect.pipe(catchORPCErrorCodes(cases))` styles. @@ -95,7 +100,7 @@ export const catchORPCErrorCode: { * ```ts * declare const program: Effect.Effect< * string, - * ORPCError<'NOT_FOUND', { id: string }> | ORPCError<'CONFLICT', undefined> | TypeError + * ORPCError<'NOT_FOUND', { id: string }> | ORPCError<'CONFLICT', undefined> * > * * const recovered = program.pipe( @@ -112,8 +117,12 @@ export const catchORPCErrorCodes: { < E, Cases extends - & { [K in Extract['code']]?: (error: Extract>) => Effect.Effect } - & (unknown extends E ? object : { [K in Exclude['code']>]: never }), + & { [K in E extends ORPCError ? TCode : AnyORPCError extends E ? ORPCErrorCode : never]?: string extends K + ? (error: never) => Effect.Effect + : K extends infer C extends ORPCErrorCode + ? (error: E extends AnyORPCError ? Extract> : AnyORPCError extends E ? ORPCError : never) => Effect.Effect + : never } + & { [K in Exclude ? TCode : AnyORPCError extends E ? ORPCErrorCode : never>]: never }, >( cases: Cases, ): (self: Effect.Effect) => Effect.Effect< @@ -127,8 +136,12 @@ export const catchORPCErrorCodes: { E, R, Cases extends - & { [K in Extract['code']]?: (error: Extract>) => Effect.Effect } - & (unknown extends E ? object : { [K in Exclude['code']>]: never }), + & { [K in E extends ORPCError ? TCode : AnyORPCError extends E ? ORPCErrorCode : never]?: string extends K + ? (error: never) => Effect.Effect + : K extends infer C extends ORPCErrorCode + ? (error: E extends AnyORPCError ? Extract> : AnyORPCError extends E ? ORPCError : never) => Effect.Effect + : never } + & { [K in Exclude ? TCode : AnyORPCError extends E ? ORPCErrorCode : never>]: never }, >( self: Effect.Effect, cases: Cases, diff --git a/packages/effect/src/index.test.ts b/packages/effect/src/index.test.ts index 565f58ff1..aebdb2d3f 100644 --- a/packages/effect/src/index.test.ts +++ b/packages/effect/src/index.test.ts @@ -1,4 +1,4 @@ -it('exports EffectSchemaToJsonSchemaConverter, handlerGen, toStandardSchema, catchORPCError, catchORPCErrorCode, catchORPCErrorCodes', async () => { +it('exports', async () => { await expect(import('./index')).resolves.toMatchObject({ handlerGen: expect.any(Function), EffectSchemaToJsonSchemaConverter: expect.any(Function), @@ -6,5 +6,6 @@ it('exports EffectSchemaToJsonSchemaConverter, handlerGen, toStandardSchema, cat catchORPCError: expect.any(Function), catchORPCErrorCode: expect.any(Function), catchORPCErrorCodes: expect.any(Function), + createEffectClient: expect.any(Function), }) }) diff --git a/packages/effect/src/index.ts b/packages/effect/src/index.ts index ff02f9a5e..c5a7c431a 100644 --- a/packages/effect/src/index.ts +++ b/packages/effect/src/index.ts @@ -1,3 +1,4 @@ +export * from './client' export * from './context' export * from './converter' export * from './error' diff --git a/packages/effect/tsconfig.json b/packages/effect/tsconfig.json index 9e694d2aa..fe827abfe 100644 --- a/packages/effect/tsconfig.json +++ b/packages/effect/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.lib.json", "references": [ + { "path": "../client" }, { "path": "../contract" }, { "path": "../server" }, { "path": "../json-schema" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9130de69..676a5d5ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -360,7 +360,7 @@ importers: version: 26.4.0 blume: specifier: ^1.5.3 - version: 1.5.3(e4d105bdff9514874e7c901a765e4db3) + version: 1.5.3(@astrojs/cloudflare@14.2.5(@types/node@26.4.0)(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(wrangler@4.126.0)(yaml@2.9.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.2)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(vue@3.5.42(typescript@6.0.3)) effect: specifier: 4.0.0-rc.112 version: 4.0.0-rc.112 @@ -517,6 +517,9 @@ importers: packages/effect: dependencies: + '@orpc/client': + specifier: workspace:* + version: link:../client '@orpc/contract': specifier: workspace:* version: link:../contract @@ -12766,9 +12769,9 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@astrojs/check@0.9.10(prettier@3.9.6)(typescript@6.0.3)': + '@astrojs/check@0.9.10(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.15(prettier@3.9.6)(typescript@6.0.3) + '@astrojs/language-server': 2.16.15(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -12869,7 +12872,7 @@ snapshots: smol-toml: 1.8.0 unified: 11.0.5 - '@astrojs/language-server@2.16.15(prettier@3.9.6)(typescript@6.0.3)': + '@astrojs/language-server@2.16.15(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -12883,14 +12886,12 @@ snapshots: volar-service-css: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)) volar-service-emmet: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)) volar-service-html: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)) - volar-service-prettier: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(prettier@3.9.6) + volar-service-prettier: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)) volar-service-typescript: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3) volar-service-typescript-twoslash-queries: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3) volar-service-yaml: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)) vscode-html-languageservice: 5.6.2 vscode-uri: 3.2.0 - optionalDependencies: - prettier: 3.9.6 transitivePeerDependencies: - typescript @@ -12923,11 +12924,11 @@ snapshots: github-slugger: 2.0.0 satteri: 0.10.5 - '@astrojs/mdx@7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(supports-color@10.2.2)': + '@astrojs/mdx@7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.4 '@astrojs/markdown-remark': 7.2.4(supports-color@10.2.2) - '@mdx-js/mdx': 3.1.1(supports-color@10.2.2) + '@mdx-js/mdx': 3.1.1 acorn: 8.18.0 astro: 7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0) es-module-lexer: 2.3.2 @@ -12945,7 +12946,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/node@11.1.4(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(supports-color@10.2.2)': + '@astrojs/node@11.1.4(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.4 astro: 7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0) @@ -12958,12 +12959,12 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@6.0.4(@types/node@26.4.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.2)(jiti@2.7.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)(terser@5.51.1)(yaml@2.9.0)': + '@astrojs/react@6.0.4(@types/node@26.4.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.2)(jiti@2.7.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@astrojs/internal-helpers': 0.10.4 '@types/react': 19.2.18 '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@vitejs/plugin-react': 5.2.0(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0)) devalue: 5.9.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -12993,12 +12994,12 @@ snapshots: '@astrojs/underscore-redirects@1.0.4': {} - '@astrojs/vercel@11.0.8(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(next@16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rollup@4.63.0)(supports-color@10.2.2)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))(ws@8.21.3)': + '@astrojs/vercel@11.0.8(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))': dependencies: '@astrojs/internal-helpers': 0.10.4 - '@vercel/analytics': 1.6.1(next@16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3)) + '@vercel/analytics': 1.6.1(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3)) '@vercel/functions': 3.9.5(ws@8.21.3) - '@vercel/nft': 1.11.0(rollup@4.63.0)(supports-color@10.2.2) + '@vercel/nft': 1.11.0 '@vercel/routing-utils': 5.3.3 astro: 7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0) esbuild: 0.28.2 @@ -13071,6 +13072,26 @@ snapshots: '@babel/compat-data@7.29.7': {} + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@10.2.2) + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/core@7.29.7(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 @@ -13125,6 +13146,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + '@babel/helper-plugin-utils@7.29.7': {} '@babel/helper-string-parser@7.29.7': {} @@ -13145,14 +13175,14 @@ snapshots: dependencies: '@babel/types': 7.29.8 - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/runtime-corejs3@7.29.7': @@ -14494,7 +14524,7 @@ snapshots: '@lukeed/csprng@1.1.0': {} - '@mapbox/node-pre-gyp@2.0.3(supports-color@10.2.2)': + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 detect-libc: 2.1.2 @@ -14509,7 +14539,7 @@ snapshots: '@marijn/find-cluster-break@1.0.4': {} - '@mdx-js/mdx@3.1.1(supports-color@10.2.2)': + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -14521,13 +14551,13 @@ snapshots: estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.1 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6(supports-color@10.2.2) + hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 recma-jsx: 1.0.1(acorn@8.18.0) recma-stringify: 1.0.0 - rehype-recma: 1.0.0(supports-color@10.2.2) - remark-mdx: 3.1.1(supports-color@10.2.2) + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 remark-parse: 11.0.0(supports-color@10.2.2) remark-rehype: 11.1.2 source-map: 0.7.6 @@ -14554,7 +14584,7 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)': dependencies: '@hono/node-server': 2.1.1(hono@4.13.5) ajv: 8.20.0 @@ -14565,7 +14595,7 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.1.1 express: 5.2.1(supports-color@10.2.2) - express-rate-limit: 8.6.2(express@5.2.1(supports-color@10.2.2))(supports-color@10.2.2) + express-rate-limit: 8.6.2(express@5.2.1) hono: 4.13.5 jose: 6.2.10 json-schema-typed: 8.0.2 @@ -15629,10 +15659,10 @@ snapshots: '@phosphor-icons/core@2.1.1': {} - '@pierre/diffs@1.3.6(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@pierre/diffs@1.3.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@pierre/theme': 2.0.0 - '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3) + '@pierre/theming': 1.0.1(@pierre/theme@2.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3) '@shikijs/transformers': 4.4.3 diff: 9.0.0 hast-util-to-html: 9.0.5 @@ -15645,10 +15675,9 @@ snapshots: '@pierre/theme@2.0.0': {} - '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3)': + '@pierre/theming@1.0.1(@pierre/theme@2.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@4.4.3)': optionalDependencies: '@pierre/theme': 2.0.0 - '@shikijs/themes': 4.4.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) shiki: 4.4.3 @@ -16292,11 +16321,11 @@ snapshots: '@shikijs/core': 4.4.3 '@shikijs/types': 4.4.3 - '@shikijs/twoslash@4.4.3(supports-color@10.2.2)(typescript@6.0.3)': + '@shikijs/twoslash@4.4.3(typescript@6.0.3)': dependencies: '@shikijs/core': 4.4.3 '@shikijs/types': 4.4.3 - twoslash: 0.3.9(supports-color@10.2.2)(typescript@6.0.3) + twoslash: 0.3.9(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -17082,7 +17111,7 @@ snapshots: '@takumi-rs/core-win32-x64-msvc@2.12.0': optional: true - '@takumi-rs/core@2.12.0(csstype@3.2.3)(react@19.2.8)': + '@takumi-rs/core@2.12.0(react@19.2.8)': dependencies: '@takumi-rs/helpers': 2.12.0(react@19.2.8) optionalDependencies: @@ -17094,7 +17123,6 @@ snapshots: '@takumi-rs/core-linux-x64-musl': 2.12.0 '@takumi-rs/core-win32-arm64-msvc': 2.12.0 '@takumi-rs/core-win32-x64-msvc': 2.12.0 - csstype: 3.2.3 transitivePeerDependencies: - preact - react @@ -17103,11 +17131,9 @@ snapshots: optionalDependencies: react: 19.2.8 - '@takumi-rs/wasm@2.12.0(csstype@3.2.3)(react@19.2.8)': + '@takumi-rs/wasm@2.12.0(react@19.2.8)': dependencies: '@takumi-rs/helpers': 2.12.0(react@19.2.8) - optionalDependencies: - csstype: 3.2.3 transitivePeerDependencies: - preact - react @@ -17676,7 +17702,7 @@ snapshots: '@typescript-eslint/types': 8.68.0 eslint-visitor-keys: 5.0.1 - '@typescript/vfs@1.6.4(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript/vfs@1.6.4(typescript@6.0.3)': dependencies: debug: 4.4.3(supports-color@10.2.2) typescript: 6.0.3 @@ -17713,16 +17739,14 @@ snapshots: dependencies: valibot: 1.4.2(typescript@6.0.3) - '@vercel/analytics@1.6.1(next@16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))': + '@vercel/analytics@1.6.1(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))': optionalDependencies: - next: 16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 svelte: 5.56.10(@typescript-eslint/types@8.68.0) vue: 3.5.42(typescript@6.0.3) - '@vercel/analytics@2.0.1(next@16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))': + '@vercel/analytics@2.0.1(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))': optionalDependencies: - next: 16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 svelte: 5.56.10(@typescript-eslint/types@8.68.0) vue: 3.5.42(typescript@6.0.3) @@ -17742,9 +17766,9 @@ snapshots: optionalDependencies: ws: 8.21.3 - '@vercel/nft@1.11.0(rollup@4.63.0)(supports-color@10.2.2)': + '@vercel/nft@1.11.0': dependencies: - '@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2) + '@mapbox/node-pre-gyp': 2.0.3 '@rollup/pluginutils': 5.4.0(rollup@4.63.0) acorn: 8.18.0 acorn-import-attributes: 1.9.5(acorn@8.18.0) @@ -17788,11 +17812,11 @@ snapshots: optionalDependencies: ajv: 6.15.0 - '@vitejs/plugin-react@5.2.0(supports-color@10.2.2)(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.7(supports-color@10.2.2) - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 @@ -18532,31 +18556,31 @@ snapshots: blake3-wasm@2.1.5: {} - blume@1.5.3(e4d105bdff9514874e7c901a765e4db3): + blume@1.5.3(@astrojs/cloudflare@14.2.5(@types/node@26.4.0)(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(wrangler@4.126.0)(yaml@2.9.0))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.2)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(vue@3.5.42(typescript@6.0.3)): dependencies: - '@astrojs/check': 0.9.10(prettier@3.9.6)(typescript@6.0.3) + '@astrojs/check': 0.9.10(typescript@6.0.3) '@astrojs/markdown-satteri': 0.3.8 - '@astrojs/mdx': 7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(supports-color@10.2.2) - '@astrojs/node': 11.1.4(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(supports-color@10.2.2) - '@astrojs/react': 6.0.4(@types/node@26.4.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.2)(jiti@2.7.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)(terser@5.51.1)(yaml@2.9.0) - '@astrojs/vercel': 11.0.8(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(next@16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rollup@4.63.0)(supports-color@10.2.2)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3))(ws@8.21.3) + '@astrojs/mdx': 7.0.8(@astrojs/markdown-satteri@0.3.8)(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0)) + '@astrojs/node': 11.1.4(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0)) + '@astrojs/react': 6.0.4(@types/node@26.4.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.2)(jiti@2.7.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@astrojs/vercel': 11.0.8(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0))(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3)) '@asyncapi/converter': 2.0.2 '@clack/prompts': 1.7.0 '@iconify-json/lucide': 1.2.126 '@iconify/types': 2.0.0 '@iconify/utils': 3.1.4 - '@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3) '@orama/orama': 3.1.18 - '@pierre/diffs': 1.3.6(@shikijs/themes@4.4.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@pierre/diffs': 1.3.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@scalar/astro': 0.4.16(astro@7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0)) '@scalar/openapi-parser': 0.28.16 '@scalar/openapi-types': 0.9.5 '@shikijs/transformers': 4.4.3 - '@shikijs/twoslash': 4.4.3(supports-color@10.2.2)(typescript@6.0.3) + '@shikijs/twoslash': 4.4.3(typescript@6.0.3) '@tailwindcss/typography': 0.5.20(tailwindcss@4.3.3) '@tailwindcss/vite': 4.3.3(vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0)) '@types/mdast': 4.0.4 - '@vercel/analytics': 2.0.1(next@16.3.3(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3)) + '@vercel/analytics': 2.0.1(react@19.2.8)(svelte@5.56.10(@typescript-eslint/types@8.68.0))(vue@3.5.42(typescript@6.0.3)) ai: 7.0.83(zod@4.4.3) astro: 7.2.8(@astrojs/markdown-remark@7.2.4(supports-color@10.2.2))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.4.0)(@upstash/redis@1.38.3)(@vercel/functions@3.9.5(ws@8.21.3))(jiti@2.7.0)(terser@5.51.1)(yaml@2.9.0) babel-plugin-react-compiler: 1.0.0 @@ -18605,9 +18629,9 @@ snapshots: simple-icons: 13.21.0 string-width: 8.2.2 tailwindcss: 4.3.3 - takumi-js: 2.12.0(csstype@3.2.3)(react@19.2.8) + takumi-js: 2.12.0(react@19.2.8) tinyglobby: 0.2.17 - twoslash: 0.3.9(supports-color@10.2.2)(typescript@6.0.3) + twoslash: 0.3.9(typescript@6.0.3) typescript: 6.0.3 ufo: 1.6.4 undici: 8.10.0 @@ -20221,7 +20245,7 @@ snapshots: expr-eval-fork@3.0.3: {} - express-rate-limit@8.6.2(express@5.2.1(supports-color@10.2.2))(supports-color@10.2.2): + express-rate-limit@8.6.2(express@5.2.1): dependencies: debug: 4.4.3(supports-color@10.2.2) express: 5.2.1(supports-color@10.2.2) @@ -20831,7 +20855,7 @@ snapshots: '@ungap/structured-clone': 1.3.4 unist-util-position: 5.0.0 - hast-util-to-estree@3.1.3(supports-color@10.2.2): + hast-util-to-estree@3.1.3: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -20841,9 +20865,9 @@ snapshots: estree-util-attach-comments: 3.0.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1(supports-color@10.2.2) - mdast-util-mdx-jsx: 3.2.0(supports-color@10.2.2) - mdast-util-mdxjs-esm: 2.0.1(supports-color@10.2.2) + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -20866,7 +20890,7 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 - hast-util-to-jsx-runtime@2.3.6(supports-color@10.2.2): + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 @@ -20875,9 +20899,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1(supports-color@10.2.2) - mdast-util-mdx-jsx: 3.2.0(supports-color@10.2.2) - mdast-util-mdxjs-esm: 2.0.1(supports-color@10.2.2) + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -21800,7 +21824,7 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1(supports-color@10.2.2): + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -21811,7 +21835,7 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0(supports-color@10.2.2): + mdast-util-mdx-jsx@3.2.0: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -21828,17 +21852,17 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0(supports-color@10.2.2): + mdast-util-mdx@3.0.0: dependencies: mdast-util-from-markdown: 2.0.3(supports-color@10.2.2) - mdast-util-mdx-expression: 2.0.1(supports-color@10.2.2) - mdast-util-mdx-jsx: 3.2.0(supports-color@10.2.2) - mdast-util-mdxjs-esm: 2.0.1(supports-color@10.2.2) + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1(supports-color@10.2.2): + mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -22432,6 +22456,33 @@ snapshots: - '@types/node' - babel-plugin-macros + next@16.3.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@next/env': 16.3.3 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.19 + caniuse-lite: 1.0.30001810 + postcss: 8.5.23 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.3 + '@next/swc-darwin-x64': 16.3.3 + '@next/swc-linux-arm64-gnu': 16.3.3 + '@next/swc-linux-arm64-musl': 16.3.3 + '@next/swc-linux-x64-gnu': 16.3.3 + '@next/swc-linux-x64-musl': 16.3.3 + '@next/swc-win32-arm64-msvc': 16.3.3 + '@next/swc-win32-x64-msvc': 16.3.3 + '@opentelemetry/api': 1.9.1 + babel-plugin-react-compiler: 1.0.0 + sharp: 0.35.4(@types/node@26.4.0) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + next@16.3.3(@opentelemetry/api@1.9.1)(@types/node@26.4.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@next/env': 16.3.3 @@ -22441,7 +22492,7 @@ snapshots: postcss: 8.5.23 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.8) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8) optionalDependencies: '@next/swc-darwin-arm64': 16.3.3 '@next/swc-darwin-x64': 16.3.3 @@ -23454,11 +23505,11 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 - rehype-recma@1.0.0(supports-color@10.2.2): + rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 - hast-util-to-estree: 3.1.3(supports-color@10.2.2) + hast-util-to-estree: 3.1.3 transitivePeerDependencies: - supports-color @@ -23484,9 +23535,9 @@ snapshots: transitivePeerDependencies: - supports-color - remark-mdx@3.1.1(supports-color@10.2.2): + remark-mdx@3.1.1: dependencies: - mdast-util-mdx: 3.0.0(supports-color@10.2.2) + mdast-util-mdx: 3.0.0 micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color @@ -24229,6 +24280,13 @@ snapshots: optionalDependencies: '@babel/core': 7.29.7(supports-color@10.2.2) + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.8): + dependencies: + client-only: 0.0.1 + react: 19.2.8 + optionalDependencies: + '@babel/core': 7.29.7 + stylehacks@7.0.11(postcss@8.5.26): dependencies: browserslist: 4.28.8 @@ -24429,11 +24487,11 @@ snapshots: tailwindcss@4.3.3: {} - takumi-js@2.12.0(csstype@3.2.3)(react@19.2.8): + takumi-js@2.12.0(react@19.2.8): dependencies: - '@takumi-rs/core': 2.12.0(csstype@3.2.3)(react@19.2.8) + '@takumi-rs/core': 2.12.0(react@19.2.8) '@takumi-rs/helpers': 2.12.0(react@19.2.8) - '@takumi-rs/wasm': 2.12.0(csstype@3.2.3)(react@19.2.8) + '@takumi-rs/wasm': 2.12.0(react@19.2.8) transitivePeerDependencies: - csstype - preact @@ -24640,9 +24698,9 @@ snapshots: twoslash-protocol@0.3.9: {} - twoslash@0.3.9(supports-color@10.2.2)(typescript@6.0.3): + twoslash@0.3.9(typescript@6.0.3): dependencies: - '@typescript/vfs': 1.6.4(supports-color@10.2.2)(typescript@6.0.3) + '@typescript/vfs': 1.6.4(typescript@6.0.3) twoslash-protocol: 0.3.9 typescript: 6.0.3 transitivePeerDependencies: @@ -25030,12 +25088,11 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28(typescript@6.0.3) - volar-service-prettier@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(prettier@3.9.6): + volar-service-prettier@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)): dependencies: vscode-uri: 3.2.0 optionalDependencies: '@volar/language-service': 2.4.28(typescript@6.0.3) - prettier: 3.9.6 volar-service-typescript-twoslash-queries@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3): dependencies: