Skip to content

Commit 90cdf38

Browse files
committed
Merge branch 'release/v5.3.0' into main
2 parents 01277d2 + 990d534 commit 90cdf38

39 files changed

Lines changed: 730 additions & 81 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [5.3.0] - 2026-02-21
11+
12+
### Added
13+
14+
- Added `deleteAsync(StreamId)` to `Session` for marking aggregates for deletion within a unit of work. No prior load is required — the stream ID alone is sufficient.
15+
- Added `deleteAsync(StreamId)` to `TargetPersistenceAdapter` for physical entity deletion in state-based persistence.
16+
- Added `softDeleteStreamAsync(StreamId)` to `EventStore` for tombstone-based stream deletion in event sourcing.
17+
- `StateBasedSession.saveChangesAsync` now processes deletion-marked entities by calling `adapter.deleteAsync` and removing them from the identity map.
18+
- Event sourcing `SessionImpl.saveChangesAsync` now soft-deletes marked streams via `EventStore.softDeleteStreamAsync`, writing a tombstone flag instead of rejecting deletion.
19+
- `InMemoryEventStore`, `HiveEventStore`, and `SembastEventStore` implement `softDeleteStreamAsync` — soft-deleted streams are excluded from `loadStreamAsync` and `getStreamIdsByAggregateTypeAsync` but their events are retained for auditability.
20+
- `InMemoryPersistenceAdapter`, `HivePersistenceAdapter`, and `SembastPersistenceAdapter` now implement `deleteAsync`.
21+
1022

1123
## [5.2.0] - 2026-02-20
1224

packages/continuum/example/lib/continuum.g.dart

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/continuum/example/lib/store_state_based.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ class FakeUserApiAdapter implements TargetPersistenceAdapter<User> {
7777
'(${pendingOperations.length} event(s)) → 200 OK',
7878
);
7979
}
80+
81+
@override
82+
Future<void> deleteAsync(StreamId streamId) async {
83+
// Simulate network latency.
84+
await Future<void>.delayed(const Duration(milliseconds: 50));
85+
86+
_backendDb.remove(streamId.value);
87+
print(' [Backend] DELETE /users/${streamId.value} → 200 OK');
88+
}
8089
}
8190

8291
/// Simple record representing server-side user state.

packages/continuum/example/lib/store_state_based_local_db.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ final class UserLocalDbAdapter implements TargetPersistenceAdapter<User> {
135135
'deactivatedAt': target.deactivatedAt?.toIso8601String(),
136136
});
137137
}
138+
139+
@override
140+
Future<void> deleteAsync(StreamId streamId) async {
141+
// Remove the target from the local database.
142+
await _db.delete(streamId.value);
143+
}
138144
}
139145

140146
// ── Example ─────────────────────────────────────────────────────────────────

packages/continuum/example/lib/store_state_based_transactional.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ final class FakeBackendApi {
9191

9292
print(' [Backend] PATCH /users/$id → 200 OK');
9393
}
94+
95+
/// Simulates `DELETE /users/:id` — removes a user from the backend.
96+
Future<void> deleteUserAsync(String id) async {
97+
await Future<void>.delayed(const Duration(milliseconds: 30));
98+
99+
_database.remove(id);
100+
print(' [Backend] DELETE /users/$id → 200 OK');
101+
}
94102
}
95103

96104
/// Simple record representing server-side user state.
@@ -183,6 +191,12 @@ final class UserApiAdapter implements TargetPersistenceAdapter<User> {
183191
}
184192
}
185193
}
194+
195+
@override
196+
Future<void> deleteAsync(StreamId streamId) async {
197+
// Delete the user from the backend.
198+
await _api.deleteUserAsync(streamId.value);
199+
}
186200
}
187201

188202
// ── Example ─────────────────────────────────────────────────────────────────

packages/continuum/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: continuum
22
description: An event sourcing library for Dart with code generation support.
3-
version: 5.2.0
3+
version: 5.3.0
44
repository: https://github.com/zooper/continuum
55
homepage: https://zooper.dev
66

packages/continuum_event_sourcing/lib/src/persistence/event_store.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,20 @@ abstract interface class EventStore {
4242
/// passed to [appendEventsAsync] or [AtomicEventStore.appendEventsToStreamsAsync]
4343
/// when events were first persisted for a stream.
4444
///
45-
/// Returns an empty list if no streams match.
45+
/// Returns an empty list if no streams match. Streams that have been
46+
/// soft-deleted via [softDeleteStreamAsync] are excluded.
4647
Future<List<StreamId>> getStreamIdsByAggregateTypeAsync(
4748
String aggregateType,
4849
);
50+
51+
/// Marks a stream as deleted using a tombstone flag in stream
52+
/// metadata.
53+
///
54+
/// The events remain in the store but the stream is treated as
55+
/// non-existent: [loadStreamAsync] returns an empty list and
56+
/// [getStreamIdsByAggregateTypeAsync] excludes it.
57+
///
58+
/// Must be idempotent — soft-deleting a non-existent or already
59+
/// deleted stream is a no-op.
60+
Future<void> softDeleteStreamAsync(StreamId streamId);
4961
}

packages/continuum_event_sourcing/lib/src/persistence/session_impl.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ final class SessionImpl extends SessionBase {
267267
for (var attempt = 0; attempt <= maxRetries; attempt++) {
268268
try {
269269
final batch = await _persistPendingEventsAsync();
270+
271+
// Soft-delete streams marked for deletion via tombstone metadata.
272+
// This happens after successful event persistence so pending
273+
// operations on the same stream are not lost.
274+
await _softDeleteMarkedStreamsAsync();
275+
270276
return batch;
271277
} on ConcurrencyException {
272278
final isLastAttempt = attempt == maxRetries;
@@ -281,6 +287,20 @@ final class SessionImpl extends SessionBase {
281287
return CommitBatch.empty;
282288
}
283289

290+
/// Soft-deletes all streams in [streamsMarkedForDeletion] via
291+
/// [EventStore.softDeleteStreamAsync] and removes them from the
292+
/// identity map.
293+
Future<void> _softDeleteMarkedStreamsAsync() async {
294+
// Copy to avoid concurrent modification during iteration.
295+
final streamIds = Set<StreamId>.of(streamsMarkedForDeletion);
296+
297+
for (final streamId in streamIds) {
298+
await _eventStore.softDeleteStreamAsync(streamId);
299+
trackedEntities.remove(streamId);
300+
streamsMarkedForDeletion.remove(streamId);
301+
}
302+
}
303+
284304
/// Core persistence logic extracted so [saveChangesAsync] can retry it.
285305
Future<CommitBatch> _persistPendingEventsAsync() async {
286306
final pendingEntries = <MapEntry<StreamId, TrackedEntity>>[];

packages/continuum_event_sourcing/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: continuum_event_sourcing
22
description: Event sourcing persistence strategy for Continuum — event store, serialization, projections, and aggregate replay.
3-
version: 5.2.0
3+
version: 5.3.0
44
repository: https://github.com/zooper/continuum
55
homepage: https://zooper.dev
66

@@ -17,7 +17,7 @@ dependencies:
1717

1818
dev_dependencies:
1919
build_runner: ^2.4.0
20-
continuum_store_memory: ^5.2.0
20+
continuum_store_memory: ^5.0.0
2121
lints: ^6.0.0
2222
mockito: ^5.4.0
2323
test: ^1.25.6

packages/continuum_event_sourcing/test/persistence/event_application_mode_test.mocks.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,13 @@ class MockEventStore extends _i1.Mock implements _i2.EventStore {
7878
),
7979
)
8080
as _i3.Future<List<_i5.StreamId>>);
81+
82+
@override
83+
_i3.Future<void> softDeleteStreamAsync(_i5.StreamId? streamId) =>
84+
(super.noSuchMethod(
85+
Invocation.method(#softDeleteStreamAsync, [streamId]),
86+
returnValue: _i3.Future<void>.value(),
87+
returnValueForMissingStub: _i3.Future<void>.value(),
88+
)
89+
as _i3.Future<void>);
8190
}

0 commit comments

Comments
 (0)