Closing Streams
Event streams can be permanently closed to prevent further appends. Once a stream is closed, any attempt to append events to it will result in a constraint violation of type StreamClosed.
Why close a stream?
Section titled “Why close a stream?”Closing a stream is useful when a logical unit of work is complete and no further events should be recorded for that stream. Examples include:
- Finalizing an invoice — once issued, no further line items should be added.
- Archiving a case — the case is resolved and the event history is sealed.
- Completing an order — the order lifecycle has ended and further mutations are disallowed.
How to close a stream
Section titled “How to close a stream”Call CompleteStream on the event log with the stream type and stream identifier you want to close:
using Cratis.Chronicle.EventSequences;
public class ClosingStreamsInvoiceCloser(IEventLog eventLog){ public async Task CloseInvoiceStream(EventStreamId invoiceStreamId) { var result = await eventLog.CompleteStream(new EventStreamType("invoices"), invoiceStreamId);
result.Switch( sequenceNumber => Console.WriteLine($"Stream closed at sequence number {sequenceNumber}"), error => Console.WriteLine($"Failed to close stream: {error}")); }}import { IEventLog } from '@cratis/chronicle';
async function closeInvoiceStream(eventLog: IEventLog, invoiceStreamId: string): Promise<void> { const result = await eventLog.completeStream('invoices', invoiceStreamId);
if (result.isSuccess) { console.log(`Stream closed at sequence number ${result.sequenceNumber.value}`); } else { console.log(`Failed to close stream: ${result.error}`); }}The method returns Result<EventSequenceNumber, CompleteStreamError>.
Error cases
Section titled “Error cases”| Error | Meaning |
|---|---|
AlreadyCompleted | The stream has already been closed. |
DefaultStreamCannotBeCompleted | The default stream (EventStreamType.All / EventStreamId.Default) cannot be closed. |
What happens after closing
Section titled “What happens after closing”After a stream is closed, any append targeting that stream is rejected with a StreamClosed constraint violation:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;using Cratis.Chronicle.EventSequences;using System.Linq;
[EventType]public record ClosingStreamsInvoiceLineAdded(string Description, decimal Amount);
public class ClosingStreamsInvoiceLineAppender(IEventLog eventLog){ public async Task<bool> TryAppendLine(EventSourceId invoiceId) { var appendResult = await eventLog.Append( invoiceId, new ClosingStreamsInvoiceLineAdded("Consulting", 500m), new EventStreamType("invoices"), new EventStreamId("invoice-42"));
if (!appendResult.IsSuccess) { var violation = appendResult.ConstraintViolations .FirstOrDefault(v => v.ConstraintType == ConstraintType.StreamClosed); return violation is null; }
return true; }}import { eventType, IEventLog } from '@cratis/chronicle';
@eventType()class ClosingStreamsInvoiceLineAdded { constructor(readonly description: string = '', readonly amount: number = 0) {}}
async function tryAppendLine(eventLog: IEventLog, invoiceId: string): Promise<boolean> { const [appendResult] = await eventLog.appendMany([{ eventSourceId: invoiceId, event: new ClosingStreamsInvoiceLineAdded('Consulting', 500), eventStreamType: 'invoices', eventStreamId: 'invoice-42' }]);
if (!appendResult.isSuccess) { const wasStreamClosed = appendResult.constraintViolations.some(violation => violation.constraintId === 'StreamClosed'); return !wasStreamClosed; }
return true;}The rejection is enforced by the ClosedStreamConstraintValidator which is automatically active for every event sequence — no additional configuration is required.