metaxy_cli/codegen/
solid.rs1use crate::model::Manifest;
2
3use super::common::{self, FrameworkConfig};
4
5const QUERY_OPTIONS_INTERFACE: &str = r#"export interface QueryOptions<K extends QueryKey> {
6 /**
7 * Whether to execute the query. @default true
8 *
9 * Pass a getter `() => bool` for reactive updates — a plain `boolean` is
10 * read once when `createQuery` is called and will not trigger re-fetches.
11 */
12 enabled?: boolean | (() => boolean);
13
14 /** Auto-refetch interval in milliseconds. Set to 0 or omit to disable. */
15 refetchInterval?: number;
16
17 /** Initial data shown before the first fetch completes. */
18 placeholderData?: QueryOutput<K>;
19
20 /** Per-call options forwarded to client.query(). */
21 callOptions?: CallOptions;
22
23 /** Called when the query succeeds. */
24 onSuccess?: (data: QueryOutput<K>) => void;
25
26 /** Called when the query fails. */
27 onError?: (error: RpcError) => void;
28
29 /** Called when the query settles (success or failure). */
30 onSettled?: () => void;
31}"#;
32
33const QUERY_RESULT_INTERFACE: &str = r#"export interface QueryResult<K extends QueryKey> {
34 /** The latest successfully resolved data, or placeholderData. */
35 data: () => QueryOutput<K> | undefined;
36
37 /** The error from the most recent failed fetch, cleared on next attempt. */
38 error: () => RpcError | undefined;
39
40 /** True while a fetch is in-flight (including the initial fetch). */
41 isLoading: () => boolean;
42
43 /** True after the first successful fetch. Stays true even if a later refetch fails. */
44 isSuccess: () => boolean;
45
46 /** True when the most recent fetch failed. */
47 isError: () => boolean;
48
49 /** Manually trigger a refetch. Works even when `enabled` is false. Resets the polling interval. */
50 refetch: () => Promise<void>;
51}"#;
52
53const MUTATION_OPTIONS_INTERFACE: &str = r#"export interface MutationOptions<K extends MutationKey> {
54 /** Per-call options forwarded to client.mutate(). */
55 callOptions?: CallOptions;
56
57 /** Called when the mutation succeeds. */
58 onSuccess?: (data: MutationOutput<K>) => void;
59
60 /** Called when the mutation fails. */
61 onError?: (error: RpcError) => void;
62
63 /** Called when the mutation settles (success or failure). */
64 onSettled?: () => void;
65}"#;
66
67const MUTATION_RESULT_INTERFACE: &str = r#"export interface MutationResult<K extends MutationKey> {
68 /** Execute the mutation. Rejects on error. */
69 mutate: (...args: MutationArgs<K>) => Promise<void>;
70
71 /** Execute the mutation and return the result. Rejects on error. */
72 mutateAsync: (...args: MutationArgs<K>) => Promise<MutationOutput<K>>;
73
74 /** The latest successfully resolved data. */
75 data: () => MutationOutput<K> | undefined;
76
77 /** The error from the most recent failed mutation, cleared on next attempt. */
78 error: () => RpcError | undefined;
79
80 /** True while a mutation is in-flight. */
81 isLoading: () => boolean;
82
83 /** True after the most recent mutation succeeded. */
84 isSuccess: () => boolean;
85
86 /** True when the most recent mutation failed. */
87 isError: () => boolean;
88
89 /** Reset state back to idle (clear data, error, status). */
90 reset: () => void;
91}"#;
92
93const CREATE_QUERY_IMPL: &str = r#"export function createQuery<K extends QueryKey>(
94 client: RpcClient,
95 ...args: unknown[]
96): QueryResult<K> {
97 const key = args[0] as K;
98
99 let inputFn: (() => QueryInput<K>) | undefined;
100 let optionsArg: QueryOptions<K> | (() => QueryOptions<K>) | undefined;
101
102 if (typeof args[1] === "function" && args[2] !== undefined) {
103 inputFn = args[1] as () => QueryInput<K>;
104 optionsArg = args[2] as QueryOptions<K> | (() => QueryOptions<K>) | undefined;
105 } else if (typeof args[1] === "function") {
106 if (VOID_QUERY_KEYS.has(key)) {
107 optionsArg = args[1] as () => QueryOptions<K>;
108 } else {
109 inputFn = args[1] as () => QueryInput<K>;
110 }
111 } else if (typeof args[1] === "object") {
112 optionsArg = args[1] as QueryOptions<K>;
113 }
114
115 function resolveOptions(): QueryOptions<K> | undefined {
116 return typeof optionsArg === "function" ? optionsArg() : optionsArg;
117 }
118
119 function resolveEnabled(): boolean {
120 const opts = resolveOptions();
121 return typeof opts?.enabled === "function"
122 ? opts.enabled()
123 : (opts?.enabled ?? true);
124 }
125
126 const initialOpts = resolveOptions();
127 const initialEnabled = typeof initialOpts?.enabled === "function"
128 ? initialOpts.enabled()
129 : (initialOpts?.enabled ?? true);
130
131 const [data, setData] = createSignal<QueryOutput<K> | undefined>(initialOpts?.placeholderData);
132 const [error, setError] = createSignal<RpcError | undefined>();
133 const [isLoading, setIsLoading] = createSignal(initialEnabled);
134 const [hasFetched, setHasFetched] = createSignal(false);
135
136 const resolvedEnabled = createMemo(() => resolveEnabled());
137 const isSuccess = createMemo(() => hasFetched());
138 const isError = createMemo(() => error() !== undefined);
139
140 let generation = 0;
141 let controller: AbortController | undefined;
142 let intervalId: ReturnType<typeof setInterval> | undefined;
143
144 async function fetchData(input: QueryInput<K> | undefined, signal: AbortSignal, gen: number) {
145 const opts = resolveOptions();
146 setIsLoading(true);
147 setError(undefined);
148 try {
149 const callArgs: unknown[] = [key];
150 if (input !== undefined) callArgs.push(input);
151 const mergedCallOptions = { ...opts?.callOptions, signal: opts?.callOptions?.signal
152 ? AbortSignal.any([signal, opts.callOptions.signal])
153 : signal };
154 callArgs.push(mergedCallOptions);
155 const result = await (client.query as (...a: unknown[]) => Promise<unknown>)(
156 ...callArgs
157 ) as QueryOutput<K>;
158 if (gen !== generation) return;
159 setData(result as Exclude<QueryOutput<K> | undefined, Function>);
160 setHasFetched(true);
161 opts?.onSuccess?.(result);
162 } catch (e) {
163 if (gen !== generation) return;
164 const err = e as RpcError;
165 setError(err as Exclude<RpcError | undefined, Function>);
166 opts?.onError?.(err);
167 } finally {
168 if (gen === generation) {
169 setIsLoading(false);
170 opts?.onSettled?.();
171 }
172 }
173 }
174
175 function setupInterval(enabled: boolean, refetchInterval: number | undefined) {
176 if (intervalId) { clearInterval(intervalId); intervalId = undefined; }
177 if (enabled && refetchInterval) {
178 intervalId = setInterval(() => {
179 if (controller && !controller.signal.aborted) {
180 void fetchData(inputFn?.(), controller.signal, generation);
181 }
182 }, refetchInterval);
183 }
184 }
185
186 createEffect(() => {
187 const enabled = resolvedEnabled();
188 const input = inputFn?.();
189
190 if (controller) controller.abort();
191 if (enabled) {
192 generation++;
193 const gen = generation;
194 controller = new AbortController();
195 untrack(() => { void fetchData(input, controller.signal, gen); });
196 } else {
197 setIsLoading(false);
198 controller = undefined;
199 }
200
201 onCleanup(() => {
202 if (controller) { controller.abort(); controller = undefined; }
203 });
204 });
205
206 createEffect(() => {
207 const enabled = resolveEnabled();
208 const refetchInterval = resolveOptions()?.refetchInterval;
209
210 setupInterval(enabled, refetchInterval);
211
212 onCleanup(() => {
213 if (intervalId) { clearInterval(intervalId); intervalId = undefined; }
214 });
215 });
216
217 return {
218 data: data as () => QueryOutput<K> | undefined,
219 error,
220 isLoading,
221 isSuccess,
222 isError,
223 refetch: async () => {
224 generation++;
225 const gen = generation;
226 const localController = new AbortController();
227 if (controller) controller.abort();
228 controller = localController;
229 const enabled = resolveEnabled();
230 setupInterval(enabled, resolveOptions()?.refetchInterval);
231 await fetchData(inputFn?.(), localController.signal, gen);
232 },
233 };
234}"#;
235
236const CREATE_MUTATION_IMPL: &str = r#"export function createMutation<K extends MutationKey>(
237 client: RpcClient,
238 key: K,
239 options?: MutationOptions<K>,
240): MutationResult<K> {
241 const [data, setData] = createSignal<MutationOutput<K> | undefined>();
242 const [error, setError] = createSignal<RpcError | undefined>();
243 const [isLoading, setIsLoading] = createSignal(false);
244 const [hasSucceeded, setHasSucceeded] = createSignal(false);
245
246 const isSuccess = createMemo(() => hasSucceeded());
247 const isError = createMemo(() => error() !== undefined);
248
249 async function execute(...input: MutationArgs<K>): Promise<MutationOutput<K>> {
250 setIsLoading(true);
251 setError(undefined);
252 setHasSucceeded(false);
253 try {
254 const callArgs: unknown[] = [key];
255 if (input.length > 0) callArgs.push(input[0]);
256 if (options?.callOptions) callArgs.push(options.callOptions);
257 const result = await (client.mutate as (...a: unknown[]) => Promise<unknown>)(
258 ...callArgs
259 ) as MutationOutput<K>;
260 setData(result as Exclude<MutationOutput<K> | undefined, Function>);
261 setHasSucceeded(true);
262 options?.onSuccess?.(result);
263 return result;
264 } catch (e) {
265 const err = e as RpcError;
266 setError(err as Exclude<RpcError | undefined, Function>);
267 options?.onError?.(err);
268 throw e;
269 } finally {
270 setIsLoading(false);
271 options?.onSettled?.();
272 }
273 }
274
275 return {
276 mutate: async (...args: MutationArgs<K>) => { await execute(...args); },
277 mutateAsync: (...args: MutationArgs<K>) => execute(...args),
278 data: data as () => MutationOutput<K> | undefined,
279 error,
280 isLoading,
281 isSuccess,
282 isError,
283 reset: () => batch(() => { setData(undefined); setError(undefined); setIsLoading(false); setHasSucceeded(false); }),
284 };
285}"#;
286
287const FRAMEWORK_IMPORT: &str = "import { createSignal, createEffect, createMemo, onCleanup, batch, untrack } from \"solid-js\";";
288
289pub fn generate_solid_file(
294 manifest: &Manifest,
295 client_import_path: &str,
296 types_import_path: &str,
297 preserve_docs: bool,
298) -> String {
299 common::generate_framework_file(
300 manifest,
301 client_import_path,
302 types_import_path,
303 preserve_docs,
304 &FrameworkConfig {
305 framework_import: Some(FRAMEWORK_IMPORT),
306 query_fn_name: "createQuery",
307 input_as_getter: true,
308 query_interfaces: &[QUERY_OPTIONS_INTERFACE, QUERY_RESULT_INTERFACE],
309 mutation_interfaces: &[MUTATION_OPTIONS_INTERFACE, MUTATION_RESULT_INTERFACE],
310 query_impl: CREATE_QUERY_IMPL,
311 mutation_impl: CREATE_MUTATION_IMPL,
312 },
313 )
314}