forked from eva/focus-flow
300 lines
9 KiB
Dart
300 lines
9 KiB
Dart
// Persistence boundary interfaces for the scheduling core.
|
|
//
|
|
// These interfaces are intentionally database-client-free. A future MongoDB
|
|
// adapter can implement them with document storage while the domain layer and
|
|
// tests keep using the same pure Dart contracts.
|
|
|
|
import 'locked_time.dart';
|
|
import 'models.dart';
|
|
import 'scheduling_engine.dart';
|
|
|
|
/// Repository contract for task documents.
|
|
abstract interface class TaskRepository {
|
|
/// Return a task by stable id, or null when it does not exist.
|
|
Future<Task?> findById(String id);
|
|
|
|
/// Return all tasks currently known to the repository.
|
|
Future<List<Task>> findAll();
|
|
|
|
/// Return tasks with a lifecycle status.
|
|
Future<List<Task>> findByStatus(TaskStatus status);
|
|
|
|
/// Return tasks that overlap [window] by scheduled start/end.
|
|
Future<List<Task>> findScheduledInWindow(SchedulingWindow window);
|
|
|
|
/// Save or replace [task] by stable id.
|
|
Future<void> save(Task task);
|
|
|
|
/// Save or replace every task by stable id.
|
|
Future<void> saveAll(Iterable<Task> tasks);
|
|
}
|
|
|
|
/// Repository contract for project profile documents.
|
|
abstract interface class ProjectRepository {
|
|
/// Return a project by stable id, or null when it does not exist.
|
|
Future<ProjectProfile?> findById(String id);
|
|
|
|
/// Return all projects currently known to the repository.
|
|
Future<List<ProjectProfile>> findAll();
|
|
|
|
/// Save or replace [project] by stable id.
|
|
Future<void> save(ProjectProfile project);
|
|
}
|
|
|
|
/// Repository contract for locked-block and one-day override documents.
|
|
abstract interface class LockedBlockRepository {
|
|
/// Return a locked block by stable id, or null when it does not exist.
|
|
Future<LockedBlock?> findBlockById(String id);
|
|
|
|
/// Return all locked block definitions.
|
|
Future<List<LockedBlock>> findAllBlocks();
|
|
|
|
/// Return an override by stable id, or null when it does not exist.
|
|
Future<LockedBlockOverride?> findOverrideById(String id);
|
|
|
|
/// Return all one-day overrides.
|
|
Future<List<LockedBlockOverride>> findAllOverrides();
|
|
|
|
/// Save or replace [block] by stable id.
|
|
Future<void> saveBlock(LockedBlock block);
|
|
|
|
/// Save or replace [override] by stable id.
|
|
Future<void> saveOverride(LockedBlockOverride override);
|
|
}
|
|
|
|
/// Snapshot of one scheduling operation or persisted planning state.
|
|
class SchedulingStateSnapshot {
|
|
const SchedulingStateSnapshot({
|
|
required this.id,
|
|
required this.capturedAt,
|
|
required this.window,
|
|
required this.tasks,
|
|
this.lockedIntervals = const <TimeInterval>[],
|
|
this.requiredVisibleIntervals = const <TimeInterval>[],
|
|
this.notices = const <SchedulingNotice>[],
|
|
this.changes = const <SchedulingChange>[],
|
|
this.overlaps = const <SchedulingOverlap>[],
|
|
this.operationName,
|
|
});
|
|
|
|
/// Stable snapshot id.
|
|
final String id;
|
|
|
|
/// Time this snapshot was created.
|
|
final DateTime capturedAt;
|
|
|
|
/// Planning window represented by the snapshot.
|
|
final SchedulingWindow window;
|
|
|
|
/// Task documents visible to the operation.
|
|
final List<Task> tasks;
|
|
|
|
/// Locked intervals supplied to the scheduler.
|
|
final List<TimeInterval> lockedIntervals;
|
|
|
|
/// Extra visible required intervals supplied to the scheduler.
|
|
final List<TimeInterval> requiredVisibleIntervals;
|
|
|
|
/// Human-readable scheduling notices.
|
|
final List<SchedulingNotice> notices;
|
|
|
|
/// Machine-readable task movements.
|
|
final List<SchedulingChange> changes;
|
|
|
|
/// Analysis-only overlap findings.
|
|
final List<SchedulingOverlap> overlaps;
|
|
|
|
/// Optional operation label, such as `push` or `rollover`.
|
|
final String? operationName;
|
|
|
|
/// Rebuild scheduler input from the persisted state shape.
|
|
SchedulingInput get input {
|
|
return SchedulingInput(
|
|
tasks: List<Task>.unmodifiable(tasks),
|
|
window: window,
|
|
lockedIntervals: List<TimeInterval>.unmodifiable(lockedIntervals),
|
|
requiredVisibleIntervals:
|
|
List<TimeInterval>.unmodifiable(requiredVisibleIntervals),
|
|
);
|
|
}
|
|
|
|
/// Rebuild scheduler result details from the persisted state shape.
|
|
SchedulingResult get result {
|
|
return SchedulingResult(
|
|
tasks: List<Task>.unmodifiable(tasks),
|
|
notices: List<SchedulingNotice>.unmodifiable(notices),
|
|
changes: List<SchedulingChange>.unmodifiable(changes),
|
|
overlaps: List<SchedulingOverlap>.unmodifiable(overlaps),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Repository contract for scheduling operation/state snapshot documents.
|
|
abstract interface class SchedulingSnapshotRepository {
|
|
/// Return a snapshot by stable id, or null when it does not exist.
|
|
Future<SchedulingStateSnapshot?> findById(String id);
|
|
|
|
/// Return all snapshots overlapping [window].
|
|
Future<List<SchedulingStateSnapshot>> findInWindow(SchedulingWindow window);
|
|
|
|
/// Save or replace [snapshot] by stable id.
|
|
Future<void> save(SchedulingStateSnapshot snapshot);
|
|
}
|
|
|
|
/// In-memory task repository useful for tests and early application wiring.
|
|
class InMemoryTaskRepository implements TaskRepository {
|
|
InMemoryTaskRepository([Iterable<Task> initialTasks = const []])
|
|
: _tasksById = {
|
|
for (final task in initialTasks) task.id: task,
|
|
};
|
|
|
|
final Map<String, Task> _tasksById;
|
|
|
|
@override
|
|
Future<Task?> findById(String id) async {
|
|
return _tasksById[id];
|
|
}
|
|
|
|
@override
|
|
Future<List<Task>> findAll() async {
|
|
return List<Task>.unmodifiable(_tasksById.values);
|
|
}
|
|
|
|
@override
|
|
Future<List<Task>> findByStatus(TaskStatus status) async {
|
|
return List<Task>.unmodifiable(
|
|
_tasksById.values.where((task) => task.status == status),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
|
|
return List<Task>.unmodifiable(
|
|
_tasksById.values.where((task) {
|
|
final start = task.scheduledStart;
|
|
final end = task.scheduledEnd;
|
|
if (start == null || end == null) {
|
|
return false;
|
|
}
|
|
return TimeInterval(start: start, end: end).overlaps(window.interval);
|
|
}),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(Task task) async {
|
|
_tasksById[task.id] = task;
|
|
}
|
|
|
|
@override
|
|
Future<void> saveAll(Iterable<Task> tasks) async {
|
|
for (final task in tasks) {
|
|
_tasksById[task.id] = task;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// In-memory project repository useful for tests and early application wiring.
|
|
class InMemoryProjectRepository implements ProjectRepository {
|
|
InMemoryProjectRepository(
|
|
[Iterable<ProjectProfile> initialProjects = const []])
|
|
: _projectsById = {
|
|
for (final project in initialProjects) project.id: project,
|
|
};
|
|
|
|
final Map<String, ProjectProfile> _projectsById;
|
|
|
|
@override
|
|
Future<ProjectProfile?> findById(String id) async {
|
|
return _projectsById[id];
|
|
}
|
|
|
|
@override
|
|
Future<List<ProjectProfile>> findAll() async {
|
|
return List<ProjectProfile>.unmodifiable(_projectsById.values);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(ProjectProfile project) async {
|
|
_projectsById[project.id] = project;
|
|
}
|
|
}
|
|
|
|
/// In-memory locked-time repository useful for tests and early wiring.
|
|
class InMemoryLockedBlockRepository implements LockedBlockRepository {
|
|
InMemoryLockedBlockRepository({
|
|
Iterable<LockedBlock> initialBlocks = const [],
|
|
Iterable<LockedBlockOverride> initialOverrides = const [],
|
|
}) : _blocksById = {
|
|
for (final block in initialBlocks) block.id: block,
|
|
},
|
|
_overridesById = {
|
|
for (final override in initialOverrides) override.id: override,
|
|
};
|
|
|
|
final Map<String, LockedBlock> _blocksById;
|
|
final Map<String, LockedBlockOverride> _overridesById;
|
|
|
|
@override
|
|
Future<LockedBlock?> findBlockById(String id) async {
|
|
return _blocksById[id];
|
|
}
|
|
|
|
@override
|
|
Future<List<LockedBlock>> findAllBlocks() async {
|
|
return List<LockedBlock>.unmodifiable(_blocksById.values);
|
|
}
|
|
|
|
@override
|
|
Future<LockedBlockOverride?> findOverrideById(String id) async {
|
|
return _overridesById[id];
|
|
}
|
|
|
|
@override
|
|
Future<List<LockedBlockOverride>> findAllOverrides() async {
|
|
return List<LockedBlockOverride>.unmodifiable(_overridesById.values);
|
|
}
|
|
|
|
@override
|
|
Future<void> saveBlock(LockedBlock block) async {
|
|
_blocksById[block.id] = block;
|
|
}
|
|
|
|
@override
|
|
Future<void> saveOverride(LockedBlockOverride override) async {
|
|
_overridesById[override.id] = override;
|
|
}
|
|
}
|
|
|
|
/// In-memory snapshot repository useful for tests and early application wiring.
|
|
class InMemorySchedulingSnapshotRepository
|
|
implements SchedulingSnapshotRepository {
|
|
InMemorySchedulingSnapshotRepository([
|
|
Iterable<SchedulingStateSnapshot> initialSnapshots = const [],
|
|
]) : _snapshotsById = {
|
|
for (final snapshot in initialSnapshots) snapshot.id: snapshot,
|
|
};
|
|
|
|
final Map<String, SchedulingStateSnapshot> _snapshotsById;
|
|
|
|
@override
|
|
Future<SchedulingStateSnapshot?> findById(String id) async {
|
|
return _snapshotsById[id];
|
|
}
|
|
|
|
@override
|
|
Future<List<SchedulingStateSnapshot>> findInWindow(
|
|
SchedulingWindow window,
|
|
) async {
|
|
return List<SchedulingStateSnapshot>.unmodifiable(
|
|
_snapshotsById.values.where(
|
|
(snapshot) => snapshot.window.interval.overlaps(window.interval),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> save(SchedulingStateSnapshot snapshot) async {
|
|
_snapshotsById[snapshot.id] = snapshot;
|
|
}
|
|
}
|