@sylphx/lens-solid 1.0.2

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.
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @sylphx/lens-solid - Primitives
3
+ *
4
+ * SolidJS reactive primitives for Lens queries and mutations.
5
+ * Uses SolidJS fine-grained reactivity for optimal performance.
6
+ */
7
+
8
+ import type { MutationResult, QueryResult } from "@sylphx/lens-client";
9
+ import { type Accessor, createSignal, onCleanup } from "solid-js";
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ /** Query result with reactive signals */
16
+ export interface CreateQueryResult<T> {
17
+ /** Reactive data accessor */
18
+ data: Accessor<T | null>;
19
+ /** Reactive loading state */
20
+ loading: Accessor<boolean>;
21
+ /** Reactive error state */
22
+ error: Accessor<Error | null>;
23
+ /** Refetch the query */
24
+ refetch: () => void;
25
+ }
26
+
27
+ /** Mutation result with reactive signals */
28
+ export interface CreateMutationResult<TInput, TOutput> {
29
+ /** Reactive data accessor */
30
+ data: Accessor<TOutput | null>;
31
+ /** Reactive loading state */
32
+ loading: Accessor<boolean>;
33
+ /** Reactive error state */
34
+ error: Accessor<Error | null>;
35
+ /** Execute the mutation */
36
+ mutate: (input: TInput) => Promise<MutationResult<TOutput>>;
37
+ /** Reset state */
38
+ reset: () => void;
39
+ }
40
+
41
+ /** Lazy query result */
42
+ export interface CreateLazyQueryResult<T> {
43
+ /** Reactive data accessor */
44
+ data: Accessor<T | null>;
45
+ /** Reactive loading state */
46
+ loading: Accessor<boolean>;
47
+ /** Reactive error state */
48
+ error: Accessor<Error | null>;
49
+ /** Execute the query */
50
+ execute: () => Promise<T>;
51
+ /** Reset state */
52
+ reset: () => void;
53
+ }
54
+
55
+ /** Query options */
56
+ export interface CreateQueryOptions {
57
+ /** Skip the query (don't execute) */
58
+ skip?: boolean;
59
+ }
60
+
61
+ /** Mutation function type */
62
+ export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationResult<TOutput>>;
63
+
64
+ // =============================================================================
65
+ // createQuery
66
+ // =============================================================================
67
+
68
+ /**
69
+ * Create a reactive query from a QueryResult.
70
+ * Automatically subscribes to updates and manages cleanup.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * import { createQuery } from '@sylphx/lens-solid';
75
+ *
76
+ * function UserProfile(props: { userId: string }) {
77
+ * const user = createQuery(() => client.queries.getUser({ id: props.userId }));
78
+ *
79
+ * return (
80
+ * <Show when={!user.loading()} fallback={<Spinner />}>
81
+ * <Show when={user.data()} fallback={<NotFound />}>
82
+ * {(data) => <h1>{data().name}</h1>}
83
+ * </Show>
84
+ * </Show>
85
+ * );
86
+ * }
87
+ * ```
88
+ */
89
+ export function createQuery<T>(
90
+ queryFn: () => QueryResult<T>,
91
+ options?: CreateQueryOptions,
92
+ ): CreateQueryResult<T> {
93
+ const [data, setData] = createSignal<T | null>(null);
94
+ const [loading, setLoading] = createSignal(!options?.skip);
95
+ const [error, setError] = createSignal<Error | null>(null);
96
+
97
+ let unsubscribe: (() => void) | null = null;
98
+
99
+ const executeQuery = () => {
100
+ if (options?.skip) {
101
+ setLoading(false);
102
+ return;
103
+ }
104
+
105
+ const queryResult = queryFn();
106
+
107
+ // Subscribe to updates
108
+ unsubscribe = queryResult.subscribe((value) => {
109
+ setData(() => value);
110
+ setLoading(false);
111
+ setError(null);
112
+ });
113
+
114
+ // Handle initial load via promise
115
+ queryResult.then(
116
+ (value) => {
117
+ setData(() => value);
118
+ setLoading(false);
119
+ setError(null);
120
+ },
121
+ (err) => {
122
+ const queryError = err instanceof Error ? err : new Error(String(err));
123
+ setError(queryError);
124
+ setLoading(false);
125
+ },
126
+ );
127
+ };
128
+
129
+ // Execute query immediately (not in effect) for initial load
130
+ executeQuery();
131
+
132
+ // Cleanup on unmount
133
+ onCleanup(() => {
134
+ if (unsubscribe) {
135
+ unsubscribe();
136
+ unsubscribe = null;
137
+ }
138
+ });
139
+
140
+ const refetch = () => {
141
+ if (unsubscribe) {
142
+ unsubscribe();
143
+ unsubscribe = null;
144
+ }
145
+ setLoading(true);
146
+ setError(null);
147
+ executeQuery();
148
+ };
149
+
150
+ return {
151
+ data,
152
+ loading,
153
+ error,
154
+ refetch,
155
+ };
156
+ }
157
+
158
+ // =============================================================================
159
+ // createMutation
160
+ // =============================================================================
161
+
162
+ /**
163
+ * Create a reactive mutation with loading/error state.
164
+ *
165
+ * @example
166
+ * ```tsx
167
+ * import { createMutation } from '@sylphx/lens-solid';
168
+ *
169
+ * function CreatePostForm() {
170
+ * const createPost = createMutation(client.mutations.createPost);
171
+ *
172
+ * const handleSubmit = async (e: Event) => {
173
+ * e.preventDefault();
174
+ * try {
175
+ * const result = await createPost.mutate({ title: 'Hello World' });
176
+ * console.log('Created:', result.data);
177
+ * } catch (err) {
178
+ * console.error('Failed:', err);
179
+ * }
180
+ * };
181
+ *
182
+ * return (
183
+ * <form onSubmit={handleSubmit}>
184
+ * <button type="submit" disabled={createPost.loading()}>
185
+ * {createPost.loading() ? 'Creating...' : 'Create'}
186
+ * </button>
187
+ * <Show when={createPost.error()}>
188
+ * {(err) => <p class="error">{err().message}</p>}
189
+ * </Show>
190
+ * </form>
191
+ * );
192
+ * }
193
+ * ```
194
+ */
195
+ export function createMutation<TInput, TOutput>(
196
+ mutationFn: MutationFn<TInput, TOutput>,
197
+ ): CreateMutationResult<TInput, TOutput> {
198
+ const [data, setData] = createSignal<TOutput | null>(null);
199
+ const [loading, setLoading] = createSignal(false);
200
+ const [error, setError] = createSignal<Error | null>(null);
201
+
202
+ const mutate = async (input: TInput): Promise<MutationResult<TOutput>> => {
203
+ setLoading(true);
204
+ setError(null);
205
+
206
+ try {
207
+ const result = await mutationFn(input);
208
+ setData(() => result.data);
209
+ setLoading(false);
210
+ return result;
211
+ } catch (err) {
212
+ const mutationError = err instanceof Error ? err : new Error(String(err));
213
+ setError(mutationError);
214
+ setLoading(false);
215
+ throw mutationError;
216
+ }
217
+ };
218
+
219
+ const reset = () => {
220
+ setData(null);
221
+ setLoading(false);
222
+ setError(null);
223
+ };
224
+
225
+ return {
226
+ data,
227
+ loading,
228
+ error,
229
+ mutate,
230
+ reset,
231
+ };
232
+ }
233
+
234
+ // =============================================================================
235
+ // createLazyQuery
236
+ // =============================================================================
237
+
238
+ /**
239
+ * Create a lazy query that executes on demand.
240
+ *
241
+ * @example
242
+ * ```tsx
243
+ * import { createLazyQuery } from '@sylphx/lens-solid';
244
+ *
245
+ * function SearchUsers() {
246
+ * const [searchTerm, setSearchTerm] = createSignal('');
247
+ * const search = createLazyQuery(() =>
248
+ * client.queries.searchUsers({ query: searchTerm() })
249
+ * );
250
+ *
251
+ * const handleSearch = async () => {
252
+ * const results = await search.execute();
253
+ * console.log('Found:', results);
254
+ * };
255
+ *
256
+ * return (
257
+ * <div>
258
+ * <input
259
+ * value={searchTerm()}
260
+ * onInput={(e) => setSearchTerm(e.currentTarget.value)}
261
+ * />
262
+ * <button onClick={handleSearch} disabled={search.loading()}>
263
+ * Search
264
+ * </button>
265
+ * <Show when={search.data()}>
266
+ * {(users) => (
267
+ * <ul>
268
+ * <For each={users()}>
269
+ * {(user) => <li>{user.name}</li>}
270
+ * </For>
271
+ * </ul>
272
+ * )}
273
+ * </Show>
274
+ * </div>
275
+ * );
276
+ * }
277
+ * ```
278
+ */
279
+ export function createLazyQuery<T>(queryFn: () => QueryResult<T>): CreateLazyQueryResult<T> {
280
+ const [data, setData] = createSignal<T | null>(null);
281
+ const [loading, setLoading] = createSignal(false);
282
+ const [error, setError] = createSignal<Error | null>(null);
283
+
284
+ const execute = async (): Promise<T> => {
285
+ setLoading(true);
286
+ setError(null);
287
+
288
+ try {
289
+ const queryResult = queryFn();
290
+ const result = await queryResult;
291
+ setData(() => result);
292
+ setLoading(false);
293
+ return result;
294
+ } catch (err) {
295
+ const queryError = err instanceof Error ? err : new Error(String(err));
296
+ setError(queryError);
297
+ setLoading(false);
298
+ throw queryError;
299
+ }
300
+ };
301
+
302
+ const reset = () => {
303
+ setData(null);
304
+ setLoading(false);
305
+ setError(null);
306
+ };
307
+
308
+ return {
309
+ data,
310
+ loading,
311
+ error,
312
+ execute,
313
+ reset,
314
+ };
315
+ }