init: initial commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
134
README.md
Normal file
134
README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# @crema/arcadia-client
|
||||
|
||||
Typed HTTP client + React bindings for the [arcadia](https://git.sky-ai.com/CremaUIStudio/arcadia-app) Phoenix API. Wraps the OpenAPI-spec'd surface at `/api/v1` with auth (Bearer JWT and/or service-account API key), tenant context (`X-Tenant-ID`), idempotency keys, error normalization, and rate-limit-aware retry.
|
||||
|
||||
The client is **stateless about how the JWT is obtained** — callers pass `getToken` so token refresh and storage stay app-owned.
|
||||
|
||||
## Public API
|
||||
|
||||
```ts
|
||||
import {
|
||||
createArcadiaClient,
|
||||
ArcadiaProvider,
|
||||
useArcadia,
|
||||
useArcadiaClient,
|
||||
ArcadiaError,
|
||||
} from "@crema/arcadia-client";
|
||||
```
|
||||
|
||||
## Usage in a Crema app
|
||||
|
||||
In `app/root.tsx`, wrap providers:
|
||||
|
||||
```tsx
|
||||
import { ArcadiaProvider } from "@crema/arcadia-client";
|
||||
|
||||
<ArcadiaProvider
|
||||
baseUrl={import.meta.env.VITE_ARCADIA_URL}
|
||||
initialTenantId={tenantId}
|
||||
getToken={() => sessionStorage.getItem("arcadia_token")}
|
||||
onUnauthorized={() => navigate("/login")}
|
||||
>
|
||||
{children}
|
||||
</ArcadiaProvider>;
|
||||
```
|
||||
|
||||
In any component:
|
||||
|
||||
```tsx
|
||||
import { useArcadiaClient, ArcadiaError } from "@crema/arcadia-client";
|
||||
|
||||
function ResourceList() {
|
||||
const arcadia = useArcadiaClient();
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
arcadia
|
||||
.GET<{ data: Resource[] }>("/api/v1/digital_objects", { params: { page: 1 } })
|
||||
.then((res) => setItems(res.data))
|
||||
.catch((err: ArcadiaError) => {
|
||||
if (err.isRateLimited) showToast("Too many requests, slow down.");
|
||||
});
|
||||
}, [arcadia]);
|
||||
}
|
||||
```
|
||||
|
||||
## Generated types
|
||||
|
||||
Endpoint request/response types come from arcadia's live OpenAPI spec. Regenerate with:
|
||||
|
||||
```bash
|
||||
ARCADIA_OPENAPI_URL=http://localhost:4000/api/openapi \
|
||||
node ../lib-arcadia-client/scripts/sync-spec.mjs
|
||||
```
|
||||
|
||||
Requires `openapi-typescript` in the consuming app's devDeps:
|
||||
|
||||
```bash
|
||||
npm i -D openapi-typescript
|
||||
```
|
||||
|
||||
The generated file lives at `src/generated/openapi.d.ts`. Until it's been generated at least once, the file is a stub and only the hand-written types in `src/types.ts` are useful.
|
||||
|
||||
## Two surfaces: generic + typed
|
||||
|
||||
The client gives you both a generic-string API and a fully typed (OpenAPI-driven) API. They share the same auth/retry/error plumbing.
|
||||
|
||||
```ts
|
||||
const arcadia = useArcadiaClient();
|
||||
|
||||
// Generic — accepts any string path. Use when the spec is incomplete or
|
||||
// when calling endpoints outside the spec.
|
||||
const res = await arcadia.GET<{ data: Resource[] }>(
|
||||
"/api/v1/digital_objects",
|
||||
{ params: { page: 1 } },
|
||||
);
|
||||
|
||||
// Typed — paths, params, and responses inferred from the generated spec.
|
||||
// Throws ArcadiaError on non-2xx.
|
||||
const { data } = await arcadia.typed.GET("/api/v1/digital_objects", {
|
||||
params: { query: { page: 1 } },
|
||||
});
|
||||
```
|
||||
|
||||
## Realtime
|
||||
|
||||
Phoenix Channels at `/socket/tenant`, opt-in via the provider. The socket auto-connects when `enableRealtime` is on, joins `tenant:<id>` (and optionally `tenant:<id>:user:<userId>`), and disconnects on unmount.
|
||||
|
||||
```tsx
|
||||
<ArcadiaProvider
|
||||
baseUrl={ARCADIA_URL}
|
||||
initialTenantId={tenantId}
|
||||
userId={userId}
|
||||
enableRealtime
|
||||
getToken={() => sessionStorage.getItem("arcadia_access_token")}
|
||||
>
|
||||
…
|
||||
</ArcadiaProvider>
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { useArcadiaSubscription } from "@crema/arcadia-client";
|
||||
|
||||
function NotificationToasts() {
|
||||
useArcadiaSubscription("notification", (n) => {
|
||||
toast(n.title, n.body);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
Known events on `TenantEventMap`: `notification`, `digital_object`, `announcement`, `status_update`, `event`. The map is open-ended — apps can subscribe to any string event arcadia emits; payload type defaults to `Record<string, unknown>`.
|
||||
|
||||
For user-scoped events (those filtered to the current user), pass `{ scope: "user" }` and ensure `userId` was provided to the provider.
|
||||
|
||||
## What's not in here yet
|
||||
|
||||
- **TanStack Query helpers** — opt-in. Vibespace doesn't use Query today; we can layer it via a sub-export later.
|
||||
- **Token refresh helper** — the client surfaces 401 via `onUnauthorized`; the app decides whether to refresh + retry. A reference refresh helper may land later.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Inline imports only — no own `package.json` (lib lives by the consuming app's deps).
|
||||
- Path-aliased into apps via `tsconfig.json` `paths`: `@crema/arcadia-client` → `../lib-arcadia-client/src/index.tsx`.
|
||||
- Tailwind doesn't scan this lib — no UI; nothing to scan.
|
||||
Reference in New Issue
Block a user