Archiving Streams
Polecat supports archiving event streams to logically remove them from active queries without permanently deleting the data.
Archiving a Stream
session.Events.ArchiveStream(streamId);
await session.SaveChangesAsync();This sets is_archived = 1 on both the pc_streams and pc_events tables for the stream.
Effects of Archiving
When a stream is archived:
FetchStreamAsyncexcludes the archived stream- The async daemon's event loader skips archived events
- Attempting to append to an archived stream throws
ArchivedStreamException
The append refusal is raised at SaveChangesAsync(), when Polecat reads the stream's state, and it names the way back:
Event stream 'a1b2…' is archived and cannot be appended to. Call UnArchiveStream to reopen it, or start a new stream.
Polecat.Exceptions.ArchivedStreamException derives from JasperFx.Events.ArchivedStreamException, so store-agnostic code can catch the shared type. (It replaces InvalidStreamException, which is now obsolete and never thrown.)
Unarchiving a Stream
Restore an archived stream — this is what the refusal above points you at:
session.Events.UnArchiveStream(streamId);
await session.SaveChangesAsync();This sets is_archived = 0 on both tables, making the stream active again.
Tombstoning (Hard Delete)
For permanent removal of a stream and all its events:
session.Events.TombstoneStream(streamId);
await session.SaveChangesAsync();WARNING
Tombstoning permanently DELETEs the stream record and all associated events from the database. This cannot be undone.
Tombstoning works with both Guid and string stream IDs.
Archiving vs Tombstoning
| Operation | Reversible | Data Preserved | Use Case |
|---|---|---|---|
| Archive | Yes | Yes | Soft removal, compliance holds |
| Tombstone | No | No | GDPR right to erasure, cleanup |
Compacting a Stream
Compaction is the third option in that table: it replaces a stream's history with a single Compacted<T> snapshot event and deletes the events it folded. The stream keeps its version, so appending carries on as before, but a fold no longer has to replay everything below the compaction point — Compacted<T> fast-forwards it.
await using var session = store.LightweightSession();
await session.Events.CompactStreamAsync<Freighter>(streamId);
await session.SaveChangesAsync();Like every other session operation, the typed overload only queues the work — the replace, the deletes and the compaction watermark all land on SaveChangesAsync().
WARNING
Compaction DELETEs the events it folds. What survives is the snapshot inside the marker, so an aggregate that cannot be rebuilt from that snapshot alone cannot be rebuilt at all. Use StreamCompactingRequest<T>.Archiver to copy the events somewhere first if you need them.
Compacting without a compile-time type
IEventStore.CompactStreamAsync is the untyped overload. It reads the stream's recorded aggregate type from pc_streams and closes the typed operation over it, which is the only form available to a caller holding a runtime Type — a compaction policy that selects streams by aggregate type, for instance, or a tool with a "compact this stream" button:
var streams = ((IEventStore)store).OpenReadOnlyEventStore().QueryStreamStates();
var overgrown = await streams
.Where(x => x.AggregateType == typeof(Freighter) && x.Version - x.CompactedVersion > 500)
.ToListAsync();
foreach (var state in overgrown)
{
// Resolves Freighter from the stream state, and commits on its own
await ((IEventStore)store).CompactStreamAsync(state.Id, cancellationToken);
}Two differences from the typed overload are worth knowing:
- It opens and commits its own session, so there is no
SaveChangesAsync()to call. - It needs an aggregate type on the stream. A stream started with
StartStream<T>()records one; a stream started without a type has nothing to resolve, and the call is refused rather than silently doing nothing. Name the type explicitly withCompactStreamAsync<T>in that case.
StreamState.CompactedVersion is the watermark the last compaction reached, so Version - CompactedVersion is the stream's un-compacted growth — which is what makes a policy like the one above idempotent instead of re-compacting the same streams on every pass.
Compacting a stream in one tenant's scope
Both untyped overloads take an optional tenant id. The stream-state read and the compaction itself then run on a session opened for that tenant, which is the action-side twin of OpenReadOnlyEventStore(tenantId):
var streams = ((IEventStore)store).OpenReadOnlyEventStore("blue").QueryStreamStates();
var overgrown = await streams
.Where(x => x.Version - x.CompactedVersion > 500)
.ToListAsync();
foreach (var state in overgrown)
{
await ((IEventStore)store).CompactStreamAsync(state.Id, "blue", cancellationToken);
}Pass this overload whenever the selector was tenanted, and the tenant-less one only for a single-tenanted store. A policy that selects a tenant's streams through the tenanted reader and then calls the tenant-less action is reading and writing in two different scopes: on a conjoined store the action looks for those stream ids in the default tenant's partition, where they are not visible and are reported as missing, and on a store whose default tenant is disabled the call is refused outright. A null or empty tenant id means the default tenant, matching the reader.

JasperFx provides formal support for Polecat and other Critter Stack libraries. Please check our