Invoke

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!

Was this page helpful?
Can't find what you're looking for? Spot an error in the documentation? Get in touch with us on our Community Forum
Continue reading