Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ unicode-ident = "1.0.12"
unicode-normalization = "0.1.23"
url = "2.3.1"
urlencoding = "2.1.2"
uuid = { version = "1.18.1", features = ["v4"] }
uuid = { version = "1.18.1", default-features = false }
v8 = "140.2"
walkdir = "2.2.5"
wasmbin = "0.6"
Expand Down
16 changes: 16 additions & 0 deletions crates/bindings-csharp/BSATN.Runtime/BSATN/U128.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,20 @@ private BigInteger AsBigInt() =>

/// <inheritdoc cref="object.ToString()" />
public override string ToString() => AsBigInt().ToString();

public Guid ToGuid()
{
Span<byte> bytes = stackalloc byte[16];
if (BitConverter.IsLittleEndian)
{
BitConverter.TryWriteBytes(bytes, _lower);
BitConverter.TryWriteBytes(bytes[8..], _upper);
}
else
{
BitConverter.TryWriteBytes(bytes, _upper);
BitConverter.TryWriteBytes(bytes[8..], _lower);
}
return new Guid(bytes);
}
}
225 changes: 224 additions & 1 deletion crates/bindings-csharp/BSATN.Runtime/Builtins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ public readonly TimeDuration TimeDurationSince(Timestamp earlier) =>
public static Timestamp operator -(Timestamp point, TimeDuration interval) =>
new Timestamp(checked(point.MicrosecondsSinceUnixEpoch - interval.Microseconds));

public int CompareTo(Timestamp that)
public readonly int CompareTo(Timestamp that)
{
return this.MicrosecondsSinceUnixEpoch.CompareTo(that.MicrosecondsSinceUnixEpoch);
}
Expand Down Expand Up @@ -605,3 +605,226 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
// --- / customized ---
}
}

/// <summary>
/// A generator for monotonically increasing <see cref="Timestamp"/> s by millisecond increments.
/// </summary>
public sealed class ClockGenerator(Timestamp start)
{
private long _microsSinceUnixEpoch = start.MicrosecondsSinceUnixEpoch;

/// <summary>
/// Returns the next <see cref="Timestamp"/> in the sequence, guaranteed to be
/// greater than the previous one returned by this method.
///
/// UUIDv7 requires monotonic millisecond timestamps, so each tick
/// increases the timestamp by at least 1 millisecond (1_000 microseconds).
///
/// # Exceptions
///
/// If the internal timestamp overflows i64 microseconds.
/// </summary>
public Timestamp Tick()
{
checked
{
_microsSinceUnixEpoch += 1000;
}
return new Timestamp(_microsSinceUnixEpoch);
}

public static implicit operator ClockGenerator(Timestamp t) => new(t);
}

/// <summary>
/// A universally unique identifier (UUID).
///
/// Wraps the native <see cref="Guid"/> type and provides methods
/// to generate nil, random (v4), and time-ordered (v7) UUIDs.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly record struct Uuid : IEquatable<Uuid>, IComparable, IComparable<Uuid>
{
private readonly U128 value;
internal Uuid(U128 val) => value = val;
public static readonly Uuid NIL = new(FromGuid(Guid.Empty));

public static Uuid Nil() => NIL;

public static U128 FromGuid(Guid guid)
{
Span<byte> bytes = stackalloc byte[16];
guid.TryWriteBytes(bytes);
if (BitConverter.IsLittleEndian)
{
var lower = BitConverter.ToUInt64(bytes);
var upper = BitConverter.ToUInt64(bytes[8..]);
return new U128(upper, lower);
}
else
{
var upper = BitConverter.ToUInt64(bytes);
var lower = BitConverter.ToUInt64(bytes[8..]);
return new U128(upper, lower);
}
}

private static Guid GuidV4(ReadOnlySpan<byte> randomBytes)
{
if (randomBytes.Length != 16)
{
throw new ArgumentException("Must be 16 bytes", nameof(randomBytes));
}

Span<byte> bytes = stackalloc byte[16];
randomBytes.CopyTo(bytes);
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x40); // version 4
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant RFC 4122

return new Guid(randomBytes);
}

/// <summary>
/// Create a UUIDv4 from explicit random bytes.
/// </summary>
/// <remarks>
/// This method assumes the provided bytes are already sufficiently random;
/// it will only set the appropriate bits for the UUID version and variant.
/// </remarks>
/// <example>
/// <code>
/// var randomBytes = new byte[16];
/// var uuid = Uuid.FromRandomBytesV4(randomBytes);
/// Console.WriteLine(uuid);
/// // Output: 00000000-0000-4000-8000-000000000000
/// </code>
/// </example>
public static Uuid FromRandomBytesV4(ReadOnlySpan<byte> randomBytes)
{
return new(FromGuid(GuidV4(randomBytes)));
}

/// <summary>
/// Create a UUIDv7 from a UNIX timestamp (milliseconds) and 10 random bytes.
/// </summary>
/// <remarks>
/// This method sets the variant field within the counter bytes without
/// shifting data around it. Callers using the counter as a monotonic
/// value should avoid storing significant data in the two least significant
/// bits of the third byte.
/// </remarks>
/// <example>
/// <code>
/// ulong millis = 1686000000000UL;
/// var randomBytes = new byte[10];
/// var uuid = Uuid.FromUnixMillisV7(millis, randomBytes);
/// Console.WriteLine(uuid);
/// // Output: 01888d6e-5c00-7000-8000-000000000000
/// </code>
/// </example>
public static Uuid FromUnixMillisV7(long millisSinceUnixEpoch, ReadOnlySpan<byte> randomBytes)
{
// TODO: Convert to ` CreateVersion7` from .NET 9 when we can.
if (millisSinceUnixEpoch < 0)
{
throw new ArgumentOutOfRangeException(nameof(millisSinceUnixEpoch), "Timestamp precedes Unix epoch");
}

// Generate random 16 bytes
var bytes = GuidV4(randomBytes).ToByteArray();

// Insert 48-bit timestamp (big endian)
bytes[0] = (byte)((millisSinceUnixEpoch >> 40) & 0xFF);
bytes[1] = (byte)((millisSinceUnixEpoch >> 32) & 0xFF);
bytes[2] = (byte)((millisSinceUnixEpoch >> 24) & 0xFF);
bytes[3] = (byte)((millisSinceUnixEpoch >> 16) & 0xFF);
bytes[4] = (byte)((millisSinceUnixEpoch >> 8) & 0xFF);
bytes[5] = (byte)(millisSinceUnixEpoch & 0xFF);

// Set version (0111) and variant (10xx)
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x70);
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);

return new Uuid(FromGuid(new Guid(bytes)));
}

/// <summary>
/// Generate a UUIDv7 using a monotonic <see cref="ClockGenerator"/>.
/// </summary>
/// <remarks>
/// This method sets the variant field within the counter bytes without
/// shifting data around it. Callers using the counter as a monotonic
/// value should avoid storing significant data in the two least significant
/// bits of the third byte.
/// </remarks>
/// <example>
/// <code>
/// var clock = new ClockGenerator(1686000000000UL);
/// var randomBytes = new byte[10];
/// var uuid = Uuid.FromClockV7(clock, randomBytes);
/// Console.WriteLine(uuid);
/// // Output: 0000647e-5181-7000-8000-000000000000
/// </code>
/// </example>
public static Uuid FromClockV7(ClockGenerator clock, ReadOnlySpan<byte> randomBytes)

{
var millis = clock.Tick().MicrosecondsSinceUnixEpoch / 1000;
return FromUnixMillisV7(millis, randomBytes);
}

/// <summary>
/// Parses a UUID from its string representation.
/// </summary>
/// <example>
/// <code>
/// var s = "01888d6e-5c00-7000-8000-000000000000";
/// var uuid = Uuid.Parse(s);
/// Console.WriteLine(uuid.ToString() == s); // True
/// </code>
/// </example>
public static Uuid Parse(string s) => new(FromGuid(Guid.Parse(s)));

/// <summary>
/// Converts this instance to a <see cref="Guid"/>.
/// </summary>
public Guid ToGuid() => value.ToGuid();

public override readonly string ToString() => ToGuid().ToString();

public readonly int CompareTo(Uuid other) => ToGuid().CompareTo(other.ToGuid());
/// <inheritdoc cref="IComparable.CompareTo(object)" />
public int CompareTo(object? value)
{
if (value is Uuid other)
{
return CompareTo(other);
}
else if (value is null)
{
return 1;
}
else
{
throw new ArgumentException("Argument must be a Uuid", nameof(value));
}
}
public static bool operator <(Uuid l, Uuid r) => l.CompareTo(r) < 0;
public static bool operator >(Uuid l, Uuid r) => l.CompareTo(r) > 0;


public readonly partial struct BSATN : IReadWrite<Uuid>
{
public Uuid Read(BinaryReader reader) => new(new SpacetimeDB.BSATN.U128Stdb().Read(reader));
public void Write(BinaryWriter writer, Uuid value) => new SpacetimeDB.BSATN.U128Stdb().Write(writer, value.value);
// --- / 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("__uuid__", new AlgebraicType.U128(default))
]);
}
}
50 changes: 47 additions & 3 deletions crates/bindings-csharp/Codegen/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ or SpecialType.System_Int64
SpecialType.System_String or SpecialType.System_Boolean => true,
SpecialType.None => type.ToString()
is "SpacetimeDB.ConnectionId"
or "SpacetimeDB.Identity",
or "SpacetimeDB.Identity"
or "SpacetimeDB.Uuid",
_ => false,
}
)
Expand Down Expand Up @@ -1116,15 +1117,58 @@ public sealed record ReducerContext : DbContext<Local>, Internal.IReducerContext
// We need this property to be non-static for parity with client SDK.
public Identity Identity => Internal.IReducerContext.GetIdentity();

internal ReducerContext(Identity identity, ConnectionId? connectionId, Random random, Timestamp time) {
internal ReducerContext(Identity identity, ConnectionId? connectionId, Random random, Timestamp time) {
Sender = identity;
ConnectionId = connectionId;
Rng = random;
Timestamp = time;
AuthCtx = AuthCtx.BuildFromSystemTables(connectionId, identity);
}
}


/// <summary>
/// Create a new UUIDv4 using the built-in RNG.
/// </summary>
/// <remarks>
/// This method fills 16 random bytes using the context RNG,
/// sets version and variant bits for UUIDv4, and returns the result.
/// </remarks>
/// <example>
/// <code>
/// var uuid = ctx.NewUuidV4();
/// Console.WriteLine(uuid);
/// </code>
/// </example>
public Uuid NewUuidV4()
{
var bytes = new byte[16];
Rng.NextBytes(bytes);
return Uuid.FromRandomBytesV4(bytes);
}

/// <summary>
/// Create a new UUIDv7 using the provided <see cref="ClockGenerator"/>.
/// </summary>
/// <remarks>
/// To preserve monotonicity guarantees, do not call this from multiple
/// threads or contexts sharing the same <see cref="ClockGenerator"/>.
/// Use a dedicated instance per logical context.
/// </remarks>
/// <example>
/// <code>
/// var clock = new ClockGenerator(ctx.Timestamp);
/// var uuid = ctx.NewUuidV7(clock);
/// Console.WriteLine(uuid);
/// </code>
/// </example>
public Uuid NewUuidV7(ClockGenerator clock)
{
var bytes = new byte[10];
Rng.NextBytes(bytes);
return Uuid.FromClockV7(clock, bytes);
}
}

namespace Internal.TableHandles {
{{string.Join("\n", tableViews.Select(v => v.view))}}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/bindings-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@
"dependencies": {
"base64-js": "^1.5.1",
"fast-text-encoding": "^1.0.0",
"prettier": "^3.3.3"
"prettier": "^3.3.3",
"uuid": "^13.0.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0",
Expand Down
1 change: 1 addition & 0 deletions crates/bindings-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as BinaryWriter } from './lib/binary_writer';
export * from './lib/schedule_at';
export * from './lib/time_duration';
export * from './lib/timestamp';
export * from './lib/uuid';
export * from './lib/utils';
export * from './lib/identity';
export * from './lib/option';
Expand Down
Loading