search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Cache

Magic provides an expressive, unified API for various caching backends, with built-in event dispatching so you can react to cache reads, writes, and removals.

Introduction

Magic provides an expressive, unified API for various caching backends. The framework ships with a native FileStore driver that supports both Mobile/Desktop (File System) and Web (LocalStorage).

Caching is essential for reducing API calls, storing computed values, and improving application performance.

Configuration

Enabling Cache Support

Add CacheServiceProvider to your providers in config/app.dart:

'providers': [
  (app) => CacheServiceProvider(app),
  // ... other providers
],

Cache Configuration

Create lib/config/cache.dart:

import 'package:magic/magic.dart';

Map get cacheConfig => {
  'cache': {
    'driver': FileStore(fileName: 'magic_cache'),
    'ttl': 3600, // Default TTL in seconds
  },
};

Retrieving Items

Use the Cache facade to retrieve items from the cache:

// Basic retrieval
final value = await Cache.get('key');

// With default value
final username = await Cache.get('username', defaultValue: 'Guest');

// Check existence
if (await Cache.has('user_settings')) {
  final settings = await Cache.get('user_settings');
}

Storing Items

Store items with optional TTL (time-to-live):

// With custom TTL
await Cache.put('key', 'value', ttl: Duration(minutes: 10));

// With default TTL from config
await Cache.put('key', 'value');

// Store any serializable data
await Cache.put('user', {
  'id': 1,
  'name': 'John',
  'email': '[email protected]',
});


The Remember Method

The most powerful caching pattern—retrieve from cache or compute and store:

final users = await Cache.remember(
  'users', 
  Duration(minutes: 5), 
  () async {
    return await Http.get('/users');
  },
);

This method:

  1. Checks if users exists in cache
  2. If yes, returns the cached value
  3. If no, calls the closure, stores the result, and returns it

API Response Caching

class UserController extends MagicController {
  Future> getUsers() async {
    return await Cache.remember('all_users', Duration(minutes: 10), () async {
      final response = await Http.get('/users');
      return (response.body as List).map((u) => User.fromMap(u)).toList();
    });
  }
}

Computed Values

final dashboardStats = await Cache.remember('dashboard_stats', Duration(hours: 1), () async {
  return {
    'total_users': await User.count(),
    'active_monitors': await Monitor.where('is_active', true).count(),
    'incidents_today': await Incident.whereDate('created_at', Carbon.today()).count(),
  };
});

Removing Items

// Remove a single item
await Cache.forget('users');

// Clear entire cache
await Cache.flush();
class UserController extends MagicController {
  Future updateUser(Map data) async {
    final response = await Http.put('/users/${data['id']}', data: data);
    
    if (response.successful) {
      // Invalidate related caches
      await Cache.forget('all_users');
      await Cache.forget('user_${data['id']}');
      await Cache.forget('dashboard_stats');
    }
  }
}

Custom Cache Drivers

Implement CacheStore for custom backends:

class RedisStore implements CacheStore {
  final RedisClient _client;
  
  RedisStore(this._client);

  @override
  Future init() async {
    await _client.connect();
  }

  @override
  Future get(String key, {dynamic defaultValue}) async {
    final value = await _client.get(key);
    if (value == null) return defaultValue;
    return jsonDecode(value);
  }

  @override
  Future put(String key, dynamic value, {Duration? ttl}) async {
    final encoded = jsonEncode(value);
    if (ttl != null) {
      await _client.setex(key, ttl.inSeconds, encoded);
    } else {
      await _client.set(key, encoded);
    }
  }

  @override
  Future has(String key) async {
    return await _client.exists(key) > 0;
  }

  @override
  Future forget(String key) async {
    await _client.del(key);
  }

  @override
  Future flush() async {
    await _client.flushdb();
  }
}

Use your custom driver in config:

'cache': {
  'driver': RedisStore(RedisClient()),
},

[!TIP] Use caching for expensive API calls, complex computations, and data that doesn't change frequently.

Cache Events and Listeners

Magic dispatches events on every cache operation. You can listen to these events via your AppEventServiceProvider to add logging, metrics, or invalidation side-effects without coupling that logic to call sites.

Event Classes

All cache events extend MagicEvent and live in package:magic/magic.dart:

Event Payload Fired when
CacheHit key, value A Cache.get() returns a stored value.
CacheMiss key A Cache.get() finds no stored value.
CachePut key, value, ttl? A value is written via Cache.put() or Cache.remember().
CacheForget key A single key is removed via Cache.forget().
CacheFlush (none) The entire cache is cleared via Cache.flush().

Listening to Cache Events

Register listeners in your AppEventServiceProvider:

import 'package:magic/magic.dart';

class LogCacheHit extends MagicListener {
  @override
  Future handle(CacheHit event) async {
    Log.debug('Cache hit: ${event.key}');
  }
}

class LogCacheMiss extends MagicListener {
  @override
  Future handle(CacheMiss event) async {
    Log.debug('Cache miss: ${event.key}');
  }
}

Then wire them in your event service provider:

import 'package:magic/magic.dart';

class AppEventServiceProvider extends EventServiceProvider {
  AppEventServiceProvider(super.app);

  @override
  Map> get listen => {
    CacheHit: [() => LogCacheHit()],
    CacheMiss: [() => LogCacheMiss()],
    CachePut: [() => RecordCacheWrite()],
    CacheForget: [() => RecordCacheRemoval()],
    CacheFlush: [() => RecordCacheFlush()],
  };
}

Telescope Integration

When magic_devtools is installed, the MagicCacheWatcher registers listeners for all five cache events automatically and streams them into Telescope's ring buffer, where they appear as cache entries readable by AI agents and the telescope:tail command. No configuration is required on your side; Telescope observes passively without a write path.

See Magic DevTools for installation and setup.

Testing

Use Cache.fake() to replace the real cache manager with an in-memory FakeCacheManager during tests. The fake supports assertions and records every operation.

import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';
import 'package:magic/testing.dart';

void main() {
  setUp(() {
    MagicApp.reset();
    Magic.flush();
  });

  test('caches users after first fetch', () async {
    final fake = Cache.fake();

    await Cache.put('users', ['alice', 'bob']);

    fake.assertHas('users');
    fake.assertPut('users');
  });

  test('cleans up after test', () async {
    final fake = Cache.fake();
    fake.reset(); // clears store and recorded operations
    Cache.unfake();
  });
}

FakeCacheManager exposes:

  • assertHas(key): asserts the key currently exists in the store.
  • assertMissing(key): asserts the key is absent.
  • assertPut(key): asserts the key was stored at least once via put.
  • recorded: the full chronological list of cache operations for manual inspection.
  • reset(): clears the store and the recorded list between assertions.