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 if (signals.length > 0) {
226 init.signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
227 }
228
229 const res = await fetchFn(url, init);
230 if (!res.ok) {
231 let data: unknown;
232 try { data = await res.json(); } catch { data = null; }
233 throw new RpcError(res.status, `RPC stream error on "${procedure}": ${res.status} ${res.statusText}`, data);
234 }
235
236 const reader = res.body!.getReader();
237 const decoder = new TextDecoder();
238 let buffer = "";
239
240 try {
241 while (true) {
242 const { done, value } = await reader.read();
243 if (done) break;
244 buffer += decoder.decode(value, { stream: true });
245 const parts = buffer.split("\n\n");
246 buffer = parts.pop()!;
247 for (const part of parts) {
248 for (const line of part.split("\n")) {
249 if (line.startsWith("data: ")) {
250 const payload = line.slice(6);
251 yield (config.deserialize ? config.deserialize(payload) : JSON.parse(payload)) as T;
252 }
253 }
254 }
255 }
256 } finally {
257 reader.releaseLock();
258 }
259}"#;
260
261pub fn generate_client_file(
270 manifest: &Manifest,
271 types_import_path: &str,
272 preserve_docs: bool,
273) -> String {
274 let mut out = String::with_capacity(2048);
275
276 out.push_str(GENERATED_HEADER);
278 out.push('\n');
279
280 let type_names: Vec<&str> = manifest
282 .structs
283 .iter()
284 .map(|s| s.name.as_str())
285 .chain(manifest.enums.iter().map(|e| e.name.as_str()))
286 .collect();
287
288 if type_names.is_empty() {
290 emit!(
291 out,
292 "import type {{ Procedures }} from \"{types_import_path}\";\n"
293 );
294 emit!(out, "export type {{ Procedures }};\n");
295 } else {
296 let types_csv = type_names.join(", ");
297 emit!(
298 out,
299 "import type {{ Procedures, {types_csv} }} from \"{types_import_path}\";\n"
300 );
301 emit!(out, "export type {{ Procedures, {types_csv} }};\n");
302 }
303
304 emit!(out, "{ERROR_CLASS}\n");
306
307 emit!(out, "{REQUEST_CONTEXT_INTERFACE}\n");
309 emit!(out, "{RESPONSE_CONTEXT_INTERFACE}\n");
310 emit!(out, "{ERROR_CONTEXT_INTERFACE}\n");
311
312 emit!(out, "{RETRY_POLICY_INTERFACE}\n");
314
315 emit!(out, "{CONFIG_INTERFACE}\n");
317
318 emit!(out, "{CALL_OPTIONS_INTERFACE}\n");
320
321 generate_procedure_timeouts(manifest, &mut out);
323
324 generate_idempotent_mutations(manifest, &mut out);
326
327 emit!(out, "{FETCH_HELPER}\n");
329
330 let has_queries = manifest
332 .procedures
333 .iter()
334 .any(|p| p.kind == ProcedureKind::Query);
335 if has_queries {
336 emit!(out, "{DEDUP_KEY_FN}\n");
337 emit!(out, "{WRAP_WITH_SIGNAL_FN}\n");
338 }
339
340 let has_streams = manifest
342 .procedures
343 .iter()
344 .any(|p| p.kind == ProcedureKind::Stream);
345 if has_streams {
346 emit!(out, "{STREAM_HELPER}\n");
347 }
348
349 generate_type_helpers(&mut out);
351 out.push('\n');
352
353 generate_client_factory(manifest, preserve_docs, &mut out);
355
356 out
357}
358
359fn generate_procedure_timeouts(manifest: &Manifest, out: &mut String) {
361 let entries: Vec<_> = manifest
362 .procedures
363 .iter()
364 .filter_map(|p| p.timeout_ms.map(|ms| format!(" \"{}\": {}", p.name, ms)))
365 .collect();
366
367 if entries.is_empty() {
368 emit!(
369 out,
370 "const PROCEDURE_TIMEOUTS: Record<string, number> = {{}};\n"
371 );
372 } else {
373 emit!(out, "const PROCEDURE_TIMEOUTS: Record<string, number> = {{");
374 for entry in &entries {
375 emit!(out, "{entry},");
376 }
377 emit!(out, "}};\n");
378 }
379}
380
381fn generate_idempotent_mutations(manifest: &Manifest, out: &mut String) {
383 let names: Vec<_> = manifest
384 .procedures
385 .iter()
386 .filter(|p| p.idempotent)
387 .map(|p| format!("\"{}\"", p.name))
388 .collect();
389
390 if names.is_empty() {
391 emit!(
392 out,
393 "const IDEMPOTENT_MUTATIONS: Set<string> = new Set();\n"
394 );
395 } else {
396 emit!(
397 out,
398 "const IDEMPOTENT_MUTATIONS: Set<string> = new Set([{}]);\n",
399 names.join(", ")
400 );
401 }
402}
403
404fn generate_type_helpers(out: &mut String) {
406 emit!(out, "type QueryKey = keyof Procedures[\"queries\"];");
407 emit!(out, "type MutationKey = keyof Procedures[\"mutations\"];");
408 emit!(out, "type StreamKey = keyof Procedures[\"streams\"];");
409 emit!(
410 out,
411 "type QueryInput<K extends QueryKey> = Procedures[\"queries\"][K][\"input\"];"
412 );
413 emit!(
414 out,
415 "type QueryOutput<K extends QueryKey> = Procedures[\"queries\"][K][\"output\"];"
416 );
417 emit!(
418 out,
419 "type MutationInput<K extends MutationKey> = Procedures[\"mutations\"][K][\"input\"];"
420 );
421 emit!(
422 out,
423 "type MutationOutput<K extends MutationKey> = Procedures[\"mutations\"][K][\"output\"];"
424 );
425 emit!(
426 out,
427 "type StreamInput<K extends StreamKey> = Procedures[\"streams\"][K][\"input\"];"
428 );
429 emit!(
430 out,
431 "type StreamOutput<K extends StreamKey> = Procedures[\"streams\"][K][\"output\"];"
432 );
433}
434
435fn generate_client_factory(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
437 let queries: Vec<_> = manifest
438 .procedures
439 .iter()
440 .filter(|p| p.kind == ProcedureKind::Query)
441 .collect();
442 let mutations: Vec<_> = manifest
443 .procedures
444 .iter()
445 .filter(|p| p.kind == ProcedureKind::Mutation)
446 .collect();
447 let streams: Vec<_> = manifest
448 .procedures
449 .iter()
450 .filter(|p| p.kind == ProcedureKind::Stream)
451 .collect();
452 let has_queries = !queries.is_empty();
453 let has_mutations = !mutations.is_empty();
454 let has_streams = !streams.is_empty();
455
456 let void_queries: Vec<_> = queries.iter().filter(|p| is_void_input(p)).collect();
458 let non_void_queries: Vec<_> = queries.iter().filter(|p| !is_void_input(p)).collect();
459 let void_mutations: Vec<_> = mutations.iter().filter(|p| is_void_input(p)).collect();
460 let non_void_mutations: Vec<_> = mutations.iter().filter(|p| !is_void_input(p)).collect();
461
462 let void_streams: Vec<_> = streams.iter().filter(|p| is_void_input(p)).collect();
463 let non_void_streams: Vec<_> = streams.iter().filter(|p| !is_void_input(p)).collect();
464
465 let query_mixed = !void_queries.is_empty() && !non_void_queries.is_empty();
466 let mutation_mixed = !void_mutations.is_empty() && !non_void_mutations.is_empty();
467 let stream_mixed = !void_streams.is_empty() && !non_void_streams.is_empty();
468
469 if query_mixed {
471 let names: Vec<_> = void_queries
472 .iter()
473 .map(|p| format!("\"{}\"", p.name))
474 .collect();
475 emit!(
476 out,
477 "const VOID_QUERIES: Set<string> = new Set([{}]);",
478 names.join(", ")
479 );
480 out.push('\n');
481 }
482 if mutation_mixed {
483 let names: Vec<_> = void_mutations
484 .iter()
485 .map(|p| format!("\"{}\"", p.name))
486 .collect();
487 emit!(
488 out,
489 "const VOID_MUTATIONS: Set<string> = new Set([{}]);",
490 names.join(", ")
491 );
492 out.push('\n');
493 }
494 if stream_mixed {
495 let names: Vec<_> = void_streams
496 .iter()
497 .map(|p| format!("\"{}\"", p.name))
498 .collect();
499 emit!(
500 out,
501 "const VOID_STREAMS: Set<string> = new Set([{}]);",
502 names.join(", ")
503 );
504 out.push('\n');
505 }
506
507 emit!(out, "export interface RpcClient {{");
509
510 if has_queries {
511 generate_query_overloads(manifest, preserve_docs, out);
512 }
513
514 if has_mutations {
515 if has_queries {
516 out.push('\n');
517 }
518 generate_mutation_overloads(manifest, preserve_docs, out);
519 }
520
521 if has_streams {
522 if has_queries || has_mutations {
523 out.push('\n');
524 }
525 generate_stream_overloads(manifest, preserve_docs, out);
526 }
527
528 emit!(out, "}}");
529 out.push('\n');
530
531 emit!(
533 out,
534 "export function createRpcClient(config: RpcClientConfig): RpcClient {{"
535 );
536
537 if has_queries {
538 emit!(
539 out,
540 " const inflight = new Map<string, Promise<unknown>>();\n"
541 );
542 }
543
544 emit!(out, " return {{");
545
546 if has_queries {
547 emit!(
548 out,
549 " query(key: QueryKey, ...args: unknown[]): Promise<unknown> {{"
550 );
551
552 if query_mixed {
554 emit!(out, " let input: unknown;");
555 emit!(out, " let callOptions: CallOptions | undefined;");
556 emit!(out, " if (VOID_QUERIES.has(key)) {{");
557 emit!(out, " input = undefined;");
558 emit!(
559 out,
560 " callOptions = args[0] as CallOptions | undefined;"
561 );
562 emit!(out, " }} else {{");
563 emit!(out, " input = args[0];");
564 emit!(
565 out,
566 " callOptions = args[1] as CallOptions | undefined;"
567 );
568 emit!(out, " }}");
569 } else if !void_queries.is_empty() {
570 emit!(out, " const input = undefined;");
571 emit!(
572 out,
573 " const callOptions = args[0] as CallOptions | undefined;"
574 );
575 } else {
576 emit!(out, " const input = args[0];");
577 emit!(
578 out,
579 " const callOptions = args[1] as CallOptions | undefined;"
580 );
581 }
582
583 emit!(
585 out,
586 " const shouldDedupe = callOptions?.dedupe ?? config.dedupe ?? true;"
587 );
588 emit!(out, " if (shouldDedupe) {{");
589 emit!(out, " const k = dedupKey(key, input, config);");
590 emit!(out, " const existing = inflight.get(k);");
591 emit!(
592 out,
593 " if (existing) return wrapWithSignal(existing, callOptions?.signal);"
594 );
595 emit!(
596 out,
597 " const promise = rpcFetch(config, \"GET\", key, input, callOptions)"
598 );
599 emit!(out, " .finally(() => inflight.delete(k));");
600 emit!(out, " inflight.set(k, promise);");
601 emit!(
602 out,
603 " return wrapWithSignal(promise, callOptions?.signal);"
604 );
605 emit!(out, " }}");
606 emit!(
607 out,
608 " return rpcFetch(config, \"GET\", key, input, callOptions);"
609 );
610 emit!(out, " }},");
611 }
612
613 if has_mutations {
614 emit!(
615 out,
616 " mutate(key: MutationKey, ...args: unknown[]): Promise<unknown> {{"
617 );
618 if mutation_mixed {
619 emit!(out, " if (VOID_MUTATIONS.has(key)) {{");
621 emit!(
622 out,
623 " return rpcFetch(config, \"POST\", key, undefined, args[0] as CallOptions | undefined);"
624 );
625 emit!(out, " }}");
626 emit!(
627 out,
628 " return rpcFetch(config, \"POST\", key, args[0], args[1] as CallOptions | undefined);"
629 );
630 } else if !void_mutations.is_empty() {
631 emit!(
633 out,
634 " return rpcFetch(config, \"POST\", key, undefined, args[0] as CallOptions | undefined);"
635 );
636 } else {
637 emit!(
639 out,
640 " return rpcFetch(config, \"POST\", key, args[0], args[1] as CallOptions | undefined);"
641 );
642 }
643 emit!(out, " }},");
644 }
645
646 if has_streams {
647 emit!(
648 out,
649 " stream(key: StreamKey, ...args: unknown[]): AsyncGenerator<unknown> {{"
650 );
651 if stream_mixed {
652 emit!(out, " if (VOID_STREAMS.has(key)) {{");
653 emit!(
654 out,
655 " return rpcStream(config, key, undefined, args[0] as CallOptions | undefined);"
656 );
657 emit!(out, " }}");
658 emit!(
659 out,
660 " return rpcStream(config, key, args[0], args[1] as CallOptions | undefined);"
661 );
662 } else if !void_streams.is_empty() {
663 emit!(
664 out,
665 " return rpcStream(config, key, undefined, args[0] as CallOptions | undefined);"
666 );
667 } else {
668 emit!(
669 out,
670 " return rpcStream(config, key, args[0], args[1] as CallOptions | undefined);"
671 );
672 }
673 emit!(out, " }},");
674 }
675
676 emit!(out, " }} as RpcClient;");
677 emit!(out, "}}");
678}
679
680fn generate_query_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
682 let (void_queries, non_void_queries): (Vec<_>, Vec<_>) = manifest
683 .procedures
684 .iter()
685 .filter(|p| p.kind == ProcedureKind::Query)
686 .partition(|p| is_void_input(p));
687
688 for proc in &void_queries {
690 if preserve_docs && let Some(doc) = &proc.docs {
691 emit_jsdoc(doc, " ", out);
692 }
693 let output_ts = proc
694 .output
695 .as_ref()
696 .map(rust_type_to_ts)
697 .unwrap_or_else(|| "void".to_string());
698 emit!(
699 out,
700 " query(key: \"{}\"): Promise<{}>;",
701 proc.name,
702 output_ts,
703 );
704 emit!(
705 out,
706 " query(key: \"{}\", options: CallOptions): Promise<{}>;",
707 proc.name,
708 output_ts,
709 );
710 }
711
712 for proc in &non_void_queries {
714 if preserve_docs && let Some(doc) = &proc.docs {
715 emit_jsdoc(doc, " ", out);
716 }
717 let input_ts = proc
718 .input
719 .as_ref()
720 .map(rust_type_to_ts)
721 .unwrap_or_else(|| "void".to_string());
722 let output_ts = proc
723 .output
724 .as_ref()
725 .map(rust_type_to_ts)
726 .unwrap_or_else(|| "void".to_string());
727 emit!(
728 out,
729 " query(key: \"{}\", input: {}): Promise<{}>;",
730 proc.name,
731 input_ts,
732 output_ts,
733 );
734 emit!(
735 out,
736 " query(key: \"{}\", input: {}, options: CallOptions): Promise<{}>;",
737 proc.name,
738 input_ts,
739 output_ts,
740 );
741 }
742}
743
744fn generate_mutation_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
746 let (void_mutations, non_void_mutations): (Vec<_>, Vec<_>) = manifest
747 .procedures
748 .iter()
749 .filter(|p| p.kind == ProcedureKind::Mutation)
750 .partition(|p| is_void_input(p));
751
752 for proc in &void_mutations {
754 if preserve_docs && let Some(doc) = &proc.docs {
755 emit_jsdoc(doc, " ", out);
756 }
757 let output_ts = proc
758 .output
759 .as_ref()
760 .map(rust_type_to_ts)
761 .unwrap_or_else(|| "void".to_string());
762 emit!(
763 out,
764 " mutate(key: \"{}\"): Promise<{}>;",
765 proc.name,
766 output_ts,
767 );
768 emit!(
769 out,
770 " mutate(key: \"{}\", options: CallOptions): Promise<{}>;",
771 proc.name,
772 output_ts,
773 );
774 }
775
776 for proc in &non_void_mutations {
778 if preserve_docs && let Some(doc) = &proc.docs {
779 emit_jsdoc(doc, " ", out);
780 }
781 let input_ts = proc
782 .input
783 .as_ref()
784 .map(rust_type_to_ts)
785 .unwrap_or_else(|| "void".to_string());
786 let output_ts = proc
787 .output
788 .as_ref()
789 .map(rust_type_to_ts)
790 .unwrap_or_else(|| "void".to_string());
791 emit!(
792 out,
793 " mutate(key: \"{}\", input: {}): Promise<{}>;",
794 proc.name,
795 input_ts,
796 output_ts,
797 );
798 emit!(
799 out,
800 " mutate(key: \"{}\", input: {}, options: CallOptions): Promise<{}>;",
801 proc.name,
802 input_ts,
803 output_ts,
804 );
805 }
806}
807
808fn generate_stream_overloads(manifest: &Manifest, preserve_docs: bool, out: &mut String) {
810 let (void_streams, non_void_streams): (Vec<_>, Vec<_>) = manifest
811 .procedures
812 .iter()
813 .filter(|p| p.kind == ProcedureKind::Stream)
814 .partition(|p| is_void_input(p));
815
816 for proc in &void_streams {
818 if preserve_docs && let Some(doc) = &proc.docs {
819 emit_jsdoc(doc, " ", out);
820 }
821 let output_ts = proc
822 .output
823 .as_ref()
824 .map(rust_type_to_ts)
825 .unwrap_or_else(|| "void".to_string());
826 emit!(
827 out,
828 " stream(key: \"{}\"): AsyncGenerator<{}>;",
829 proc.name,
830 output_ts,
831 );
832 emit!(
833 out,
834 " stream(key: \"{}\", options: CallOptions): AsyncGenerator<{}>;",
835 proc.name,
836 output_ts,
837 );
838 }
839
840 for proc in &non_void_streams {
842 if preserve_docs && let Some(doc) = &proc.docs {
843 emit_jsdoc(doc, " ", out);
844 }
845 let input_ts = proc
846 .input
847 .as_ref()
848 .map(rust_type_to_ts)
849 .unwrap_or_else(|| "void".to_string());
850 let output_ts = proc
851 .output
852 .as_ref()
853 .map(rust_type_to_ts)
854 .unwrap_or_else(|| "void".to_string());
855 emit!(
856 out,
857 " stream(key: \"{}\", input: {}): AsyncGenerator<{}>;",
858 proc.name,
859 input_ts,
860 output_ts,
861 );
862 emit!(
863 out,
864 " stream(key: \"{}\", input: {}, options: CallOptions): AsyncGenerator<{}>;",
865 proc.name,
866 input_ts,
867 output_ts,
868 );
869 }
870}