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. No-op 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 const enabled = resolveEnabled();
225 if (!enabled) return;
226 generation++;
227 const gen = generation;
228 const localController = new AbortController();
229 if (controller) controller.abort();
230 controller = localController;
231 setupInterval(enabled, resolveOptions()?.refetchInterval);
232 await fetchData(inputFn?.(), localController.signal, gen);
233 },
234 };
235}"#;
236
237const CREATE_MUTATION_IMPL: &str = r#"export function createMutation<K extends MutationKey>(
238 client: RpcClient,
239 key: K,
240 options?: MutationOptions<K>,
241): MutationResult<K> {
242 const [data, setData] = createSignal<MutationOutput<K> | undefined>();
243 const [error, setError] = createSignal<RpcError | undefined>();
244 const [isLoading, setIsLoading] = createSignal(false);
245 const [hasSucceeded, setHasSucceeded] = createSignal(false);
246
247 const isSuccess = createMemo(() => hasSucceeded());
248 const isError = createMemo(() => error() !== undefined);
249
250 async function execute(...input: MutationArgs<K>): Promise<MutationOutput<K>> {
251 setIsLoading(true);
252 setError(undefined);
253 setHasSucceeded(false);
254 try {
255 const callArgs: unknown[] = [key];
256 if (input.length > 0) callArgs.push(input[0]);
257 if (options?.callOptions) callArgs.push(options.callOptions);
258 const result = await (client.mutate as (...a: unknown[]) => Promise<unknown>)(
259 ...callArgs
260 ) as MutationOutput<K>;
261 setData(result as Exclude<MutationOutput<K> | undefined, Function>);
262 setHasSucceeded(true);
263 options?.onSuccess?.(result);
264 return result;
265 } catch (e) {
266 const err = e as RpcError;
267 setError(err as Exclude<RpcError | undefined, Function>);
268 options?.onError?.(err);
269 throw e;
270 } finally {
271 setIsLoading(false);
272 options?.onSettled?.();
273 }
274 }
275
276 return {
277 mutate: async (...args: MutationArgs<K>) => { await execute(...args); },
278 mutateAsync: (...args: MutationArgs<K>) => execute(...args),
279 data: data as () => MutationOutput<K> | undefined,
280 error,
281 isLoading,
282 isSuccess,
283 isError,
284 reset: () => batch(() => { setData(undefined); setError(undefined); setIsLoading(false); setHasSucceeded(false); }),
285 };
286}"#;
287
288const FRAMEWORK_IMPORT: &str = "import { createSignal, createEffect, createMemo, onCleanup, batch, untrack } from \"solid-js\";";
289
290pub fn generate_solid_file(
295 manifest: &Manifest,
296 client_import_path: &str,
297 types_import_path: &str,
298 preserve_docs: bool,
299) -> String {
300 common::generate_framework_file(
301 manifest,
302 client_import_path,
303 types_import_path,
304 preserve_docs,
305 &FrameworkConfig {
306 framework_import: Some(FRAMEWORK_IMPORT),
307 query_fn_name: "createQuery",
308 input_as_getter: true,
309 query_interfaces: &[QUERY_OPTIONS_INTERFACE, QUERY_RESULT_INTERFACE],
310 mutation_interfaces: &[MUTATION_OPTIONS_INTERFACE, MUTATION_RESULT_INTERFACE],
311 query_impl: CREATE_QUERY_IMPL,
312 mutation_impl: CREATE_MUTATION_IMPL,
313 },
314 )
315}