A API Invoke é um cliente RPC (Remote Procedure Call) tipado. Ela torna a interação com loaders e actions mais segura e tão fácil quanto chamar uma função, abstraindo os detalhes do transporte de rede acontecendo por baixo dos panos.

Um único cliente Invoke pode ser usado para interagir com ações e carregadores do seu site e de qualquer App instalado. O Invoke pode ser usado tanto no Cliente quanto no Servidor e suporta padrões mais complexos, como chamar múltiplos loaders/actions em uma única requisição (Veja o Exemplo 4: Invoke em Batch) ou enviar arquivos via uma requisição multipart.

A assinatura de tipo de Invoke será sempre dinâmica e será inferida com base no tipo do seu manifesto e no tipo da ação/carregador que você está chamando:

Por exemplo:

import { invoke } from "site/runtime.ts";

const resultado = await invoke.site.loaders.exemplo(
    props: T, // Este será o tipo das props da action/loader sendo chamado
    init?: InvokerRequestInit // Este é um objeto de inicialização de requisição fetch estendido com algumas opções extras
);

console.log(resultado); // Este será o tipo do valor de retorno da action/loader

Importando a API

Uso no Browser (client-side)

Para uso no Cliente, o Invoke é exportado da runtime.ts no raiz do projeto.

Abaixo está um exemplo de um arquivo runtime.ts típico, que cria um cliente para interagir com actions e loaders do seu site, e de dois apps: VTEX e Linx Impulse. Todos os Apps podem ser usados da mesma forma, já que exportam um Manifest.

import { proxy } from "deco/clients/withManifest.ts";
import type { Manifest } from "./manifest.gen.ts";
import type { Manifest as ManifestVTEX } from "apps/vtex/manifest.gen.ts";
import type { Manifest as ManifestLinxImpulse } from "apps/linx-impulse/manifest.gen.ts";

export const invoke = proxy<Manifest & ManifestVTEX & ManifestLinxImpulse>();

Uso no Servidor

Para uso no Servidor, o Invoke pode sempre ser acessado a partir do Contexto da Aplicação. Isso torna o Invoke mais fácil de usar dentro de actions e loaders.

Abaixo está um exemplo de um loader que utiliza o Invoke para chamar outro loader da mesma Aplicação:

import type { AppContext } from "site/apps/site.ts";

export const async function getUserNotes(
    props: Props, req: Request, ctx: AppContext
): Promise<User> {
    const user = await ctx.invoke.site.loaders.getUser({ token: req.headers.get("Authorization") });

    if (!user) {
        throw new Error("Usuário não encontrado");
    }

    return user.notes;
}

Exemplos de Uso

Exemplo 1: Chamando uma Action ou Loader a partir do Navegador

Suponha que temos um loader chamado getUser, que retorna um objeto de usuário, baseado em um id de usuário enviado.

import type { AppContext } from "site/apps/site.ts";

export interface Props {
    id: string;
}

export const async function getUser(
    props: Props, req: Request, ctx: AppContext
): Promise<User> {
    return fetchUser(props.id);
}

Podemos agora chamar esse loader a partir do Navegador, usando o cliente invoke exportado do arquivo runtime.ts:

import { invoke } from "site/runtime.ts";

const user = await invoke.site.loaders.getUser({ id: "123" });

Como o cliente Invoke é tipado, o tipo de retorno do getUser é automaticamente inferido, e o tipo da variável user é User. Todos os tipos de parâmetros são também inferidos, então temos mais confiança para interagir com nossas APIs.

Importante: Isso deve ser usado apenas no Navegador. Tentar importar e usar o cliente Invoke do arquivo runtime.ts no servidor resultará em um erro. Para chamar actions/loaders a partir do servidor, veja o próximo exemplo.

Exemplo 2: Chamando uma Action ou Loader a partir do Servidor

Suponha que estamos criando uma ação chamada addItem que adiciona um item a um carrinho.

Suponha também que já temos um loader chamado cart, que retorna o carrinho atual para um usuário, baseado em uma sessão contida nos cookies da requisição:

import type { AppContext } from "site/apps/site.ts";
import { getSessionFromRequest } from "site/lib/session.ts";
import { getCartFromDatabase } from "site/lib/cart.ts";

export interface CartItem {
  productId: string;
  quantity: number;
}

export interface Cart {
  items: CartItem[];
  id: string;
}

export const async function cart(
    _props: unknown, req: Request, ctx: AppContext
): Promise<Cart> {
    // Pegar a sessão a partir da requisição
    const session = await getSessionFromRequest(req);

    // Pegar o carrinho a partir da base de dados usando o ID vindo da sessão
    const cart = await getCartFromDatabase(session.cartId);

    return cart;
}

Agora, quando criamos a ação addItem, podemos reutilizar o loader cart para buscar o carrinho atual e então adicionar o item ao carrinho:

import type { AppContext } from "site/apps/site.ts";
import { saveCartToDatabase } from "site/lib/cart.ts";

export interface Props {
    item: CartItem;
}

export const async function addItem(
    props: Props, req: Request, ctx: AppContext
): Promise<Cart> {
    const currentCart = await ctx.invoke.site.loaders.cart();

    // Adicionar o item ao carrinho
    cart.items.push(props.item);

    // Salvar o carrinho atualizado na base de dados
    await saveCartToDatabase(cart);

    return cart;
}

O cliente Invoke que vem do Contexto da Aplicação é também tipado, baseado no tipo AppContext exportado por convenção do seu site app.

Exemplo 3: Enviando um arquivo para o Servidor

Suponha que temos uma ação chamada uploadFile, que envia um arquivo para um destino. A ação recebe uma propriedade file, que é um objeto de arquivo que contém os dados do arquivo, e uma propriedade destination, que é uma string que especifica o destino para onde o arquivo deve ser enviado.

import type { AppContext } from "site/apps/site.ts";

export interface Props {
    file: File;
    destination: string;
}

export const async function uploadFile(
    props: Props, req: Request, ctx: AppContext
): Promise<void> {
    // Enviar o arquivo para o destino
    await uploadFileToDestination(props.file, props.destination);
}

Estamos usando a web API File como tipo de propriedade aqui, mas isso cria um problema:

O objeto File não é serializável via JSON, que é o que o Invoke usa internamente. Isso significa que tentar passar um objeto File como propriedade para uma ação resultará em um erro ao tentar acessar a propriedade file dentro da sua action.

Para resolver isso, o cliente Invoke oferece uma maneira de fazer upload de arquivos via uma requisição multipart, que é uma maneira prática de enviar arquivos para o servidor, usando a API FormData e o tipo de conteúdo multipart/form-data.

Para usar isso, você só precisa adicionar uma opção multipart: true ao InvokerRequestInit do Invoke (que é o segundo argumento para qualquer chamada de invoke), e o cliente usará automaticamente um protocolo personalizado para enviar o payload via multipart, tornando possível enviar arquivos para o servidor.

Podemos agora chamar essa ação a partir do Navegador, usando o cliente invoke exportado do arquivo runtime.ts:

import { invoke } from "site/runtime.ts";

export function UploadFileInput() {
  const uploadFile = async (file: File) => {
    await invoke.site.actions.uploadFile({
      file: file,
      destination: "/uploads/files",
    }, { multipart: true });
  };

  return (
    <input
      type="file"
      onChange={async (e) => {
        const file = e.target.files[0];
        if (file) {
          await uploadFile(file);
        }
      }}
    />
  );
}

Agora o arquivo file pode ser acessado seguramente na action!

Importante: Quando usando a opção multipart, o cliente Invoke enviará um FormData objeto para o servidor, que só suporta arquivos e strings. Isso significa que qualquer propriedade que seja um número ou um booleano será convertida para uma string.

Exemplo 4: Batch Invoke

Batch Invoke é útil quando você precisa realizar múltiplas operações simultaneamente e quer minimizar a latência de rede, reduzindo o número de requisições.

Aqui está um exemplo de cenário onde usar Batch Invoke faz sentido: recuperar múltiplos conjuntos de dados relacionados em uma única requisição.

Suponha que temos um usuário logado e temos três diferentes loaders que retornam dados relacionados ao usuário: um para anotações (notes), um para o endereço (address) e um para os pedidos (orders).

Podemos recuperar todos esses três conjuntos de dados em uma única requisição usando um Batch Invoke:

import { invoke } from "site/runtime.ts";

// Podemos sempre desstructurar o cliente Invoke
// para escrever código mais fácil de ler
const { loaders } = invoke.site;

const user = ...; // Obtenha o usuário atual de alguma maneira

const {
  userNotes,
  userAddress,
  userOrders,
} = await invoke({
  userNotes: loaders.getUserNotes({ userId: user.id, orderBy: "latest" }),
  userAddress: loaders.getUserAddress({ token: user.token }),
  userOrders: loaders.getUserOrders({ userId: user.id }),
});

Passando um objeto com os loaders/actions como propriedades, o cliente Invoke automaticamente faz o batch das requisições e retorna os resultados no mesmo formato que o objeto passado. Continuamos tendo todos os tipos inferidos automaticamente ao fazer Batch Invoke desta maneira!