|
2 | 2 | // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). |
3 | 3 |
|
4 | 4 | // ReSharper disable CheckNamespace |
| 5 | +// ReSharper disable ArrangeTypeModifiers |
| 6 | +// ReSharper disable ArrangeTypeMemberModifiers |
5 | 7 |
|
6 | 8 | #nullable enable |
7 | 9 |
|
|
19 | 21 |
|
20 | 22 | namespace KurrentDB.Core.ClientPublisher; |
21 | 23 |
|
| 24 | +// TODO: We can remove usages of WriteEvents later |
22 | 25 | using WriteEventsResult = (Position Position, StreamRevision StreamRevision); |
| 26 | +using MultiStreamWriteResult = (Position Position, StreamRevision[] StreamRevisions); |
23 | 27 |
|
24 | 28 | [PublicAPI] |
25 | 29 | public static class PublisherWriteExtensions { |
@@ -53,6 +57,40 @@ public static async Task<WriteEventsResult> WriteEvents( |
53 | 57 |
|
54 | 58 | return await operation.WaitForReply; |
55 | 59 | } |
| 60 | + |
| 61 | + public static async Task<MultiStreamWriteResult> WriteEventsToMultipleStreams( |
| 62 | + this IPublisher publisher, |
| 63 | + string[] eventStreamIds, |
| 64 | + long[] expectedVersions, |
| 65 | + Event[] events, |
| 66 | + int[] eventStreamIndexes, |
| 67 | + CancellationToken cancellationToken = default |
| 68 | + ) { |
| 69 | + var cid = Guid.NewGuid(); |
| 70 | + |
| 71 | + var operation = new MultiStreamWriteEventsOperation(eventStreamIds, expectedVersions); |
| 72 | + |
| 73 | + try { |
| 74 | + var command = new ClientMessage.WriteEvents( |
| 75 | + internalCorrId: cid, |
| 76 | + correlationId: cid, |
| 77 | + envelope: operation, |
| 78 | + requireLeader: false, |
| 79 | + eventStreamIds: eventStreamIds, |
| 80 | + expectedVersions: expectedVersions, |
| 81 | + events: events, |
| 82 | + eventStreamIndexes: eventStreamIndexes, |
| 83 | + user: SystemAccounts.System, |
| 84 | + cancellationToken: cancellationToken |
| 85 | + ); |
| 86 | + |
| 87 | + publisher.Publish(command); |
| 88 | + } catch (Exception ex) { |
| 89 | + throw new($"{nameof(WriteEventsToMultipleStreams)}: Unable to execute request!", ex); |
| 90 | + } |
| 91 | + |
| 92 | + return await operation.WaitForReply; |
| 93 | + } |
56 | 94 | } |
57 | 95 |
|
58 | 96 | class WriteEventsOperation(string stream, long expectedRevision) : IEnvelope { |
@@ -94,3 +132,57 @@ static ReadResponseException MapToError(Message message, string stream, long exp |
94 | 132 |
|
95 | 133 | public Task<WriteEventsResult> WaitForReply => Operation.Task; |
96 | 134 | } |
| 135 | + |
| 136 | +class MultiStreamWriteEventsOperation(string[] streams, long[] expectedVersions) : IEnvelope { |
| 137 | + TaskCompletionSource<MultiStreamWriteResult> Operation { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); |
| 138 | + |
| 139 | + public void ReplyWith<T>(T message) where T : Message { |
| 140 | + if (message is ClientMessage.WriteEventsCompleted { Result: OperationResult.Success } success) |
| 141 | + Operation.TrySetResult(MapToResult(success)); |
| 142 | + else |
| 143 | + Operation.TrySetException(MapToError(message, streams, expectedVersions)); |
| 144 | + |
| 145 | + return; |
| 146 | + |
| 147 | + static MultiStreamWriteResult MapToResult(ClientMessage.WriteEventsCompleted completed) { |
| 148 | + Debug.Assert(completed.CommitPosition >= 0); |
| 149 | + Debug.Assert(completed.PreparePosition >= 0); |
| 150 | + var position = Position.FromInt64(completed.CommitPosition, completed.PreparePosition); |
| 151 | + var streamRevisions = new StreamRevision[completed.LastEventNumbers.Length]; |
| 152 | + for (int i = 0; i < completed.LastEventNumbers.Length; i++) { |
| 153 | + streamRevisions[i] = StreamRevision.FromInt64(completed.LastEventNumbers.Span[i]); |
| 154 | + } |
| 155 | + return new MultiStreamWriteResult(position, streamRevisions); |
| 156 | + } |
| 157 | + |
| 158 | + static ReadResponseException MapToError(Message message, string[] streams, long[] expectedVersions) { |
| 159 | + return message switch { |
| 160 | + ClientMessage.WriteEventsCompleted completed => completed.Result switch { |
| 161 | + OperationResult.PrepareTimeout => new ReadResponseException.Timeout($"{completed.Result}"), |
| 162 | + OperationResult.CommitTimeout => new ReadResponseException.Timeout($"{completed.Result}"), |
| 163 | + OperationResult.ForwardTimeout => new ReadResponseException.Timeout($"{completed.Result}"), |
| 164 | + OperationResult.AccessDenied => new ReadResponseException.AccessDenied(), |
| 165 | + OperationResult.StreamDeleted => MapStreamDeletedError(completed, streams), |
| 166 | + OperationResult.WrongExpectedVersion => MapWrongExpectedVersionError(completed, streams, expectedVersions), |
| 167 | + _ => ReadResponseException.UnknownError.Create(completed.Result) |
| 168 | + }, |
| 169 | + ClientMessage.NotHandled notHandled => notHandled.MapToException(), |
| 170 | + not null => new ReadResponseException.UnknownMessage(message.GetType(), typeof(ClientMessage.WriteEventsCompleted)), |
| 171 | + _ => throw new ArgumentOutOfRangeException(nameof(message), message, null) |
| 172 | + }; |
| 173 | + |
| 174 | + static ReadResponseException MapStreamDeletedError(ClientMessage.WriteEventsCompleted completed, string[] streams) { |
| 175 | + var streamIndex = completed.FailureStreamIndexes.Span[0]; |
| 176 | + return new ReadResponseException.StreamDeleted(streams[streamIndex]); |
| 177 | + } |
| 178 | + |
| 179 | + static ReadResponseException MapWrongExpectedVersionError(ClientMessage.WriteEventsCompleted completed, string[] streams, long[] expectedVersions) { |
| 180 | + var streamIndex = completed.FailureStreamIndexes.Span[0]; |
| 181 | + var currentVersion = completed.FailureCurrentVersions.Span[0]; |
| 182 | + return new ReadResponseException.WrongExpectedRevision(streams[streamIndex], expectedVersions[streamIndex], currentVersion); |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + public Task<MultiStreamWriteResult> WaitForReply => Operation.Task; |
| 188 | +} |
0 commit comments