@pylonsync/react 0.3.292 → 0.3.294

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 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;
@@ -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
+ };
@@ -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>;