metaxy_cli/codegen/
svelte.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 type QueryStatus = "idle" | "loading" | "success" | "error";
34
35export interface QueryResult<K extends QueryKey> {
36 /** The latest successfully resolved data, or placeholderData. */
37 readonly data: QueryOutput<K> | undefined;
38
39 /** The error from the most recent failed fetch, cleared on next attempt. */
40 readonly error: RpcError | undefined;
41
42 /** Current status of the query. Derived: loading > error > success > idle. */
43 readonly status: QueryStatus;
44
45 /** True while a fetch is in-flight (including the initial fetch). */
46 readonly isLoading: boolean;
47
48 /** True after the first successful fetch. Stays true even if a later refetch fails. */
49 readonly isSuccess: boolean;
50
51 /** True when the most recent fetch failed. */
52 readonly isError: boolean;
53
54 /** True when placeholderData is being shown and no real fetch has completed yet. */
55 readonly isPlaceholderData: boolean;
56
57 /** Manually trigger a refetch. No-op when `enabled` is false. Resets the polling interval. */
58 refetch: () => Promise<void>;
59}"#;
60
61const MUTATION_OPTIONS_INTERFACE: &str = r#"export interface MutationOptions<K extends MutationKey> {
62 /** Per-call options forwarded to client.mutate(). */
63 callOptions?: CallOptions;
64
65 /** Called when the mutation succeeds. */
66 onSuccess?: (data: MutationOutput<K>) => void;
67
68 /** Called when the mutation fails. */
69 onError?: (error: RpcError) => void;
70
71 /** Called when the mutation settles (success or failure). */
72 onSettled?: () => void;
73}"#;
74
75const MUTATION_RESULT_INTERFACE: &str = r#"export interface MutationResult<K extends MutationKey> {
76 /** Execute the mutation. Rejects on error. */
77 mutate: (...args: MutationArgs<K>) => Promise<void>;
78
79 /** Execute the mutation and return the result. Rejects on error. */
80 mutateAsync: (...args: MutationArgs<K>) => Promise<MutationOutput<K>>;
81
82 /** The latest successfully resolved data. */
83 readonly data: MutationOutput<K> | undefined;
84
85 /** The error from the most recent failed mutation, cleared on next attempt. */
86 readonly error: RpcError | undefined;
87
88 /** True while a mutation is in-flight. */
89 readonly isLoading: boolean;
90
91 /** True after the most recent mutation succeeded. */
92 readonly isSuccess: boolean;
93
94 /** True when the most recent mutation failed. */
95 readonly isError: boolean;
96
97 /** Reset state back to idle (clear data, error, status). */
98 reset: () => void;
99}"#;
100
101const CREATE_QUERY_IMPL: &str = r#"export function createQuery<K extends QueryKey>(
102 client: RpcClient,
103 ...args: unknown[]
104): QueryResult<K> {
105 const key = args[0] as K;
106
107 let inputFn: (() => QueryInput<K>) | undefined;
108 let optionsArg: QueryOptions<K> | (() => QueryOptions<K>) | undefined;
109
110 if (typeof args[1] === "function" && args[2] !== undefined) {
111 inputFn = args[1] as () => QueryInput<K>;
112 optionsArg = args[2] as QueryOptions<K> | (() => QueryOptions<K>) | undefined;
113 } else if (typeof args[1] === "function") {
114 if (VOID_QUERY_KEYS.has(key)) {
115 optionsArg = args[1] as () => QueryOptions<K>;
116 } else {
117 inputFn = args[1] as () => QueryInput<K>;
118 }
119 } else if (typeof args[1] === "object") {
120 optionsArg = args[1] as QueryOptions<K>;
121 }
122
123 function resolveOptions(): QueryOptions<K> | undefined {
124 return typeof optionsArg === "function" ? optionsArg() : optionsArg;
125 }
126
127 function resolveEnabled(): boolean {
128 const opts = resolveOptions();
129 return typeof opts?.enabled === "function"
130 ? opts.enabled()
131 : (opts?.enabled ?? true);
132 }
133
134 let data = $state<QueryOutput<K> | undefined>(resolveOptions()?.placeholderData);
135 let error = $state<RpcError | undefined>();
136 let hasFetched = $state(false);
137 let loading = $state(false);
138
139 let generation = 0;
140 let controller: AbortController | undefined;
141 let intervalId: ReturnType<typeof setInterval> | undefined;
142 async function fetchData(input: QueryInput<K> | undefined, signal: AbortSignal, gen: number) {
143 const opts = resolveOptions();
144 loading = true;
145 error = undefined;
146 try {
147 const callArgs: unknown[] = [key];
148 if (input !== undefined) callArgs.push(input);
149 const mergedCallOptions = { ...opts?.callOptions, signal: opts?.callOptions?.signal
150 ? AbortSignal.any([signal, opts.callOptions.signal])
151 : signal };
152 callArgs.push(mergedCallOptions);
153 const result = await (client.query as (...a: unknown[]) => Promise<unknown>)(...callArgs) as QueryOutput<K>;
154 if (gen !== generation) return;
155 data = result;
156 hasFetched = true;
157 opts?.onSuccess?.(result);
158 } catch (e) {
159 if (gen !== generation) return;
160 error = e as RpcError;
161 opts?.onError?.(error);
162 } finally {
163 if (gen === generation) {
164 loading = false;
165 opts?.onSettled?.();
166 }
167 }
168 }
169
170 function setupInterval(enabled: boolean, refetchInterval: number | undefined) {
171 if (intervalId) { clearInterval(intervalId); intervalId = undefined; }
172 if (enabled && refetchInterval) {
173 intervalId = setInterval(() => {
174 if (controller && !controller.signal.aborted) {
175 void fetchData(inputFn?.(), controller.signal, generation);
176 }
177 }, refetchInterval);
178 }
179 }
180
181 $effect(() => {
182 const enabled = resolveEnabled();
183 const input = inputFn?.();
184 const refetchInterval = resolveOptions()?.refetchInterval;
185
186 if (controller) controller.abort();
187 if (enabled) {
188 generation++;
189 const gen = generation;
190 controller = new AbortController();
191 void fetchData(input, controller.signal, gen);
192 } else {
193 loading = false;
194 controller = undefined;
195 }
196
197 setupInterval(enabled, refetchInterval);
198
199 return () => {
200 if (intervalId) { clearInterval(intervalId); intervalId = undefined; }
201 };
202 });
203
204 $effect(() => {
205 return () => {
206 generation++;
207 if (controller) { controller.abort(); controller = undefined; }
208 };
209 });
210
211 return {
212 get data() { return data; },
213 get error() { return error; },
214 get status(): QueryStatus {
215 if (loading) return "loading";
216 if (error !== undefined) return "error";
217 if (hasFetched) return "success";
218 return "idle";
219 },
220 get isLoading() { return loading; },
221 get isSuccess() { return hasFetched; },
222 get isError() { return error !== undefined; },
223 get isPlaceholderData() { return !hasFetched && data !== undefined; },
224 refetch: async () => {
225 const enabled = resolveEnabled();
226 if (!enabled) return;
227 generation++;
228 const gen = generation;
229 const localController = new AbortController();
230 if (controller) controller.abort();
231 controller = localController;
232 setupInterval(enabled, resolveOptions()?.refetchInterval);
233 await fetchData(inputFn?.(), localController.signal, gen);
234 },
235 };
236}"#;
237
238const CREATE_MUTATION_IMPL: &str = r#"export function createMutation<K extends MutationKey>(
239 client: RpcClient,
240 key: K,
241 options?: MutationOptions<K>,
242): MutationResult<K> {
243 let data = $state<MutationOutput<K> | undefined>();
244 let error = $state<RpcError | undefined>();
245 let loading = $state(false);
246 let hasSucceeded = $state(false);
247
248 async function execute(...input: MutationArgs<K>): Promise<MutationOutput<K>> {
249 loading = true;
250 error = undefined;
251 hasSucceeded = false;
252 try {
253 const callArgs: unknown[] = [key];
254 if (input.length > 0) callArgs.push(input[0]);
255 if (options?.callOptions) callArgs.push(options.callOptions);
256 const result = await (client.mutate as (...a: unknown[]) => Promise<unknown>)(...callArgs) as MutationOutput<K>;
257 data = result;
258 hasSucceeded = true;
259 options?.onSuccess?.(result);
260 return result;
261 } catch (e) {
262 error = e as RpcError;
263 options?.onError?.(error);
264 throw e;
265 } finally {
266 loading = false;
267 options?.onSettled?.();
268 }
269 }
270
271 return {
272 mutate: async (...args: MutationArgs<K>) => { await execute(...args); },
273 mutateAsync: (...args: MutationArgs<K>) => execute(...args),
274 get data() { return data; },
275 get error() { return error; },
276 get isLoading() { return loading; },
277 get isSuccess() { return hasSucceeded; },
278 get isError() { return error !== undefined; },
279 reset: () => { data = undefined; error = undefined; loading = false; hasSucceeded = false; },
280 } as MutationResult<K>;
281}"#;
282
283pub fn generate_svelte_file(
288 manifest: &Manifest,
289 client_import_path: &str,
290 types_import_path: &str,
291 preserve_docs: bool,
292) -> String {
293 common::generate_framework_file(
294 manifest,
295 client_import_path,
296 types_import_path,
297 preserve_docs,
298 &FrameworkConfig {
299 framework_import: None,
300 query_fn_name: "createQuery",
301 input_as_getter: true,
302 query_interfaces: &[QUERY_OPTIONS_INTERFACE, QUERY_RESULT_INTERFACE],
303 mutation_interfaces: &[MUTATION_OPTIONS_INTERFACE, MUTATION_RESULT_INTERFACE],
304 query_impl: CREATE_QUERY_IMPL,
305 mutation_impl: CREATE_MUTATION_IMPL,
306 },
307 )
308}