Skip to content

mikaelwills/spacetimedb-dart-sdk

Repository files navigation

SpacetimeDB Dart SDK

License Dart

Dart SDK for SpacetimeDB with WebSocket sync, BSATN encoding, and code generation.

Features

  • WebSocket connection with auto-reconnect and SSL/TLS
  • Connection status monitoring and quality metrics
  • BSATN binary encoding/decoding
  • Client-side table cache with change streams
  • Subscription management with SQL queries
  • Type-safe reducer calling with results
  • Code generation (tables, reducers, sum types, views)
  • Authentication with OIDC and token persistence
  • Transaction events with context
  • Offline-first support with optimistic updates and mutation queue

Quick Start

1. Install SpacetimeDB CLI

curl --proto '=https' --tlsv1.2 -sSf https://install.spacetimedb.com | sh

2. Add dependency

dependencies:
  spacetimedb_dart_sdk:
    git: https://github.com/mikaelwills/spacetimedb_dart_sdk.git

3. Generate client code

# From running server
dart run spacetimedb_dart_sdk:generate -s http://localhost:3000 -d your_database -o lib/generated

# Or from local Rust project
dart run spacetimedb_dart_sdk:generate -p path/to/module -o lib/generated

4. Connect

import 'package:your_app/generated/client.dart';

final client = await SpacetimeDbClient.connect(
  host: 'localhost:3000',
  database: 'your_database',
  ssl: false,
  authStorage: InMemoryTokenStore(),
  initialSubscriptions: ['SELECT * FROM users'],
  subscriptionTimeout: Duration(seconds: 10),
);

Tables

// Iterate
for (final user in client.users.iter()) {
  print(user.name);
}

// Count and check
print(client.users.count);
print(client.users.isEmpty);

// All change streams
client.users.insertStream.listen((user) => print('Added: ${user.name}'));
client.users.updateStream.listen((update) => print('${update.oldRow.name} → ${update.newRow.name}'));
client.users.deleteStream.listen((user) => print('Removed: ${user.name}'));

// With transaction context
client.users.insertEventStream.listen((event) {
  print('Row: ${event.row.name}');
  print('Timestamp: ${event.context.timestamp}');
  print('Energy: ${event.context.energyConsumed}');
});

// Filter to only my transactions
client.users.myInserts.listen((event) => showToast('Created ${event.row.name}'));
client.users.myUpdates.listen((event) => showToast('Updated'));
client.users.myDeletes.listen((event) => showToast('Deleted'));

Reducers

// Call and get result
final result = await client.reducers.createUser(name: 'Alice', email: 'alice@example.com');
print(result.isSuccess);
print(result.energyConsumed);
print(result.executionDuration);

// Listen to reducer events (from any client)
client.reducers.onCreateUser((ctx, name, email) {
  print('User created: $name');
  print('By: ${ctx.callerIdentity}');
});

Views

// Vec<T> view - multiple rows
for (final user in client.activeUsers.iter()) {
  print(user.name);
}

// Option<T> view - single optional row
final admin = client.currentAdmin; // User?
if (admin != null) {
  print('Admin: ${admin.name}');
}

// T view - single required row (throws if empty)
final config = client.appConfig; // Config
print(config.version);

Subscriptions

// Subscribe to more queries after connect
await client.subscriptions.subscribe([
  'SELECT * FROM messages WHERE room_id = 123',
]);

Sum Types (Rust Enums)

// Rust enum becomes Dart sealed class
enum Status {
    Pending,
    Active { since: u64 },
    Banned { reason: String },
}

// Pattern match with exhaustiveness checking
final message = switch (user.status) {
  StatusPending() => 'Waiting for approval',
  StatusActive(:final since) => 'Active since $since',
  StatusBanned(:final reason) => 'Banned: $reason',
};

// Construct
final status = StatusActive(DateTime.now().millisecondsSinceEpoch);

Authentication

// Connect with persistent storage
final client = await SpacetimeDbClient.connect(
  host: 'spacetimedb.com',
  database: 'myapp',
  ssl: true,
  authStorage: YourTokenStore(), // implements AuthTokenStore
);

// Access identity after connect
print(client.identity?.toHexString);  // Full 32-byte hex
print(client.identity?.toAbbreviated); // "2ab4...9f1c"
print(client.address);
print(client.token);

// OAuth flow
final authUrl = client.getAuthUrl('google', redirectUri: 'myapp://callback');
// Open authUrl in browser, then handle callback:
final token = client.parseTokenFromCallback(callbackUrl);

// Logout
await client.logout();

Connection Status

client.connection.connectionStatus.listen((status) {
  switch (status) {
    case ConnectionStatus.connecting: showSpinner();
    case ConnectionStatus.connected: hideSpinner();
    case ConnectionStatus.reconnecting: showBanner();
    case ConnectionStatus.disconnected: showError();
    case ConnectionStatus.fatalError: showRetry();
  }
});

client.connection.connectionQuality.listen((quality) {
  print('Health: ${quality.healthScore}');
  print('Latency: ${quality.latency}');
  print('Reconnects: ${quality.reconnectAttempts}');
});

SSL

ssl: false  // Development: ws://, http://
ssl: true   // Production: wss://, https://

Offline Support

final client = await SpacetimeDbClient.connect(
  host: 'localhost:3000',
  database: 'myapp',
  offlineStorage: JsonFileStorage(basePath: '/path/to/cache'),
  onCacheLoaded: (client) => print('Loaded ${client.notes.count} cached notes'),
);

Storage options: JsonFileStorage (file-based), InMemoryOfflineStorage (testing), or implement OfflineStorage interface for custom backends.

Optimistic Updates

// Insert with immediate UI update
await client.reducers.createNote(
  id: uuid.v4(), // Client-side IDs required for optimistic inserts
  title: 'New Note',
  optimisticChanges: [OptimisticChange.insert('note', note.toJson())],
);

// Update/delete
optimisticChanges: [OptimisticChange.update('note', oldRow.toJson(), newRow.toJson())]
optimisticChanges: [OptimisticChange.delete('note', row.toJson())]

Sync State

print(client.syncState.pendingCount);  // Queued mutations
client.onSyncStateChanged.listen((state) => showSyncIndicator(state.hasPending));
client.onMutationSyncResult.listen((r) => r.success ? null : showError(r.error));

Offline behavior: cached data loads instantly, mutations queue locally, optimistic changes update UI, queued mutations replay on reconnect, failed mutations roll back.

Testing

dart test

See TESTING.md for details.

License

Apache 2.0

About

A dart SDK for SpacetimeDB

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages