@pylonsync/react 0.3.130 → 0.3.133

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.130",
6
+ "version": "0.3.133",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
@@ -12,8 +12,8 @@
12
12
  "check": "tsc -p tsconfig.json --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@pylonsync/sdk": "0.3.130",
16
- "@pylonsync/sync": "0.3.130"
15
+ "@pylonsync/sdk": "0.3.133",
16
+ "@pylonsync/sync": "0.3.133"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": ">=19.0.0"
package/src/db.ts CHANGED
@@ -4,6 +4,7 @@ import { SyncEngine, createSyncEngine, type Row, type SyncEngineConfig } from "@
4
4
  import {
5
5
  useQuery as useQueryHook,
6
6
  useQueryOne as useQueryOneHook,
7
+ useReactiveQuery as useReactiveQueryHook,
7
8
  useMutation as useMutationHook,
8
9
  useInfiniteQuery as useInfiniteQueryHook,
9
10
  useAggregate as useAggregateHook,
@@ -12,6 +13,7 @@ import {
12
13
  type QueryOptions,
13
14
  type UseQueryReturn,
14
15
  type UseQueryOneReturn,
16
+ type UseReactiveQueryReturn,
15
17
  type UseMutationReturn,
16
18
  type UseInfiniteQueryReturn,
17
19
  type AggregateSpec,
@@ -99,6 +101,31 @@ export const db = {
99
101
  return useQueryOneHook<T>(getSync(), entity, id);
100
102
  },
101
103
 
104
+ /**
105
+ * Reactive query — Convex-style auto-rerunning server handler.
106
+ *
107
+ * The server runs your `query()` handler with dependency tracking
108
+ * (every `ctx.db.*` read is recorded), registers the subscription,
109
+ * and pushes the initial result. Any future mutation touching the
110
+ * dep set triggers a re-run + push.
111
+ *
112
+ * ```tsx
113
+ * const { data: feed, loading } = db.useReactiveQuery<FeedItem[]>(
114
+ * "getFeed",
115
+ * { userId: currentUser.id },
116
+ * );
117
+ * ```
118
+ *
119
+ * Authoring side: define the handler with `query()` from
120
+ * `@pylonsync/functions`. Any handler is eligible — no opt-in flag.
121
+ */
122
+ useReactiveQuery<T = unknown>(
123
+ fnName: string,
124
+ args?: unknown,
125
+ ): UseReactiveQueryReturn<T> {
126
+ return useReactiveQueryHook<T>(getSync(), fnName, args);
127
+ },
128
+
102
129
  /**
103
130
  * Server-side function call with mutation state (loading, data, error).
104
131
  *
package/src/hooks.ts CHANGED
@@ -184,6 +184,88 @@ export function useQueryOne<T = Row>(
184
184
  return { data, loading: loading.current, error: error.current, refetch };
185
185
  }
186
186
 
187
+ // ---------------------------------------------------------------------------
188
+ // useReactiveQuery — Convex-style auto-rerunning server query
189
+ // ---------------------------------------------------------------------------
190
+
191
+ export interface UseReactiveQueryReturn<T> {
192
+ /** Latest server-pushed result. `null` until the initial run lands. */
193
+ data: T | null;
194
+ /** True until the first result lands (or the first error). */
195
+ loading: boolean;
196
+ /** Most recent error from the server-side handler, if any. */
197
+ error: Error | null;
198
+ }
199
+
200
+ /**
201
+ * Subscribe to a server-side `query()` handler with automatic re-run
202
+ * on dependency changes. Mirrors Convex's reactive query model:
203
+ *
204
+ * 1. Mount: client sends `reactive-subscribe` over WS with `fn_name`
205
+ * + `args`. Server runs the handler under the connection's auth,
206
+ * records which entities the handler read via `ctx.db.*`, registers
207
+ * the subscription, and pushes the initial result.
208
+ * 2. On every server-side mutation, the runtime's reactive registry
209
+ * looks up subs whose dep set overlaps the changed entity, re-runs
210
+ * them, hashes the result, and pushes only when the hash changed.
211
+ * 3. Unmount: client sends `reactive-unsubscribe`; server tears down
212
+ * the registration and stops re-running.
213
+ *
214
+ * Auth context for re-runs is captured at subscribe time — the
215
+ * subscriber's identity, not the mutating user's. Policy gates the
216
+ * handler runs at first execution apply on every re-run.
217
+ *
218
+ * ```tsx
219
+ * const { data, loading } = useReactiveQuery<MessageWithAuthor[]>(
220
+ * sync,
221
+ * "getMessagesWithAuthors",
222
+ * { channelId: "c_1" },
223
+ * );
224
+ * ```
225
+ *
226
+ * Args object identity matters: changing the args reference triggers
227
+ * an unsubscribe + resubscribe with a fresh sub_id. Stabilize via
228
+ * `useMemo` if you build args inline on every render.
229
+ */
230
+ export function useReactiveQuery<T = unknown>(
231
+ sync: SyncEngine,
232
+ fnName: string,
233
+ args?: unknown,
234
+ ): UseReactiveQueryReturn<T> {
235
+ const [data, setData] = useState<T | null>(null);
236
+ const [loading, setLoading] = useState<boolean>(true);
237
+ const [error, setError] = useState<Error | null>(null);
238
+ // Stable serialization of args — when the JSON shape changes we
239
+ // re-subscribe. Object identity changes alone don't re-subscribe.
240
+ const argsKey = useMemo(() => JSON.stringify(args ?? null), [args]);
241
+
242
+ useEffect(() => {
243
+ const sub_id = generateId();
244
+ setLoading(true);
245
+ setError(null);
246
+ sync.subscribeReactive(sub_id, fnName, args ?? null, (msg) => {
247
+ if (msg.kind === "result") {
248
+ setData(msg.result as T);
249
+ setLoading(false);
250
+ setError(null);
251
+ } else {
252
+ // Reactive error pushes (e.g. handler unavailable) — surface
253
+ // to the consumer and stop spinning.
254
+ setError(
255
+ Object.assign(new Error(msg.message || msg.code), { code: msg.code }),
256
+ );
257
+ setLoading(false);
258
+ }
259
+ });
260
+ return () => {
261
+ sync.unsubscribeReactive(sub_id);
262
+ };
263
+ // eslint-disable-next-line react-hooks/exhaustive-deps
264
+ }, [sync, fnName, argsKey]);
265
+
266
+ return { data, loading, error };
267
+ }
268
+
187
269
  // ---------------------------------------------------------------------------
188
270
  // Client-side filter application (matches the server's operator set)
189
271
  // ---------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { defaultStorage, type Storage as PylonStorage } from "@pylonsync/sync";
7
7
  export {
8
8
  useQuery,
9
9
  useQueryOne,
10
+ useReactiveQuery,
10
11
  useMutation,
11
12
  useInfiniteQuery,
12
13
  usePaginatedQuery,
@@ -29,6 +30,7 @@ export type {
29
30
  IncludeSpec,
30
31
  UseQueryReturn,
31
32
  UseQueryOneReturn,
33
+ UseReactiveQueryReturn,
32
34
  UseMutationReturn,
33
35
  UseInfiniteQueryReturn,
34
36
  UsePaginatedQueryReturn,