spacetimedb_codegen/
typescript.rs

1use crate::indent_scope;
2use crate::util::{is_reducer_invokable, iter_reducers, iter_tables, iter_types, iter_unique_cols};
3
4use super::util::{collect_case, print_auto_generated_file_comment, type_ref_name};
5
6use std::collections::BTreeSet;
7use std::fmt::{self, Write};
8use std::ops::Deref;
9
10use convert_case::{Case, Casing};
11use spacetimedb_lib::sats::AlgebraicTypeRef;
12use spacetimedb_schema::def::{ModuleDef, ReducerDef, ScopedTypeName, TableDef, TypeDef};
13use spacetimedb_schema::identifier::Identifier;
14use spacetimedb_schema::schema::{Schema, TableSchema};
15use spacetimedb_schema::type_for_generate::{AlgebraicTypeDef, AlgebraicTypeUse, PrimitiveType};
16
17use super::code_indenter::{CodeIndenter, Indenter};
18use super::Lang;
19use spacetimedb_lib::version::spacetimedb_lib_version;
20
21type Imports = BTreeSet<AlgebraicTypeRef>;
22
23const INDENT: &str = "  ";
24
25pub struct TypeScript;
26
27impl Lang for TypeScript {
28    fn table_filename(
29        &self,
30        _module: &spacetimedb_schema::def::ModuleDef,
31        table: &spacetimedb_schema::def::TableDef,
32    ) -> String {
33        table_module_name(&table.name) + ".ts"
34    }
35
36    fn type_filename(&self, type_name: &ScopedTypeName) -> String {
37        type_module_name(type_name) + ".ts"
38    }
39
40    fn reducer_filename(&self, reducer_name: &Identifier) -> String {
41        reducer_module_name(reducer_name) + ".ts"
42    }
43
44    fn generate_type(&self, module: &ModuleDef, typ: &TypeDef) -> String {
45        // TODO(cloutiertyler): I do think TypeScript does support namespaces:
46        // https://www.typescriptlang.org/docs/handbook/namespaces.html
47        let type_name = collect_case(Case::Pascal, typ.name.name_segments());
48
49        let mut output = CodeIndenter::new(String::new(), INDENT);
50        let out = &mut output;
51
52        print_file_header(out);
53
54        match &module.typespace_for_generate()[typ.ty] {
55            AlgebraicTypeDef::Product(product) => {
56                gen_and_print_imports(module, out, &product.elements, &[typ.ty]);
57                define_namespace_and_object_type_for_product(module, out, &type_name, &product.elements);
58            }
59            AlgebraicTypeDef::Sum(sum) => {
60                gen_and_print_imports(module, out, &sum.variants, &[typ.ty]);
61                define_namespace_and_types_for_sum(module, out, &type_name, &sum.variants);
62            }
63            AlgebraicTypeDef::PlainEnum(plain_enum) => {
64                let variants = plain_enum
65                    .variants
66                    .iter()
67                    .cloned()
68                    .map(|var| (var, AlgebraicTypeUse::Unit))
69                    .collect::<Vec<_>>();
70                define_namespace_and_types_for_sum(module, out, &type_name, &variants);
71            }
72        }
73        out.newline();
74
75        output.into_inner()
76    }
77
78    fn generate_table(&self, module: &ModuleDef, table: &TableDef) -> String {
79        let schema = TableSchema::from_module_def(module, table, (), 0.into())
80            .validated()
81            .expect("Failed to generate table due to validation errors");
82
83        let mut output = CodeIndenter::new(String::new(), INDENT);
84        let out = &mut output;
85
86        print_file_header(out);
87
88        let type_ref = table.product_type_ref;
89        let row_type = type_ref_name(module, type_ref);
90        let row_type_module = type_ref_module_name(module, type_ref);
91
92        writeln!(out, "import {{ {row_type} }} from \"./{row_type_module}\";");
93
94        let product_def = module.typespace_for_generate()[type_ref].as_product().unwrap();
95
96        // Import the types of all fields.
97        // We only need to import fields which have indices or unique constraints,
98        // but it's easier to just import all of 'em, since we have `// @ts-nocheck` anyway.
99        gen_and_print_imports(
100            module,
101            out,
102            &product_def.elements,
103            &[], // No need to skip any imports; we're not defining a type, so there's no chance of circular imports.
104        );
105
106        writeln!(
107            out,
108            "import {{ type EventContext, type Reducer, RemoteReducers, RemoteTables }} from \".\";"
109        );
110
111        let table_name = table.name.deref();
112        let table_name_pascalcase = table.name.deref().to_case(Case::Pascal);
113        let table_handle = table_name_pascalcase.clone() + "TableHandle";
114        let accessor_method = table_method_name(&table.name);
115
116        writeln!(out);
117
118        write!(
119            out,
120            "/**
121 * Table handle for the table `{table_name}`.
122 *
123 * Obtain a handle from the [`{accessor_method}`] property on [`RemoteTables`],
124 * like `ctx.db.{accessor_method}`.
125 *
126 * Users are encouraged not to explicitly reference this type,
127 * but to directly chain method calls,
128 * like `ctx.db.{accessor_method}.on_insert(...)`.
129 */
130export class {table_handle} {{
131"
132        );
133        out.indent(1);
134        writeln!(out, "tableCache: TableCache<{row_type}>;");
135        writeln!(out);
136        writeln!(out, "constructor(tableCache: TableCache<{row_type}>) {{");
137        out.with_indent(|out| writeln!(out, "this.tableCache = tableCache;"));
138        writeln!(out, "}}");
139        writeln!(out);
140        writeln!(out, "count(): number {{");
141        out.with_indent(|out| {
142            writeln!(out, "return this.tableCache.count();");
143        });
144        writeln!(out, "}}");
145        writeln!(out);
146        writeln!(out, "iter(): Iterable<{row_type}> {{");
147        out.with_indent(|out| {
148            writeln!(out, "return this.tableCache.iter();");
149        });
150        writeln!(out, "}}");
151
152        for (unique_field_ident, unique_field_type_use) in
153            iter_unique_cols(module.typespace_for_generate(), &schema, product_def)
154        {
155            let unique_field_name = unique_field_ident.deref().to_case(Case::Camel);
156            let unique_field_name_pascalcase = unique_field_name.to_case(Case::Pascal);
157
158            let unique_constraint = table_name_pascalcase.clone() + &unique_field_name_pascalcase + "Unique";
159            let unique_field_type = type_name(module, unique_field_type_use);
160
161            writeln!(
162                out,
163                "/**
164 * Access to the `{unique_field_name}` unique index on the table `{table_name}`,
165 * which allows point queries on the field of the same name
166 * via the [`{unique_constraint}.find`] method.
167 *
168 * Users are encouraged not to explicitly reference this type,
169 * but to directly chain method calls,
170 * like `ctx.db.{accessor_method}.{unique_field_name}().find(...)`.
171 *
172 * Get a handle on the `{unique_field_name}` unique index on the table `{table_name}`.
173 */"
174            );
175            writeln!(out, "{unique_field_name} = {{");
176            out.with_indent(|out| {
177                writeln!(
178                    out,
179                    "// Find the subscribed row whose `{unique_field_name}` column value is equal to `col_val`,"
180                );
181                writeln!(out, "// if such a row is present in the client cache.");
182                writeln!(
183                    out,
184                    "find: (col_val: {unique_field_type}): {row_type} | undefined => {{"
185                );
186                out.with_indent(|out| {
187                    writeln!(out, "for (let row of this.tableCache.iter()) {{");
188                    out.with_indent(|out| {
189                        writeln!(out, "if (deepEqual(row.{unique_field_name}, col_val)) {{");
190                        out.with_indent(|out| {
191                            writeln!(out, "return row;");
192                        });
193                        writeln!(out, "}}");
194                    });
195                    writeln!(out, "}}");
196                });
197                writeln!(out, "}},");
198            });
199            writeln!(out, "}};");
200        }
201
202        writeln!(out);
203
204        // TODO: expose non-unique indices.
205
206        writeln!(
207            out,
208            "onInsert = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
209{INDENT}return this.tableCache.onInsert(cb);
210}}
211
212removeOnInsert = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
213{INDENT}return this.tableCache.removeOnInsert(cb);
214}}
215
216onDelete = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
217{INDENT}return this.tableCache.onDelete(cb);
218}}
219
220removeOnDelete = (cb: (ctx: EventContext, row: {row_type}) => void) => {{
221{INDENT}return this.tableCache.removeOnDelete(cb);
222}}"
223        );
224
225        if schema.pk().is_some() {
226            write!(
227                out,
228                "
229// Updates are only defined for tables with primary keys.
230onUpdate = (cb: (ctx: EventContext, oldRow: {row_type}, newRow: {row_type}) => void) => {{
231{INDENT}return this.tableCache.onUpdate(cb);
232}}
233
234removeOnUpdate = (cb: (ctx: EventContext, onRow: {row_type}, newRow: {row_type}) => void) => {{
235{INDENT}return this.tableCache.removeOnUpdate(cb);
236}}"
237            );
238        }
239        out.dedent(1);
240
241        writeln!(out, "}}");
242        output.into_inner()
243    }
244
245    fn generate_reducer(&self, module: &ModuleDef, reducer: &ReducerDef) -> String {
246        let mut output = CodeIndenter::new(String::new(), INDENT);
247        let out = &mut output;
248
249        print_file_header(out);
250
251        out.newline();
252
253        gen_and_print_imports(
254            module,
255            out,
256            &reducer.params_for_generate.elements,
257            // No need to skip any imports; we're not emitting a type that other modules can import.
258            &[],
259        );
260
261        let args_type = reducer_args_type_name(&reducer.name);
262
263        define_namespace_and_object_type_for_product(module, out, &args_type, &reducer.params_for_generate.elements);
264
265        output.into_inner()
266    }
267
268    fn generate_globals(&self, module: &ModuleDef) -> Vec<(String, String)> {
269        let mut output = CodeIndenter::new(String::new(), INDENT);
270        let out = &mut output;
271
272        print_file_header(out);
273
274        out.newline();
275
276        writeln!(out, "// Import and reexport all reducer arg types");
277        for reducer in iter_reducers(module) {
278            let reducer_name = &reducer.name;
279            let reducer_module_name = reducer_module_name(reducer_name) + ".ts";
280            let args_type = reducer_args_type_name(&reducer.name);
281            writeln!(out, "import {{ {args_type} }} from \"./{reducer_module_name}\";");
282            writeln!(out, "export {{ {args_type} }};");
283        }
284
285        writeln!(out);
286        writeln!(out, "// Import and reexport all table handle types");
287        for table in iter_tables(module) {
288            let table_name = &table.name;
289            let table_module_name = table_module_name(table_name) + ".ts";
290            let table_name_pascalcase = table.name.deref().to_case(Case::Pascal);
291            let table_handle = table_name_pascalcase.clone() + "TableHandle";
292            writeln!(out, "import {{ {table_handle} }} from \"./{table_module_name}\";");
293            writeln!(out, "export {{ {table_handle} }};");
294        }
295
296        writeln!(out);
297        writeln!(out, "// Import and reexport all types");
298        for ty in iter_types(module) {
299            let type_name = collect_case(Case::Pascal, ty.name.name_segments());
300            let type_module_name = type_module_name(&ty.name) + ".ts";
301            writeln!(out, "import {{ {type_name} }} from \"./{type_module_name}\";");
302            writeln!(out, "export {{ {type_name} }};");
303        }
304
305        out.newline();
306
307        // Define SpacetimeModule
308        writeln!(out, "const REMOTE_MODULE = {{");
309        out.indent(1);
310        writeln!(out, "tables: {{");
311        out.indent(1);
312        for table in iter_tables(module) {
313            let type_ref = table.product_type_ref;
314            let row_type = type_ref_name(module, type_ref);
315            let schema = TableSchema::from_module_def(module, table, (), 0.into())
316                .validated()
317                .expect("Failed to generate table due to validation errors");
318            writeln!(out, "{}: {{", table.name);
319            out.indent(1);
320            writeln!(out, "tableName: \"{}\",", table.name);
321            writeln!(out, "rowType: {row_type}.getTypeScriptAlgebraicType(),");
322            if let Some(pk) = schema.pk() {
323                // This is left here so we can release the codegen change before releasing a new
324                // version of the SDK.
325                //
326                // Eventually we can remove this and only generate use the `primaryKeyInfo` field.
327                writeln!(out, "primaryKey: \"{}\",", pk.col_name.to_string().to_case(Case::Camel));
328
329                writeln!(out, "primaryKeyInfo: {{");
330                out.indent(1);
331                writeln!(out, "colName: \"{}\",", pk.col_name.to_string().to_case(Case::Camel));
332                writeln!(
333                    out,
334                    "colType: {row_type}.getTypeScriptAlgebraicType().product.elements[{}].algebraicType,",
335                    pk.col_pos.0
336                );
337                out.dedent(1);
338                writeln!(out, "}},");
339            }
340            out.dedent(1);
341            writeln!(out, "}},");
342        }
343        out.dedent(1);
344        writeln!(out, "}},");
345        writeln!(out, "reducers: {{");
346        out.indent(1);
347        for reducer in iter_reducers(module) {
348            writeln!(out, "{}: {{", reducer.name);
349            out.indent(1);
350            writeln!(out, "reducerName: \"{}\",", reducer.name);
351            writeln!(
352                out,
353                "argsType: {args_type}.getTypeScriptAlgebraicType(),",
354                args_type = reducer_args_type_name(&reducer.name)
355            );
356            out.dedent(1);
357            writeln!(out, "}},");
358        }
359        out.dedent(1);
360        writeln!(out, "}},");
361        writeln!(out, "versionInfo: {{");
362        out.indent(1);
363        writeln!(out, "cliVersion: \"{}\",", spacetimedb_lib_version());
364        out.dedent(1);
365        writeln!(out, "}},");
366        writeln!(
367            out,
368            "// Constructors which are used by the DbConnectionImpl to
369// extract type information from the generated RemoteModule.
370//
371// NOTE: This is not strictly necessary for `eventContextConstructor` because
372// all we do is build a TypeScript object which we could have done inside the
373// SDK, but if in the future we wanted to create a class this would be
374// necessary because classes have methods, so we'll keep it.
375eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {{
376  return {{
377    ...(imp as DbConnection),
378    event
379  }}
380}},
381dbViewConstructor: (imp: DbConnectionImpl) => {{
382  return new RemoteTables(imp);
383}},
384reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {{
385  return new RemoteReducers(imp, setReducerFlags);
386}},
387setReducerFlagsConstructor: () => {{
388  return new SetReducerFlags();
389}}"
390        );
391        out.dedent(1);
392        writeln!(out, "}}");
393
394        // Define `type Reducer` enum.
395        writeln!(out);
396        print_reducer_enum_defn(module, out);
397
398        out.newline();
399
400        print_remote_reducers(module, out);
401
402        out.newline();
403
404        print_set_reducer_flags(module, out);
405
406        out.newline();
407
408        print_remote_tables(module, out);
409
410        out.newline();
411
412        print_subscription_builder(module, out);
413
414        out.newline();
415
416        print_db_connection(module, out);
417
418        out.newline();
419
420        writeln!(
421            out,
422            "export type EventContext = EventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags, Reducer>;"
423        );
424
425        writeln!(
426            out,
427            "export type ReducerEventContext = ReducerEventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags, Reducer>;"
428        );
429
430        writeln!(
431            out,
432            "export type SubscriptionEventContext = SubscriptionEventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags>;"
433        );
434
435        writeln!(
436            out,
437            "export type ErrorContext = ErrorContextInterface<RemoteTables, RemoteReducers, SetReducerFlags>;"
438        );
439
440        vec![("index.ts".to_string(), (output.into_inner()))]
441    }
442}
443
444fn print_remote_reducers(module: &ModuleDef, out: &mut Indenter) {
445    writeln!(out, "export class RemoteReducers {{");
446    out.indent(1);
447    writeln!(
448        out,
449        "constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {{}}"
450    );
451    out.newline();
452
453    for reducer in iter_reducers(module) {
454        // The reducer argument names and types as `ident: ty, ident: ty, ident: ty`,
455        // and the argument names as `ident, ident, ident`
456        // for passing to function call and struct literal expressions.
457        let mut arg_list = "".to_string();
458        let mut arg_name_list = "".to_string();
459        for (arg_ident, arg_ty) in &reducer.params_for_generate.elements[..] {
460            let arg_name = arg_ident.deref().to_case(Case::Camel);
461            arg_name_list += &arg_name;
462            arg_list += &arg_name;
463            arg_list += ": ";
464            write_type(module, &mut arg_list, arg_ty, None).unwrap();
465            arg_list += ", ";
466            arg_name_list += ", ";
467        }
468        let arg_list = arg_list.trim_end_matches(", ");
469        let arg_name_list = arg_name_list.trim_end_matches(", ");
470
471        let reducer_name = &reducer.name;
472
473        if is_reducer_invokable(reducer) {
474            let reducer_function_name = reducer_function_name(reducer);
475            let reducer_variant = reducer_variant_name(&reducer.name);
476            if reducer.params_for_generate.elements.is_empty() {
477                writeln!(out, "{reducer_function_name}() {{");
478                out.with_indent(|out| {
479                    writeln!(
480                        out,
481                        "this.connection.callReducer(\"{reducer_name}\", new Uint8Array(0), this.setCallReducerFlags.{reducer_function_name}Flags);"
482                    );
483                });
484            } else {
485                writeln!(out, "{reducer_function_name}({arg_list}) {{");
486                out.with_indent(|out| {
487                    writeln!(out, "const __args = {{ {arg_name_list} }};");
488                    writeln!(out, "let __writer = new BinaryWriter(1024);");
489                    writeln!(
490                        out,
491                        "{reducer_variant}.getTypeScriptAlgebraicType().serialize(__writer, __args);"
492                    );
493                    writeln!(out, "let __argsBuffer = __writer.getBuffer();");
494                    writeln!(out, "this.connection.callReducer(\"{reducer_name}\", __argsBuffer, this.setCallReducerFlags.{reducer_function_name}Flags);");
495                });
496            }
497            writeln!(out, "}}");
498            out.newline();
499        }
500
501        let arg_list_padded = if arg_list.is_empty() {
502            String::new()
503        } else {
504            format!(", {arg_list}")
505        };
506        let reducer_name_pascal = reducer_name.deref().to_case(Case::Pascal);
507        writeln!(
508            out,
509            "on{reducer_name_pascal}(callback: (ctx: ReducerEventContext{arg_list_padded}) => void) {{"
510        );
511        out.indent(1);
512        writeln!(out, "this.connection.onReducer(\"{reducer_name}\", callback);");
513        out.dedent(1);
514        writeln!(out, "}}");
515        out.newline();
516        writeln!(
517            out,
518            "removeOn{reducer_name_pascal}(callback: (ctx: ReducerEventContext{arg_list_padded}) => void) {{"
519        );
520        out.indent(1);
521        writeln!(out, "this.connection.offReducer(\"{reducer_name}\", callback);");
522        out.dedent(1);
523        writeln!(out, "}}");
524        out.newline();
525    }
526
527    out.dedent(1);
528    writeln!(out, "}}");
529}
530
531fn print_set_reducer_flags(module: &ModuleDef, out: &mut Indenter) {
532    writeln!(out, "export class SetReducerFlags {{");
533    out.indent(1);
534
535    for reducer in iter_reducers(module).filter(|r| is_reducer_invokable(r)) {
536        let reducer_function_name = reducer_function_name(reducer);
537        writeln!(out, "{reducer_function_name}Flags: CallReducerFlags = 'FullUpdate';");
538        writeln!(out, "{reducer_function_name}(flags: CallReducerFlags) {{");
539        out.with_indent(|out| {
540            writeln!(out, "this.{reducer_function_name}Flags = flags;");
541        });
542        writeln!(out, "}}");
543        out.newline();
544    }
545
546    out.dedent(1);
547    writeln!(out, "}}");
548}
549
550fn print_remote_tables(module: &ModuleDef, out: &mut Indenter) {
551    writeln!(out, "export class RemoteTables {{");
552    out.indent(1);
553    writeln!(out, "constructor(private connection: DbConnectionImpl) {{}}");
554
555    for table in iter_tables(module) {
556        writeln!(out);
557        let table_name = table.name.deref();
558        let table_name_pascalcase = table.name.deref().to_case(Case::Pascal);
559        let table_name_camelcase = table.name.deref().to_case(Case::Camel);
560        let table_handle = table_name_pascalcase.clone() + "TableHandle";
561        let type_ref = table.product_type_ref;
562        let row_type = type_ref_name(module, type_ref);
563        writeln!(out, "get {table_name_camelcase}(): {table_handle} {{");
564        out.with_indent(|out| {
565            writeln!(
566                out,
567                "return new {table_handle}(this.connection.clientCache.getOrCreateTable<{row_type}>(REMOTE_MODULE.tables.{table_name}));"
568            );
569        });
570        writeln!(out, "}}");
571    }
572
573    out.dedent(1);
574    writeln!(out, "}}");
575}
576
577fn print_subscription_builder(_module: &ModuleDef, out: &mut Indenter) {
578    writeln!(
579        out,
580        "export class SubscriptionBuilder extends SubscriptionBuilderImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{ }}"
581    );
582}
583
584fn print_db_connection(_module: &ModuleDef, out: &mut Indenter) {
585    writeln!(
586        out,
587        "export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{"
588    );
589    out.indent(1);
590    writeln!(
591        out,
592        "static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {{"
593    );
594    out.indent(1);
595    writeln!(
596        out,
597        "return new DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext>(REMOTE_MODULE, (imp: DbConnectionImpl) => imp as DbConnection);"
598    );
599    out.dedent(1);
600    writeln!(out, "}}");
601    writeln!(out, "subscriptionBuilder = (): SubscriptionBuilder => {{");
602    out.indent(1);
603    writeln!(out, "return new SubscriptionBuilder(this);");
604    out.dedent(1);
605    writeln!(out, "}}");
606    out.dedent(1);
607    writeln!(out, "}}");
608}
609
610fn print_reducer_enum_defn(module: &ModuleDef, out: &mut Indenter) {
611    writeln!(out, "// A type representing all the possible variants of a reducer.");
612    writeln!(out, "export type Reducer = never");
613    for reducer in iter_reducers(module) {
614        writeln!(
615            out,
616            "| {{ name: \"{}\", args: {} }}",
617            reducer_variant_name(&reducer.name),
618            reducer_args_type_name(&reducer.name)
619        );
620    }
621    writeln!(out, ";");
622}
623
624fn print_spacetimedb_imports(out: &mut Indenter) {
625    let mut types = [
626        "AlgebraicType",
627        "ProductType",
628        "ProductTypeElement",
629        "SumType",
630        "SumTypeVariant",
631        "AlgebraicValue",
632        "Identity",
633        "ConnectionId",
634        "Timestamp",
635        "TimeDuration",
636        "DbConnectionBuilder",
637        "TableCache",
638        "BinaryWriter",
639        "type CallReducerFlags",
640        "type EventContextInterface",
641        "type ReducerEventContextInterface",
642        "type SubscriptionEventContextInterface",
643        "type ErrorContextInterface",
644        "SubscriptionBuilderImpl",
645        "BinaryReader",
646        "DbConnectionImpl",
647        "type DbContext",
648        "type Event",
649        "deepEqual",
650    ];
651    types.sort();
652    writeln!(out, "import {{");
653    out.indent(1);
654    for ty in &types {
655        writeln!(out, "{ty},");
656    }
657    out.dedent(1);
658    writeln!(out, "}} from \"@clockworklabs/spacetimedb-sdk\";");
659}
660
661fn print_file_header(output: &mut Indenter) {
662    print_auto_generated_file_comment(output);
663    print_lint_suppression(output);
664    print_spacetimedb_imports(output);
665}
666
667fn print_lint_suppression(output: &mut Indenter) {
668    writeln!(output, "/* eslint-disable */");
669    writeln!(output, "/* tslint:disable */");
670    writeln!(output, "// @ts-nocheck");
671}
672
673fn write_get_algebraic_type_for_product(
674    module: &ModuleDef,
675    out: &mut Indenter,
676    elements: &[(Identifier, AlgebraicTypeUse)],
677) {
678    writeln!(
679        out,
680        "/**
681* A function which returns this type represented as an AlgebraicType.
682* This function is derived from the AlgebraicType used to generate this type.
683*/"
684    );
685    writeln!(out, "export function getTypeScriptAlgebraicType(): AlgebraicType {{");
686    {
687        out.indent(1);
688        write!(out, "return ");
689        convert_product_type(module, out, elements, "__");
690        writeln!(out, ";");
691        out.dedent(1);
692    }
693    writeln!(out, "}}");
694}
695
696fn define_namespace_and_object_type_for_product(
697    module: &ModuleDef,
698    out: &mut Indenter,
699    name: &str,
700    elements: &[(Identifier, AlgebraicTypeUse)],
701) {
702    write!(out, "export type {name} = {{");
703    if elements.is_empty() {
704        writeln!(out, "}};");
705    } else {
706        writeln!(out);
707        out.with_indent(|out| write_arglist_no_delimiters(module, out, elements, None, true).unwrap());
708        writeln!(out, "}};");
709    }
710
711    out.newline();
712
713    writeln!(
714        out,
715        "/**
716 * A namespace for generated helper functions.
717 */"
718    );
719    writeln!(out, "export namespace {name} {{");
720    out.indent(1);
721    write_get_algebraic_type_for_product(module, out, elements);
722    writeln!(out);
723
724    writeln!(
725        out,
726        "export function serialize(writer: BinaryWriter, value: {name}): void {{"
727    );
728    out.indent(1);
729    writeln!(out, "{name}.getTypeScriptAlgebraicType().serialize(writer, value);");
730    out.dedent(1);
731    writeln!(out, "}}");
732    writeln!(out);
733
734    writeln!(out, "export function deserialize(reader: BinaryReader): {name} {{");
735    out.indent(1);
736    writeln!(out, "return {name}.getTypeScriptAlgebraicType().deserialize(reader);");
737    out.dedent(1);
738    writeln!(out, "}}");
739    writeln!(out);
740
741    out.dedent(1);
742    writeln!(out, "}}");
743
744    out.newline();
745}
746
747fn write_arglist_no_delimiters(
748    module: &ModuleDef,
749    out: &mut impl Write,
750    elements: &[(Identifier, AlgebraicTypeUse)],
751    prefix: Option<&str>,
752    convert_case: bool,
753) -> anyhow::Result<()> {
754    for (ident, ty) in elements {
755        if let Some(prefix) = prefix {
756            write!(out, "{prefix} ")?;
757        }
758
759        let name = if convert_case {
760            ident.deref().to_case(Case::Camel)
761        } else {
762            ident.deref().into()
763        };
764
765        write!(out, "{name}: ")?;
766        write_type(module, out, ty, Some("__"))?;
767        writeln!(out, ",")?;
768    }
769
770    Ok(())
771}
772
773fn write_sum_variant_type(module: &ModuleDef, out: &mut Indenter, ident: &Identifier, ty: &AlgebraicTypeUse) {
774    let name = ident.deref().to_case(Case::Pascal);
775    write!(out, "export type {name} = ");
776
777    // If the contained type is the unit type, i.e. this variant has no members,
778    // write only the tag.
779    // ```
780    // { tag: "Foo" }
781    // ```
782    write!(out, "{{ ");
783    write!(out, "tag: \"{name}\"");
784
785    // If the contained type is not the unit type, write the tag and the value.
786    // ```
787    // { tag: "Bar", value: Bar }
788    // { tag: "Bar", value: number }
789    // { tag: "Bar", value: string }
790    // ```
791    // Note you could alternatively do:
792    // ```
793    // { tag: "Bar" } & Bar
794    // ```
795    // for non-primitive types but that doesn't extend to primitives.
796    // Another alternative would be to name the value field the same as the tag field, but lowercased
797    // ```
798    // { tag: "Bar", bar: Bar }
799    // { tag: "Bar", bar: number }
800    // { tag: "Bar", bar: string }
801    // ```
802    // but this is a departure from our previous convention and is not much different.
803    if !matches!(ty, AlgebraicTypeUse::Unit) {
804        write!(out, ", value: ");
805        write_type(module, out, ty, Some("__")).unwrap();
806    }
807
808    writeln!(out, " }};");
809}
810
811fn write_variant_types(module: &ModuleDef, out: &mut Indenter, variants: &[(Identifier, AlgebraicTypeUse)]) {
812    // Write all the variant types.
813    for (ident, ty) in variants {
814        write_sum_variant_type(module, out, ident, ty);
815    }
816}
817
818fn write_variant_constructors(
819    module: &ModuleDef,
820    out: &mut Indenter,
821    name: &str,
822    variants: &[(Identifier, AlgebraicTypeUse)],
823) {
824    // Write all the variant constructors.
825    // Write all of the variant constructors.
826    for (ident, ty) in variants {
827        if matches!(ty, AlgebraicTypeUse::Unit) {
828            // If the variant has no members, we can export a simple object.
829            // ```
830            // export const Foo = { tag: "Foo" };
831            // ```
832            write!(out, "export const {ident} = ");
833            writeln!(out, "{{ tag: \"{ident}\" }};");
834            continue;
835        }
836        let variant_name = ident.deref().to_case(Case::Pascal);
837        write!(out, "export const {variant_name} = (value: ");
838        write_type(module, out, ty, Some("__")).unwrap();
839        writeln!(out, "): {name} => ({{ tag: \"{variant_name}\", value }});");
840    }
841}
842
843fn write_get_algebraic_type_for_sum(
844    module: &ModuleDef,
845    out: &mut Indenter,
846    variants: &[(Identifier, AlgebraicTypeUse)],
847) {
848    writeln!(out, "export function getTypeScriptAlgebraicType(): AlgebraicType {{");
849    {
850        indent_scope!(out);
851        write!(out, "return ");
852        convert_sum_type(module, &mut out, variants, "__");
853        writeln!(out, ";");
854    }
855    writeln!(out, "}}");
856}
857
858fn define_namespace_and_types_for_sum(
859    module: &ModuleDef,
860    out: &mut Indenter,
861    name: &str,
862    variants: &[(Identifier, AlgebraicTypeUse)],
863) {
864    writeln!(out, "// A namespace for generated variants and helper functions.");
865    writeln!(out, "export namespace {name} {{");
866    out.indent(1);
867
868    // Write all of the variant types.
869    writeln!(
870        out,
871        "// These are the generated variant types for each variant of the tagged union.
872// One type is generated per variant and will be used in the `value` field of
873// the tagged union."
874    );
875    write_variant_types(module, out, variants);
876    writeln!(out);
877
878    // Write all of the variant constructors.
879    writeln!(
880        out,
881        "// Helper functions for constructing each variant of the tagged union.
882// ```
883// const foo = Foo.A(42);
884// assert!(foo.tag === \"A\");
885// assert!(foo.value === 42);
886// ```"
887    );
888    write_variant_constructors(module, out, name, variants);
889    writeln!(out);
890
891    // Write the function that generates the algebraic type.
892    write_get_algebraic_type_for_sum(module, out, variants);
893    writeln!(out);
894
895    writeln!(
896        out,
897        "export function serialize(writer: BinaryWriter, value: {name}): void {{
898    {name}.getTypeScriptAlgebraicType().serialize(writer, value);
899}}"
900    );
901    writeln!(out);
902
903    writeln!(
904        out,
905        "export function deserialize(reader: BinaryReader): {name} {{
906    return {name}.getTypeScriptAlgebraicType().deserialize(reader);
907}}"
908    );
909    writeln!(out);
910
911    out.dedent(1);
912
913    writeln!(out, "}}");
914    out.newline();
915
916    writeln!(out, "// The tagged union or sum type for the algebraic type `{name}`.");
917    write!(out, "export type {name} = ");
918
919    let names = variants
920        .iter()
921        .map(|(ident, _)| format!("{name}.{}", ident.deref().to_case(Case::Pascal)))
922        .collect::<Vec<String>>()
923        .join(" | ");
924
925    writeln!(out, "{names};");
926    out.newline();
927
928    writeln!(out, "export default {name};");
929}
930
931fn type_ref_module_name(module: &ModuleDef, type_ref: AlgebraicTypeRef) -> String {
932    let (name, _) = module.type_def_from_ref(type_ref).unwrap();
933    type_module_name(name)
934}
935
936fn type_module_name(type_name: &ScopedTypeName) -> String {
937    collect_case(Case::Snake, type_name.name_segments()) + "_type"
938}
939
940fn table_module_name(table_name: &Identifier) -> String {
941    table_name.deref().to_case(Case::Snake) + "_table"
942}
943
944fn table_method_name(table_name: &Identifier) -> String {
945    table_name.deref().to_case(Case::Camel)
946}
947
948fn reducer_args_type_name(reducer_name: &Identifier) -> String {
949    reducer_name.deref().to_case(Case::Pascal)
950}
951
952fn reducer_variant_name(reducer_name: &Identifier) -> String {
953    reducer_name.deref().to_case(Case::Pascal)
954}
955
956fn reducer_module_name(reducer_name: &Identifier) -> String {
957    reducer_name.deref().to_case(Case::Snake) + "_reducer"
958}
959
960fn reducer_function_name(reducer: &ReducerDef) -> String {
961    reducer.name.deref().to_case(Case::Camel)
962}
963
964pub fn type_name(module: &ModuleDef, ty: &AlgebraicTypeUse) -> String {
965    let mut s = String::new();
966    write_type(module, &mut s, ty, None).unwrap();
967    s
968}
969
970// This should return true if we should wrap the type in parentheses when it is the element type of
971// an array. This is needed if the type has a `|` in it, e.g. `Option<T>` or `Foo | Bar`, since
972// without parens, `Foo | Bar[]` would be parsed as `Foo | (Bar[])`.
973fn needs_parens_within_array(ty: &AlgebraicTypeUse) -> bool {
974    match ty {
975        AlgebraicTypeUse::Unit
976        | AlgebraicTypeUse::Never
977        | AlgebraicTypeUse::Identity
978        | AlgebraicTypeUse::ConnectionId
979        | AlgebraicTypeUse::Timestamp
980        | AlgebraicTypeUse::TimeDuration
981        | AlgebraicTypeUse::Primitive(_)
982        | AlgebraicTypeUse::Array(_)
983        | AlgebraicTypeUse::Ref(_) // We use the type name for these.
984        | AlgebraicTypeUse::String => {
985            false
986        }
987        AlgebraicTypeUse::ScheduleAt | AlgebraicTypeUse::Option(_) => {
988            true
989        }
990    }
991}
992
993pub fn write_type<W: Write>(
994    module: &ModuleDef,
995    out: &mut W,
996    ty: &AlgebraicTypeUse,
997    ref_prefix: Option<&str>,
998) -> fmt::Result {
999    match ty {
1000        AlgebraicTypeUse::Unit => write!(out, "void")?,
1001        AlgebraicTypeUse::Never => write!(out, "never")?,
1002        AlgebraicTypeUse::Identity => write!(out, "Identity")?,
1003        AlgebraicTypeUse::ConnectionId => write!(out, "ConnectionId")?,
1004        AlgebraicTypeUse::Timestamp => write!(out, "Timestamp")?,
1005        AlgebraicTypeUse::TimeDuration => write!(out, "TimeDuration")?,
1006        AlgebraicTypeUse::ScheduleAt => write!(
1007            out,
1008            "{{ tag: \"Interval\", value: TimeDuration }} | {{ tag: \"Time\", value: Timestamp }}"
1009        )?,
1010        AlgebraicTypeUse::Option(inner_ty) => {
1011            write_type(module, out, inner_ty, ref_prefix)?;
1012            write!(out, " | undefined")?;
1013        }
1014        AlgebraicTypeUse::Primitive(prim) => match prim {
1015            PrimitiveType::Bool => write!(out, "boolean")?,
1016            PrimitiveType::I8 => write!(out, "number")?,
1017            PrimitiveType::U8 => write!(out, "number")?,
1018            PrimitiveType::I16 => write!(out, "number")?,
1019            PrimitiveType::U16 => write!(out, "number")?,
1020            PrimitiveType::I32 => write!(out, "number")?,
1021            PrimitiveType::U32 => write!(out, "number")?,
1022            PrimitiveType::I64 => write!(out, "bigint")?,
1023            PrimitiveType::U64 => write!(out, "bigint")?,
1024            PrimitiveType::I128 => write!(out, "bigint")?,
1025            PrimitiveType::U128 => write!(out, "bigint")?,
1026            PrimitiveType::I256 => write!(out, "bigint")?,
1027            PrimitiveType::U256 => write!(out, "bigint")?,
1028            PrimitiveType::F32 => write!(out, "number")?,
1029            PrimitiveType::F64 => write!(out, "number")?,
1030        },
1031        AlgebraicTypeUse::String => write!(out, "string")?,
1032        AlgebraicTypeUse::Array(elem_ty) => {
1033            if matches!(&**elem_ty, AlgebraicTypeUse::Primitive(PrimitiveType::U8)) {
1034                return write!(out, "Uint8Array");
1035            }
1036            let needs_parens = needs_parens_within_array(elem_ty);
1037            // We wrap the inner type in parentheses to avoid ambiguity with the [] binding.
1038            if needs_parens {
1039                write!(out, "(")?;
1040            }
1041            write_type(module, out, elem_ty, ref_prefix)?;
1042            if needs_parens {
1043                write!(out, ")")?;
1044            }
1045            write!(out, "[]")?;
1046        }
1047        AlgebraicTypeUse::Ref(r) => {
1048            if let Some(prefix) = ref_prefix {
1049                write!(out, "{prefix}")?;
1050            }
1051            write!(out, "{}", type_ref_name(module, *r))?;
1052        }
1053    }
1054    Ok(())
1055}
1056
1057fn convert_algebraic_type<'a>(
1058    module: &'a ModuleDef,
1059    out: &mut Indenter,
1060    ty: &'a AlgebraicTypeUse,
1061    ref_prefix: &'a str,
1062) {
1063    match ty {
1064        AlgebraicTypeUse::ScheduleAt => write!(out, "AlgebraicType.createScheduleAtType()"),
1065        AlgebraicTypeUse::Identity => write!(out, "AlgebraicType.createIdentityType()"),
1066        AlgebraicTypeUse::ConnectionId => write!(out, "AlgebraicType.createConnectionIdType()"),
1067        AlgebraicTypeUse::Timestamp => write!(out, "AlgebraicType.createTimestampType()"),
1068        AlgebraicTypeUse::TimeDuration => write!(out, "AlgebraicType.createTimeDurationType()"),
1069        AlgebraicTypeUse::Option(inner_ty) => {
1070            write!(out, "AlgebraicType.createOptionType(");
1071            convert_algebraic_type(module, out, inner_ty, ref_prefix);
1072            write!(out, ")");
1073        }
1074        AlgebraicTypeUse::Array(ty) => {
1075            write!(out, "AlgebraicType.createArrayType(");
1076            convert_algebraic_type(module, out, ty, ref_prefix);
1077            write!(out, ")");
1078        }
1079        AlgebraicTypeUse::Ref(r) => write!(
1080            out,
1081            "{ref_prefix}{}.getTypeScriptAlgebraicType()",
1082            type_ref_name(module, *r)
1083        ),
1084        AlgebraicTypeUse::Primitive(prim) => {
1085            write!(out, "AlgebraicType.create{prim:?}Type()");
1086        }
1087        AlgebraicTypeUse::Unit => write!(out, "AlgebraicType.createProductType([])"),
1088        AlgebraicTypeUse::Never => unimplemented!(),
1089        AlgebraicTypeUse::String => write!(out, "AlgebraicType.createStringType()"),
1090    }
1091}
1092
1093fn convert_sum_type<'a>(
1094    module: &'a ModuleDef,
1095    out: &mut Indenter,
1096    variants: &'a [(Identifier, AlgebraicTypeUse)],
1097    ref_prefix: &'a str,
1098) {
1099    writeln!(out, "AlgebraicType.createSumType([");
1100    out.indent(1);
1101    for (ident, ty) in variants {
1102        write!(out, "new SumTypeVariant(\"{ident}\", ",);
1103        convert_algebraic_type(module, out, ty, ref_prefix);
1104        writeln!(out, "),");
1105    }
1106    out.dedent(1);
1107    write!(out, "])")
1108}
1109
1110fn convert_product_type<'a>(
1111    module: &'a ModuleDef,
1112    out: &mut Indenter,
1113    elements: &'a [(Identifier, AlgebraicTypeUse)],
1114    ref_prefix: &'a str,
1115) {
1116    writeln!(out, "AlgebraicType.createProductType([");
1117    out.indent(1);
1118    for (ident, ty) in elements {
1119        write!(
1120            out,
1121            "new ProductTypeElement(\"{}\", ",
1122            ident.deref().to_case(Case::Camel)
1123        );
1124        convert_algebraic_type(module, out, ty, ref_prefix);
1125        writeln!(out, "),");
1126    }
1127    out.dedent(1);
1128    write!(out, "])")
1129}
1130
1131/// Print imports for each of the `imports`.
1132fn print_imports(module: &ModuleDef, out: &mut Indenter, imports: Imports) {
1133    for typeref in imports {
1134        let module_name = type_ref_module_name(module, typeref);
1135        let type_name = type_ref_name(module, typeref);
1136        writeln!(
1137            out,
1138            "import {{ {type_name} as __{type_name} }} from \"./{module_name}\";"
1139        );
1140    }
1141}
1142
1143/// Use `search_function` on `roots` to detect required imports, then print them with `print_imports`.
1144///
1145/// `this_file` is passed and excluded for the case of recursive types:
1146/// without it, the definition for a type like `struct Foo { foos: Vec<Foo> }`
1147/// would attempt to include `import { Foo } from "./foo"`.
1148fn gen_and_print_imports(
1149    module: &ModuleDef,
1150    out: &mut Indenter,
1151    roots: &[(Identifier, AlgebraicTypeUse)],
1152    dont_import: &[AlgebraicTypeRef],
1153) {
1154    let mut imports = BTreeSet::new();
1155
1156    for (_, ty) in roots {
1157        ty.for_each_ref(|r| {
1158            imports.insert(r);
1159        });
1160    }
1161    for skip in dont_import {
1162        imports.remove(skip);
1163    }
1164    let len = imports.len();
1165
1166    print_imports(module, out, imports);
1167
1168    if len > 0 {
1169        out.newline();
1170    }
1171}
1172
1173// const RESERVED_KEYWORDS: [&str; 36] = [
1174//     "break",
1175//     "case",
1176//     "catch",
1177//     "class",
1178//     "const",
1179//     "continue",
1180//     "debugger",
1181//     "default",
1182//     "delete",
1183//     "do",
1184//     "else",
1185//     "enum",
1186//     "export",
1187//     "extends",
1188//     "false",
1189//     "finally",
1190//     "for",
1191//     "function",
1192//     "if",
1193//     "import",
1194//     "in",
1195//     "instanceof",
1196//     "new",
1197//     "null",
1198//     "return",
1199//     "super",
1200//     "switch",
1201//     "this",
1202//     "throw",
1203//     "true",
1204//     "try",
1205//     "typeof",
1206//     "var",
1207//     "void",
1208//     "while",
1209//     "with",
1210// ];
1211
1212// fn typescript_field_name(field_name: String) -> String {
1213//     if RESERVED_KEYWORDS
1214//         .into_iter()
1215//         .map(String::from)
1216//         .collect::<Vec<String>>()
1217//         .contains(&field_name)
1218//     {
1219//         return format!("_{field_name}");
1220//     }
1221
1222//     field_name
1223// }