Quick start

Choose either the unified package or a standalone package for stricter dependency separation. Both options expose the same API.

Install

pnpm add omni-async

The unified package installs only core by default. It exposes core from both omni-async and omni-async/core, plus framework subpaths such as omni-async/react when the corresponding optional adapter is installed. Standalone packages use the corresponding @omni-async/* import.

Create an operation

import { createAsync } from "@omni-async/core";
// Unified: import { createAsync } from "omni-async";
// Explicit equivalent: import { createAsync } from "omni-async/core";

const user = createAsync(
  async ({ signal }, id: string) => {
    const response = await fetch(`/api/users/${id}`, { signal: signal ?? undefined });
    if (!response.ok) throw new Error("Unable to load user");
    return response.json() as Promise<{ id: string; name: string }>;
  },
  {
    abortable: true,
    concurrency: "latest",
  },
);

const unsubscribe = user.subscribe(() => {
  console.log(user.getSnapshot());
});

await user.execute("42");
unsubscribe();

The snapshot moves from idle to loading, then to success or error. Calling reset() restores the initial snapshot; calling abort() invalidates active requests.

Use vanilla JavaScript

createAsync works without TypeScript or a UI framework. Subscribe to changes and update the DOM from the current snapshot:

import { createAsync } from "@omni-async/core";

const output = document.querySelector("#result");
const search = createAsync(async (_context, term) => {
  const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`);
  if (!response.ok) throw new Error("Search failed");
  return response.json();
});

const unsubscribe = search.subscribe(() => {
  const { data, error, isLoading } = search.getSnapshot();
  output.textContent = isLoading ? "Loading…" : error ? "Search failed" : JSON.stringify(data);
});

void search.execute("async").catch(() => {}); // The snapshot holds the error for display.
// Call unsubscribe() when the view is removed.

Use a framework adapter

The adapters use the same request rules but own their state through native reactive values. For example, React exposes a trigger and independent state fields:

import { useQuery } from "@omni-async/react";
// Unified equivalent: import { useQuery } from "omni-async/react";

function UserSearch() {
  const user = useQuery(async (id: string) => {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error("Unable to load user");
    return response.json() as Promise<{ name: string }>;
  });

  return (
    <button disabled={user.loading} onClick={() => void user.trigger("42")}>
      {user.data?.name ?? "Load user"}
    </button>
  );
}

See framework adapters for Vue and Svelte examples.