@sylphx/lens-solid 2.0.5 → 2.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,KAAK,QAAQ,EAA2B,MAAM,UAAU,CAAC;AAMlE,4EAA4E;AAC5E,MAAM,MAAM,UAAU,CAAC,CAAC,IACrB,WAAW,CAAC,CAAC,CAAC,GACd,IAAI,GACJ,SAAS,GACT,CAAC,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAW7C,yCAAyC;AACzC,MAAM,WAAW,iBAAiB,CAAC,CAAC;IACnC,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,wBAAwB;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB,CAAC,MAAM,EAAE,OAAO;IACpD,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC/B,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAwB;AACxB,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACvC,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,wBAAwB;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,oBAAoB;AACpB,MAAM,WAAW,kBAAkB;IAClC,qCAAqC;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,6BAA6B;AAC7B,MAAM,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;AAM9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,OAAO,CAAC,EAAE,kBAAkB,GAC1B,iBAAiB,CAAC,CAAC,CAAC,CAsEtB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAC7C,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAmCvC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC,CA2CtF"}
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,KAAK,QAAQ,EAAyC,MAAM,UAAU,CAAC;AAMhF,4EAA4E;AAC5E,MAAM,MAAM,UAAU,CAAC,CAAC,IACrB,WAAW,CAAC,CAAC,CAAC,GACd,IAAI,GACJ,SAAS,GACT,CAAC,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAW7C,yCAAyC;AACzC,MAAM,WAAW,iBAAiB,CAAC,CAAC;IACnC,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,wBAAwB;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB,CAAC,MAAM,EAAE,OAAO;IACpD,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC/B,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAwB;AACxB,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACvC,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,wBAAwB;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,oBAAoB;AACpB,MAAM,WAAW,kBAAkB;IAClC,qCAAqC;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,6BAA6B;AAC7B,MAAM,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;AAM9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,OAAO,CAAC,EAAE,kBAAkB,GAC1B,iBAAiB,CAAC,CAAC,CAAC,CAuGtB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAC7C,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAmCvC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC,CA2CtF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-solid",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "SolidJS bindings for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,8 @@
31
31
  "author": "SylphxAI",
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
- "@sylphx/lens-client": "^2.0.5"
34
+ "@sylphx/lens-client": "^2.1.0",
35
+ "@sylphx/lens-core": "^2.0.1"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "solid-js": ">=1.8.0"
package/src/create.ts ADDED
@@ -0,0 +1,472 @@
1
+ /**
2
+ * @sylphx/lens-solid - Create Client
3
+ *
4
+ * Creates a typed Lens client with SolidJS primitives.
5
+ * Each endpoint can be called directly as a primitive or via .fetch() for promises.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // lib/client.ts
10
+ * import { createClient } from '@sylphx/lens-solid';
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
+ * const { data, loading } = client.user.get({ input: { id } });
20
+ *
21
+ * // In SSR
22
+ * const user = await client.user.get.fetch({ input: { id } });
23
+ * ```
24
+ */
25
+
26
+ import {
27
+ createClient as createBaseClient,
28
+ type LensClientConfig,
29
+ type QueryResult,
30
+ type SelectionObject,
31
+ type TypedClientConfig,
32
+ } from "@sylphx/lens-client";
33
+ import type { MutationDef, QueryDef, RouterDef, RouterRoutes } from "@sylphx/lens-core";
34
+ import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js";
35
+
36
+ // =============================================================================
37
+ // Types
38
+ // =============================================================================
39
+
40
+ /** Query hook options */
41
+ export interface QueryHookOptions<TInput> {
42
+ /** Query input parameters */
43
+ input?: TInput;
44
+ /** Field selection */
45
+ select?: SelectionObject;
46
+ /** Skip query execution */
47
+ skip?: boolean;
48
+ }
49
+
50
+ /** Query hook result */
51
+ export interface QueryHookResult<T> {
52
+ /** Reactive data accessor */
53
+ data: Accessor<T | null>;
54
+ /** Reactive loading state */
55
+ loading: Accessor<boolean>;
56
+ /** Reactive error state */
57
+ error: Accessor<Error | null>;
58
+ /** Refetch the query */
59
+ refetch: () => void;
60
+ }
61
+
62
+ /** Mutation hook options */
63
+ export interface MutationHookOptions<TOutput> {
64
+ /** Called on successful mutation */
65
+ onSuccess?: (data: TOutput) => void;
66
+ /** Called on mutation error */
67
+ onError?: (error: Error) => void;
68
+ /** Called when mutation settles (success or error) */
69
+ onSettled?: () => void;
70
+ }
71
+
72
+ /** Mutation hook result */
73
+ export interface MutationHookResult<TInput, TOutput> {
74
+ /** Execute the mutation */
75
+ mutate: (options: { input: TInput; select?: SelectionObject }) => Promise<TOutput>;
76
+ /** Reactive loading state */
77
+ loading: Accessor<boolean>;
78
+ /** Reactive error state */
79
+ error: Accessor<Error | null>;
80
+ /** Reactive last mutation result */
81
+ data: Accessor<TOutput | null>;
82
+ /** Reset mutation state */
83
+ reset: () => void;
84
+ }
85
+
86
+ /** Query endpoint type */
87
+ export interface QueryEndpoint<TInput, TOutput> {
88
+ /** Primitive call (in component) */
89
+ <_TSelect extends SelectionObject = Record<string, never>>(
90
+ options: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>,
91
+ ): QueryHookResult<TOutput>;
92
+
93
+ /** Promise call (SSR) */
94
+ fetch: <TSelect extends SelectionObject = Record<string, never>>(
95
+ options: TInput extends void
96
+ ? { input?: void; select?: TSelect } | void
97
+ : { input: TInput; select?: TSelect },
98
+ ) => Promise<TOutput>;
99
+ }
100
+
101
+ /** Mutation endpoint type */
102
+ export interface MutationEndpoint<TInput, TOutput> {
103
+ /** Primitive call (in component) */
104
+ (options?: MutationHookOptions<TOutput>): MutationHookResult<TInput, TOutput>;
105
+
106
+ /** Promise call (SSR) */
107
+ fetch: <TSelect extends SelectionObject = Record<string, never>>(options: {
108
+ input: TInput;
109
+ select?: TSelect;
110
+ }) => Promise<TOutput>;
111
+ }
112
+
113
+ /** Infer client type from router routes */
114
+ type InferTypedClient<TRoutes extends RouterRoutes> = {
115
+ [K in keyof TRoutes]: TRoutes[K] extends RouterDef<infer TNestedRoutes>
116
+ ? InferTypedClient<TNestedRoutes>
117
+ : TRoutes[K] extends QueryDef<infer TInput, infer TOutput>
118
+ ? QueryEndpoint<TInput, TOutput>
119
+ : TRoutes[K] extends MutationDef<infer TInput, infer TOutput>
120
+ ? MutationEndpoint<TInput, TOutput>
121
+ : never;
122
+ };
123
+
124
+ /** Typed client from router */
125
+ export type TypedClient<TRouter extends RouterDef> =
126
+ TRouter extends RouterDef<infer TRoutes> ? InferTypedClient<TRoutes> : never;
127
+
128
+ // =============================================================================
129
+ // Hook Factories
130
+ // =============================================================================
131
+
132
+ /**
133
+ * Create a query primitive for a specific endpoint
134
+ */
135
+ function createQueryHook<TInput, TOutput>(
136
+ baseClient: unknown,
137
+ path: string,
138
+ ): QueryEndpoint<TInput, TOutput> {
139
+ const getEndpoint = (p: string) => {
140
+ const parts = p.split(".");
141
+ let current: unknown = baseClient;
142
+ for (const part of parts) {
143
+ current = (current as Record<string, unknown>)[part];
144
+ }
145
+ return current as (options: unknown) => QueryResult<TOutput>;
146
+ };
147
+
148
+ const useQueryPrimitive = (options?: QueryHookOptions<TInput>): QueryHookResult<TOutput> => {
149
+ const [data, setData] = createSignal<TOutput | null>(null);
150
+ const [loading, setLoading] = createSignal(!options?.skip);
151
+ const [error, setError] = createSignal<Error | null>(null);
152
+
153
+ let unsubscribe: (() => void) | null = null;
154
+
155
+ const executeQuery = () => {
156
+ if (unsubscribe) {
157
+ unsubscribe();
158
+ unsubscribe = null;
159
+ }
160
+
161
+ if (options?.skip) {
162
+ setData(null);
163
+ setLoading(false);
164
+ setError(null);
165
+ return;
166
+ }
167
+
168
+ const endpoint = getEndpoint(path);
169
+ const query = endpoint({ input: options?.input, select: options?.select });
170
+
171
+ setLoading(true);
172
+ setError(null);
173
+
174
+ unsubscribe = query.subscribe((value) => {
175
+ setData(() => value);
176
+ setLoading(false);
177
+ setError(null);
178
+ });
179
+
180
+ query.then(
181
+ (value) => {
182
+ setData(() => value);
183
+ setLoading(false);
184
+ setError(null);
185
+ },
186
+ (err) => {
187
+ const queryError = err instanceof Error ? err : new Error(String(err));
188
+ setError(queryError);
189
+ setLoading(false);
190
+ },
191
+ );
192
+ };
193
+
194
+ // Execute initial query synchronously
195
+ executeQuery();
196
+
197
+ // Use createEffect for reactive updates when options change
198
+ createEffect(() => {
199
+ // Access options to track them (Solid will re-run when they change)
200
+ const _ = JSON.stringify(options);
201
+ executeQuery();
202
+ });
203
+
204
+ onCleanup(() => {
205
+ if (unsubscribe) {
206
+ unsubscribe();
207
+ unsubscribe = null;
208
+ }
209
+ });
210
+
211
+ const refetch = () => {
212
+ if (unsubscribe) {
213
+ unsubscribe();
214
+ unsubscribe = null;
215
+ }
216
+ setLoading(true);
217
+ setError(null);
218
+ const endpoint = getEndpoint(path);
219
+ const query = endpoint({ input: options?.input, select: options?.select });
220
+ if (query) {
221
+ unsubscribe = query.subscribe((value) => {
222
+ setData(() => value);
223
+ setLoading(false);
224
+ setError(null);
225
+ });
226
+ query.then(
227
+ (value) => {
228
+ setData(() => value);
229
+ setLoading(false);
230
+ },
231
+ (err) => {
232
+ const queryError = err instanceof Error ? err : new Error(String(err));
233
+ setError(queryError);
234
+ setLoading(false);
235
+ },
236
+ );
237
+ }
238
+ };
239
+
240
+ return { data, loading, error, refetch };
241
+ };
242
+
243
+ // Fetch method for promises (SSR)
244
+ const fetch = async (options?: {
245
+ input?: TInput;
246
+ select?: SelectionObject;
247
+ }): Promise<TOutput> => {
248
+ const endpoint = getEndpoint(path);
249
+ const queryResult = endpoint({ input: options?.input, select: options?.select });
250
+ return queryResult.then((data) => data);
251
+ };
252
+
253
+ // Attach fetch method to the hook function
254
+ useQueryPrimitive.fetch = fetch;
255
+
256
+ return useQueryPrimitive as unknown as QueryEndpoint<TInput, TOutput>;
257
+ }
258
+
259
+ /**
260
+ * Create a mutation primitive for a specific endpoint
261
+ */
262
+ function createMutationHook<TInput, TOutput>(
263
+ baseClient: unknown,
264
+ path: string,
265
+ ): MutationEndpoint<TInput, TOutput> {
266
+ const getEndpoint = (p: string) => {
267
+ const parts = p.split(".");
268
+ let current: unknown = baseClient;
269
+ for (const part of parts) {
270
+ current = (current as Record<string, unknown>)[part];
271
+ }
272
+ return current as (options: unknown) => QueryResult<{ data: TOutput }>;
273
+ };
274
+
275
+ const useMutationPrimitive = (
276
+ hookOptions?: MutationHookOptions<TOutput>,
277
+ ): MutationHookResult<TInput, TOutput> => {
278
+ const [data, setData] = createSignal<TOutput | null>(null);
279
+ const [loading, setLoading] = createSignal(false);
280
+ const [error, setError] = createSignal<Error | null>(null);
281
+
282
+ const mutate = async (options: {
283
+ input: TInput;
284
+ select?: SelectionObject;
285
+ }): Promise<TOutput> => {
286
+ setLoading(true);
287
+ setError(null);
288
+
289
+ try {
290
+ const endpoint = getEndpoint(path);
291
+ const result = await endpoint({ input: options.input, select: options.select });
292
+ const mutationResult = result as unknown as { data: TOutput };
293
+
294
+ setData(() => mutationResult.data);
295
+ setLoading(false);
296
+
297
+ hookOptions?.onSuccess?.(mutationResult.data);
298
+ hookOptions?.onSettled?.();
299
+
300
+ return mutationResult.data;
301
+ } catch (err) {
302
+ const mutationError = err instanceof Error ? err : new Error(String(err));
303
+ setError(mutationError);
304
+ setLoading(false);
305
+
306
+ hookOptions?.onError?.(mutationError);
307
+ hookOptions?.onSettled?.();
308
+
309
+ throw mutationError;
310
+ }
311
+ };
312
+
313
+ const reset = () => {
314
+ setData(null);
315
+ setLoading(false);
316
+ setError(null);
317
+ };
318
+
319
+ return { mutate, loading, error, data, reset };
320
+ };
321
+
322
+ // Fetch method for promises (SSR)
323
+ const fetch = async (options: { input: TInput; select?: SelectionObject }): Promise<TOutput> => {
324
+ const endpoint = getEndpoint(path);
325
+ const result = await endpoint({ input: options.input, select: options.select });
326
+ const mutationResult = result as unknown as { data: TOutput };
327
+ return mutationResult.data;
328
+ };
329
+
330
+ // Attach fetch method to the hook function
331
+ useMutationPrimitive.fetch = fetch;
332
+
333
+ return useMutationPrimitive as unknown as MutationEndpoint<TInput, TOutput>;
334
+ }
335
+
336
+ // =============================================================================
337
+ // Create Client
338
+ // =============================================================================
339
+
340
+ // Cache for hook functions
341
+ const hookCache = new Map<string, unknown>();
342
+
343
+ /**
344
+ * Create a Lens client with SolidJS primitives.
345
+ *
346
+ * Each endpoint can be called:
347
+ * - Directly as a primitive: `client.user.get({ input: { id } })`
348
+ * - Via .fetch() for promises: `await client.user.get.fetch({ input: { id } })`
349
+ *
350
+ * @example
351
+ * ```tsx
352
+ * // lib/client.ts
353
+ * import { createClient } from '@sylphx/lens-solid';
354
+ * import { httpTransport } from '@sylphx/lens-client';
355
+ * import type { AppRouter } from '@/server/router';
356
+ *
357
+ * export const client = createClient<AppRouter>({
358
+ * transport: httpTransport({ url: '/api/lens' }),
359
+ * });
360
+ *
361
+ * // Component usage
362
+ * function UserProfile(props: { id: string }) {
363
+ * const { data, loading, error } = client.user.get({
364
+ * input: { id: props.id },
365
+ * select: { name: true },
366
+ * });
367
+ *
368
+ * const { mutate, loading: saving } = client.user.update({
369
+ * onSuccess: () => console.log('Updated!'),
370
+ * });
371
+ *
372
+ * return (
373
+ * <Show when={!loading()} fallback={<Spinner />}>
374
+ * <h1>{data()?.name}</h1>
375
+ * <button
376
+ * onClick={() => mutate({ input: { id: props.id, name: 'New' } })}
377
+ * disabled={saving()}
378
+ * >
379
+ * Update
380
+ * </button>
381
+ * </Show>
382
+ * );
383
+ * }
384
+ * ```
385
+ */
386
+ export function createClient<TRouter extends RouterDef>(
387
+ config: LensClientConfig | TypedClientConfig<{ router: TRouter }>,
388
+ ): TypedClient<TRouter> {
389
+ const baseClient = createBaseClient(config as LensClientConfig);
390
+
391
+ function createProxy(path: string): unknown {
392
+ const handler: ProxyHandler<(...args: unknown[]) => unknown> = {
393
+ get(_target, prop) {
394
+ if (typeof prop === "symbol") return undefined;
395
+ const key = prop as string;
396
+
397
+ if (key === "fetch") {
398
+ return async (options: unknown) => {
399
+ const parts = path.split(".");
400
+ let current: unknown = baseClient;
401
+ for (const part of parts) {
402
+ current = (current as Record<string, unknown>)[part];
403
+ }
404
+ const endpointFn = current as (opts: unknown) => QueryResult<unknown>;
405
+ const queryResult = endpointFn(options);
406
+ const result = await queryResult;
407
+
408
+ if (
409
+ result &&
410
+ typeof result === "object" &&
411
+ "data" in result &&
412
+ Object.keys(result).length === 1
413
+ ) {
414
+ return (result as { data: unknown }).data;
415
+ }
416
+ return result;
417
+ };
418
+ }
419
+
420
+ if (key === "then") return undefined;
421
+ if (key.startsWith("_")) return undefined;
422
+
423
+ const newPath = path ? `${path}.${key}` : key;
424
+ return createProxy(newPath);
425
+ },
426
+
427
+ apply(_target, _thisArg, args) {
428
+ const options = args[0] as Record<string, unknown> | undefined;
429
+
430
+ const isQueryOptions =
431
+ options && ("input" in options || "select" in options || "skip" in options);
432
+
433
+ const isMutationOptions =
434
+ !options ||
435
+ (!isQueryOptions &&
436
+ (Object.keys(options).length === 0 ||
437
+ "onSuccess" in options ||
438
+ "onError" in options ||
439
+ "onSettled" in options));
440
+
441
+ const cacheKeyQuery = `${path}:query`;
442
+ const cacheKeyMutation = `${path}:mutation`;
443
+
444
+ if (isQueryOptions) {
445
+ if (!hookCache.has(cacheKeyQuery)) {
446
+ hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
447
+ }
448
+ const hook = hookCache.get(cacheKeyQuery) as (opts: unknown) => unknown;
449
+ return hook(options);
450
+ }
451
+
452
+ if (isMutationOptions) {
453
+ if (!hookCache.has(cacheKeyMutation)) {
454
+ hookCache.set(cacheKeyMutation, createMutationHook(baseClient, path));
455
+ }
456
+ const hook = hookCache.get(cacheKeyMutation) as (opts: unknown) => unknown;
457
+ return hook(options);
458
+ }
459
+
460
+ if (!hookCache.has(cacheKeyQuery)) {
461
+ hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
462
+ }
463
+ const hook = hookCache.get(cacheKeyQuery) as (opts: unknown) => unknown;
464
+ return hook(options);
465
+ },
466
+ };
467
+
468
+ return new Proxy((() => {}) as (...args: unknown[]) => unknown, handler);
469
+ }
470
+
471
+ return createProxy("") as TypedClient<TRouter>;
472
+ }
package/src/index.ts CHANGED
@@ -3,10 +3,43 @@
3
3
  *
4
4
  * SolidJS bindings for Lens API framework.
5
5
  * Reactive primitives that integrate with SolidJS fine-grained reactivity.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // lib/client.ts
10
+ * import { createClient } from '@sylphx/lens-solid';
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
+ * const { data, loading } = client.user.get({ input: { id } });
20
+ *
21
+ * // SSR usage
22
+ * const user = await client.user.get.fetch({ input: { id } });
23
+ * ```
6
24
  */
7
25
 
8
26
  // =============================================================================
9
- // Context & Provider
27
+ // New API (v4) - Recommended
28
+ // =============================================================================
29
+
30
+ export {
31
+ createClient,
32
+ type MutationEndpoint,
33
+ type MutationHookOptions,
34
+ type MutationHookResult,
35
+ type QueryEndpoint,
36
+ type QueryHookOptions,
37
+ type QueryHookResult,
38
+ type TypedClient,
39
+ } from "./create.js";
40
+
41
+ // =============================================================================
42
+ // Legacy API (v3) - Deprecated
10
43
  // =============================================================================
11
44
 
12
45
  export { LensProvider, type LensProviderProps, useLensClient } from "./context.js";
package/src/primitives.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { MutationResult, QueryResult } from "@sylphx/lens-client";
9
- import { type Accessor, createSignal, onCleanup } from "solid-js";
9
+ import { type Accessor, createEffect, createSignal, onCleanup } from "solid-js";
10
10
 
11
11
  // =============================================================================
12
12
  // Query Input Types
@@ -120,8 +120,12 @@ export function createQuery<T>(
120
120
 
121
121
  let unsubscribe: (() => void) | null = null;
122
122
 
123
- const executeQuery = () => {
124
- const queryResult = resolveQuery(queryInput);
123
+ const executeQuery = (queryResult: QueryResult<T> | null | undefined) => {
124
+ // Cleanup previous subscription
125
+ if (unsubscribe) {
126
+ unsubscribe();
127
+ unsubscribe = null;
128
+ }
125
129
 
126
130
  // Handle null/undefined query or skip
127
131
  if (options?.skip || queryResult == null) {
@@ -156,8 +160,19 @@ export function createQuery<T>(
156
160
  );
157
161
  };
158
162
 
159
- // Execute query immediately (not in effect) for initial load
160
- executeQuery();
163
+ // Execute initial query synchronously
164
+ const initialQuery = resolveQuery(queryInput);
165
+ executeQuery(initialQuery);
166
+
167
+ // Use createEffect to track reactive dependencies for re-execution
168
+ // This ensures the query re-runs when signals used in the accessor change
169
+ createEffect(() => {
170
+ const queryResult = resolveQuery(queryInput); // Solid tracks reactive deps here
171
+ // Only re-execute if query changed (not on initial mount)
172
+ if (queryResult !== initialQuery) {
173
+ executeQuery(queryResult);
174
+ }
175
+ });
161
176
 
162
177
  // Cleanup on unmount
163
178
  onCleanup(() => {
@@ -174,7 +189,25 @@ export function createQuery<T>(
174
189
  }
175
190
  setLoading(true);
176
191
  setError(null);
177
- executeQuery();
192
+ const queryResult = resolveQuery(queryInput);
193
+ if (queryResult) {
194
+ unsubscribe = queryResult.subscribe((value) => {
195
+ setData(() => value);
196
+ setLoading(false);
197
+ setError(null);
198
+ });
199
+ queryResult.then(
200
+ (value) => {
201
+ setData(() => value);
202
+ setLoading(false);
203
+ },
204
+ (err) => {
205
+ const queryError = err instanceof Error ? err : new Error(String(err));
206
+ setError(queryError);
207
+ setLoading(false);
208
+ },
209
+ );
210
+ }
178
211
  };
179
212
 
180
213
  return {