LogoSkills

Client Cache Conventions

Client cache architecture rules for this project.

Client cache architecture rules for this project. Refer to the /client-cache skill for detailed implementation guide.


3 Cache Strategies#

StrategyUseCase Return TypeEntry PointSelection Criteria
SWR Stream<Either<Failure, T>> watchStream() GET lists, real-time, externally mutable
CacheFirst Future<Either<Failure, T>> execute() GET single, offline first
NetworkFirst Future<Either<Failure, T>> execute() Payment/auth, always need latest

Automatic Strategy Selection Rules#

GET ๋ชฉ๋ก  โ†’ SWR          โ†’ Stream < Either < Failure, List < T > > > 
 GET ๋‹จ์ผ  โ†’ CacheFirst or NetworkFirst  โ†’ Future < Either < Failure, T > > 
 POST/PUT/DELETE โ†’ ์ง์ ‘ ํ˜ธ์ถœ (์บ์‹œ ์—†์Œ) + ๊ด€๋ จ ์บ์‹œ ๋ฌดํšจํ™”

BLoC Transformer Rules#

Event TypetransformerReason
SWR Stream subscription restartable() Cancel previous Stream on parameter change
All events using emit.forEach restartable() Prevent duplicate subscriptions
Write operations (POST/PUT/DELETE) droppable() Prevent duplicate submissions
Independent parallel requests concurrent() (default, can omit) Complete independently
// โœ… CORRECT: Apply restartable to SWR events
on<ItemsLoad>(_onLoad, transformer: restartable());

// โœ… CORRECT: Apply droppable to write events
on<ItemCreate>(_onCreate, transformer: droppable());

emit.forEach Rules#

When integrating with SWR UseCase (call() returns Stream), always use emit.forEach. Manual StreamSubscription management prohibited. onError callback required.

// โœ… CORRECT: emit.forEach + onError
Future<void> _onLoad(ItemsLoad event, Emitter<ItemsState> emit) async {
  await emit.forEach(
    const GetItemsUseCase().call(GetItemsParams(...)),
    onData: (result) => result.fold(
      (failure) => state.copyWith(status: LoadingStatus.error, failure: failure),
      (items) => state.copyWith(status: LoadingStatus.loaded, items: items),
    ),
    onError: (_, __) => state.copyWith(status: LoadingStatus.error),
  );
}

// โŒ WRONG: Manual StreamSubscription management prohibited
StreamSubscription? _subscription;

// โŒ WRONG: .last loses SWR benefits (ignores cache)
final result = await useCase.call(params).last;

// โŒ WRONG: .first ignores server refresh (receives cache only)
final result = await useCase.call(params).first;

Anti-pattern details: See swr-pattern.md ยง8


cacheKey Format#

' {domain}__{usecase}__key:value '
  • Separator: double underscore (__)
  • Pagination parameters (page, size, limit, offset) not included
  • Filter parameters are nullable, included only when != null
  • Date: Use CacheQuery.fmtDate() (yyyy-MM-dd)

Example:

'schedule__get_schedule_data__start:2026-03-01__end:2026-03-31__class:5566'
'homework__get_homework_list__classId:42'

TTL (Cache Expiration Time)#

PolicyTTLSuitable For
CachePolicies.realtime5 minNotifications, attendance
CachePolicies.standard10 minHomework, schedule (default)
CachePolicies.stable1 hourSettings, products
CachePolicies.immutable24 hoursNotices, terms

Drift Table Rules#

Requirements#

  • Row tables only (JSON blob prohibited)
  • Do not set primaryKey (use rowid)
  • cacheKey + cachedAt columns required
  • Column names must match domain entity field names
  • For nested structures, include parent entity fields as columns in each row
// โœ… CORRECT: ํ–‰ ํ…Œ์ด๋ธ”, primaryKey ์—†์Œ
class HomeworkItems extends Table {
  TextColumn get cacheKey => text()();
  TextColumn get homeworkId => text()();
  TextColumn get title => text()();
  DateTimeColumn get cachedAt => dateTime()();
  // No primaryKey (use rowid)
}

// โŒ WRONG: JSON blob
class HomeworkDetailItems extends Table {
  TextColumn get cacheKey => text()();
  TextColumn get json => text()();  // JSON ๊ธˆ์ง€
  @override
  Set<Column> get primaryKey => {cacheKey};  // primaryKey ์„ค์ • ๊ธˆ์ง€
}

upsert prohibited -> DELETE + INSERT#

// โœ… CORRECT: DELETE then INSERT
Future<void> saveItems(List<XxxCompanion> companions, String cacheKey) async {
  await (delete(table)..where((t) => t.cacheKey.equals(cacheKey))).go();
  await batch((b) => b.insertAll(table, companions));
}

// โŒ WRONG: upsert prohibited
await into(table).insertOnConflictUpdate(companion);

DB Migration: deleteTable + createTable#

onUpgrade: (Migrator migrator, int from, int to) async {
  for (final table in allTables) {
    await migrator.deleteTable(table.actualTableName);
    await migrator.createTable(table);
  }
},

DAO Rules#

  • Handle Drift CRUD only (no domain entity dependency)
  • Accept only Companion for INSERT
  • Entity <-> Companion conversion is CacheRepository's responsibility

CacheRepository Conversion Methods#

// โœ… CORRECT
static XxxEntity _toEntity(XxxItemData data) { ... }
static XxxCompanion _toCompanion(XxxEntity entity, String cacheKey) { ... }

// โŒ WRONG
static XxxEntity _dataToXxx(XxxItemData data) { ... }

UseCase Rules#

  • All UseCases use Params class (when parameters exist)
  • SWR UseCase: Does not implement UseCase interface, _repository getter, returns Stream
  • Future UseCase: implements UseCase, repo getter, try-catch + Log.e
  • Unified OpenApiService (OpenApiClient prohibited)

Cache Invalidation After Write#

Delete related caches by prefix after POST/PUT/DELETE completion.

await dao.deleteByCacheKeyPrefix('schedule__get_memo');
await dao.deleteByCacheKeyPrefix('schedule__get_memos');