Skip to content

Commit 25c359f

Browse files
committed
chore(bridge): Refactor TS-Core bridge
1 parent c6e7c88 commit 25c359f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+5197
-3820
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"temporal.replayerEntrypoint": "packages/test/src/debug-replayer.ts",
3-
"rust-analyzer.linkedProjects": ["./packages/core-bridge/Cargo.toml"]
3+
"rust-analyzer.linkedProjects": ["./packages/core-bridge/Cargo.toml"],
4+
"vim.textwidth": 100
45
}

packages/core-bridge/Cargo.lock

+28-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core-bridge/Cargo.toml

+5-3
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ lto = true
1717
incremental = false
1818

1919
[dependencies]
20+
anyhow = "1.0"
2021
async-trait = "0.1.83"
22+
bridge-macros = { path = "bridge-macros" }
2123
futures = { version = "0.3", features = ["executor"] }
2224
log = "0.4"
23-
neon = { version = "1.0.0", default-features = false, features = ["napi-6", "futures"] }
25+
neon = { version = "1.0.0", default-features = false, features = ["napi-6", "futures", "sys"] }
2426
opentelemetry = "0.24"
2527
parking_lot = "0.12"
2628
prost = "0.13"
2729
prost-types = "0.13"
2830
serde_json = "1.0"
29-
tokio = "1.13"
30-
once_cell = "1.19"
3131
temporal-sdk-core = { version = "*", path = "./sdk-core/core", features = ["ephemeral-server"] }
3232
temporal-client = { version = "*", path = "./sdk-core/client" }
33+
thiserror = "2"
34+
tokio = "1.13"
3335
tokio-stream = "0.1"

packages/core-bridge/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22

33
[![NPM](https://img.shields.io/npm/v/@temporalio/core-bridge?style=for-the-badge)](https://www.npmjs.com/package/@temporalio/core-bridge)
44

5+
56
Part of [Temporal](https://temporal.io)'s [TypeScript SDK](https://docs.temporal.io/typescript/introduction/).
7+
8+
> [!CAUTION] **Important**: This package is not intended to be used directly.
9+
> Any API provided by this package is internal and subject to change without notice.
10+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "bridge-macros"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
syn = { version = "2.0", features = ["full", "extra-traits"] }
11+
quote = "1.0"
12+
proc-macro2 = "1.0"
13+
convert_case = "0.6"
14+
15+
[dev-dependencies]
16+
temporal-sdk-typescript-bridge = { path = ".." }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use convert_case::{Case, Casing};
2+
use proc_macro2::TokenStream;
3+
use quote::quote;
4+
use syn::{DeriveInput, FieldsNamed, FieldsUnnamed};
5+
6+
pub fn derive_tryfromjs_struct(input: &DeriveInput, data: &syn::DataStruct) -> TokenStream {
7+
let struct_ident = &input.ident;
8+
let generics = &input.generics;
9+
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
10+
11+
let field_conversions = if let syn::Fields::Named(ref fields) = data.fields {
12+
field_conversions_for_named_fields(fields)
13+
} else {
14+
panic!("Only named fields are supported")
15+
};
16+
17+
let expanded = quote! {
18+
impl #impl_generics crate::helpers::TryFromJs for #struct_ident #ty_generics #where_clause {
19+
fn try_from_js<'cx, 'b>(cx: &mut impl neon::prelude::Context<'cx>, js_value: neon::prelude::Handle<'b, neon::prelude::JsValue>) -> crate::helpers::BridgeResult<Self> {
20+
use crate::helpers::{ObjectExt as _, FunctionContextExt as _, AppendFieldContext as _};
21+
22+
let obj = js_value.downcast::<neon::prelude::JsObject, _>(cx)?;
23+
Ok(Self {
24+
#(#field_conversions),*
25+
})
26+
}
27+
}
28+
};
29+
30+
// eprintln!(
31+
// "=== struct {} ===\n{}\n======",
32+
// struct_ident.to_string(),
33+
// expanded
34+
// );
35+
36+
TokenStream::from(expanded)
37+
}
38+
39+
pub fn derive_tryfromjs_enum(input: &DeriveInput, data: &syn::DataEnum) -> TokenStream {
40+
let enum_ident = &input.ident;
41+
let enum_name = enum_ident.to_string();
42+
let variants = &data.variants;
43+
let generics = &input.generics;
44+
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
45+
46+
let variant_conversions = variants.iter().map(|v| {
47+
let variant_ident = &v.ident;
48+
let discriminant = variant_ident.to_string().to_case(Case::Kebab);
49+
let js_discriminant = variant_ident.to_string().to_case(Case::Camel);
50+
51+
match &v.fields {
52+
syn::Fields::Unit => {
53+
// e.g. "otel" => Ok(MetricsExporter::Otel)
54+
quote! {
55+
#discriminant => Ok(#enum_ident::#variant_ident),
56+
}
57+
}
58+
syn::Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
59+
if unnamed.len() != 1 {
60+
panic!("Enum variant must have a single unnamed field that implements the TryFromJs trait");
61+
}
62+
let ty = unnamed.first().map(|f| f.ty.clone()).unwrap();
63+
match ty {
64+
syn::Type::Path(path) => {
65+
// Example output:
66+
//
67+
// "otel" => {
68+
// <OtelConfig>::try_from_js(cx, js_value).field("otel").map(MetricsExporter::Otel)
69+
// }
70+
quote! {
71+
#discriminant => {
72+
<#path>::try_from_js(cx, js_value).field(&#js_discriminant).map(#enum_ident::#variant_ident)
73+
},
74+
}
75+
},
76+
_ => panic!("Enum variant must have a single unnamed field that implements the TryFromJs trait"),
77+
}
78+
}
79+
syn::Fields::Named(fields) => {
80+
// Example output:
81+
//
82+
// "console" => Ok((|| {
83+
// Ok::<LogExporter, BridgeError>(LogExporter::Console {
84+
// filter: { obj.get_property_into(cx, "filter")? },
85+
// })
86+
// })()
87+
// .field(format!("type={}", type_str).as_str())?),
88+
//
89+
// The inner closure is required so that we can use the `field` method on the result.
90+
// An alternative would be to do that at the field level, but then that concern would
91+
// spill into the field_conversions_for_named_fields function, which is used in
92+
// other places.
93+
let field_conversions = field_conversions_for_named_fields(fields);
94+
quote! {
95+
#discriminant => Ok(( || {
96+
Ok::<#enum_ident #ty_generics, crate::helpers::BridgeError>(#enum_ident::#variant_ident {
97+
#(#field_conversions),*
98+
})
99+
})()
100+
.field(&#js_discriminant)?),
101+
}
102+
}
103+
}
104+
});
105+
106+
let expanded = quote! {
107+
impl #impl_generics crate::helpers::TryFromJs for #enum_ident #ty_generics #where_clause {
108+
fn try_from_js<'cx, 'b>(cx: &mut impl neon::prelude::Context<'cx>, js_value: neon::prelude::Handle<'b, neon::prelude::JsValue>) -> crate::helpers::BridgeResult<Self> {
109+
use crate::helpers::{ObjectExt as _, FunctionContextExt as _, AppendFieldContext as _};
110+
111+
let obj = js_value.downcast::<neon::prelude::JsObject, _>(cx)?;
112+
let type_str: String = obj.get_property_into(cx, "type")?;
113+
114+
match type_str.as_str() {
115+
#(#variant_conversions)*
116+
_ => Err(crate::helpers::BridgeError::InvalidVariant {
117+
enum_name: #enum_name.to_string(),
118+
variant: type_str,
119+
}),
120+
}
121+
}
122+
}
123+
};
124+
125+
// eprintln!(
126+
// "=== enum {} ===\n{}\n======",
127+
// enum_ident.to_string(),
128+
// expanded
129+
// );
130+
131+
TokenStream::from(expanded)
132+
}
133+
134+
fn field_conversions_for_named_fields(
135+
fields: &FieldsNamed,
136+
) -> impl Iterator<Item = TokenStream> + '_ {
137+
fields.named.iter().map(|f| {
138+
let field_ident = f
139+
.ident
140+
.as_ref()
141+
.expect("FieldsNamed.named must have an identifier");
142+
let js_name = field_ident.to_string().to_case(Case::Camel);
143+
144+
quote! {
145+
#field_ident: {
146+
obj.get_property_into(cx, #js_name)?
147+
}
148+
}
149+
})
150+
}

0 commit comments

Comments
 (0)