/// In-memory implementations of the scheduler persistence repository contracts. /// /// These repositories are intended for core tests, local integration tests, and /// adapter conformance checks. They model optimistic compare-and-set behavior /// but do not provide durable storage. library; import 'dart:convert'; import 'package:scheduler_core/scheduler_core.dart' as core; import 'package:scheduler_persistence/persistence.dart'; const _snapshotRetention = Duration(days: 30); final _epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); /// In-memory task repository with owner scoping and revision checks. final class InMemoryTaskRepository implements TaskRepository { final Map> _records = >{}; @override Future> findById({ required core.OwnerId ownerId, required String taskId, }) async { final record = _records[taskId]; if (record == null || record.ownerId != ownerId) { return const Right(null); } return Right(_cloneTask(record.value, record.ownerId, record.revision)); } @override Future>> findByOwner({ required core.OwnerId ownerId, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _records.values.where((record) => record.ownerId == ownerId), page, (record) => _cloneTask(record.value, record.ownerId, record.revision), )); } @override Future>> findByParentTaskId({ required core.OwnerId ownerId, required String parentTaskId, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _records.values.where( (record) => record.ownerId == ownerId && record.value.parentTaskId == parentTaskId, ), page, (record) => _cloneTask(record.value, record.ownerId, record.revision), )); } @override Future>> findByProjectId({ required core.OwnerId ownerId, required String projectId, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _records.values.where( (record) => record.ownerId == ownerId && record.value.projectId == projectId, ), page, (record) => _cloneTask(record.value, record.ownerId, record.revision), )); } @override Future>> findByStatus({ required core.OwnerId ownerId, required core.TaskStatus status, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _records.values.where( (record) => record.ownerId == ownerId && record.value.status == status, ), page, (record) => _cloneTask(record.value, record.ownerId, record.revision), )); } @override Future> insert({ required core.OwnerId ownerId, required core.Task task, }) async { if (_records.containsKey(task.id)) { return Left(RepositoryDuplicateId(entityId: task.id)); } _records[task.id] = _Record( id: task.id, ownerId: ownerId, value: _cloneTask(task, ownerId, core.Revision.initial), revision: core.Revision.initial, ); return const Right(core.Revision.initial); } @override Future> save({ required core.OwnerId ownerId, required core.Task task, required core.Revision expectedRevision, }) async { final checked = _checkWrite( records: _records, id: task.id, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); _records[task.id] = record.copyWith( value: _cloneTask(task, ownerId, nextRevision), revision: nextRevision, updatedAt: task.updatedAt, ); return Right(nextRevision); } @override Future> delete({ required core.OwnerId ownerId, required String taskId, required core.Revision expectedRevision, }) async { final checked = _checkWrite( records: _records, id: taskId, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final nextRevision = checked.right.revision.next(); _records.remove(taskId); return Right(nextRevision); } } /// In-memory project repository with owner scoping and revision checks. final class InMemoryProjectRepository implements ProjectRepository { final Map> _records = >{}; @override Future> findById({ required core.OwnerId ownerId, required String projectId, }) async { final record = _records[projectId]; if (record == null || record.ownerId != ownerId) { return const Right(null); } return Right(_cloneProject(record)); } @override Future>> findByOwner({ required core.OwnerId ownerId, bool includeArchived = false, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _records.values.where( (record) => record.ownerId == ownerId && (includeArchived || !record.value.isArchived), ), page, _cloneProject, )); } @override Future> insert({ required core.OwnerId ownerId, required core.ProjectProfile project, }) async { if (_records.containsKey(project.id)) { return Left(RepositoryDuplicateId(entityId: project.id)); } _records[project.id] = _Record( id: project.id, ownerId: ownerId, value: _cloneProjectValue( project, ownerId: ownerId, revision: core.Revision.initial, createdAt: _epoch, updatedAt: _epoch, ), revision: core.Revision.initial, createdAt: _epoch, updatedAt: _epoch, ); return const Right(core.Revision.initial); } @override Future> save({ required core.OwnerId ownerId, required core.ProjectProfile project, required core.Revision expectedRevision, }) async { final checked = _checkWrite( records: _records, id: project.id, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); _records[project.id] = record.copyWith( value: _cloneProjectValue( project, ownerId: ownerId, revision: nextRevision, createdAt: record.createdAt, updatedAt: _epoch, ), revision: nextRevision, updatedAt: _epoch, ); return Right(nextRevision); } @override Future> archive({ required core.OwnerId ownerId, required String projectId, required core.Revision expectedRevision, required DateTime archivedAtUtc, }) async { final checked = _checkWrite( records: _records, id: projectId, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); final archived = record.value.copyWith(archivedAt: archivedAtUtc); _records[projectId] = record.copyWith( value: _cloneProjectValue( archived, ownerId: ownerId, revision: nextRevision, createdAt: record.createdAt, updatedAt: _epoch, ), revision: nextRevision, updatedAt: _epoch, ); return Right(nextRevision); } } /// In-memory locked-block repository with owner scoping and revision checks. final class InMemoryLockedBlockRepository implements LockedBlockRepository { final Map> _blocks = >{}; final Map> _overrides = >{}; @override Future> findBlockById({ required core.OwnerId ownerId, required String blockId, }) async { final record = _blocks[blockId]; if (record == null || record.ownerId != ownerId) { return const Right(null); } return Right( _cloneLockedBlock(record.value, record.ownerId, record.revision)); } @override Future>> findBlocksByOwner({ required core.OwnerId ownerId, bool includeArchived = false, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _blocks.values.where( (record) => record.ownerId == ownerId && (includeArchived || !record.value.isArchived), ), page, (record) => _cloneLockedBlock( record.value, record.ownerId, record.revision, ), )); } @override Future> findOverrideById({ required core.OwnerId ownerId, required String overrideId, }) async { final record = _overrides[overrideId]; if (record == null || record.ownerId != ownerId) { return const Right(null); } return Right( _cloneLockedBlockOverride(record.value, record.ownerId, record.revision), ); } @override Future>> findOverridesForDate({ required core.OwnerId ownerId, required core.CivilDate date, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _overrides.values.where( (record) => record.ownerId == ownerId && record.value.date == date, ), page, (record) => _cloneLockedBlockOverride( record.value, record.ownerId, record.revision, ), )); } @override Future> insertBlock({ required core.OwnerId ownerId, required core.LockedBlock block, }) async { if (_blocks.containsKey(block.id)) { return Left(RepositoryDuplicateId(entityId: block.id)); } _blocks[block.id] = _Record( id: block.id, ownerId: ownerId, value: _cloneLockedBlock(block, ownerId, core.Revision.initial), revision: core.Revision.initial, ); return const Right(core.Revision.initial); } @override Future> saveBlock({ required core.OwnerId ownerId, required core.LockedBlock block, required core.Revision expectedRevision, }) async { final checked = _checkWrite( records: _blocks, id: block.id, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); _blocks[block.id] = record.copyWith( value: _cloneLockedBlock(block, ownerId, nextRevision), revision: nextRevision, updatedAt: block.updatedAt, ); return Right(nextRevision); } @override Future> archiveBlock({ required core.OwnerId ownerId, required String blockId, required core.Revision expectedRevision, required DateTime archivedAtUtc, }) async { final checked = _checkWrite( records: _blocks, id: blockId, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); final archived = record.value.copyWith(archivedAt: archivedAtUtc); _blocks[blockId] = record.copyWith( value: _cloneLockedBlock(archived, ownerId, nextRevision), revision: nextRevision, updatedAt: archived.updatedAt, ); return Right(nextRevision); } @override Future> insertOverride({ required core.OwnerId ownerId, required core.LockedBlockOverride override, }) async { if (_overrides.containsKey(override.id)) { return Left(RepositoryDuplicateId(entityId: override.id)); } _overrides[override.id] = _Record( id: override.id, ownerId: ownerId, value: _cloneLockedBlockOverride( override, ownerId, core.Revision.initial, ), revision: core.Revision.initial, ); return const Right(core.Revision.initial); } @override Future> saveOverride({ required core.OwnerId ownerId, required core.LockedBlockOverride override, required core.Revision expectedRevision, }) async { final checked = _checkWrite( records: _overrides, id: override.id, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); _overrides[override.id] = record.copyWith( value: _cloneLockedBlockOverride(override, ownerId, nextRevision), revision: nextRevision, updatedAt: override.updatedAt, ); return Right(nextRevision); } } /// In-memory owner settings repository with owner scoping and revision checks. final class InMemorySettingsRepository implements SettingsRepository { final Map> _records = >{}; @override Future> findByOwner({ required core.OwnerId ownerId, }) async { final record = _records[ownerId.value]; if (record == null || record.ownerId != ownerId) { return const Right(null); } return Right(_cloneSettings(record)); } @override Future> insert({ required core.OwnerId ownerId, required core.OwnerSettings settings, }) async { if (_records.containsKey(ownerId.value)) { return Left(RepositoryDuplicateId(entityId: ownerId.value)); } final ownedSettings = settings.ownerId == ownerId.value ? settings : settings.copyWith(ownerId: ownerId.value); _records[ownerId.value] = _Record( id: ownerId.value, ownerId: ownerId, value: _cloneSettingsValue( ownedSettings, revision: core.Revision.initial, createdAt: _epoch, updatedAt: _epoch, ), revision: core.Revision.initial, createdAt: _epoch, updatedAt: _epoch, ); return const Right(core.Revision.initial); } @override Future> save({ required core.OwnerId ownerId, required core.OwnerSettings settings, required core.Revision expectedRevision, }) async { final checked = _checkWrite( records: _records, id: ownerId.value, ownerId: ownerId, expectedRevision: expectedRevision, ); if (checked is Left>) { return Left(checked.value); } final record = checked.right; final nextRevision = record.revision.next(); final ownedSettings = settings.ownerId == ownerId.value ? settings : settings.copyWith(ownerId: ownerId.value); _records[ownerId.value] = record.copyWith( value: _cloneSettingsValue( ownedSettings, revision: nextRevision, createdAt: record.createdAt, updatedAt: _epoch, ), revision: nextRevision, updatedAt: _epoch, ); return Right(nextRevision); } } /// In-memory diagnostic schedule snapshot repository. final class InMemoryScheduleSnapshotRepository implements ScheduleSnapshotRepository { final Map _records = {}; @override Future> findById({ required core.OwnerId ownerId, required String snapshotId, }) async { final record = _records[snapshotId]; if (record == null || record.ownerId != ownerId) { return const Right(null); } return Right(_cloneSnapshot(record.value, record.ownerId, record.revision)); } @override Future>> findInWindow({ required core.OwnerId ownerId, required core.SchedulingWindow window, core.PageRequest page = const core.PageRequest(), }) async { return Right(_pageRecords( _records.values.where( (record) => record.ownerId == ownerId && _windowsOverlap(record.value.window, window), ), page, (record) => _cloneSnapshot(record.value, record.ownerId, record.revision), )); } @override Future> insert({ required core.OwnerId ownerId, required core.SchedulingStateSnapshot snapshot, }) async { if (_records.containsKey(snapshot.id)) { return Left(RepositoryDuplicateId(entityId: snapshot.id)); } _records[snapshot.id] = _SnapshotRecord( id: snapshot.id, ownerId: ownerId, value: _cloneSnapshot(snapshot, ownerId, core.Revision.initial), revision: core.Revision.initial, retentionExpiresAt: snapshot.capturedAt.add(_snapshotRetention), ); return const Right(core.Revision.initial); } @override Future> deleteExpired({ required core.OwnerId ownerId, required DateTime nowUtc, }) async { final expiredIds = _records.values .where( (record) => record.ownerId == ownerId && record.retentionExpiresAt.isBefore(nowUtc), ) .map((record) => record.id) .toList(growable: false); for (final id in expiredIds) { _records.remove(id); } return Right(expiredIds.length); } } Either> _checkWrite({ required Map> records, required String id, required core.OwnerId ownerId, required core.Revision expectedRevision, }) { if (expectedRevision.value <= 0) { return Left( RepositoryInvalidRevision( entityId: id, expectedRevision: expectedRevision, ), ); } final record = records[id]; if (record == null) { return Left(RepositoryNotFound(entityId: id)); } if (record.ownerId != ownerId) { return Left(RepositoryOwnerMismatch(entityId: id)); } if (record.revision != expectedRevision) { return Left( RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: record.revision, ), ); } return Right(record); } core.Page _pageRecords( Iterable records, core.PageRequest page, R Function(T record) clone, ) { final ordered = records.toList(growable: false) ..sort((left, right) => left.id.compareTo(right.id)); final start = _cursorOffset(page.cursor); final end = (start + page.limit).clamp(0, ordered.length); final items = ordered.sublist(start, end).map(clone).toList(growable: false); final nextCursor = end < ordered.length ? end.toString() : null; return core.Page(items: items, nextCursor: nextCursor); } int _cursorOffset(String? cursor) { if (cursor == null) { return 0; } final parsed = int.tryParse(cursor); if (parsed == null || parsed < 0) { return 0; } return parsed; } bool _windowsOverlap(core.SchedulingWindow left, core.SchedulingWindow right) { return left.start.isBefore(right.end) && right.start.isBefore(left.end); } core.Task _cloneTask( core.Task task, core.OwnerId ownerId, core.Revision revision, ) { return core.TaskDocumentMapping.fromDocument(_roundTrip( core.TaskDocumentMapping.toDocument( task, ownerId: ownerId.value, revision: revision.value, ), )); } core.ProjectProfile _cloneProject(_Record record) { return _cloneProjectValue( record.value, ownerId: record.ownerId, revision: record.revision, createdAt: record.createdAt, updatedAt: record.updatedAt, ); } core.ProjectProfile _cloneProjectValue( core.ProjectProfile project, { required core.OwnerId ownerId, required core.Revision revision, required DateTime createdAt, required DateTime updatedAt, }) { return core.ProjectDocumentMapping.fromDocument(_roundTrip( core.ProjectDocumentMapping.toDocument( project, ownerId: ownerId.value, revision: revision.value, createdAt: createdAt, updatedAt: updatedAt, ), )); } core.LockedBlock _cloneLockedBlock( core.LockedBlock block, core.OwnerId ownerId, core.Revision revision, ) { return core.LockedBlockDocumentMapping.fromDocument(_roundTrip( core.LockedBlockDocumentMapping.toDocument( block, ownerId: ownerId.value, revision: revision.value, ), )); } core.LockedBlockOverride _cloneLockedBlockOverride( core.LockedBlockOverride override, core.OwnerId ownerId, core.Revision revision, ) { return core.LockedBlockOverrideDocumentMapping.fromDocument(_roundTrip( core.LockedBlockOverrideDocumentMapping.toDocument( override, ownerId: ownerId.value, revision: revision.value, ), )); } core.OwnerSettings _cloneSettings(_Record record) { return _cloneSettingsValue( record.value, revision: record.revision, createdAt: record.createdAt, updatedAt: record.updatedAt, ); } core.OwnerSettings _cloneSettingsValue( core.OwnerSettings settings, { required core.Revision revision, required DateTime createdAt, required DateTime updatedAt, }) { return core.OwnerSettingsDocumentMapping.fromDocument(_roundTrip( core.OwnerSettingsDocumentMapping.toDocument( settings, revision: revision.value, createdAt: createdAt, updatedAt: updatedAt, ), )); } core.SchedulingStateSnapshot _cloneSnapshot( core.SchedulingStateSnapshot snapshot, core.OwnerId ownerId, core.Revision revision, ) { return core.SchedulingSnapshotDocumentMapping.fromDocument(_roundTrip( core.SchedulingSnapshotDocumentMapping.toDocument( snapshot, ownerId: ownerId.value, revision: revision.value, retentionExpiresAt: snapshot.capturedAt.add(_snapshotRetention), ), )); } Map _roundTrip(Map document) { return Map.from( jsonDecode(jsonEncode(document)) as Map, ); } abstract interface class _IdentifiedRecord { String get id; } final class _Record implements _IdentifiedRecord { _Record({ required this.id, required this.ownerId, required this.value, required this.revision, DateTime? createdAt, DateTime? updatedAt, }) : createdAt = createdAt ?? _epoch, updatedAt = updatedAt ?? _epoch; @override final String id; final core.OwnerId ownerId; final T value; final core.Revision revision; final DateTime createdAt; final DateTime updatedAt; _Record copyWith({ T? value, core.Revision? revision, DateTime? updatedAt, }) { return _Record( id: id, ownerId: ownerId, value: value ?? this.value, revision: revision ?? this.revision, createdAt: createdAt, updatedAt: updatedAt ?? this.updatedAt, ); } } final class _SnapshotRecord implements _IdentifiedRecord { _SnapshotRecord({ required this.id, required this.ownerId, required this.value, required this.revision, required this.retentionExpiresAt, }); @override final String id; final core.OwnerId ownerId; final core.SchedulingStateSnapshot value; final core.Revision revision; final DateTime retentionExpiresAt; }