# @picloud/client

TypeScript client for [PiCloud](../../README.md). Three capabilities, all
**script-mediated** — there is no direct KV / docs / users access from the
browser (the hybrid model, by design):

1. **Typed HTTP** to dev-defined script endpoints.
2. **SSE realtime** subscriptions to externally-subscribable pub/sub topics.
3. **Auth-flow helpers** over your own dev-defined login/logout endpoints.

```ts
import { PicloudClient } from '@picloud/client';

const client = new PicloudClient({
  baseURL: 'https://api.example.com',
  getAuthToken: () => localStorage.getItem('auth_token')
});

// Typed HTTP
interface CreateUserReq { name: string; email?: string; role: string }
interface CreateUserRes { id: string; name: string; created_at: string }
const user = await client
  .endpoint<CreateUserReq, CreateUserRes>('/api/users')
  .post({ name: 'alice', role: 'admin' });

// SSE subscription
const unsubscribe = client.subscribe('chat-room-123', (event) => {
  console.log('got event:', event.message);
});
unsubscribe();

// Token-gated topic (token obtained from one of YOUR script endpoints,
// which calls `pubsub::subscriber_token`)
client.subscribe('chat-room-123', cb, { token: 'eyJhbGc...' });

// Auth helpers (call dev-defined endpoints under the hood)
await client.auth.login('alice@example.com', 'password');
await client.auth.logout();
const token = client.auth.token;
```

## React

```tsx
import {
  PicloudProvider,
  useTopic,
  useEndpointGet,
  useEndpointPost
} from '@picloud/client/react';

// Wrap your tree once: <PicloudProvider client={client}>…</PicloudProvider>

function ChatRoom({ roomId }: { roomId: string }) {
  const messages = useTopic<ChatMessage>(`chat-room-${roomId}`);
  return <ul>{messages.map((m, i) => <li key={i}>{m.text}</li>)}</ul>;
}

function UserProfile({ id }: { id: string }) {
  // Auto-fires when `id` changes; conforms to Rules of Hooks.
  const { data, loading, error } = useEndpointGet<UserRes>(`/api/users/${id}`);
  if (loading) return <Spinner />;
  if (error) return <ErrorView error={error} />;
  return <div>{data?.name}</div>;
}

function CreateUserForm() {
  // Event-driven: NOT fired on mount; call `mutate(body)` on submit.
  const { mutate, loading, error } = useEndpointPost<CreateUserReq, CreateUserRes>(
    '/api/users'
  );
  return (
    <form onSubmit={(e) => { e.preventDefault(); mutate({ name: 'Alice' }); }}>
      <button disabled={loading}>Create</button>
      {error ? <ErrorView error={error} /> : null}
    </form>
  );
}
```

## Svelte

```ts
import { topicStore, endpointStore } from '@picloud/client/svelte';

const messages = topicStore<ChatMessage>(client, `chat-room-${roomId}`);
// $messages is an array that grows as events arrive

const userQuery = endpointStore<UserRes>(client, `/api/users/${id}`).get();
// $userQuery is { data, loading, error }
```

> The Svelte helpers take the `client` explicitly (a store isn't a component,
> so there's no React-style context to read).

## Optional runtime validation (zod / valibot)

No hard dependency — the adapter is the `{ parse(input): T }` shape. A Zod
schema satisfies it directly; wrap Valibot in one line:

```ts
import { z } from 'zod';
const UserSchema = z.object({ id: z.string(), name: z.string() });
const user = await client.endpoint('/api/users/1').get({ validate: UserSchema });

// valibot:
import * as v from 'valibot';
const schema = v.object({ id: v.string() });
const adapter = { parse: (i: unknown) => v.parse(schema, i) };
```

## Transport notes

- SSE is implemented over streaming `fetch` (not native `EventSource`) so the
  client can refresh an expired token on a 401, send `Last-Event-ID` on resume,
  and apply its own exponential backoff (1s → 2s → 4s … capped at 30s).
- **React Native** has no native `EventSource`, but it also can't stream
  `fetch` bodies on all engines — if you target RN, supply a streaming-capable
  `fetch` polyfill via the `fetch` option, or use a `react-native-sse`-based
  adapter. (Server-side `Last-Event-ID` replay is not implemented in v1.1.6;
  the client sends the header so it's ready when the server adds replay.)

## Build / test

```sh
npm install
npm run lint    # tsc --noEmit (strict)
npm run test    # vitest
npm run build   # tsup → dist/ (ESM + CJS + .d.ts)
```
