import {
  ConvexError,
  convexToJson,
  GenericValidator,
  jsonToConvex,
  v,
  Validator,
  Value,
} from "../../values/index.js";
import { GenericDataModel } from "../data_model.js";
import {
  ActionBuilder,
  DefaultFunctionArgs,
  GenericActionCtx,
  GenericMutationCtx,
  GenericQueryCtx,
  MutationBuilder,
  PublicHttpAction,
  QueryBuilder,
  RegisteredAction,
  RegisteredMutation,
  RegisteredQuery,
} from "../registration.js";
import { setupActionCalls } from "./actions_impl.js";
import { setupActionVectorSearch } from "./vector_search_impl.js";
import { setupAuth } from "./authentication_impl.js";
import { setupReader, setupWriter } from "./database_impl.js";
import { QueryImpl, QueryInitializerImpl } from "./query_impl.js";
import {
  setupActionScheduler,
  setupMutationScheduler,
} from "./scheduler_impl.js";
import {
  setupStorageActionWriter,
  setupStorageReader,
  setupStorageWriter,
} from "./storage_impl.js";
import { parseArgs } from "../../common/index.js";
import { performAsyncSyscall } from "./syscall.js";
import { asObjectValidator } from "../../values/validator.js";
import { getFunctionAddress } from "../components/paths.js";

async function invokeMutation<
  F extends (ctx: GenericMutationCtx<GenericDataModel>, ...args: any) => any,
>(func: F, argsStr: string) {
  // TODO(presley): Change the function signature and propagate the requestId from Rust.
  // Ok, to mock it out for now, since queries are only running in V8.
  const requestId = "";
  const args = jsonToConvex(JSON.parse(argsStr));
  const mutationCtx = {
    db: setupWriter(),
    auth: setupAuth(requestId),
    storage: setupStorageWriter(requestId),
    scheduler: setupMutationScheduler(),

    runQuery: (reference: any, args?: any) => runUdf("query", reference, args),
    runMutation: (reference: any, args?: any) =>
      runUdf("mutation", reference, args),
  };
  const result = await invokeFunction(func, mutationCtx, args as any);
  validateReturnValue(result);
  return JSON.stringify(convexToJson(result === undefined ? null : result));
}

export function validateReturnValue(v: any) {
  if (v instanceof QueryInitializerImpl || v instanceof QueryImpl) {
    throw new Error(
      "Return value is a Query. Results must be retrieved with `.collect()`, `.take(n), `.unique()`, or `.first()`.",
    );
  }
}

export async function invokeFunction<
  Ctx,
  Args extends any[],
  F extends (ctx: Ctx, ...args: Args) => any,
>(func: F, ctx: Ctx, args: Args) {
  let result;
  try {
    result = await Promise.resolve(func(ctx, ...args));
  } catch (thrown: unknown) {
    throw serializeConvexErrorData(thrown);
  }
  return result;
}

function dontCallDirectly(
  funcType: string,
  handler: (ctx: any, args: any) => any,
): unknown {
  return (ctx: any, args: any) => {
    globalThis.console.warn(
      "Convex functions should not directly call other Convex functions. Consider calling a helper function instead. " +
        `e.g. \`export const foo = ${funcType}(...); await foo(ctx);\` is not supported. ` +
        "See https://docs.convex.dev/production/best-practices/#use-helper-functions-to-write-shared-code",
    );
    return handler(ctx, args);
  };
}

// Keep in sync with node executor
function serializeConvexErrorData(thrown: unknown) {
  if (
    typeof thrown === "object" &&
    thrown !== null &&
    Symbol.for("ConvexError") in thrown
  ) {
    const error = thrown as ConvexError<any>;
    error.data = JSON.stringify(
      convexToJson(error.data === undefined ? null : error.data),
    );
    (error as any).ConvexErrorSymbol = Symbol.for("ConvexError");
    return error;
  } else {
    return thrown;
  }
}

/**
 * Guard against Convex functions accidentally getting included in a browser bundle.
 * Convex functions may include secret logic or credentials that should not be
 * send to untrusted clients (browsers).
 */
function assertNotBrowser() {
  if (
    typeof window === "undefined" ||
    (window as any).__convexAllowFunctionsInBrowser
  ) {
    return;
  }
  // JSDom doesn't count, developers are allowed to use JSDom in Convex functions.
  const isRealBrowser =
    Object.getOwnPropertyDescriptor(globalThis, "window")
      ?.get?.toString()
      .includes("[native code]") ?? false;
  if (isRealBrowser) {
    // eslint-disable-next-line no-console
    console.error(
      "Convex functions should not be imported in the browser. This will throw an error in future versions of `convex`. If this is a false negative, please report it to Convex support.",
    );
  }
}

type FunctionDefinition =
  | ((ctx: any, args: DefaultFunctionArgs) => any)
  | {
      args?: GenericValidator | Record<string, GenericValidator>;
      returns?: GenericValidator | Record<string, GenericValidator>;
      handler: (ctx: any, args: DefaultFunctionArgs) => any;
    };

function strictReplacer(key: string, value: any) {
  if (value === undefined) {
    throw new Error(
      `A validator is undefined for field "${key}". ` +
        `This is often caused by circular imports. ` +
        `See https://docs.convex.dev/error#undefined-validator for details.`,
    );
  }
  return value;
}
function exportArgs(functionDefinition: FunctionDefinition) {
  return () => {
    let args: GenericValidator = v.any();
    if (
      typeof functionDefinition === "object" &&
      functionDefinition.args !== undefined
    ) {
      args = asObjectValidator(functionDefinition.args);
    }
    return JSON.stringify(args.json, strictReplacer);
  };
}

function exportReturns(functionDefinition: FunctionDefinition) {
  return () => {
    let returns: Validator<any, any, any> | undefined;
    if (
      typeof functionDefinition === "object" &&
      functionDefinition.returns !== undefined
    ) {
      returns = asObjectValidator(functionDefinition.returns);
    }
    return JSON.stringify(returns ? returns.json : null, strictReplacer);
  };
}

/**
 * Define a mutation in this Convex app's public API.
 *
 * You should generally use the `mutation` function from
 * `"./_generated/server"`.
 *
 * Mutations can read from and write to the database, and are accessible from
 * the client. They run **transactionally**, all database reads and writes
 * within a single mutation are atomic and isolated from other mutations.
 *
 * @example
 * ```typescript
 * import { mutation } from "./_generated/server";
 * import { v } from "convex/values";
 *
 * export const createTask = mutation({
 *   args: { text: v.string() },
 *   returns: v.id("tasks"),
 *   handler: async (ctx, args) => {
 *     const taskId = await ctx.db.insert("tasks", {
 *       text: args.text,
 *       completed: false,
 *     });
 *     return taskId;
 *   },
 * });
 * ```
 *
 * **Best practice:** Always include `args` and `returns` validators on all
 * mutations. If the function doesn't return a value, use `returns: v.null()`.
 * Argument validation is critical for security since public mutations are
 * exposed to the internet.
 *
 * **Common mistake:** Mutations cannot call third-party APIs or use `fetch`.
 * They must be deterministic. Use actions for external API calls.
 *
 * **Common mistake:** Do not use `mutation` for sensitive internal functions
 * that should not be called by clients. Use `internalMutation` instead.
 *
 * @param func - The mutation function. It receives a {@link GenericMutationCtx} as its first argument.
 * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
 *
 * @see https://docs.convex.dev/functions/mutation-functions
 * @public
 */
export const mutationGeneric: MutationBuilder<any, "public"> = ((
  functionDefinition: FunctionDefinition,
) => {
  const handler = (
    typeof functionDefinition === "function"
      ? functionDefinition
      : functionDefinition.handler
  ) as (ctx: GenericMutationCtx<any>, args: any) => any;
  const func = dontCallDirectly("mutation", handler) as RegisteredMutation<
    "public",
    any,
    any
  >;

  assertNotBrowser();
  func.isMutation = true;
  func.isPublic = true;
  func.invokeMutation = (argsStr) => invokeMutation(handler, argsStr);
  func.exportArgs = exportArgs(functionDefinition);
  func.exportReturns = exportReturns(functionDefinition);
  func._handler = handler;
  return func;
}) as MutationBuilder<any, "public">;

/**
 * Define a mutation that is only accessible from other Convex functions (but not from the client).
 *
 * You should generally use the `internalMutation` function from
 * `"./_generated/server"`.
 *
 * Internal mutations can read from and write to the database but are **not**
 * exposed as part of your app's public API. They can only be called by other
 * Convex functions using `ctx.runMutation` or by the scheduler. Like public
 * mutations, they run transactionally.
 *
 * @example
 * ```typescript
 * import { internalMutation } from "./_generated/server";
 * import { v } from "convex/values";
 *
 * // This mutation can only be called from other Convex functions:
 * export const markTaskCompleted = internalMutation({
 *   args: { taskId: v.id("tasks") },
 *   returns: v.null(),
 *   handler: async (ctx, args) => {
 *     await ctx.db.patch("tasks", args.taskId, { completed: true });
 *     return null;
 *   },
 * });
 * ```
 *
 * **Best practice:** Use `internalMutation` for any mutation that should not
 * be directly callable by clients, such as write-back functions from actions
 * or scheduled background work. Reference it via the `internal` object:
 * `await ctx.runMutation(internal.myModule.markTaskCompleted, { taskId })`.
 *
 * @param func - The mutation function. It receives a {@link GenericMutationCtx} as its first argument.
 * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
 *
 * @see https://docs.convex.dev/functions/internal-functions
 * @public
 */
export const internalMutationGeneric: MutationBuilder<any, "internal"> = ((
  functionDefinition: FunctionDefinition,
) => {
  const handler = (
    typeof functionDefinition === "function"
      ? functionDefinition
      : functionDefinition.handler
  ) as (ctx: GenericMutationCtx<any>, args: any) => any;
  const func = dontCallDirectly(
    "internalMutation",
    handler,
  ) as RegisteredMutation<"internal", any, any>;

  assertNotBrowser();
  func.isMutation = true;
  func.isInternal = true;
  func.invokeMutation = (argsStr) => invokeMutation(handler, argsStr);
  func.exportArgs = exportArgs(functionDefinition);
  func.exportReturns = exportReturns(functionDefinition);
  func._handler = handler;
  return func;
}) as MutationBuilder<any, "internal">;

async function invokeQuery<
  F extends (ctx: GenericQueryCtx<GenericDataModel>, ...args: any) => any,
>(func: F, argsStr: string) {
  // TODO(presley): Change the function signature and propagate the requestId from Rust.
  // Ok, to mock it out for now, since queries are only running in V8.
  const requestId = "";
  const args = jsonToConvex(JSON.parse(argsStr));
  const queryCtx = {
    db: setupReader(),
    auth: setupAuth(requestId),
    storage: setupStorageReader(requestId),
    runQuery: (reference: any, args?: any) => runUdf("query", reference, args),
  };
  const result = await invokeFunction(func, queryCtx, args as any);
  validateReturnValue(result);
  return JSON.stringify(convexToJson(result === undefined ? null : result));
}

/**
 * Define a query in this Convex app's public API.
 *
 * You should generally use the `query` function from
 * `"./_generated/server"`.
 *
 * Queries can read from the database and are accessible from the client. They
 * are **reactive**, when used with `useQuery` in React, the component
 * automatically re-renders whenever the underlying data changes. Queries
 * cannot modify the database.
 * Query results are automatically cached by the Convex client and kept
 * consistent via WebSocket subscriptions.
 *
 *
 * @example
 * ```typescript
 * import { query } from "./_generated/server";
 * import { v } from "convex/values";
 *
 * export const listTasks = query({
 *   args: { completed: v.optional(v.boolean()) },
 *   returns: v.array(v.object({
 *     _id: v.id("tasks"),
 *     _creationTime: v.number(),
 *     text: v.string(),
 *     completed: v.boolean(),
 *   })),
 *   handler: async (ctx, args) => {
 *     if (args.completed !== undefined) {
 *       return await ctx.db
 *         .query("tasks")
 *         .withIndex("by_completed", (q) => q.eq("completed", args.completed))
 *         .collect();
 *     }
 *     return await ctx.db.query("tasks").collect();
 *   },
 * });
 * ```
 *
 * **Best practice:** Always include `args` and `returns` validators. Use
 * `.withIndex()` instead of `.filter()` for efficient database queries.
 * Queries should be fast since they run on every relevant data change.
 *
 * **Common mistake:** Queries are pure reads, they cannot write to the
 * database, call external APIs, or schedule functions. Use actions for HTTP
 * calls and mutations for database writes and scheduling.
 *
 * @param func - The query function. It receives a {@link GenericQueryCtx} as its first argument.
 * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
 *
 * @see https://docs.convex.dev/functions/query-functions
 * @public
 */
export const queryGeneric: QueryBuilder<any, "public"> = ((
  functionDefinition: FunctionDefinition,
) => {
  const handler = (
    typeof functionDefinition === "function"
      ? functionDefinition
      : functionDefinition.handler
  ) as (ctx: GenericQueryCtx<any>, args: any) => any;
  const func = dontCallDirectly("query", handler) as RegisteredQuery<
    "public",
    any,
    any
  >;

  assertNotBrowser();
  func.isQuery = true;
  func.isPublic = true;
  func.invokeQuery = (argsStr) => invokeQuery(handler, argsStr);
  func.exportArgs = exportArgs(functionDefinition);
  func.exportReturns = exportReturns(functionDefinition);
  func._handler = handler;
  return func;
}) as QueryBuilder<any, "public">;

/**
 * Define a query that is only accessible from other Convex functions (but not from the client).
 *
 * You should generally use the `internalQuery` function from
 * `"./_generated/server"`.
 *
 * Internal queries can read from the database but are **not** exposed as part
 * of your app's public API. They can only be called by other Convex functions
 * using `ctx.runQuery`. This is useful for loading data in actions or for
 * helper queries that shouldn't be client-facing.
 *
 * @example
 * ```typescript
 * import { internalQuery } from "./_generated/server";
 * import { v } from "convex/values";
 *
 * // Only callable from other Convex functions:
 * export const getUser = internalQuery({
 *   args: { userId: v.id("users") },
 *   returns: v.union(
 *     v.object({
 *       _id: v.id("users"),
 *       _creationTime: v.number(),
 *       name: v.string(),
 *       email: v.string(),
 *     }),
 *     v.null(),
 *   ),
 *   handler: async (ctx, args) => {
 *     return await ctx.db.get("users", args.userId);
 *   },
 * });
 * ```
 *
 * **Best practice:** Use `internalQuery` for data-loading in actions via
 * `ctx.runQuery(internal.myModule.getUser, { userId })`.
 *
 * @param func - The query function. It receives a {@link GenericQueryCtx} as its first argument.
 * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
 *
 * @see https://docs.convex.dev/functions/internal-functions
 * @public
 */
export const internalQueryGeneric: QueryBuilder<any, "internal"> = ((
  functionDefinition: FunctionDefinition,
) => {
  const handler = (
    typeof functionDefinition === "function"
      ? functionDefinition
      : functionDefinition.handler
  ) as (ctx: GenericQueryCtx<any>, args: any) => any;
  const func = dontCallDirectly("internalQuery", handler) as RegisteredQuery<
    "internal",
    any,
    any
  >;

  assertNotBrowser();
  func.isQuery = true;
  func.isInternal = true;
  func.invokeQuery = (argsStr) => invokeQuery(handler as any, argsStr);
  func.exportArgs = exportArgs(functionDefinition);
  func.exportReturns = exportReturns(functionDefinition);
  func._handler = handler;
  return func;
}) as QueryBuilder<any, "internal">;

async function invokeAction<
  F extends (ctx: GenericActionCtx<GenericDataModel>, ...args: any) => any,
>(func: F, requestId: string, argsStr: string) {
  const args = jsonToConvex(JSON.parse(argsStr));
  const calls = setupActionCalls(requestId);
  const ctx = {
    ...calls,
    auth: setupAuth(requestId),
    scheduler: setupActionScheduler(requestId),
    storage: setupStorageActionWriter(requestId),
    vectorSearch: setupActionVectorSearch(requestId) as any,
  };
  const result = await invokeFunction(func, ctx, args as any);
  return JSON.stringify(convexToJson(result === undefined ? null : result));
}

/**
 * Define an action in this Convex app's public API.
 *
 * Actions can call third-party APIs, use Node.js libraries, and perform other
 * side effects. Unlike queries and mutations, actions do **not** have direct
 * database access (`ctx.db` is not available). Instead, use `ctx.runQuery`
 * and `ctx.runMutation` to read and write data.
 *
 * You should generally use the `action` function from
 * `"./_generated/server"`.
 *
 * Actions are accessible from the client and run outside of the database
 * transaction, so they are not atomic. They are best for integrating with
 * external services.
 *
 * @example
 * ```typescript
 * // Add "use node"; at the top of the file if using Node.js built-in modules.
 * import { action } from "./_generated/server";
 * import { v } from "convex/values";
 * import { internal } from "./_generated/api";
 *
 * export const generateSummary = action({
 *   args: { text: v.string() },
 *   returns: v.string(),
 *   handler: async (ctx, args) => {
 *     // Call an external API:
 *     const response = await fetch("https://api.example.com/summarize", {
 *       method: "POST",
 *       body: JSON.stringify({ text: args.text }),
 *     });
 *     const { summary } = await response.json();
 *
 *     // Write results back via a mutation:
 *     await ctx.runMutation(internal.myModule.saveSummary, {
 *       text: args.text,
 *       summary,
 *     });
 *
 *     return summary;
 *   },
 * });
 * ```
 *
 * **Best practice:** Minimize the number of `ctx.runQuery` and
 * `ctx.runMutation` calls from actions. Each call is a separate transaction,
 * so splitting logic across multiple calls introduces the risk of race
 * conditions. Try to batch reads/writes into single query/mutation calls.
 *
 * **`"use node"` runtime:** Actions run in Convex's default JavaScript
 * runtime, which supports `fetch` and most NPM packages. Only add
 * `"use node";` at the top of the file if a third-party library specifically
 * requires Node.js built-in APIs, it is a last resort, not the default.
 * Node.js actions have slower cold starts, and **only actions can be defined
 * in `"use node"` files** (no queries or mutations), so prefer the default
 * runtime whenever possible.
 *
 * **Common mistake:** Do not try to access `ctx.db` in an action, it is
 * not available. Use `ctx.runQuery` and `ctx.runMutation` instead.
 *
 * @param func - The function. It receives a {@link GenericActionCtx} as its first argument.
 * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
 *
 * @see https://docs.convex.dev/functions/actions
 * @public
 */
export const actionGeneric: ActionBuilder<any, "public"> = ((
  functionDefinition: FunctionDefinition,
) => {
  const handler = (
    typeof functionDefinition === "function"
      ? functionDefinition
      : functionDefinition.handler
  ) as (ctx: GenericActionCtx<any>, args: any) => any;
  const func = dontCallDirectly("action", handler) as RegisteredAction<
    "public",
    any,
    any
  >;

  assertNotBrowser();
  func.isAction = true;
  func.isPublic = true;
  func.invokeAction = (requestId, argsStr) =>
    invokeAction(handler, requestId, argsStr);
  func.exportArgs = exportArgs(functionDefinition);
  func.exportReturns = exportReturns(functionDefinition);
  func._handler = handler;
  return func;
}) as ActionBuilder<any, "public">;

/**
 * Define an action that is only accessible from other Convex functions (but not from the client).
 *
 * You should generally use the `internalAction` function from
 * `"./_generated/server"`.
 *
 * Internal actions behave like public actions (they can call external APIs and
 * use Node.js libraries) but are **not** exposed in your app's public API. They
 * can only be called by other Convex functions using `ctx.runAction` or via the
 * scheduler.
 *
 * @example
 * ```typescript
 * import { internalAction } from "./_generated/server";
 * import { v } from "convex/values";
 *
 * export const sendEmail = internalAction({
 *   args: { to: v.string(), subject: v.string(), body: v.string() },
 *   returns: v.null(),
 *   handler: async (ctx, args) => {
 *     // Call an external email service (fetch works in the default runtime):
 *     await fetch("https://api.email-service.com/send", {
 *       method: "POST",
 *       headers: { "Content-Type": "application/json" },
 *       body: JSON.stringify(args),
 *     });
 *     return null;
 *   },
 * });
 * ```
 *
 * **Best practice:** Use `internalAction` for background work scheduled from
 * mutations: `await ctx.scheduler.runAfter(0, internal.myModule.sendEmail, { ... })`.
 * Only use `ctx.runAction` from another action if you need to cross runtimes
 * (e.g., default Convex runtime to Node.js). Otherwise, extract shared code
 * into a helper function.
 *
 * **`"use node"` runtime:** Only add `"use node";` at the top of the file
 * as a last resort when a third-party library requires Node.js APIs. Node.js
 * actions have slower cold starts, and **only actions can be defined in
 * `"use node"` files** (no queries or mutations).
 *
 * @param func - The function. It receives a {@link GenericActionCtx} as its first argument.
 * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
 *
 * @see https://docs.convex.dev/functions/internal-functions
 * @public
 */
export const internalActionGeneric: ActionBuilder<any, "internal"> = ((
  functionDefinition: FunctionDefinition,
) => {
  const handler = (
    typeof functionDefinition === "function"
      ? functionDefinition
      : functionDefinition.handler
  ) as (ctx: GenericActionCtx<any>, args: any) => any;
  const func = dontCallDirectly("internalAction", handler) as RegisteredAction<
    "internal",
    any,
    any
  >;

  assertNotBrowser();
  func.isAction = true;
  func.isInternal = true;
  func.invokeAction = (requestId, argsStr) =>
    invokeAction(handler, requestId, argsStr);
  func.exportArgs = exportArgs(functionDefinition);
  func.exportReturns = exportReturns(functionDefinition);
  func._handler = handler;
  return func;
}) as ActionBuilder<any, "internal">;

async function invokeHttpAction<
  F extends (ctx: GenericActionCtx<GenericDataModel>, request: Request) => any,
>(func: F, request: Request) {
  // TODO(presley): Change the function signature and propagate the requestId from Rust.
  // Ok, to mock it out for now, since http endpoints are only running in V8.
  const requestId = "";
  const calls = setupActionCalls(requestId);
  const ctx = {
    ...calls,
    auth: setupAuth(requestId),
    storage: setupStorageActionWriter(requestId),
    scheduler: setupActionScheduler(requestId),
    vectorSearch: setupActionVectorSearch(requestId) as any,
  };
  return await invokeFunction(func, ctx, [request]);
}

/**
 * Define a Convex HTTP action.
 *
 * HTTP actions handle raw HTTP requests and return HTTP responses. They are
 * registered by routing URL paths to them in `convex/http.ts` using
 * {@link HttpRouter}. Like regular actions, they can call external APIs and
 * use `ctx.runQuery` / `ctx.runMutation` but do not have direct `ctx.db` access.
 *
 * @example
 * ```typescript
 * // convex/http.ts
 * import { httpRouter } from "convex/server";
 * import { httpAction } from "./_generated/server";
 *
 * const http = httpRouter();
 *
 * http.route({
 *   path: "/api/webhook",
 *   method: "POST",
 *   handler: httpAction(async (ctx, request) => {
 *     const body = await request.json();
 *     // Process the webhook payload...
 *     return new Response(JSON.stringify({ ok: true }), {
 *       status: 200,
 *       headers: { "Content-Type": "application/json" },
 *     });
 *   }),
 * });
 *
 * export default http;
 * ```
 *
 * **Best practice:** HTTP actions are registered at the exact path specified.
 * For example, `path: "/api/webhook"` registers at `/api/webhook`.
 *
 * @param func - The function. It receives a {@link GenericActionCtx} as its first argument, and a `Request` object
 * as its second.
 * @returns The wrapped function. Route a URL path to this function in `convex/http.ts`.
 *
 * @see https://docs.convex.dev/functions/http-actions
 * @public
 */
export const httpActionGeneric = (
  func: (
    ctx: GenericActionCtx<GenericDataModel>,
    request: Request,
  ) => Promise<Response>,
): PublicHttpAction => {
  const q = dontCallDirectly("httpAction", func) as PublicHttpAction;
  assertNotBrowser();
  q.isHttp = true;
  q.invokeHttpAction = (request) => invokeHttpAction(func as any, request);
  q._handler = func;
  return q;
};

async function runUdf(
  udfType: "query" | "mutation",
  f: any,
  args?: Record<string, Value>,
): Promise<any> {
  const queryArgs = parseArgs(args);
  const syscallArgs = {
    udfType,
    args: convexToJson(queryArgs),
    ...getFunctionAddress(f),
  };
  const result = await performAsyncSyscall("1.0/runUdf", syscallArgs);
  return jsonToConvex(result);
}
