Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix TS mxstream acceptChannelAsync for empty channel names #653

Merged
merged 1 commit into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 6 additions & 11 deletions src/Nerdbank.Streams/MultiplexingStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -559,22 +559,17 @@ public async Task<Channel> OfferChannelAsync(string name, ChannelOptions? option
}
}

/// <summary>
/// Accepts a channel that the remote end has attempted or may attempt to create.
/// </summary>
/// <param name="name">The name of the channel to accept.</param>
/// <param name="cancellationToken">A token to indicate lost interest in accepting the channel.</param>
/// <returns>The <see cref="Channel"/>, after its offer has been received from the remote party and accepted.</returns>
/// <remarks>
/// If multiple offers exist with the specified <paramref name="name"/>, the first one received will be accepted.
/// </remarks>
/// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken"/> is canceled before a request to create the channel has been received.</exception>
/// <inheritdoc cref="AcceptChannelAsync(string, ChannelOptions?, CancellationToken)"/>
public Task<Channel> AcceptChannelAsync(string name, CancellationToken cancellationToken) => this.AcceptChannelAsync(name, options: null, cancellationToken);

/// <summary>
/// Accepts a channel that the remote end has attempted or may attempt to create.
/// </summary>
/// <param name="name">The name of the channel to accept.</param>
/// <param name="name">
/// The name of the channel to accept.
/// An empty string will match an offer made via <see cref="OfferChannelAsync(string, ChannelOptions?, CancellationToken)"/> with an empty channel name.
/// It will also match an anonymous channel offer made with <see cref="CreateChannel(ChannelOptions?)"/>.
/// </param>
/// <param name="options">A set of options that describe local treatment of this channel.</param>
/// <param name="cancellationToken">A token to indicate lost interest in accepting the channel.</param>
/// <returns>The <see cref="Channel"/>, after its offer has been received from the remote party and accepted.</returns>
Expand Down
14 changes: 7 additions & 7 deletions src/nerdbank-streams/src/MultiplexingStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ export abstract class MultiplexingStream implements IDisposableObservable {
/**
* Accepts a channel that the remote end has attempted or may attempt to create.
* @param name The name of the channel to accept.
* An empty string will match an offer made via {@link offerChannelAsync} with an empty channel name.
* It will also match an anonymous channel offer made with {@link createChannel}.
* @param options A set of options that describe local treatment of this channel.
* @param cancellationToken A token to indicate lost interest in accepting the channel.
* Do NOT let this be a long-lived token
Expand Down Expand Up @@ -676,14 +678,12 @@ export class MultiplexingStreamClass extends MultiplexingStream {
}

if (!acceptingChannelAlreadyPresent) {
if (offerParameters.name) {
let offeredChannels: Channel[]
if (!(offeredChannels = this.channelsOfferedByThemByName[offerParameters.name])) {
this.channelsOfferedByThemByName[offerParameters.name] = offeredChannels = []
}

offeredChannels.push(channel)
let offeredChannels: Channel[]
if (!(offeredChannels = this.channelsOfferedByThemByName[offerParameters.name])) {
this.channelsOfferedByThemByName[offerParameters.name] = offeredChannels = []
}

offeredChannels.push(channel)
}

this.setOpenChannel(channel)
Expand Down
23 changes: 23 additions & 0 deletions src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ import { nextTick } from 'process'
await expectThrow(offer.acceptance)
})

describe('An offered anonymous channel is accepted by an empty name', () => {
async function helper(waiting: boolean) {
const ch1 = mx1.createChannel()
if (waiting) {
await waitForEphemeralChannelOfferToPropagate()
}

const ch2 = await mx2.acceptChannelAsync('')
ch1.stream.end()
ch2.stream.end()
}
it('without waiting', () => helper(false))
it('with waiting', () => helper(true))
})

it('Channel offer is canceled by sender', async () => {
const cts = CancellationToken.create()
const offer = mx1.offerChannelAsync('', undefined, cts.token)
Expand Down Expand Up @@ -367,6 +382,14 @@ import { nextTick } from 'process'
rpcChannels[0].stream.end()
rpcChannels[1].stream.end()
})

async function waitForEphemeralChannelOfferToPropagate() {
const channelName = 'EphemeralChannelWaiter'
const [mx1Channel, mx2Channel] = await Promise.all([mx1.offerChannelAsync(channelName), mx2.acceptChannelAsync(channelName)])
mx1Channel.stream.end()
mx2Channel.stream.end()
// await Promise.all([mx1Channel.completion, mx2Channel.completion])
}
})

async function expectThrow<T>(promise: Promise<T>): Promise<any> {
Expand Down
17 changes: 17 additions & 0 deletions test/Nerdbank.Streams.Tests/MultiplexingStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,23 @@ public async Task CreateChannelAsync_AcceptByAnotherId()
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => acceptTask).WithCancellation(this.TimeoutToken);
}

/// <summary>
/// Documents behavior when an anonymous channel is created and an accept of an empty named channel is attempted.
/// </summary>
[Theory, PairwiseData]
public async Task CreateChannel_AcceptChannelAsync(bool waitForPropagation)
{
MultiplexingStream.Channel ch1 = this.mx1.CreateChannel();
if (waitForPropagation)
{
await this.WaitForEphemeralChannelOfferToPropagateAsync();
}

MultiplexingStream.Channel ch2 = await this.mx2.AcceptChannelAsync(string.Empty, this.TimeoutToken);
ch1.Dispose();
ch2.Dispose();
}

[Fact]
public void ChannelExposesMultiplexingStream()
{
Expand Down