wake-alarm/phone_app/lib/services/sick_day_keep.dart
Krzysztof kuhy Rudnicki 7fe6108df3 phone: sick-day keep set (sickdays 0.2.0) in the Dart pool; alarm cache filtered
SickDayPool gains keep: mark/clear write state+actor+keep on one tick,
setKept re-stamps only keep+actor, read rules 1-5 in sick_day_keep.dart.
The native SickDays cache now holds only days on which the alarm stands
down, so a sick day that keeps the alarm rings like a normal day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
2026-10-16 15:30:11 +02:00

83 lines
3.0 KiB
Dart

import 'dart:developer' as developer;
import 'package:crdt_sync/crdt_sync.dart';
/// The per-day keep set (sickdays 0.2.0): which lockers stay active on a
/// sick day. A Dart port of `sickdays/_keep.py`; the contract is the
/// README's "keep (0.2.0)" section.
///
/// `keep` is one more last-writer-wins field on the day's record, holding a
/// **sorted, de-duplicated** list of locker ids. Sorting is wire format, not
/// tidiness: the RTDB rev is a SHA-256 of the exact serialised string.
///
/// Reading is where the safety lives ([readKeep]): a missing `keep` (every
/// 0.1.0 record) or one stamped older than `state` keeps nothing; a value
/// that is not a list of strings keeps *everything* (the locks apply); an
/// unknown id inside a valid list is ignored, for forward compatibility.
abstract final class SickLocker {
/// screen-locker's workout lock.
static const workout = 'workout';
/// leetcode-guard.
static const leetcode = 'leetcode';
/// The book locker.
static const book = 'book';
/// diet_guard.
static const diet = 'diet';
/// This app: the phone ring, the PC fallback ring, the RTC self-wake, the
/// missed-alarm penalty / stick and the morning routine.
static const alarm = 'alarm';
/// Every known id, in `sickdays.LOCKERS` order.
static const all = <String>[workout, leetcode, book, diet, alarm];
/// Returns [locker] if it is a known id; throws [ArgumentError] otherwise
/// (a programmer error, never a data problem).
static String validate(String locker) {
if (!all.contains(locker)) {
throw ArgumentError.value(
locker,
'locker',
'unknown locker; expected one of ${all.join(', ')}',
);
}
return locker;
}
}
/// The record field holding the keep set.
const keepField = 'keep';
/// The on-the-wire value of [keep]: validated, de-duplicated and sorted, so
/// two devices holding the same set write the same bytes.
List<String> keepWire(Iterable<String> keep) =>
keep.map(SickLocker.validate).toSet().toList()..sort();
/// Which known lockers [record]'s day keeps active.
///
/// Only meaningful while the day is sick; callers check that first. The
/// rules run in the README's order: missing -> keep nothing; older than
/// `state` -> stale, keep nothing (a stale malformed value is just stale);
/// not a list of strings -> keep every locker, logged; else the list minus
/// ids this version does not know.
Set<String> readKeep(Record record) {
final field = record.fields[keepField];
if (field == null) return <String>{};
final (value, hlc) = field;
final state = record.fields['state'];
if (state != null && hlc < state.$2) return <String>{};
if (value is! List || value.any((v) => v is! String)) {
developer.log(
'sick day ${record.id} has a malformed keep field $value -- every '
'locker applies',
name: 'SickDayPool',
level: 900,
);
return SickLocker.all.toSet();
}
return value.cast<String>().where(SickLocker.all.contains).toSet();
}