@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.
- package/dist/Form.d.ts +11 -0
- package/dist/Image.d.ts +39 -0
- package/dist/Link.d.ts +27 -0
- package/dist/db.d.ts +163 -0
- package/dist/hooks.d.ts +388 -0
- package/dist/index.d.ts +189 -0
- package/dist/ssr.d.ts +415 -0
- package/dist/typed.d.ts +75 -0
- package/dist/useRoom.d.ts +93 -0
- package/dist/useRouter.d.ts +74 -0
- package/dist/useSession.d.ts +41 -0
- package/dist/useShard.d.ts +58 -0
- package/dist/useSyncStatus.d.ts +30 -0
- package/package.json +14 -8
- package/src/ssr.ts +42 -0
- package/tsconfig.json +0 -10
package/dist/Form.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface FormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "method" | "action"> {
|
|
3
|
+
/** The `route.ts` handler path (e.g. "/notes"). */
|
|
4
|
+
action: string;
|
|
5
|
+
/** HTTP method. Default "post" (the only method usable without JS). */
|
|
6
|
+
method?: "post" | "put" | "patch" | "delete";
|
|
7
|
+
/** Opt out of client interception — force a native full-page submit. */
|
|
8
|
+
navigate?: boolean;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare function Form({ action, method, navigate, children, ...rest }: FormProps): React.JSX.Element;
|
package/dist/Image.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height" | "loading" | "srcSet"> {
|
|
3
|
+
/** Source URL — site-relative (`/foo.jpg`) or http(s) (allowlisted via env). */
|
|
4
|
+
src: string;
|
|
5
|
+
/** Intrinsic width in CSS px (used for aspect ratio + the 1x candidate). */
|
|
6
|
+
width: number;
|
|
7
|
+
/** Intrinsic height in CSS px. */
|
|
8
|
+
height: number;
|
|
9
|
+
/** Required alt text. Pass `""` for purely decorative images. */
|
|
10
|
+
alt: string;
|
|
11
|
+
/**
|
|
12
|
+
* JPEG/WebP quality 1..=100. Default 75 — matches Next.js.
|
|
13
|
+
* PNG ignores it (lossless).
|
|
14
|
+
*/
|
|
15
|
+
quality?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Override the candidate widths used in `srcset`. By default we
|
|
18
|
+
* emit 1x and 2x of `width`, capped at 3840px.
|
|
19
|
+
*/
|
|
20
|
+
widths?: number[];
|
|
21
|
+
/**
|
|
22
|
+
* `<img sizes>` attribute. Default `100vw` — change to match the
|
|
23
|
+
* container width so the browser picks the smallest srcset
|
|
24
|
+
* candidate that fits. Example: `(max-width: 768px) 100vw, 50vw`.
|
|
25
|
+
*/
|
|
26
|
+
sizes?: string;
|
|
27
|
+
/** Skip lazy-loading + bump fetch priority. Use for above-the-fold hero images. */
|
|
28
|
+
priority?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Skip the Pylon optimizer and render `src` directly. Useful for
|
|
31
|
+
* SVGs (the optimizer rejects them as a security precaution),
|
|
32
|
+
* animated GIFs, or formats Pylon doesn't process. The browser
|
|
33
|
+
* still gets `width`/`height` for layout stability, but there's
|
|
34
|
+
* no `srcset` and no caching beyond whatever the source URL
|
|
35
|
+
* declares.
|
|
36
|
+
*/
|
|
37
|
+
unoptimized?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function Image({ src, width, height, alt, quality, widths, sizes, priority, unoptimized, className, style, ...rest }: ImageProps): React.JSX.Element;
|
package/dist/Link.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
__pylon?: {
|
|
5
|
+
prefetch: (href: string) => Promise<void>;
|
|
6
|
+
navigate: (href: string, opts?: {
|
|
7
|
+
push?: boolean;
|
|
8
|
+
replace?: boolean;
|
|
9
|
+
}) => Promise<void>;
|
|
10
|
+
/** Current route's dynamic params (read by useParams). A getter on the
|
|
11
|
+
* runtime side, so it always reflects the latest navigation. */
|
|
12
|
+
readonly params?: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
17
|
+
/** Destination path. Same-origin paths get client-side nav; off-origin paths render as plain <a>. */
|
|
18
|
+
href: string;
|
|
19
|
+
/**
|
|
20
|
+
* Prefetch the destination on viewport entry. Default true.
|
|
21
|
+
* Set `false` to skip prefetch (useful for links the user
|
|
22
|
+
* is unlikely to follow — pagination tail, etc.).
|
|
23
|
+
*/
|
|
24
|
+
prefetch?: boolean;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare function Link({ href, prefetch, children, ...rest }: LinkProps): React.JSX.Element;
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { SyncEngine, type Row, type SyncEngineConfig } from "@pylonsync/sync";
|
|
2
|
+
import { type QueryOptions, type UseQueryReturn, type UseQueryOneReturn, type UseReactiveQueryReturn, type UseMutationReturn, type UseInfiniteQueryReturn, type AggregateSpec, type UseAggregateReturn, type SearchSpec, type UseSearchReturn } from "./hooks";
|
|
3
|
+
import { type UploadedFile } from "./index";
|
|
4
|
+
/**
|
|
5
|
+
* Initialize the pylon client. Call once at app startup.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { init } from "@pylonsync/react";
|
|
9
|
+
* init({ baseUrl: "http://localhost:4321" });
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Omitting `baseUrl` in a browser context falls back to
|
|
13
|
+
* `window.location.origin` — the right answer for same-origin
|
|
14
|
+
* deployments (Next.js + Vercel rewrites, embedded SPA). Passing an
|
|
15
|
+
* explicit `baseUrl` always wins. We deliberately do NOT default to
|
|
16
|
+
* `http://localhost:4321` in browsers — that footgun caused production
|
|
17
|
+
* dashboards to fire requests at the engineer's dev port.
|
|
18
|
+
*/
|
|
19
|
+
export declare function init(config?: Partial<SyncEngineConfig> & {
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
}): void;
|
|
22
|
+
/** Module-internal accessor for the global sync engine. Exported so
|
|
23
|
+
* hooks living outside this file (e.g. `useRoom`) can share the same
|
|
24
|
+
* engine instance and benefit from the same lazy-start / lazy-init
|
|
25
|
+
* semantics — without re-implementing the resolution rules. */
|
|
26
|
+
export declare function getSync(): SyncEngine;
|
|
27
|
+
/**
|
|
28
|
+
* Live query with loading/error state.
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const { data, loading, error } = db.useQuery<Todo>("Todo", {
|
|
32
|
+
* where: { done: false },
|
|
33
|
+
* orderBy: { createdAt: "desc" },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const db: {
|
|
38
|
+
/** Live query for entity rows with loading/error state. */
|
|
39
|
+
useQuery<T = Row>(entity: string, options?: QueryOptions): UseQueryReturn<T>;
|
|
40
|
+
/** Live query for a single row by ID. */
|
|
41
|
+
useQueryOne<T = Row>(entity: string, id: string): UseQueryOneReturn<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Reactive query — Convex-style auto-rerunning server handler.
|
|
44
|
+
*
|
|
45
|
+
* The server runs your `query()` handler with dependency tracking
|
|
46
|
+
* (every `ctx.db.*` read is recorded), registers the subscription,
|
|
47
|
+
* and pushes the initial result. Any future mutation touching the
|
|
48
|
+
* dep set triggers a re-run + push.
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const { data: feed, loading } = db.useReactiveQuery<FeedItem[]>(
|
|
52
|
+
* "getFeed",
|
|
53
|
+
* { userId: currentUser.id },
|
|
54
|
+
* );
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Authoring side: define the handler with `query()` from
|
|
58
|
+
* `@pylonsync/functions`. Any handler is eligible — no opt-in flag.
|
|
59
|
+
*/
|
|
60
|
+
useReactiveQuery<T = unknown>(fnName: string, args?: unknown): UseReactiveQueryReturn<T>;
|
|
61
|
+
/**
|
|
62
|
+
* Server-side function call with mutation state (loading, data, error).
|
|
63
|
+
*
|
|
64
|
+
* ```tsx
|
|
65
|
+
* const placeBid = db.useMutation<{lotId: string}, {accepted: boolean}>("placeBid");
|
|
66
|
+
* await placeBid.mutate({ lotId: "x", amount: 150 });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* For optimistic UI, pass an `optimistic` builder — the framework
|
|
70
|
+
* paints the row into the local store immediately, threads a
|
|
71
|
+
* matching id through to the server function, and reconciles the
|
|
72
|
+
* canonical broadcast as an in-place merge. See
|
|
73
|
+
* docs/concepts/optimistic-updates for the full pattern.
|
|
74
|
+
*
|
|
75
|
+
* ```tsx
|
|
76
|
+
* const send = db.useMutation<{channelId: string; body: string}, {messageId: string}>(
|
|
77
|
+
* "sendMessage",
|
|
78
|
+
* {
|
|
79
|
+
* optimistic: (args, ctx) => ({
|
|
80
|
+
* entity: "Message",
|
|
81
|
+
* data: { id: ctx.id, ...args, authorId: me.id, createdAt: ctx.now },
|
|
82
|
+
* }),
|
|
83
|
+
* }
|
|
84
|
+
* );
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
useMutation<TArgs = Record<string, unknown>, TResult = unknown>(fnName: string, options?: {
|
|
88
|
+
optimistic?: import("./hooks").OptimisticBuilder<TArgs>;
|
|
89
|
+
}): UseMutationReturn<TArgs, TResult>;
|
|
90
|
+
/** Paginated live query with loadMore(). */
|
|
91
|
+
useInfiniteQuery<T = Row>(entity: string, options?: {
|
|
92
|
+
pageSize?: number;
|
|
93
|
+
}): UseInfiniteQueryReturn<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Live aggregate query (count / sum / avg / groupBy). Automatically
|
|
96
|
+
* re-runs when the entity's rows change in the sync replica — dashboard
|
|
97
|
+
* charts stay up to date without polling.
|
|
98
|
+
*/
|
|
99
|
+
useAggregate<Row = Record<string, unknown>>(entity: string, spec: AggregateSpec): UseAggregateReturn<Row>;
|
|
100
|
+
/**
|
|
101
|
+
* Live faceted full-text search. Returns ranked hits + per-facet
|
|
102
|
+
* counts + total; re-runs when the entity's rows change so facet
|
|
103
|
+
* counts and result lists stay in lockstep with writes.
|
|
104
|
+
*
|
|
105
|
+
* ```tsx
|
|
106
|
+
* const { hits, facetCounts, total } = db.useSearch<Product>("Product", {
|
|
107
|
+
* query: "red sneakers",
|
|
108
|
+
* filters: { category: "shoes" },
|
|
109
|
+
* facets: ["brand", "color"],
|
|
110
|
+
* sort: ["price", "desc"],
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
useSearch<T = Row>(entity: string, spec: SearchSpec): UseSearchReturn<T>;
|
|
115
|
+
/** Entity-level optimistic CRUD (not server-side functions). */
|
|
116
|
+
useEntity(entity: string): {
|
|
117
|
+
insert: (data: Row) => Promise<string>;
|
|
118
|
+
update: (id: string, data: Partial<Row>) => Promise<void>;
|
|
119
|
+
remove: (id: string) => Promise<void>;
|
|
120
|
+
};
|
|
121
|
+
/** Get the sync engine instance. */
|
|
122
|
+
readonly sync: SyncEngine;
|
|
123
|
+
/** Insert a row (optimistic). */
|
|
124
|
+
insert(entity: string, data: Row): Promise<string>;
|
|
125
|
+
/** Update a row (optimistic). */
|
|
126
|
+
update(entity: string, id: string, data: Partial<Row>): Promise<void>;
|
|
127
|
+
/** Delete a row (optimistic). */
|
|
128
|
+
delete(entity: string, id: string): Promise<void>;
|
|
129
|
+
/** Set presence data. */
|
|
130
|
+
setPresence(data: Record<string, unknown>): void;
|
|
131
|
+
/** Publish to a topic. */
|
|
132
|
+
publishTopic(topic: string, data: unknown): void;
|
|
133
|
+
/**
|
|
134
|
+
* Call a server-side function (query, mutation, or action).
|
|
135
|
+
*
|
|
136
|
+
* Routes through `SyncEngine.fn` (not the free `callFn`) so the response's
|
|
137
|
+
* `X-Pylon-Change-Seq` header triggers a fallback pull when the WS
|
|
138
|
+
* broadcast for the same event hasn't landed yet — closes the gap where
|
|
139
|
+
* a mutation succeeds but the cached query doesn't observe the new row.
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* const result = await db.fn("placeBid", { lotId: "x", amount: 150 });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
fn<T = unknown>(name: string, args?: Record<string, unknown>): Promise<T>;
|
|
146
|
+
/**
|
|
147
|
+
* Stream output from a server-side function as SSE chunks.
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* for await (const chunk of db.streamFn("chat", { message: "hi" })) {
|
|
151
|
+
* console.log(chunk);
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
streamFn(name: string, args?: Record<string, unknown>): AsyncGenerator<string, unknown, unknown>;
|
|
156
|
+
/** Upload a file to /api/files/upload. */
|
|
157
|
+
uploadFile(input: File | Blob | ArrayBuffer | Uint8Array, options?: {
|
|
158
|
+
filename?: string;
|
|
159
|
+
contentType?: string;
|
|
160
|
+
}): Promise<UploadedFile>;
|
|
161
|
+
/** Upload via multipart/form-data with extra fields. */
|
|
162
|
+
uploadFileMultipart(file: File | Blob, fields?: Record<string, string>): Promise<UploadedFile>;
|
|
163
|
+
};
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { SyncEngine, type Row } from "@pylonsync/sync";
|
|
2
|
+
/** Operator-based filter matching the server's query_filtered API. */
|
|
3
|
+
export type QueryFilter = Record<string, unknown> & {
|
|
4
|
+
$order?: Record<string, "asc" | "desc">;
|
|
5
|
+
$limit?: number;
|
|
6
|
+
};
|
|
7
|
+
/** Include syntax for nested relations: `{ author: {}, tags: {} }`. */
|
|
8
|
+
export type IncludeSpec = Record<string, Record<string, unknown>>;
|
|
9
|
+
export interface QueryOptions {
|
|
10
|
+
/** Filter by fields and operators (server-side). */
|
|
11
|
+
where?: QueryFilter;
|
|
12
|
+
/** Expand relations inline (server-side graph query). */
|
|
13
|
+
include?: IncludeSpec;
|
|
14
|
+
/** Limit number of rows. */
|
|
15
|
+
limit?: number;
|
|
16
|
+
/** Order by field(s). */
|
|
17
|
+
orderBy?: Record<string, "asc" | "desc">;
|
|
18
|
+
}
|
|
19
|
+
export interface UseQueryReturn<T> {
|
|
20
|
+
data: T[];
|
|
21
|
+
loading: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
/** Re-fetch from the server. Rarely needed — data is live. */
|
|
24
|
+
refetch: () => void;
|
|
25
|
+
}
|
|
26
|
+
export interface UseQueryOneReturn<T> {
|
|
27
|
+
data: T | null;
|
|
28
|
+
loading: boolean;
|
|
29
|
+
error: Error | null;
|
|
30
|
+
refetch: () => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Live query hook. Returns rows for an entity with loading/error state.
|
|
34
|
+
*
|
|
35
|
+
* Automatically re-renders when underlying data changes via the sync engine.
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const { data: todos, loading, error } = useQuery<Todo>(sync, "Todo");
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* With filters and ordering:
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const { data } = useQuery<Todo>(sync, "Todo", {
|
|
45
|
+
* where: { done: false, priority: { $gte: 3 } },
|
|
46
|
+
* orderBy: { createdAt: "desc" },
|
|
47
|
+
* limit: 20,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Filter/order/limit are applied client-side against the sync store;
|
|
52
|
+
* the sync engine pulls the full entity in the background.
|
|
53
|
+
*/
|
|
54
|
+
export declare function useQuery<T = Row>(sync: SyncEngine, entity: string, options?: QueryOptions): UseQueryReturn<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Live single-row query by ID. Returns the row or null, with loading/error state.
|
|
57
|
+
*
|
|
58
|
+
* ```tsx
|
|
59
|
+
* const { data: todo, loading } = useQueryOne<Todo>(sync, "Todo", todoId);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function useQueryOne<T = Row>(sync: SyncEngine, entity: string, id: string): UseQueryOneReturn<T>;
|
|
63
|
+
export interface UseReactiveQueryReturn<T> {
|
|
64
|
+
/** Latest server-pushed result. `null` until the initial run lands. */
|
|
65
|
+
data: T | null;
|
|
66
|
+
/** True until the first result lands (or the first error). */
|
|
67
|
+
loading: boolean;
|
|
68
|
+
/** Most recent error from the server-side handler, if any. */
|
|
69
|
+
error: Error | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to a server-side `query()` handler with automatic re-run
|
|
73
|
+
* on dependency changes. Mirrors Convex's reactive query model:
|
|
74
|
+
*
|
|
75
|
+
* 1. Mount: client sends `reactive-subscribe` over WS with `fn_name`
|
|
76
|
+
* + `args`. Server runs the handler under the connection's auth,
|
|
77
|
+
* records which entities the handler read via `ctx.db.*`, registers
|
|
78
|
+
* the subscription, and pushes the initial result.
|
|
79
|
+
* 2. On every server-side mutation, the runtime's reactive registry
|
|
80
|
+
* looks up subs whose dep set overlaps the changed entity, re-runs
|
|
81
|
+
* them, hashes the result, and pushes only when the hash changed.
|
|
82
|
+
* 3. Unmount: client sends `reactive-unsubscribe`; server tears down
|
|
83
|
+
* the registration and stops re-running.
|
|
84
|
+
*
|
|
85
|
+
* Auth context for re-runs is captured at subscribe time — the
|
|
86
|
+
* subscriber's identity, not the mutating user's. Policy gates the
|
|
87
|
+
* handler runs at first execution apply on every re-run.
|
|
88
|
+
*
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const { data, loading } = useReactiveQuery<MessageWithAuthor[]>(
|
|
91
|
+
* sync,
|
|
92
|
+
* "getMessagesWithAuthors",
|
|
93
|
+
* { channelId: "c_1" },
|
|
94
|
+
* );
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* Args object identity matters: changing the args reference triggers
|
|
98
|
+
* an unsubscribe + resubscribe with a fresh sub_id. Stabilize via
|
|
99
|
+
* `useMemo` if you build args inline on every render.
|
|
100
|
+
*/
|
|
101
|
+
export declare function useReactiveQuery<T = unknown>(sync: SyncEngine, fnName: string, args?: unknown): UseReactiveQueryReturn<T>;
|
|
102
|
+
export interface UseMutationReturn<TArgs, TResult> {
|
|
103
|
+
mutate: (args: TArgs) => Promise<TResult>;
|
|
104
|
+
mutateAsync: (args: TArgs) => Promise<TResult>;
|
|
105
|
+
loading: boolean;
|
|
106
|
+
data: TResult | null;
|
|
107
|
+
error: Error | null;
|
|
108
|
+
reset: () => void;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builder for the optimistic ghost row painted in the local store
|
|
112
|
+
* before the server function returns. Receives the args passed to
|
|
113
|
+
* `mutate()` plus a `ctx` object the framework fills in for you:
|
|
114
|
+
*
|
|
115
|
+
* - `ctx.id` — the freshly-minted Pylon-shaped row id (40-char hex)
|
|
116
|
+
* that the framework also threads into the mutation
|
|
117
|
+
* args as `_optimisticId`. Use this as the row's `id`
|
|
118
|
+
* so the optimistic ghost and the canonical broadcast
|
|
119
|
+
* share the same `row_id` and the WS update is an
|
|
120
|
+
* in-place merge instead of a delete-then-replace flash.
|
|
121
|
+
* - `ctx.now` — `new Date().toISOString()` evaluated once, so the
|
|
122
|
+
* optimistic ghost has a `createdAt` that's stable
|
|
123
|
+
* across the same gesture.
|
|
124
|
+
*
|
|
125
|
+
* Return either a single `{ entity, data }` for the common one-row
|
|
126
|
+
* case or an array for mutations that touch multiple entities (e.g.
|
|
127
|
+
* an "accept invite" that inserts a Membership AND an AuditLog row).
|
|
128
|
+
*/
|
|
129
|
+
export interface OptimisticContext {
|
|
130
|
+
id: string;
|
|
131
|
+
now: string;
|
|
132
|
+
}
|
|
133
|
+
export type OptimisticChange = {
|
|
134
|
+
entity: string;
|
|
135
|
+
data: Row;
|
|
136
|
+
};
|
|
137
|
+
export type OptimisticBuilder<TArgs> = (args: TArgs, ctx: OptimisticContext) => OptimisticChange | OptimisticChange[];
|
|
138
|
+
export interface UseMutationOptions<TArgs> {
|
|
139
|
+
token?: string;
|
|
140
|
+
/**
|
|
141
|
+
* Paint a row into the local store immediately, before the server
|
|
142
|
+
* function returns. The row uses `ctx.id` as its `id` and the
|
|
143
|
+
* framework threads that id through the mutation args as
|
|
144
|
+
* `_optimisticId` — your server function should accept it and pass
|
|
145
|
+
* it on to `ctx.db.insert("Entity", { id: args._optimisticId, ... })`
|
|
146
|
+
* (the runtime honors caller-supplied ids for any 40-char hex value).
|
|
147
|
+
*
|
|
148
|
+
* The WS broadcast that follows will carry the same `row_id`, so the
|
|
149
|
+
* canonical row lands as a field-level merge on top of the
|
|
150
|
+
* optimistic ghost — no flash, no temp-row swap, no manual cleanup.
|
|
151
|
+
*
|
|
152
|
+
* On rejection, the optimistic insert is rolled back without leaving
|
|
153
|
+
* a tombstone, so retrying the mutation works.
|
|
154
|
+
*/
|
|
155
|
+
optimistic?: OptimisticBuilder<TArgs>;
|
|
156
|
+
/**
|
|
157
|
+
* Active sync engine. Required when `optimistic` is set so the hook
|
|
158
|
+
* can paint the ghost into the right store; ignored otherwise. The
|
|
159
|
+
* `db.useMutation` wrapper supplies this automatically via `getSync`.
|
|
160
|
+
*/
|
|
161
|
+
sync?: SyncEngine;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Hook for calling a server-side mutation/action function.
|
|
165
|
+
*
|
|
166
|
+
* ```tsx
|
|
167
|
+
* const placeBid = useMutation<{lotId: string; amount: number}, {accepted: boolean}>(
|
|
168
|
+
* "placeBid"
|
|
169
|
+
* );
|
|
170
|
+
*
|
|
171
|
+
* const onClick = async () => {
|
|
172
|
+
* const result = await placeBid.mutate({ lotId: "lot_1", amount: 150 });
|
|
173
|
+
* if (result.accepted) alert("Bid placed!");
|
|
174
|
+
* };
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* For optimistic UI, pass an `optimistic` builder. See
|
|
178
|
+
* `OptimisticBuilder` above for the contract.
|
|
179
|
+
*/
|
|
180
|
+
export declare function useMutation<TArgs = Record<string, unknown>, TResult = unknown>(fnName: string, options?: UseMutationOptions<TArgs>): UseMutationReturn<TArgs, TResult>;
|
|
181
|
+
export interface UseInfiniteQueryReturn<T> {
|
|
182
|
+
data: T[];
|
|
183
|
+
loading: boolean;
|
|
184
|
+
hasMore: boolean;
|
|
185
|
+
loadMore: () => void;
|
|
186
|
+
error: Error | null;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Paginated query hook that accumulates pages as you `loadMore()`.
|
|
190
|
+
*
|
|
191
|
+
* ```tsx
|
|
192
|
+
* const { data, hasMore, loadMore, loading } = useInfiniteQuery<Todo>(
|
|
193
|
+
* sync, "Todo", { pageSize: 20 }
|
|
194
|
+
* );
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export declare function useInfiniteQuery<T = Row>(sync: SyncEngine, entity: string, options?: {
|
|
198
|
+
pageSize?: number;
|
|
199
|
+
}): UseInfiniteQueryReturn<T>;
|
|
200
|
+
export type PaginatedQueryStatus = "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted";
|
|
201
|
+
export interface UsePaginatedQueryReturn<T> {
|
|
202
|
+
/** Rows loaded so far, across all pages. */
|
|
203
|
+
results: T[];
|
|
204
|
+
/** State-machine value — render based on this rather than booleans. */
|
|
205
|
+
status: PaginatedQueryStatus;
|
|
206
|
+
/** Fetch the next page. Idempotent: no-op while loading or exhausted. */
|
|
207
|
+
loadMore: (numItems?: number) => void;
|
|
208
|
+
/** The most recent error, if any. Resets on the next successful load. */
|
|
209
|
+
error: Error | null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Cursor-paginated live query. Pairs with `ctx.db.paginate()` server-side
|
|
213
|
+
* and the `GET /api/entities/:entity/cursor` endpoint.
|
|
214
|
+
*
|
|
215
|
+
* ```tsx
|
|
216
|
+
* const { results, status, loadMore } = usePaginatedQuery<Order>(
|
|
217
|
+
* sync,
|
|
218
|
+
* "Order",
|
|
219
|
+
* { initialNumItems: 20 }
|
|
220
|
+
* );
|
|
221
|
+
*
|
|
222
|
+
* return (
|
|
223
|
+
* <>
|
|
224
|
+
* {results.map(o => <Row key={o.id} order={o} />)}
|
|
225
|
+
* {status === "CanLoadMore" && <button onClick={() => loadMore()}>More</button>}
|
|
226
|
+
* {status === "LoadingMore" && <Spinner />}
|
|
227
|
+
* {status === "Exhausted" && <footer>end</footer>}
|
|
228
|
+
* </>
|
|
229
|
+
* );
|
|
230
|
+
* ```
|
|
231
|
+
*
|
|
232
|
+
* Same engine as `useInfiniteQuery`; different surface. Prefer this one in
|
|
233
|
+
* new code — the `status` enum makes exhaustive rendering easier to get
|
|
234
|
+
* right than `hasMore/loading` booleans.
|
|
235
|
+
*/
|
|
236
|
+
export declare function usePaginatedQuery<T = Row>(sync: SyncEngine, entity: string, options?: {
|
|
237
|
+
initialNumItems?: number;
|
|
238
|
+
}): UsePaginatedQueryReturn<T>;
|
|
239
|
+
/**
|
|
240
|
+
* Low-level hook returning `{subscribe, getSnapshot, getServerSnapshot}` for
|
|
241
|
+
* `useSyncExternalStore`. Prefer [`useQuery`] above for most cases; use this
|
|
242
|
+
* when you need precise control over subscription timing.
|
|
243
|
+
*/
|
|
244
|
+
export declare function useQueryRaw(sync: SyncEngine, entity: string): {
|
|
245
|
+
subscribe: (callback: () => void) => () => void;
|
|
246
|
+
getSnapshot: () => Row[];
|
|
247
|
+
getServerSnapshot: () => Row[];
|
|
248
|
+
};
|
|
249
|
+
export declare function useQueryOneRaw(sync: SyncEngine, entity: string, id: string): {
|
|
250
|
+
subscribe: (callback: () => void) => () => void;
|
|
251
|
+
getSnapshot: () => Row | null;
|
|
252
|
+
getServerSnapshot: () => Row | null;
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Entity-level CRUD helpers backed by the sync engine (optimistic updates).
|
|
256
|
+
* Separate from [`useMutation`] which calls server-side TypeScript functions.
|
|
257
|
+
*/
|
|
258
|
+
export declare function useEntityMutation(sync: SyncEngine, entity: string): {
|
|
259
|
+
insert: (data: Row) => Promise<string>;
|
|
260
|
+
update: (id: string, data: Partial<Row>) => Promise<void>;
|
|
261
|
+
remove: (id: string) => Promise<void>;
|
|
262
|
+
};
|
|
263
|
+
export declare const useLiveList: typeof useQueryRaw;
|
|
264
|
+
export declare const useLiveRow: typeof useQueryOneRaw;
|
|
265
|
+
export declare function useInsert(sync: SyncEngine, entity: string): (data: Row) => Promise<string>;
|
|
266
|
+
export declare function useUpdate(sync: SyncEngine, entity: string): (id: string, data: Partial<Row>) => Promise<void>;
|
|
267
|
+
export declare function useDelete(sync: SyncEngine, entity: string): (id: string) => Promise<void>;
|
|
268
|
+
export declare function useAction(sync: SyncEngine, entity: string, actionFn: (data: Row) => Promise<void>): (data: Row) => Promise<void>;
|
|
269
|
+
export interface UseFnReturn<TResult> {
|
|
270
|
+
call: (args?: Record<string, unknown>) => Promise<TResult>;
|
|
271
|
+
loading: boolean;
|
|
272
|
+
data: TResult | null;
|
|
273
|
+
error: Error | null;
|
|
274
|
+
reset: () => void;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Call a server-side function with loading/error/data state.
|
|
278
|
+
* Prefer [`useMutation`] for new code — same functionality, better API.
|
|
279
|
+
*/
|
|
280
|
+
export declare function useFn<TResult = unknown>(name: string, options?: {
|
|
281
|
+
token?: string;
|
|
282
|
+
}): UseFnReturn<TResult>;
|
|
283
|
+
/**
|
|
284
|
+
* Aggregate spec — server matches this shape in
|
|
285
|
+
* `POST /api/aggregate/:entity`. The server auto-injects an `orgId`
|
|
286
|
+
* clamp into `where` when the caller has a tenant, so a malicious
|
|
287
|
+
* client can't sum across orgs.
|
|
288
|
+
*/
|
|
289
|
+
export interface AggregateSpec {
|
|
290
|
+
/** "*" for COUNT(*), a column name for COUNT(col). */
|
|
291
|
+
count?: string;
|
|
292
|
+
/** Columns to sum. */
|
|
293
|
+
sum?: string[];
|
|
294
|
+
/** Columns to average. */
|
|
295
|
+
avg?: string[];
|
|
296
|
+
/** Columns to take the minimum of. */
|
|
297
|
+
min?: string[];
|
|
298
|
+
/** Columns to take the maximum of. */
|
|
299
|
+
max?: string[];
|
|
300
|
+
/** Columns to COUNT DISTINCT. */
|
|
301
|
+
countDistinct?: string[];
|
|
302
|
+
/**
|
|
303
|
+
* Group keys. Each entry is either a column name, or a date-bucket
|
|
304
|
+
* spec `{ field, bucket }` where bucket ∈ hour/day/week/month/year.
|
|
305
|
+
*/
|
|
306
|
+
groupBy?: (string | {
|
|
307
|
+
field: string;
|
|
308
|
+
bucket: "hour" | "day" | "week" | "month" | "year";
|
|
309
|
+
})[];
|
|
310
|
+
/** Equality filter applied before aggregation. */
|
|
311
|
+
where?: Record<string, unknown>;
|
|
312
|
+
}
|
|
313
|
+
export interface UseAggregateReturn<Row = Record<string, unknown>> {
|
|
314
|
+
data: Row[] | null;
|
|
315
|
+
loading: boolean;
|
|
316
|
+
error: Error | null;
|
|
317
|
+
/** Re-run the query. Rarely needed — the hook refreshes on sync notify. */
|
|
318
|
+
refresh: () => void;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Run an aggregate query and keep it fresh as the sync store mutates.
|
|
322
|
+
*
|
|
323
|
+
* The hook re-fetches whenever the given entity changes in the local
|
|
324
|
+
* sync replica — so charts stay live without polling. Subscribes to
|
|
325
|
+
* the entity's sync events; any change triggers a debounced re-fetch.
|
|
326
|
+
*
|
|
327
|
+
* ```tsx
|
|
328
|
+
* const { data } = useAggregate(sync, "Order", {
|
|
329
|
+
* count: "*",
|
|
330
|
+
* groupBy: [{ field: "createdAt", bucket: "day" }],
|
|
331
|
+
* where: { status: "delivered" },
|
|
332
|
+
* });
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
export declare function useAggregate<Row = Record<string, unknown>>(sync: SyncEngine, entity: string, spec: AggregateSpec): UseAggregateReturn<Row>;
|
|
336
|
+
export interface SearchSpec {
|
|
337
|
+
/** Free-text match across the entity's declared `text` fields. */
|
|
338
|
+
query?: string;
|
|
339
|
+
/** Equality filters. Keys must be facet fields in the entity's schema. */
|
|
340
|
+
filters?: Record<string, string | number | boolean>;
|
|
341
|
+
/** Facet fields to return counts for. If omitted, all declared facets. */
|
|
342
|
+
facets?: string[];
|
|
343
|
+
/** Sort by `[field, "asc" | "desc"]`. Field must be in `sortable`. */
|
|
344
|
+
sort?: [string, "asc" | "desc"];
|
|
345
|
+
/** Zero-indexed page. Default 0. */
|
|
346
|
+
page?: number;
|
|
347
|
+
/** Results per page. Clamped server-side to 1..=100. Default 20. */
|
|
348
|
+
pageSize?: number;
|
|
349
|
+
}
|
|
350
|
+
export interface UseSearchReturn<T = Row> {
|
|
351
|
+
/** The current page of hits, already sorted. */
|
|
352
|
+
hits: T[];
|
|
353
|
+
/** `{facet: {value: count}}` for every declared (or requested) facet. */
|
|
354
|
+
facetCounts: Record<string, Record<string, number>>;
|
|
355
|
+
/** Total hit count across all pages. */
|
|
356
|
+
total: number;
|
|
357
|
+
/** Server-reported query latency in ms. */
|
|
358
|
+
tookMs: number;
|
|
359
|
+
loading: boolean;
|
|
360
|
+
error: Error | null;
|
|
361
|
+
refresh: () => Promise<void>;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Live faceted search hook. Wraps the `POST /api/search/:entity`
|
|
365
|
+
* endpoint, re-runs the query when the sync replica signals a write
|
|
366
|
+
* on the target entity, and returns ranked hits plus live facet
|
|
367
|
+
* counts in one call.
|
|
368
|
+
*
|
|
369
|
+
* ```tsx
|
|
370
|
+
* const { hits, facetCounts, total, loading } = useSearch<Product>(
|
|
371
|
+
* sync, "Product",
|
|
372
|
+
* {
|
|
373
|
+
* query: "red sneakers",
|
|
374
|
+
* filters: { category: "shoes" },
|
|
375
|
+
* facets: ["brand", "color"],
|
|
376
|
+
* sort: ["price", "desc"],
|
|
377
|
+
* page: 0, pageSize: 20,
|
|
378
|
+
* },
|
|
379
|
+
* );
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* Live-update model matches `useAggregate`: subscribes to the sync
|
|
383
|
+
* store and re-fetches on any change for this entity. Facet counts
|
|
384
|
+
* reflect server-computed bitmap intersections — adding/removing a
|
|
385
|
+
* Product row drops the freshly-recomputed counts back into the UI
|
|
386
|
+
* in under 100ms on typical catalogs.
|
|
387
|
+
*/
|
|
388
|
+
export declare function useSearch<T = Row>(sync: SyncEngine, entity: string, spec: SearchSpec): UseSearchReturn<T>;
|