Files
PiCloud/clients/typescript
MechaCat02 b1dddb9cb9 feat(v1.1.6-client): @picloud/client TypeScript package
First frontend library (v1.0.0), co-shipped with realtime. Hybrid
model — no direct service access from the browser.

- endpoint<Req,Res>(path).get()/.post() — typed HTTP, auth-token
  injection, structured errors, optional zod/valibot validate adapter.
- subscribe(topic, cb, {token, onTokenExpired}) — streaming-fetch SSE
  with exponential-backoff reconnect, 401 token refresh, Last-Event-ID
  resume.
- auth.login/logout/token over dev-defined endpoints.
- React (useTopic/useEndpoint + PicloudProvider) and Svelte
  (topicStore/endpointStore) subpath exports.

Build: tsup (ESM+CJS+.d.ts); tests: vitest (15); lint: tsc --noEmit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:19:07 +02:00
..

@picloud/client

TypeScript client for PiCloud. 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.
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

import { PicloudProvider, useTopic, useEndpoint } 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 }) {
  const { data, loading, error } = useEndpoint<UserRes>(`/api/users/${id}`).get();
  if (loading) return <Spinner />;
  if (error) return <ErrorView error={error} />;
  return <div>{data?.name}</div>;
}

Svelte

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:

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

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