@sylphx/lens-react 2.1.7 → 2.3.0

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/README.md CHANGED
@@ -5,34 +5,72 @@ React hooks for the Lens API framework.
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bun add @sylphx/lens-react
8
+ bun add @sylphx/lens-react @sylphx/lens-client
9
9
  ```
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Setup Client
14
+
13
15
  ```typescript
14
- import { LensProvider, useQuery, useMutation } from "@sylphx/lens-react";
15
- import { client } from "./client";
16
+ // lib/client.ts
17
+ import { createClient } from "@sylphx/lens-react";
18
+ import { http } from "@sylphx/lens-client";
19
+ import type { AppRouter } from "@/server/router";
16
20
 
17
- function App() {
18
- return (
19
- <LensProvider client={client}>
20
- <UserProfile />
21
- </LensProvider>
22
- );
23
- }
21
+ export const client = createClient<AppRouter>({
22
+ transport: http({ url: "/api/lens" }),
23
+ });
24
+ ```
24
25
 
25
- function UserProfile() {
26
- const { data, loading, error } = useQuery(client.user.get({ id: "1" }));
27
- const [createUser, { loading: creating }] = useMutation(client.user.create);
26
+ ### Query (in component)
27
+
28
+ ```tsx
29
+ import { client } from "@/lib/client";
30
+
31
+ function UserProfile({ id }: { id: string }) {
32
+ const { data, loading, error, refetch } = client.user.get({
33
+ input: { id },
34
+ select: { name: true, email: true },
35
+ });
28
36
 
29
37
  if (loading) return <div>Loading...</div>;
30
38
  if (error) return <div>Error: {error.message}</div>;
31
39
 
32
- return <div>{data.name}</div>;
40
+ return <div>{data?.name}</div>;
33
41
  }
34
42
  ```
35
43
 
44
+ ### Mutation (in component)
45
+
46
+ ```tsx
47
+ import { client } from "@/lib/client";
48
+
49
+ function CreateUser() {
50
+ const { mutate, loading, error, data, reset } = client.user.create({
51
+ onSuccess: (data) => console.log("Created:", data),
52
+ onError: (error) => console.error("Failed:", error),
53
+ });
54
+
55
+ const handleSubmit = async () => {
56
+ await mutate({ input: { name: "New User" } });
57
+ };
58
+
59
+ return (
60
+ <button onClick={handleSubmit} disabled={loading}>
61
+ {loading ? "Creating..." : "Create User"}
62
+ </button>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ### SSR / Server-side
68
+
69
+ ```tsx
70
+ // Use .fetch() for promise-based calls
71
+ const user = await client.user.get.fetch({ input: { id } });
72
+ ```
73
+
36
74
  ## License
37
75
 
38
76
  MIT
@@ -41,4 +79,4 @@ MIT
41
79
 
42
80
  Built with [@sylphx/lens-client](https://github.com/SylphxAI/Lens).
43
81
 
44
- Powered by Sylphx
82
+ Powered by Sylphx
package/dist/index.d.ts CHANGED
@@ -1,3 +1,138 @@
1
+ import { LensClientConfig, SelectionObject, TypedClientConfig } from "@sylphx/lens-client";
2
+ import { MutationDef, QueryDef, RouterDef, RouterRoutes } from "@sylphx/lens-core";
3
+ /** Query hook options */
4
+ interface QueryHookOptions<TInput> {
5
+ /** Query input parameters */
6
+ input?: TInput;
7
+ /** Field selection */
8
+ select?: SelectionObject;
9
+ /** Skip query execution */
10
+ skip?: boolean;
11
+ }
12
+ /** Query hook result */
13
+ interface QueryHookResult<T> {
14
+ /** Query data (null if loading or error) */
15
+ data: T | null;
16
+ /** Loading state */
17
+ loading: boolean;
18
+ /** Error state */
19
+ error: Error | null;
20
+ /** Refetch the query */
21
+ refetch: () => void;
22
+ }
23
+ /** Mutation hook options */
24
+ interface MutationHookOptions<TOutput> {
25
+ /** Called on successful mutation */
26
+ onSuccess?: (data: TOutput) => void;
27
+ /** Called on mutation error */
28
+ onError?: (error: Error) => void;
29
+ /** Called when mutation settles (success or error) */
30
+ onSettled?: () => void;
31
+ }
32
+ /** Mutation hook result */
33
+ interface MutationHookResult<
34
+ TInput,
35
+ TOutput
36
+ > {
37
+ /** Execute the mutation */
38
+ mutate: (options: {
39
+ input: TInput;
40
+ select?: SelectionObject;
41
+ }) => Promise<TOutput>;
42
+ /** Mutation is in progress */
43
+ loading: boolean;
44
+ /** Mutation error */
45
+ error: Error | null;
46
+ /** Last mutation result */
47
+ data: TOutput | null;
48
+ /** Reset mutation state */
49
+ reset: () => void;
50
+ }
51
+ /** Query endpoint type */
52
+ interface QueryEndpoint<
53
+ TInput,
54
+ TOutput
55
+ > {
56
+ /** Hook call (in component) */
57
+ <_TSelect extends SelectionObject = Record<string, never>>(options: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>): QueryHookResult<TOutput>;
58
+ /** Promise call (SSR) */
59
+ fetch: <TSelect extends SelectionObject = Record<string, never>>(options: TInput extends void ? {
60
+ input?: void;
61
+ select?: TSelect;
62
+ } | void : {
63
+ input: TInput;
64
+ select?: TSelect;
65
+ }) => Promise<TOutput>;
66
+ }
67
+ /** Mutation endpoint type */
68
+ interface MutationEndpoint<
69
+ TInput,
70
+ TOutput
71
+ > {
72
+ /** Hook call (in component) */
73
+ (options?: MutationHookOptions<TOutput>): MutationHookResult<TInput, TOutput>;
74
+ /** Promise call (SSR) */
75
+ fetch: <TSelect extends SelectionObject = Record<string, never>>(options: {
76
+ input: TInput;
77
+ select?: TSelect;
78
+ }) => Promise<TOutput>;
79
+ }
80
+ /** Infer client type from router routes */
81
+ type InferTypedClient<TRoutes extends RouterRoutes> = { [K in keyof TRoutes] : TRoutes[K] extends RouterDef<infer TNestedRoutes> ? InferTypedClient<TNestedRoutes> : TRoutes[K] extends QueryDef<infer TInput, infer TOutput> ? QueryEndpoint<TInput, TOutput> : TRoutes[K] extends MutationDef<infer TInput, infer TOutput> ? MutationEndpoint<TInput, TOutput> : never };
82
+ /** Typed client from router */
83
+ type TypedClient<TRouter extends RouterDef> = TRouter extends RouterDef<infer TRoutes> ? InferTypedClient<TRoutes> : never;
84
+ /**
85
+ * Create a Lens client with React hooks.
86
+ *
87
+ * Each endpoint can be called:
88
+ * - Directly as a hook: `client.user.get({ input: { id } })`
89
+ * - Via .fetch() for promises: `await client.user.get.fetch({ input: { id } })`
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * // lib/client.ts
94
+ * import { createClient } from '@sylphx/lens-react';
95
+ * import { httpTransport } from '@sylphx/lens-client';
96
+ * import type { AppRouter } from '@/server/router';
97
+ *
98
+ * const client = createClient<AppRouter>({
99
+ * transport: httpTransport({ url: '/api/lens' }),
100
+ * });
101
+ *
102
+ * // Component usage
103
+ * function UserProfile({ id }: { id: string }) {
104
+ * // Query hook - auto-subscribes
105
+ * const { data, loading, error } = client.user.get({
106
+ * input: { id },
107
+ * select: { name: true },
108
+ * });
109
+ *
110
+ * // Mutation hook - returns mutate function
111
+ * const { mutate, loading: saving } = client.user.update({
112
+ * onSuccess: () => toast('Updated!'),
113
+ * });
114
+ *
115
+ * if (loading) return <Spinner />;
116
+ * return (
117
+ * <div>
118
+ * <h1>{data?.name}</h1>
119
+ * <button onClick={() => mutate({ input: { id, name: 'New' } })}>
120
+ * Update
121
+ * </button>
122
+ * </div>
123
+ * );
124
+ * }
125
+ *
126
+ * // SSR usage
127
+ * async function UserPage({ id }: { id: string }) {
128
+ * const user = await client.user.get.fetch({ input: { id } });
129
+ * return <div>{user.name}</div>;
130
+ * }
131
+ * ```
132
+ */
133
+ declare function createClient<TRouter extends RouterDef>(config: LensClientConfig | TypedClientConfig<{
134
+ router: TRouter;
135
+ }>): TypedClient<TRouter>;
1
136
  import { LensClient } from "@sylphx/lens-client";
2
137
  import { ReactElement, ReactNode } from "react";
3
138
  interface LensProviderProps {
@@ -278,4 +413,4 @@ declare function useLazyQuery<
278
413
  TResult,
279
414
  TSelected = TResult
280
415
  >(selector: QuerySelector<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseLazyQueryResult<TSelected>;
281
- export { useQuery, useMutation, useLensClient, useLazyQuery, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, RouteSelector, QuerySelector, MutationSelector, LensProviderProps, LensProvider };
416
+ export { useQuery, useMutation, useLensClient, useLazyQuery, createClient, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, TypedClient, RouteSelector, QuerySelector, QueryHookResult, QueryHookOptions, QueryEndpoint, MutationSelector, MutationHookResult, MutationHookOptions, MutationEndpoint, LensProviderProps, LensProvider };
package/dist/index.js CHANGED
@@ -1,3 +1,252 @@
1
+ // src/create.tsx
2
+ import {
3
+ createClient as createBaseClient
4
+ } from "@sylphx/lens-client";
5
+ import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
6
+ import * as React from "react";
7
+ function queryReducer(state, action) {
8
+ switch (action.type) {
9
+ case "RESET":
10
+ return { data: null, loading: false, error: null };
11
+ case "START":
12
+ return { ...state, loading: true, error: null };
13
+ case "SUCCESS":
14
+ return { data: action.data, loading: false, error: null };
15
+ case "ERROR":
16
+ return { ...state, loading: false, error: action.error };
17
+ case "LOADING_DONE":
18
+ return { ...state, loading: false };
19
+ default:
20
+ return state;
21
+ }
22
+ }
23
+ function createQueryHook(baseClient, path) {
24
+ let cachedHook = null;
25
+ const getEndpoint = (p) => {
26
+ const parts = p.split(".");
27
+ let current = baseClient;
28
+ for (const part of parts) {
29
+ current = current[part];
30
+ }
31
+ return current;
32
+ };
33
+ const useQueryHook = (options) => {
34
+ const _optionsKey = JSON.stringify(options ?? {});
35
+ const query = useMemo(() => {
36
+ if (options?.skip)
37
+ return null;
38
+ const endpoint2 = getEndpoint(path);
39
+ return endpoint2({ input: options?.input, select: options?.select });
40
+ }, [options?.input, options?.select, options?.skip, path]);
41
+ const initialState = {
42
+ data: null,
43
+ loading: query != null && !options?.skip,
44
+ error: null
45
+ };
46
+ const [state, dispatch] = useReducer(queryReducer, initialState);
47
+ const mountedRef = useRef(true);
48
+ const queryRef = useRef(query);
49
+ queryRef.current = query;
50
+ useEffect(() => {
51
+ mountedRef.current = true;
52
+ if (query == null) {
53
+ dispatch({ type: "RESET" });
54
+ return;
55
+ }
56
+ dispatch({ type: "START" });
57
+ let hasReceivedData = false;
58
+ const unsubscribe = query.subscribe((value) => {
59
+ if (mountedRef.current) {
60
+ hasReceivedData = true;
61
+ dispatch({ type: "SUCCESS", data: value });
62
+ }
63
+ });
64
+ query.then((value) => {
65
+ if (mountedRef.current) {
66
+ if (!hasReceivedData) {
67
+ dispatch({ type: "SUCCESS", data: value });
68
+ } else {
69
+ dispatch({ type: "LOADING_DONE" });
70
+ }
71
+ }
72
+ }, (err) => {
73
+ if (mountedRef.current) {
74
+ dispatch({
75
+ type: "ERROR",
76
+ error: err instanceof Error ? err : new Error(String(err))
77
+ });
78
+ }
79
+ });
80
+ return () => {
81
+ mountedRef.current = false;
82
+ unsubscribe();
83
+ };
84
+ }, [query]);
85
+ const refetch = useCallback(() => {
86
+ const currentQuery = queryRef.current;
87
+ if (currentQuery == null)
88
+ return;
89
+ dispatch({ type: "START" });
90
+ currentQuery.then((value) => {
91
+ if (mountedRef.current) {
92
+ dispatch({ type: "SUCCESS", data: value });
93
+ }
94
+ }, (err) => {
95
+ if (mountedRef.current) {
96
+ dispatch({
97
+ type: "ERROR",
98
+ error: err instanceof Error ? err : new Error(String(err))
99
+ });
100
+ }
101
+ });
102
+ }, []);
103
+ return { data: state.data, loading: state.loading, error: state.error, refetch };
104
+ };
105
+ const fetch = async (options) => {
106
+ const endpoint2 = getEndpoint(path);
107
+ const queryResult = endpoint2({ input: options?.input, select: options?.select });
108
+ return queryResult.then((data) => data);
109
+ };
110
+ const endpoint = useQueryHook;
111
+ endpoint.fetch = fetch;
112
+ cachedHook = endpoint;
113
+ return cachedHook;
114
+ }
115
+ function createMutationHook(baseClient, path) {
116
+ let cachedHook = null;
117
+ const getEndpoint = (p) => {
118
+ const parts = p.split(".");
119
+ let current = baseClient;
120
+ for (const part of parts) {
121
+ current = current[part];
122
+ }
123
+ return current;
124
+ };
125
+ const useMutationHook = (hookOptions) => {
126
+ const [loading, setLoading] = React.useState(false);
127
+ const [error, setError] = React.useState(null);
128
+ const [data, setData] = React.useState(null);
129
+ const mountedRef = useRef(true);
130
+ const hookOptionsRef = useRef(hookOptions);
131
+ hookOptionsRef.current = hookOptions;
132
+ useEffect(() => {
133
+ mountedRef.current = true;
134
+ return () => {
135
+ mountedRef.current = false;
136
+ };
137
+ }, []);
138
+ const mutate = useCallback(async (options) => {
139
+ setLoading(true);
140
+ setError(null);
141
+ try {
142
+ const endpoint2 = getEndpoint(path);
143
+ const result = await endpoint2({ input: options.input, select: options.select });
144
+ const mutationResult = result;
145
+ if (mountedRef.current) {
146
+ setData(mutationResult.data);
147
+ setLoading(false);
148
+ }
149
+ hookOptionsRef.current?.onSuccess?.(mutationResult.data);
150
+ hookOptionsRef.current?.onSettled?.();
151
+ return mutationResult.data;
152
+ } catch (err) {
153
+ const mutationError = err instanceof Error ? err : new Error(String(err));
154
+ if (mountedRef.current) {
155
+ setError(mutationError);
156
+ setLoading(false);
157
+ }
158
+ hookOptionsRef.current?.onError?.(mutationError);
159
+ hookOptionsRef.current?.onSettled?.();
160
+ throw mutationError;
161
+ }
162
+ }, [path]);
163
+ const reset = useCallback(() => {
164
+ setLoading(false);
165
+ setError(null);
166
+ setData(null);
167
+ }, []);
168
+ return { mutate, loading, error, data, reset };
169
+ };
170
+ const fetch = async (options) => {
171
+ const endpoint2 = getEndpoint(path);
172
+ const result = await endpoint2({ input: options.input, select: options.select });
173
+ const mutationResult = result;
174
+ return mutationResult.data;
175
+ };
176
+ const endpoint = useMutationHook;
177
+ endpoint.fetch = fetch;
178
+ cachedHook = endpoint;
179
+ return cachedHook;
180
+ }
181
+ var hookCache = new Map;
182
+ function createClient(config) {
183
+ const baseClient = createBaseClient(config);
184
+ const _endpointTypes = new Map;
185
+ function createProxy(path) {
186
+ const cacheKey = path;
187
+ if (hookCache.has(cacheKey)) {
188
+ return hookCache.get(cacheKey);
189
+ }
190
+ const handler = {
191
+ get(_target, prop) {
192
+ if (typeof prop === "symbol")
193
+ return;
194
+ const key = prop;
195
+ if (key === "fetch") {
196
+ return async (options) => {
197
+ const parts = path.split(".");
198
+ let current = baseClient;
199
+ for (const part of parts) {
200
+ current = current[part];
201
+ }
202
+ const endpointFn = current;
203
+ const queryResult = endpointFn(options);
204
+ const result = await queryResult;
205
+ if (result && typeof result === "object" && "data" in result && Object.keys(result).length === 1) {
206
+ return result.data;
207
+ }
208
+ return result;
209
+ };
210
+ }
211
+ if (key === "then")
212
+ return;
213
+ if (key.startsWith("_"))
214
+ return;
215
+ const newPath = path ? `${path}.${key}` : key;
216
+ return createProxy(newPath);
217
+ },
218
+ apply(_target, _thisArg, args) {
219
+ const options = args[0];
220
+ const isQueryOptions = options && (("input" in options) || ("select" in options) || ("skip" in options));
221
+ const isMutationOptions = !options || !isQueryOptions && (Object.keys(options).length === 0 || ("onSuccess" in options) || ("onError" in options) || ("onSettled" in options));
222
+ const cacheKeyQuery = `${path}:query`;
223
+ const cacheKeyMutation = `${path}:mutation`;
224
+ if (isQueryOptions) {
225
+ if (!hookCache.has(cacheKeyQuery)) {
226
+ hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
227
+ }
228
+ const hook2 = hookCache.get(cacheKeyQuery);
229
+ return hook2(options);
230
+ }
231
+ if (isMutationOptions) {
232
+ if (!hookCache.has(cacheKeyMutation)) {
233
+ hookCache.set(cacheKeyMutation, createMutationHook(baseClient, path));
234
+ }
235
+ const hook2 = hookCache.get(cacheKeyMutation);
236
+ return hook2(options);
237
+ }
238
+ if (!hookCache.has(cacheKeyQuery)) {
239
+ hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
240
+ }
241
+ const hook = hookCache.get(cacheKeyQuery);
242
+ return hook(options);
243
+ }
244
+ };
245
+ const proxy = new Proxy(() => {}, handler);
246
+ return proxy;
247
+ }
248
+ return createProxy("");
249
+ }
1
250
  // src/context.tsx
2
251
  import { createContext, useContext } from "react";
3
252
  import { jsx } from "react/jsx-runtime";
@@ -25,14 +274,14 @@ function useLensClient() {
25
274
  }
26
275
  // src/hooks.ts
27
276
  import {
28
- useCallback,
29
- useEffect,
30
- useMemo,
31
- useReducer,
32
- useRef,
33
- useState
277
+ useCallback as useCallback2,
278
+ useEffect as useEffect2,
279
+ useMemo as useMemo2,
280
+ useReducer as useReducer2,
281
+ useRef as useRef2,
282
+ useState as useState2
34
283
  } from "react";
35
- function queryReducer(state, action) {
284
+ function queryReducer2(state, action) {
36
285
  switch (action.type) {
37
286
  case "RESET":
38
287
  return { data: null, loading: false, error: null };
@@ -52,9 +301,9 @@ function useQuery(selector, paramsOrDeps, options) {
52
301
  const client = useLensClient();
53
302
  const isAccessorMode = Array.isArray(paramsOrDeps);
54
303
  const paramsKey = !isAccessorMode ? JSON.stringify(paramsOrDeps) : null;
55
- const selectorRef = useRef(selector);
304
+ const selectorRef = useRef2(selector);
56
305
  selectorRef.current = selector;
57
- const query = useMemo(() => {
306
+ const query = useMemo2(() => {
58
307
  if (options?.skip)
59
308
  return null;
60
309
  if (isAccessorMode) {
@@ -67,21 +316,21 @@ function useQuery(selector, paramsOrDeps, options) {
67
316
  return null;
68
317
  return route(paramsOrDeps);
69
318
  }, isAccessorMode ? [client, options?.skip, ...paramsOrDeps] : [client, paramsKey, options?.skip]);
70
- const selectRef = useRef(options?.select);
319
+ const selectRef = useRef2(options?.select);
71
320
  selectRef.current = options?.select;
72
321
  const initialState = {
73
322
  data: null,
74
323
  loading: query != null && !options?.skip,
75
324
  error: null
76
325
  };
77
- const [state, dispatch] = useReducer(queryReducer, initialState);
78
- const mountedRef = useRef(true);
79
- const queryRef = useRef(query);
326
+ const [state, dispatch] = useReducer2(queryReducer2, initialState);
327
+ const mountedRef = useRef2(true);
328
+ const queryRef = useRef2(query);
80
329
  queryRef.current = query;
81
- const transform = useCallback((value) => {
330
+ const transform = useCallback2((value) => {
82
331
  return selectRef.current ? selectRef.current(value) : value;
83
332
  }, []);
84
- useEffect(() => {
333
+ useEffect2(() => {
85
334
  mountedRef.current = true;
86
335
  if (query == null) {
87
336
  dispatch({ type: "RESET" });
@@ -116,7 +365,7 @@ function useQuery(selector, paramsOrDeps, options) {
116
365
  unsubscribe();
117
366
  };
118
367
  }, [query, transform]);
119
- const refetch = useCallback(() => {
368
+ const refetch = useCallback2(() => {
120
369
  const currentQuery = queryRef.current;
121
370
  if (currentQuery == null)
122
371
  return;
@@ -139,19 +388,19 @@ function useQuery(selector, paramsOrDeps, options) {
139
388
  function useMutation(selector) {
140
389
  const client = useLensClient();
141
390
  const mutationFn = selector(client);
142
- const [loading, setLoading] = useState(false);
143
- const [error, setError] = useState(null);
144
- const [data, setData] = useState(null);
145
- const mountedRef = useRef(true);
146
- const mutationRef = useRef(mutationFn);
391
+ const [loading, setLoading] = useState2(false);
392
+ const [error, setError] = useState2(null);
393
+ const [data, setData] = useState2(null);
394
+ const mountedRef = useRef2(true);
395
+ const mutationRef = useRef2(mutationFn);
147
396
  mutationRef.current = mutationFn;
148
- useEffect(() => {
397
+ useEffect2(() => {
149
398
  mountedRef.current = true;
150
399
  return () => {
151
400
  mountedRef.current = false;
152
401
  };
153
402
  }, []);
154
- const mutate = useCallback(async (input) => {
403
+ const mutate = useCallback2(async (input) => {
155
404
  setLoading(true);
156
405
  setError(null);
157
406
  try {
@@ -172,7 +421,7 @@ function useMutation(selector) {
172
421
  }
173
422
  }
174
423
  }, []);
175
- const reset = useCallback(() => {
424
+ const reset = useCallback2(() => {
176
425
  setLoading(false);
177
426
  setError(null);
178
427
  setData(null);
@@ -181,24 +430,24 @@ function useMutation(selector) {
181
430
  }
182
431
  function useLazyQuery(selector, paramsOrDeps, options) {
183
432
  const client = useLensClient();
184
- const [data, setData] = useState(null);
185
- const [loading, setLoading] = useState(false);
186
- const [error, setError] = useState(null);
187
- const mountedRef = useRef(true);
433
+ const [data, setData] = useState2(null);
434
+ const [loading, setLoading] = useState2(false);
435
+ const [error, setError] = useState2(null);
436
+ const mountedRef = useRef2(true);
188
437
  const isAccessorMode = Array.isArray(paramsOrDeps);
189
- const selectorRef = useRef(selector);
438
+ const selectorRef = useRef2(selector);
190
439
  selectorRef.current = selector;
191
- const paramsOrDepsRef = useRef(paramsOrDeps);
440
+ const paramsOrDepsRef = useRef2(paramsOrDeps);
192
441
  paramsOrDepsRef.current = paramsOrDeps;
193
- const selectRef = useRef(options?.select);
442
+ const selectRef = useRef2(options?.select);
194
443
  selectRef.current = options?.select;
195
- useEffect(() => {
444
+ useEffect2(() => {
196
445
  mountedRef.current = true;
197
446
  return () => {
198
447
  mountedRef.current = false;
199
448
  };
200
449
  }, []);
201
- const execute = useCallback(async () => {
450
+ const execute = useCallback2(async () => {
202
451
  let query;
203
452
  if (isAccessorMode) {
204
453
  const querySelector = selectorRef.current;
@@ -236,7 +485,7 @@ function useLazyQuery(selector, paramsOrDeps, options) {
236
485
  }
237
486
  }
238
487
  }, [client, isAccessorMode]);
239
- const reset = useCallback(() => {
488
+ const reset = useCallback2(() => {
240
489
  setLoading(false);
241
490
  setError(null);
242
491
  setData(null);
@@ -248,5 +497,6 @@ export {
248
497
  useMutation,
249
498
  useLensClient,
250
499
  useLazyQuery,
500
+ createClient,
251
501
  LensProvider
252
502
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-react",
3
- "version": "2.1.7",
3
+ "version": "2.3.0",
4
4
  "description": "React bindings for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,7 +30,8 @@
30
30
  "author": "SylphxAI",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@sylphx/lens-client": "^2.0.5"
33
+ "@sylphx/lens-client": "^2.2.0",
34
+ "@sylphx/lens-core": "^2.1.0"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "react": ">=18.0.0"
package/src/context.tsx CHANGED
@@ -27,7 +27,10 @@ const LENS_CONTEXT_KEY = Symbol.for("@sylphx/lens-react/context");
27
27
  * (common in monorepos), all instances share the same React context.
28
28
  */
29
29
  function getOrCreateContext(): React.Context<LensClient<any, any> | null> {
30
- const globalObj = globalThis as unknown as Record<symbol, React.Context<LensClient<any, any> | null>>;
30
+ const globalObj = globalThis as unknown as Record<
31
+ symbol,
32
+ React.Context<LensClient<any, any> | null>
33
+ >;
31
34
 
32
35
  if (!globalObj[LENS_CONTEXT_KEY]) {
33
36
  globalObj[LENS_CONTEXT_KEY] = createContext<LensClient<any, any> | null>(null);
package/src/create.tsx ADDED
@@ -0,0 +1,573 @@
1
+ /**
2
+ * @sylphx/lens-react - Create Client
3
+ *
4
+ * Creates a typed Lens client with React hooks.
5
+ * Each endpoint can be called directly as a hook or via .fetch() for promises.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // lib/client.ts
10
+ * import { createClient } from '@sylphx/lens-react';
11
+ * import { httpTransport } from '@sylphx/lens-client';
12
+ * import type { AppRouter } from '@/server/router';
13
+ *
14
+ * export const client = createClient<AppRouter>({
15
+ * transport: httpTransport({ url: '/api/lens' }),
16
+ * });
17
+ *
18
+ * // In component
19
+ * function UserProfile({ id }: { id: string }) {
20
+ * const { data, loading } = client.user.get({ input: { id } });
21
+ * return <div>{data?.name}</div>;
22
+ * }
23
+ *
24
+ * // In SSR
25
+ * const user = await client.user.get.fetch({ input: { id } });
26
+ * ```
27
+ */
28
+
29
+ import {
30
+ createClient as createBaseClient,
31
+ type LensClientConfig,
32
+ type QueryResult,
33
+ type SelectionObject,
34
+ type TypedClientConfig,
35
+ } from "@sylphx/lens-client";
36
+ import type { MutationDef, QueryDef, RouterDef, RouterRoutes } from "@sylphx/lens-core";
37
+ import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
38
+
39
+ // =============================================================================
40
+ // Types
41
+ // =============================================================================
42
+
43
+ /** Query hook options */
44
+ export interface QueryHookOptions<TInput> {
45
+ /** Query input parameters */
46
+ input?: TInput;
47
+ /** Field selection */
48
+ select?: SelectionObject;
49
+ /** Skip query execution */
50
+ skip?: boolean;
51
+ }
52
+
53
+ /** Query hook result */
54
+ export interface QueryHookResult<T> {
55
+ /** Query data (null if loading or error) */
56
+ data: T | null;
57
+ /** Loading state */
58
+ loading: boolean;
59
+ /** Error state */
60
+ error: Error | null;
61
+ /** Refetch the query */
62
+ refetch: () => void;
63
+ }
64
+
65
+ /** Mutation hook options */
66
+ export interface MutationHookOptions<TOutput> {
67
+ /** Called on successful mutation */
68
+ onSuccess?: (data: TOutput) => void;
69
+ /** Called on mutation error */
70
+ onError?: (error: Error) => void;
71
+ /** Called when mutation settles (success or error) */
72
+ onSettled?: () => void;
73
+ }
74
+
75
+ /** Mutation hook result */
76
+ export interface MutationHookResult<TInput, TOutput> {
77
+ /** Execute the mutation */
78
+ mutate: (options: { input: TInput; select?: SelectionObject }) => Promise<TOutput>;
79
+ /** Mutation is in progress */
80
+ loading: boolean;
81
+ /** Mutation error */
82
+ error: Error | null;
83
+ /** Last mutation result */
84
+ data: TOutput | null;
85
+ /** Reset mutation state */
86
+ reset: () => void;
87
+ }
88
+
89
+ /** Query endpoint type */
90
+ export interface QueryEndpoint<TInput, TOutput> {
91
+ /** Hook call (in component) */
92
+ <_TSelect extends SelectionObject = Record<string, never>>(
93
+ options: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>,
94
+ ): QueryHookResult<TOutput>;
95
+
96
+ /** Promise call (SSR) */
97
+ fetch: <TSelect extends SelectionObject = Record<string, never>>(
98
+ options: TInput extends void
99
+ ? { input?: void; select?: TSelect } | void
100
+ : { input: TInput; select?: TSelect },
101
+ ) => Promise<TOutput>;
102
+ }
103
+
104
+ /** Mutation endpoint type */
105
+ export interface MutationEndpoint<TInput, TOutput> {
106
+ /** Hook call (in component) */
107
+ (options?: MutationHookOptions<TOutput>): MutationHookResult<TInput, TOutput>;
108
+
109
+ /** Promise call (SSR) */
110
+ fetch: <TSelect extends SelectionObject = Record<string, never>>(options: {
111
+ input: TInput;
112
+ select?: TSelect;
113
+ }) => Promise<TOutput>;
114
+ }
115
+
116
+ /** Infer client type from router routes */
117
+ type InferTypedClient<TRoutes extends RouterRoutes> = {
118
+ [K in keyof TRoutes]: TRoutes[K] extends RouterDef<infer TNestedRoutes>
119
+ ? InferTypedClient<TNestedRoutes>
120
+ : TRoutes[K] extends QueryDef<infer TInput, infer TOutput>
121
+ ? QueryEndpoint<TInput, TOutput>
122
+ : TRoutes[K] extends MutationDef<infer TInput, infer TOutput>
123
+ ? MutationEndpoint<TInput, TOutput>
124
+ : never;
125
+ };
126
+
127
+ /** Typed client from router */
128
+ export type TypedClient<TRouter extends RouterDef> =
129
+ TRouter extends RouterDef<infer TRoutes> ? InferTypedClient<TRoutes> : never;
130
+
131
+ // =============================================================================
132
+ // Query State Reducer
133
+ // =============================================================================
134
+
135
+ interface QueryState<T> {
136
+ data: T | null;
137
+ loading: boolean;
138
+ error: Error | null;
139
+ }
140
+
141
+ type QueryAction<T> =
142
+ | { type: "RESET" }
143
+ | { type: "START" }
144
+ | { type: "SUCCESS"; data: T }
145
+ | { type: "ERROR"; error: Error }
146
+ | { type: "LOADING_DONE" };
147
+
148
+ function queryReducer<T>(state: QueryState<T>, action: QueryAction<T>): QueryState<T> {
149
+ switch (action.type) {
150
+ case "RESET":
151
+ return { data: null, loading: false, error: null };
152
+ case "START":
153
+ return { ...state, loading: true, error: null };
154
+ case "SUCCESS":
155
+ return { data: action.data, loading: false, error: null };
156
+ case "ERROR":
157
+ return { ...state, loading: false, error: action.error };
158
+ case "LOADING_DONE":
159
+ return { ...state, loading: false };
160
+ default:
161
+ return state;
162
+ }
163
+ }
164
+
165
+ // =============================================================================
166
+ // Hook Factories
167
+ // =============================================================================
168
+
169
+ /**
170
+ * Create a query hook for a specific endpoint
171
+ */
172
+ function createQueryHook<TInput, TOutput>(
173
+ baseClient: unknown,
174
+ path: string,
175
+ ): QueryEndpoint<TInput, TOutput> {
176
+ // Cache for stable hook reference
177
+ let cachedHook: QueryEndpoint<TInput, TOutput> | null = null;
178
+
179
+ const getEndpoint = (p: string) => {
180
+ const parts = p.split(".");
181
+ let current: unknown = baseClient;
182
+ for (const part of parts) {
183
+ current = (current as Record<string, unknown>)[part];
184
+ }
185
+ return current as (options: unknown) => QueryResult<TOutput>;
186
+ };
187
+
188
+ const useQueryHook = (options?: QueryHookOptions<TInput>): QueryHookResult<TOutput> => {
189
+ const _optionsKey = JSON.stringify(options ?? {});
190
+
191
+ // Get query result from base client
192
+ const query = useMemo(() => {
193
+ if (options?.skip) return null;
194
+ const endpoint = getEndpoint(path);
195
+ return endpoint({ input: options?.input, select: options?.select });
196
+ }, [options?.input, options?.select, options?.skip, path]);
197
+
198
+ // State management
199
+ const initialState: QueryState<TOutput> = {
200
+ data: null,
201
+ loading: query != null && !options?.skip,
202
+ error: null,
203
+ };
204
+ const [state, dispatch] = useReducer(queryReducer<TOutput>, initialState);
205
+
206
+ // Track mounted state
207
+ const mountedRef = useRef(true);
208
+ const queryRef = useRef(query);
209
+ queryRef.current = query;
210
+
211
+ // Subscribe to query
212
+ useEffect(() => {
213
+ mountedRef.current = true;
214
+
215
+ if (query == null) {
216
+ dispatch({ type: "RESET" });
217
+ return;
218
+ }
219
+
220
+ dispatch({ type: "START" });
221
+
222
+ let hasReceivedData = false;
223
+
224
+ const unsubscribe = query.subscribe((value) => {
225
+ if (mountedRef.current) {
226
+ hasReceivedData = true;
227
+ dispatch({ type: "SUCCESS", data: value });
228
+ }
229
+ });
230
+
231
+ query.then(
232
+ (value) => {
233
+ if (mountedRef.current) {
234
+ if (!hasReceivedData) {
235
+ dispatch({ type: "SUCCESS", data: value });
236
+ } else {
237
+ dispatch({ type: "LOADING_DONE" });
238
+ }
239
+ }
240
+ },
241
+ (err) => {
242
+ if (mountedRef.current) {
243
+ dispatch({
244
+ type: "ERROR",
245
+ error: err instanceof Error ? err : new Error(String(err)),
246
+ });
247
+ }
248
+ },
249
+ );
250
+
251
+ return () => {
252
+ mountedRef.current = false;
253
+ unsubscribe();
254
+ };
255
+ }, [query]);
256
+
257
+ // Refetch function
258
+ const refetch = useCallback(() => {
259
+ const currentQuery = queryRef.current;
260
+ if (currentQuery == null) return;
261
+
262
+ dispatch({ type: "START" });
263
+
264
+ currentQuery.then(
265
+ (value) => {
266
+ if (mountedRef.current) {
267
+ dispatch({ type: "SUCCESS", data: value });
268
+ }
269
+ },
270
+ (err) => {
271
+ if (mountedRef.current) {
272
+ dispatch({
273
+ type: "ERROR",
274
+ error: err instanceof Error ? err : new Error(String(err)),
275
+ });
276
+ }
277
+ },
278
+ );
279
+ }, []);
280
+
281
+ return { data: state.data, loading: state.loading, error: state.error, refetch };
282
+ };
283
+
284
+ // Fetch method for promises (SSR)
285
+ const fetch = async (options?: {
286
+ input?: TInput;
287
+ select?: SelectionObject;
288
+ }): Promise<TOutput> => {
289
+ const endpoint = getEndpoint(path);
290
+ const queryResult = endpoint({ input: options?.input, select: options?.select });
291
+ return queryResult.then((data) => data);
292
+ };
293
+
294
+ // Create the endpoint object with hook + fetch
295
+ const endpoint = useQueryHook as unknown as QueryEndpoint<TInput, TOutput>;
296
+ endpoint.fetch = fetch as QueryEndpoint<TInput, TOutput>["fetch"];
297
+
298
+ cachedHook = endpoint;
299
+ return cachedHook;
300
+ }
301
+
302
+ /**
303
+ * Create a mutation hook for a specific endpoint
304
+ */
305
+ function createMutationHook<TInput, TOutput>(
306
+ baseClient: unknown,
307
+ path: string,
308
+ ): MutationEndpoint<TInput, TOutput> {
309
+ let cachedHook: MutationEndpoint<TInput, TOutput> | null = null;
310
+
311
+ const getEndpoint = (p: string) => {
312
+ const parts = p.split(".");
313
+ let current: unknown = baseClient;
314
+ for (const part of parts) {
315
+ current = (current as Record<string, unknown>)[part];
316
+ }
317
+ return current as (options: unknown) => QueryResult<{ data: TOutput }>;
318
+ };
319
+
320
+ const useMutationHook = (
321
+ hookOptions?: MutationHookOptions<TOutput>,
322
+ ): MutationHookResult<TInput, TOutput> => {
323
+ const [loading, setLoading] = React.useState(false);
324
+ const [error, setError] = React.useState<Error | null>(null);
325
+ const [data, setData] = React.useState<TOutput | null>(null);
326
+
327
+ const mountedRef = useRef(true);
328
+ const hookOptionsRef = useRef(hookOptions);
329
+ hookOptionsRef.current = hookOptions;
330
+
331
+ useEffect(() => {
332
+ mountedRef.current = true;
333
+ return () => {
334
+ mountedRef.current = false;
335
+ };
336
+ }, []);
337
+
338
+ const mutate = useCallback(
339
+ async (options: { input: TInput; select?: SelectionObject }): Promise<TOutput> => {
340
+ setLoading(true);
341
+ setError(null);
342
+
343
+ try {
344
+ const endpoint = getEndpoint(path);
345
+ const result = await endpoint({ input: options.input, select: options.select });
346
+ const mutationResult = result as unknown as { data: TOutput };
347
+
348
+ if (mountedRef.current) {
349
+ setData(mutationResult.data);
350
+ setLoading(false);
351
+ }
352
+
353
+ hookOptionsRef.current?.onSuccess?.(mutationResult.data);
354
+ hookOptionsRef.current?.onSettled?.();
355
+
356
+ return mutationResult.data;
357
+ } catch (err) {
358
+ const mutationError = err instanceof Error ? err : new Error(String(err));
359
+
360
+ if (mountedRef.current) {
361
+ setError(mutationError);
362
+ setLoading(false);
363
+ }
364
+
365
+ hookOptionsRef.current?.onError?.(mutationError);
366
+ hookOptionsRef.current?.onSettled?.();
367
+
368
+ throw mutationError;
369
+ }
370
+ },
371
+ [path],
372
+ );
373
+
374
+ const reset = useCallback(() => {
375
+ setLoading(false);
376
+ setError(null);
377
+ setData(null);
378
+ }, []);
379
+
380
+ return { mutate, loading, error, data, reset };
381
+ };
382
+
383
+ // Fetch method for promises (SSR)
384
+ const fetch = async (options: { input: TInput; select?: SelectionObject }): Promise<TOutput> => {
385
+ const endpoint = getEndpoint(path);
386
+ const result = await endpoint({ input: options.input, select: options.select });
387
+ const mutationResult = result as unknown as { data: TOutput };
388
+ return mutationResult.data;
389
+ };
390
+
391
+ const endpoint = useMutationHook as MutationEndpoint<TInput, TOutput>;
392
+ endpoint.fetch = fetch;
393
+
394
+ cachedHook = endpoint;
395
+ return cachedHook;
396
+ }
397
+
398
+ // =============================================================================
399
+ // React import for useState (needed in mutation hook)
400
+ // =============================================================================
401
+
402
+ import * as React from "react";
403
+
404
+ // =============================================================================
405
+ // Create Client
406
+ // =============================================================================
407
+
408
+ // Cache for hook functions to ensure stable references
409
+ const hookCache = new Map<string, unknown>();
410
+
411
+ /**
412
+ * Create a Lens client with React hooks.
413
+ *
414
+ * Each endpoint can be called:
415
+ * - Directly as a hook: `client.user.get({ input: { id } })`
416
+ * - Via .fetch() for promises: `await client.user.get.fetch({ input: { id } })`
417
+ *
418
+ * @example
419
+ * ```tsx
420
+ * // lib/client.ts
421
+ * import { createClient } from '@sylphx/lens-react';
422
+ * import { httpTransport } from '@sylphx/lens-client';
423
+ * import type { AppRouter } from '@/server/router';
424
+ *
425
+ * export const client = createClient<AppRouter>({
426
+ * transport: httpTransport({ url: '/api/lens' }),
427
+ * });
428
+ *
429
+ * // Component usage
430
+ * function UserProfile({ id }: { id: string }) {
431
+ * // Query hook - auto-subscribes
432
+ * const { data, loading, error } = client.user.get({
433
+ * input: { id },
434
+ * select: { name: true },
435
+ * });
436
+ *
437
+ * // Mutation hook - returns mutate function
438
+ * const { mutate, loading: saving } = client.user.update({
439
+ * onSuccess: () => toast('Updated!'),
440
+ * });
441
+ *
442
+ * if (loading) return <Spinner />;
443
+ * return (
444
+ * <div>
445
+ * <h1>{data?.name}</h1>
446
+ * <button onClick={() => mutate({ input: { id, name: 'New' } })}>
447
+ * Update
448
+ * </button>
449
+ * </div>
450
+ * );
451
+ * }
452
+ *
453
+ * // SSR usage
454
+ * async function UserPage({ id }: { id: string }) {
455
+ * const user = await client.user.get.fetch({ input: { id } });
456
+ * return <div>{user.name}</div>;
457
+ * }
458
+ * ```
459
+ */
460
+ export function createClient<TRouter extends RouterDef>(
461
+ config: LensClientConfig | TypedClientConfig<{ router: TRouter }>,
462
+ ): TypedClient<TRouter> {
463
+ // Create base client for transport
464
+ const baseClient = createBaseClient(config as LensClientConfig);
465
+
466
+ // Track endpoint types (query vs mutation) - determined at runtime via metadata
467
+ // For now, we'll detect based on the operation result
468
+ const _endpointTypes = new Map<string, "query" | "mutation">();
469
+
470
+ function createProxy(path: string): unknown {
471
+ const cacheKey = path;
472
+
473
+ // Return cached hook if available
474
+ if (hookCache.has(cacheKey)) {
475
+ return hookCache.get(cacheKey);
476
+ }
477
+
478
+ const handler: ProxyHandler<(...args: unknown[]) => unknown> = {
479
+ get(_target, prop) {
480
+ if (typeof prop === "symbol") return undefined;
481
+ const key = prop as string;
482
+
483
+ // Handle .fetch() method - returns a promise
484
+ if (key === "fetch") {
485
+ return async (options: unknown) => {
486
+ // Navigate to the endpoint in base client
487
+ const parts = path.split(".");
488
+ let current: unknown = baseClient;
489
+ for (const part of parts) {
490
+ current = (current as Record<string, unknown>)[part];
491
+ }
492
+ const endpointFn = current as (opts: unknown) => QueryResult<unknown>;
493
+ const queryResult = endpointFn(options);
494
+
495
+ // Await the result
496
+ const result = await queryResult;
497
+
498
+ // For mutations, the result is { data: ... }
499
+ // For queries, the result is the data directly
500
+ if (
501
+ result &&
502
+ typeof result === "object" &&
503
+ "data" in result &&
504
+ Object.keys(result).length === 1
505
+ ) {
506
+ return (result as { data: unknown }).data;
507
+ }
508
+ return result;
509
+ };
510
+ }
511
+
512
+ if (key === "then") return undefined;
513
+ if (key.startsWith("_")) return undefined;
514
+
515
+ const newPath = path ? `${path}.${key}` : key;
516
+ return createProxy(newPath);
517
+ },
518
+
519
+ apply(_target, _thisArg, args) {
520
+ // This is called when the endpoint is invoked as a function
521
+ // Detect query vs mutation based on options shape:
522
+ // - Query: has `input` or `select` or `skip` (QueryHookOptions)
523
+ // - Mutation: has `onSuccess`, `onError`, `onSettled` or no options (MutationHookOptions)
524
+
525
+ const options = args[0] as Record<string, unknown> | undefined;
526
+
527
+ // Detect based on option keys
528
+ const isQueryOptions =
529
+ options && ("input" in options || "select" in options || "skip" in options);
530
+
531
+ const isMutationOptions =
532
+ !options ||
533
+ (!isQueryOptions &&
534
+ (Object.keys(options).length === 0 ||
535
+ "onSuccess" in options ||
536
+ "onError" in options ||
537
+ "onSettled" in options));
538
+
539
+ // Check cache - but we need to know the type first
540
+ const cacheKeyQuery = `${path}:query`;
541
+ const cacheKeyMutation = `${path}:mutation`;
542
+
543
+ if (isQueryOptions) {
544
+ if (!hookCache.has(cacheKeyQuery)) {
545
+ hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
546
+ }
547
+ const hook = hookCache.get(cacheKeyQuery) as (opts: unknown) => unknown;
548
+ return hook(options);
549
+ }
550
+
551
+ if (isMutationOptions) {
552
+ if (!hookCache.has(cacheKeyMutation)) {
553
+ hookCache.set(cacheKeyMutation, createMutationHook(baseClient, path));
554
+ }
555
+ const hook = hookCache.get(cacheKeyMutation) as (opts: unknown) => unknown;
556
+ return hook(options);
557
+ }
558
+
559
+ // Fallback to query
560
+ if (!hookCache.has(cacheKeyQuery)) {
561
+ hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
562
+ }
563
+ const hook = hookCache.get(cacheKeyQuery) as (opts: unknown) => unknown;
564
+ return hook(options);
565
+ },
566
+ };
567
+
568
+ const proxy = new Proxy((() => {}) as (...args: unknown[]) => unknown, handler);
569
+ return proxy;
570
+ }
571
+
572
+ return createProxy("") as TypedClient<TRouter>;
573
+ }
package/src/index.ts CHANGED
@@ -3,18 +3,50 @@
3
3
  *
4
4
  * React bindings for Lens API framework.
5
5
  * Hooks and context provider for reactive data access.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // lib/client.ts
10
+ * import { createClient } from '@sylphx/lens-react';
11
+ * import { httpTransport } from '@sylphx/lens-client';
12
+ * import type { AppRouter } from '@/server/router';
13
+ *
14
+ * export const client = createClient<AppRouter>({
15
+ * transport: httpTransport({ url: '/api/lens' }),
16
+ * });
17
+ *
18
+ * // Component usage
19
+ * function UserProfile({ id }: { id: string }) {
20
+ * const { data, loading } = client.user.get({ input: { id } });
21
+ * return <div>{data?.name}</div>;
22
+ * }
23
+ *
24
+ * // SSR usage
25
+ * const user = await client.user.get.fetch({ input: { id } });
26
+ * ```
6
27
  */
7
28
 
8
29
  // =============================================================================
9
- // Context & Provider
30
+ // New API (v4) - Recommended
10
31
  // =============================================================================
11
32
 
12
- export { LensProvider, type LensProviderProps, useLensClient } from "./context.js";
33
+ export {
34
+ createClient,
35
+ type MutationEndpoint,
36
+ type MutationHookOptions,
37
+ type MutationHookResult,
38
+ type QueryEndpoint,
39
+ type QueryHookOptions,
40
+ type QueryHookResult,
41
+ type TypedClient,
42
+ } from "./create.js";
13
43
 
14
44
  // =============================================================================
15
- // Hooks (Operations-based API)
45
+ // Legacy API (v3) - Deprecated, will be removed in v3.0
16
46
  // =============================================================================
17
47
 
48
+ export { LensProvider, type LensProviderProps, useLensClient } from "./context.js";
49
+
18
50
  export {
19
51
  // Types
20
52
  type MutationSelector,