mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:07:02 +01:00
First step of moving the desktop app off Flutter's Linux embedder, which tops
out at ~20fps at 4K (see docs/desktop-performance-findings.md). Android is
unaffected and keeps every existing code path.
The blocker was that `dart:io` was imported by note_repository, capture_screen
and settings_screen. On web it compiles to a stub and then throws at runtime --
the first web build rendered a blank white window. So each `dart:io` use now
sits behind a conditional export, with the shared logic hoisted into pure Dart:
- note_repository loses `dart:io`/sqlite entirely. Platform wiring moves to
repository_factory{,_io,_web}, and the legacy sqlite import moves to its own
io-only library. The HLC-from-updated_at seeding is preserved verbatim, so
independently-migrating devices still resolve pre-cutover edits by real edit
time.
- Notes persist to IndexedDB on web, not localStorage: the log is the primary
copy of the backlog and localStorage caps at ~5-10MB and is evicted eagerly.
- BACKLOG.md and the explicit Export keep working through a local wrapper
(added next). A browser cannot write `~/todo/BACKLOG.md`, and the user's
tooling plus the todo MCP server read that path, so leaving it to rot was not
an option. Writes are best-effort so the app still works with no wrapper.
- Notes are also mirrored to the wrapper's on-disk copy, so a wiped Chrome
profile is not a total-loss event for notes that have not reached GitHub yet.
`openRepository` is split so `openRepositoryIn` keeps the migration tests; the
suite stays at 174 green and 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.0 KiB
Dart
91 lines
3.0 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
// Only SqliteCrdt/CrdtTableExecutor are needed to read the legacy DB; hide its
|
|
// Hlc so the crdt_sync Hlc (the store's clock type) is unambiguous.
|
|
import 'package:sqlite_crdt/sqlite_crdt.dart' hide Hlc;
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_repository.dart';
|
|
|
|
/// Reads a pre-crdt_sync `sqlite_crdt` database into a crdt_sync [Log].
|
|
///
|
|
/// Lives in its own `dart:io` library so [NoteRepository] can stay pure Dart:
|
|
/// the desktop app is a web build, and one `dart:io` import anywhere in the
|
|
/// graph fails the whole web compile. Mobile still runs this on first launch
|
|
/// after the cutover; web never does, because there is no legacy DB in a
|
|
/// browser profile.
|
|
class LegacySqliteMigration {
|
|
/// Whether a legacy database exists at [path] to migrate from.
|
|
static bool exists(String path) => File(path).existsSync();
|
|
|
|
/// Opens the legacy DB at [path] and returns its notes as a [Log], plus the
|
|
/// legacy CRDT node id.
|
|
///
|
|
/// Reusing the legacy node id keeps a device that already synced on the same
|
|
/// `devices/<id>` identity across the cutover, so it does not appear as a new
|
|
/// peer and re-upload everything.
|
|
static Future<({Log log, String nodeId})> read(
|
|
String path,
|
|
String preferredNodeId,
|
|
) async {
|
|
final legacy = await SqliteCrdt.open(
|
|
path,
|
|
version: _version,
|
|
onCreate: _onCreate,
|
|
onUpgrade: _onUpgrade,
|
|
);
|
|
final nodeId = preferredNodeId.isEmpty ? legacy.nodeId : preferredNodeId;
|
|
final rows = await legacy.query(
|
|
'SELECT id, text, priority, status, created_at, updated_at, is_deleted '
|
|
'FROM notes',
|
|
);
|
|
final log = <String, Record>{};
|
|
for (final row in rows) {
|
|
final note = Note.fromRow(row);
|
|
log[note.id] = NoteRepository.legacyRecordFor(
|
|
note,
|
|
nodeId,
|
|
deleted: (row['is_deleted'] as int? ?? 0) != 0,
|
|
);
|
|
}
|
|
await legacy.close();
|
|
return (log: log, nodeId: nodeId);
|
|
}
|
|
|
|
static const int _version = 3;
|
|
|
|
// coverage:ignore-start
|
|
// Unreachable: migration only ever opens a legacy DB that already exists
|
|
// (guarded by [exists]), so sqlite never runs onCreate. Kept because
|
|
// SqliteCrdt.open requires the callback.
|
|
static Future<void> _onCreate(CrdtTableExecutor db, int version) async {
|
|
await db.execute('''
|
|
CREATE TABLE notes (
|
|
id TEXT NOT NULL,
|
|
text TEXT NOT NULL DEFAULT '',
|
|
priority INTEGER NOT NULL DEFAULT 2,
|
|
status INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
''');
|
|
}
|
|
// coverage:ignore-end
|
|
|
|
static Future<void> _onUpgrade(
|
|
CrdtTableExecutor db,
|
|
int from,
|
|
int to,
|
|
) async {
|
|
if (from < 2) {
|
|
await db.execute(
|
|
'ALTER TABLE notes ADD COLUMN status INTEGER NOT NULL DEFAULT 0',
|
|
);
|
|
}
|
|
if (from < 3) {
|
|
await db.execute('UPDATE notes SET priority = 2 WHERE priority = 0');
|
|
}
|
|
}
|
|
}
|