getAll<T> method

Future<Map<String, T>> getAll<T>(
  1. List<String> keys, {
  2. Map<String, Future<T> Function()>? refreshCallbacks,
  3. CachePolicy? policy,
})

Retrieves multiple values associated with the given keys.

Returns a map where the keys are the original keys and the values are the retrieved values. If a key is not found in the cache or its value is expired, it will not be included in the returned map.

If refreshCallbacks is provided and an item is stale according to its policy, the callback for that key will be used to refresh the data based on the refresh strategy.

Throws a CacheException if there is an error retrieving the data.

Implementation

Future<Map<String, T>> getAll<T>(
  List<String> keys, {
  Map<String, Future<T> Function()>? refreshCallbacks,
  CachePolicy? policy,
}) async {
  try {
    if (keys.isEmpty) {
      return {};
    }

    // Check for empty keys
    for (final key in keys) {
      if (key.isEmpty) {
        throw ArgumentError('Keys cannot be empty');
      }
    }

    final result = <String, T>{};
    final cacheItems = await _cacheAdapter.getAll(keys);
    final keysToDelete = <String>[];
    final effectivePolicy = policy ?? CachePolicy.defaultPolicy;

    // For large datasets without refresh callbacks, process decompression in isolate
    if (keys.length > 50 &&
        (refreshCallbacks == null || refreshCallbacks.isEmpty) &&
        cacheItems.isNotEmpty) {
      _log.fine(
          'Processing ${cacheItems.length} items in isolate for getAll');

      // Create a list of compressed items that need decompression
      final compressedItems = <String, String>{};
      for (final entry in cacheItems.entries) {
        if (entry.value.isCompressed && entry.value.value is String) {
          compressedItems[entry.key] = entry.value.value as String;
        }
      }

      // If there are compressed items, decompress them in an isolate
      if (compressedItems.isNotEmpty) {
        final decompressedItems =
            await IsolateRunner.run<Map<String, String>, Map<String, String>>(
          function: (items) {
            final compression = Compression();
            final results = <String, String>{};

            for (final entry in items.entries) {
              try {
                results[entry.key] =
                    compression.decompressString(entry.value);
              } catch (e) {
                // If decompression fails, keep the original value
                results[entry.key] = entry.value;
              }
            }

            return results;
          },
          message: compressedItems,
        );

        // Update the cache items with decompressed values
        for (final entry in decompressedItems.entries) {
          final cacheItem = cacheItems[entry.key]!;
          result[entry.key] = entry.value as T;

          // Update sliding expiry if needed
          if (cacheItem.slidingExpiry != null) {
            final updatedCacheItem = cacheItem.updateExpiry();
            await _cacheAdapter.put(entry.key, updatedCacheItem);
          } else {
            // Update access metadata
            final updatedCacheItem = cacheItem.updateExpiry();
            if (updatedCacheItem != cacheItem) {
              await _cacheAdapter.put(entry.key, updatedCacheItem);
            }
          }

          // Record cache hit in analytics
          _analytics.recordHit(entry.key);
        }

        // Process non-compressed items
        for (final entry in cacheItems.entries) {
          if (!entry.value.isCompressed && !result.containsKey(entry.key)) {
            if (entry.value.isExpired) {
              // Record cache miss in analytics
              _analytics.recordMiss(entry.key);
              keysToDelete.add(entry.key);
              continue;
            }

            result[entry.key] = entry.value.value as T;

            // Update sliding expiry if needed
            if (entry.value.slidingExpiry != null) {
              final updatedCacheItem = entry.value.updateExpiry();
              await _cacheAdapter.put(entry.key, updatedCacheItem);
            } else {
              // Update access metadata
              final updatedCacheItem = entry.value.updateExpiry();
              if (updatedCacheItem != entry.value) {
                await _cacheAdapter.put(entry.key, updatedCacheItem);
              }
            }

            // Record cache hit in analytics
            _analytics.recordHit(entry.key);
          }
        }

        // Delete expired items in batch
        if (keysToDelete.isNotEmpty) {
          await _cacheAdapter.deleteAll(keysToDelete);
        }

        return result;
      }
    }

    // Process cache hits normally for smaller datasets or when refresh callbacks are provided
    for (final entry in cacheItems.entries) {
      final key = entry.key;
      final cacheItem = entry.value;

      if (cacheItem.isExpired) {
        // Record cache miss in analytics
        _analytics.recordMiss(key);
        keysToDelete.add(key);

        // If a refresh callback is provided for this key, use it to get fresh data
        if (refreshCallbacks != null && refreshCallbacks.containsKey(key)) {
          final freshValue = await refreshCallbacks[key]!();
          await put(key, freshValue, policy: policy);
          result[key] = freshValue;
        }

        continue;
      }

      // Check if the item is stale and needs refreshing
      if (refreshCallbacks != null &&
          refreshCallbacks.containsKey(key) &&
          effectivePolicy.staleTime != null &&
          cacheItem.isStale(effectivePolicy.staleTime!)) {
        // Handle different refresh strategies
        switch (effectivePolicy.refreshStrategy) {
          case RefreshStrategy.backgroundRefresh:
            // Update in the background without blocking
            _refreshInBackground(
                key, refreshCallbacks[key]!, effectivePolicy);
            break;
          case RefreshStrategy.immediateRefresh:
            // Refresh immediately and return the fresh value
            final freshValue = await refreshCallbacks[key]!();
            await put(key, freshValue, policy: effectivePolicy);
            result[key] = freshValue;
            continue; // Skip the rest of the loop for this item
          case RefreshStrategy.never:
            // Do nothing, just use the cached value
            break;
        }
      }

      // Decompress the value if it's compressed
      T? resultValue;
      if (cacheItem.isCompressed &&
          _compression != null &&
          cacheItem.value is String) {
        try {
          final decompressedValue =
              _compression.decompressString(cacheItem.value as String);
          resultValue = decompressedValue as T?;
          _log.fine('Decompressed value for key $key');
        } catch (e) {
          _log.warning('Failed to decompress value for key $key: $e');
          resultValue = cacheItem.value as T?;
        }
      } else {
        resultValue = cacheItem.value as T?;
      }

      // Update sliding expiry if needed
      if (cacheItem.slidingExpiry != null) {
        final updatedCacheItem = cacheItem.updateExpiry();
        await _cacheAdapter.put(key, updatedCacheItem);
        // Record cache hit in analytics
        _analytics.recordHit(key);
        result[key] = resultValue as T;
      } else {
        // Update access metadata
        final updatedCacheItem = cacheItem.updateExpiry();
        if (updatedCacheItem != cacheItem) {
          await _cacheAdapter.put(key, updatedCacheItem);
        }

        // Record cache hit in analytics
        _analytics.recordHit(key);
        result[key] = resultValue as T;
      }
    }

    // Process cache misses with refresh callbacks
    if (refreshCallbacks != null) {
      final missingKeys = keys.where((key) =>
          !result.containsKey(key) &&
          !keysToDelete.contains(key) &&
          refreshCallbacks.containsKey(key));

      for (final key in missingKeys) {
        // Record cache miss in analytics
        _analytics.recordMiss(key);
        final freshValue = await refreshCallbacks[key]!();
        await put(key, freshValue, policy: policy);
        result[key] = freshValue;
      }
    }

    // Delete expired items in batch
    if (keysToDelete.isNotEmpty) {
      await _cacheAdapter.deleteAll(keysToDelete);
    }

    return result;
  } on HiveError catch (e) {
    _log.severe('Failed to get data from cache (HiveError): $e');
    throw CacheException('Failed to get data from cache: ${e.message}');
  } catch (e) {
    _log.severe('Failed to get data from cache (Unknown Error): $e');
    throw CacheException('Failed to get data from cache: $e');
  }
}