From 41af71567b349c3f27f2ddaa610a40055298095f Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Fri, 26 Sep 2025 17:01:19 -0500 Subject: [PATCH 1/2] Allow TimeZoneProvider to be configurable via the ContextBuilder --- .../src/builtins/temporal/duration/mod.rs | 16 +++-- .../src/builtins/temporal/instant/mod.rs | 8 +-- core/engine/src/builtins/temporal/mod.rs | 4 +- core/engine/src/builtins/temporal/now.rs | 13 ++-- .../src/builtins/temporal/plain_date/mod.rs | 8 ++- .../builtins/temporal/plain_date_time/mod.rs | 2 +- .../builtins/temporal/zoneddatetime/mod.rs | 54 +++++++-------- core/engine/src/context/mod.rs | 41 ++++++++++-- core/engine/src/context/time.rs | 67 +++++++++++++++++++ 9 files changed, 160 insertions(+), 53 deletions(-) diff --git a/core/engine/src/builtins/temporal/duration/mod.rs b/core/engine/src/builtins/temporal/duration/mod.rs index bf46adb5519..bbd3c22c96b 100644 --- a/core/engine/src/builtins/temporal/duration/mod.rs +++ b/core/engine/src/builtins/temporal/duration/mod.rs @@ -616,7 +616,10 @@ impl Duration { // 4. Let relativeToRecord be ? GetTemporalRelativeToOption(resolvedOptions). let relative_to = get_relative_to_option(&options, context)?; - Ok((one.compare_with_provider(&two, relative_to, context.tz_provider())? as i8).into()) + Ok( + (one.compare_with_provider(&two, relative_to, context.timezone_provider())? as i8) + .into(), + ) } } @@ -962,10 +965,11 @@ impl Duration { // NOTE: execute step 21 earlier before initial values are shadowed. // 21. If smallestUnitPresent is false and largestUnitPresent is false, then - let rounded_duration = - duration - .inner - .round_with_provider(options, relative_to, context.tz_provider())?; + let rounded_duration = duration.inner.round_with_provider( + options, + relative_to, + context.timezone_provider(), + )?; create_temporal_duration(rounded_duration, None, context).map(Into::into) } @@ -1042,7 +1046,7 @@ impl Duration { Ok(duration .inner - .total_with_provider(unit, relative_to, context.tz_provider())? + .total_with_provider(unit, relative_to, context.timezone_provider())? .as_inner() .into()) } diff --git a/core/engine/src/builtins/temporal/instant/mod.rs b/core/engine/src/builtins/temporal/instant/mod.rs index b5d5a4917d4..e62e7f92342 100644 --- a/core/engine/src/builtins/temporal/instant/mod.rs +++ b/core/engine/src/builtins/temporal/instant/mod.rs @@ -649,7 +649,7 @@ impl Instant { let ixdtf = instant.inner.to_ixdtf_string_with_provider( timezone, options, - context.tz_provider(), + context.timezone_provider(), )?; Ok(JsString::from(ixdtf).into()) @@ -678,7 +678,7 @@ impl Instant { let ixdtf = instant.inner.to_ixdtf_string_with_provider( None, ToStringRoundingOptions::default(), - context.tz_provider(), + context.timezone_provider(), )?; Ok(JsString::from(ixdtf).into()) } @@ -705,7 +705,7 @@ impl Instant { let ixdtf = instant.inner.to_ixdtf_string_with_provider( None, ToStringRoundingOptions::default(), - context.tz_provider(), + context.timezone_provider(), )?; Ok(JsString::from(ixdtf).into()) } @@ -758,7 +758,7 @@ impl Instant { // 4. Return ! CreateTemporalZonedDateTime(instant.[[EpochNanoseconds]], timeZone, "iso8601"). let zdt = instant .inner - .to_zoned_date_time_iso_with_provider(timezone, context.tz_provider())?; + .to_zoned_date_time_iso_with_provider(timezone, context.timezone_provider())?; create_temporal_zoneddatetime(zdt, None, context).map(Into::into) } } diff --git a/core/engine/src/builtins/temporal/mod.rs b/core/engine/src/builtins/temporal/mod.rs index 698f62cda65..7ae6451d484 100644 --- a/core/engine/src/builtins/temporal/mod.rs +++ b/core/engine/src/builtins/temporal/mod.rs @@ -224,7 +224,7 @@ pub(crate) fn get_relative_to_option( None, None, None, - context.tz_provider(), + context.timezone_provider(), )?; return Ok(Some(RelativeTo::ZonedDateTime(zdt))); } @@ -238,7 +238,7 @@ pub(crate) fn get_relative_to_option( // Steps 7-12 are handled by temporal_rs Ok(Some(RelativeTo::try_from_str_with_provider( &relative_to_str.to_std_string_escaped(), - context.tz_provider(), + context.timezone_provider(), )?)) } diff --git a/core/engine/src/builtins/temporal/now.rs b/core/engine/src/builtins/temporal/now.rs index f6652a0ef98..a404ca3b41f 100644 --- a/core/engine/src/builtins/temporal/now.rs +++ b/core/engine/src/builtins/temporal/now.rs @@ -81,8 +81,8 @@ impl Now { // TODO: this should be optimized once system time zone is in context // 1. Return ! SystemTimeZone(). let context: &Context = context; - let time_zone = context.get_system_time_zone(context.tz_provider())?; - Ok(JsString::from(time_zone.identifier_with_provider(context.tz_provider())?).into()) + let time_zone = context.get_system_time_zone(context.timezone_provider())?; + Ok(JsString::from(time_zone.identifier_with_provider(context.timezone_provider())?).into()) } /// 2.2.2 `Temporal.Now.instant()` @@ -125,7 +125,8 @@ impl Now { let now: InnerNow<&Context> = InnerNow::new(context); - let datetime = now.plain_date_time_iso_with_provider(time_zone, context.tz_provider())?; + let datetime = + now.plain_date_time_iso_with_provider(time_zone, context.timezone_provider())?; create_temporal_datetime(datetime, None, context).map(Into::into) } @@ -152,7 +153,7 @@ impl Now { .transpose()?; let now: InnerNow<&Context> = InnerNow::new(context); - let zdt = now.zoned_date_time_iso_with_provider(time_zone, context.tz_provider())?; + let zdt = now.zoned_date_time_iso_with_provider(time_zone, context.timezone_provider())?; create_temporal_zoneddatetime(zdt, None, context).map(Into::into) } @@ -176,7 +177,7 @@ impl Now { let now: InnerNow<&Context> = InnerNow::new(context); - let pd = now.plain_date_iso_with_provider(time_zone, context.tz_provider())?; + let pd = now.plain_date_iso_with_provider(time_zone, context.timezone_provider())?; create_temporal_date(pd, None, context).map(Into::into) } @@ -200,7 +201,7 @@ impl Now { let now: InnerNow<&Context> = InnerNow::new(context); - let pt = now.plain_time_with_provider(time_zone, context.tz_provider())?; + let pt = now.plain_time_with_provider(time_zone, context.timezone_provider())?; create_temporal_time(pt, None, context).map(Into::into) } } diff --git a/core/engine/src/builtins/temporal/plain_date/mod.rs b/core/engine/src/builtins/temporal/plain_date/mod.rs index 281b964f96b..2b0eff7b7d2 100644 --- a/core/engine/src/builtins/temporal/plain_date/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date/mod.rs @@ -1174,9 +1174,11 @@ impl PlainDate { (to_temporal_timezone_identifier(item, context)?, None) }; - let result = - date.inner - .to_zoned_date_time_with_provider(timezone, time, context.tz_provider())?; + let result = date.inner.to_zoned_date_time_with_provider( + timezone, + time, + context.timezone_provider(), + )?; // 7. Return ! CreateTemporalZonedDateTime(epochNs, timeZone, temporalDate.[[Calendar]]). create_temporal_zoneddatetime(result, None, context).map(Into::into) diff --git a/core/engine/src/builtins/temporal/plain_date_time/mod.rs b/core/engine/src/builtins/temporal/plain_date_time/mod.rs index 02ce0ba25e3..5a34e2658df 100644 --- a/core/engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date_time/mod.rs @@ -1553,7 +1553,7 @@ impl PlainDateTime { let result = dt.inner.to_zoned_date_time_with_provider( timezone, disambiguation, - context.tz_provider(), + context.timezone_provider(), )?; create_temporal_zoneddatetime(result, None, context).map(Into::into) } diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index d96d5905bec..2a2fed26e63 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -431,7 +431,7 @@ impl BuiltInConstructor for ZonedDateTime { // a. Set timeZone to FormatOffsetTimeZoneIdentifier(timeZoneParse.[[OffsetMinutes]]). let timezone = TimeZone::try_from_identifier_str_with_provider( &timezone_str.to_std_string_escaped(), - context.tz_provider(), + context.timezone_provider(), )?; // 8. If calendar is undefined, set calendar to "iso8601". @@ -454,7 +454,7 @@ impl BuiltInConstructor for ZonedDateTime { epoch_nanos.to_i128(), timezone, calendar, - context.tz_provider(), + context.timezone_provider(), )?; // 11. Return ? CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar, NewTarget). @@ -511,7 +511,7 @@ impl ZonedDateTime { Ok(JsString::from( zdt.inner .time_zone() - .identifier_with_provider(context.tz_provider())?, + .identifier_with_provider(context.timezone_provider())?, ) .into()) } @@ -955,7 +955,7 @@ impl ZonedDateTime { Ok(zdt .inner - .hours_in_day_with_provider(context.tz_provider())? + .hours_in_day_with_provider(context.timezone_provider())? .into()) } @@ -1228,7 +1228,7 @@ impl ZonedDateTime { disambiguation, offset, overflow, - context.tz_provider(), + context.timezone_provider(), )?; create_temporal_zoneddatetime(result, None, context).map(Into::into) } @@ -1264,7 +1264,7 @@ impl ZonedDateTime { let inner = zdt .inner - .with_plain_time_and_provider(time, context.tz_provider())?; + .with_plain_time_and_provider(time, context.timezone_provider())?; create_temporal_zoneddatetime(inner, None, context).map(Into::into) } @@ -1292,7 +1292,7 @@ impl ZonedDateTime { let inner = zdt .inner - .with_time_zone_with_provider(timezone, context.tz_provider())?; + .with_time_zone_with_provider(timezone, context.timezone_provider())?; create_temporal_zoneddatetime(inner, None, context).map(Into::into) } @@ -1347,9 +1347,9 @@ impl ZonedDateTime { let options = get_options_object(args.get_or_undefined(1))?; let overflow = get_option::(&options, js_string!("overflow"), context)?; - let result = zdt - .inner - .add_with_provider(&duration, overflow, context.tz_provider())?; + let result = + zdt.inner + .add_with_provider(&duration, overflow, context.timezone_provider())?; create_temporal_zoneddatetime(result, None, context).map(Into::into) } @@ -1380,7 +1380,7 @@ impl ZonedDateTime { let result = zdt.inner - .subtract_with_provider(&duration, overflow, context.tz_provider())?; + .subtract_with_provider(&duration, overflow, context.timezone_provider())?; create_temporal_zoneddatetime(result, None, context).map(Into::into) } @@ -1409,9 +1409,9 @@ impl ZonedDateTime { let options = get_options_object(args.get_or_undefined(1))?; let settings = get_difference_settings(&options, context)?; - let result = zdt - .inner - .until_with_provider(&other, settings, context.tz_provider())?; + let result = + zdt.inner + .until_with_provider(&other, settings, context.timezone_provider())?; create_temporal_duration(result, None, context).map(Into::into) } @@ -1440,9 +1440,9 @@ impl ZonedDateTime { let options = get_options_object(args.get_or_undefined(1))?; let settings = get_difference_settings(&options, context)?; - let result = zdt - .inner - .since_with_provider(&other, settings, context.tz_provider())?; + let result = + zdt.inner + .since_with_provider(&other, settings, context.timezone_provider())?; create_temporal_duration(result, None, context).map(Into::into) } @@ -1521,7 +1521,7 @@ impl ZonedDateTime { let result = zdt .inner - .round_with_provider(options, context.tz_provider())?; + .round_with_provider(options, context.timezone_provider())?; create_temporal_zoneddatetime(result, None, context).map(Into::into) } @@ -1548,7 +1548,7 @@ impl ZonedDateTime { let other = to_temporal_zoneddatetime(args.get_or_undefined(0), None, context)?; Ok(zdt .inner - .equals_with_provider(&other, context.tz_provider())? + .equals_with_provider(&other, context.timezone_provider())? .into()) } @@ -1598,7 +1598,7 @@ impl ZonedDateTime { display_timezone, show_calendar, options, - context.tz_provider(), + context.timezone_provider(), )?; Ok(JsString::from(ixdtf).into()) @@ -1628,7 +1628,7 @@ impl ZonedDateTime { DisplayTimeZone::Auto, DisplayCalendar::Auto, ToStringRoundingOptions::default(), - context.tz_provider(), + context.timezone_provider(), )?; Ok(JsString::from(ixdtf).into()) @@ -1657,7 +1657,7 @@ impl ZonedDateTime { DisplayTimeZone::Auto, DisplayCalendar::Auto, ToStringRoundingOptions::default(), - context.tz_provider(), + context.timezone_provider(), )?; Ok(JsString::from(ixdtf).into()) @@ -1700,7 +1700,7 @@ impl ZonedDateTime { let new = zdt .inner - .start_of_day_with_provider(context.tz_provider())?; + .start_of_day_with_provider(context.timezone_provider())?; create_temporal_zoneddatetime(new, None, context).map(Into::into) } @@ -1767,7 +1767,7 @@ impl ZonedDateTime { // Step 8-12 let result = zdt .inner - .get_time_zone_transition_with_provider(direction, context.tz_provider())?; + .get_time_zone_transition_with_provider(direction, context.timezone_provider())?; match result { Some(zdt) => create_temporal_zoneddatetime(zdt, None, context).map(Into::into), @@ -1964,7 +1964,7 @@ pub(crate) fn to_temporal_zoneddatetime( overflow, disambiguation, offset_option, - context.tz_provider(), + context.timezone_provider(), )?) } JsVariant::String(zdt_source) => { @@ -2003,7 +2003,7 @@ pub(crate) fn to_temporal_zoneddatetime( zdt_source.to_std_string_escaped().as_bytes(), disambiguation, offset_option, - context.tz_provider(), + context.timezone_provider(), )?) } // 5. Else, @@ -2044,7 +2044,7 @@ pub(crate) fn to_temporal_timezone_identifier( // 9. Return timeZoneIdentifierRecord.[[Identifier]]. let timezone = TimeZone::try_from_str_with_provider( &tz_string.to_std_string_escaped(), - context.tz_provider(), + context.timezone_provider(), )?; Ok(timezone) diff --git a/core/engine/src/context/mod.rs b/core/engine/src/context/mod.rs index c81b78d55b9..601a877e69d 100644 --- a/core/engine/src/context/mod.rs +++ b/core/engine/src/context/mod.rs @@ -14,6 +14,8 @@ use temporal_rs::provider::TimeZoneProvider; #[cfg(feature = "temporal")] use timezone_provider::tzif::CompiledTzdbProvider; +#[cfg(feature = "temporal")] +use crate::context::time::DynamicTimeZoneProvider; use crate::job::Job; use crate::module::DynModuleLoader; use crate::vm::RuntimeLimits; @@ -108,7 +110,7 @@ pub struct Context { can_block: bool, #[cfg(feature = "temporal")] - tz_provider: CompiledTzdbProvider, + timezone_provider: DynamicTimeZoneProvider, /// Intl data provider. #[cfg(feature = "intl")] @@ -149,6 +151,10 @@ impl std::fmt::Debug for Context { #[cfg(feature = "intl")] debug.field("intl_provider", &self.intl_provider); + // TODO: Support TimeZoneProvider debug names + #[cfg(feature = "temporal")] + debug.field("timezone_provider", &"TimeZoneProvider"); + debug.finish_non_exhaustive() } } @@ -892,8 +898,8 @@ impl Context { /// Get the Time Zone Provider #[cfg(feature = "temporal")] - pub(crate) fn tz_provider(&self) -> &impl TimeZoneProvider { - &self.tz_provider + pub(crate) fn timezone_provider(&self) -> &impl TimeZoneProvider { + &self.timezone_provider } } @@ -911,6 +917,8 @@ pub struct ContextBuilder { can_block: bool, #[cfg(feature = "intl")] icu: Option, + #[cfg(feature = "temporal")] + time_zone_provider: Option, #[cfg(feature = "fuzz")] instructions_remaining: usize, } @@ -944,6 +952,12 @@ impl std::fmt::Debug for ContextBuilder { #[cfg(feature = "intl")] out.field("icu", &self.icu); + #[cfg(feature = "temporal")] + out.field( + "timezone_provider", + &self.time_zone_provider.as_ref().map(|_| "TimeZoneProvider"), + ); + #[cfg(feature = "fuzz")] out.field("instructions_remaining", &self.instructions_remaining); @@ -1004,6 +1018,21 @@ impl ContextBuilder { Ok(self) } + /// Set the [`timezone_provider::provider::TimeZoneProvider`] that should be used to source + /// time zone data. + /// + /// ## Default + /// + /// If no time zone provider is provided, a compiled time zone provider will be used + /// which includes the time zone data in the binary. This may increase binary sizes + /// by up to 200 Kb. + #[cfg(feature = "temporal")] + #[must_use] + pub fn time_zone_provider(mut self, provider: T) -> Self { + self.time_zone_provider = Some(DynamicTimeZoneProvider::new(provider)); + self + } + /// Initializes the [`HostHooks`] for the context. /// /// [`Host Hooks`]: https://tc39.es/ecma262/#sec-host-hooks-summary @@ -1103,7 +1132,11 @@ impl ContextBuilder { vm, strict: false, #[cfg(feature = "temporal")] - tz_provider: CompiledTzdbProvider::default(), + timezone_provider: if let Some(provider) = self.time_zone_provider { + provider + } else { + DynamicTimeZoneProvider::new(CompiledTzdbProvider::default()) + }, #[cfg(feature = "intl")] intl_provider: if let Some(icu) = self.icu { icu diff --git a/core/engine/src/context/time.rs b/core/engine/src/context/time.rs index 397d85f752b..bcd871bdd9f 100644 --- a/core/engine/src/context/time.rs +++ b/core/engine/src/context/time.rs @@ -1,5 +1,10 @@ //! Clock related types and functions. +use std::fmt::Debug; + +#[cfg(feature = "temporal")] +use timezone_provider::{provider::{TimeZoneId, TimeZoneProvider}, TimeZoneProviderError}; + /// A monotonic instant in time, in the Boa engine. /// /// This type is guaranteed to be monotonic, i.e. if two instants @@ -179,6 +184,68 @@ impl Clock for FixedClock { } } +/// The `DynamicTimeZoneProvider` is a wrapper type that allows users +/// to dynamically set which time zone provider they would like to use +/// for sourcing time zone data. +#[cfg(feature = "temporal")] +pub(crate) struct DynamicTimeZoneProvider { + inner: Box, +} + +#[cfg(feature = "temporal")] +impl DynamicTimeZoneProvider { + pub(crate) fn new(provider: T) -> Self { + let inner = Box::new(provider); + Self { inner } + } +} + +#[cfg(feature = "temporal")] +impl TimeZoneProvider for DynamicTimeZoneProvider { + fn get(&self, ident: &[u8]) -> Result { + self.inner.get(ident) + } + + fn identifier( + &self, + id: TimeZoneId, + ) -> Result, TimeZoneProviderError> { + self.inner.identifier(id) + } + + fn canonicalized(&self, id: TimeZoneId) -> Result { + self.inner.canonicalized(id) + } + + fn transition_nanoseconds_for_utc_epoch_nanoseconds( + &self, + id: TimeZoneId, + epoch_nanoseconds: i128, + ) -> Result { + self.inner + .transition_nanoseconds_for_utc_epoch_nanoseconds(id, epoch_nanoseconds) + } + + fn candidate_nanoseconds_for_local_epoch_nanoseconds( + &self, + id: TimeZoneId, + local_datetime: timezone_provider::provider::IsoDateTime, + ) -> Result { + self.inner + .candidate_nanoseconds_for_local_epoch_nanoseconds(id, local_datetime) + } + + fn get_time_zone_transition( + &self, + id: TimeZoneId, + epoch_nanoseconds: i128, + direction: temporal_rs::provider::TransitionDirection, + ) -> Result, TimeZoneProviderError> { + self.inner + .get_time_zone_transition(id, epoch_nanoseconds, direction) + } +} + #[test] fn basic() { let now = StdClock.now(); From 0952a79e1ab1e58be0eca267f16a255e37d1e416 Mon Sep 17 00:00:00 2001 From: Kevin Ness Date: Fri, 26 Sep 2025 17:13:16 -0500 Subject: [PATCH 2/2] cargo fmt --all --- core/engine/src/context/time.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/engine/src/context/time.rs b/core/engine/src/context/time.rs index bcd871bdd9f..5cbccea50a9 100644 --- a/core/engine/src/context/time.rs +++ b/core/engine/src/context/time.rs @@ -3,7 +3,10 @@ use std::fmt::Debug; #[cfg(feature = "temporal")] -use timezone_provider::{provider::{TimeZoneId, TimeZoneProvider}, TimeZoneProviderError}; +use timezone_provider::{ + TimeZoneProviderError, + provider::{TimeZoneId, TimeZoneProvider}, +}; /// A monotonic instant in time, in the Boa engine. ///