read method

Future<T?> read(
  1. String id, {
  2. String? userId,
  3. List<String> withRelated = const [],
  4. bool includeDeleted = false,
})

Reads a single entity by its ID from the primary local adapter. Reads a single entity by its ID from the primary local adapter.

Soft-delete tombstones (isDeleted: true) are treated as absent unless includeDeleted is true — a deleted entity should be gone from the app's perspective while the tombstone still syncs underneath.

The withRelated parameter allows eager loading of related entities.

Implementation

Future<T?> read(String id, {String? userId, List<String> withRelated = const [], bool includeDeleted = false}) async {
  _ensureInitialized();

  // Create cache key for entity existence
  final cacheKey = '${T.toString()}:$id:${userId ?? ''}';

  // The existence cache holds only positive entries (see below), and a
  // positive hit still requires the fetch — so no read shortcut exists here.
  var entity = await localAdapter.read(id, userId: userId);
  if (!includeDeleted && (entity?.isDeleted ?? false)) entity = null;

  // Only cache POSITIVE existence. Caching a negative (absent) result caused
  // stale reads in offline-first/realtime scenarios: once an entity was cached
  // as "does not exist", data that later arrived via sync or a realtime push
  // was never observed, breaking delete/conflict resolution and reactive
  // reads. A positive entry is safe because reads always re-fetch the row.
  if (entity != null) {
    _cacheCoordinator.entityExistenceCache[cacheKey] = true;
    _logger.debug('Cached entity existence for key: $cacheKey (exists: true)');
  }

  if (entity == null) return null;

  if (withRelated.isNotEmpty) {
    await _fetchAndStitchRelations([entity], withRelated, DataSource.local, userId);
  }

  return _applyPostFetchTransforms(entity);
}