import type { FunctionReference as ConvexFunctionReference, FunctionVisibility, } from "convex/server"; import { makeFunctionReference } from "convex/server"; import type { Value } from "convex/values"; import { ConvexError } from "convex/values"; import type { ParseResult } from "effect"; import * as Effect from "effect/Effect"; import * as Match from "effect/Match"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import type * as FunctionSpec from "./FunctionSpec"; import type * as RuntimeAndFunctionType from "./RuntimeAndFunctionType"; export interface Ref< _RuntimeAndFunctionType extends RuntimeAndFunctionType.RuntimeAndFunctionType, _FunctionVisibility extends FunctionVisibility, _Args, _Returns, _Error = never, > { readonly _RuntimeAndFunctionType?: _RuntimeAndFunctionType; readonly _FunctionVisibility?: _FunctionVisibility; readonly _Args?: _Args; readonly _Returns?: _Returns; readonly _Error?: _Error; /** @internal */ readonly functionSpec: FunctionSpec.AnyWithProps; /** @internal */ readonly functionNamespace: string; } export interface Any extends Ref {} export interface AnyInternal extends Ref {} export interface AnyPublic extends Ref {} export interface AnyQuery extends Ref< RuntimeAndFunctionType.AnyQuery, FunctionVisibility, any, any, any > {} export interface AnyMutation extends Ref< RuntimeAndFunctionType.AnyMutation, FunctionVisibility, any, any, any > {} export interface AnyAction extends Ref< RuntimeAndFunctionType.AnyAction, FunctionVisibility, any, any, any > {} export interface AnyPublicQuery extends Ref< RuntimeAndFunctionType.AnyQuery, "public", any, any, any > {} export interface AnyPublicMutation extends Ref< RuntimeAndFunctionType.AnyMutation, "public", any, any, any > {} export interface AnyPublicAction extends Ref< RuntimeAndFunctionType.AnyAction, "public", any, any, any > {} export type GetRuntimeAndFunctionType = Ref_ extends Ref< infer RuntimeAndFunctionType_, infer _FunctionVisibility, infer _Args, infer _Returns, infer _Error > ? RuntimeAndFunctionType_ : never; export type GetRuntime = Ref_ extends Ref< infer RuntimeAndFunctionType_, infer _FunctionVisibility, infer _Args, infer _Returns, infer _Error > ? RuntimeAndFunctionType.GetRuntime : never; export type GetFunctionType = Ref_ extends Ref< infer RuntimeAndFunctionType_, infer _FunctionVisibility, infer _Args, infer _Returns, infer _Error > ? RuntimeAndFunctionType.GetFunctionType : never; export type GetFunctionVisibility = Ref_ extends Ref< infer _RuntimeAndFunctionType, infer FunctionVisibility_, infer _Args, infer _Returns, infer _Error > ? FunctionVisibility_ : never; export type Args = Ref_ extends Ref< infer _RuntimeAndFunctionType, infer _FunctionVisibility, infer Args_, infer _Returns, infer _Error > ? Args_ : never; export type OptionalArgs = keyof Args extends never ? [args?: Args] : [args: Args]; export type Returns = Ref_ extends Ref< infer _RuntimeAndFunctionType, infer _FunctionVisibility, infer _Args, infer Returns_, infer _Error > ? Returns_ : never; export type Error = Ref_ extends Ref< infer _RuntimeAndFunctionType, infer _FunctionVisibility, infer _Args, infer _Returns, infer Error_ > ? Error_ : never; export type FunctionReference = ConvexFunctionReference< GetFunctionType, GetFunctionVisibility >; export type FromFunctionSpec = Ref< FunctionSpec.GetRuntimeAndFunctionType, FunctionSpec.GetFunctionVisibility, FunctionSpec.Args, FunctionSpec.Returns, FunctionSpec.Error >; export const make = ( /** * The namespace portion of a Convex function name, i.e. the part before the * colon. For example, for `myGroupDir/myGroupMod:myFunc` this would be * `myGroupDir/myGroupMod`. */ functionNamespace: string, functionSpec: FunctionSpec_, ): FromFunctionSpec => ({ functionSpec, functionNamespace }); export const getConvexFunctionName = (ref: Any): string => `${ref.functionNamespace}:${ref.functionSpec.name}`; const functionReferenceCache = new Map>(); export const getFunctionReference = ( ref: Ref_, ): FunctionReference => { const functionName = getConvexFunctionName(ref); const cached = functionReferenceCache.get(functionName); if (cached !== undefined) { return cached as FunctionReference; } const functionReference = makeFunctionReference(functionName); functionReferenceCache.set(functionName, functionReference); return functionReference as FunctionReference; }; export const hasErrorSchema = (ref: Any): boolean => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag( "Confect", (confectFunctionProvenance) => "error" in confectFunctionProvenance, ), Match.tag("Convex", () => false), Match.exhaustive, ); export const encodeArgs = ( ref: Ref_, args: Args, ): Effect.Effect => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Schema.encode(confectFunctionProvenance.args)(args), ), Match.tag("Convex", () => Effect.succeed(args)), Match.exhaustive, ); export const decodeReturns = ( ref: Ref_, returns: unknown, ): Effect.Effect, ParseResult.ParseError> => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Schema.decode(confectFunctionProvenance.returns)(returns), ), Match.tag("Convex", () => Effect.succeed(returns)), Match.exhaustive, ); export const encodeArgsSync = ( ref: Ref_, args: Args, ): unknown => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Schema.encodeSync(confectFunctionProvenance.args)(args), ), Match.tag("Convex", () => args), Match.exhaustive, ); export const decodeArgsSync = ( ref: Ref_, encodedArgs: unknown, ): Args => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Schema.decodeSync(confectFunctionProvenance.args)(encodedArgs), ), Match.tag("Convex", () => encodedArgs), Match.exhaustive, ) as Args; export const encodeReturnsSync = ( ref: Ref_, returns: Returns, ): unknown => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Schema.encodeSync(confectFunctionProvenance.returns)(returns), ), Match.tag("Convex", () => returns), Match.exhaustive, ); export const decodeReturnsSync = ( ref: Ref_, encodedReturns: unknown, ): Returns => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Schema.decodeSync(confectFunctionProvenance.returns)(encodedReturns), ), Match.tag("Convex", () => encodedReturns), Match.exhaustive, ) as Returns; const ConvexErrorIdentifier = Symbol.for("ConvexError"); export const isConvexError = (error: unknown): error is ConvexError => error instanceof ConvexError || (typeof error === "object" && error !== null && ConvexErrorIdentifier in error); /** * Build a callback-style handler that decodes the ref's typed error from a * caught `ConvexError`, or else forwards the value to `mapUnknownError`. The * fallback is also invoked when the input *is* a `ConvexError` but the ref * doesn't declare a typed-error schema—by definition such a value falls * outside the ref's error contract. Useful when adapting non-Effect APIs (e.g. * emitter callbacks for streamed subscriptions) to the same error semantics * that `runWithCodec` provides. */ export const decodeErrorOrElse = (ref: Ref_, mapUnknownError: (error: unknown) => E) => (error: unknown): Error | E => { if (isConvexError(error)) { const decoded = decodeErrorSync(ref, error.data); if (Option.isSome(decoded)) { return decoded.value; } } return mapUnknownError(error); }; /** * Decode `encodedError` against the ref's error schema. Returns `None` if the * ref doesn't declare a typed error (Confect ref without an `error` schema, or * a Convex-provenance ref)—by definition there's nothing to decode the value * into, and the caller is responsible for deciding what to do (typically: * surface the original value as a defect). */ export const decodeError = ( ref: Ref_, encodedError: unknown, ): Effect.Effect>, ParseResult.ParseError> => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => "error" in confectFunctionProvenance ? Effect.map( Schema.decode(confectFunctionProvenance.error)(encodedError), Option.some, ) : Effect.succeed(Option.none>()), ), Match.tag("Convex", () => Effect.succeed(Option.none>())), Match.exhaustive, ); /** * Synchronous counterpart to `decodeError`. Throws on schema decode failure; * returns `None` when the ref doesn't declare a typed error. */ export const decodeErrorSync = ( ref: Ref_, encodedError: unknown, ): Option.Option> => Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => "error" in confectFunctionProvenance ? Option.some( Schema.decodeSync(confectFunctionProvenance.error)( encodedError, ) as Error, ) : Option.none>(), ), Match.tag("Convex", () => Option.none>()), Match.exhaustive, ); export const maybeDecodeErrorSync = ( ref: Ref_, error: unknown, ): unknown => isConvexError(error) ? Match.value(ref.functionSpec.functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => "error" in confectFunctionProvenance ? Schema.decodeSync(confectFunctionProvenance.error)(error.data) : error, ), Match.tag("Convex", () => error), Match.exhaustive, ) : error; /** * Encode args via the ref's args schema, invoke `call`, decode returns via the * ref's returns schema, and translate any thrown `ConvexError` into the ref's * typed error. Anything else the Promise rejects with—network failures, * server-side runtime errors, validation failures, etc.—is passed to * `mapUnknownError` to be turned into a typed `E`, or surfaced as a defect when * no handler is provided. */ export const runWithCodec = ( ref: Ref_, args: Args, call: ( functionReference: FunctionReference, encodedArgs: unknown, ) => PromiseLike, mapUnknownError?: (error: unknown) => E, ): Effect.Effect, E | Error | ParseResult.ParseError> => Effect.gen(function* () { const functionReference = getFunctionReference(ref); const functionProvenance = ref.functionSpec.functionProvenance; const invoke = ( encodedArgs: unknown, ): Effect.Effect | E> => Effect.tryPromise({ try: () => Promise.resolve(call(functionReference, encodedArgs)), catch: (error): Error | E => { if (isConvexError(error)) { const decoded = decodeErrorSync(ref, error.data); if (Option.isSome(decoded)) { return decoded.value; } } if (mapUnknownError !== undefined) { return mapUnknownError(error); } throw error; }, }); return yield* Match.value(functionProvenance).pipe( Match.tag("Confect", (confectFunctionProvenance) => Effect.gen(function* () { const encodedArgs = yield* Schema.encode( confectFunctionProvenance.args, )(args); const encodedReturns = yield* invoke(encodedArgs); return yield* Schema.decode(confectFunctionProvenance.returns)( encodedReturns, ); }), ), Match.tag("Convex", () => invoke(args)), Match.exhaustive, ); });