@pylonsync/react 0.3.291 → 0.3.293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ export { defineRoute } from "@pylonsync/sdk";
2
+ export type { RouteMode, AppManifest } from "@pylonsync/sdk";
3
+ export { Link } from "./Link";
4
+ export type { LinkProps } from "./Link";
5
+ export { Image } from "./Image";
6
+ export type { ImageProps } from "./Image";
7
+ export { Form } from "./Form";
8
+ export type { FormProps } from "./Form";
9
+ export type { PageProps, PageAuth, ServerData, SsrResponse, SsrCookieOptions, Metadata, GenerateMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, RouteSegmentConfig, ErrorBoundaryProps, NotFoundProps, FormFields, FormDb, FormRequest, RouteHandler, RawRouteHandler, RawResponse, } from "./ssr";
10
+ export { useRouter, useSearchParams, usePathname, useParams, redirect, notFound, NotFoundError, } from "./useRouter";
11
+ export type { PylonRouter } from "./useRouter";
12
+ import { type Storage as PylonStorage } from "@pylonsync/sync";
13
+ export { useQuery, useQueryOne, useReactiveQuery, useMutation, useInfiniteQuery, usePaginatedQuery, useEntityMutation, useAction, useQueryRaw, useQueryOneRaw, useLiveList, useLiveRow, useInsert, useUpdate, useDelete, useFn, useAggregate, useSearch, } from "./hooks";
14
+ export type { QueryOptions, QueryFilter, IncludeSpec, UseQueryReturn, UseQueryOneReturn, UseReactiveQueryReturn, UseMutationReturn, UseInfiniteQueryReturn, UsePaginatedQueryReturn, PaginatedQueryStatus, UseFnReturn, AggregateSpec, UseAggregateReturn, SearchSpec, UseSearchReturn, } from "./hooks";
15
+ export { useRoom } from "./useRoom";
16
+ export type { RoomPeer, RoomSnapshot, UseRoomOptions, UseRoomReturn, } from "./useRoom";
17
+ export { useShard, connectShard } from "./useShard";
18
+ export type { UseShardOptions, UseShardReturn, ShardClient, } from "./useShard";
19
+ export { useSession } from "./useSession";
20
+ export type { UseSessionReturn, ResolvedSession } from "./useSession";
21
+ export { useSyncStatus } from "./useSyncStatus";
22
+ export type { SyncConnectionStatus } from "./useSyncStatus";
23
+ export { db, init, getSync } from "./db";
24
+ export { createTypedDb } from "./typed";
25
+ export type { TypedDb, AgentDBSchema } from "./typed";
26
+ export { SyncEngine, createSyncEngine, getServerData, LocalStore, MutationQueue, } from "@pylonsync/sync";
27
+ export type { ChangeEvent, SyncCursor, PullResponse, HydrationData, Row, } from "@pylonsync/sync";
28
+ export interface AgentDBClientConfig {
29
+ baseUrl?: string;
30
+ /**
31
+ * App identifier used to namespace all client-side storage keys —
32
+ * localStorage (token, cached user, feature-flag toggles) and
33
+ * IndexedDB (sync replica). Two apps served from the same browser
34
+ * origin (different ports in dev, or the same domain in prod) must
35
+ * pick different names or they'll see each other's sessions and
36
+ * local replicas. Defaults to "default" for a single-app setup.
37
+ */
38
+ appName?: string;
39
+ }
40
+ /** Current effective base URL. Used by hooks (useRoom, useShard) and the
41
+ * @pylonsync/client auth helpers (createOrg, passwordRegister, createInvite,
42
+ * …) that share the client config but don't have access to the module-private
43
+ * state.
44
+ *
45
+ * When NOT explicitly configured, default to the page origin in a browser
46
+ * instead of the `http://localhost:4321` dev constant. A unified SSR/embedded
47
+ * app serves its API same-origin, so the static default was a footgun: every
48
+ * auth/org call fired at `localhost:4321` — broken on any non-4321 dev port
49
+ * AND in production (it would hit the engineer's dev port, not the app's
50
+ * domain). `init()`/`createSyncEngine` already resolve `window.location.origin`
51
+ * for the sync engine; this brings the auth helpers to the same origin so the
52
+ * two never disagree. An explicit `configureClient({ baseUrl })` still wins
53
+ * (separate-origin API setups), and SSR/node (no `window`) keeps the dev
54
+ * default (server-side calls use same-process paths anyway). */
55
+ export declare function getBaseUrl(): string;
56
+ /** Current app name. Used by sync engine + storage helpers to namespace keys. */
57
+ export declare function getAppName(): string;
58
+ /**
59
+ * Resolve the localStorage key for a conceptual slot (e.g. "token",
60
+ * "user") into its actual storage key. When `appName` is "default" we
61
+ * fall back to the legacy unprefixed key so older single-app setups
62
+ * keep working without migration.
63
+ */
64
+ export declare function storageKey(slot: string): string;
65
+ export declare function configureClient(config: AgentDBClientConfig): void;
66
+ export declare function fetchList(entity: string): Promise<Record<string, unknown>[]>;
67
+ export declare function fetchById(entity: string, id: string): Promise<Record<string, unknown> | null>;
68
+ export declare function insert(entity: string, data: Record<string, unknown>): Promise<{
69
+ id: string;
70
+ }>;
71
+ export declare function update(entity: string, id: string, data: Record<string, unknown>): Promise<{
72
+ updated: boolean;
73
+ }>;
74
+ export declare function remove(entity: string, id: string): Promise<{
75
+ deleted: boolean;
76
+ }>;
77
+ export declare function createSession(userId: string): Promise<{
78
+ token: string;
79
+ user_id: string;
80
+ }>;
81
+ export declare function getAuthContext(token?: string): Promise<{
82
+ user_id: string | null;
83
+ }>;
84
+ /**
85
+ * Exchange a current session token for a new one with a fresh 30-day expiry.
86
+ * The old token is revoked server-side. Call this before expiry to keep
87
+ * long-lived sessions alive without forcing a re-login.
88
+ *
89
+ * Returns `null` if the old token is already expired or invalid — the
90
+ * caller should treat that as "log back in."
91
+ */
92
+ export declare function refreshSession(token: string): Promise<{
93
+ token: string;
94
+ user_id: string;
95
+ expires_at: number;
96
+ } | null>;
97
+ /**
98
+ * Keep a session alive by automatically refreshing ~1 hour before expiry.
99
+ *
100
+ * ```ts
101
+ * const session = await createSession("alice");
102
+ * const stop = startSessionAutoRefresh(session, {
103
+ * onRefresh: (next) => localStorage.setItem("token", next.token),
104
+ * onExpired: () => redirect("/login"),
105
+ * });
106
+ * // later:
107
+ * stop();
108
+ * ```
109
+ *
110
+ * Returns a cleanup function that cancels the scheduled refresh. Call it
111
+ * on logout or unmount — otherwise the timer leaks.
112
+ *
113
+ * Default refresh margin is 1 hour. Pass `{ marginSecs }` to tune.
114
+ */
115
+ export declare function startSessionAutoRefresh(session: {
116
+ token: string;
117
+ expires_at: number;
118
+ }, opts: {
119
+ onRefresh: (next: {
120
+ token: string;
121
+ user_id: string;
122
+ expires_at: number;
123
+ }) => void;
124
+ onExpired?: () => void;
125
+ marginSecs?: number;
126
+ }): () => void;
127
+ /**
128
+ * Swap the storage adapter used by the React free helpers (`callFn`,
129
+ * `useSession`, `getAuthToken`, etc). React Native's `init()` calls this
130
+ * with an AsyncStorage-backed adapter so token reads/writes go through
131
+ * the same backend as the sync engine.
132
+ */
133
+ export declare function setReactStorage(storage: PylonStorage): void;
134
+ /** Current storage adapter used by the React layer. Exposed for adapters. */
135
+ export declare function getReactStorage(): PylonStorage;
136
+ export declare function callFn<T = unknown>(name: string, args?: Record<string, unknown>, options?: {
137
+ token?: string;
138
+ }): Promise<T>;
139
+ /**
140
+ * Stream a server-side function's output as Server-Sent Events.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * for await (const chunk of streamFn("chat", { message: "hello" })) {
145
+ * console.log(chunk);
146
+ * }
147
+ * ```
148
+ */
149
+ export declare function streamFn(name: string, args?: Record<string, unknown>, options?: {
150
+ token?: string;
151
+ }): AsyncGenerator<string, unknown, unknown>;
152
+ /**
153
+ * List all server-side functions available.
154
+ */
155
+ export declare function listFns(): Promise<{
156
+ name: string;
157
+ fn_type: "query" | "mutation" | "action";
158
+ }[]>;
159
+ export interface UploadedFile {
160
+ id: string;
161
+ url: string;
162
+ size: number;
163
+ }
164
+ /**
165
+ * Upload a file (File/Blob or raw bytes) to /api/files/upload.
166
+ *
167
+ * For File / Blob inputs this sends a single raw binary request with the
168
+ * filename and content-type as headers (the server short-circuits on this
169
+ * shape so uploads avoid being coerced through string-based handling).
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const uploaded = await uploadFile(fileFromInput);
174
+ * console.log(uploaded.url, uploaded.id, uploaded.size);
175
+ * ```
176
+ */
177
+ export declare function uploadFile(input: File | Blob | ArrayBuffer | Uint8Array, options?: {
178
+ filename?: string;
179
+ contentType?: string;
180
+ token?: string;
181
+ }): Promise<UploadedFile>;
182
+ /**
183
+ * Upload via multipart/form-data. Useful when the app needs to pass extra
184
+ * fields alongside the file (captions, categories, etc.), though only the
185
+ * first file part is stored today.
186
+ */
187
+ export declare function uploadFileMultipart(file: File | Blob, fields?: Record<string, string>, options?: {
188
+ token?: string;
189
+ }): Promise<UploadedFile>;
package/dist/ssr.d.ts ADDED
@@ -0,0 +1,415 @@
1
+ /**
2
+ * The resolved Pylon auth context for the request that rendered the page.
3
+ * Safe to read in the component body — it is serialized into the hydration
4
+ * payload, so the server render and the client hydration see the same
5
+ * values (no hydration mismatch).
6
+ */
7
+ export interface PageAuth {
8
+ /** The signed-in user's id, or null for an anonymous request. */
9
+ user_id: string | null;
10
+ /** True when the session is an admin session (PYLON_ADMIN_EMAILS). */
11
+ is_admin: boolean;
12
+ /** The active tenant/org id, or null. */
13
+ tenant_id: string | null;
14
+ /** Role slugs granted to the session. */
15
+ roles: string[];
16
+ }
17
+ /** Options for `response.setCookie`. Defaults: HttpOnly + SameSite=Lax. */
18
+ export interface SsrCookieOptions {
19
+ path?: string;
20
+ domain?: string;
21
+ maxAge?: number;
22
+ expires?: Date | string;
23
+ /** Defaults to true (secure default). Pass false for a client-readable cookie. */
24
+ httpOnly?: boolean;
25
+ secure?: boolean;
26
+ sameSite?: "Strict" | "Lax" | "None";
27
+ }
28
+ /**
29
+ * The per-render `response` controller. Pylon already has a backend for
30
+ * data + mutations, so SSR's job is just the HTTP response envelope:
31
+ * status, redirects, 404, and the occasional Set-Cookie.
32
+ *
33
+ * IMPORTANT — call these during the SYNCHRONOUS shell render (the component
34
+ * body, before any `await` / Suspense boundary). The HTTP head is committed
35
+ * when the shell is ready; status/headers/cookies set from a suspended
36
+ * subtree that streams in later are lost, and a `redirect()` / `notFound()`
37
+ * thrown below a Suspense boundary is swallowed by React's error handling
38
+ * rather than turned into a 3xx / 404.
39
+ */
40
+ export interface SsrResponse {
41
+ /** Set the HTTP status (100–599). Default 200. */
42
+ setStatus(code: number): void;
43
+ /** Set a response header (name must be a token; value CR/LF/NUL-free). */
44
+ setHeader(name: string, value: string): void;
45
+ /** Append a Set-Cookie. Defaults: HttpOnly + SameSite=Lax. */
46
+ setCookie(name: string, value: string, opts?: SsrCookieOptions): void;
47
+ /** Throw to send a 3xx (default 307) + Location, no body. Shell-render only. */
48
+ redirect(url: string, status?: number): never;
49
+ /**
50
+ * Throw to send a 404. Renders the nearest `not-found.tsx` (walking up
51
+ * from the page's directory, wrapped in the route's layout chain), or a
52
+ * minimal framework body if none is defined. Shell-render only.
53
+ */
54
+ notFound(): never;
55
+ }
56
+ /**
57
+ * Read-only database handle a page reaches during render via React 19
58
+ * `use()` + `<Suspense>`. Reads run through the same store + policy gate as
59
+ * a query function's `ctx.db`; writes are rejected. Resolved values are
60
+ * cached and replayed into the hydration payload, so the client does not
61
+ * re-fetch on hydration.
62
+ *
63
+ * ```tsx
64
+ * export default function Page({ serverData }: PageProps) {
65
+ * const posts = use(serverData.list<Post>("Post"));
66
+ * return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
67
+ * }
68
+ * ```
69
+ */
70
+ export interface ServerData {
71
+ /** Get a single row by id. Resolves to null if not found. */
72
+ get<T = Record<string, unknown>>(entity: string, id: string): Promise<T | null>;
73
+ /** List all (policy-visible) rows for an entity. */
74
+ list<T = Record<string, unknown>>(entity: string): Promise<T[]>;
75
+ /** Look up a row by a field value (e.g. email). Null if not found. */
76
+ lookup<T = Record<string, unknown>>(entity: string, field: string, value: string): Promise<T | null>;
77
+ /** Query with filters ($gt, $lt, $in, $like, $order, $limit, …). */
78
+ query<T = Record<string, unknown>>(entity: string, filter: Record<string, unknown>): Promise<T[]>;
79
+ /** Execute a graph query with nested relation includes. */
80
+ queryGraph<T = Record<string, unknown>>(query: Record<string, unknown>): Promise<T>;
81
+ /** Cursor-paginated list. Pass `cursor` from a previous page's result. */
82
+ paginate<T = Record<string, unknown>>(entity: string, opts: {
83
+ numItems: number;
84
+ cursor?: string | null;
85
+ }): Promise<{
86
+ rows?: T[];
87
+ page?: T[];
88
+ nextCursor?: string | null;
89
+ }>;
90
+ /** Faceted full-text search against an entity with a `search:` config. */
91
+ search<T = Record<string, unknown>>(entity: string, query: Record<string, unknown>): Promise<{
92
+ hits: T[];
93
+ facetCounts?: Record<string, Record<string, number>>;
94
+ total: number;
95
+ tookMs?: number;
96
+ }>;
97
+ }
98
+ /**
99
+ * Props every `page.tsx` / `layout.tsx` receives. Generic over the dynamic
100
+ * route params and the parsed query string, so a route like
101
+ * `app/blog/[slug]/page.tsx` can type them:
102
+ *
103
+ * ```tsx
104
+ * export default function Post({ params }: PageProps<{ slug: string }>) {
105
+ * return <article>{params.slug}</article>;
106
+ * }
107
+ * ```
108
+ *
109
+ * Note: the incoming request's headers + cookies are intentionally NOT on
110
+ * this type. They are available only during the server render and are
111
+ * stripped from the hydration payload (a session cookie must never reach
112
+ * client JS), so reading them in the component body would hydrate-mismatch.
113
+ * Read request-derived data through `serverData` or a server function.
114
+ */
115
+ export interface PageProps<TParams extends Record<string, string> = Record<string, string>, TSearchParams extends Record<string, string> = Record<string, string>> {
116
+ /** The incoming URL path (e.g. `/blog/hello-world`). */
117
+ url: string;
118
+ /** Dynamic-segment matches keyed by name (e.g. `{ slug: "hello-world" }`). */
119
+ params: TParams;
120
+ /** Parsed query string (e.g. `?start=10` → `{ start: "10" }`). */
121
+ searchParams: TSearchParams;
122
+ /** The resolved auth context for the request. */
123
+ auth: PageAuth;
124
+ /** The HTTP response controller (status / headers / cookies / redirect). */
125
+ response: SsrResponse;
126
+ /** Read-only database handle for in-render data (use with `use()`). */
127
+ serverData: ServerData;
128
+ }
129
+ /**
130
+ * Page SEO metadata. Export `const metadata` (static) or
131
+ * `async function generateMetadata(props)` (dynamic, e.g. param-derived
132
+ * titles) from a `page.tsx` / `layout.tsx`. React 19 hoists the resulting
133
+ * `<title>` / `<meta>` / `<link>` into `<head>`.
134
+ */
135
+ /** A single OpenGraph image (for `openGraph.images` — multiple images). */
136
+ export interface OgImage {
137
+ url: string;
138
+ secureUrl?: string;
139
+ type?: string;
140
+ width?: number;
141
+ height?: number;
142
+ alt?: string;
143
+ }
144
+ export interface Metadata {
145
+ title?: string;
146
+ description?: string;
147
+ keywords?: string | string[];
148
+ canonical?: string;
149
+ robots?: string;
150
+ /** `<meta name="author">` — one tag per author. */
151
+ authors?: string | string[];
152
+ /** `<meta name="theme-color">` — browser UI tint for the page. */
153
+ themeColor?: string;
154
+ openGraph?: {
155
+ title?: string;
156
+ description?: string;
157
+ image?: string;
158
+ imageSecureUrl?: string;
159
+ imageType?: string;
160
+ imageWidth?: number;
161
+ imageHeight?: number;
162
+ imageAlt?: string;
163
+ /** Additional images beyond the primary `image` (each emits its own
164
+ * `og:image` + dimensions). Provide absolute URLs. */
165
+ images?: OgImage[];
166
+ url?: string;
167
+ type?: string;
168
+ /** `og:locale` (e.g. "en_US"). */
169
+ locale?: string;
170
+ /** `og:site_name` — the brand the page belongs to (e.g. "Pylon").
171
+ * Discord and other unfurlers show this above the title. */
172
+ siteName?: string;
173
+ /** `article:*` tags for `og:type=article` pages. */
174
+ article?: {
175
+ author?: string | string[];
176
+ publishedTime?: string;
177
+ modifiedTime?: string;
178
+ section?: string;
179
+ tags?: string | string[];
180
+ };
181
+ };
182
+ twitter?: {
183
+ card?: string;
184
+ title?: string;
185
+ description?: string;
186
+ image?: string;
187
+ /** `twitter:site` / `twitter:creator` — @handles. */
188
+ site?: string;
189
+ creator?: string;
190
+ /** `twitter:image:alt` — alt text for the card image. */
191
+ imageAlt?: string;
192
+ };
193
+ icons?: {
194
+ icon?: {
195
+ url: string;
196
+ type?: string;
197
+ sizes?: string;
198
+ };
199
+ apple?: {
200
+ url: string;
201
+ type?: string;
202
+ sizes?: string;
203
+ };
204
+ };
205
+ /** Alternate URLs. `languages` emits `<link rel="alternate" hreflang>`
206
+ * per locale; `canonical` is an alias for the top-level `canonical`. */
207
+ alternates?: {
208
+ canonical?: string;
209
+ languages?: Record<string, string>;
210
+ };
211
+ /** Structured data (schema.org), emitted as `<script type="application/ld+json">`.
212
+ * Pass one object or an array (each item gets its own script). The payload is
213
+ * serialized with `<`/`>`/`&` escaped so it can't break out of the script. */
214
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
215
+ }
216
+ /**
217
+ * Signature of a dynamic `generateMetadata` export. Receives the same props
218
+ * as the page; return (or resolve to) the page's `Metadata`. Awaited before
219
+ * the first byte, so keep it to cheap derivations.
220
+ */
221
+ export type GenerateMetadata<TParams extends Record<string, string> = Record<string, string>, TSearchParams extends Record<string, string> = Record<string, string>> = (props: PageProps<TParams, TSearchParams>) => Metadata | Promise<Metadata>;
222
+ /**
223
+ * Return type for an `app/sitemap.ts` default export (Next-shaped). The runtime
224
+ * serializes it to `/sitemap.xml`. The export may be sync or async, so it can
225
+ * enumerate dynamic pages from the DB:
226
+ *
227
+ * export default async function sitemap(): Promise<Sitemap> {
228
+ * const posts = await getPosts();
229
+ * return [{ url: "https://x.com/", priority: 1 }, ...posts.map(...)];
230
+ * }
231
+ */
232
+ export interface SitemapEntry {
233
+ url: string;
234
+ lastModified?: string | Date;
235
+ changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
236
+ priority?: number;
237
+ /** hreflang alternates, e.g. `{ languages: { "en-US": "https://…/en" } }`. */
238
+ alternates?: {
239
+ languages?: Record<string, string>;
240
+ };
241
+ }
242
+ export type Sitemap = SitemapEntry[];
243
+ export interface RobotsRule {
244
+ userAgent?: string | string[];
245
+ allow?: string | string[];
246
+ disallow?: string | string[];
247
+ crawlDelay?: number;
248
+ }
249
+ /**
250
+ * Return type for an `app/robots.ts` default export (Next-shaped). The runtime
251
+ * serializes it to `/robots.txt`.
252
+ *
253
+ * export default function robots(): Robots {
254
+ * return {
255
+ * rules: { userAgent: "*", allow: "/", disallow: "/admin" },
256
+ * sitemap: "https://x.com/sitemap.xml",
257
+ * };
258
+ * }
259
+ */
260
+ export interface Robots {
261
+ rules: RobotsRule | RobotsRule[];
262
+ sitemap?: string | string[];
263
+ host?: string;
264
+ }
265
+ /**
266
+ * Per-route configuration, declared as top-level `export const` in a
267
+ * `page.tsx` (Next-shaped). All optional. The runtime reads these statically
268
+ * before the render.
269
+ *
270
+ * ```ts
271
+ * export const revalidate = 60; // CDN + origin-disk cache for 60s
272
+ * export const dynamic = "force-dynamic"; // never cache
273
+ * export const streaming = true; // progressive Suspense streaming
274
+ * ```
275
+ *
276
+ * - `revalidate` — seconds an auth-independent render stays cacheable
277
+ * (`public, s-maxage=N`); also kept in the origin disk cache. See the
278
+ * "Anonymous output caching" rules — it only takes effect if the render
279
+ * never reads `auth`, sets no cookie, and isn't running strict policies.
280
+ * - `dynamic` — `"force-static"` caches until the next deploy; `"force-dynamic"`
281
+ * opts out of all caching.
282
+ * - `streaming` — opt into PROGRESSIVE streaming: the shell + each inner
283
+ * `<Suspense>` fallback flush immediately, then each boundary's real content
284
+ * streams in as its data resolves (instead of buffering the whole document).
285
+ * A page with a `loading.tsx` already streams at the route level; this
286
+ * extends it to a page's OWN inner boundaries. Mutually exclusive with
287
+ * caching: a streaming render commits its HTTP head before suspended
288
+ * subtrees finish, so it can never be marked cacheable (and a deep
289
+ * `<Suspense>` throw resolves via its `error.tsx` at HTTP 200, not a 5xx).
290
+ * Set `response.*` (status/cookies) only in the synchronous shell — late
291
+ * calls from a suspended subtree are dropped (the runtime warns).
292
+ */
293
+ export interface RouteSegmentConfig {
294
+ revalidate?: number;
295
+ dynamic?: "force-static" | "force-dynamic";
296
+ streaming?: boolean;
297
+ }
298
+ /**
299
+ * Props an `app/.../error.tsx` boundary receives. Error boundaries are now
300
+ * HYDRATED (interactive — useState/onClick/effects work), so `reset` is a
301
+ * real callback that re-attempts rendering the segment (a transient error
302
+ * clears to the page; a deterministic one re-shows the boundary).
303
+ *
304
+ * `error` carries ONLY the thrown error's `message` plus a short,
305
+ * non-reversible `digest` (a correlation id matching the server log). The
306
+ * stack NEVER reaches the client — read it from the dev overlay
307
+ * (`PYLON_DEV_MODE`) or the server logs.
308
+ *
309
+ * ```tsx
310
+ * export default function Error({ error, reset }: ErrorBoundaryProps) {
311
+ * return (
312
+ * <div>
313
+ * <p>Something went wrong: {error.message}</p>
314
+ * <button onClick={reset}>Try again</button>
315
+ * </div>
316
+ * );
317
+ * }
318
+ * ```
319
+ */
320
+ export interface ErrorBoundaryProps {
321
+ error: {
322
+ message: string;
323
+ digest?: string;
324
+ };
325
+ reset: () => void;
326
+ }
327
+ /**
328
+ * Props an `app/.../not-found.tsx` boundary receives. Not-found boundaries
329
+ * are hydrated (interactive) too, but — matching Next — receive NO `reset`.
330
+ * Same shape as a page.
331
+ */
332
+ export type NotFoundProps = PageProps;
333
+ /**
334
+ * Parsed form fields handed to a `route.ts` handler. Mirrors URLSearchParams
335
+ * `get`/`getAll`/`has` semantics over the submitted body.
336
+ */
337
+ export interface FormFields {
338
+ /** First value for `name`, or null. */
339
+ get(name: string): string | null;
340
+ /** All values for `name` (empty array if none). */
341
+ getAll(name: string): string[];
342
+ has(name: string): boolean;
343
+ /** Raw map: name → value (single) or values (repeated field). */
344
+ readonly fields: Record<string, string | string[]>;
345
+ }
346
+ /**
347
+ * Read + write DB handle for a `route.ts` form handler (mutation-shaped). The
348
+ * read surface is `ServerData`; writes go through the same policy-checked,
349
+ * broadcast-firing path a mutation's `ctx.db` uses.
350
+ */
351
+ export interface FormDb extends ServerData {
352
+ insert<T = Record<string, unknown>>(entity: string, data: Record<string, unknown>): Promise<T>;
353
+ update<T = Record<string, unknown>>(entity: string, id: string, data: Record<string, unknown>): Promise<T>;
354
+ delete(entity: string, id: string): Promise<void>;
355
+ }
356
+ /**
357
+ * The request a `route.ts` POST/PUT/PATCH/DELETE handler receives. Shape the
358
+ * reply through `response` — usually `response.redirect("/x?ok=1")` (303
359
+ * POST-redirect-GET, the default) after a write, so a no-JS browser follows
360
+ * with a GET. Enforce trust with `auth` inside the handler (forms run with the
361
+ * standard function trust model).
362
+ *
363
+ * ```ts
364
+ * import type { RouteHandler } from "@pylonsync/react";
365
+ * export const POST: RouteHandler = async ({ form, db, response, auth }) => {
366
+ * const body = form.get("body");
367
+ * if (!body) return response.redirect("/notes?error=empty");
368
+ * await db.insert("Note", { body });
369
+ * response.redirect("/notes?created=1");
370
+ * };
371
+ * ```
372
+ */
373
+ export interface FormRequest<TParams extends Record<string, string> = Record<string, string>, TSearchParams extends Record<string, string> = Record<string, string>> {
374
+ form: FormFields;
375
+ params: TParams;
376
+ searchParams: TSearchParams;
377
+ auth: PageAuth;
378
+ cookies: Record<string, string>;
379
+ headers: Record<string, string>;
380
+ db: FormDb;
381
+ response: SsrResponse;
382
+ }
383
+ /** Signature of a `route.ts` method handler export (POST/PUT/PATCH/DELETE). */
384
+ export type RouteHandler<TParams extends Record<string, string> = Record<string, string>, TSearchParams extends Record<string, string> = Record<string, string>> = (req: FormRequest<TParams, TSearchParams>) => void | Promise<void>;
385
+ /**
386
+ * What a `route.ts` `GET` (raw) handler returns. The body is streamed verbatim
387
+ * with `contentType` (default `text/plain; charset=utf-8`), `status` (default
388
+ * 200), and any extra `headers` — no React render, no hydration tail. This is
389
+ * the GET analogue of `sitemap.ts`/`robots.ts`, at an arbitrary route path:
390
+ * RSS/Atom feeds, dynamic XML, plain text, JSON, `.well-known` documents.
391
+ */
392
+ export interface RawResponse {
393
+ body?: string;
394
+ /** Defaults to `text/plain; charset=utf-8`. */
395
+ contentType?: string;
396
+ /** Defaults to 200. */
397
+ status?: number;
398
+ /** Extra response headers (e.g. `cache-control`). Lower-cased by the host. */
399
+ headers?: Record<string, string>;
400
+ }
401
+ /**
402
+ * Signature of a `route.ts` `GET` export — a RAW handler. Return a
403
+ * {@link RawResponse} to stream a body, or use `response.redirect()` /
404
+ * `response.notFound()` and return nothing.
405
+ *
406
+ * ```ts
407
+ * import type { RawRouteHandler } from "@pylonsync/react";
408
+ * export const GET: RawRouteHandler = async () => ({
409
+ * body: "<rss>…</rss>",
410
+ * contentType: "application/xml; charset=utf-8",
411
+ * headers: { "cache-control": "public, max-age=300" },
412
+ * });
413
+ * ```
414
+ */
415
+ export type RawRouteHandler<TParams extends Record<string, string> = Record<string, string>, TSearchParams extends Record<string, string> = Record<string, string>> = (req: FormRequest<TParams, TSearchParams>) => RawResponse | void | Promise<RawResponse | void>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Typed database client — narrow pylon's untyped `db` object using your
3
+ * generated `AppSchema` (from `pylon codegen client`).
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * // src/pylon.client.ts (generated by `pylon codegen client`)
8
+ * export interface AppSchema {
9
+ * entities: { Todo: Todo; User: User };
10
+ * functions: {
11
+ * placeBid: { input: { lotId: string; amount: number }; output: { accepted: boolean } };
12
+ * };
13
+ * queries: {};
14
+ * }
15
+ *
16
+ * // src/db.ts
17
+ * import { createTypedDb } from "@pylonsync/react";
18
+ * import type { AppSchema } from "./pylon.client";
19
+ * export const db = createTypedDb<AppSchema>();
20
+ *
21
+ * // Components get full type safety:
22
+ * const { data } = db.useQuery("Todo"); // data: Todo[]
23
+ * const { data: user } = db.useQueryOne("User", "u_1"); // user: User | null
24
+ * const bid = db.useMutation("placeBid"); // typed args + result
25
+ * await bid.mutate({ lotId: "x", amount: 150 }); // TS checks input type
26
+ * ```
27
+ */
28
+ import { db as untypedDb } from "./db";
29
+ import type { QueryOptions, UseQueryReturn, UseQueryOneReturn, UseMutationReturn, UseInfiniteQueryReturn } from "./hooks";
30
+ /**
31
+ * Shape of the generated `AppSchema` interface.
32
+ *
33
+ * The CLI's `client_codegen` emits a type matching this shape. Consumers
34
+ * never construct `AppSchema` manually.
35
+ */
36
+ export interface AgentDBSchema {
37
+ entities: Record<string, unknown>;
38
+ functions: Record<string, {
39
+ input: unknown;
40
+ output: unknown;
41
+ }>;
42
+ queries: Record<string, {
43
+ input: unknown;
44
+ output: unknown;
45
+ }>;
46
+ }
47
+ export interface TypedDb<S extends AgentDBSchema> {
48
+ /** Live query with type inferred from the schema. */
49
+ useQuery<K extends keyof S["entities"]>(entity: K, options?: QueryOptions): UseQueryReturn<S["entities"][K]>;
50
+ /** Live single-row query. */
51
+ useQueryOne<K extends keyof S["entities"]>(entity: K, id: string): UseQueryOneReturn<S["entities"][K]>;
52
+ /** Server-side function call with typed input/output. */
53
+ useMutation<K extends keyof S["functions"]>(fnName: K): UseMutationReturn<S["functions"][K]["input"], S["functions"][K]["output"]>;
54
+ /** Paginated live query. */
55
+ useInfiniteQuery<K extends keyof S["entities"]>(entity: K, options?: {
56
+ pageSize?: number;
57
+ }): UseInfiniteQueryReturn<S["entities"][K]>;
58
+ /** Call a server-side function directly (outside React). */
59
+ fn<K extends keyof S["functions"]>(name: K, args: S["functions"][K]["input"]): Promise<S["functions"][K]["output"]>;
60
+ /** Insert a row (optimistic). */
61
+ insert<K extends keyof S["entities"]>(entity: K, data: Partial<S["entities"][K]>): unknown;
62
+ /** Update a row (optimistic). */
63
+ update<K extends keyof S["entities"]>(entity: K, id: string, data: Partial<S["entities"][K]>): unknown;
64
+ /** Delete a row (optimistic). */
65
+ delete<K extends keyof S["entities"]>(entity: K, id: string): unknown;
66
+ /** Underlying untyped db (escape hatch). */
67
+ readonly untyped: typeof untypedDb;
68
+ }
69
+ /**
70
+ * Create a typed client from a generated `AppSchema`.
71
+ *
72
+ * At runtime this is literally the same object as `db`; types are narrowed
73
+ * at compile time via generics.
74
+ */
75
+ export declare function createTypedDb<S extends AgentDBSchema>(): TypedDb<S>;