dartvex 0.2.0 copy "dartvex: ^0.2.0" to clipboard
dartvex: ^0.2.0 copied to clipboard

Pure Dart client for Convex with WebSocket sync, type-safe values, and reactive subscriptions. Works on iOS, Android, web, and desktop.

Dartvex

dartvex #

Pure Dart client for Convex with WebSocket sync, type-safe values, and reactive subscriptions. Works on iOS, Android, web, and desktop.

Dartvex Flutter demo — real-time chats running on iOS and macOS

The Dartvex ecosystem #

Package Description
dartvex Core client — WebSocket sync, subscriptions, auth
dartvex_flutter Flutter widgets — Provider, Query, Mutation
dartvex_codegen CLI code generator — type-safe Dart bindings from schema
dartvex_local Offline support — SQLite cache, mutation queue
dartvex_auth_better Better Auth adapter

Building a Flutter app? Start with dartvex_flutter — it pulls in dartvex automatically.

Source and full docs: github.com/AndreFrelicot/dartvex

Features #

  • Pure Dart — no Rust FFI, no Flutter dependency, works in CLI/server apps
  • Full Convex sync protocol: subscribe, query, mutate, action
  • Read-your-writes mutation semantics
  • Transition chunk reassembly
  • Special value encoding ($integer, $bytes, $float)
  • Structured query errors with Convex error data and server log lines
  • Auth framework with pluggable AuthProvider<T> abstraction
  • One-shot query via queryOnce<T>() for non-reactive reads
  • File storage helpers via ConvexStorage (upload/download)
  • Reconnection with bounded handshake, jittered exponential backoff, connectivity-triggered immediate reconnect, full query set rebuild, and safe replay of queued mutations
  • Optimistic updates with automatic rollback when the mutation settles or fails
  • Reactive, gapless pagination via paginatedQuery
  • Rich connection status (inflight counts, retries, hasEverConnected) plus an auth-refreshing signal for "authenticating…" indicators
  • Native and browser WebSocket adapters (conditional import)
  • Structured opt-in logging for transport, auth, and storage diagnostics

Platform Support #

Platform Default transport Status
iOS / Android dart:io WebSocket Tested
macOS / Linux / Windows dart:io WebSocket Tested
Web (JS / Wasm) package:web WebSocket Tested

The web adapter is selected automatically via conditional import. These are the defaults for the pure-Dart package; in Flutter apps, dartvex_flutter automatically replaces the dart:io transport on iOS and macOS with an NSURLSession-backed one (the same system network path Safari and native apps use), via the process-wide defaultWebSocketAdapterOverride / defaultHttpClientFactory seams exported by this package. An explicit ConvexClientConfig.adapterFactory always takes precedence.

Installation #

dependencies:
  dartvex: ^0.2.0

Usage #

import 'package:dartvex/dartvex.dart';

final client = ConvexClient('https://your-deployment.convex.cloud');

final subscription = client.subscribe('messages:list', {'channel': 'general'});
subscription.stream.listen((result) {
  switch (result) {
    case QuerySuccess(:final value):
      print(value);
    case QueryError(:final message):
      print(message);
    case QueryLoading():
      break; // Cleared by an optimistic update; a concrete result follows.
  }
});

final current = await client.query('messages:list', {'channel': 'general'});
await client.mutate('messages:send', {'body': 'Hello'});

Auth #

Preferred mobile-style auth:

final authClient = client.withAuth(myAuthProvider);

authClient.authState.listen((state) {
  switch (state) {
    case AuthAuthenticated(:final userInfo):
      print(userInfo);
    case AuthLoading():
      print('Signing in...');
    case AuthUnauthenticated():
      print('Signed out');
  }
});

await authClient.login();
await authClient.loginFromCache();
await authClient.logout();

Low-level manual token auth remains available:

await client.setAuth('jwt-token');

final handle = await client.setAuthWithRefresh(
  fetchToken: ({required bool forceRefresh}) async {
    return obtainJwtFromYourAuthProvider(forceRefresh: forceRefresh);
  },
  onAuthChange: (isAuthenticated) => print(isAuthenticated),
);

await handle.cancel();
await client.clearAuth();

One-shot Query #

For non-reactive reads (splash screen, config loading), use queryOnce<T>():

final config = await client.queryOnce<Map<String, dynamic>>('settings:get');
final userName = await client.queryOnce<String>('users:getName', {'id': userId});

Convex Values #

Use Dart int or double for Convex v.number() arguments:

await client.mutate('scores:set', {'score': 42, 'ratio': 0.5});

Use convexInt64(value) or BigInt.from(value) for Convex v.int64() arguments. Convex int64 results decode as BigInt.

await client.mutate('counters:set', {'count': convexInt64(42)});

final count = await client.queryOnce<BigInt>('counters:get');

Plain Dart int values intentionally stay JSON numbers so v.number() calls continue to work as expected.

Query Errors #

Subscription errors expose the human-readable message plus optional structured Convex error data and server log lines:

subscription.stream.listen((result) {
  switch (result) {
    case QuerySuccess(:final value):
      print(value);
    case QueryError(:final message, :final data, :final logLines):
      print(message);
      print(data);
      print(logLines);
    case QueryLoading():
      break;
  }
});

One-shot query() and queryOnce<T>() failures throw ConvexException with the same message, data, and logLines fields.

File Storage #

Upload and download files using ConvexStorage:

final storage = ConvexStorage(client);

// Upload
final storageId = await storage.uploadFile(
  uploadUrlAction: 'files:generateUploadUrl',
  bytes: imageBytes,
  filename: 'photo.jpg',
  contentType: 'image/jpeg',
);

// Get download URL
final url = await storage.getFileUrl(
  getUrlAction: 'files:getUrl',
  storageId: storageId,
);

Logging #

dartvex is silent by default.

To enable structured diagnostic logs, configure ConvexClientConfig:

final client = ConvexClient(
  'https://your-deployment.convex.cloud',
  config: ConvexClientConfig(
    logLevel: DartvexLogLevel.info,
    logger: (event) {
      print('[${event.level.name}] ${event.tag}: ${event.message}');
    },
  ),
);

Recommended usage:

  • error: request or transport failures
  • warn: degraded behavior, retries, large or slow transitions
  • info: lifecycle events
  • debug: integration diagnostics

Sensitive values such as auth tokens should not be logged.

Connection Control #

By default, ConvexClient opens its WebSocket when constructed. Set connectImmediately: false to defer the connection until the first backend operation, auth update, or explicit reconnect:

final client = ConvexClient(
  'https://your-deployment.convex.cloud',
  config: const ConvexClientConfig(connectImmediately: false),
);

await client.reconnectNow('manual-refresh');

Dropped connections are retried with exponential backoff and jitter, classifying server overload reasons to back off more conservatively. Tune the behavior — or bound the handshake itself so a dead connection cannot hang on the platform TCP timeout — via ConvexClientConfig:

final client = ConvexClient(
  'https://your-deployment.convex.cloud',
  config: const ConvexClientConfig(
    connectTimeout: Duration(seconds: 10),
    initialBackoff: Duration(seconds: 1),
    maxBackoff: Duration(seconds: 16),
    backoffJitter: 0.5,
  ),
);

To reconnect the instant the device regains connectivity instead of waiting out the backoff, supply a connectivitySignal. Flutter apps can use ConnectivityPlusSignal from dartvex_flutter.

Optimistic Updates #

Pass an OptimisticUpdate as the third argument to mutate to overlay query results the instant the mutation is sent. The overlay is replayed whenever fresh server data arrives while the mutation is pending, and is rolled back automatically when the mutation completes (replaced by the authoritative result, without flicker) or fails:

await client.mutate(
  'messages:send',
  {'channel': 'general', 'body': 'Hello'},
  (store) {
    final existing = store.getQuery('messages:list', {'channel': 'general'});
    final messages = existing is List ? List<dynamic>.from(existing) : <dynamic>[];
    messages.add({'_id': 'optimistic', 'body': 'Hello'});
    store.setQuery('messages:list', {'channel': 'general'}, messages);
  },
);

The update must be pure (it can be replayed multiple times). Read current values with store.getQuery(name, args) / store.getAllQueries(name) and overlay new ones with store.setQuery(name, args, value)null is the real Convex null value; use store.clearQuery(name, args) to clear a query to a loading state.

Reactive Pagination #

paginatedQuery drives a Convex paginated query (one taking paginationOpts) as a growing, reactive list. Loaded pages update live and stay gapless across reconnects via query journals:

final page = client.paginatedQuery(
  'messages:paginate',
  {'channel': 'general'},
  pageSize: 20,
);

page.stream.listen((result) {
  print('${result.results.length} items, status: ${result.status}');
});

if (!page.isDone) {
  page.loadMore();
}

// Release every page subscription when done.
page.cancel();

ConvexPaginationStatus reports loadingFirstPage, loadingMore, canLoadMore, exhausted, or error.

Connection Status #

currentConnectionStatus and the value-deduplicated connectionStatus stream expose a rich ConnectionStatus snapshot — useful for loading and retry indicators. The coarse ConnectionState enum and connectionState stream remain available as a derived convenience:

client.connectionStatus.listen((status) {
  print('connected: ${status.isConnected}');     // socket up AND fully synced
  print('loading:   ${status.isLoading}');        // not yet re-synced
  print('inflight:  ${status.inflightMutations} mutations, '
      '${status.inflightActions} actions');
  print('retries:   ${status.connectionRetries}');
});

ConnectionStatus also carries isWebSocketConnected, hasEverConnected, connectionCount, timeOfOldestInflightRequest, hasInflightRequests, and the derived coarse state.

Auth Refreshing #

When auth is recovered after a server rejection, the socket briefly stops while a fresh token is fetched. isAuthRefreshing and the authRefreshing stream report this so you can show an "authenticating…" indicator instead of surfacing the transient disconnect:

client.authRefreshing.listen((isRefreshing) {
  if (isRefreshing) showAuthenticatingBanner();
  else hideAuthenticatingBanner();
});

Flutter apps can use ConvexAuthRefreshingBuilder and ConvexConnectionStatusBuilder from dartvex_flutter.

API Overview #

Core #

Class Description
ConvexClient Main client — connect, subscribe, mutate, act
ConvexClientConfig Configuration (client ID, timeouts, backoff, logging)
ConvexSubscription Reactive subscription handle with stream
QueryResult Base type for QuerySuccess / QueryError / QueryLoading
ConnectionStatus Rich connection snapshot — inflight counts, retries, sync
ConvexPaginatedQuery Reactive, gapless paginated query handle
OptimisticLocalStore Overlay store passed to optimistic updates
ConvexStorage File upload and URL generation

Auth #

Class Description
ConvexClientWithAuth Client with integrated authentication
ConvexAuthClient Auth-aware client wrapper
AuthProvider Interface for auth adapters
AuthState AuthAuthenticated / AuthUnauthenticated / AuthLoading

Full Documentation #

See the Dartvex monorepo for full documentation, examples, and the Flutter widget package.

1
likes
160
points
143
downloads

Documentation

API reference

Publisher

verified publisherandrefrelicot.dev

Weekly Downloads

Pure Dart client for Convex with WebSocket sync, type-safe values, and reactive subscriptions. Works on iOS, Android, web, and desktop.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#convex #realtime #websocket #sync #client

License

MIT (license)

Dependencies

collection, http, meta, uuid, web

More

Packages that depend on dartvex