diff --git a/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs b/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs index 938cf9f1a28..3a68eeff9ae 100644 --- a/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs +++ b/crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs @@ -143,12 +143,28 @@ public static void NonHexStrings() [Fact] public static void TimestampConversionChecks() { - ulong us = 1737582793990639; - var time = ScheduleAt.DateTimeOffsetFromMicrosSinceUnixEpoch(us); + var us = 1737582793990639L; + var time = ScheduleAt.DateTimeOffsetFromMicrosSinceUnixEpoch(us); Assert.Equal(ScheduleAt.ToMicrosecondsSinceUnixEpoch(time), us); var interval = ScheduleAt.TimeSpanFromMicroseconds(us); Assert.Equal(ScheduleAt.ToMicroseconds(interval), us); + + var stamp = new Timestamp(us); + var dto = (DateTimeOffset)stamp; + var stamp_ = (Timestamp)dto; + Assert.Equal(stamp, stamp_); + + var duration = new TimeDuration(us); + var timespan = (TimeSpan)duration; + var duration_ = (TimeDuration)timespan; + Assert.Equal(duration, duration_); + + var newIntervalUs = 333L; + var newInterval = new TimeDuration(newIntervalUs); + var laterStamp = stamp + newInterval; + Assert.Equal(laterStamp.MicrosecondsSinceUnixEpoch, us + newIntervalUs); + Assert.Equal(laterStamp.TimeDurationSince(stamp), newInterval); } } diff --git a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj index d170e44130c..f3d9ac7a62d 100644 --- a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj +++ b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj @@ -11,6 +11,8 @@ netstandard2.1;net8.0 SpacetimeDB + + diff --git a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs index 50384d47707..281cfb58345 100644 --- a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs +++ b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs @@ -1,10 +1,8 @@ namespace SpacetimeDB; using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SpacetimeDB.BSATN; -using SpacetimeDB.Internal; internal static class Util { @@ -95,11 +93,18 @@ public static byte[] StringToByteArray(string hex) return bytes; #endif } + + // Similarly, we need some constants that are not available in .NET Standard. + public const long TicksPerMicrosecond = 10; + public const long MicrosecondsPerSecond = 1_000_000; } +// The following types are "special" types: they has a special (Ref-less) AlgebraicType representations. +// See `spacetimedb-sats::AlgebraicType::is_valid_for_client_type_[use|generate]` for more information. +// We don't use [Type] here; instead we manually implement the serialization stuff that would be generated by +// [Type] so that we can override GetAlgebraicType to return types in a special, Ref-less form. public readonly partial struct Unit { - // Custom BSATN that returns an inline empty product type that can be recognised by SpacetimeDB. public readonly struct BSATN : IReadWrite { public Unit Read(BinaryReader reader) => default; @@ -107,6 +112,7 @@ public readonly partial struct Unit public void Write(BinaryWriter writer, Unit value) { } public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => + // Return a Product directly, not a Ref, because this is a special type. new AlgebraicType.Product([]); } } @@ -167,6 +173,7 @@ public static Address Random() return addr; } + // --- auto-generated --- public readonly struct BSATN : IReadWrite
{ public Address Read(BinaryReader reader) => @@ -175,8 +182,18 @@ public Address Read(BinaryReader reader) => public void Write(BinaryWriter writer, Address value) => new SpacetimeDB.BSATN.U128Stdb().Write(writer, value.value); + // --- / auto-generated --- + + // --- customized --- public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - new AlgebraicType.Product([new("__address__", new AlgebraicType.U128(default))]); + // Return a Product directly, not a Ref, because this is a special type. + new AlgebraicType.Product( + [ + // Using this specific name here is important. + new("__address__", new AlgebraicType.U128(default)), + ] + ); + // --- / customized --- } public override string ToString() => Util.ToHexBigEndian(value); @@ -229,6 +246,7 @@ public static Identity FromBigEndian(ReadOnlySpan bytes) => /// public static Identity FromHexString(string hex) => FromBigEndian(Util.StringToByteArray(hex)); + // --- auto-generated --- public readonly struct BSATN : IReadWrite { public Identity Read(BinaryReader reader) => new(new SpacetimeDB.BSATN.U256().Read(reader)); @@ -236,93 +254,249 @@ public static Identity FromBigEndian(ReadOnlySpan bytes) => public void Write(BinaryWriter writer, Identity value) => new SpacetimeDB.BSATN.U256().Write(writer, value.value); + // --- / auto-generated --- + + // --- customized --- public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - new AlgebraicType.Product([new("__identity__", new AlgebraicType.U256(default))]); + // Return a Product directly, not a Ref, because this is a special type. + new AlgebraicType.Product( + [ + // Using this specific name here is important. + new("__identity__", new AlgebraicType.U256(default)), + ] + ); + // --- / customized --- } - // This must be explicitly forwarded to base, otherwise record will generate a new implementation. + // This must be explicitly implemented, otherwise record will generate a new implementation. public override string ToString() => Util.ToHexBigEndian(value); } -// [SpacetimeDB.Type] - we have custom representation of time in microseconds, so implementing BSATN manually -public abstract partial record ScheduleAt - : SpacetimeDB.TaggedEnum<(TimeSpan Interval, DateTimeOffset Time)> +/// +/// A timestamp that represents a unique moment in time (in the Earth's reference frame). +/// +/// This type may be converted to/from a DateTimeOffset, but the conversion can lose precision. +/// This type has less precision than DateTimeOffset (units of microseconds rather than units of 100ns). +/// +[StructLayout(LayoutKind.Sequential)] // we should be able to use it in FFI +public record struct Timestamp(long MicrosecondsSinceUnixEpoch) : IStructuralReadWrite { - // Manual expansion of what would be otherwise generated by the [SpacetimeDB.Type] codegen. - public sealed record Interval(TimeSpan Interval_) : ScheduleAt; + public static implicit operator DateTimeOffset(Timestamp t) => + DateTimeOffset.UnixEpoch.AddTicks(t.MicrosecondsSinceUnixEpoch * Util.TicksPerMicrosecond); - public sealed record Time(DateTimeOffset Time_) : ScheduleAt; + public static implicit operator Timestamp(DateTimeOffset offset) => + new Timestamp(offset.Subtract(DateTimeOffset.UnixEpoch).Ticks / Util.TicksPerMicrosecond); - public static implicit operator ScheduleAt(TimeSpan interval) => new Interval(interval); + // For backwards-compatibility. + public readonly DateTimeOffset ToStd() => this; - public static implicit operator ScheduleAt(DateTimeOffset time) => new Time(time); + // Should be consistent with Rust implementation of Display. + public override readonly string ToString() + { + var sign = MicrosecondsSinceUnixEpoch < 0 ? "-" : ""; + var pos = Math.Abs(MicrosecondsSinceUnixEpoch); + var secs = pos / Util.MicrosecondsPerSecond; + var microsRemaining = pos % Util.MicrosecondsPerSecond; + return $"{sign}{secs}.{microsRemaining:D6}"; + } - /// - /// There are 10 C# Timestamp "Ticks" per microsecond. - /// - public static readonly ulong TicksPerMicrosecond = 10; + public static readonly Timestamp UNIX_EPOCH = new(0); + + public static Timestamp FromTimeDurationSinceUnixEpoch(TimeDuration timeDuration) => + new Timestamp(timeDuration.Microseconds); + + public readonly TimeDuration ToTimeDurationSinceUnixEpoch() => TimeDurationSince(UNIX_EPOCH); + + public static Timestamp FromTimeSpanSinceUnixEpoch(TimeSpan timeSpan) => + FromTimeDurationSinceUnixEpoch((TimeDuration)timeSpan); + + public readonly TimeSpan ToTimeSpanSinceUnixEpoch() => (TimeSpan)ToTimeDurationSinceUnixEpoch(); + + public readonly TimeDuration TimeDurationSince(Timestamp earlier) => + new TimeDuration(MicrosecondsSinceUnixEpoch - earlier.MicrosecondsSinceUnixEpoch); - public static ulong ToMicroseconds(TimeSpan interval) + public static Timestamp operator +(Timestamp point, TimeDuration interval) => + new Timestamp(point.MicrosecondsSinceUnixEpoch + interval.Microseconds); + + // --- auto-generated --- + + public void ReadFields(BinaryReader reader) { - return (ulong)interval.Ticks / TicksPerMicrosecond; + MicrosecondsSinceUnixEpoch = BSATN.MicrosecondsSinceUnixEpoch.Read(reader); } - public static TimeSpan TimeSpanFromMicroseconds(ulong intervalMicros) + public readonly void WriteFields(BinaryWriter writer) { - return TimeSpan.FromTicks((long)(TicksPerMicrosecond * intervalMicros)); + BSATN.MicrosecondsSinceUnixEpoch.Write(writer, MicrosecondsSinceUnixEpoch); } - public static ulong ToMicrosecondsSinceUnixEpoch(DateTimeOffset time) + public readonly partial struct BSATN : IReadWrite { - return ToMicroseconds(time - DateTimeOffset.UnixEpoch); + internal static readonly I64 MicrosecondsSinceUnixEpoch = new(); + + public Timestamp Read(BinaryReader reader) => IStructuralReadWrite.Read(reader); + + public void Write(BinaryWriter writer, Timestamp value) + { + value.WriteFields(writer); + } + + // --- / auto-generated --- + + // --- customized --- + public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => + // Return a Product directly, not a Ref, because this is a special type. + new AlgebraicType.Product( + // Using this specific name here is important. + [new("__timestamp_micros_since_unix_epoch__", new AlgebraicType.I64(default))] + ); + // --- / customized --- } +} + +/// +/// A duration that represents an interval between two events (in a particular reference frame). +/// +/// This type may be converted to/from a TimeSpan, but the conversion can lose precision. +/// This type has less precision than TimeSpan (units of microseconds rather than units of 100ns). +/// +[StructLayout(LayoutKind.Sequential)] +public record struct TimeDuration(long Microseconds) : IStructuralReadWrite +{ + public static readonly TimeDuration ZERO = new(0); - public static DateTimeOffset DateTimeOffsetFromMicrosSinceUnixEpoch(ulong microsSinceUnixEpoch) + public static implicit operator TimeSpan(TimeDuration d) => + new(d.Microseconds * Util.TicksPerMicrosecond); + + public static implicit operator TimeDuration(TimeSpan timeSpan) => + new(timeSpan.Ticks / Util.TicksPerMicrosecond); + + // For backwards-compatibility. + public readonly TimeSpan ToStd() => this; + + // Should be consistent with Rust implementation of Display. + public override readonly string ToString() { - return DateTimeOffset.UnixEpoch + TimeSpanFromMicroseconds(microsSinceUnixEpoch); + var sign = Microseconds < 0 ? "-" : "+"; + var pos = Math.Abs(Microseconds); + var secs = pos / Util.MicrosecondsPerSecond; + var microsRemaining = pos % Util.MicrosecondsPerSecond; + return $"{sign}{secs}.{microsRemaining:D6}"; } - public readonly partial struct BSATN : IReadWrite + // --- auto-generated --- + public void ReadFields(BinaryReader reader) { - [SpacetimeDB.Type] - private partial record ScheduleAtRepr - : SpacetimeDB.TaggedEnum<(TimeSpanRepr Interval, DateTimeOffsetRepr Time)>; + Microseconds = BSATN.__time_duration_micros__.Read(reader); + } + + public readonly void WriteFields(BinaryWriter writer) + { + BSATN.__time_duration_micros__.Write(writer, Microseconds); + } + + public readonly partial struct BSATN : IReadWrite + { + internal static readonly I64 __time_duration_micros__ = new(); + + public TimeDuration Read(BinaryReader reader) => + IStructuralReadWrite.Read(reader); + + public void Write(BinaryWriter writer, TimeDuration value) + { + value.WriteFields(writer); + } + + // --- customized --- + public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => + // Return a Product directly, not a Ref, because this is a special type. + new AlgebraicType.Product( + // Using this specific name here is important. + [new("__time_duration_micros__", new AlgebraicType.I64(default))] + ); + // --- / customized --- + } +} + +public partial record ScheduleAt : TaggedEnum<(TimeDuration Interval, Timestamp Time)> +{ + public static implicit operator ScheduleAt(TimeDuration duration) => new Interval(duration); + + public static implicit operator ScheduleAt(Timestamp time) => new Time(time); - private static readonly ScheduleAtRepr.BSATN ReprBSATN = new(); + public static implicit operator ScheduleAt(TimeSpan duration) => new Interval(duration); + + public static implicit operator ScheduleAt(DateTimeOffset time) => new Time(time); + + public static long ToMicroseconds(TimeSpan interval) => ((TimeDuration)interval).Microseconds; + + public static TimeSpan TimeSpanFromMicroseconds(long intervalMicros) => + (TimeSpan)(new TimeDuration(intervalMicros)); + + public static long ToMicrosecondsSinceUnixEpoch(DateTimeOffset time) => + ((Timestamp)time).MicrosecondsSinceUnixEpoch; + + public static DateTimeOffset DateTimeOffsetFromMicrosSinceUnixEpoch( + long microsSinceUnixEpoch + ) => (DateTimeOffset)(new Timestamp(microsSinceUnixEpoch)); + + // --- auto-generated --- + private ScheduleAt() { } + + internal enum @enum : byte + { + Interval, + Time, + } + + public sealed record Interval(TimeDuration Interval_) : ScheduleAt; + + public sealed record Time(Timestamp Time_) : ScheduleAt; + + public readonly partial struct BSATN : IReadWrite + { + internal static readonly SpacetimeDB.BSATN.Enum<@enum> __enumTag = new(); + internal static readonly TimeDuration.BSATN Interval = new(); + internal static readonly Timestamp.BSATN Time = new(); public ScheduleAt Read(BinaryReader reader) => - ReprBSATN.Read(reader) switch + __enumTag.Read(reader) switch { - ScheduleAtRepr.Interval(var intervalRepr) => new Interval(intervalRepr.ToStd()), - ScheduleAtRepr.Time(var timeRepr) => new Time(timeRepr.ToStd()), - _ => throw new SwitchExpressionException(), + @enum.Interval => new Interval(Interval.Read(reader)), + @enum.Time => new Time(Time.Read(reader)), + _ => throw new InvalidOperationException( + "Invalid tag value, this state should be unreachable." + ), }; public void Write(BinaryWriter writer, ScheduleAt value) { - ReprBSATN.Write( - writer, - value switch - { - Interval(var interval) => new ScheduleAtRepr.Interval(new(interval)), - Time(var time) => new ScheduleAtRepr.Time(new(time)), - _ => throw new SwitchExpressionException(), - } - ); + switch (value) + { + case Interval(var inner): + __enumTag.Write(writer, @enum.Interval); + Interval.Write(writer, inner); + break; + + case Time(var inner): + __enumTag.Write(writer, @enum.Time); + Time.Write(writer, inner); + break; + } } + // --- / auto-generated --- + + // --- customized --- public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - // Constructing a custom one instead of ScheduleAtRepr.GetAlgebraicType() - // to avoid leaking the internal *Repr wrappers in generated SATS. - // We are leveraging the fact that single-element structs are byte-compatible with their elements - // when parsing BSATN. - // TODO: this might break when working with other formats like JSON, but this is all going to be rewritten - // anyway with Phoebe's Timestamp PR. + // Return a Sum directly, not a Ref, because this is a special type. new AlgebraicType.Sum( [ - new("Interval", new AlgebraicType.U64(default)), - new("Time", new AlgebraicType.U64(default)), + // Using these specific names here is important. + new("Interval", Interval.GetAlgebraicType(registrar)), + new("Time", Time.GetAlgebraicType(registrar)), ] ); + // --- / customized --- } } diff --git a/crates/bindings-csharp/BSATN.Runtime/Repr.cs b/crates/bindings-csharp/BSATN.Runtime/Repr.cs deleted file mode 100644 index cf2657c0f58..00000000000 --- a/crates/bindings-csharp/BSATN.Runtime/Repr.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace SpacetimeDB.Internal; - -using System.Runtime.InteropServices; - -// We store time information in microseconds in internal usages. -// -// These utils allow to encode it as such in FFI and BSATN contexts -// and convert to standard C# types. - -[StructLayout(LayoutKind.Sequential)] // we should be able to use it in FFI -[SpacetimeDB.Type] // we should be able to encode it to BSATN too -public partial struct DateTimeOffsetRepr(DateTimeOffset time) -{ - public ulong MicrosecondsSinceEpoch = ScheduleAt.ToMicrosecondsSinceUnixEpoch(time); - - public readonly DateTimeOffset ToStd() => - ScheduleAt.DateTimeOffsetFromMicrosSinceUnixEpoch(MicrosecondsSinceEpoch); -} - -[StructLayout(LayoutKind.Sequential)] // we should be able to use it in FFI -[SpacetimeDB.Type] // we should be able to encode it to BSATN too -public partial struct TimeSpanRepr(TimeSpan duration) -{ - public ulong Microseconds = ScheduleAt.ToMicroseconds(duration); - - public readonly TimeSpan ToStd() => ScheduleAt.TimeSpanFromMicroseconds(Microseconds); -} diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs index b2cd9aa15be..7ca9fe09852 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs @@ -13,17 +13,12 @@ public sealed record ReducerContext : DbContext, Internal.IReducerContext public readonly Identity CallerIdentity; public readonly Address? CallerAddress; public readonly Random Rng; - public readonly DateTimeOffset Timestamp; + public readonly Timestamp Timestamp; // We need this property to be non-static for parity with client SDK. public Identity Identity => Internal.IReducerContext.GetIdentity(); - internal ReducerContext( - Identity identity, - Address? address, - Random random, - DateTimeOffset time - ) + internal ReducerContext(Identity identity, Address? address, Random random, Timestamp time) { CallerIdentity = identity; CallerAddress = address; @@ -1008,7 +1003,7 @@ public static SpacetimeDB.Internal.Errno __call_reducer__( ulong sender_3, ulong address_0, ulong address_1, - SpacetimeDB.Internal.DateTimeOffsetRepr timestamp, + SpacetimeDB.Timestamp timestamp, SpacetimeDB.Internal.BytesSource args, SpacetimeDB.Internal.BytesSink error ) => diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs index f38b3999d08..06e0ebad171 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs @@ -141,7 +141,7 @@ public static void Init(ReducerContext ctx) new SendMessageTimer { Text = "bot sending a message", - ScheduledAt = ctx.Timestamp.AddSeconds(10), + ScheduledAt = ctx.Timestamp + new TimeDuration(10_000_000), } ); } diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs index 7c8bfb9f93b..d143e87ff07 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs @@ -13,17 +13,12 @@ public sealed record ReducerContext : DbContext, Internal.IReducerContext public readonly Identity CallerIdentity; public readonly Address? CallerAddress; public readonly Random Rng; - public readonly DateTimeOffset Timestamp; + public readonly Timestamp Timestamp; // We need this property to be non-static for parity with client SDK. public Identity Identity => Internal.IReducerContext.GetIdentity(); - internal ReducerContext( - Identity identity, - Address? address, - Random random, - DateTimeOffset time - ) + internal ReducerContext(Identity identity, Address? address, Random random, Timestamp time) { CallerIdentity = identity; CallerAddress = address; @@ -1156,7 +1151,7 @@ public static SpacetimeDB.Internal.Errno __call_reducer__( ulong sender_3, ulong address_0, ulong address_1, - SpacetimeDB.Internal.DateTimeOffsetRepr timestamp, + SpacetimeDB.Timestamp timestamp, SpacetimeDB.Internal.BytesSource args, SpacetimeDB.Internal.BytesSink error ) => diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 500a36a5406..9fc3f894d92 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -492,7 +492,7 @@ public IEnumerable GenerateViews() Constraints: {{GenConstraintList(v, ColumnAttrs.Unique, $"{iTable}.MakeUniqueConstraint")}}, Sequences: {{GenConstraintList(v, ColumnAttrs.AutoInc, $"{iTable}.MakeSequence")}}, Schedule: {{( - v.Scheduled is {} scheduled + v.Scheduled is { } scheduled ? $"{iTable}.MakeSchedule(\"{scheduled.ReducerName}\", {scheduled.ScheduledAtColumn})" : "null" )}}, @@ -620,12 +620,13 @@ class {{Name}}: SpacetimeDB.Internal.IReducer { public SpacetimeDB.Internal.RawReducerDefV9 MakeReducerDef(SpacetimeDB.BSATN.ITypeRegistrar registrar) => new ( nameof({{Name}}), [{{MemberDeclaration.GenerateDefs(Args)}}], - {{Kind switch { - ReducerKind.Init => "SpacetimeDB.Internal.Lifecycle.Init", - ReducerKind.ClientConnected => "SpacetimeDB.Internal.Lifecycle.OnConnect", - ReducerKind.ClientDisconnected => "SpacetimeDB.Internal.Lifecycle.OnDisconnect", - _ => "null" - }}} + {{Kind switch + { + ReducerKind.Init => "SpacetimeDB.Internal.Lifecycle.Init", + ReducerKind.ClientConnected => "SpacetimeDB.Internal.Lifecycle.OnConnect", + ReducerKind.ClientDisconnected => "SpacetimeDB.Internal.Lifecycle.OnDisconnect", + _ => "null" + }}} ); public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx) { @@ -821,12 +822,12 @@ public sealed record ReducerContext : DbContext, Internal.IReducerContext public readonly Identity CallerIdentity; public readonly Address? CallerAddress; public readonly Random Rng; - public readonly DateTimeOffset Timestamp; + public readonly Timestamp Timestamp; // We need this property to be non-static for parity with client SDK. public Identity Identity => Internal.IReducerContext.GetIdentity(); - internal ReducerContext(Identity identity, Address? address, Random random, DateTimeOffset time) { + internal ReducerContext(Identity identity, Address? address, Random random, Timestamp time) { CallerIdentity = identity; CallerAddress = address; Rng = random; @@ -883,7 +884,7 @@ public static SpacetimeDB.Internal.Errno __call_reducer__( ulong sender_3, ulong address_0, ulong address_1, - SpacetimeDB.Internal.DateTimeOffsetRepr timestamp, + SpacetimeDB.Timestamp timestamp, SpacetimeDB.Internal.BytesSource args, SpacetimeDB.Internal.BytesSink error ) => SpacetimeDB.Internal.Module.__call_reducer__( diff --git a/crates/bindings-csharp/Runtime/Internal/Module.cs b/crates/bindings-csharp/Runtime/Internal/Module.cs index 9d62a2e14b1..cf69ab8aa11 100644 --- a/crates/bindings-csharp/Runtime/Internal/Module.cs +++ b/crates/bindings-csharp/Runtime/Internal/Module.cs @@ -42,11 +42,10 @@ public static class Module private static readonly RawModuleDefV9 moduleDef = new(); private static readonly List reducers = []; - private static Func? newContext = - null; + private static Func? newContext = null; public static void SetReducerContextConstructor( - Func ctor + Func ctor ) => newContext = ctor; readonly struct TypeRegistrar() : ITypeRegistrar @@ -172,7 +171,7 @@ public static Errno __call_reducer__( ulong sender_3, ulong address_0, ulong address_1, - DateTimeOffsetRepr timestamp, + Timestamp timestamp, BytesSource args, BytesSink error ) @@ -185,7 +184,7 @@ BytesSink error var senderAddress = Address.From( MemoryMarshal.AsBytes([address_0, address_1]).ToArray() ); - var random = new Random((int)timestamp.MicrosecondsSinceEpoch); + var random = new Random((int)timestamp.MicrosecondsSinceUnixEpoch); var time = timestamp.ToStd(); var ctx = newContext!(senderIdentity, senderAddress, random, time); diff --git a/crates/bindings-sys/src/lib.rs b/crates/bindings-sys/src/lib.rs index 5ee13c1b4b7..84a074ff254 100644 --- a/crates/bindings-sys/src/lib.rs +++ b/crates/bindings-sys/src/lib.rs @@ -650,7 +650,7 @@ pub mod raw { mod module_exports { type Encoded = Buffer; type Identity = Encoded<[u8; 32]>; - /// microseconds since the unix epoch + /// Microseconds since the unix epoch type Timestamp = u64; /// Buffer::INVALID => Ok(()); else errmsg => Err(errmsg) type Result = Buffer; diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index ce81767214e..1a83a4a4a1f 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -10,7 +10,6 @@ mod rng; pub mod rt; #[doc(hidden)] pub mod table; -mod timestamp; use spacetimedb_lib::bsatn; use std::cell::RefCell; @@ -38,10 +37,11 @@ pub use spacetimedb_lib::Address; pub use spacetimedb_lib::AlgebraicValue; pub use spacetimedb_lib::Identity; pub use spacetimedb_lib::ScheduleAt; +pub use spacetimedb_lib::TimeDuration; +pub use spacetimedb_lib::Timestamp; pub use spacetimedb_primitives::TableId; pub use sys::Errno; pub use table::{AutoIncOverflow, RangedIndex, Table, TryInsertError, UniqueColumn, UniqueConstraintViolation}; -pub use timestamp::Timestamp; pub type ReducerResult = core::result::Result<(), Box>; diff --git a/crates/bindings/src/rng.rs b/crates/bindings/src/rng.rs index 883e669bd57..cdccbcdc7c6 100644 --- a/crates/bindings/src/rng.rs +++ b/crates/bindings/src/rng.rs @@ -44,7 +44,7 @@ impl ReducerContext { /// For more information, see [`StdbRng`] and [`rand::Rng`]. pub fn rng(&self) -> &StdbRng { self.rng.get_or_init(|| StdbRng { - rng: StdRng::seed_from_u64(self.timestamp.micros_since_epoch).into(), + rng: StdRng::seed_from_u64(self.timestamp.to_micros_since_unix_epoch() as u64).into(), _marker: PhantomData, }) } diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index dfc37fd27ef..4960f16626b 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -1,20 +1,18 @@ #![deny(unsafe_op_in_unsafe_fn)] use crate::table::IndexAlgo; -use crate::timestamp::with_timestamp_set; -use crate::{sys, IterBuf, ReducerContext, ReducerResult, SpacetimeType, Table, Timestamp}; +use crate::{sys, IterBuf, ReducerContext, ReducerResult, SpacetimeType, Table}; pub use spacetimedb_lib::db::raw_def::v9::Lifecycle as LifecycleReducer; use spacetimedb_lib::db::raw_def::v9::{RawIndexAlgorithm, RawModuleDefV9Builder, TableType}; use spacetimedb_lib::de::{self, Deserialize, SeqProductAccess}; use spacetimedb_lib::sats::typespace::TypespaceBuilder; use spacetimedb_lib::sats::{impl_deserialize, impl_serialize, ProductTypeElement}; use spacetimedb_lib::ser::{Serialize, SerializeSeqProduct}; -use spacetimedb_lib::{bsatn, Address, Identity, ProductType, RawModuleDef}; +use spacetimedb_lib::{bsatn, Address, Identity, ProductType, RawModuleDef, Timestamp}; use spacetimedb_primitives::*; use std::fmt; use std::marker::PhantomData; use std::sync::{Mutex, OnceLock}; -use std::time::Duration; use sys::raw::{BytesSink, BytesSource}; /// The `sender` invokes `reducer` at `timestamp` and provides it with the given `args`. @@ -29,8 +27,7 @@ pub fn invoke_reducer<'a, A: Args<'a>>( // Deserialize the arguments from a bsatn encoding. let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args"); - // Run the reducer with the environment all set up. - with_timestamp_set(ctx.timestamp, || reducer.invoke(&ctx, args)) + reducer.invoke(&ctx, args) } /// A trait for types representing the *execution logic* of a reducer. #[diagnostic::on_unimplemented( @@ -289,22 +286,6 @@ impl_serialize!(['de, A: Args<'de>] SerDeArgs, (self, ser) => { prod.end() }); -/// A trait for types representing repeater arguments. -pub trait RepeaterArgs: for<'de> Args<'de> { - /// Returns a notion of now in time. - fn get_now() -> Self; -} - -impl RepeaterArgs for () { - fn get_now() -> Self {} -} - -impl RepeaterArgs for (Timestamp,) { - fn get_now() -> Self { - (Timestamp::now(),) - } -} - /// A trait for types that can *describe* a row-level security policy. pub trait RowLevelSecurityInfo { /// The SQL expression for the row-level security policy. @@ -424,7 +405,7 @@ struct ModuleBuilder { // Not actually a mutex; because WASM is single-threaded this basically just turns into a refcell. static DESCRIBERS: Mutex>> = Mutex::new(Vec::new()); -/// A reducer function takes in `(Sender, Timestamp, Args)` +/// A reducer function takes in `(ReducerContext, Args)` /// and returns a result with a possible error message. pub type ReducerFn = fn(ReducerContext, &[u8]) -> ReducerResult; static REDUCERS: OnceLock> = OnceLock::new(); @@ -522,7 +503,7 @@ extern "C" fn __call_reducer__( let address = (address != Address::__DUMMY).then_some(address); // Assemble the `ReducerContext`. - let timestamp = Timestamp::UNIX_EPOCH + Duration::from_micros(timestamp); + let timestamp = Timestamp::from_micros_since_unix_epoch(timestamp as i64); let ctx = ReducerContext { db: crate::Local {}, sender, diff --git a/crates/bindings/src/timestamp.rs b/crates/bindings/src/timestamp.rs deleted file mode 100644 index c74bb24583a..00000000000 --- a/crates/bindings/src/timestamp.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Defines a `Timestamp` abstraction. - -use std::ops::{Add, Sub}; -use std::time::Duration; - -use spacetimedb_lib::sats::{impl_deserialize, impl_serialize, impl_st}; - -scoped_tls::scoped_thread_local! { - static CURRENT_TIMESTAMP: Timestamp -} - -/// Set the current timestamp for the duration of the function `f`. -pub(crate) fn with_timestamp_set(ts: Timestamp, f: impl FnOnce() -> R) -> R { - CURRENT_TIMESTAMP.set(&ts, f) -} - -/// A timestamp measured as micro seconds since the UNIX epoch. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Timestamp { - /// The number of micro seconds since the UNIX epoch. - pub(crate) micros_since_epoch: u64, -} - -impl Timestamp { - /// The timestamp 0 micro seconds since the UNIX epoch. - pub const UNIX_EPOCH: Self = Timestamp { micros_since_epoch: 0 }; - - /// Returns a timestamp of how many micros have passed right now since UNIX epoch. - /// - /// Panics if not in the context of a reducer. - pub fn now() -> Timestamp { - assert!(CURRENT_TIMESTAMP.is_set(), "there is no current time in this context"); - CURRENT_TIMESTAMP.with(|x| *x) - } - - /// Returns how many micros have passed since the UNIX epoch as a `Duration`. - pub fn elapsed(&self) -> Duration { - Self::now() - .duration_since(*self) - .expect("timestamp for elapsed() is after current time") - } - - /// Returns the absolute difference between this and an `earlier` timestamp as a `Duration`. - /// - /// Returns an error when `earlier >= self`. - pub fn duration_since(&self, earlier: Timestamp) -> Result { - let dur = Duration::from_micros(self.micros_since_epoch.abs_diff(earlier.micros_since_epoch)); - if earlier < *self { - Ok(dur) - } else { - Err(dur) - } - } - - /// Returns a timestamp with `duration` added to `self`. - /// - /// Returns `None` when a `u64` is overflowed. - pub fn checked_add(&self, duration: Duration) -> Option { - let micros = duration.as_micros().try_into().ok()?; - let micros_since_epoch = self.micros_since_epoch.checked_add(micros)?; - Some(Self { micros_since_epoch }) - } - - /// Returns a timestamp with `duration` subtracted from `self`. - /// - /// Returns `None` when a `u64` is overflowed. - pub fn checked_sub(&self, duration: Duration) -> Option { - let micros = duration.as_micros().try_into().ok()?; - let micros_since_epoch = self.micros_since_epoch.checked_sub(micros)?; - Some(Self { micros_since_epoch }) - } - - /// Converts the timestamp into the number of microseconds since the UNIX epoch. - pub fn into_micros_since_epoch(self) -> u64 { - self.micros_since_epoch - } - - /// Creates a new timestamp from the given number of microseconds since the UNIX epoch. - pub fn from_micros_since_epoch(micros_since_epoch: u64) -> Self { - Self { micros_since_epoch } - } -} - -impl From for spacetimedb_lib::ScheduleAt { - fn from(ts: Timestamp) -> Self { - Self::Time(ts.into_micros_since_epoch()) - } -} - -impl Add for Timestamp { - type Output = Timestamp; - - fn add(self, rhs: Duration) -> Self::Output { - self.checked_add(rhs) - .expect("overflow when adding duration to timestamp") - } -} - -impl Sub for Timestamp { - type Output = Timestamp; - - fn sub(self, rhs: Duration) -> Self::Output { - self.checked_sub(rhs) - .expect("underflow when subtracting duration from timestamp") - } -} - -impl_st!([] Timestamp, spacetimedb_lib::AlgebraicType::U64); -impl_deserialize!([] Timestamp, de => u64::deserialize(de).map(Self::from_micros_since_epoch)); -impl_serialize!([] Timestamp, (self, ser) => self.into_micros_since_epoch().serialize(ser)); diff --git a/crates/cli/src/subcommands/generate/csharp.rs b/crates/cli/src/subcommands/generate/csharp.rs index 0606983818c..afaa7f24778 100644 --- a/crates/cli/src/subcommands/generate/csharp.rs +++ b/crates/cli/src/subcommands/generate/csharp.rs @@ -477,6 +477,8 @@ fn ty_fmt<'a>(module: &'a ModuleDef, ty: &'a AlgebraicTypeUse) -> impl fmt::Disp AlgebraicTypeUse::Identity => f.write_str("SpacetimeDB.Identity"), AlgebraicTypeUse::Address => f.write_str("SpacetimeDB.Address"), AlgebraicTypeUse::ScheduleAt => f.write_str("SpacetimeDB.ScheduleAt"), + AlgebraicTypeUse::Timestamp => f.write_str("SpacetimeDB.Timestamp"), + AlgebraicTypeUse::TimeDuration => f.write_str("SpacetimeDB.TimeDuration"), AlgebraicTypeUse::Unit => f.write_str("SpacetimeDB.Unit"), AlgebraicTypeUse::Option(inner_ty) => write!(f, "{}?", ty_fmt(module, inner_ty)), AlgebraicTypeUse::Array(elem_ty) => write!(f, "System.Collections.Generic.List<{}>", ty_fmt(module, elem_ty)), @@ -522,7 +524,11 @@ fn default_init(ctx: &TypespaceForGenerate, ty: &AlgebraicTypeUse) -> Option<&'s // Primitives are initialized to zero automatically. AlgebraicTypeUse::Primitive(_) => None, // these are structs, they are initialized to zero-filled automatically - AlgebraicTypeUse::Unit | AlgebraicTypeUse::Identity | AlgebraicTypeUse::Address => None, + AlgebraicTypeUse::Unit + | AlgebraicTypeUse::Identity + | AlgebraicTypeUse::Address + | AlgebraicTypeUse::Timestamp + | AlgebraicTypeUse::TimeDuration => None, AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in C# output"), } } diff --git a/crates/cli/src/subcommands/generate/rust.rs b/crates/cli/src/subcommands/generate/rust.rs index 0d9fe972c8f..18002ad3a83 100644 --- a/crates/cli/src/subcommands/generate/rust.rs +++ b/crates/cli/src/subcommands/generate/rust.rs @@ -583,6 +583,8 @@ pub fn write_type(module: &ModuleDef, out: &mut W, ty: &AlgebraicTypeU AlgebraicTypeUse::Never => write!(out, "std::convert::Infallible")?, AlgebraicTypeUse::Identity => write!(out, "__sdk::Identity")?, AlgebraicTypeUse::Address => write!(out, "__sdk::Address")?, + AlgebraicTypeUse::Timestamp => write!(out, "__sdk::Timestamp")?, + AlgebraicTypeUse::TimeDuration => write!(out, "__sdk::TimeDuration")?, AlgebraicTypeUse::ScheduleAt => write!(out, "__sdk::ScheduleAt")?, AlgebraicTypeUse::Option(inner_ty) => { write!(out, "Option::<")?; diff --git a/crates/cli/src/subcommands/generate/typescript.rs b/crates/cli/src/subcommands/generate/typescript.rs index 3982b1fdc29..b84cf87d616 100644 --- a/crates/cli/src/subcommands/generate/typescript.rs +++ b/crates/cli/src/subcommands/generate/typescript.rs @@ -580,6 +580,8 @@ fn print_spacetimedb_imports(out: &mut Indenter) { "AlgebraicValue", "Identity", "Address", + "Timestamp", + "TimeDuration", "DBConnectionBuilder", "TableCache", "BinaryWriter", @@ -915,9 +917,11 @@ pub fn write_type( AlgebraicTypeUse::Never => write!(out, "never")?, AlgebraicTypeUse::Identity => write!(out, "Identity")?, AlgebraicTypeUse::Address => write!(out, "Address")?, + AlgebraicTypeUse::Timestamp => write!(out, "Timestamp")?, + AlgebraicTypeUse::TimeDuration => write!(out, "TimeDuration")?, AlgebraicTypeUse::ScheduleAt => write!( out, - "{{ tag: \"Interval\", value: bigint }} | {{ tag: \"Time\", value: bigint }}" + "{{ tag: \"Interval\", value: TimeDuration }} | {{ tag: \"Time\", value: Timestamp }}" )?, AlgebraicTypeUse::Option(inner_ty) => { write_type(module, out, inner_ty, ref_prefix)?; @@ -968,6 +972,8 @@ fn convert_algebraic_type<'a>( AlgebraicTypeUse::ScheduleAt => write!(out, "AlgebraicType.createScheduleAtType()"), AlgebraicTypeUse::Identity => write!(out, "AlgebraicType.createIdentityType()"), AlgebraicTypeUse::Address => write!(out, "AlgebraicType.createAddressType()"), + AlgebraicTypeUse::Timestamp => write!(out, "AlgebraicType.createTimestampType()"), + AlgebraicTypeUse::TimeDuration => write!(out, "AlgebraicType.createTimeDurationType()"), AlgebraicTypeUse::Option(inner_ty) => { write!(out, "AlgebraicType.createOptionType("); convert_algebraic_type(module, out, inner_ty, ref_prefix); diff --git a/crates/cli/src/tasks/csharp.rs b/crates/cli/src/tasks/csharp.rs index be2a764a4aa..38956663755 100644 --- a/crates/cli/src/tasks/csharp.rs +++ b/crates/cli/src/tasks/csharp.rs @@ -74,19 +74,33 @@ pub(crate) fn build_csharp(project_path: &Path, build_debug: bool) -> anyhow::Re } else { "AppBundle" }; - let mut output_path = project_path.join(format!("bin/{config_name}/net8.0/wasi-wasm/{subdir}/StdbModule.wasm")); - if !output_path.exists() { - // check for the old .NET 7 path for projects that haven't migrated yet - output_path = project_path.join(format!("bin/{config_name}/net7.0/StdbModule.wasm")); + // TODO: This code looks for build outputs in both `bin` and `bin~` as output directories. @bfops feels like we shouldn't have to look for `bin~`, since the `~` suffix is just intended to cause Unity to ignore directories, and that shouldn't be relevant here. We do think we've seen `bin~` appear though, and it's not harmful to do the extra checks, so we're merging for now due to imminent code freeze. At some point, it would be good to figure out if we do actually see `bin~` in module directories, and where that's coming from (which could suggest a bug). + // check for the old .NET 7 path for projects that haven't migrated yet + let bad_output_paths = [ + project_path.join(format!("bin/{config_name}/net7.0/StdbModule.wasm")), + // for some reason there is sometimes a tilde here? + project_path.join(format!("bin~/{config_name}/net7.0/StdbModule.wasm")), + ]; + if bad_output_paths.iter().any(|p| p.exists()) { + anyhow::bail!(concat!( + "Looks like your project is using the deprecated .NET 7.0 WebAssembly bindings.\n", + "Please migrate your project to the new .NET 8.0 template and delete the folders: bin, bin~, obj, obj~" + )); + } + let possible_output_paths = [ + project_path.join(format!("bin/{config_name}/net8.0/wasi-wasm/{subdir}/StdbModule.wasm")), + project_path.join(format!("bin~/{config_name}/net8.0/wasi-wasm/{subdir}/StdbModule.wasm")), + ]; + if possible_output_paths.iter().all(|p| p.exists()) { + anyhow::bail!(concat!( + "For some reason, your project has both a `bin` and a `bin~` folder.\n", + "I don't know which to use, so please delete both and rerun this command so that we can see which is up-to-date." + )); + } + for output_path in possible_output_paths { if output_path.exists() { - anyhow::bail!(concat!( - "Looks like your project is using the deprecated .NET 7.0 WebAssembly bindings.\n", - "Please migrate your project to the new .NET 8.0 template." - )); - } else { - anyhow::bail!("Built project successfully but couldn't find the output file."); + return Ok(output_path); } } - - Ok(output_path) + anyhow::bail!("Built project successfully but couldn't find the output file."); } diff --git a/crates/cli/tests/snapshots/codegen__codegen_csharp.snap b/crates/cli/tests/snapshots/codegen__codegen_csharp.snap index 1b39d791f93..c849b954734 100644 --- a/crates/cli/tests/snapshots/codegen__codegen_csharp.snap +++ b/crates/cli/tests/snapshots/codegen__codegen_csharp.snap @@ -1456,12 +1456,12 @@ namespace SpacetimeDB [DataMember(Name = "scheduled_at")] public SpacetimeDB.ScheduleAt ScheduledAt; [DataMember(Name = "prev_time")] - public ulong PrevTime; + public SpacetimeDB.Timestamp PrevTime; public RepeatingTestArg( ulong ScheduledId, SpacetimeDB.ScheduleAt ScheduledAt, - ulong PrevTime + SpacetimeDB.Timestamp PrevTime ) { this.ScheduledId = ScheduledId; diff --git a/crates/cli/tests/snapshots/codegen__codegen_rust.snap b/crates/cli/tests/snapshots/codegen__codegen_rust.snap index 64495bc9871..a6d1788188f 100644 --- a/crates/cli/tests/snapshots/codegen__codegen_rust.snap +++ b/crates/cli/tests/snapshots/codegen__codegen_rust.snap @@ -2899,7 +2899,7 @@ use spacetimedb_sdk::__codegen::{ pub struct RepeatingTestArg { pub scheduled_id: u64, pub scheduled_at: __sdk::ScheduleAt, - pub prev_time: u64, + pub prev_time: __sdk::Timestamp, } diff --git a/crates/cli/tests/snapshots/codegen__codegen_typescript.snap b/crates/cli/tests/snapshots/codegen__codegen_typescript.snap index 6e74e910b5e..fc0d0c4a0de 100644 --- a/crates/cli/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/cli/tests/snapshots/codegen__codegen_typescript.snap @@ -42,6 +42,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -114,6 +118,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -186,6 +194,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -255,6 +267,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type Baz = { @@ -327,6 +343,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -396,6 +416,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -468,6 +492,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -540,6 +568,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; // @ts-ignore @@ -628,6 +660,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { HasSpecialStuff } from "./has_special_stuff_type"; @@ -716,6 +752,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type HasSpecialStuff = { @@ -790,6 +830,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -1287,6 +1331,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { Player } from "./player_type"; @@ -1449,6 +1497,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; // A namespace for generated variants and helper functions. @@ -1531,6 +1583,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; // A namespace for generated variants and helper functions. @@ -1616,6 +1672,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { PkMultiIdentity } from "./pk_multi_identity_type"; @@ -1756,6 +1816,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type PkMultiIdentity = { @@ -1830,6 +1894,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { Player } from "./player_type"; @@ -1992,6 +2060,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type Player = { @@ -2068,6 +2140,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type Point = { @@ -2142,6 +2218,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { Point } from "./point_type"; @@ -2230,6 +2310,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { Private } from "./private_type"; @@ -2318,6 +2402,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type Private = { @@ -2390,6 +2478,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -2459,6 +2551,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { RepeatingTestArg } from "./repeating_test_arg_type"; @@ -2577,12 +2673,16 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type RepeatingTestArg = { scheduledId: bigint, - scheduledAt: { tag: "Interval", value: bigint } | { tag: "Time", value: bigint }, - prevTime: bigint, + scheduledAt: { tag: "Interval", value: TimeDuration } | { tag: "Time", value: Timestamp }, + prevTime: Timestamp, }; /** @@ -2597,7 +2697,7 @@ export namespace RepeatingTestArg { return AlgebraicType.createProductType([ new ProductTypeElement("scheduledId", AlgebraicType.createU64Type()), new ProductTypeElement("scheduledAt", AlgebraicType.createScheduleAtType()), - new ProductTypeElement("prevTime", AlgebraicType.createU64Type()), + new ProductTypeElement("prevTime", AlgebraicType.createTimestampType()), ]); } @@ -2653,6 +2753,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -2728,6 +2832,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { TestA } from "./test_a_type"; @@ -2816,6 +2924,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type TestA = { @@ -2892,6 +3004,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type TestB = { @@ -2964,6 +3080,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; @@ -3033,6 +3153,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { TestD } from "./test_d_type"; @@ -3124,6 +3248,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; // @ts-ignore @@ -3199,6 +3327,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { TestE } from "./test_e_type"; @@ -3317,6 +3449,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; export type TestE = { @@ -3391,6 +3527,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; import { TestFoobar } from "./test_foobar_type"; @@ -3482,6 +3622,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; // @ts-ignore @@ -3557,6 +3701,10 @@ import { // @ts-ignore TableCache, // @ts-ignore + TimeDuration, + // @ts-ignore + Timestamp, + // @ts-ignore deepEqual, } from "@clockworklabs/spacetimedb-sdk"; diff --git a/crates/client-api-messages/DEVELOP.md b/crates/client-api-messages/DEVELOP.md index 7956459d199..554fe37f654 100644 --- a/crates/client-api-messages/DEVELOP.md +++ b/crates/client-api-messages/DEVELOP.md @@ -7,6 +7,6 @@ In this directory: ```sh cargo run --example get_ws_schema > ws_schema.json spacetime generate --lang \ - --out-dir + --out-dir \ --module-def ws_schema.json ``` diff --git a/crates/client-api-messages/src/lib.rs b/crates/client-api-messages/src/lib.rs index 8ff382909be..dcf9c684fc2 100644 --- a/crates/client-api-messages/src/lib.rs +++ b/crates/client-api-messages/src/lib.rs @@ -2,5 +2,4 @@ pub mod energy; pub mod name; -pub mod timestamp; pub mod websocket; diff --git a/crates/client-api-messages/src/timestamp.rs b/crates/client-api-messages/src/timestamp.rs deleted file mode 100644 index 4b006368dba..00000000000 --- a/crates/client-api-messages/src/timestamp.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::time::{Duration, SystemTime}; - -use spacetimedb_sats::SpacetimeType; - -/// A timestamp, as a number of microseconds since the UNIX epoch. -// Ideally this should be a transparent newtype, -// incl. `serde(transparent)` and some future `sats(transparent)` marker. -// Because `sats(transparent)` is not currently designed or implemented (as of 2024-06-20), -// we define this as a regular struct, which serializes as a `ProductValue`. -#[derive(SpacetimeType, Copy, Clone, PartialEq, Eq, Debug, serde::Serialize)] -#[sats(crate = spacetimedb_sats)] -pub struct Timestamp { - pub microseconds: u64, -} - -impl Timestamp { - pub fn from_microseconds(microseconds: u64) -> Self { - Timestamp { microseconds } - } - pub fn now() -> Self { - Self::from_systemtime(SystemTime::now()) - } - pub fn from_systemtime(systime: SystemTime) -> Self { - let dur = systime.duration_since(SystemTime::UNIX_EPOCH).expect("hello, 1969"); - // UNIX_EPOCH + u64::MAX microseconds is in 586524 CE, so it's probably fine to cast - Self { - microseconds: dur.as_micros() as u64, - } - } - pub fn to_systemtime(self) -> SystemTime { - SystemTime::UNIX_EPOCH + Duration::from_micros(self.microseconds) - } - pub fn to_duration_from_now(self) -> Duration { - self.to_systemtime() - .duration_since(SystemTime::now()) - .unwrap_or(Duration::ZERO) - } -} diff --git a/crates/client-api-messages/src/websocket.rs b/crates/client-api-messages/src/websocket.rs index be3b53c680b..30b76a27727 100644 --- a/crates/client-api-messages/src/websocket.rs +++ b/crates/client-api-messages/src/websocket.rs @@ -15,7 +15,6 @@ //! rather than using an external mirror of this schema. use crate::energy::EnergyQuanta; -use crate::timestamp::Timestamp; use bytes::Bytes; use bytestring::ByteString; use core::{ @@ -24,7 +23,7 @@ use core::{ }; use enum_as_inner::EnumAsInner; use smallvec::SmallVec; -use spacetimedb_lib::{Address, Identity}; +use spacetimedb_lib::{Address, Identity, TimeDuration, Timestamp}; use spacetimedb_primitives::TableId; use spacetimedb_sats::{ bsatn::{self, ToBsatn}, @@ -405,7 +404,7 @@ pub struct InitialSubscription { /// The server will include the same request_id in the response. pub request_id: u32, /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, + pub total_host_execution_duration: TimeDuration, } /// Received by database from client to inform of user's identity, token and client address. @@ -437,7 +436,9 @@ pub struct IdentityToken { pub struct TransactionUpdate { /// The status of the transaction. Contains the updated rows, if successful. pub status: UpdateStatus, - /// The time when the reducer started, as microseconds since the Unix epoch. + /// The time when the reducer started. + /// + /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. pub timestamp: Timestamp, /// The identity of the user who requested the reducer run. For event-driven and /// scheduled reducers, it is the identity of the database owner. @@ -456,7 +457,7 @@ pub struct TransactionUpdate { /// The amount of energy credits consumed by running the reducer. pub energy_quanta_used: EnergyQuanta, /// How long the reducer took to run. - pub host_execution_duration_micros: u64, + pub total_host_execution_duration: TimeDuration, } /// Received by client from database upon a reducer run. @@ -645,7 +646,7 @@ pub struct OneOffQueryResponse { pub tables: Box<[OneOffTable]>, /// The total duration of query compilation and evaluation on the server, in microseconds. - pub total_host_execution_duration_micros: u64, + pub total_host_execution_duration: TimeDuration, } /// A table included as part of a [`OneOffQueryResponse`]. diff --git a/crates/core/src/client/client_connection.rs b/crates/core/src/client/client_connection.rs index cdf94aac1b1..04528129bdd 100644 --- a/crates/core/src/client/client_connection.rs +++ b/crates/core/src/client/client_connection.rs @@ -334,7 +334,7 @@ impl ClientConnection { ) -> OneOffQueryResponseMessage { let result = self.module.one_off_query::(self.id.identity, query.to_owned()); let message_id = message_id.to_owned(); - let total_host_execution_duration = timer.elapsed().as_micros() as u64; + let total_host_execution_duration = timer.elapsed().into(); match result { Ok(results) => OneOffQueryResponseMessage { message_id, diff --git a/crates/core/src/client/message_handlers.rs b/crates/core/src/client/message_handlers.rs index de88fe38d9c..e31d4e1ec63 100644 --- a/crates/core/src/client/message_handlers.rs +++ b/crates/core/src/client/message_handlers.rs @@ -3,7 +3,7 @@ use super::{ClientConnection, DataMessage, Protocol}; use crate::energy::EnergyQuanta; use crate::execution_context::WorkloadType; use crate::host::module_host::{EventStatus, ModuleEvent, ModuleFunctionCall}; -use crate::host::{ReducerArgs, ReducerId, Timestamp}; +use crate::host::{ReducerArgs, ReducerId}; use crate::identity::Identity; use crate::messages::websocket::{CallReducer, ClientMessage, OneOffQuery}; use crate::worker_metrics::WORKER_METRICS; @@ -11,6 +11,7 @@ use bytes::Bytes; use bytestring::ByteString; use spacetimedb_lib::de::serde::DeserializeWrapper; use spacetimedb_lib::identity::RequestId; +use spacetimedb_lib::Timestamp; use spacetimedb_lib::{bsatn, Address}; use std::sync::Arc; use std::time::{Duration, Instant}; diff --git a/crates/core/src/client/messages.rs b/crates/core/src/client/messages.rs index 42d21eea995..a7b1cb5e11b 100644 --- a/crates/core/src/client/messages.rs +++ b/crates/core/src/client/messages.rs @@ -10,7 +10,7 @@ use spacetimedb_client_api_messages::websocket::{ }; use spacetimedb_lib::identity::RequestId; use spacetimedb_lib::ser::serde::SerializeWrapper; -use spacetimedb_lib::Address; +use spacetimedb_lib::{Address, TimeDuration}; use spacetimedb_primitives::TableId; use spacetimedb_sats::bsatn; use std::sync::Arc; @@ -170,7 +170,7 @@ impl ToProtocol for TransactionUpdateMessage { request_id, }, energy_quanta_used: event.energy_quanta_used, - host_execution_duration_micros: event.host_execution_duration.as_micros() as u64, + total_host_execution_duration: event.host_execution_duration.into(), caller_address: event.caller_address.unwrap_or(Address::ZERO), }; @@ -231,7 +231,7 @@ impl ToProtocol for SubscriptionUpdateMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, protocol: Protocol) -> Self::Encoded { let request_id = self.request_id.unwrap_or(0); - let total_host_execution_duration_micros = self.timer.map_or(0, |t| t.elapsed().as_micros() as u64); + let total_host_execution_duration = self.timer.map_or(TimeDuration::ZERO, |t| t.elapsed().into()); protocol.assert_matches_format_switch(&self.database_update); match self.database_update { @@ -239,14 +239,14 @@ impl ToProtocol for SubscriptionUpdateMessage { FormatSwitch::Bsatn(ws::ServerMessage::InitialSubscription(ws::InitialSubscription { database_update, request_id, - total_host_execution_duration_micros, + total_host_execution_duration, })) } FormatSwitch::Json(database_update) => { FormatSwitch::Json(ws::ServerMessage::InitialSubscription(ws::InitialSubscription { database_update, request_id, - total_host_execution_duration_micros, + total_host_execution_duration, })) } } @@ -409,7 +409,7 @@ pub struct OneOffQueryResponseMessage { pub message_id: Vec, pub error: Option, pub results: Vec>, - pub total_host_execution_duration: u64, + pub total_host_execution_duration: TimeDuration, } impl OneOffQueryResponseMessage { @@ -420,6 +420,7 @@ impl OneOffQueryResponseMessage { impl ToProtocol for OneOffQueryResponseMessage { type Encoded = SwitchedServerMessage; + fn to_protocol(self, _: Protocol) -> Self::Encoded { FormatSwitch::Bsatn(convert(self)) } @@ -437,6 +438,6 @@ fn convert(msg: OneOffQueryResponseMessage) -> ws::Server message_id: msg.message_id.into(), error: msg.error.map(Into::into), tables: msg.results.into_boxed_slice(), - total_host_execution_duration_micros: msg.total_host_execution_duration, + total_host_execution_duration: msg.total_host_execution_duration, }) } diff --git a/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs b/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs index c8aba57fece..168d862dde3 100644 --- a/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs +++ b/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs @@ -1025,7 +1025,7 @@ mod tests { use spacetimedb_lib::db::auth::{StAccess, StTableType}; use spacetimedb_lib::error::ResultTest; use spacetimedb_lib::st_var::StVarValue; - use spacetimedb_lib::{resolved_type_via_v9, ScheduleAt}; + use spacetimedb_lib::{resolved_type_via_v9, ScheduleAt, TimeDuration}; use spacetimedb_primitives::{col_list, ColId, ScheduleId}; use spacetimedb_sats::algebraic_value::ser::value_serialize; use spacetimedb_sats::{product, AlgebraicType, GroundSpacetimeType}; @@ -2597,7 +2597,10 @@ mod tests { .expect("there should be an index with this name"); // Make us a row and insert + identity update. - let row = &product![24u64, value_serialize(&ScheduleAt::Interval(42))]; + let row = &product![ + 24u64, + value_serialize(&ScheduleAt::Interval(TimeDuration::from_micros(42))) + ]; let row = &to_vec(row).unwrap(); let (_, _, insert_flags) = datastore.insert_mut_tx(&mut tx, table_id, row)?; let (_, _, update_flags) = datastore.update_mut_tx(&mut tx, table_id, index_id, row)?; diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 02f01d92d9c..fae0a3d8ec1 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -1623,11 +1623,11 @@ mod tests { use commitlog::Commitlog; use durability::EmptyHistory; use pretty_assertions::assert_eq; - use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_data_structures::map::IntMap; use spacetimedb_lib::db::raw_def::v9::{btree, RawTableDefBuilder}; use spacetimedb_lib::error::ResultTest; use spacetimedb_lib::Identity; + use spacetimedb_lib::Timestamp; use spacetimedb_sats::buffer::BufReader; use spacetimedb_sats::product; use spacetimedb_schema::schema::RowLevelSecuritySchema; diff --git a/crates/core/src/execution_context.rs b/crates/core/src/execution_context.rs index 31ef1ecc3b7..2d75d16550a 100644 --- a/crates/core/src/execution_context.rs +++ b/crates/core/src/execution_context.rs @@ -2,9 +2,8 @@ use std::sync::Arc; use bytes::Bytes; use derive_more::Display; -use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_commitlog::{payload::txdata, Varchar}; -use spacetimedb_lib::{Address, Identity}; +use spacetimedb_lib::{Address, Identity, Timestamp}; use spacetimedb_sats::bsatn; /// Represents the context under which a database runtime method is executed. diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index 041f492cbfc..e88405c9d2d 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -27,7 +27,6 @@ pub use host_controller::{ }; pub use module_host::{ModuleHost, NoSuchModule, ReducerCallError, UpdateDatabaseResult}; pub use scheduler::Scheduler; -pub use spacetimedb_client_api_messages::timestamp::Timestamp; #[derive(Debug)] pub enum ReducerArgs { diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index dd86377bda6..16c56d11a5e 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -26,13 +26,13 @@ use futures::{Future, FutureExt}; use indexmap::IndexSet; use itertools::Itertools; use smallvec::SmallVec; -use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_client_api_messages::websocket::{ByteListLen, Compression, OneOffTable, QueryUpdate, WebsocketFormat}; use spacetimedb_data_structures::error_stream::ErrorStream; use spacetimedb_data_structures::map::{HashCollectionExt as _, IntMap}; use spacetimedb_lib::db::raw_def::v9::Lifecycle; use spacetimedb_lib::identity::{AuthCtx, RequestId}; use spacetimedb_lib::Address; +use spacetimedb_lib::Timestamp; use spacetimedb_primitives::{col_list, TableId}; use spacetimedb_query::compile_subscription; use spacetimedb_sats::{algebraic_value, ProductValue}; diff --git a/crates/core/src/host/scheduler.rs b/crates/core/src/host/scheduler.rs index 58e1880f085..b188e0cd16f 100644 --- a/crates/core/src/host/scheduler.rs +++ b/crates/core/src/host/scheduler.rs @@ -5,9 +5,9 @@ use anyhow::anyhow; use futures::StreamExt; use rustc_hash::FxHashMap; use spacetimedb_client_api_messages::energy::EnergyQuanta; -use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_lib::scheduler::ScheduleAt; use spacetimedb_lib::Address; +use spacetimedb_lib::Timestamp; use spacetimedb_primitives::{ColId, TableId}; use spacetimedb_sats::{bsatn::ToBsatn as _, AlgebraicValue}; use spacetimedb_table::table::RowRef; @@ -190,8 +190,8 @@ impl Scheduler { // Assuming a monotonic clock, // this means we may reject some otherwise acceptable schedule calls. // - // If `Timestamp::to_duration_from_now` is not monotonic, - // i.e. `std::time::SystemTime` is not monotonic, + // If `Timestamp::now()`, i.e. `std::time::SystemTime::now()`, + // is not monotonic, // `DelayQueue::insert` may panic. // This will happen if a module attempts to schedule a reducer // with a delay just before the two-year limit, @@ -387,7 +387,9 @@ impl SchedulerActor { let schedule_at = read_schedule_at(schedule_row, id.at_column)?; if let ScheduleAt::Interval(dur) = schedule_at { - let key = self.queue.insert(QueueItem::Id(id), Duration::from_micros(dur)); + let key = self + .queue + .insert(QueueItem::Id(id), dur.to_duration().unwrap_or(Duration::ZERO)); self.key_map.insert(id, key); Ok(true) } else { diff --git a/crates/core/src/host/wasm_common/module_host_actor.rs b/crates/core/src/host/wasm_common/module_host_actor.rs index 061f66537a0..90b0687b1cc 100644 --- a/crates/core/src/host/wasm_common/module_host_actor.rs +++ b/crates/core/src/host/wasm_common/module_host_actor.rs @@ -1,6 +1,5 @@ use anyhow::Context; use bytes::Bytes; -use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_lib::db::raw_def::v9::Lifecycle; use spacetimedb_primitives::TableId; use spacetimedb_schema::auto_migrate::ponder_migrate; @@ -31,6 +30,7 @@ use crate::util::prometheus_handle::HistogramExt; use crate::worker_metrics::WORKER_METRICS; use spacetimedb_lib::buffer::DecodeError; use spacetimedb_lib::identity::AuthCtx; +use spacetimedb_lib::Timestamp; use spacetimedb_lib::{bsatn, Address, RawModuleDef}; use super::*; @@ -523,7 +523,7 @@ impl WasmModuleInstance { self.replica_context().logger.write( database_logger::LogLevel::Error, &database_logger::Record { - ts: chrono::DateTime::from_timestamp_micros(timestamp.microseconds as i64).unwrap(), + ts: chrono::DateTime::from_timestamp_micros(timestamp.to_micros_since_unix_epoch()).unwrap(), target: Some(reducer_name), filename: None, line_number: None, diff --git a/crates/core/src/host/wasmtime/wasmtime_module.rs b/crates/core/src/host/wasmtime/wasmtime_module.rs index a8ae0fe2fdf..affb1c8e7d3 100644 --- a/crates/core/src/host/wasmtime/wasmtime_module.rs +++ b/crates/core/src/host/wasmtime/wasmtime_module.rs @@ -210,7 +210,7 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { sender_3, address_0, address_1, - op.timestamp.microseconds, + op.timestamp.to_micros_since_unix_epoch() as u64, args_source, errors_sink, ), diff --git a/crates/core/src/messages/instance_db_trace_log.rs b/crates/core/src/messages/instance_db_trace_log.rs index 4397c28e335..a6e0520e641 100644 --- a/crates/core/src/messages/instance_db_trace_log.rs +++ b/crates/core/src/messages/instance_db_trace_log.rs @@ -1,4 +1,4 @@ -use spacetimedb_client_api_messages::timestamp::Timestamp; +use spacetimedb_lib::Timestamp; use spacetimedb_primitives::TableId; use spacetimedb_sats::de::Deserialize; use spacetimedb_sats::ser::Serialize; @@ -64,7 +64,7 @@ pub struct CreateIndex { } #[derive(Clone, Serialize, Deserialize)] pub struct InstanceEvent { - pub event_start_epoch_micros: Timestamp, + pub event_start: Timestamp, pub duration_micros: u64, pub r#type: InstanceEventType, } diff --git a/crates/core/src/sql/execute.rs b/crates/core/src/sql/execute.rs index d8d4d670426..855c3c032a0 100644 --- a/crates/core/src/sql/execute.rs +++ b/crates/core/src/sql/execute.rs @@ -16,11 +16,11 @@ use crate::subscription::tx::DeltaTx; use crate::util::slow::SlowQueryLogger; use crate::vm::{check_row_limit, DbProgram, TxMode}; use anyhow::anyhow; -use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_expr::statement::Statement; use spacetimedb_lib::identity::AuthCtx; use spacetimedb_lib::metrics::ExecutionMetrics; use spacetimedb_lib::relation::FieldName; +use spacetimedb_lib::Timestamp; use spacetimedb_lib::{AlgebraicType, ProductType, ProductValue}; use spacetimedb_query::{compile_sql_stmt, execute_dml_stmt, execute_select_stmt}; use spacetimedb_vm::eval::run_ast; diff --git a/crates/core/src/subscription/module_subscription_manager.rs b/crates/core/src/subscription/module_subscription_manager.rs index 75d1c372e87..1d455291b1c 100644 --- a/crates/core/src/subscription/module_subscription_manager.rs +++ b/crates/core/src/subscription/module_subscription_manager.rs @@ -528,8 +528,8 @@ fn send_to_client( mod tests { use std::{sync::Arc, time::Duration}; - use spacetimedb_client_api_messages::timestamp::Timestamp; use spacetimedb_client_api_messages::websocket::QueryId; + use spacetimedb_lib::Timestamp; use spacetimedb_lib::{error::ResultTest, identity::AuthCtx, Address, AlgebraicType, Identity}; use spacetimedb_primitives::TableId; use spacetimedb_subscription::SubscriptionPlan; diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index fa9e48774a3..032aa330813 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -29,6 +29,8 @@ pub use address::Address; pub use identity::Identity; pub use scheduler::ScheduleAt; pub use spacetimedb_sats::hash::{self, hash_bytes, Hash}; +pub use spacetimedb_sats::time_duration::TimeDuration; +pub use spacetimedb_sats::timestamp::Timestamp; pub use spacetimedb_sats::SpacetimeType; pub use spacetimedb_sats::__make_register_reftype; pub use spacetimedb_sats::{self as sats, bsatn, buffer, de, ser}; diff --git a/crates/lib/src/scheduler.rs b/crates/lib/src/scheduler.rs index 0e0ef383755..fc9c7cb9b36 100644 --- a/crates/lib/src/scheduler.rs +++ b/crates/lib/src/scheduler.rs @@ -1,5 +1,6 @@ -use std::{fmt::Debug, time::Duration}; +use std::fmt::Debug; +use spacetimedb_lib::{TimeDuration, Timestamp}; use spacetimedb_sats::{ algebraic_value::de::{ValueDeserializeError, ValueDeserializer}, de::Deserialize, @@ -18,38 +19,64 @@ use spacetimedb_sats::{ #[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum ScheduleAt { /// A regular interval at which the repeated reducer is scheduled. - /// Value is a duration in microseconds. - Interval(u64), + /// Value is a [`TimeDuration`], which has nanosecond precision. + Interval(TimeDuration), /// A specific time to which the reducer is scheduled. - /// Value is a UNIX timestamp in microseconds. - Time(u64), + Time(Timestamp), } impl_st!([] ScheduleAt, ScheduleAt::get_type()); impl ScheduleAt { /// Converts the `ScheduleAt` to a `std::time::Duration` from now. + /// + /// Returns [`Duration::ZERO`] if `self` represents a time in the past. + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] pub fn to_duration_from_now(&self) -> std::time::Duration { + use std::time::{Duration, SystemTime}; match self { ScheduleAt::Time(time) => { - let now = std::time::SystemTime::now(); - // Safety: Now is always after UNIX_EPOCH. - let now = now.duration_since(std::time::UNIX_EPOCH).unwrap(); - let time = std::time::Duration::from_micros(*time); - time.checked_sub(now).unwrap_or(Duration::from_micros(0)) + let now = SystemTime::now(); + let time = SystemTime::from(*time); + time.duration_since(now).unwrap_or(Duration::ZERO) } - ScheduleAt::Interval(dur) => Duration::from_micros(*dur), + // TODO(correctness): Determine useful behavior on negative intervals, + // as that's the case where `to_duration` fails. + // Currently, we use the magnitude / absolute value, + // which seems at least less stupid than clamping to zero. + ScheduleAt::Interval(dur) => dur.to_duration_abs(), } } /// Get the special `AlgebraicType` for `ScheduleAt`. pub fn get_type() -> AlgebraicType { - AlgebraicType::sum([("Interval", AlgebraicType::U64), ("Time", AlgebraicType::U64)]) + AlgebraicType::sum([ + ("Interval", AlgebraicType::time_duration()), + ("Time", AlgebraicType::timestamp()), + ]) + } +} + +impl From for ScheduleAt { + fn from(value: TimeDuration) -> Self { + ScheduleAt::Interval(value) } } impl From for ScheduleAt { fn from(value: std::time::Duration) -> Self { - ScheduleAt::Interval(value.as_micros() as u64) + ScheduleAt::Interval(TimeDuration::from_duration(value)) + } +} + +impl From for ScheduleAt { + fn from(value: std::time::SystemTime) -> Self { + Timestamp::from(value).into() + } +} + +impl From for ScheduleAt { + fn from(value: crate::Timestamp) -> Self { + ScheduleAt::Time(value) } } @@ -67,7 +94,7 @@ mod tests { #[test] fn test_bsatn_roundtrip() { - let schedule_at = ScheduleAt::Interval(10000); + let schedule_at = ScheduleAt::Interval(TimeDuration::from_micros(10000)); let ser = bsatn::to_vec(&schedule_at).unwrap(); let de = bsatn::from_slice(&ser).unwrap(); assert_eq!(schedule_at, de); diff --git a/crates/sats/proptest-regressions/timestamp.txt b/crates/sats/proptest-regressions/timestamp.txt new file mode 100644 index 00000000000..817144e303c --- /dev/null +++ b/crates/sats/proptest-regressions/timestamp.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 9260b4651caa8f40a8bad964329b762b19e6c6d4acf56702caf6f4f160184a5d # shrinks to micros = 910692730085477581 diff --git a/crates/sats/src/algebraic_type.rs b/crates/sats/src/algebraic_type.rs index a96081a6c18..cff0b0ef6cd 100644 --- a/crates/sats/src/algebraic_type.rs +++ b/crates/sats/src/algebraic_type.rs @@ -5,7 +5,7 @@ use crate::algebraic_value::de::{ValueDeserializeError, ValueDeserializer}; use crate::algebraic_value::ser::value_serialize; use crate::de::Deserialize; use crate::meta_type::MetaType; -use crate::product_type::{ADDRESS_TAG, IDENTITY_TAG}; +use crate::product_type::{ADDRESS_TAG, IDENTITY_TAG, TIMESTAMP_TAG, TIME_DURATION_TAG}; use crate::sum_type::{OPTION_NONE_TAG, OPTION_SOME_TAG}; use crate::{i256, u256}; use crate::{AlgebraicTypeRef, AlgebraicValue, ArrayType, ProductType, SpacetimeType, SumType, SumTypeVariant}; @@ -170,6 +170,16 @@ impl AlgebraicType { matches!(self, Self::Product(p) if p.is_identity()) } + /// Returns whether this type is the conventional point-in-time `Timestamp` type. + pub fn is_timestamp(&self) -> bool { + matches!(self, Self::Product(p) if p.is_timestamp()) + } + + /// Returns whether this type is the conventional time-delta `TimeDuration` type. + pub fn is_time_duration(&self) -> bool { + matches!(self, Self::Product(p) if p.is_time_duration()) + } + /// Returns whether this type is the conventional `ScheduleAt` type. pub fn is_schedule_at(&self) -> bool { matches!(self, Self::Sum(p) if p.is_schedule_at()) @@ -299,6 +309,16 @@ impl AlgebraicType { AlgebraicType::product([(ADDRESS_TAG, AlgebraicType::U128)]) } + /// Construct a copy of the point-in-time `Timestamp` type. + pub fn timestamp() -> Self { + AlgebraicType::product([(TIMESTAMP_TAG, AlgebraicType::I64)]) + } + + /// Construct a copy of the time-delta `TimeDuration` type. + pub fn time_duration() -> Self { + AlgebraicType::product([(TIME_DURATION_TAG, AlgebraicType::I64)]) + } + /// Returns a sum type of unit variants with names taken from `var_names`. pub fn simple_enum<'a>(var_names: impl Iterator) -> Self { Self::sum(var_names.into_iter().map(SumTypeVariant::unit).collect::>()) @@ -664,4 +684,16 @@ mod tests { let algebraic_type = AlgebraicType::meta_type(); AlgebraicType::from_value(&algebraic_type.as_value()).expect("No errors."); } + + #[test] + fn special_types_are_special() { + assert!(AlgebraicType::identity().is_identity()); + assert!(AlgebraicType::identity().is_special()); + assert!(AlgebraicType::address().is_address()); + assert!(AlgebraicType::address().is_special()); + assert!(AlgebraicType::timestamp().is_timestamp()); + assert!(AlgebraicType::timestamp().is_special()); + assert!(AlgebraicType::time_duration().is_special()); + assert!(AlgebraicType::time_duration().is_time_duration()); + } } diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index eb7432ebfd5..4cc448f0a04 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -696,5 +696,3 @@ impl_deserialize!([] bytes::Bytes, de => >::deserialize(de).map(Into::in #[cfg(feature = "bytestring")] impl_deserialize!([] bytestring::ByteString, de => ::deserialize(de).map(Into::into)); - -impl_deserialize!([] std::time::SystemTime, de => u64::deserialize(de).map(|micros| std::time::SystemTime::UNIX_EPOCH.checked_add(std::time::Duration::from_micros(micros)).unwrap())); diff --git a/crates/sats/src/lib.rs b/crates/sats/src/lib.rs index 0db23ce3d8c..9b79442897e 100644 --- a/crates/sats/src/lib.rs +++ b/crates/sats/src/lib.rs @@ -22,6 +22,8 @@ pub mod size_of; pub mod sum_type; pub mod sum_type_variant; pub mod sum_value; +pub mod time_duration; +pub mod timestamp; pub mod typespace; #[cfg(any(test, feature = "proptest"))] diff --git a/crates/sats/src/product_type.rs b/crates/sats/src/product_type.rs index 4f1699f10da..42a0fe31ae8 100644 --- a/crates/sats/src/product_type.rs +++ b/crates/sats/src/product_type.rs @@ -11,6 +11,10 @@ use crate::{AlgebraicType, AlgebraicValue, ProductTypeElement, SpacetimeType, Va pub const IDENTITY_TAG: &str = "__identity__"; /// The tag used inside the special `Address` product type. pub const ADDRESS_TAG: &str = "__address__"; +/// The tag used inside the special `Timestamp` product type. +pub const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; +/// The tag used inside the special `TimeDuration` product type. +pub const TIME_DURATION_TAG: &str = "__time_duration_micros__"; /// A structural product type of the factors given by `elements`. /// @@ -93,15 +97,38 @@ impl ProductType { self.is_newtype(ADDRESS_TAG, |i| i.is_u128()) } - /// Returns whether this is a special known `tag`, currently `Address` or `Identity`. + fn is_i64_newtype(&self, expected_tag: &str) -> bool { + match &*self.elements { + [ProductTypeElement { + name: Some(name), + algebraic_type: AlgebraicType::I64, + }] => &**name == expected_tag, + _ => false, + } + } + + /// Returns whether this is the special case of `spacetimedb_lib::Timestamp`. + /// Does not follow `Ref`s. + pub fn is_timestamp(&self) -> bool { + self.is_i64_newtype(TIMESTAMP_TAG) + } + + /// Returns whether this is the special case of `spacetimedb_lib::TimeDuration`. + /// Does not follow `Ref`s. + pub fn is_time_duration(&self) -> bool { + self.is_i64_newtype(TIME_DURATION_TAG) + } + + /// Returns whether this is a special known `tag`, + /// currently `Address`, `Identity`, `Timestamp` or `TimeDuration`. pub fn is_special_tag(tag_name: &str) -> bool { - tag_name == IDENTITY_TAG || tag_name == ADDRESS_TAG + [IDENTITY_TAG, ADDRESS_TAG, TIMESTAMP_TAG, TIME_DURATION_TAG].contains(&tag_name) } /// Returns whether this is a special known type, currently `Address` or `Identity`. /// Does not follow `Ref`s. pub fn is_special(&self) -> bool { - self.is_identity() || self.is_address() + self.is_identity() || self.is_address() || self.is_timestamp() || self.is_time_duration() } /// Returns whether this is a unit type, that is, has no elements. diff --git a/crates/sats/src/ser/impls.rs b/crates/sats/src/ser/impls.rs index 909b5fd2dec..aa127200983 100644 --- a/crates/sats/src/ser/impls.rs +++ b/crates/sats/src/ser/impls.rs @@ -254,9 +254,3 @@ impl_serialize!([] bytes::Bytes, (self, ser) => ser.serialize_bytes(self)); #[cfg(feature = "bytestring")] impl_serialize!([] bytestring::ByteString, (self, ser) => ser.serialize_str(self)); - -impl_serialize!([] std::time::SystemTime, (self, ser) => { - let duration = self.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap(); - let micros: u64 = duration.as_micros().try_into().expect("SystemTime exceeded 2^64 us since the Unix epoch"); - ser.serialize_u64(micros) -}); diff --git a/crates/sats/src/sum_type.rs b/crates/sats/src/sum_type.rs index 7c7a4dc2a12..8d2fcc92177 100644 --- a/crates/sats/src/sum_type.rs +++ b/crates/sats/src/sum_type.rs @@ -129,9 +129,9 @@ impl SumType { match &*self.variants { [first, second] => { first.has_name(SCHEDULE_AT_INTERVAL_TAG) - && first.algebraic_type.is_u64() + && first.algebraic_type.is_time_duration() && second.has_name(SCHEDULE_AT_TIME_TAG) - && second.algebraic_type.is_u64() + && second.algebraic_type.is_timestamp() } _ => false, } diff --git a/crates/sats/src/time_duration.rs b/crates/sats/src/time_duration.rs new file mode 100644 index 00000000000..727aa7ac0b1 --- /dev/null +++ b/crates/sats/src/time_duration.rs @@ -0,0 +1,132 @@ +use crate::timestamp::MICROSECONDS_PER_SECOND; +use crate::{de::Deserialize, impl_st, ser::Serialize, AlgebraicType}; +use std::fmt; +use std::time::Duration; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)] +#[sats(crate = crate)] +/// A span or delta in time, measured in microseconds. +/// +/// Analogous to [`std::time::Duration`], and to C#'s `TimeSpan`. +/// Name chosen to avoid ambiguity with either of those types. +/// +/// Unlike [`Duration`], but like C#'s `TimeSpan`, +/// `TimeDuration` can represent negative values. +/// It also offers less range than [`Duration`], so conversions in both directions may fail. +pub struct TimeDuration { + __time_duration_micros__: i64, +} + +impl_st!([] TimeDuration, AlgebraicType::time_duration()); + +impl TimeDuration { + pub const ZERO: TimeDuration = TimeDuration { + __time_duration_micros__: 0, + }; + + /// Get the number of microseconds `self` represents. + pub fn to_micros(self) -> i64 { + self.__time_duration_micros__ + } + + /// Construct a [`TimeDuration`] which is `micros` microseconds. + /// + /// A positive value means a time after the Unix epoch, + /// and a negative value means a time before. + pub fn from_micros(micros: i64) -> Self { + Self { + __time_duration_micros__: micros, + } + } + + /// Returns `Err(abs(self) as Duration)` if `self` is negative. + pub fn to_duration(self) -> Result { + let micros = self.to_micros(); + if micros >= 0 { + Ok(Duration::from_micros(micros as u64)) + } else { + Err(Duration::from_micros((-micros) as u64)) + } + } + + /// Returns a `Duration` representing the absolute magnitude of `self`. + /// + /// Regardless of whether `self` is positive or negative, the returned `Duration` is positive. + pub fn to_duration_abs(self) -> Duration { + match self.to_duration() { + Ok(dur) | Err(dur) => dur, + } + } + + /// Return a [`TimeDuration`] which represents the same span as `duration`. + /// + /// Panics if `duration.as_micros` overflows an `i64` + pub fn from_duration(duration: Duration) -> Self { + Self::from_micros( + duration + .as_micros() + .try_into() + .expect("Duration overflows i64 microseconds"), + ) + } +} + +impl From for TimeDuration { + fn from(d: Duration) -> TimeDuration { + TimeDuration::from_duration(d) + } +} + +impl TryFrom for Duration { + type Error = Duration; + /// If `d` is negative, returns its magnitude as the `Err` variant. + fn try_from(d: TimeDuration) -> Result { + d.to_duration() + } +} + +impl fmt::Display for TimeDuration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let micros = self.to_micros(); + let sign = if micros < 0 { "-" } else { "+" }; + let pos = micros.abs(); + let secs = pos / MICROSECONDS_PER_SECOND; + let micros_remaining = pos % MICROSECONDS_PER_SECOND; + write!(f, "{sign}{secs}.{micros_remaining:06}") + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::GroundSpacetimeType; + use proptest::prelude::*; + use std::time::SystemTime; + + #[test] + fn timestamp_type_matches() { + assert_eq!(AlgebraicType::time_duration(), TimeDuration::get_type()); + assert!(TimeDuration::get_type().is_time_duration()); + assert!(TimeDuration::get_type().is_special()); + } + + #[test] + fn round_trip_duration_through_time_duration() { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let rounded = Duration::from_micros(now.as_micros() as _); + let time_duration = TimeDuration::from_duration(rounded); + let now_prime = time_duration.to_duration().unwrap(); + assert_eq!(rounded, now_prime); + } + + proptest! { + #[test] + fn round_trip_time_duration_through_systemtime(micros in any::().prop_map(|n| n.abs())) { + let time_duration = TimeDuration::from_micros(micros); + let duration = time_duration.to_duration().unwrap(); + let time_duration_prime = TimeDuration::from_duration(duration); + prop_assert_eq!(time_duration_prime, time_duration); + prop_assert_eq!(time_duration_prime.to_micros(), micros); + } + } +} diff --git a/crates/sats/src/timestamp.rs b/crates/sats/src/timestamp.rs new file mode 100644 index 00000000000..2c8b577c449 --- /dev/null +++ b/crates/sats/src/timestamp.rs @@ -0,0 +1,213 @@ +use crate::{de::Deserialize, impl_st, ser::Serialize, time_duration::TimeDuration, AlgebraicType}; +use std::fmt; +use std::ops::Add; +use std::time::{Duration, SystemTime}; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)] +#[sats(crate = crate)] +/// A point in time, measured in microseconds since the Unix epoch. +pub struct Timestamp { + __timestamp_micros_since_unix_epoch__: i64, +} + +impl_st!([] Timestamp, AlgebraicType::timestamp()); + +impl Timestamp { + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + pub fn now() -> Self { + Self::from_system_time(SystemTime::now()) + } + + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + #[deprecated = "Timestamp::now() is stubbed and will panic. Read the `.timestamp` field of a `ReducerContext` instead."] + pub fn now() -> Self { + unimplemented!() + } + + pub const UNIX_EPOCH: Self = Self { + __timestamp_micros_since_unix_epoch__: 0, + }; + + /// Get the number of microseconds `self` is offset from [`Self::UNIX_EPOCH`]. + /// + /// A positive value means a time after the Unix epoch, + /// and a negative value means a time before. + pub fn to_micros_since_unix_epoch(self) -> i64 { + self.__timestamp_micros_since_unix_epoch__ + } + + /// Construct a [`Timestamp`] which is `micros` microseconds offset from [`Self::UNIX_EPOCH`]. + /// + /// A positive value means a time after the Unix epoch, + /// and a negative value means a time before. + pub fn from_micros_since_unix_epoch(micros: i64) -> Self { + Self { + __timestamp_micros_since_unix_epoch__: micros, + } + } + + pub fn from_time_duration_since_unix_epoch(time_duration: TimeDuration) -> Self { + Self::from_micros_since_unix_epoch(time_duration.to_micros()) + } + + pub fn to_time_duration_since_unix_epoch(self) -> TimeDuration { + TimeDuration::from_micros(self.to_micros_since_unix_epoch()) + } + + /// Returns `Err(duration_before_unix_epoch)` if `self` is before `Self::UNIX_EPOCH`. + pub fn to_duration_since_unix_epoch(self) -> Result { + let micros = self.to_micros_since_unix_epoch(); + if micros >= 0 { + Ok(Duration::from_micros(micros as u64)) + } else { + Err(Duration::from_micros((-micros) as u64)) + } + } + + /// Return a [`Timestamp`] which is [`Timestamp::UNIX_EPOCH`] plus `duration`. + /// + /// Panics if `duration.as_micros` overflows an `i64` + pub fn from_duration_since_unix_epoch(duration: Duration) -> Self { + Self::from_micros_since_unix_epoch( + duration + .as_micros() + .try_into() + .expect("Duration since Unix epoch overflows i64 microseconds"), + ) + } + + /// Convert `self` into a [`SystemTime`] which refers to approximately the same point in time. + /// + /// This conversion may lose precision, as [`SystemTime`]'s prevision varies depending on platform. + /// E.g. Unix targets have microsecond precision, but Windows only 100-microsecond precision. + /// + /// This conversion may panic if `self` is out of bounds for [`SystemTime`]. + /// We are not aware of any platforms for which [`SystemTime`] offers a smaller range than [`Timestamp`], + /// but such a platform may exist. + pub fn to_system_time(self) -> SystemTime { + match self.to_duration_since_unix_epoch() { + Ok(positive) => SystemTime::UNIX_EPOCH + .checked_add(positive) + .expect("Timestamp with i64 microseconds since Unix epoch overflows SystemTime"), + Err(negative) => SystemTime::UNIX_EPOCH + .checked_sub(negative) + .expect("Timestamp with i64 microseconds before Unix epoch overflows SystemTime"), + } + } + + /// Convert a [`SystemTime`] into a [`Timestamp`] which refers to approximately the same point in time. + /// + /// This conversion may panic if `system_time` is out of bounds for [`Duration`]. + /// [`SystemTime`]'s range is larger than [`Timestamp`] on both Unix and Windows targets, + /// so times in the far past or far future may panic. + /// [`Timestamp`]'s range is approximately 292 years before and after the Unix epoch. + pub fn from_system_time(system_time: SystemTime) -> Self { + let duration = system_time + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime predates the Unix epoch"); + Self::from_duration_since_unix_epoch(duration) + } + + /// Returns the [`Duration`] delta between `self` and `earlier`, if `earlier` predates `self`. + /// + /// Returns `None` if `earlier` is strictly greater than `self`, + /// or if the difference between `earlier` and `self` overflows an `i64`. + pub fn duration_since(self, earlier: Timestamp) -> Option { + self.time_duration_since(earlier)?.to_duration().ok() + } + + /// Returns the [`TimeDuration`] delta between `self` and `earlier`. + /// + /// The result may be negative if `earlier` is actually later than `self`. + /// + /// Returns `None` if the subtraction overflows or underflows `i64` microseconds. + pub fn time_duration_since(self, earlier: Timestamp) -> Option { + let delta = self + .to_micros_since_unix_epoch() + .checked_sub(earlier.to_micros_since_unix_epoch())?; + Some(TimeDuration::from_micros(delta)) + } +} + +impl Add for Timestamp { + type Output = Self; + + fn add(self, other: TimeDuration) -> Self::Output { + Timestamp::from_micros_since_unix_epoch(self.to_micros_since_unix_epoch() + other.to_micros()) + } +} + +pub(crate) const MICROSECONDS_PER_SECOND: i64 = 1_000_000; + +impl std::fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let micros = self.to_micros_since_unix_epoch(); + let sign = if micros < 0 { "-" } else { "" }; + let pos = micros.abs(); + let secs = pos / MICROSECONDS_PER_SECOND; + let micros_remaining = pos % MICROSECONDS_PER_SECOND; + + write!(f, "{sign}{secs}.{micros_remaining:06}",) + } +} + +impl From for Timestamp { + fn from(system_time: SystemTime) -> Self { + Self::from_system_time(system_time) + } +} + +impl From for SystemTime { + fn from(timestamp: Timestamp) -> Self { + timestamp.to_system_time() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::GroundSpacetimeType; + use proptest::prelude::*; + + fn round_to_micros(st: SystemTime) -> SystemTime { + let duration = st.duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let micros = duration.as_micros(); + SystemTime::UNIX_EPOCH + Duration::from_micros(micros as _) + } + + #[test] + fn timestamp_type_matches() { + assert_eq!(AlgebraicType::timestamp(), Timestamp::get_type()); + assert!(Timestamp::get_type().is_timestamp()); + assert!(Timestamp::get_type().is_special()); + } + + #[test] + fn round_trip_systemtime_through_timestamp() { + let now = round_to_micros(SystemTime::now()); + let timestamp = Timestamp::from(now); + let now_prime = SystemTime::from(timestamp); + assert_eq!(now, now_prime); + } + + proptest! { + #[test] + fn round_trip_timestamp_through_systemtime(micros in any::().prop_map(|n| n.abs())) { + let timestamp = Timestamp::from_micros_since_unix_epoch(micros); + let system_time = SystemTime::from(timestamp); + let timestamp_prime = Timestamp::from(system_time); + prop_assert_eq!(timestamp_prime, timestamp); + prop_assert_eq!(timestamp_prime.to_micros_since_unix_epoch(), micros); + } + + #[test] + fn add_duration(since_epoch in any::().prop_map(|n| n.abs()), duration in any::()) { + prop_assume!(since_epoch.checked_add(duration).is_some()); + + let timestamp = Timestamp::from_micros_since_unix_epoch(since_epoch); + let time_duration = TimeDuration::from_micros(duration); + let result = timestamp + time_duration; + prop_assert_eq!(result.to_micros_since_unix_epoch(), since_epoch + duration); + } + } +} diff --git a/crates/sats/src/typespace.rs b/crates/sats/src/typespace.rs index cbddf79620a..1935f1366b0 100644 --- a/crates/sats/src/typespace.rs +++ b/crates/sats/src/typespace.rs @@ -204,7 +204,7 @@ impl Typespace { /// /// All types in the typespace must either be /// [`valid_for_client_type_definition`](AlgebraicType::valid_for_client_type_definition) or - /// [`valid_for_client_type_use`](AlgebraicType::valid_for_client_type_definition). + /// [`valid_for_client_type_use`](AlgebraicType::valid_for_client_type_use). /// (Only the types that are `valid_for_client_type_definition` will have types generated in /// the client, but the other types are allowed for the convenience of module binding codegen.) pub fn is_valid_for_client_code_generation(&self) -> bool { diff --git a/crates/schema/src/type_for_generate.rs b/crates/schema/src/type_for_generate.rs index ff106f6a8a8..40a14f9808a 100644 --- a/crates/schema/src/type_for_generate.rs +++ b/crates/schema/src/type_for_generate.rs @@ -320,6 +320,12 @@ pub enum AlgebraicTypeUse { /// The special `Address` type. Address, + /// The special `Timestamp` type. + Timestamp, + + /// The special `TimeDuration` type. + TimeDuration, + /// The unit type (empty product). /// This is *distinct* from a use of a definition of a product type with no elements. Unit, @@ -410,6 +416,10 @@ impl TypespaceForGenerateBuilder<'_> { Ok(AlgebraicTypeUse::Address) } else if ty.is_identity() { Ok(AlgebraicTypeUse::Identity) + } else if ty.is_timestamp() { + Ok(AlgebraicTypeUse::Timestamp) + } else if ty.is_time_duration() { + Ok(AlgebraicTypeUse::TimeDuration) } else if ty.is_unit() { Ok(AlgebraicTypeUse::Unit) } else if ty.is_never() { diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/identity_connected_reducer.rs b/crates/sdk/examples/quickstart-chat/module_bindings/identity_connected_reducer.rs index 7a36ae90185..24a603c2d15 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/identity_connected_reducer.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/identity_connected_reducer.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/identity_disconnected_reducer.rs b/crates/sdk/examples/quickstart-chat/module_bindings/identity_disconnected_reducer.rs index 8276ad2a47e..a212a123cd0 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/identity_disconnected_reducer.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/identity_disconnected_reducer.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/init_reducer.rs b/crates/sdk/examples/quickstart-chat/module_bindings/init_reducer.rs deleted file mode 100644 index a6cf5b746bd..00000000000 --- a/crates/sdk/examples/quickstart-chat/module_bindings/init_reducer.rs +++ /dev/null @@ -1,93 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. - -#![allow(unused, clippy::all)] -use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub(super) struct InitArgs {} - -impl From for super::Reducer { - fn from(args: InitArgs) -> Self { - Self::Init - } -} - -impl __sdk::InModule for InitArgs { - type Module = super::RemoteModule; -} - -pub struct InitCallbackId(__sdk::CallbackId); - -#[allow(non_camel_case_types)] -/// Extension trait for access to the reducer `init`. -/// -/// Implemented for [`super::RemoteReducers`]. -pub trait init { - /// Request that the remote module invoke the reducer `init` to run as soon as possible. - /// - /// This method returns immediately, and errors only if we are unable to send the request. - /// The reducer will run asynchronously in the future, - /// and its status can be observed by listening for [`Self::on_init`] callbacks. - fn init(&self) -> __sdk::Result<()>; - /// Register a callback to run whenever we are notified of an invocation of the reducer `init`. - /// - /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] - /// to determine the reducer's status. - /// - /// The returned [`InitCallbackId`] can be passed to [`Self::remove_on_init`] - /// to cancel the callback. - fn on_init(&self, callback: impl FnMut(&super::ReducerEventContext) + Send + 'static) -> InitCallbackId; - /// Cancel a callback previously registered by [`Self::on_init`], - /// causing it not to run in the future. - fn remove_on_init(&self, callback: InitCallbackId); -} - -impl init for super::RemoteReducers { - fn init(&self) -> __sdk::Result<()> { - self.imp.call_reducer("init", InitArgs {}) - } - fn on_init(&self, mut callback: impl FnMut(&super::ReducerEventContext) + Send + 'static) -> InitCallbackId { - InitCallbackId(self.imp.on_reducer( - "init", - Box::new(move |ctx: &super::ReducerEventContext| { - let super::ReducerEventContext { - event: - __sdk::ReducerEvent { - reducer: super::Reducer::Init {}, - .. - }, - .. - } = ctx - else { - unreachable!() - }; - callback(ctx) - }), - )) - } - fn remove_on_init(&self, callback: InitCallbackId) { - self.imp.remove_on_reducer("init", callback.0) - } -} - -#[allow(non_camel_case_types)] -#[doc(hidden)] -/// Extension trait for setting the call-flags for the reducer `init`. -/// -/// Implemented for [`super::SetReducerFlags`]. -/// -/// This type is currently unstable and may be removed without a major version bump. -pub trait set_flags_for_init { - /// Set the call-reducer flags for the reducer `init` to `flags`. - /// - /// This type is currently unstable and may be removed without a major version bump. - fn init(&self, flags: __ws::CallReducerFlags); -} - -impl set_flags_for_init for super::SetReducerFlags { - fn init(&self, flags: __ws::CallReducerFlags) { - self.imp.set_call_reducer_flags("init", flags); - } -} diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/message_table.rs b/crates/sdk/examples/quickstart-chat/module_bindings/message_table.rs index 44726ee45e8..fcba951ab94 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/message_table.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/message_table.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use super::message_type::Message; diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/message_type.rs b/crates/sdk/examples/quickstart-chat/module_bindings/message_type.rs index c361b65bd7e..16f2e194ac4 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/message_type.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/message_type.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -8,7 +8,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[sats(crate = __lib)] pub struct Message { pub sender: __sdk::Identity, - pub sent: u64, + pub sent: __sdk::Timestamp, pub text: String, } diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/mod.rs b/crates/sdk/examples/quickstart-chat/module_bindings/mod.rs index b4d3b174306..eca295918ec 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/mod.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/mod.rs @@ -1,12 +1,11 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; pub mod identity_connected_reducer; pub mod identity_disconnected_reducer; -pub mod init_reducer; pub mod message_table; pub mod message_type; pub mod send_message_reducer; @@ -20,7 +19,6 @@ pub use identity_connected_reducer::{ pub use identity_disconnected_reducer::{ identity_disconnected, set_flags_for_identity_disconnected, IdentityDisconnectedCallbackId, }; -pub use init_reducer::{init, set_flags_for_init, InitCallbackId}; pub use message_table::*; pub use message_type::Message; pub use send_message_reducer::{send_message, set_flags_for_send_message, SendMessageCallbackId}; @@ -38,7 +36,6 @@ pub use user_type::User; pub enum Reducer { IdentityConnected, IdentityDisconnected, - Init, SendMessage { text: String }, SetName { name: String }, } @@ -52,7 +49,6 @@ impl __sdk::Reducer for Reducer { match self { Reducer::IdentityConnected => "identity_connected", Reducer::IdentityDisconnected => "identity_disconnected", - Reducer::Init => "init", Reducer::SendMessage { .. } => "send_message", Reducer::SetName { .. } => "set_name", } @@ -73,7 +69,6 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { identity_disconnected_reducer::IdentityDisconnectedArgs, >("identity_disconnected", &value.args)? .into()), - "init" => Ok(__sdk::parse_reducer_args::("init", &value.args)?.into()), "send_message" => Ok(__sdk::parse_reducer_args::( "send_message", &value.args, diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/send_message_reducer.rs b/crates/sdk/examples/quickstart-chat/module_bindings/send_message_reducer.rs index 5868a6f2c56..c7751556a51 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/send_message_reducer.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/send_message_reducer.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/set_name_reducer.rs b/crates/sdk/examples/quickstart-chat/module_bindings/set_name_reducer.rs index 722c6bca0b8..6664caa14e4 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/set_name_reducer.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/set_name_reducer.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/user_table.rs b/crates/sdk/examples/quickstart-chat/module_bindings/user_table.rs index a19a4054e79..81215e38df5 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/user_table.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/user_table.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use super::user_type::User; diff --git a/crates/sdk/examples/quickstart-chat/module_bindings/user_type.rs b/crates/sdk/examples/quickstart-chat/module_bindings/user_type.rs index 914103ce9af..ad82fbca031 100644 --- a/crates/sdk/examples/quickstart-chat/module_bindings/user_type.rs +++ b/crates/sdk/examples/quickstart-chat/module_bindings/user_type.rs @@ -1,5 +1,5 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD. +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/crates/sdk/src/db_connection.rs b/crates/sdk/src/db_connection.rs index 6efb28b916b..bd1929229af 100644 --- a/crates/sdk/src/db_connection.rs +++ b/crates/sdk/src/db_connection.rs @@ -39,7 +39,6 @@ use spacetimedb_lib::{bsatn, ser::Serialize, Address, Identity}; use std::{ collections::HashMap, sync::{atomic::AtomicU32, Arc, Mutex as StdMutex, OnceLock}, - time::{Duration, SystemTime}, }; use tokio::{ runtime::{self, Runtime}, @@ -1077,9 +1076,7 @@ async fn parse_loop( caller_address: caller_address.none_if_zero(), caller_identity, energy_consumed: Some(energy_quanta_used.quanta), - timestamp: SystemTime::UNIX_EPOCH - .checked_add(Duration::from_micros(timestamp.microseconds)) - .unwrap(), + timestamp, reducer, status, }) diff --git a/crates/sdk/src/event.rs b/crates/sdk/src/event.rs index 2eb2ae8c8a1..f7d72eb9a97 100644 --- a/crates/sdk/src/event.rs +++ b/crates/sdk/src/event.rs @@ -12,8 +12,7 @@ use crate::spacetime_module::{DbUpdate as _, SpacetimeModule}; use spacetimedb_client_api_messages::websocket as ws; -use spacetimedb_lib::{Address, Identity}; -use std::time::SystemTime; +use spacetimedb_lib::{Address, Identity, Timestamp}; #[non_exhaustive] #[derive(Debug, Clone)] @@ -62,7 +61,7 @@ pub enum Event { /// A state change due to a reducer, which may or may not have committed successfully. pub struct ReducerEvent { /// The time at which the reducer was invoked. - pub timestamp: SystemTime, + pub timestamp: Timestamp, /// Whether the reducer committed, was aborted due to insufficient energy, or failed with an error message. pub status: Status, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index f21c1e24009..3d2653ec378 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -30,7 +30,7 @@ pub use event::{Event, ReducerEvent, Status}; pub use table::{Table, TableWithPrimaryKey}; pub use spacetime_module::SubscriptionHandle; -pub use spacetimedb_lib::{Address, Identity, ScheduleAt}; +pub use spacetimedb_lib::{Address, Identity, ScheduleAt, TimeDuration, Timestamp}; pub use spacetimedb_sats::{i256, u256}; #[doc(hidden)] @@ -56,6 +56,7 @@ pub mod __codegen { pub use crate::subscription::{OnEndedCallback, SubscriptionBuilder, SubscriptionHandleImpl}; pub use crate::{ Address, DbConnectionBuilder, DbContext, Event, Identity, ReducerEvent, ScheduleAt, Table, TableWithPrimaryKey, + TimeDuration, Timestamp, }; } diff --git a/crates/sdk/tests/test-client/src/main.rs b/crates/sdk/tests/test-client/src/main.rs index 7158c2721a9..2f57758c5c9 100644 --- a/crates/sdk/tests/test-client/src/main.rs +++ b/crates/sdk/tests/test-client/src/main.rs @@ -9,7 +9,7 @@ use module_bindings::*; use spacetimedb_sdk::{ credentials, i256, u256, unstable::CallReducerFlags, Address, DbConnectionBuilder, DbContext, Error, Event, - Identity, ReducerEvent, Status, SubscriptionHandle, Table, + Identity, ReducerEvent, Status, SubscriptionHandle, Table, TimeDuration, Timestamp, }; use test_counter::TestCounter; @@ -44,6 +44,22 @@ fn exit_on_panic() { })); } +macro_rules! assert_eq_or_bail { + ($expected:expr, $found:expr) => {{ + let expected = &$expected; + let found = &$found; + if expected != found { + anyhow::bail!( + "Expected {} => {:?} but found {} => {:?}", + stringify!($expected), + expected, + stringify!($found), + found + ); + } + }}; +} + fn main() { env_logger::init(); exit_on_panic(); @@ -70,6 +86,9 @@ fn main() { "delete_address" => exec_delete_address(), "update_address" => exec_update_address(), + "insert_timestamp" => exec_insert_timestamp(), + "insert_call_timestamp" => exec_insert_call_timestamp(), + "on_reducer" => exec_on_reducer(), "fail_reducer" => exec_fail_reducer(), @@ -141,6 +160,8 @@ fn assert_all_tables_empty(ctx: &impl RemoteDbContext) -> anyhow::Result<()> { assert_table_empty(ctx.db().one_identity())?; assert_table_empty(ctx.db().one_address())?; + assert_table_empty(ctx.db().one_timestamp())?; + assert_table_empty(ctx.db().one_simple_enum())?; assert_table_empty(ctx.db().one_enum_with_payload())?; @@ -172,6 +193,8 @@ fn assert_all_tables_empty(ctx: &impl RemoteDbContext) -> anyhow::Result<()> { assert_table_empty(ctx.db().vec_identity())?; assert_table_empty(ctx.db().vec_address())?; + assert_table_empty(ctx.db().vec_timestamp())?; + assert_table_empty(ctx.db().vec_simple_enum())?; assert_table_empty(ctx.db().vec_enum_with_payload())?; @@ -254,6 +277,7 @@ const SUBSCRIBE_ALL: &[&str] = &[ "SELECT * FROM one_string;", "SELECT * FROM one_identity;", "SELECT * FROM one_address;", + "SELECT * FROM one_timestamp;", "SELECT * FROM one_simple_enum;", "SELECT * FROM one_enum_with_payload;", "SELECT * FROM one_unit_struct;", @@ -278,6 +302,7 @@ const SUBSCRIBE_ALL: &[&str] = &[ "SELECT * FROM vec_string;", "SELECT * FROM vec_identity;", "SELECT * FROM vec_address;", + "SELECT * FROM vec_timestamp;", "SELECT * FROM vec_simple_enum;", "SELECT * FROM vec_enum_with_payload;", "SELECT * FROM vec_unit_struct;", @@ -756,6 +781,79 @@ fn exec_update_address() { assert_all_tables_empty(&connection).unwrap(); } +fn exec_insert_timestamp() { + let test_counter = TestCounter::new(); + let sub_applied_nothing_result = test_counter.add_test("on_subscription_applied_nothing"); + + connect_then(&test_counter, { + let test_counter = test_counter.clone(); + move |ctx| { + subscribe_all_then(ctx, move |ctx| { + insert_one::(ctx, &test_counter, Timestamp::now()); + + sub_applied_nothing_result(assert_all_tables_empty(ctx)); + }) + } + }); + + test_counter.wait_for_all(); +} + +fn exec_insert_call_timestamp() { + let test_counter = TestCounter::new(); + let sub_applied_nothing_result = test_counter.add_test("on_subscription_applied_nothing"); + + connect_then(&test_counter, { + let test_counter = test_counter.clone(); + move |ctx| { + subscribe_all_then(ctx, move |ctx| { + let mut on_insert_result = Some(test_counter.add_test("on_insert")); + ctx.db.one_timestamp().on_insert(move |ctx, row| { + let run_checks = || { + let Event::Reducer(reducer_event) = &ctx.event else { + anyhow::bail!("Expected Reducer event but found {:?}", ctx.event); + }; + if reducer_event.caller_identity != ctx.identity() { + anyhow::bail!( + "Expected caller_identity to be my own identity {:?}, but found {:?}", + ctx.identity(), + reducer_event.caller_identity, + ); + } + if reducer_event.caller_address != Some(ctx.address()) { + anyhow::bail!( + "Expected caller_address to be my own address {:?}, but found {:?}", + ctx.address(), + reducer_event.caller_address, + ) + } + if !matches!(reducer_event.status, Status::Committed) { + anyhow::bail!( + "Unexpected status. Expected Committed but found {:?}", + reducer_event.status + ); + } + let expected_reducer = Reducer::InsertCallTimestamp; + if reducer_event.reducer != expected_reducer { + anyhow::bail!( + "Unexpected Reducer in ReducerEvent: expected {expected_reducer:?} but found {:?}", + reducer_event.reducer, + ); + }; + + assert_eq_or_bail!(reducer_event.timestamp, row.t); + Ok(()) + }; + (on_insert_result.take().unwrap())(run_checks()); + }); + ctx.reducers.insert_call_timestamp().unwrap(); + }); + sub_applied_nothing_result(assert_all_tables_empty(ctx)); + } + }); + test_counter.wait_for_all(); +} + /// This tests that we can observe reducer callbacks for successful reducer runs. fn exec_on_reducer() { let test_counter = TestCounter::new(); @@ -999,6 +1097,8 @@ fn exec_insert_vec() { insert_one::(ctx, &test_counter, vec![ctx.identity()]); insert_one::(ctx, &test_counter, vec![ctx.address()]); + insert_one::(ctx, &test_counter, vec![Timestamp::now()]); + sub_applied_nothing_result(assert_all_tables_empty(ctx)); } }); @@ -1035,6 +1135,8 @@ fn every_primitive_struct() -> EveryPrimitiveStruct { p: "string".to_string(), q: Identity::__dummy(), r: Address::default(), + s: Timestamp::from_micros_since_unix_epoch(9876543210), + t: TimeDuration::from_micros(-67_419_000_000_003), } } @@ -1058,6 +1160,8 @@ fn every_vec_struct() -> EveryVecStruct { p: ["vec", "of", "strings"].into_iter().map(str::to_string).collect(), q: vec![Identity::__dummy()], r: vec![Address::default()], + s: vec![Timestamp::from_micros_since_unix_epoch(9876543210)], + t: vec![TimeDuration::from_micros(-67_419_000_000_003)], } } @@ -1251,22 +1355,6 @@ fn exec_should_fail() { test_counter.wait_for_all(); } -macro_rules! assert_eq_or_bail { - ($expected:expr, $found:expr) => {{ - let expected = &$expected; - let found = &$found; - if expected != found { - anyhow::bail!( - "Expected {} => {:?} but found {} => {:?}", - stringify!($expected), - expected, - stringify!($found), - found - ); - } - }}; -} - /// This test invokes a reducer with many arguments of many types, /// and observes a callback for an inserted table with many columns of many types. fn exec_insert_delete_large_table() { @@ -1284,9 +1372,7 @@ fn exec_insert_delete_large_table() { table.on_insert(move |ctx, large_table_inserted| { if let Some(insert_result) = insert_result.take() { let run_tests = || { - let large_table = large_table(); - - assert_eq_or_bail!(large_table, *large_table_inserted); + assert_eq_or_bail!(large_table(), *large_table_inserted); if !matches!( ctx.event, Event::Reducer(ReducerEvent { @@ -1298,6 +1384,7 @@ fn exec_insert_delete_large_table() { } // Now we'll delete the row we just inserted and check that the delete callback is called. + let large_table = large_table(); ctx.reducers.delete_large_table( large_table.a, large_table.b, @@ -1412,6 +1499,8 @@ fn exec_insert_primitives_as_strings() { s.p.to_string(), s.q.to_string(), s.r.to_string(), + s.s.to_string(), + s.t.to_string(), ]; ctx.db.vec_string().on_insert(move |ctx, row| { diff --git a/crates/sdk/tests/test-client/src/module_bindings/enum_with_payload_type.rs b/crates/sdk/tests/test-client/src/module_bindings/enum_with_payload_type.rs index 0319d97fe67..5df9498b3a6 100644 --- a/crates/sdk/tests/test-client/src/module_bindings/enum_with_payload_type.rs +++ b/crates/sdk/tests/test-client/src/module_bindings/enum_with_payload_type.rs @@ -45,6 +45,8 @@ pub enum EnumWithPayload { Address(__sdk::Address), + Timestamp(__sdk::Timestamp), + Bytes(Vec), Ints(Vec), diff --git a/crates/sdk/tests/test-client/src/module_bindings/every_primitive_struct_type.rs b/crates/sdk/tests/test-client/src/module_bindings/every_primitive_struct_type.rs index c9d6434b324..b1ea92ac512 100644 --- a/crates/sdk/tests/test-client/src/module_bindings/every_primitive_struct_type.rs +++ b/crates/sdk/tests/test-client/src/module_bindings/every_primitive_struct_type.rs @@ -25,6 +25,8 @@ pub struct EveryPrimitiveStruct { pub p: String, pub q: __sdk::Identity, pub r: __sdk::Address, + pub s: __sdk::Timestamp, + pub t: __sdk::TimeDuration, } impl __sdk::InModule for EveryPrimitiveStruct { diff --git a/crates/sdk/tests/test-client/src/module_bindings/every_vec_struct_type.rs b/crates/sdk/tests/test-client/src/module_bindings/every_vec_struct_type.rs index 9e4a319cf87..7062f1e6124 100644 --- a/crates/sdk/tests/test-client/src/module_bindings/every_vec_struct_type.rs +++ b/crates/sdk/tests/test-client/src/module_bindings/every_vec_struct_type.rs @@ -25,6 +25,8 @@ pub struct EveryVecStruct { pub p: Vec, pub q: Vec<__sdk::Identity>, pub r: Vec<__sdk::Address>, + pub s: Vec<__sdk::Timestamp>, + pub t: Vec<__sdk::TimeDuration>, } impl __sdk::InModule for EveryVecStruct { diff --git a/crates/sdk/tests/test-client/src/module_bindings/insert_call_timestamp_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/insert_call_timestamp_reducer.rs new file mode 100644 index 00000000000..0cdf07f8bde --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/insert_call_timestamp_reducer.rs @@ -0,0 +1,100 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct InsertCallTimestampArgs {} + +impl From for super::Reducer { + fn from(args: InsertCallTimestampArgs) -> Self { + Self::InsertCallTimestamp + } +} + +impl __sdk::InModule for InsertCallTimestampArgs { + type Module = super::RemoteModule; +} + +pub struct InsertCallTimestampCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_call_timestamp`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_call_timestamp { + /// Request that the remote module invoke the reducer `insert_call_timestamp` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_insert_call_timestamp`] callbacks. + fn insert_call_timestamp(&self) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `insert_call_timestamp`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`InsertCallTimestampCallbackId`] can be passed to [`Self::remove_on_insert_call_timestamp`] + /// to cancel the callback. + fn on_insert_call_timestamp( + &self, + callback: impl FnMut(&super::ReducerEventContext) + Send + 'static, + ) -> InsertCallTimestampCallbackId; + /// Cancel a callback previously registered by [`Self::on_insert_call_timestamp`], + /// causing it not to run in the future. + fn remove_on_insert_call_timestamp(&self, callback: InsertCallTimestampCallbackId); +} + +impl insert_call_timestamp for super::RemoteReducers { + fn insert_call_timestamp(&self) -> __sdk::Result<()> { + self.imp + .call_reducer("insert_call_timestamp", InsertCallTimestampArgs {}) + } + fn on_insert_call_timestamp( + &self, + mut callback: impl FnMut(&super::ReducerEventContext) + Send + 'static, + ) -> InsertCallTimestampCallbackId { + InsertCallTimestampCallbackId(self.imp.on_reducer( + "insert_call_timestamp", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::InsertCallTimestamp {}, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx) + }), + )) + } + fn remove_on_insert_call_timestamp(&self, callback: InsertCallTimestampCallbackId) { + self.imp.remove_on_reducer("insert_call_timestamp", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `insert_call_timestamp`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_insert_call_timestamp { + /// Set the call-reducer flags for the reducer `insert_call_timestamp` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn insert_call_timestamp(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_insert_call_timestamp for super::SetReducerFlags { + fn insert_call_timestamp(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("insert_call_timestamp", flags); + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/insert_one_timestamp_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/insert_one_timestamp_reducer.rs new file mode 100644 index 00000000000..c36926c996c --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/insert_one_timestamp_reducer.rs @@ -0,0 +1,102 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct InsertOneTimestampArgs { + pub t: __sdk::Timestamp, +} + +impl From for super::Reducer { + fn from(args: InsertOneTimestampArgs) -> Self { + Self::InsertOneTimestamp { t: args.t } + } +} + +impl __sdk::InModule for InsertOneTimestampArgs { + type Module = super::RemoteModule; +} + +pub struct InsertOneTimestampCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_one_timestamp`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_one_timestamp { + /// Request that the remote module invoke the reducer `insert_one_timestamp` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_insert_one_timestamp`] callbacks. + fn insert_one_timestamp(&self, t: __sdk::Timestamp) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `insert_one_timestamp`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`InsertOneTimestampCallbackId`] can be passed to [`Self::remove_on_insert_one_timestamp`] + /// to cancel the callback. + fn on_insert_one_timestamp( + &self, + callback: impl FnMut(&super::ReducerEventContext, &__sdk::Timestamp) + Send + 'static, + ) -> InsertOneTimestampCallbackId; + /// Cancel a callback previously registered by [`Self::on_insert_one_timestamp`], + /// causing it not to run in the future. + fn remove_on_insert_one_timestamp(&self, callback: InsertOneTimestampCallbackId); +} + +impl insert_one_timestamp for super::RemoteReducers { + fn insert_one_timestamp(&self, t: __sdk::Timestamp) -> __sdk::Result<()> { + self.imp + .call_reducer("insert_one_timestamp", InsertOneTimestampArgs { t }) + } + fn on_insert_one_timestamp( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &__sdk::Timestamp) + Send + 'static, + ) -> InsertOneTimestampCallbackId { + InsertOneTimestampCallbackId(self.imp.on_reducer( + "insert_one_timestamp", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::InsertOneTimestamp { t }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, t) + }), + )) + } + fn remove_on_insert_one_timestamp(&self, callback: InsertOneTimestampCallbackId) { + self.imp.remove_on_reducer("insert_one_timestamp", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `insert_one_timestamp`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_insert_one_timestamp { + /// Set the call-reducer flags for the reducer `insert_one_timestamp` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn insert_one_timestamp(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_insert_one_timestamp for super::SetReducerFlags { + fn insert_one_timestamp(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("insert_one_timestamp", flags); + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/insert_vec_timestamp_reducer.rs b/crates/sdk/tests/test-client/src/module_bindings/insert_vec_timestamp_reducer.rs new file mode 100644 index 00000000000..e681a7a785c --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/insert_vec_timestamp_reducer.rs @@ -0,0 +1,102 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub(super) struct InsertVecTimestampArgs { + pub t: Vec<__sdk::Timestamp>, +} + +impl From for super::Reducer { + fn from(args: InsertVecTimestampArgs) -> Self { + Self::InsertVecTimestamp { t: args.t } + } +} + +impl __sdk::InModule for InsertVecTimestampArgs { + type Module = super::RemoteModule; +} + +pub struct InsertVecTimestampCallbackId(__sdk::CallbackId); + +#[allow(non_camel_case_types)] +/// Extension trait for access to the reducer `insert_vec_timestamp`. +/// +/// Implemented for [`super::RemoteReducers`]. +pub trait insert_vec_timestamp { + /// Request that the remote module invoke the reducer `insert_vec_timestamp` to run as soon as possible. + /// + /// This method returns immediately, and errors only if we are unable to send the request. + /// The reducer will run asynchronously in the future, + /// and its status can be observed by listening for [`Self::on_insert_vec_timestamp`] callbacks. + fn insert_vec_timestamp(&self, t: Vec<__sdk::Timestamp>) -> __sdk::Result<()>; + /// Register a callback to run whenever we are notified of an invocation of the reducer `insert_vec_timestamp`. + /// + /// Callbacks should inspect the [`__sdk::ReducerEvent`] contained in the [`super::ReducerEventContext`] + /// to determine the reducer's status. + /// + /// The returned [`InsertVecTimestampCallbackId`] can be passed to [`Self::remove_on_insert_vec_timestamp`] + /// to cancel the callback. + fn on_insert_vec_timestamp( + &self, + callback: impl FnMut(&super::ReducerEventContext, &Vec<__sdk::Timestamp>) + Send + 'static, + ) -> InsertVecTimestampCallbackId; + /// Cancel a callback previously registered by [`Self::on_insert_vec_timestamp`], + /// causing it not to run in the future. + fn remove_on_insert_vec_timestamp(&self, callback: InsertVecTimestampCallbackId); +} + +impl insert_vec_timestamp for super::RemoteReducers { + fn insert_vec_timestamp(&self, t: Vec<__sdk::Timestamp>) -> __sdk::Result<()> { + self.imp + .call_reducer("insert_vec_timestamp", InsertVecTimestampArgs { t }) + } + fn on_insert_vec_timestamp( + &self, + mut callback: impl FnMut(&super::ReducerEventContext, &Vec<__sdk::Timestamp>) + Send + 'static, + ) -> InsertVecTimestampCallbackId { + InsertVecTimestampCallbackId(self.imp.on_reducer( + "insert_vec_timestamp", + Box::new(move |ctx: &super::ReducerEventContext| { + let super::ReducerEventContext { + event: + __sdk::ReducerEvent { + reducer: super::Reducer::InsertVecTimestamp { t }, + .. + }, + .. + } = ctx + else { + unreachable!() + }; + callback(ctx, t) + }), + )) + } + fn remove_on_insert_vec_timestamp(&self, callback: InsertVecTimestampCallbackId) { + self.imp.remove_on_reducer("insert_vec_timestamp", callback.0) + } +} + +#[allow(non_camel_case_types)] +#[doc(hidden)] +/// Extension trait for setting the call-flags for the reducer `insert_vec_timestamp`. +/// +/// Implemented for [`super::SetReducerFlags`]. +/// +/// This type is currently unstable and may be removed without a major version bump. +pub trait set_flags_for_insert_vec_timestamp { + /// Set the call-reducer flags for the reducer `insert_vec_timestamp` to `flags`. + /// + /// This type is currently unstable and may be removed without a major version bump. + fn insert_vec_timestamp(&self, flags: __ws::CallReducerFlags); +} + +impl set_flags_for_insert_vec_timestamp for super::SetReducerFlags { + fn insert_vec_timestamp(&self, flags: __ws::CallReducerFlags) { + self.imp.set_call_reducer_flags("insert_vec_timestamp", flags); + } +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/mod.rs b/crates/sdk/tests/test-client/src/module_bindings/mod.rs index 0c836224df3..94584042ce8 100644 --- a/crates/sdk/tests/test-client/src/module_bindings/mod.rs +++ b/crates/sdk/tests/test-client/src/module_bindings/mod.rs @@ -45,6 +45,7 @@ pub mod indexed_table_2_table; pub mod indexed_table_2_type; pub mod indexed_table_table; pub mod indexed_table_type; +pub mod insert_call_timestamp_reducer; pub mod insert_caller_one_address_reducer; pub mod insert_caller_one_identity_reducer; pub mod insert_caller_pk_address_reducer; @@ -71,6 +72,7 @@ pub mod insert_one_i_8_reducer; pub mod insert_one_identity_reducer; pub mod insert_one_simple_enum_reducer; pub mod insert_one_string_reducer; +pub mod insert_one_timestamp_reducer; pub mod insert_one_u_128_reducer; pub mod insert_one_u_16_reducer; pub mod insert_one_u_256_reducer; @@ -135,6 +137,7 @@ pub mod insert_vec_i_8_reducer; pub mod insert_vec_identity_reducer; pub mod insert_vec_simple_enum_reducer; pub mod insert_vec_string_reducer; +pub mod insert_vec_timestamp_reducer; pub mod insert_vec_u_128_reducer; pub mod insert_vec_u_16_reducer; pub mod insert_vec_u_256_reducer; @@ -179,6 +182,8 @@ pub mod one_simple_enum_table; pub mod one_simple_enum_type; pub mod one_string_table; pub mod one_string_type; +pub mod one_timestamp_table; +pub mod one_timestamp_type; pub mod one_u_128_table; pub mod one_u_128_type; pub mod one_u_16_table; @@ -342,6 +347,8 @@ pub mod vec_simple_enum_table; pub mod vec_simple_enum_type; pub mod vec_string_table; pub mod vec_string_type; +pub mod vec_timestamp_table; +pub mod vec_timestamp_type; pub mod vec_u_128_table; pub mod vec_u_128_type; pub mod vec_u_16_table; @@ -418,6 +425,9 @@ pub use indexed_table_2_table::*; pub use indexed_table_2_type::IndexedTable2; pub use indexed_table_table::*; pub use indexed_table_type::IndexedTable; +pub use insert_call_timestamp_reducer::{ + insert_call_timestamp, set_flags_for_insert_call_timestamp, InsertCallTimestampCallbackId, +}; pub use insert_caller_one_address_reducer::{ insert_caller_one_address, set_flags_for_insert_caller_one_address, InsertCallerOneAddressCallbackId, }; @@ -477,6 +487,9 @@ pub use insert_one_simple_enum_reducer::{ insert_one_simple_enum, set_flags_for_insert_one_simple_enum, InsertOneSimpleEnumCallbackId, }; pub use insert_one_string_reducer::{insert_one_string, set_flags_for_insert_one_string, InsertOneStringCallbackId}; +pub use insert_one_timestamp_reducer::{ + insert_one_timestamp, set_flags_for_insert_one_timestamp, InsertOneTimestampCallbackId, +}; pub use insert_one_u_128_reducer::{insert_one_u_128, set_flags_for_insert_one_u_128, InsertOneU128CallbackId}; pub use insert_one_u_16_reducer::{insert_one_u_16, set_flags_for_insert_one_u_16, InsertOneU16CallbackId}; pub use insert_one_u_256_reducer::{insert_one_u_256, set_flags_for_insert_one_u_256, InsertOneU256CallbackId}; @@ -591,6 +604,9 @@ pub use insert_vec_simple_enum_reducer::{ insert_vec_simple_enum, set_flags_for_insert_vec_simple_enum, InsertVecSimpleEnumCallbackId, }; pub use insert_vec_string_reducer::{insert_vec_string, set_flags_for_insert_vec_string, InsertVecStringCallbackId}; +pub use insert_vec_timestamp_reducer::{ + insert_vec_timestamp, set_flags_for_insert_vec_timestamp, InsertVecTimestampCallbackId, +}; pub use insert_vec_u_128_reducer::{insert_vec_u_128, set_flags_for_insert_vec_u_128, InsertVecU128CallbackId}; pub use insert_vec_u_16_reducer::{insert_vec_u_16, set_flags_for_insert_vec_u_16, InsertVecU16CallbackId}; pub use insert_vec_u_256_reducer::{insert_vec_u_256, set_flags_for_insert_vec_u_256, InsertVecU256CallbackId}; @@ -637,6 +653,8 @@ pub use one_simple_enum_table::*; pub use one_simple_enum_type::OneSimpleEnum; pub use one_string_table::*; pub use one_string_type::OneString; +pub use one_timestamp_table::*; +pub use one_timestamp_type::OneTimestamp; pub use one_u_128_table::*; pub use one_u_128_type::OneU128; pub use one_u_16_table::*; @@ -820,6 +838,8 @@ pub use vec_simple_enum_table::*; pub use vec_simple_enum_type::VecSimpleEnum; pub use vec_string_table::*; pub use vec_string_type::VecString; +pub use vec_timestamp_table::*; +pub use vec_timestamp_type::VecTimestamp; pub use vec_u_128_table::*; pub use vec_u_128_type::VecU128; pub use vec_u_16_table::*; @@ -963,6 +983,7 @@ pub enum Reducer { DeleteUniqueU8 { n: u8, }, + InsertCallTimestamp, InsertCallerOneAddress, InsertCallerOneIdentity, InsertCallerPkAddress { @@ -1054,6 +1075,9 @@ pub enum Reducer { InsertOneString { s: String, }, + InsertOneTimestamp { + t: __sdk::Timestamp, + }, InsertOneU128 { n: u128, }, @@ -1279,6 +1303,9 @@ pub enum Reducer { InsertVecString { s: Vec, }, + InsertVecTimestamp { + t: Vec<__sdk::Timestamp>, + }, InsertVecU128 { n: Vec, }, @@ -1474,6 +1501,7 @@ impl __sdk::Reducer for Reducer { Reducer::DeleteUniqueU32 { .. } => "delete_unique_u32", Reducer::DeleteUniqueU64 { .. } => "delete_unique_u64", Reducer::DeleteUniqueU8 { .. } => "delete_unique_u8", + Reducer::InsertCallTimestamp => "insert_call_timestamp", Reducer::InsertCallerOneAddress => "insert_caller_one_address", Reducer::InsertCallerOneIdentity => "insert_caller_one_identity", Reducer::InsertCallerPkAddress { .. } => "insert_caller_pk_address", @@ -1500,6 +1528,7 @@ impl __sdk::Reducer for Reducer { Reducer::InsertOneIdentity { .. } => "insert_one_identity", Reducer::InsertOneSimpleEnum { .. } => "insert_one_simple_enum", Reducer::InsertOneString { .. } => "insert_one_string", + Reducer::InsertOneTimestamp { .. } => "insert_one_timestamp", Reducer::InsertOneU128 { .. } => "insert_one_u128", Reducer::InsertOneU16 { .. } => "insert_one_u16", Reducer::InsertOneU256 { .. } => "insert_one_u256", @@ -1564,6 +1593,7 @@ impl __sdk::Reducer for Reducer { Reducer::InsertVecIdentity { .. } => "insert_vec_identity", Reducer::InsertVecSimpleEnum { .. } => "insert_vec_simple_enum", Reducer::InsertVecString { .. } => "insert_vec_string", + Reducer::InsertVecTimestamp { .. } => "insert_vec_timestamp", Reducer::InsertVecU128 { .. } => "insert_vec_u128", Reducer::InsertVecU16 { .. } => "insert_vec_u16", Reducer::InsertVecU256 { .. } => "insert_vec_u256", @@ -1808,6 +1838,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { )? .into(), ), + "insert_call_timestamp" => Ok(__sdk::parse_reducer_args::< + insert_call_timestamp_reducer::InsertCallTimestampArgs, + >("insert_call_timestamp", &value.args)? + .into()), "insert_caller_one_address" => Ok(__sdk::parse_reducer_args::< insert_caller_one_address_reducer::InsertCallerOneAddressArgs, >("insert_caller_one_address", &value.args)? @@ -1937,6 +1971,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { )? .into(), ), + "insert_one_timestamp" => Ok(__sdk::parse_reducer_args::< + insert_one_timestamp_reducer::InsertOneTimestampArgs, + >("insert_one_timestamp", &value.args)? + .into()), "insert_one_u128" => Ok( __sdk::parse_reducer_args::( "insert_one_u128", @@ -2291,6 +2329,10 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { )? .into(), ), + "insert_vec_timestamp" => Ok(__sdk::parse_reducer_args::< + insert_vec_timestamp_reducer::InsertVecTimestampArgs, + >("insert_vec_timestamp", &value.args)? + .into()), "insert_vec_u128" => Ok( __sdk::parse_reducer_args::( "insert_vec_u128", @@ -2556,6 +2598,7 @@ pub struct DbUpdate { one_identity: __sdk::TableUpdate, one_simple_enum: __sdk::TableUpdate, one_string: __sdk::TableUpdate, + one_timestamp: __sdk::TableUpdate, one_u_128: __sdk::TableUpdate, one_u_16: __sdk::TableUpdate, one_u_256: __sdk::TableUpdate, @@ -2620,6 +2663,7 @@ pub struct DbUpdate { vec_identity: __sdk::TableUpdate, vec_simple_enum: __sdk::TableUpdate, vec_string: __sdk::TableUpdate, + vec_timestamp: __sdk::TableUpdate, vec_u_128: __sdk::TableUpdate, vec_u_16: __sdk::TableUpdate, vec_u_256: __sdk::TableUpdate, @@ -2668,6 +2712,7 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { db_update.one_simple_enum = one_simple_enum_table::parse_table_update(table_update)? } "one_string" => db_update.one_string = one_string_table::parse_table_update(table_update)?, + "one_timestamp" => db_update.one_timestamp = one_timestamp_table::parse_table_update(table_update)?, "one_u128" => db_update.one_u_128 = one_u_128_table::parse_table_update(table_update)?, "one_u16" => db_update.one_u_16 = one_u_16_table::parse_table_update(table_update)?, "one_u256" => db_update.one_u_256 = one_u_256_table::parse_table_update(table_update)?, @@ -2760,6 +2805,7 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { db_update.vec_simple_enum = vec_simple_enum_table::parse_table_update(table_update)? } "vec_string" => db_update.vec_string = vec_string_table::parse_table_update(table_update)?, + "vec_timestamp" => db_update.vec_timestamp = vec_timestamp_table::parse_table_update(table_update)?, "vec_u128" => db_update.vec_u_128 = vec_u_128_table::parse_table_update(table_update)?, "vec_u16" => db_update.vec_u_16 = vec_u_16_table::parse_table_update(table_update)?, "vec_u256" => db_update.vec_u_256 = vec_u_256_table::parse_table_update(table_update)?, @@ -2808,6 +2854,7 @@ impl __sdk::DbUpdate for DbUpdate { cache.apply_diff_to_table::("one_identity", &self.one_identity); cache.apply_diff_to_table::("one_simple_enum", &self.one_simple_enum); cache.apply_diff_to_table::("one_string", &self.one_string); + cache.apply_diff_to_table::("one_timestamp", &self.one_timestamp); cache.apply_diff_to_table::("one_u128", &self.one_u_128); cache.apply_diff_to_table::("one_u16", &self.one_u_16); cache.apply_diff_to_table::("one_u256", &self.one_u_256); @@ -2878,6 +2925,7 @@ impl __sdk::DbUpdate for DbUpdate { cache.apply_diff_to_table::("vec_identity", &self.vec_identity); cache.apply_diff_to_table::("vec_simple_enum", &self.vec_simple_enum); cache.apply_diff_to_table::("vec_string", &self.vec_string); + cache.apply_diff_to_table::("vec_timestamp", &self.vec_timestamp); cache.apply_diff_to_table::("vec_u128", &self.vec_u_128); cache.apply_diff_to_table::("vec_u16", &self.vec_u_16); cache.apply_diff_to_table::("vec_u256", &self.vec_u_256); @@ -2919,6 +2967,7 @@ impl __sdk::DbUpdate for DbUpdate { callbacks.invoke_table_row_callbacks::("one_identity", &self.one_identity, event); callbacks.invoke_table_row_callbacks::("one_simple_enum", &self.one_simple_enum, event); callbacks.invoke_table_row_callbacks::("one_string", &self.one_string, event); + callbacks.invoke_table_row_callbacks::("one_timestamp", &self.one_timestamp, event); callbacks.invoke_table_row_callbacks::("one_u128", &self.one_u_128, event); callbacks.invoke_table_row_callbacks::("one_u16", &self.one_u_16, event); callbacks.invoke_table_row_callbacks::("one_u256", &self.one_u_256, event); @@ -3003,6 +3052,7 @@ impl __sdk::DbUpdate for DbUpdate { callbacks.invoke_table_row_callbacks::("vec_identity", &self.vec_identity, event); callbacks.invoke_table_row_callbacks::("vec_simple_enum", &self.vec_simple_enum, event); callbacks.invoke_table_row_callbacks::("vec_string", &self.vec_string, event); + callbacks.invoke_table_row_callbacks::("vec_timestamp", &self.vec_timestamp, event); callbacks.invoke_table_row_callbacks::("vec_u128", &self.vec_u_128, event); callbacks.invoke_table_row_callbacks::("vec_u16", &self.vec_u_16, event); callbacks.invoke_table_row_callbacks::("vec_u256", &self.vec_u_256, event); @@ -3604,6 +3654,7 @@ impl __sdk::SpacetimeModule for RemoteModule { one_identity_table::register_table(client_cache); one_simple_enum_table::register_table(client_cache); one_string_table::register_table(client_cache); + one_timestamp_table::register_table(client_cache); one_u_128_table::register_table(client_cache); one_u_16_table::register_table(client_cache); one_u_256_table::register_table(client_cache); @@ -3668,6 +3719,7 @@ impl __sdk::SpacetimeModule for RemoteModule { vec_identity_table::register_table(client_cache); vec_simple_enum_table::register_table(client_cache); vec_string_table::register_table(client_cache); + vec_timestamp_table::register_table(client_cache); vec_u_128_table::register_table(client_cache); vec_u_16_table::register_table(client_cache); vec_u_256_table::register_table(client_cache); diff --git a/crates/sdk/tests/test-client/src/module_bindings/one_timestamp_table.rs b/crates/sdk/tests/test-client/src/module_bindings/one_timestamp_table.rs new file mode 100644 index 00000000000..1d5f7b6951e --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/one_timestamp_table.rs @@ -0,0 +1,94 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::one_timestamp_type::OneTimestamp; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `one_timestamp`. +/// +/// Obtain a handle from the [`OneTimestampTableAccess::one_timestamp`] method on [`super::RemoteTables`], +/// like `ctx.db.one_timestamp()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.one_timestamp().on_insert(...)`. +pub struct OneTimestampTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `one_timestamp`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait OneTimestampTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`OneTimestampTableHandle`], which mediates access to the table `one_timestamp`. + fn one_timestamp(&self) -> OneTimestampTableHandle<'_>; +} + +impl OneTimestampTableAccess for super::RemoteTables { + fn one_timestamp(&self) -> OneTimestampTableHandle<'_> { + OneTimestampTableHandle { + imp: self.imp.get_table::("one_timestamp"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct OneTimestampInsertCallbackId(__sdk::CallbackId); +pub struct OneTimestampDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for OneTimestampTableHandle<'ctx> { + type Row = OneTimestamp; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = OneTimestampInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> OneTimestampInsertCallbackId { + OneTimestampInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: OneTimestampInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = OneTimestampDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> OneTimestampDeleteCallbackId { + OneTimestampDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: OneTimestampDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("one_timestamp"); +} +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update_no_primary_key(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/one_timestamp_type.rs b/crates/sdk/tests/test-client/src/module_bindings/one_timestamp_type.rs new file mode 100644 index 00000000000..0544dcad403 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/one_timestamp_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct OneTimestamp { + pub t: __sdk::Timestamp, +} + +impl __sdk::InModule for OneTimestamp { + type Module = super::RemoteModule; +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/vec_timestamp_table.rs b/crates/sdk/tests/test-client/src/module_bindings/vec_timestamp_table.rs new file mode 100644 index 00000000000..77d9e8d1634 --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/vec_timestamp_table.rs @@ -0,0 +1,94 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::vec_timestamp_type::VecTimestamp; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `vec_timestamp`. +/// +/// Obtain a handle from the [`VecTimestampTableAccess::vec_timestamp`] method on [`super::RemoteTables`], +/// like `ctx.db.vec_timestamp()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.vec_timestamp().on_insert(...)`. +pub struct VecTimestampTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `vec_timestamp`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait VecTimestampTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`VecTimestampTableHandle`], which mediates access to the table `vec_timestamp`. + fn vec_timestamp(&self) -> VecTimestampTableHandle<'_>; +} + +impl VecTimestampTableAccess for super::RemoteTables { + fn vec_timestamp(&self) -> VecTimestampTableHandle<'_> { + VecTimestampTableHandle { + imp: self.imp.get_table::("vec_timestamp"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct VecTimestampInsertCallbackId(__sdk::CallbackId); +pub struct VecTimestampDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for VecTimestampTableHandle<'ctx> { + type Row = VecTimestamp; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = VecTimestampInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> VecTimestampInsertCallbackId { + VecTimestampInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: VecTimestampInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = VecTimestampDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> VecTimestampDeleteCallbackId { + VecTimestampDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: VecTimestampDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("vec_timestamp"); +} +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update_no_primary_key(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} diff --git a/crates/sdk/tests/test-client/src/module_bindings/vec_timestamp_type.rs b/crates/sdk/tests/test-client/src/module_bindings/vec_timestamp_type.rs new file mode 100644 index 00000000000..22812ad1d5e --- /dev/null +++ b/crates/sdk/tests/test-client/src/module_bindings/vec_timestamp_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VecTimestamp { + pub t: Vec<__sdk::Timestamp>, +} + +impl __sdk::InModule for VecTimestamp { + type Module = super::RemoteModule; +} diff --git a/crates/sdk/tests/test-client/src/simple_test_table.rs b/crates/sdk/tests/test-client/src/simple_test_table.rs index 7fafa597327..a66ff9b8faa 100644 --- a/crates/sdk/tests/test-client/src/simple_test_table.rs +++ b/crates/sdk/tests/test-client/src/simple_test_table.rs @@ -1,5 +1,5 @@ use crate::module_bindings::*; -use spacetimedb_sdk::{i256, u256, Address, Event, Identity, Table}; +use spacetimedb_sdk::{i256, u256, Address, Event, Identity, Table, Timestamp}; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -185,6 +185,14 @@ impl_simple_test_table! { accessor_method = one_address; } + OneTimestamp { + Contents = Timestamp; + field_name = t; + insert_reducer = insert_one_timestamp; + insert_reducer_event = InsertOneTimestamp; + accessor_method = one_timestamp; + } + OneSimpleEnum { Contents = SimpleEnum; field_name = e; @@ -362,6 +370,14 @@ impl_simple_test_table! { accessor_method = vec_address; } + VecTimestamp { + Contents = Vec; + field_name = t; + insert_reducer = insert_vec_timestamp; + insert_reducer_event = InsertVecTimestamp; + accessor_method = vec_timestamp; + } + VecSimpleEnum { Contents = Vec; field_name = e; diff --git a/crates/sdk/tests/test.rs b/crates/sdk/tests/test.rs index 65278b75ab2..85e0d3decba 100644 --- a/crates/sdk/tests/test.rs +++ b/crates/sdk/tests/test.rs @@ -87,6 +87,16 @@ macro_rules! declare_tests_with_suffix { make_test("delete_address").run(); } + #[test] + fn insert_timestamp() { + make_test("insert_timestamp").run(); + } + + #[test] + fn insert_call_timestamp() { + make_test("insert_call_timestamp").run(); + } + #[test] fn on_reducer() { make_test("on_reducer").run(); diff --git a/modules/benchmarks-cs/circles.cs b/modules/benchmarks-cs/circles.cs index ad41ac90d32..d369427e551 100644 --- a/modules/benchmarks-cs/circles.cs +++ b/modules/benchmarks-cs/circles.cs @@ -31,7 +31,7 @@ public partial struct Circle(uint entity_id, uint player_id, float x, float y, f public uint player_id = player_id; public Vector2 direction = new(x, y); public float magnitude = magnitude; - public ulong last_split_time = (ulong)(DateTimeOffset.UtcNow.Ticks / 10); + public Timestamp last_split_time = (Timestamp)DateTimeOffset.UtcNow; } [SpacetimeDB.Table(Name = "food")] diff --git a/modules/benchmarks-cs/synthetic.cs b/modules/benchmarks-cs/synthetic.cs index 365f98cea62..cf888fd7a56 100644 --- a/modules/benchmarks-cs/synthetic.cs +++ b/modules/benchmarks-cs/synthetic.cs @@ -539,7 +539,8 @@ public static void fn_with_32_args( string _arg30, string _arg31, string _arg32 - ) { } + ) + { } [SpacetimeDB.Reducer] public static void print_many_things(ReducerContext ctx, uint n) diff --git a/modules/benchmarks/src/circles.rs b/modules/benchmarks/src/circles.rs index bf6cba6d88f..a578355f755 100644 --- a/modules/benchmarks/src/circles.rs +++ b/modules/benchmarks/src/circles.rs @@ -42,13 +42,13 @@ pub struct Circle { } impl Circle { - pub fn new(entity_id: u32, player_id: u32, x: f32, y: f32, magnitude: f32) -> Self { + pub fn new(entity_id: u32, player_id: u32, x: f32, y: f32, magnitude: f32, last_split_time: Timestamp) -> Self { Self { entity_id, player_id, direction: Vector2 { x, y }, magnitude, - last_split_time: Timestamp::now(), + last_split_time, } } } @@ -91,9 +91,14 @@ pub fn insert_bulk_entity(ctx: &ReducerContext, count: u32) { #[spacetimedb::reducer] pub fn insert_bulk_circle(ctx: &ReducerContext, count: u32) { for id in 0..count { - ctx.db - .circle() - .insert(Circle::new(id, id, id as f32, (id + 5) as f32, (id * 5) as f32)); + ctx.db.circle().insert(Circle::new( + id, + id, + id as f32, + (id + 5) as f32, + (id * 5) as f32, + ctx.timestamp, + )); } log::info!("INSERT CIRCLE: {count}"); } diff --git a/modules/benchmarks/src/ia_loop.rs b/modules/benchmarks/src/ia_loop.rs index 666ba466d4d..ae530e9e6ba 100644 --- a/modules/benchmarks/src/ia_loop.rs +++ b/modules/benchmarks/src/ia_loop.rs @@ -3,7 +3,7 @@ #![allow(clippy::too_many_arguments, unused_variables)] use crate::Load; -use spacetimedb::{log, ReducerContext, SpacetimeType, Table, Timestamp}; +use spacetimedb::{log, ReducerContext, SpacetimeType, Table}; use std::hash::{Hash, Hasher}; #[spacetimedb::table(name = velocity)] @@ -48,11 +48,14 @@ impl Position { } pub fn moment_milliseconds() -> u64 { - Timestamp::from_micros_since_epoch(1000) - .duration_since(Timestamp::UNIX_EPOCH) - .ok() - .unwrap() - .as_millis() as u64 + 1 + // Duration::from_micros(1000).as_millis() as u64 + // or previously... + // Timestamp::from_micros_since_unix_epoch(1000) + // .duration_since(Timestamp::UNIX_EPOCH) + // .ok() + // .unwrap() + // .as_millis() as u64 } #[derive(SpacetimeType, Debug, Clone, Copy)] diff --git a/modules/rust-wasm-test/src/lib.rs b/modules/rust-wasm-test/src/lib.rs index 598e53464b4..8415ae37aea 100644 --- a/modules/rust-wasm-test/src/lib.rs +++ b/modules/rust-wasm-test/src/lib.rs @@ -120,7 +120,7 @@ pub struct HasSpecialStuff { #[spacetimedb::reducer(init)] pub fn init(ctx: &ReducerContext) { ctx.db.repeating_test_arg().insert(RepeatingTestArg { - prev_time: Timestamp::now(), + prev_time: ctx.timestamp, scheduled_id: 0, scheduled_at: duration!("1000ms").into(), }); @@ -128,7 +128,10 @@ pub fn init(ctx: &ReducerContext) { #[spacetimedb::reducer] pub fn repeating_test(ctx: &ReducerContext, arg: RepeatingTestArg) { - let delta_time = arg.prev_time.elapsed(); + let delta_time = ctx + .timestamp + .duration_since(arg.prev_time) + .expect("arg.prev_time is later than ctx.timestamp... huh?"); log::trace!("Timestamp: {:?}, Delta time: {:?}", ctx.timestamp, delta_time); } diff --git a/modules/sdk-test-cs/Lib.cs b/modules/sdk-test-cs/Lib.cs index e99f07ff095..9bf7a8065f9 100644 --- a/modules/sdk-test-cs/Lib.cs +++ b/modules/sdk-test-cs/Lib.cs @@ -33,6 +33,7 @@ public partial record EnumWithPayload string Str, Identity Identity, Address Address, + Timestamp Timestamp, List Bytes, List Ints, List Strings, @@ -69,6 +70,8 @@ public partial struct EveryPrimitiveStruct public string p; public Identity q; public Address r; + public Timestamp s; + public TimeDuration t; } [SpacetimeDB.Type] @@ -92,6 +95,8 @@ public partial struct EveryVecStruct public List p; public List q; public List
r; + public List s; + public List t; } [SpacetimeDB.Table(Name = "one_u8", Public = true)] @@ -310,6 +315,18 @@ public static void insert_one_address(ReducerContext ctx, Address a) ctx.Db.one_address.Insert(new OneAddress { a = a }); } + [SpacetimeDB.Table(Name = "one_timestamp", Public = true)] + public partial struct OneTimestamp + { + public Timestamp t; + } + + [SpacetimeDB.Reducer] + public static void insert_one_timestamp(ReducerContext ctx, Timestamp t) + { + ctx.Db.one_timestamp.Insert(new OneTimestamp { t = t }); + } + [SpacetimeDB.Table(Name = "one_simple_enum", Public = true)] public partial struct OneSimpleEnum { @@ -598,6 +615,18 @@ public static void insert_vec_address(ReducerContext ctx, List
a) ctx.Db.vec_address.Insert(new VecAddress { a = a }); } + [SpacetimeDB.Table(Name = "vec_timestamp", Public = true)] + public partial struct VecTimestamp + { + public List t; + } + + [SpacetimeDB.Reducer] + public static void insert_vec_timestamp(ReducerContext ctx, List t) + { + ctx.Db.vec_timestamp.Insert(new VecTimestamp { t = t }); + } + [SpacetimeDB.Table(Name = "vec_simple_enum", Public = true)] public partial struct VecSimpleEnum { @@ -1822,6 +1851,12 @@ public static void insert_primitives_as_strings(ReducerContext ctx, EveryPrimiti ); } + [SpacetimeDB.Reducer] + public static void insert_call_timestamp(ReducerContext ctx) + { + ctx.Db.one_timestamp.Insert(new OneTimestamp { t = ctx.Timestamp }); + } + [SpacetimeDB.Table(Name = "table_holds_table", Public = true)] public partial struct TableHoldsTable { diff --git a/modules/sdk-test/src/lib.rs b/modules/sdk-test/src/lib.rs index ea867727ef5..c9fcf147da9 100644 --- a/modules/sdk-test/src/lib.rs +++ b/modules/sdk-test/src/lib.rs @@ -9,7 +9,8 @@ use anyhow::{Context, Result}; use spacetimedb::{ sats::{i256, u256}, - Address, Identity, ReducerContext, SpacetimeType, Table, + spacetimedb_lib::TimeDuration, + Address, Identity, ReducerContext, SpacetimeType, Table, Timestamp, }; #[derive(SpacetimeType)] @@ -39,6 +40,7 @@ pub enum EnumWithPayload { Str(String), Identity(Identity), Address(Address), + Timestamp(Timestamp), Bytes(Vec), Ints(Vec), Strings(Vec), @@ -75,6 +77,8 @@ pub struct EveryPrimitiveStruct { p: String, q: Identity, r: Address, + s: Timestamp, + t: TimeDuration, } #[derive(SpacetimeType)] @@ -97,6 +101,8 @@ pub struct EveryVecStruct { p: Vec, q: Vec, r: Vec
, + s: Vec, + t: Vec, } /// Defines one or more tables, and optionally reducers alongside them. @@ -272,6 +278,8 @@ define_tables! { OneIdentity { insert insert_one_identity } i Identity; OneAddress { insert insert_one_address } a Address; + OneTimestamp { insert insert_one_timestamp } t Timestamp; + OneSimpleEnum { insert insert_one_simple_enum } e SimpleEnum; OneEnumWithPayload { insert insert_one_enum_with_payload } e EnumWithPayload; @@ -307,6 +315,8 @@ define_tables! { VecIdentity { insert insert_vec_identity } i Vec; VecAddress { insert insert_vec_address } a Vec
; + VecTimestamp { insert insert_vec_timestamp } t Vec; + VecSimpleEnum { insert insert_vec_simple_enum } e Vec; VecEnumWithPayload { insert insert_vec_enum_with_payload } e Vec; @@ -587,6 +597,11 @@ fn insert_caller_pk_address(ctx: &ReducerContext, data: i32) -> anyhow::Result<( Ok(()) } +#[spacetimedb::reducer] +fn insert_call_timestamp(ctx: &ReducerContext) { + ctx.db.one_timestamp().insert(OneTimestamp { t: ctx.timestamp }); +} + #[spacetimedb::reducer] fn insert_primitives_as_strings(ctx: &ReducerContext, s: EveryPrimitiveStruct) { ctx.db.vec_string().insert(VecString { @@ -609,6 +624,8 @@ fn insert_primitives_as_strings(ctx: &ReducerContext, s: EveryPrimitiveStruct) { s.p.to_string(), s.q.to_string(), s.r.to_string(), + s.s.to_string(), + s.t.to_string(), ], }); } diff --git a/smoketests/tests/modules.py b/smoketests/tests/modules.py index e93f6b41484..da40510700b 100644 --- a/smoketests/tests/modules.py +++ b/smoketests/tests/modules.py @@ -153,12 +153,12 @@ class UploadModule2(Smoketest): #[spacetimedb::reducer(init)] fn init(ctx: &ReducerContext) { - ctx.db.scheduled_message().insert(ScheduledMessage { prev: Timestamp::now(), scheduled_id: 0, scheduled_at: duration!(100ms).into(), }); + ctx.db.scheduled_message().insert(ScheduledMessage { prev: ctx.timestamp, scheduled_id: 0, scheduled_at: duration!(100ms).into(), }); } #[spacetimedb::reducer] -pub fn my_repeating_reducer(_ctx: &ReducerContext, arg: ScheduledMessage) { - log::info!("Invoked: ts={:?}, delta={:?}", Timestamp::now(), arg.prev.elapsed()); +pub fn my_repeating_reducer(ctx: &ReducerContext, arg: ScheduledMessage) { + log::info!("Invoked: ts={:?}, delta={:?}", ctx.timestamp, ctx.timestamp.duration_since(arg.prev)); } """ def test_upload_module_2(self): diff --git a/smoketests/tests/schedule_reducer.py b/smoketests/tests/schedule_reducer.py index 1ef37e4840e..c5f805bed95 100644 --- a/smoketests/tests/schedule_reducer.py +++ b/smoketests/tests/schedule_reducer.py @@ -1,6 +1,7 @@ from .. import Smoketest import time + class CancelReducer(Smoketest): MODULE_CODE = """ @@ -51,6 +52,9 @@ def test_cancel_reducer(self): self.assertNotIn("the reducer ran", logs) +TIMESTAMP_ZERO = {"__timestamp_micros_since_unix_epoch__": 0} + + class SubscribeScheduledTable(Smoketest): MODULE_CODE = """ use spacetimedb::{log, duration, ReducerContext, Table, Timestamp}; @@ -66,19 +70,20 @@ class SubscribeScheduledTable(Smoketest): #[spacetimedb::reducer] fn schedule_reducer(ctx: &ReducerContext) { - ctx.db.scheduled_table().insert(ScheduledTable { prev: Timestamp::from_micros_since_epoch(0), scheduled_id: 2, sched_at: Timestamp::from_micros_since_epoch(0).into(), }); + ctx.db.scheduled_table().insert(ScheduledTable { prev: Timestamp::from_micros_since_unix_epoch(0), scheduled_id: 2, sched_at: Timestamp::from_micros_since_unix_epoch(0).into(), }); } #[spacetimedb::reducer] fn schedule_repeated_reducer(ctx: &ReducerContext) { - ctx.db.scheduled_table().insert(ScheduledTable { prev: Timestamp::from_micros_since_epoch(0), scheduled_id: 1, sched_at: duration!(100ms).into(), }); + ctx.db.scheduled_table().insert(ScheduledTable { prev: Timestamp::from_micros_since_unix_epoch(0), scheduled_id: 1, sched_at: duration!(100ms).into(), }); } #[spacetimedb::reducer] -pub fn my_reducer(_ctx: &ReducerContext, arg: ScheduledTable) { - log::info!("Invoked: ts={:?}, delta={:?}", Timestamp::now(), arg.prev.elapsed()); +pub fn my_reducer(ctx: &ReducerContext, arg: ScheduledTable) { + log::info!("Invoked: ts={:?}, delta={:?}", ctx.timestamp, ctx.timestamp.duration_since(arg.prev)); } """ + def test_scheduled_table_subscription(self): """This test deploys a module with a scheduled reducer and check if client receives subscription update for scheduled table entry and deletion of reducer once it ran""" # subscribe to empy scheduled_table @@ -91,11 +96,19 @@ def test_scheduled_table_subscription(self): # scheduled reducer should be ran by now self.assertEqual(lines, 1) - row_entry = {'prev': 0, 'scheduled_id': 2, 'sched_at': {'Time': 0}} + row_entry = { + "prev": TIMESTAMP_ZERO, + "scheduled_id": 2, + "sched_at": {"Time": TIMESTAMP_ZERO}, + } # subscription should have 2 updates, first for row insert in scheduled table and second for row deletion. - self.assertEqual(sub(), [{'scheduled_table': {'deletes': [], 'inserts': [row_entry]}}, {'scheduled_table': {'deletes': [row_entry], 'inserts': []}}]) - - + self.assertEqual( + sub(), + [ + {"scheduled_table": {"deletes": [], "inserts": [row_entry]}}, + {"scheduled_table": {"deletes": [row_entry], "inserts": []}}, + ], + ) def test_scheduled_table_subscription_repeated_reducer(self): """This test deploys a module with a repeated reducer and check if client receives subscription update for scheduled table entry and no delete entry""" @@ -112,11 +125,25 @@ def test_scheduled_table_subscription_repeated_reducer(self): # scheduling repeated reducer again just to get 2nd subscription update. self.call("schedule_reducer") - repeated_row_entry = {'prev': 0, 'scheduled_id': 1, 'sched_at': {'Interval': 100000}} - row_entry = {'prev': 0, 'scheduled_id': 2, 'sched_at': {'Time': 0}} + repeated_row_entry = { + "prev": TIMESTAMP_ZERO, + "scheduled_id": 1, + "sched_at": {"Interval": {"__time_duration_micros__": 100000}}, + } + row_entry = { + "prev": TIMESTAMP_ZERO, + "scheduled_id": 2, + "sched_at": {"Time": TIMESTAMP_ZERO}, + } # subscription should have 2 updates and should not have any deletes - self.assertEqual(sub(), [{'scheduled_table': {'deletes': [], 'inserts': [repeated_row_entry]}}, {'scheduled_table': {'deletes': [], 'inserts': [row_entry]}}]) + self.assertEqual( + sub(), + [ + {"scheduled_table": {"deletes": [], "inserts": [repeated_row_entry]}}, + {"scheduled_table": {"deletes": [], "inserts": [row_entry]}}, + ], + ) class VolatileNonatomicScheduleImmediate(Smoketest): @@ -139,6 +166,7 @@ class VolatileNonatomicScheduleImmediate(Smoketest): ctx.db.my_table().insert(MyTable { x }); } """ + def test_volatile_nonatomic_schedule_immediate(self): """Check that volatile_nonatomic_schedule_immediate works""" @@ -147,4 +175,10 @@ def test_volatile_nonatomic_schedule_immediate(self): self.call("do_insert", "yay!") self.call("do_schedule") - self.assertEqual(sub(), [{'my_table': {'deletes': [], 'inserts': [{'x': 'yay!'}]}}, {'my_table': {'deletes': [], 'inserts': [{'x': 'hello'}]}}]) + self.assertEqual( + sub(), + [ + {"my_table": {"deletes": [], "inserts": [{"x": "yay!"}]}}, + {"my_table": {"deletes": [], "inserts": [{"x": "hello"}]}}, + ], + )