Skip to content
86 changes: 64 additions & 22 deletions packages/common/src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,43 @@ export interface Metric {
* The description of the metric, if any.
*/
description?: string;

/**
* The kind of the metric (e.g. `counter`, `histogram`, `gauge`).
*/
kind: MetricKind;

/**
* The type of value recorded by the metric. Either `int` or `float`.
*/
valueType: NumericMetricValueType;
}

/**
* Tags to be attached to some metrics.
*
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
export type MetricTags = Record<string, string | number | boolean>;

/**
* Type of numerical values recorded by a metric.
*
* Note that this represents the _configuration_ of the metric; however, since JavaScript doesn't
* actually have different representation for integers and floats, the actual value type is always
* a JS 'number'.
*
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
export type NumericMetricValueType = 'int' | 'float';

/**
* The kind of a metric.
*
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
export type MetricKind = 'counter' | 'histogram' | 'gauge';

/**
* A metric that supports adding values as a counter.
*
Expand All @@ -91,6 +126,9 @@ export interface MetricCounter extends Metric {
* @param tags Tags to append to existing tags.
*/
withTags(tags: MetricTags): MetricCounter;

kind: 'counter';
valueType: 'int';
}

/**
Expand All @@ -99,11 +137,6 @@ export interface MetricCounter extends Metric {
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
export interface MetricHistogram extends Metric {
/**
* The type of value to record. Either `int` or `float`.
*/
valueType: NumericMetricValueType;

/**
* Record the given value on the histogram.
*
Expand All @@ -120,6 +153,8 @@ export interface MetricHistogram extends Metric {
* @param tags Tags to append to existing tags.
*/
withTags(tags: MetricTags): MetricHistogram;

kind: 'histogram';
}

/**
Expand All @@ -128,11 +163,6 @@ export interface MetricHistogram extends Metric {
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
export interface MetricGauge extends Metric {
/**
* The type of value to set. Either `int` or `float`.
*/
valueType: NumericMetricValueType;

/**
* Set the given value on the gauge.
*
Expand All @@ -147,16 +177,9 @@ export interface MetricGauge extends Metric {
* @param tags Tags to append to existing tags.
*/
withTags(tags: MetricTags): MetricGauge;
}

/**
* Tags to be attached to some metrics.
*
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
export type MetricTags = Record<string, string | number | boolean>;

export type NumericMetricValueType = 'int' | 'float';
kind: 'gauge';
}

////////////////////////////////////////////////////////////////////////////////////////////////////

Expand All @@ -170,6 +193,9 @@ class NoopMetricMeter implements MetricMeter {
unit,
description,

kind: 'counter',
valueType: 'int',

add(_value, _extraTags) {},

withTags(_extraTags) {
Expand All @@ -186,10 +212,12 @@ class NoopMetricMeter implements MetricMeter {
): MetricHistogram {
return {
name,
valueType,
unit,
description,

kind: 'histogram',
valueType,

record(_value, _extraTags) {},

withTags(_extraTags) {
Expand All @@ -198,13 +226,20 @@ class NoopMetricMeter implements MetricMeter {
};
}

createGauge(name: string, valueType?: NumericMetricValueType, unit?: string, description?: string): MetricGauge {
createGauge(
name: string,
valueType: NumericMetricValueType = 'int',
unit?: string,
description?: string
): MetricGauge {
return {
name,
valueType: valueType ?? 'int',
unit,
description,

kind: 'gauge',
valueType,

set(_value, _extraTags) {},

withTags(_extraTags) {
Expand Down Expand Up @@ -300,6 +335,9 @@ export class MetricMeterWithComposedTags implements MetricMeter {
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
class MetricCounterWithComposedTags implements MetricCounter {
public readonly kind = 'counter';
public readonly valueType = 'int';

constructor(
private parentCounter: MetricCounter,
private contributors: MetricTagsOrFunc[]
Expand Down Expand Up @@ -332,6 +370,8 @@ class MetricCounterWithComposedTags implements MetricCounter {
* @experimental The Metric API is an experimental feature and may be subject to change.
*/
class MetricHistogramWithComposedTags implements MetricHistogram {
public readonly kind = 'histogram';

constructor(
private parentHistogram: MetricHistogram,
private contributors: MetricTagsOrFunc[]
Expand Down Expand Up @@ -369,6 +409,8 @@ class MetricHistogramWithComposedTags implements MetricHistogram {
* @hidden
*/
class MetricGaugeWithComposedTags implements MetricGauge {
public readonly kind = 'gauge';

constructor(
private parentGauge: MetricGauge,
private contributors: MetricTagsOrFunc[]
Expand Down
40 changes: 40 additions & 0 deletions packages/core-bridge/bridge-macros/src/derive_tryintojs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,46 @@ pub fn derive_tryintojs_struct(input: &DeriveInput, data: &syn::DataStruct) -> T
}

pub fn derive_tryintojs_enum(input: &DeriveInput, data: &syn::DataEnum) -> TokenStream {
let all_unit = data
.variants
.iter()
.all(|v| matches!(v.fields, syn::Fields::Unit));
if all_unit {
derive_tryintojs_enum_as_string(input, data)
} else {
derive_tryintojs_enum_as_objects(input, data)
}
}

fn derive_tryintojs_enum_as_string(input: &DeriveInput, data: &syn::DataEnum) -> TokenStream {
let enum_ident = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

let variant_conversions = data.variants.iter().map(|v| {
let variant_ident = &v.ident;
let js_discriminant = variant_ident.to_string().to_case(Case::Camel);
quote! {
#enum_ident::#variant_ident => cx.string(#js_discriminant)
}
});

let expanded = quote! {
impl #impl_generics crate::helpers::TryIntoJs for #enum_ident #ty_generics #where_clause {
type Output = neon::types::JsString;

fn try_into_js<'a>(self, cx: &mut impl neon::prelude::Context<'a>) -> neon::result::JsResult<'a, Self::Output> {
Ok(match self {
#(#variant_conversions),*
})
}
}
};

TokenStream::from(expanded)
}

fn derive_tryintojs_enum_as_objects(input: &DeriveInput, data: &syn::DataEnum) -> TokenStream {
let enum_ident = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Expand Down
90 changes: 88 additions & 2 deletions packages/core-bridge/src/helpers/try_into_js.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::{
sync::Arc,
sync::{Arc, Mutex},
time::{Duration, SystemTime, UNIX_EPOCH},
};

use neon::{
object::Object,
prelude::Context,
prelude::{Context, Root},
result::JsResult,
types::{
JsArray, JsBigInt, JsBoolean, JsBuffer, JsNumber, JsString, JsUndefined, JsValue, Value,
Expand Down Expand Up @@ -36,6 +36,13 @@ impl TryIntoJs for u32 {
}
}

impl TryIntoJs for f64 {
type Output = JsNumber;
fn try_into_js<'a>(self, cx: &mut impl Context<'a>) -> JsResult<'a, JsNumber> {
Ok(cx.number(self))
}
}

impl TryIntoJs for String {
type Output = JsString;
fn try_into_js<'a>(self, cx: &mut impl Context<'a>) -> JsResult<'a, JsString> {
Expand Down Expand Up @@ -127,3 +134,82 @@ impl<T0: TryIntoJs, T1: TryIntoJs> TryIntoJs for (T0, T1) {
Ok(array)
}
}

/// A handle that wraps another `TryIntoJs` type, memoizing the converted JavaScript value.
/// That is, the value is converted to JavaScript only once, and then the same JavaScript value
/// is returned for subsequent calls. This notably ensures that the value sent to the JS side
/// is exactly the same object every time (i.e. `===` comparison is true).
#[derive(Clone, Debug)]
pub struct MemoizedHandle<T: TryIntoJs + Clone + std::fmt::Debug>
where
T::Output: std::fmt::Debug,
{
internal: Arc<Mutex<MemoizedInternal<T>>>,
}

#[derive(Debug)]
enum MemoizedInternal<T: TryIntoJs + Clone + std::fmt::Debug> {
Pending(T),
Rooted(Root<T::Output>),
}

impl<T: TryIntoJs + Clone + std::fmt::Debug> MemoizedHandle<T>
where
T::Output: Object + std::fmt::Debug,
{
pub fn new(value: T) -> Self {
Self {
internal: Arc::new(Mutex::new(MemoizedInternal::Pending(value))),
}
}
}

impl<T: TryIntoJs + Clone + std::fmt::Debug> TryIntoJs for MemoizedHandle<T>
where
T::Output: Object + std::fmt::Debug,
{
type Output = T::Output;
fn try_into_js<'cx>(self, cx: &mut impl Context<'cx>) -> JsResult<'cx, T::Output> {
let mut guard = self.internal.lock().expect("MemoizedHandle lock");
match *guard {
MemoizedInternal::Pending(ref value) => {
let rooted_value = value.clone().try_into_js(cx)?.root(cx);
let js_value = rooted_value.to_inner(cx);
*guard = MemoizedInternal::Rooted(rooted_value);
Ok(js_value)
}
MemoizedInternal::Rooted(ref handle) => Ok(handle.to_inner(cx)),
}
}
}

/// To avoid some recuring error patterns when crossing the JS bridge, we normally translate
/// `Option<T>` to `T | null` on the JS side. This however implies extra code on the JS side
/// to check for `null` and convert to `undefined` as appropriate. This generally poses no
/// problem, as manipulation of objects on the JS side is anyway desirable for other reasons.
///
/// In rare cases, however, this extra manipulation may not be desirable. For example, when
/// passing buffered metrics to the JS Side, we want to preserve object identity. Modifying
/// objects on the JS side would either break object identity or introduce unnecessary overhead.
///
/// For those rare cases, this newtype wrapper to indicate that an option property should be
/// translated to `undefined` on the JS side, rather than `null`.
#[derive(Clone, Debug)]
pub struct OptionAsUndefined<T: TryIntoJs + Clone + std::fmt::Debug>(Option<T>);

impl<T: TryIntoJs + Clone + std::fmt::Debug> From<Option<T>> for OptionAsUndefined<T> {
fn from(value: Option<T>) -> Self {
Self(value)
}
}

impl<T: TryIntoJs + Clone + std::fmt::Debug> TryIntoJs for OptionAsUndefined<T> {
type Output = JsValue;
fn try_into_js<'a>(self, cx: &mut impl Context<'a>) -> JsResult<'a, JsValue> {
if let Some(value) = self.0 {
Ok(value.try_into_js(cx)?.upcast())
} else {
Ok(cx.undefined().upcast())
}
}
}
Loading
Loading