@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.
- package/dist/context.d.ts +50 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +765 -0
- package/dist/primitives.d.ts +151 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.test.d.ts +5 -0
- package/dist/primitives.test.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/context.tsx +86 -0
- package/src/index.ts +30 -0
- package/src/primitives.test.tsx +279 -0
- package/src/primitives.ts +315 -0
|
@@ -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
|
+
}
|