Document createSocialBindings + useSocial pattern + the three new TenantEventMap entries (social:notification, social:post, social:article) so consumers know the channel events exist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
5.2 KiB
Markdown
161 lines
5.2 KiB
Markdown
# @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`, plus social: `social:notification`, `social:post`, `social:article`. 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.
|
|
|
|
## Social bindings
|
|
|
|
For the social surfaces (profiles, articles, discussion board,
|
|
favourites, DNA-change subscriptions, notifications, search,
|
|
@-mention search), there's a typed wrapper over the generic client:
|
|
|
|
```ts
|
|
import { createArcadiaClient, createSocialBindings } from "@crema/arcadia-client";
|
|
|
|
const client = createArcadiaClient({ baseUrl, getToken });
|
|
const social = createSocialBindings(client);
|
|
|
|
await social.createPost({ body_md: "hello" });
|
|
await social.search("agent design");
|
|
await social.addSubscription(agentId, { lastSeenDnaHash: currentHash });
|
|
```
|
|
|
|
In a React app, prefer the `useSocial()` hook pattern (see
|
|
`arcadia-aifirst-starter/app/lib/api/social.ts`) so the bindings
|
|
pick up the ArcadiaProvider's auth context automatically.
|
|
|
|
Realtime: the social write paths broadcast via Phoenix.PubSub →
|
|
`ArcadiaWeb.TenantChannel` forwards them as `social:notification` /
|
|
`social:post` / `social:article` channel events. Subscribe via
|
|
`useArcadiaSubscription`.
|
|
|
|
## 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.
|