Skip to content

Commit 3e8c615

Browse files
committed
chore: document and test empty request stream behavior
1 parent e9b52df commit 3e8c615

4 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/Eventa/EventInvoke.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,16 @@ async Task HandleInvokeAsync(RequestStreamInvocationState<TRequest> state)
343343
}),
344344
context.On(sendStreamEndEvent, envelope =>
345345
{
346+
// Keep empty request streams as a supported C# contract even though they
347+
// materialize state for unknown invokeIds. This is not current TypeScript parity.
346348
var state = GetOrCreateState(envelope.Body.InvokeId);
347349
state.Requests.Complete();
348350
}),
349351
context.On(sendAbortEvent, envelope =>
350352
{
353+
// Keep pre-first-item aborts as a supported C# contract even though they
354+
// can materialize state for unknown invokeIds so the handler still
355+
// observes cancellation. This is not current TypeScript parity.
351356
var state = GetOrCreateState(envelope.Body.InvokeId);
352357
state.Requests.Fault(new OperationCanceledException(state.CancellationSource.Token));
353358
state.CancellationSource.Cancel();

src/Eventa/EventStream.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,16 @@ async Task HandleInvokeAsync(RequestStreamInvocationState<TRequest> state)
246246
}),
247247
context.On(sendStreamEndEvent, envelope =>
248248
{
249+
// Keep empty request streams as a supported C# contract even though
250+
// TypeScript stream.ts ignores sendEventStreamEnd for unknown invokeIds.
249251
var state = GetOrCreateState(envelope.Body.InvokeId);
250252
state.Requests.Complete();
251253
}),
252254
context.On(sendAbortEvent, envelope =>
253255
{
256+
// Keep pre-first-item aborts as a supported C# contract even though they
257+
// can materialize state for unknown invokeIds so the handler still
258+
// observes cancellation. This is not current TypeScript parity.
254259
var state = GetOrCreateState(envelope.Body.InvokeId);
255260
state.Requests.Fault(new OperationCanceledException(state.CancellationSource.Token));
256261
state.CancellationSource.Cancel();

tests/Eventa.Tests/StreamTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,72 @@ public async Task DefineStreamInvoke_SupportsEmptyRequestStreamInput()
401401
Assert.Equal([0], results);
402402
}
403403

404+
[Fact]
405+
public async Task DefineStreamInvokeHandler_CompletesEmptyRequestStreamProtocolMessages()
406+
{
407+
var context = new EventContext();
408+
var definition = new InvokeEventDefinition<int, int>("sum-stream-empty-protocol");
409+
var invokeId = "invoke-empty";
410+
var sendStreamEndEvent = new EventDefinition<StreamEndPayload>(definition.SendStreamEndId);
411+
var receiveEvent = new EventDefinition<ReceivePayload<int>>(definition.ReceiveEventId);
412+
var receiveErrorEvent = new EventDefinition<ReceiveErrorPayload>(definition.ReceiveErrorId);
413+
var receiveStreamEndEvent = new EventDefinition<StreamEndPayload>(definition.ReceiveStreamEndId);
414+
var response = new TaskCompletionSource<ReceivePayload<int>>(TaskCreationOptions.RunContinuationsAsynchronously);
415+
var streamEnded = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
416+
var received = new List<int>();
417+
var handlerStarts = 0;
418+
419+
async IAsyncEnumerable<int> Handler(
420+
IAsyncEnumerable<int> request,
421+
[EnumeratorCancellation] CancellationToken cancellationToken)
422+
{
423+
Interlocked.Increment(ref handlerStarts);
424+
425+
await foreach (var value in request.WithCancellation(cancellationToken))
426+
{
427+
received.Add(value);
428+
yield return value;
429+
}
430+
431+
yield return 0;
432+
}
433+
434+
using var _ = context.On(receiveEvent, envelope =>
435+
{
436+
if (envelope.Body.InvokeId == invokeId)
437+
{
438+
response.TrySetResult(envelope.Body);
439+
}
440+
});
441+
using var __ = context.On(receiveErrorEvent, envelope =>
442+
{
443+
if (envelope.Body.InvokeId == invokeId)
444+
{
445+
response.TrySetException(envelope.Body.Error);
446+
}
447+
});
448+
using var ___ = context.On(receiveStreamEndEvent, envelope =>
449+
{
450+
if (envelope.Body.InvokeId == invokeId)
451+
{
452+
streamEnded.TrySetResult(true);
453+
}
454+
});
455+
using var ____ = EventStream.DefineStreamInvokeHandler(
456+
context,
457+
definition,
458+
Handler);
459+
460+
context.Emit(sendStreamEndEvent, new StreamEndPayload(invokeId));
461+
462+
var result = await response.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
463+
await streamEnded.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
464+
465+
Assert.Equal(1, Volatile.Read(ref handlerStarts));
466+
Assert.Empty(received);
467+
Assert.Equal(new ReceivePayload<int>(invokeId, 0), result);
468+
}
469+
404470
[Fact]
405471
public async Task DefineStreamInvoke_AbortsRequestStreamAndNotifiesHandler()
406472
{

tests/Eventa.Tests/TypeScriptTestInventory.md renamed to tests/Eventa.Tests/docs/typescript-test-inventory.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Status labels:
1212
| Doc section | Status | Current C# target | Covered / deferred details |
1313
| --- | --- | --- | --- |
1414
| `context.spec.ts` | `covered` | `EventContextTests.cs` | Covers register+emit, same handler only once, once listeners, `off(event)`, returned disposer, `off(event, handler)`, and returned disposer for a specific listener. |
15-
| `invoke.spec.ts` | `covered` + `adapted` + `deferred` | `InvokeTests.cs` | Covered: request-response, sync lazy context, request-derived error message, exact error instance propagation, abort/cancel with handler notification, concurrent invokes, same handler once, returned handler removal. Adapted: request-stream input and request-stream abort are currently covered at the protocol/handler layer because C# has no public client invoke overload for request streams. Deferred: async lazy context, undefine handler, batch registration, public client-side request stream invoke parity. |
16-
| `stream.spec.ts` | `covered` | `StreamTests.cs` | Covers server-streaming, `ToStreamHandler`, concurrent streams, error surfacing, abort stream, cancel stream via async enumerator disposal, abort request stream with paced input, request stream input, `ToStreamHandler` + stream input. |
15+
| `invoke.spec.ts` | `covered` + `adapted` + `deferred` | `InvokeTests.cs` | Covered: request-response, sync lazy context, request-derived error message, exact error instance propagation, abort/cancel with handler notification, concurrent invokes, same handler once, returned handler removal. Adapted: request-stream input and request-stream abort are currently covered at the protocol/handler layer because C# has no public client invoke overload for request streams. Extra C# contract within that adapted coverage: empty request streams are accepted by materializing handler state on `sendStreamEndEvent`, and pre-first-item abort still notifies the handler; those behaviors are not current TS parity. Deferred: async lazy context, undefine handler, batch registration, public client-side request stream invoke parity. |
16+
| `stream.spec.ts` | `covered` + `extra` | `StreamTests.cs` | Covers server-streaming, `ToStreamHandler`, concurrent streams, error surfacing, abort stream, cancel stream via async enumerator disposal, abort request stream with paced input, abort request stream before first item, request stream input, `ToStreamHandler` + stream input. Extra C# contract: empty request stream input is intentionally supported even though TS `stream.ts` currently ignores `sendEventStreamEnd` for unknown `invokeId`, and pre-first-item abort still materializes handler state so cancellation is observed; these behaviors are not current TS parity. |
1717
| `invoke-shared.spec.ts` | `covered` | `EventaTests.cs`, `Primitives/EventDefinitionTests.cs`, `Primitives/InvokeEventDefinitionTests.cs` | Adapted to C# by validating tag-derived event IDs and generated uniqueness instead of TS `invokeType` enum objects. |
1818
| `invoke-remote-methods.spec.ts` | `deferred` | none | No corresponding C# public API for remote method stubs. |
1919
| `context-extension-invoke-internal.spec.ts` | `covered` | `InvokeExtensionsTests.cs` | Validates `RegisterAbortEvent` through the public extension surface. |

0 commit comments

Comments
 (0)