import '../../core/exceptions.dart'; import '../api/notes_api.dart'; import '../local/database.dart'; import '../models/note.dart'; /// Notes repository with read-through caching for Tier 2 offline support /// (Phase 1+2) and offline-write queueing (Phase 3). /// /// Reads attempt the network first; on success the response is written to /// the local Drift cache. On `NetworkException` the read falls back to the /// cache (rethrowing if the cache is empty so the UI can show its /// fresh-install empty state). /// /// Writes hit the server then sync the cache. On `NetworkException` the /// write is queued in `pending_writes` and applied optimistically to the /// cache (negative temp-id for creates, in-place update for edits, removal /// for deletes). Subsequent edits to a temp-id row are coalesced into the /// queued create — only one server call is made per offline-created row. /// /// `AuthStatus.offline` is owned by `AuthNotifier.verify()` (a periodic /// heartbeat). This repository deliberately does not poke that state. class NotesRepository { final NotesApi _api; final FabledDatabase _db; const NotesRepository(this._api, this._db); Future> getAll() async { try { final notes = await _api.getAll(); await _db.replaceAllNotes(notes); return notes; } on NetworkException { final cached = await _db.getAllNotes(); if (cached.isEmpty) rethrow; return cached; } } Future getOne(int id) async { try { final note = await _api.getOne(id); await _db.upsertNote(note); return note; } on NetworkException { final cached = await _db.getNote(id); if (cached == null) rethrow; return cached; } } Future create( String title, String body, { List tags = const [], int? projectId, String noteType = 'note', }) async { try { final note = await _api.create( title, body, tags: tags, projectId: projectId, noteType: noteType, ); await _db.upsertNote(note); return note; } on NetworkException { final tempId = await _db.nextTempId(); final now = DateTime.now(); final optimistic = Note( id: tempId, title: title, body: body, tags: tags, noteType: noteType, projectId: projectId, createdAt: now, updatedAt: now, ); await _db.upsertNote(optimistic); await _db.enqueuePending( domain: kSyncDomainNotes, verb: kWriteVerbCreate, tempId: tempId, payload: { 'title': title, 'body': body, 'tags': tags, 'project_id': projectId, 'note_type': noteType, }, ); return optimistic; } } Future update( int id, String title, String body, { List tags = const [], int? projectId, bool clearProject = false, String noteType = 'note', }) async { try { final updated = await _api.update( id, title, body, tags: tags, projectId: projectId, clearProject: clearProject, noteType: noteType, ); await _db.upsertNote(updated); return updated; } on NetworkException { final cached = await _db.getNote(id); if (cached == null) rethrow; final payload = { 'title': title, 'body': body, 'tags': tags, 'project_id': projectId, 'clear_project': clearProject, 'note_type': noteType, }; final optimistic = cached.copyWith( title: title, body: body, tags: tags, noteType: noteType, projectId: clearProject ? null : projectId, ); await _db.upsertNote(optimistic); // Edits to an offline-created row coalesce into the queued create. if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainNotes, id); if (queued != null) { await _db.updatePendingPayload(queued.id, payload); return optimistic; } } await _db.enqueuePending( domain: kSyncDomainNotes, verb: kWriteVerbUpdate, targetId: id, payload: payload, baselineUpdatedAt: cached.updatedAt, ); return optimistic; } } Future delete(int id) async { try { await _api.delete(id); await _db.deleteNote(id); } on NetworkException { // Deleting an offline-created row that never reached the server // just drops the queued create — no server call needed. if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainNotes, id); if (queued != null) await _db.deletePending(queued.id); await _db.deleteNote(id); return; } await _db.deleteNote(id); await _db.enqueuePending( domain: kSyncDomainNotes, verb: kWriteVerbDelete, targetId: id, ); } } }