1use super::common::{GENERATED_HEADER, is_void_input};
2use super::typescript::{emit_jsdoc, rust_type_to_ts};
3use crate::model::{Manifest, ProcedureKind};
4
5const ERROR_CLASS: &str = r#"export class RpcError extends Error {
7 readonly status: number;
8 readonly data: unknown;
9
10 constructor(status: number, message: string, data?: unknown) {
11 super(message);
12 this.name = "RpcError";
13 this.status = status;
14 this.data = data;
15 }
16}"#;
17
18const REQUEST_CONTEXT_INTERFACE: &str = r#"export interface RequestContext {
20 procedure: string;
21 method: "GET" | "POST";
22 url: string;
23 headers: Record<string, string>;
24 input?: unknown;
25}"#;
26
27const RESPONSE_CONTEXT_INTERFACE: &str = r#"export interface ResponseContext {
29 procedure: string;
30 method: "GET" | "POST";
31 url: string;
32 response: Response;
33 data: unknown;
34 duration: number;
35}"#;
36
37const ERROR_CONTEXT_INTERFACE: &str = r#"export interface ErrorContext {
39 procedure: string;
40 method: "GET" | "POST";
41 url: string;
42 error: unknown;
43 attempt: number;
44 willRetry: boolean;
45}"#;
46
47const RETRY_POLICY_INTERFACE: &str = r#"export interface RetryPolicy {
49 attempts: number;
50 delay: number | ((attempt: number) => number);
51 retryOn?: number[];
52}"#;
53
54const CONFIG_INTERFACE: &str = r#"export interface RpcClientConfig {
56 baseUrl: string;
57 fetch?: typeof globalThis.fetch;
58 headers?:
59 | Record<string, string>
60 | (() => Record<string, string> | Promise<Record<string, string>>);
61 onRequest?: (ctx: RequestContext) => void | Promise<void>;
62 onResponse?: (ctx: ResponseContext) => void | Promise<void>;
63 onError?: (ctx: ErrorContext) => void | Promise<void>;
64 retry?: RetryPolicy;
65 timeout?: number;
66 serialize?: (input: unknown) => string;
67 deserialize?: (text: string) => unknown;
68 // AbortSignal for cancelling all requests made by this client.
69 signal?: AbortSignal;
70 dedupe?: boolean;
71}"#;
72
73const CALL_OPTIONS_INTERFACE: &str = r#"export interface CallOptions {
75 headers?: Record<string, string>;
76 timeout?: number;
77 signal?: AbortSignal;
78 dedupe?: boolean;
79}"#;
80
81const DEDUP_KEY_FN: &str = r#"function dedupKey(procedure: string, input: unknown, config: RpcClientConfig): string {
83 const serialized = input === undefined
84 ? ""
85 : config.serialize
86 ? config.serialize(input)
87 : JSON.stringify(input);
88 return procedure + ":" + serialized;
89}"#;
90
91const WRAP_WITH_SIGNAL_FN: &str = r#"function wrapWithSignal<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
93 if (!signal) return promise;
94 if (signal.aborted) return Promise.reject(signal.reason);
95 return new Promise<T>((resolve, reject) => {
96 const onAbort = () => reject(signal.reason);
97 signal.addEventListener("abort", onAbort, { once: true });
98 promise.then(
99 (value) => { signal.removeEventListener("abort", onAbort); resolve(value); },
100 (error) => { signal.removeEventListener("abort", onAbort); reject(error); },
101 );
102 });
103}"#;
104
105const FETCH_HELPER: &str = r#"const DEFAULT_RETRY_ON = [408, 429, 500, 502, 503, 504];
107
108async function rpcFetch(
109 config: RpcClientConfig,
110 method: "GET" | "POST",
111 procedure: string,
112 input?: unknown,
113 callOptions?: CallOptions,
114): Promise<unknown> {
115 let url = `${config.baseUrl}/${procedure}`;
116 const customHeaders = typeof config.headers === "function"
117 ? await config.headers()
118 : config.headers;
119 const baseHeaders: Record<string, string> = { ...customHeaders, ...callOptions?.headers };
120
121 if (method === "GET" && input !== undefined) {
122 const serialized = config.serialize ? config.serialize(input) : JSON.stringify(input);
123 url += `?input=${encodeURIComponent(serialized)}`;
124 } else if (method === "POST" && input !== undefined) {
125 baseHeaders["Content-Type"] = "application/json";
126 }
127
128 const fetchFn = config.fetch ?? globalThis.fetch;
129 const maxAttempts = 1 + (config.retry?.attempts ?? 0);
130 const retryOn = config.retry?.retryOn ?? DEFAULT_RETRY_ON;
131 const effectiveTimeout = callOptions?.timeout ?? PROCEDURE_TIMEOUTS[procedure] ?? config.timeout;
132 const start = Date.now();
133
134 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
135 const reqCtx: RequestContext = { procedure, method, url, headers: { ...baseHeaders }, input };
136 await config.onRequest?.(reqCtx);
137
138 const init: RequestInit = { method, headers: reqCtx.headers };
139 if (method === "POST" && input !== undefined) {
140 init.body = config.serialize ? config.serialize(input) : JSON.stringify(input);
141 }
142
143 let timeoutId: ReturnType<typeof setTimeout> | undefined;
144 const signals: AbortSignal[] = [];
145 if (config.signal) signals.push(config.signal);
146 if (callOptions?.signal) signals.push(callOptions.signal);
147 if (effectiveTimeout) {
148 const controller = new AbortController();
149 timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
150 signals.push(controller.signal);
151 }
152 if (signals.length > 0) {
153 init.signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
154 }
155
156 const isRetryable = attempt < maxAttempts && (method === "GET" || IDEMPOTENT_MUTATIONS.has(procedure));
157
158 try {
159 const res = await fetchFn(url, init);
160
161 if (!res.ok) {
162 let data: unknown;
163 try {
164 data = await res.json();
165 } catch {
166 data = await res.text().catch(() => null);
167 }
168 const rpcError = new RpcError(
169 res.status,
170 `RPC error on "${procedure}": ${res.status} ${res.statusText}`,
171 data,
172 );
173 const canRetry = retryOn.includes(res.status) && isRetryable;
174 await config.onError?.({ procedure, method, url, error: rpcError, attempt, willRetry: canRetry });
175 if (!canRetry) throw rpcError;
176 } else {
177 const json = config.deserialize ? config.deserialize(await res.text()) : await res.json();
178 const result = json?.result?.data ?? json;
179 const duration = Date.now() - start;
180 await config.onResponse?.({ procedure, method, url, response: res, data: result, duration });
181 return result;
182 }
183 } catch (err) {
184 if (err instanceof RpcError) throw err;
185 await config.onError?.({ procedure, method, url, error: err, attempt, willRetry: isRetryable });
186 if (!isRetryable) throw err;
187 } finally {
188 if (timeoutId !== undefined) clearTimeout(timeoutId);
189 }
190
191 if (config.retry) {
192 const d = typeof config.retry.delay === "function"
193 ? config.retry.delay(attempt) : config.retry.delay;
194 await new Promise(r => setTimeout(r, d));
195 }
196 }
197}"#;
198
199const STREAM_HELPER: &str = r#"async function* rpcStream<T>(
201 config: RpcClientConfig,
202 procedure: string,
203 input?: unknown,
204 callOptions?: CallOptions,
205): AsyncGenerator<T> {
206 let url = `${config.baseUrl}/${procedure}`;
207 const customHeaders = typeof config.headers === "function"
208 ? await config.headers()
209 : config.headers;
210 const headers: Record<string, string> = {
211 "Content-Type": "application/json",
212 ...customHeaders,
213 ...callOptions?.headers,
214 };
215
216 const fetchFn = config.fetch ?? globalThis.fetch;
217 const init: RequestInit = { method: "POST", headers };
218 if (input !== undefined) {
219 init.body = config.serialize ? config.serialize(input) : JSON.stringify(input);
220 }
221
222 const signals: AbortSignal[] = [];
223 if (config.signal) signals.push(config.signal);
224 if (callOptions?.signal) signals.push(callOptions.signal);
225 // callOptions.timeout aborts the connection; per-procedure default timeouts are intentionally
226 // not applied for streams — the server manages stream duration via #[rpc_stream(timeout = "...")].
227 if (callOptions?.timeout) {
228 const timeoutController = new AbortController();
229 setTimeout(() => timeoutController.abort(), callOptions.timeout);
230 signals.push(timeoutController.signal);
231 }
232 if (signals.length > 0) {
233 init.signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
234 }
235
236 await config.onRequest?.({ procedure, method: "POST", url, headers: { ...headers }, input });
237
238 try {
239 const res = await fetchFn(url, init);
240 if (!res.ok) {
241 let data: unknown;
242 try { data = await res.json(); } catch { data = null; }
243 const err = new RpcError(res.status, `RPC stream error on "${procedure}": ${res.status} ${res.statusText}`, data);
244 await config.onError?.({ procedure, method: "POST", url, error: err, attempt: 1, willRetry: false });
245 throw err;
246 }
247
248 const reader = res.body!.getReader();
249 const decoder = new TextDecoder();
250 let buffer = "";
251
252 try {
253 while (true) {
254 const { done, value } = await reader.read();
255 if (done) break;
256 buffer += decoder.decode(value, { stream: true });
257 const parts = buffer.split("\n\n");
258 buffer = parts.pop()!;
259 for (const part of parts) {
260 let eventType = "message";
261 const dataLines: string[] = [];
262 for (const line of part.split("\n")) {
263 if (line.startsWith("event: ")) {
264 eventType = line.slice(7).trim();
265 } else if (line.startsWith("data: ")) {
266 dataLines.push(line.slice(6));
267 }
268 }
269 for (const payload of dataLines) {
270 if (eventType === "error") {
271 throw new RpcError(500, `RPC stream error on "${procedure}": ${payload}`, null);
272 }
273 yield (config.deserialize ? config.deserialize(payload) : JSON.parse(payload)) as T;
274 }
275 }
276 }
277 } finally {
278 reader.releaseLock();
279 }
280 } catch (err) {
281 if (!(err instanceof RpcError)) {
282 await config.onError?.({ procedure, method: "POST", url, error: err, attempt: 1, willRetry: false });
283 }
284 throw err;
285 }
286}"#;
287
288pub fn generate_client_file(
297 manifest: &Manifest,
298 types_import_path: &str,
299 preserve_docs: bool,
300) -> String {
301 let mut out = String::with_capacity(2048);
302
303 out.push_str(GENERATED_HEADER);
305 out.push('\n');
306
307 let type_names: Vec<&str> = manifest
309 .structs
310 .iter()
311 .map(|s| s.name.as_str())
312 .chain(manifest.enums.iter().map(|e| e.name.as_str()))
313 .collect();
314
315 if type_names.is_empty() {
317 emit!(
318 out,
319 "import type {{ Procedures }} from \"{types_import_path}\";\n"
320 );
321 emit!(out, "export type {{ Procedures }};\n");
322 } else {
323 let types_csv = type_names.join(", ");
324 emit!(
325 out,
326 "import type {{ Procedures, {types_csv} }} from \"{types_import_path}\";\n"
327 );
328 emit!(out, "export type {{ Procedures, {types_csv} }};\n");
329 }
330
331 emit!(out, "{ERROR_CLASS}\n");
333
334 emit!(out, "{REQUEST_CONTEXT_INTERFACE}\n");
336 emit!(out, "{RESPONSE_CONTEXT_INTERFACE}\n");
337 emit!(out, "{ERROR_CONTEXT_INTERFACE}\n");
338
339 emit!(out, "{RETRY_POLICY_INTERFACE}\n");
341
342 emit!(out, "{CONFIG_INTERFACE}\n");
344
345 emit!(out, "{CALL_OPTIONS_INTERFACE}\n");
347
348 generate_procedure_timeouts(manifest, &mut out);
350
351 generate_idempotent_mutations(manifest, &mut out);
353
354 emit!(out, "{FETCH_HELPER}\n");
356
357 let has_queries = manifest
359 .procedures
360 .iter()
361 .any(|p| p.kind == ProcedureKind::Query);
362 if has_queries {
363 emit!(out, "{DEDUP_KEY_FN}\n");
364 emit!(out, "{WRAP_WITH_SIGNAL_FN}\n");
365 }
366
367 let has_streams = manifest
369 .procedures
370 .iter()
371 .any(|p| p.kind == ProcedureKind::Stream);
372 if has_streams {
373 emit!(out, "{STREAM_HELPER}\n");
374 }
375
376 generate_type_helpers(&mut out);
378 out.push('\n');
379
380 generate_client_factory(manifest, preserve_docs, &mut out);
382
383 out
384}
385
386fn generate_procedure_timeouts(manifest: &Manifest, out: &mut String) {
388 let entries: Vec<_> = manifest
389 .procedures
390 .iter()
391 .filter_map(|p| p.timeout_ms.map(|ms| format!(" \"{}\": {}", p.name, ms)))
392 .collect();
393
394 if entries.is_empty() {
395 emit!(
396 out,
397 "const PROCEDURE_TIMEOUTS: Record<string, number> = {{}};\n"
398 );
399 } else {
400 emit!(out, "const PROCEDURE_TIMEOUTS: Record<string, number> = {{");
401 for entry in &entries {
402 emit!(out, "{entry},");
403 }
404 emit!(out, "}};\n");
405 }
406}
407
408fn generate_idempotent_mutations(manifest: &Manifest, out: &mut String) {
410 let names: Vec<_> = manifest
411 .procedures
412 .iter()
413 .filter(|p| p.idempotent)
414 .map(|p| format!("\"{}\"", p.name))
415 .collect();
416
417 if names.is_empty() {
418 emit!(
419 out,
420 "const IDEMPOTENT_MUTATIONS: Set<string> = new Set();\n"
421 );
422 } else {
423 emit!(
424 out,
425 "const IDEMPOTENT_MUTATIONS: Set<string> = new Set([{}]);\n",
426 names.join(", ")
427 );
428 }
429}
430
431fn generate_type_helpers(out: &mut String) {
433 emit!(out, "type QueryKey = keyof Procedures[\"queries\"];");
434 emit!(out, "type MutationKey = keyof Procedures[\"mutations\"];");
435 emit!(out, "type StreamKey = keyof Procedures[\"streams\"];");
436 emit!(
437 out,
438 "type QueryInput<K extends QueryKey> = Procedures[\"queries\"][K][\"input\"];"
439 );
440 emit!(
441 out,
442 "type QueryOutput<K extends QueryKey> = Procedures[\"queries\"][K][\"output\"];"
443 );
444 emit!(
445 out,
446 "type MutationInput<K extends MutationKey> = Procedures[\"mutations\"][K][\"input\"];"
447 );
448 emit!(
449 out,
450 "type MutationOutput<K extends MutationKey> = Procedures[\"mutations\"][K][\"output\"];"
451 );
452 emit!(
453 out,
454 "type StreamInput<K extends StreamKey> = Procedures[\"streams\"][K][\"input\"];"
455 );
456 emit!(
457 out,
458 "type StreamOutput<K extends StreamKey> = Procedures[\"streams\"][K][\"output\"];"
459 );
460}
461
462fn generate_client_factory(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
464 let queries: Vec<_> = manifest
465 .procedures
466 .iter()
467 .filter(|p| p.kind == ProcedureKind::Query)
468 .collect();
469 let mutations: Vec<_> = manifest
470 .procedures
471 .iter()
472 .filter(|p| p.kind == ProcedureKind::Mutation)
473 .collect();
474 let streams: Vec<_> = manifest
475 .procedures
476 .iter()
477 .filter(|p| p.kind == ProcedureKind::Stream)
478 .collect();
479 let has_queries = !queries.is_empty();
480 let has_mutations = !mutations.is_empty();
481 let has_streams = !streams.is_empty();
482
483 let void_queries: Vec<_> = queries.iter().filter(|p| is_void_input(p)).collect();
485 let non_void_queries: Vec<_> = queries.iter().filter(|p| !is_void_input(p)).collect();
486 let void_mutations: Vec<_> = mutations.iter().filter(|p| is_void_input(p)).collect();
487 let non_void_mutations: Vec<_> = mutations.iter().filter(|p| !is_void_input(p)).collect();
488
489 let void_streams: Vec<_> = streams.iter().filter(|p| is_void_input(p)).collect();
490 let non_void_streams: Vec<_> = streams.iter().filter(|p| !is_void_input(p)).collect();
491
492 let query_mixed = !void_queries.is_empty() && !non_void_queries.is_empty();
493 let mutation_mixed = !void_mutations.is_empty() && !non_void_mutations.is_empty();
494 let stream_mixed = !void_streams.is_empty() && !non_void_streams.is_empty();
495
496 if query_mixed {
498 let names: Vec<_> = void_queries
499 .iter()
500 .map(|p| format!("\"{}\"", p.name))
501 .collect();
502 emit!(
503 out,
504 "const VOID_QUERIES: Set<string> = new Set([{}]);",
505 names.join(", ")
506 );
507 out.push('\n');
508 }
509 if mutation_mixed {
510 let names: Vec<_> = void_mutations
511 .iter()
512 .map(|p| format!("\"{}\"", p.name))
513 .collect();
514 emit!(
515 out,
516 "const VOID_MUTATIONS: Set<string> = new Set([{}]);",
517 names.join(", ")
518 );
519 out.push('\n');
520 }
521 if stream_mixed {
522 let names: Vec<_> = void_streams
523 .iter()
524 .map(|p| format!("\"{}\"", p.name))
525 .collect();
526 emit!(
527 out,
528 "const VOID_STREAMS: Set<string> = new Set([{}]);",
529 names.join(", ")
530 );
531 out.push('\n');
532 }
533
534 emit!(out, "export interface RpcClient {{");
536
537 if has_queries {
538 generate_query_overloads(manifest, preserve_docs, out);
539 }
540
541 if has_mutations {
542 if has_queries {
543 out.push('\n');
544 }
545 generate_mutation_overloads(manifest, preserve_docs, out);
546 }
547
548 if has_streams {
549 if has_queries || has_mutations {
550 out.push('\n');
551 }
552 generate_stream_overloads(manifest, preserve_docs, out);
553 }
554
555 emit!(out, "}}");
556 out.push('\n');
557
558 emit!(
560 out,
561 "export function createRpcClient(config: RpcClientConfig): RpcClient {{"
562 );
563
564 if has_queries {
565 emit!(
566 out,
567 " const inflight = new Map<string, Promise<unknown>>();\n"
568 );
569 }
570
571 emit!(out, " return {{");
572
573 if has_queries {
574 emit!(
575 out,
576 " query(key: QueryKey, ...args: unknown[]): Promise<unknown> {{"
577 );
578
579 if query_mixed {
581 emit!(out, " let input: unknown;");
582 emit!(out, " let callOptions: CallOptions | undefined;");
583 emit!(out, " if (VOID_QUERIES.has(key)) {{");
584 emit!(out, " input = undefined;");
585 emit!(
586 out,
587 " callOptions = args[0] as CallOptions | undefined;"
588 );
589 emit!(out, " }} else {{");
590 emit!(out, " input = args[0];");
591 emit!(
592 out,
593 " callOptions = args[1] as CallOptions | undefined;"
594 );
595 emit!(out, " }}");
596 } else if !void_queries.is_empty() {
597 emit!(out, " const input = undefined;");
598 emit!(
599 out,
600 " const callOptions = args[0] as CallOptions | undefined;"
601 );
602 } else {
603 emit!(out, " const input = args[0];");
604 emit!(
605 out,
606 " const callOptions = args[1] as CallOptions | undefined;"
607 );
608 }
609
610 emit!(
612 out,
613 " const shouldDedupe = callOptions?.dedupe ?? config.dedupe ?? true;"
614 );
615 emit!(out, " if (shouldDedupe) {{");
616 emit!(out, " const k = dedupKey(key, input, config);");
617 emit!(out, " const existing = inflight.get(k);");
618 emit!(
619 out,
620 " if (existing) return wrapWithSignal(existing, callOptions?.signal);"
621 );
622 emit!(
623 out,
624 " const promise = rpcFetch(config, \"GET\", key, input, callOptions)"
625 );
626 emit!(out, " .finally(() => inflight.delete(k));");
627 emit!(out, " inflight.set(k, promise);");
628 emit!(
629 out,
630 " return wrapWithSignal(promise, callOptions?.signal);"
631 );
632 emit!(out, " }}");
633 emit!(
634 out,
635 " return rpcFetch(config, \"GET\", key, input, callOptions);"
636 );
637 emit!(out, " }},");
638 }
639
640 if has_mutations {
641 emit!(
642 out,
643 " mutate(key: MutationKey, ...args: unknown[]): Promise<unknown> {{"
644 );
645 if mutation_mixed {
646 emit!(out, " if (VOID_MUTATIONS.has(key)) {{");
648 emit!(
649 out,
650 " return rpcFetch(config, \"POST\", key, undefined, args[0] as CallOptions | undefined);"
651 );
652 emit!(out, " }}");
653 emit!(
654 out,
655 " return rpcFetch(config, \"POST\", key, args[0], args[1] as CallOptions | undefined);"
656 );
657 } else if !void_mutations.is_empty() {
658 emit!(
660 out,
661 " return rpcFetch(config, \"POST\", key, undefined, args[0] as CallOptions | undefined);"
662 );
663 } else {
664 emit!(
666 out,
667 " return rpcFetch(config, \"POST\", key, args[0], args[1] as CallOptions | undefined);"
668 );
669 }
670 emit!(out, " }},");
671 }
672
673 if has_streams {
674 emit!(
675 out,
676 " stream(key: StreamKey, ...args: unknown[]): AsyncGenerator<unknown> {{"
677 );
678 if stream_mixed {
679 emit!(out, " if (VOID_STREAMS.has(key)) {{");
680 emit!(
681 out,
682 " return rpcStream(config, key, undefined, args[0] as CallOptions | undefined);"
683 );
684 emit!(out, " }}");
685 emit!(
686 out,
687 " return rpcStream(config, key, args[0], args[1] as CallOptions | undefined);"
688 );
689 } else if !void_streams.is_empty() {
690 emit!(
691 out,
692 " return rpcStream(config, key, undefined, args[0] as CallOptions | undefined);"
693 );
694 } else {
695 emit!(
696 out,
697 " return rpcStream(config, key, args[0], args[1] as CallOptions | undefined);"
698 );
699 }
700 emit!(out, " }},");
701 }
702
703 emit!(out, " }} as RpcClient;");
704 emit!(out, "}}");
705}
706
707fn generate_query_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
709 let (void_queries, non_void_queries): (Vec<_>, Vec<_>) = manifest
710 .procedures
711 .iter()
712 .filter(|p| p.kind == ProcedureKind::Query)
713 .partition(|p| is_void_input(p));
714
715 for proc in &void_queries {
717 if preserve_docs && let Some(doc) = &proc.docs {
718 emit_jsdoc(doc, " ", out);
719 }
720 let output_ts = proc
721 .output
722 .as_ref()
723 .map(rust_type_to_ts)
724 .unwrap_or_else(|| "void".to_string());
725 emit!(
726 out,
727 " query(key: \"{}\"): Promise<{}>;",
728 proc.name,
729 output_ts,
730 );
731 emit!(
732 out,
733 " query(key: \"{}\", options: CallOptions): Promise<{}>;",
734 proc.name,
735 output_ts,
736 );
737 }
738
739 for proc in &non_void_queries {
741 if preserve_docs && let Some(doc) = &proc.docs {
742 emit_jsdoc(doc, " ", out);
743 }
744 let input_ts = proc
745 .input
746 .as_ref()
747 .map(rust_type_to_ts)
748 .unwrap_or_else(|| "void".to_string());
749 let output_ts = proc
750 .output
751 .as_ref()
752 .map(rust_type_to_ts)
753 .unwrap_or_else(|| "void".to_string());
754 emit!(
755 out,
756 " query(key: \"{}\", input: {}): Promise<{}>;",
757 proc.name,
758 input_ts,
759 output_ts,
760 );
761 emit!(
762 out,
763 " query(key: \"{}\", input: {}, options: CallOptions): Promise<{}>;",
764 proc.name,
765 input_ts,
766 output_ts,
767 );
768 }
769}
770
771fn generate_mutation_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
773 let (void_mutations, non_void_mutations): (Vec<_>, Vec<_>) = manifest
774 .procedures
775 .iter()
776 .filter(|p| p.kind == ProcedureKind::Mutation)
777 .partition(|p| is_void_input(p));
778
779 for proc in &void_mutations {
781 if preserve_docs && let Some(doc) = &proc.docs {
782 emit_jsdoc(doc, " ", out);
783 }
784 let output_ts = proc
785 .output
786 .as_ref()
787 .map(rust_type_to_ts)
788 .unwrap_or_else(|| "void".to_string());
789 emit!(
790 out,
791 " mutate(key: \"{}\"): Promise<{}>;",
792 proc.name,
793 output_ts,
794 );
795 emit!(
796 out,
797 " mutate(key: \"{}\", options: CallOptions): Promise<{}>;",
798 proc.name,
799 output_ts,
800 );
801 }
802
803 for proc in &non_void_mutations {
805 if preserve_docs && let Some(doc) = &proc.docs {
806 emit_jsdoc(doc, " ", out);
807 }
808 let input_ts = proc
809 .input
810 .as_ref()
811 .map(rust_type_to_ts)
812 .unwrap_or_else(|| "void".to_string());
813 let output_ts = proc
814 .output
815 .as_ref()
816 .map(rust_type_to_ts)
817 .unwrap_or_else(|| "void".to_string());
818 emit!(
819 out,
820 " mutate(key: \"{}\", input: {}): Promise<{}>;",
821 proc.name,
822 input_ts,
823 output_ts,
824 );
825 emit!(
826 out,
827 " mutate(key: \"{}\", input: {}, options: CallOptions): Promise<{}>;",
828 proc.name,
829 input_ts,
830 output_ts,
831 );
832 }
833}
834
835fn generate_stream_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
837 let (void_streams, non_void_streams): (Vec<_>, Vec<_>) = manifest
838 .procedures
839 .iter()
840 .filter(|p| p.kind == ProcedureKind::Stream)
841 .partition(|p| is_void_input(p));
842
843 for proc in &void_streams {
845 if preserve_docs && let Some(doc) = &proc.docs {
846 emit_jsdoc(doc, " ", out);
847 }
848 let output_ts = proc
849 .output
850 .as_ref()
851 .map(rust_type_to_ts)
852 .unwrap_or_else(|| "void".to_string());
853 emit!(
854 out,
855 " stream(key: \"{}\"): AsyncGenerator<{}>;",
856 proc.name,
857 output_ts,
858 );
859 emit!(
860 out,
861 " stream(key: \"{}\", options: CallOptions): AsyncGenerator<{}>;",
862 proc.name,
863 output_ts,
864 );
865 }
866
867 for proc in &non_void_streams {
869 if preserve_docs && let Some(doc) = &proc.docs {
870 emit_jsdoc(doc, " ", out);
871 }
872 let input_ts = proc
873 .input
874 .as_ref()
875 .map(rust_type_to_ts)
876 .unwrap_or_else(|| "void".to_string());
877 let output_ts = proc
878 .output
879 .as_ref()
880 .map(rust_type_to_ts)
881 .unwrap_or_else(|| "void".to_string());
882 emit!(
883 out,
884 " stream(key: \"{}\", input: {}): AsyncGenerator<{}>;",
885 proc.name,
886 input_ts,
887 output_ts,
888 );
889 emit!(
890 out,
891 " stream(key: \"{}\", input: {}, options: CallOptions): AsyncGenerator<{}>;",
892 proc.name,
893 input_ts,
894 output_ts,
895 );
896 }
897}