Files
MechaCat02 b0f7b72dd6 fix(review): close 5 follow-up gaps from Stage 6 audit re-review
- F-T-003 actually closed: clients/typescript/src/subscribe.ts now
  bounds the 401-refresh loop at 3 consecutive refusals, surfaces an
  onError describing the loop, and resets on any successful stream
  open. New test covers the cap. The Stage 2 dashboard fix only
  addressed adminRequest in dashboard/src/lib/api.ts; the originally-
  cited TS client file was untouched.

- users/invitations subtab dropped the redundant api.apps.get fetch
  the other 5 subtabs already shed in Stage 6.

- Queue visibility-timeout validator pulled out as
  validate_queue_visibility_timeout, with two thresholds: hard-reject
  below MIN_QUEUE_VISIBILITY_TIMEOUT_SECS (30s — catches typos), warn-
  log between MIN and SAFE_VISIBILITY_VS_EXEC_BUDGET_SECS (300s) so
  operators see when their visibility is below the dispatcher's
  per-message executor budget. Stage 6 only had the hard floor; the
  reviewer caught that a 60s handler still races a 30s visibility
  even after the floor. Four new unit tests cover none/above-safe/
  between/below-min.

- pic dead-letters count subcommand: cheap headless probe for
  unresolved DL totals, parallels the dashboard's badge query.

- pic dead-letters replay now has a happy-path integration test
  (replay_against_real_dl_row_succeeds): inserts a synthetic DL row
  directly via the rust-postgres sync driver (added as dev-dep),
  drives `pic dead-letters replay`, asserts the row is resolved with
  reason=replayed and count drops back to 0. Plus a count smoke test.

All 75 CLI integration tests + 16 TS client tests + 4 new
visibility-timeout unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 17:20:34 +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,
  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

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)