// The async surface over the raw ABI: every operation runs on a short-lived // isolate via Isolate.run, because the Rust side blocks — a fetch can hold a // thread for seconds, which must never be the UI thread. Handles are plain // addresses (ints) and the master is bytes, so both cross isolates freely, // and fumi-core's `Send + Sync` store is safe to call from any of them. // // Error codes cross in two ranges: 0-10 are wire statuses, 64 up are local // failures (see ffi.dart) — recovery paths branch on the range. After a // smolPanic the store's lock is poisoned and every later call panics; the // UI treats that as "close and reopen the store", not an unsolvable error. import "dart:isolate"; import "dart:typed_data"; import "package:smol_mail/native/ffi.dart"; class FumiNative { FumiNative(this.dbPath); /// The SQLite file the store owns; one store per process. final String dbPath; int? _store; int? _account; int? _flag; /// The 32-byte master, held by the app (Hive in kirakira), never by the /// library. Uint8List? _master; int? get store => _store; /// The account handle, when a master is set; readers need it for describe. int? get account => _account; /// Binds the account locally, as register and restore do at their end. void setAccount(String address) => SmolFfi.open().setAccount(_store!, address); Future _io(T Function(SmolFfi ffi) op) => Isolate.run(() { final ffi = SmolFfi.open(); return op(ffi); }); Future open() async { if (_store != null) return; _store = await _io((ffi) => ffi.openStore(dbPath)); _flag = await _io((ffi) => ffi.newFlag()); } /// Drops the identity from memory the honest way Dart allows: the /// master's bytes are overwritten before the reference goes, and the /// account handle is freed. What persists is the caller's business /// (the store wipes its own copy). void clearMaster() { _master?.fillRange(0, _master!.length, 0); _master = null; final old = _account; if (old != null) { SmolFfi.open().freeAccount(old); } _account = null; } /// Sets the master and rebuilds the account handle synchronously: the /// rebuild is a few key derivations (microseconds native), not a network /// operation, so it never needs an isolate — and widget tests can settle /// it inside their fake-async zones. void setMaster(Uint8List master, {int? rotations}) { // Own copy: the caller keeps its buffer (the onboarding screen zeroes // its reference on dispose, and wipe zeroes Hive's), and the account // rebuilds after register/restore/rotate must derive from untouched // bytes — a shared buffer zeroized elsewhere would rebuild an account // that matches none of our keys. _master = Uint8List.fromList(master); _rebuildAccount(rotations: rotations); } /// Rebuilds the account handle from the master and the store's rotation /// index — required after register, restore and rotate, which all change /// the index the account stands at. void _rebuildAccount({int? rotations}) { final ffi = SmolFfi.open(); final index = rotations ?? ffi.rotations(_store!); final old = _account; if (old != null) { ffi.freeAccount(old); } _account = ffi.newAccount(_master!, index); } int get _s => _store!; int get _a => _account!; Future close() async { final store = _store; final account = _account; final flag = _flag; _store = null; _account = null; _flag = null; if (account != null) await _io((ffi) => ffi.freeAccount(account)); if (flag != null) await _io((ffi) => ffi.freeFlag(flag)); if (store != null) await _io((ffi) => ffi.closeStore(store)); } // --- identity and pins ------------------------------------------------------ Future accountPk() => _io((ffi) => ffi.accountPk(_a)); Future accountAddress() => _io((ffi) => ffi.accountAddress(_s)); Future rotations() => _io((ffi) => ffi.rotations(_s)); Future pinServer(String host, String keyB32, int port) => _io((ffi) => ffi.pinServer(_s, host, keyB32, port)); Future serverPin(String host, int port) => _io((ffi) => ffi.serverPin(_s, host, port)); Future unpinServer(String host, int port) => _io((ffi) => ffi.unpinServer(_s, host, port)); Future> pins() => _io((ffi) => ffi.pins(_s)); Future syncOk() => _io((ffi) => ffi.syncOk(_s)); Future saveContact(String address, String keyB32, {required bool verified}) => _io((ffi) => ffi.saveContact(_s, address, keyB32, verified: verified)); // --- account lifecycle ------------------------------------------------------ Future register(String address, {String? invite, String? dial, int timeout = 30}) => _io((ffi) { ffi.register(_s, _a, address, invite: invite, dial: dial, timeout: timeout); }).then((_) => _rebuildAccount()); Future restore(String address, {String? dial, int timeout = 30}) => _io((ffi) => ffi.restore(_s, _master!, address, dial: dial, timeout: timeout)) .then((_) => _rebuildAccount()); Future> rotate({String? dial, int timeout = 30}) => _io((ffi) => ffi.rotate(_s, _a, dial: dial, timeout: timeout)) .then((out) { _rebuildAccount(); return out; }); // --- mail ------------------------------------------------------------------- /// Raises the cancellation flag; the running fetch stops between /// envelopes and returns a partial summary. void cancelFetch() => SmolFfi.open().setFlag(_flag ?? 0, true); Future> fetch( {bool keep = false, bool reset = false, String? dial, int timeout = 30}) => _io((ffi) { ffi.setFlag(_flag!, false); return ffi.fetch(_s, _a, keep: keep, reset: reset, dial: dial, timeout: timeout, cancel: _flag); }); Future> send(String to, String body, {String? subject, String? replyTo, String? dial, int timeout = 30}) => _io((ffi) => ffi.send(_s, _a, to, body, subject: subject, replyTo: replyTo, dial: dial)); Future delete(List idsHex, {String? dial, int timeout = 30}) => _io((ffi) => ffi.delete(_s, _a, idsHex, dial: dial, timeout: timeout)); /// The reader's delete: local removal plus seen-marking, no server round /// trip. Use [delete] for the server-side DELETE. Future deleteLocal(String folder, List idsHex) => _io((ffi) => ffi.deleteLocal(_s, folder, idsHex)); Future> mail(String folder) => _io((ffi) => ffi.mail(_s, folder)); Future> describe(String idHex) => _io((ffi) => ffi.describe(_s, _a, idHex)); // --- contacts --------------------------------------------------------------- Future> contacts() => _io((ffi) => ffi.contacts(_s)); Future> contact(String address) => _io((ffi) => ffi.contact(_s, address)); Future> resolve(String address, {String? dial, int timeout = 30}) => _io((ffi) => ffi.resolve(_s, address, dial: dial, timeout: timeout)); Future importContact(String uri) => _io((ffi) => ffi.importContact(_s, uri)); Future accept(String address, {String? dial, int timeout = 30}) => _io((ffi) => ffi.accept(_s, _a, address, dial: dial, timeout: timeout)); Future block(String address, {String? dial, int timeout = 30}) => _io((ffi) => ffi.block(_s, _a, address, dial: dial, timeout: timeout)); // --- backups ---------------------------------------------------------------- Future exportBackup() => _io((ffi) => ffi.exportBackup(_s, _master!)); Future importBackup(String text) => _io((ffi) => ffi.importBackup(_s, _master!, text)); }